Como buscar um valor específico em todas as tabelas (PostgreSQL)?

111

É possível pesquisar cada coluna de cada tabela por um valor específico no PostgreSQL?

Uma pergunta semelhante está disponível aqui para a Oracle.

Sandro Munda
fonte
Você está procurando uma ferramenta ou uma implementação dos procedimentos mostrados na pergunta vinculada?
a_horse_with_no_name
Não, apenas a maneira mais simples de encontrar um valor específico em todos os campos / tabelas.
Sandro Munda
Então você não quer usar uma ferramenta externa?
a_horse_with_no_name
1
Se for a maneira mais simples => ok para uma ferramenta externa :-)
Sandro Munda

Respostas:

131

Que tal despejar o conteúdo do banco de dados e depois usar grep?

$ pg_dump --data-only --inserts -U postgres your-db-name > a.tmp
$ grep United a.tmp
INSERT INTO countries VALUES ('US', 'United States');
INSERT INTO countries VALUES ('GB', 'United Kingdom');

O mesmo utilitário, pg_dump, pode incluir nomes de colunas na saída. Basta mudar --insertspara --column-inserts. Dessa forma, você também pode pesquisar nomes de coluna específicos. Mas se eu estivesse procurando nomes de colunas, provavelmente descartaria o esquema em vez dos dados.

$ pg_dump --data-only --column-inserts -U postgres your-db-name > a.tmp
$ grep country_code a.tmp
INSERT INTO countries (iso_country_code, iso_country_name) VALUES ('US', 'United  States');
INSERT INTO countries (iso_country_code, iso_country_name) VALUES ('GB', 'United Kingdom');
Mike Sherrill 'Cat Recall'
fonte
5
+1 grátis e simples. E se você quiser estrutura, o pg_dump pode fazer isso também. Além disso, se grep não for sua praia, use qualquer ferramenta de busca de conteúdo de arquivo que você quiser nas estruturas e / ou dados despejados.
Kuberchaun
Se você deseja realizar grep em dados de texto (que normalmente são codificados em versões mais recentes do postgres), pode ser necessário ALTER DATABASE your_db_name SET bytea_output = 'escape';no banco de dados (ou uma cópia dele) antes de despejá-lo. (Não estou vendo uma maneira de especificar isso apenas para um pg_dumpcomando.)
phils
você pode explicar em detalhes ..? Como pesquisar a string 'ABC' em todas as tabelas?
Sr. Bhosale
1
Se você estiver usando o IntelliJ, basta clicar com o botão direito em seu banco de dados e selecionar "Despejar com 'pg_dump'" ou "Despejar dados para arquivo (s)"
Laurens
3
Como isso é uma solução válida para qualquer banco de dados que seja grande o suficiente para que você não possa despejá-lo em seu disco?
Govind Parmar
76

Aqui está uma função pl / pgsql que localiza registros onde qualquer coluna contém um valor específico. Leva como argumentos o valor a ser pesquisado em formato de texto, uma matriz de nomes de tabela a ser pesquisada (o padrão é todas as tabelas) e uma matriz de nomes de esquema (o padrão é todos os nomes de esquema).

Ele retorna uma estrutura de tabela com esquema, nome da tabela, nome da coluna e pseudocoluna ctid(localização física não durável da linha na tabela, consulte Colunas do sistema )

CREATE OR REPLACE FUNCTION search_columns(
    needle text,
    haystack_tables name[] default '{}',
    haystack_schema name[] default '{}'
)
RETURNS table(schemaname text, tablename text, columnname text, rowctid text)
AS $$
begin
  FOR schemaname,tablename,columnname IN
      SELECT c.table_schema,c.table_name,c.column_name
      FROM information_schema.columns c
        JOIN information_schema.tables t ON
          (t.table_name=c.table_name AND t.table_schema=c.table_schema)
        JOIN information_schema.table_privileges p ON
          (t.table_name=p.table_name AND t.table_schema=p.table_schema
              AND p.privilege_type='SELECT')
        JOIN information_schema.schemata s ON
          (s.schema_name=t.table_schema)
      WHERE (c.table_name=ANY(haystack_tables) OR haystack_tables='{}')
        AND (c.table_schema=ANY(haystack_schema) OR haystack_schema='{}')
        AND t.table_type='BASE TABLE'
  LOOP
    FOR rowctid IN
      EXECUTE format('SELECT ctid FROM %I.%I WHERE cast(%I as text)=%L',
       schemaname,
       tablename,
       columnname,
       needle
      )
    LOOP
      -- uncomment next line to get some progress report
      -- RAISE NOTICE 'hit in %.%', schemaname, tablename;
      RETURN NEXT;
    END LOOP;
 END LOOP;
END;
$$ language plpgsql;

Veja também a versão no github baseada no mesmo princípio, mas adicionando algumas melhorias de velocidade e relatórios.

Exemplos de uso em um banco de dados de teste:

  • Pesquisar em todas as tabelas dentro do esquema público:
select * from search_columns ('foobar');
 schemaname | tablename | columnname | rowctid
------------ + ----------- + ------------ + ---------
 público | s3 | nome de usuário | (0,11)
 público | s2 | relname | (7,29)
 público | w | corpo | (0,2)
(3 linhas)
  • Pesquise em uma tabela específica:
 select * from search_columns ('foobar', '{w}');
 schemaname | tablename | columnname | rowctid
------------ + ----------- + ------------ + ---------
 público | w | corpo | (0,2)
(1 linha)
  • Pesquise em um subconjunto de tabelas obtidas a partir de uma seleção:
select * from search_columns ('foobar', array (select table_name :: name from information_schema.tables onde table_name like 's%'), array ['public']);
 schemaname | tablename | columnname | rowctid
------------ + ----------- + ------------ + ---------
 público | s2 | relname | (7,29)
 público | s3 | nome de usuário | (0,11)
(2 linhas)
  • Obtenha uma linha de resultado com a tabela base correspondente ee ctid:
selecione * de public.w onde ctid = '(0,2)';
 título | corpo | tsv         
------- + -------- + ---------------------
 toto | foobar | 'foobar': 2 'toto': 1

Variantes

  • Para testar em uma expressão regular em vez de igualdade estrita, como grep, esta parte da consulta:

    SELECT ctid FROM %I.%I WHERE cast(%I as text)=%L

    pode ser alterado para:

    SELECT ctid FROM %I.%I WHERE cast(%I as text) ~ %L

  • Para comparações que não diferenciam maiúsculas de minúsculas, você pode escrever:

    SELECT ctid FROM %I.%I WHERE lower(cast(%I as text)) = lower(%L)

Daniel Vérité
fonte
ERROR: erro de sintaxe em ou próximo ao "default" LINE 3: haystack_tables name [] default '{}' (Usando PostgreSQL 8.2.17 e não é possível atualizar)
Henno
@Henno: sim, requer PG-9.1. Editado agora para tornar isso explícito. Para usá-lo com versões mais antigas, você terá que adaptá-lo.
Daniel Vérité
1
@Rajendra_Prasad: o operador de expressão regular tem uma variante que não diferencia maiúsculas de minúsculas: ~*mais adequado do que lower (). Mas de qualquer maneira, o t.*não faz parte da resposta acima. Pesquisar coluna por coluna não é o mesmo que pesquisar a linha como um valor por causa dos separadores de coluna.
Daniel Vérité
2
Isso retorna apenas uma linha por coluna da tabela de esquema.
theGtknerd
1
Muito obrigado. Esta solução funciona perfeitamente para mim. Tive que localizar uma tabela em uma lista de mais de 1000 tabelas que contém um url específico. Você salvou meu dia !.
Sunil
7

para pesquisar cada coluna de cada tabela por um valor particular

Isso não define como fazer a correspondência exatamente.
Nem define o que retornar exatamente.

Supondo:

  • Encontre qualquer linha com qualquer coluna que contenha o valor fornecido em sua representação de texto - em vez de igualar o valor fornecido.
  • Retorne o nome da tabela ( regclass) e o ID da tupla ( ctid), porque é o mais simples.

Aqui está um jeito bem simples, rápido e um pouco sujo:

CREATE OR REPLACE FUNCTION search_whole_db(_like_pattern text)
  RETURNS TABLE(_tbl regclass, _ctid tid) AS
$func$
BEGIN
   FOR _tbl IN
      SELECT c.oid::regclass
      FROM   pg_class c
      JOIN   pg_namespace n ON n.oid = relnamespace
      WHERE  c.relkind = 'r'                           -- only tables
      AND    n.nspname !~ '^(pg_|information_schema)'  -- exclude system schemas
      ORDER BY n.nspname, c.relname
   LOOP
      RETURN QUERY EXECUTE format(
         'SELECT $1, ctid FROM %s t WHERE t::text ~~ %L'
       , _tbl, '%' || _like_pattern || '%')
      USING _tbl;
   END LOOP;
END
$func$  LANGUAGE plpgsql;

Ligar:

SELECT * FROM search_whole_db('mypattern');

Fornece o padrão de pesquisa sem incluir %.

Por que um pouco sujo?

Se separadores e decoradores para a linha em textrepresentação puderem fazer parte do padrão de pesquisa, pode haver falsos positivos:

  • separador de coluna: ,por padrão
  • a linha inteira está entre parênteses:()
  • alguns valores estão entre aspas duplas "
  • \ pode ser adicionado como caractere de escape

E a representação de texto de algumas colunas pode depender das configurações locais - mas essa ambigüidade é inerente à questão, não à minha solução.

Cada linha de qualificação é retornada apenas uma vez , mesmo quando corresponde várias vezes (ao contrário de outras respostas aqui).

Isso pesquisa todo o banco de dados, exceto os catálogos do sistema. Normalmente leva muito tempo para terminar . Você pode querer restringir a certos esquemas / tabelas (ou mesmo colunas) como demonstrado em outras respostas. Ou adicione avisos e um indicador de progresso, também demonstrado em outra resposta.

O regclasstipo de identificador de objeto é representado como nome de tabela, qualificado pelo esquema onde necessário para desambiguar de acordo com o atual search_path:

O que é ctid?

Você pode querer escapar caracteres com significado especial no padrão de pesquisa. Vejo:

Erwin Brandstetter
fonte
Esta ótima solução é ainda melhor com lower () - 'SELECT $ 1, ctid FROM% st WHERE lower (t :: text) ~~ lower (% L)'
Georgi Bonchev
5

E se alguém achar que pode ajudar. Aqui está a função de @Daniel Vérité, com outro parâmetro que aceita nomes de colunas que podem ser usados ​​na pesquisa. Desta forma diminui o tempo de processamento. Pelo menos no meu teste diminuiu muito.

CREATE OR REPLACE FUNCTION search_columns(
    needle text,
    haystack_columns name[] default '{}',
    haystack_tables name[] default '{}',
    haystack_schema name[] default '{public}'
)
RETURNS table(schemaname text, tablename text, columnname text, rowctid text)
AS $$
begin
  FOR schemaname,tablename,columnname IN
      SELECT c.table_schema,c.table_name,c.column_name
      FROM information_schema.columns c
      JOIN information_schema.tables t ON
        (t.table_name=c.table_name AND t.table_schema=c.table_schema)
      WHERE (c.table_name=ANY(haystack_tables) OR haystack_tables='{}')
        AND c.table_schema=ANY(haystack_schema)
        AND (c.column_name=ANY(haystack_columns) OR haystack_columns='{}')
        AND t.table_type='BASE TABLE'
  LOOP
    EXECUTE format('SELECT ctid FROM %I.%I WHERE cast(%I as text)=%L',
       schemaname,
       tablename,
       columnname,
       needle
    ) INTO rowctid;
    IF rowctid is not null THEN
      RETURN NEXT;
    END IF;
 END LOOP;
END;
$$ language plpgsql;

Abaixo está um exemplo de uso da função search_function criada acima.

SELECT * FROM search_columns('86192700'
    , array(SELECT DISTINCT a.column_name::name FROM information_schema.columns AS a
            INNER JOIN information_schema.tables as b ON (b.table_catalog = a.table_catalog AND b.table_schema = a.table_schema AND b.table_name = a.table_name)
        WHERE 
            a.column_name iLIKE '%cep%' 
            AND b.table_type = 'BASE TABLE'
            AND b.table_schema = 'public'
    )

    , array(SELECT b.table_name::name FROM information_schema.columns AS a
            INNER JOIN information_schema.tables as b ON (b.table_catalog = a.table_catalog AND b.table_schema = a.table_schema AND b.table_name = a.table_name)
        WHERE 
            a.column_name iLIKE '%cep%' 
            AND b.table_type = 'BASE TABLE'
            AND b.table_schema = 'public')
);
Daniel A. Martinhao
fonte
5

Sem armazenar um novo procedimento, você pode usar um bloco de código e executar para obter uma tabela de ocorrências. Você pode filtrar os resultados por esquema, tabela ou nome de coluna.

DO $$
DECLARE
  value int := 0;
  sql text := 'The constructed select statement';
  rec1 record;
  rec2 record;
BEGIN
  DROP TABLE IF EXISTS _x;
  CREATE TEMPORARY TABLE _x (
    schema_name text, 
    table_name text, 
    column_name text,
    found text
  );
  FOR rec1 IN 
        SELECT table_schema, table_name, column_name
        FROM information_schema.columns 
        WHERE table_name <> '_x'
                AND UPPER(column_name) LIKE UPPER('%%')                  
                AND table_schema <> 'pg_catalog'
                AND table_schema <> 'information_schema'
                AND data_type IN ('character varying', 'text', 'character', 'char', 'varchar')
        LOOP
    sql := concat('SELECT ', rec1."column_name", ' AS "found" FROM ',rec1."table_schema" , '.',rec1."table_name" , ' WHERE UPPER(',rec1."column_name" , ') LIKE UPPER(''','%my_substring_to_find_goes_here%' , ''')');
    RAISE NOTICE '%', sql;
    BEGIN
        FOR rec2 IN EXECUTE sql LOOP
            RAISE NOTICE '%', sql;
            INSERT INTO _x VALUES (rec1."table_schema", rec1."table_name", rec1."column_name", rec2."found");
        END LOOP;
    EXCEPTION WHEN OTHERS THEN
    END;
  END LOOP;
  END; $$;

SELECT * FROM _x;
profimedica
fonte
Onde você especifica a string de pesquisa? Ou isso é apenas despejar todo o banco de dados, tabela por tabela?
jimtut
1
Não criei um parâmetro para a string. Você pode codificá-lo e executá-lo diretamente como um bloco ou criar um procedimento armazenado a partir dele. Em qualquer caso, sua string a ser pesquisada vai aqui entre os dois sinais de porcentagem: WHERE UPPER (', rec1. "Column_name",') LIKE UPPER ('' ',' %% ',' '')
profimedica
5

Existe uma maneira de fazer isso sem criar uma função ou usar uma ferramenta externa. Usando a query_to_xml()função do Postgres, que pode executar dinamicamente uma consulta dentro de outra consulta, é possível pesquisar um texto em muitas tabelas. Isso se baseia em minha resposta para recuperar o número de linhas para todas as tabelas :

Para pesquisar a string fooem todas as tabelas em um esquema, o seguinte pode ser usado:

with found_rows as (
  select format('%I.%I', table_schema, table_name) as table_name,
         query_to_xml(format('select to_jsonb(t) as table_row 
                              from %I.%I as t 
                              where t::text like ''%%foo%%'' ', table_schema, table_name), 
                      true, false, '') as table_rows
  from information_schema.tables 
  where table_schema = 'public'
)
select table_name, x.table_row
from found_rows f
  left join xmltable('//table/row' 
                     passing table_rows
                       columns
                         table_row text path 'table_row') as x on true

Observe que o uso de xmltablerequer Postgres 10 ou mais recente. Para versões anteriores do Postgres, isso também pode ser feito usando xpath ().

with found_rows as (
  select format('%I.%I', table_schema, table_name) as table_name,
         query_to_xml(format('select to_jsonb(t) as table_row 
                              from %I.%I as t 
                              where t::text like ''%%foo%%'' ', table_schema, table_name), 
                      true, false, '') as table_rows
  from information_schema.tables 
  where table_schema = 'public'
)
select table_name, x.table_row
from found_rows f
   cross join unnest(xpath('/table/row/table_row/text()', table_rows)) as r(data)

A expressão de tabela comum ( WITH ...) é usada apenas por conveniência. Ele percorre todas as tabelas do publicesquema. Para cada tabela, a seguinte consulta é executada por meio da query_to_xml()função:

select to_jsonb(t)
from some_table t
where t::text like '%foo%';

A cláusula where é usada para garantir que a geração cara de conteúdo XML seja feita apenas para linhas que contêm a string de pesquisa. Isso pode retornar algo assim:

<table xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<row>
  <table_row>{"id": 42, "some_column": "foobar"}</table_row>
</row>
</table>

É feita a conversão da linha completa em jsonb, para que no resultado se possa ver qual valor pertence a qual coluna.

O texto acima pode retornar algo assim:

table_name   |   table_row
-------------+----------------------------------------
public.foo   |  {"id": 1, "some_column": "foobar"}
public.bar   |  {"id": 42, "another_column": "barfoo"}

Exemplo online para Postgres 10+

Exemplo online para versões mais antigas do Postgres

um cavalo sem nome
fonte
Estou tentando executar o código para versões mais antigas do PostgreSQL e estou recebendo o seguinte erroERROR: 42883: function format("unknown", information_schema.sql_identifier, information_schema.sql_identifier) does not exist
Matt
Você provavelmente precisará lançá-los:format('%I.%I', table_schema::text, table_name::text)
a_horse_with_no_name
Ok, feito isso, agora eu tenhoERROR: 42883: function format("unknown", character varying, character varying) does not exist
Matt
Então, muitas das suas versões do Postgres são tão antigas que id nem mesmo tem a format()função
a_horse_with_no_name
Eu acho que Redshift é baseado em 8.3?
Matt
3

Aqui está a função de @Daniel Vérité com funcionalidade de relatório de progresso. Ele relata o progresso de três maneiras:

  1. por RAISE NOTICE;
  2. diminuindo o valor da sequência {progress_seq} fornecida de {número total de colunas a pesquisar} até 0;
  3. escrevendo o progresso junto com as tabelas encontradas em um arquivo de texto, localizado em c: \ windows \ temp \ {progress_seq} .txt.

_

CREATE OR REPLACE FUNCTION search_columns(
    needle text,
    haystack_tables name[] default '{}',
    haystack_schema name[] default '{public}',
    progress_seq text default NULL
)
RETURNS table(schemaname text, tablename text, columnname text, rowctid text)
AS $$
DECLARE
currenttable text;
columnscount integer;
foundintables text[];
foundincolumns text[];
begin
currenttable='';
columnscount = (SELECT count(1)
      FROM information_schema.columns c
      JOIN information_schema.tables t ON
        (t.table_name=c.table_name AND t.table_schema=c.table_schema)
      WHERE (c.table_name=ANY(haystack_tables) OR haystack_tables='{}')
        AND c.table_schema=ANY(haystack_schema)
        AND t.table_type='BASE TABLE')::integer;
PERFORM setval(progress_seq::regclass, columnscount);

  FOR schemaname,tablename,columnname IN
      SELECT c.table_schema,c.table_name,c.column_name
      FROM information_schema.columns c
      JOIN information_schema.tables t ON
        (t.table_name=c.table_name AND t.table_schema=c.table_schema)
      WHERE (c.table_name=ANY(haystack_tables) OR haystack_tables='{}')
        AND c.table_schema=ANY(haystack_schema)
        AND t.table_type='BASE TABLE'
  LOOP
    EXECUTE format('SELECT ctid FROM %I.%I WHERE cast(%I as text)=%L',
       schemaname,
       tablename,
       columnname,
       needle
    ) INTO rowctid;
    IF rowctid is not null THEN
      RETURN NEXT;
      foundintables = foundintables || tablename;
      foundincolumns = foundincolumns || columnname;
      RAISE NOTICE 'FOUND! %, %, %, %', schemaname,tablename,columnname, rowctid;
    END IF;
         IF (progress_seq IS NOT NULL) THEN 
        PERFORM nextval(progress_seq::regclass);
    END IF;
    IF(currenttable<>tablename) THEN  
    currenttable=tablename;
     IF (progress_seq IS NOT NULL) THEN 
        RAISE NOTICE 'Columns left to look in: %; looking in table: %', currval(progress_seq::regclass), tablename;
        EXECUTE 'COPY (SELECT unnest(string_to_array(''Current table (column ' || columnscount-currval(progress_seq::regclass) || ' of ' || columnscount || '): ' || tablename || '\n\nFound in tables/columns:\n' || COALESCE(
        (SELECT string_agg(c1 || '/' || c2, '\n') FROM (SELECT unnest(foundintables) AS c1,unnest(foundincolumns) AS c2) AS t1)
        , '') || ''',''\n''))) TO ''c:\WINDOWS\temp\' || progress_seq || '.txt''';
    END IF;
    END IF;
 END LOOP;
END;
$$ language plpgsql;
alexkovelsky
fonte
3

- A função abaixo irá listar todas as tabelas que contêm uma string específica no banco de dados

 select TablesCount(‘StringToSearch’);

--Itra através de todas as tabelas no banco de dados

CREATE OR REPLACE FUNCTION **TablesCount**(_searchText TEXT)
RETURNS text AS 
$$ -- here start procedural part
   DECLARE _tname text;
   DECLARE cnt int;
   BEGIN
    FOR _tname IN SELECT table_name FROM information_schema.tables where table_schema='public' and table_type='BASE TABLE'  LOOP
         cnt= getMatchingCount(_tname,Columnames(_tname,_searchText));
                                RAISE NOTICE 'Count% ', CONCAT('  ',cnt,' Table name: ', _tname);
                END LOOP;
    RETURN _tname;
   END;
$$ -- here finish procedural part
LANGUAGE plpgsql; -- language specification

- Retorna a contagem de tabelas para as quais a condição é atendida. - Por exemplo, se o texto pretendido existe em qualquer um dos campos da tabela, - então a contagem será maior que 0. Podemos encontrar as notificações - na seção Mensagens do visualizador de resultados no banco de dados postgres.

CREATE OR REPLACE FUNCTION **getMatchingCount**(_tname TEXT, _clause TEXT)
RETURNS int AS 
$$
Declare outpt text;
    BEGIN
    EXECUTE 'Select Count(*) from '||_tname||' where '|| _clause
       INTO outpt;
       RETURN outpt;
    END;
$$ LANGUAGE plpgsql;

--Obter os campos de cada tabela. Constrói a cláusula where com todas as colunas de uma tabela.

CREATE OR REPLACE FUNCTION **Columnames**(_tname text,st text)
RETURNS text AS 
$$ -- here start procedural part
DECLARE
                _name text;
                _helper text;
   BEGIN
                FOR _name IN SELECT column_name FROM information_schema.Columns WHERE table_name =_tname LOOP
                                _name=CONCAT('CAST(',_name,' as VarChar)',' like ','''%',st,'%''', ' OR ');
                                _helper= CONCAT(_helper,_name,' ');
                END LOOP;
                RETURN CONCAT(_helper, ' 1=2');

   END;
$$ -- here finish procedural part
LANGUAGE plpgsql; -- language specification
Ganesh
fonte