Jak wyszukać określoną wartość we wszystkich tabelach (PostgreSQL)?


111

Czy można przeszukać każdą kolumnę każdej tabeli pod kątem określonej wartości w PostgreSQL?

Podobne pytanie jest dostępne tutaj dla Oracle.


Szukasz narzędzia lub wdrożenia procedur przedstawionych w powiązanym pytaniu?
a_horse_with_no_name

Nie, tylko najprostszy sposób na znalezienie określonej wartości we wszystkich polach / tabelach.
Sandro Munda,

Więc nie chcesz używać zewnętrznego narzędzia?
a_horse_with_no_name

1
Jeśli to najprostszy sposób => ok dla zewnętrznego narzędzia :-)
Sandro Munda

Odpowiedzi:


131

Co powiesz na zrzucenie zawartości bazy danych, a następnie użycie 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');

To samo narzędzie, pg_dump, może zawierać nazwy kolumn w danych wyjściowych. Po prostu zmień --insertsna --column-inserts. W ten sposób możesz również wyszukiwać określone nazwy kolumn. Ale gdybym szukał nazw kolumn, prawdopodobnie zrzuciłbym schemat zamiast danych.

$ 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');

5
+1 darmowe i proste. A jeśli chcesz struktury, pg_dump też może to zrobić. Ponadto, jeśli grep nie jest twoją rzeczą, użyj dowolnego narzędzia do wyszukiwania zawartości plików, które chcesz na zrzuconych strukturach i / lub danych.
Kuberchaun,

Jeśli chcesz grepować dane tekstowe (które są zwykle zakodowane w nowszych wersjach postgres), może być konieczne skorzystanie ALTER DATABASE your_db_name SET bytea_output = 'escape';z bazy danych (lub jej kopii) przed jej zrzuceniem. (Nie widzę sposobu, aby określić to tylko dla pg_dumppolecenia.)
phils

czy możesz wyjaśnić szczegółowo ..? Jak wyszukać ciąg „ABC” we wszystkich tabelach?
Pan Bhosale,

1
Jeśli używasz IntelliJ, możesz po prostu kliknąć prawym przyciskiem myszy swoją bazę danych i wybrać „Dump with 'pg_dump'” lub „Dump data to file (s)”
Laurens,

3
W jaki sposób jest to poprawne rozwiązanie dla dowolnej bazy danych, która jest wystarczająco duża, aby nie można było jej zrzucić na dysk?
Govind Parmar

76

Oto funkcja pl / pgsql, która lokalizuje rekordy, w których każda kolumna zawiera określoną wartość. Jako argumenty przyjmuje wartość do przeszukania w formacie tekstowym, tablicę nazw tabel do przeszukania (domyślnie wszystkie tabele) i tablicę nazw schematów (domyślnie wszystkie nazwy schematów).

Zwraca strukturę tabeli ze schematem, nazwą tabeli, nazwą kolumny i pseudokolumną ctid(nietrwała fizyczna lokalizacja wiersza w tabeli, patrz Kolumny systemowe )

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;

Zobacz także wersję na github opartą na tej samej zasadzie, ale dodającą pewne ulepszenia szybkości i raportowania.

Przykłady użycia w testowej bazie danych:

  • Wyszukaj we wszystkich tabelach w schemacie publicznym:
wybierz * z search_columns ('foobar');
 schemaname | nazwa tabeli | nazwa kolumny | rowctid
------------ + ----------- + ------------ + ---------
 publiczny | s3 | nazwa_użytkownika | (0,11)
 publiczny | s2 | relname | (7,29)
 publiczny | w | ciało | (0,2)
(3 rzędy)
  • Wyszukaj w określonej tabeli:
 wybierz * z search_columns ('foobar', '{w}');
 schemaname | nazwa tabeli | nazwa kolumny | rowctid
------------ + ----------- + ------------ + ---------
 publiczny | w | ciało | (0,2)
(1 rząd)
  • Szukaj w podzbiorze tabel uzyskanych z selekcji:
select * from search_columns ('foobar', array (select table_name :: name from information_schema.tables gdzie table_name jak 's%'), array ['public']);
 schemaname | nazwa tabeli | nazwa kolumny | rowctid
------------ + ----------- + ------------ + ---------
 publiczny | s2 | relname | (7,29)
 publiczny | s3 | nazwa_użytkownika | (0,11)
(2 rzędy)
  • Uzyskaj wiersz wynikowy z odpowiednią tabelą podstawową i i ctid:
wybierz * z public.w gdzie ctid = '(0,2)';
 tytuł | ciało | tsv         
------- + -------- + ---------------------
 toto | foobar | „foobar”: 2 „toto”: 1

Warianty

  • Aby przetestować pod kątem wyrażenia regularnego zamiast ścisłej równości, takiej jak grep, ta część zapytania:

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

    można zmienić na:

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

  • W przypadku porównań bez rozróżniania wielkości liter możesz napisać:

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


BŁĄD: błąd składni na poziomie „default” lub w pobliżu „default” LINE 3: haystack_tables name [] default '{}' (przy użyciu PostgreSQL 8.2.17 i nie można go zaktualizować)
Henno

@Henno: tak, wymaga PG-9.1. Zmieniono teraz, aby było to wyraźne. Aby używać go ze starszymi wersjami, musisz go dostosować.
Daniel Vérité

1
@Rajendra_Prasad: operator wyrażenia regularnego ma wariant niewrażliwy na wielkość liter: ~*bardziej odpowiedni niż niższy (). Ale tak czy inaczej, t.*nie jest częścią powyższej odpowiedzi. Wyszukiwanie kolumna po kolumnie to nie to samo, co wyszukiwanie wiersza jako wartości ze względu na separatory kolumn.
Daniel Vérité

2
Zwraca tylko jeden wiersz na kolumnę tabeli schematu.
theGtknerd

1
Wielkie dzięki. U mnie to rozwiązanie działa idealnie. Musiałem znaleźć tabelę na liście ponad 1000 tabel, która zawiera określony adres URL. Uratowałeś mi dzień!
Sunil

7

przeszukać każdą kolumnę każdej tabeli pod kątem określonej wartości

To nie określa, jak dokładnie dopasować.
Nie definiuje też dokładnie, co zwrócić.

Zarozumiały:

  • Znajdź dowolny wiersz z dowolną kolumną zawierającą daną wartość w reprezentacji tekstowej - w przeciwieństwie do równej podanej wartości.
  • Zwróć nazwę tabeli ( regclass) i identyfikator krotki ( ctid), ponieważ to jest najprostsze.

Oto martwy prosty, szybki i nieco brudny sposób:

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;

Połączenie:

SELECT * FROM search_whole_db('mypattern');

Podaj wzorzec wyszukiwania bez zamykania %.

Dlaczego lekko zabrudzony?

Jeśli separatory i dekoratory wiersza w textreprezentacji mogą być częścią wzorca wyszukiwania, mogą występować fałszywe alarmy:

  • separator kolumn: ,domyślnie
  • cały wiersz jest ujęty w nawiasy:()
  • niektóre wartości są ujęte w podwójne cudzysłowy "
  • \ można dodać jako znak zmiany znaczenia

A tekstowa reprezentacja niektórych kolumn może zależeć od lokalnych ustawień - ale ta niejednoznaczność jest nieodłącznym elementem pytania, a nie mojego rozwiązania.

Każdy kwalifikujący się wiersz jest zwracany tylko raz , nawet jeśli pasuje wiele razy (w przeciwieństwie do innych odpowiedzi tutaj).

Przeszukuje całą bazę danych z wyjątkiem katalogów systemowych. Zwykle trwa to długo . Możesz chcieć ograniczyć się do niektórych schematów / tabel (lub nawet kolumn), jak pokazano w innych odpowiedziach. Lub dodaj powiadomienia i wskaźnik postępu, również pokazane w innej odpowiedzi.

regclassTyp identyfikator obiektu jest reprezentowany jako nazwa tabeli, schematu wykwalifikowanych gdzie konieczne disambiguate według obecnego search_path:

Co to jest ctid?

Możesz chcieć zmienić znaczenie znaków o specjalnym znaczeniu we wzorcu wyszukiwania. Widzieć:


To świetne rozwiązanie jest jeszcze lepsze w przypadku lower () - 'SELECT $ 1, ctid FROM% st WHERE lower (t :: text) ~~ lower (% L)'
Georgi Bonchev

5

A jeśli ktoś pomyśli, że to mogłoby pomóc. Oto funkcja @Daniel Vérité z innym parametrem, który akceptuje nazwy kolumn, których można użyć w wyszukiwaniu. W ten sposób skraca czas przetwarzania. Przynajmniej w moim teście znacznie się zmniejszyło.

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;

Poniżej znajduje się przykład użycia funkcji search_function utworzonej powyżej.

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')
);

5

Bez zapisywania nowej procedury możesz użyć bloku kodu i wykonać, aby uzyskać tabelę zdarzeń. Możesz filtrować wyniki według schematu, tabeli lub nazwy kolumny.

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;

Gdzie określasz ciąg wyszukiwania? Czy to po prostu zrzucenie całej bazy danych, tabela po tabeli?
jimtut

1
Nie utworzyłem parametru dla ciągu. Możesz go zakodować na stałe i uruchomić bezpośrednio jako blok lub utworzyć z niego procedurę składowaną. W każdym razie szukany ciąg znajduje się tutaj między dwoma znakami procentu: WHERE UPPER (', rec1. "Nazwa_kolumny",') LIKE UPPER ('' ',' %% ',' '')
profimedica

5

Można to osiągnąć bez tworzenia funkcji lub korzystania z zewnętrznego narzędzia. Używając funkcji Postgresa, query_to_xml()która może dynamicznie uruchamiać zapytanie wewnątrz innego zapytania, możliwe jest przeszukiwanie tekstu w wielu tabelach. Jest to oparte na mojej odpowiedzi, aby pobrać liczbę wierszy dla wszystkich tabel :

Aby wyszukać ciąg foowe wszystkich tabelach w schemacie, można użyć następujących metod:

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

Zauważ, że użycie xmltablewymaga Postgres 10 lub nowszego. W przypadku starszych wersji Postgres można to również zrobić za pomocą 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)

Typowe wyrażenie tabeli ( WITH ...) jest używane tylko dla wygody. Przechodzi przez wszystkie tabele w publicschemacie. Dla każdej tabeli przez query_to_xml()funkcję jest wykonywane następujące zapytanie :

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

Klauzula where służy do upewnienia się, że kosztowne generowanie zawartości XML jest wykonywane tylko dla wierszy zawierających wyszukiwany ciąg. To może zwrócić coś takiego:

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

jsonbWykonywana jest konwersja całego wiersza na , dzięki czemu w wyniku można zobaczyć, która wartość należy do której kolumny.

Powyższe może zwrócić coś takiego:

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

Przykład online dla Postgres 10+

Przykład online dla starszych wersji Postgres


Próbuję uruchomić kod dla starszych wersji PostgreSQL i pojawia się następujący błądERROR: 42883: function format("unknown", information_schema.sql_identifier, information_schema.sql_identifier) does not exist
Matt

Prawdopodobnie musisz je rzucić:format('%I.%I', table_schema::text, table_name::text)
a_horse_with_no_name

Ok, zrobiłem to, teraz mamERROR: 42883: function format("unknown", character varying, character varying) does not exist
Matt

W takim razie wiele twoich wersji Postgres jest tak starych, że id nie ma nawet format()funkcji
a_horse_with_no_name

Myślę, że Redshift jest oparty na 8.3?
Matt,

3

Oto funkcja @Daniel Vérité z funkcją raportowania postępów. Informuje o postępach na trzy sposoby:

  1. przez PODNOSZENIE POWIADOMIENIA;
  2. zmniejszając wartość dostarczonej sekwencji {progress_seq} z {całkowitej liczby kolumn do przeszukania} w dół do 0;
  3. zapisując postęp wraz ze znalezionymi tabelami do pliku tekstowego, znajdującego się w 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;

3

- Funkcja poniżej wyświetli listę wszystkich tabel, które zawierają określony ciąg w bazie danych

 select TablesCount(‘StringToSearch’);

- wykonuje iterację po wszystkich tabelach w bazie danych

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

- Zwraca liczbę tabel, dla których warunek jest spełniony. - Na przykład, jeśli zamierzony tekst istnieje w którymkolwiek z pól tabeli, - wtedy liczba będzie większa od 0. Powiadomienia możemy znaleźć w sekcji Wiadomości przeglądarki wyników w bazie danych 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;

- Pobierz pola każdej tabeli. Tworzy klauzulę where ze wszystkimi kolumnami tabeli.

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.