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.
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.
Odpowiedzi:
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ń --inserts
na --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');
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_dump
polecenia.)
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:
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)
wybierz * z search_columns ('foobar', '{w}'); schemaname | nazwa tabeli | nazwa kolumny | rowctid ------------ + ----------- + ------------ + --------- publiczny | w | ciało | (0,2) (1 rząd)
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)
wybierz * z public.w gdzie ctid = '(0,2)'; tytuł | ciało | tsv ------- + -------- + --------------------- toto | foobar | „foobar”: 2 „toto”: 1
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)
~*
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.
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:
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 text
reprezentacji mogą być częścią wzorca wyszukiwania, mogą występować fałszywe alarmy:
,
domyślnie()
"
\
można dodać jako znak zmiany znaczeniaA 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.
regclass
Typ 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ć:
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')
);
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;
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 foo
we 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 xmltable
wymaga 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 public
schemacie. 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>
jsonb
Wykonywana 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"}
ERROR: 42883: function format("unknown", information_schema.sql_identifier, information_schema.sql_identifier) does not exist
format('%I.%I', table_schema::text, table_name::text)
ERROR: 42883: function format("unknown", character varying, character varying) does not exist
format()
funkcji
Oto funkcja @Daniel Vérité z funkcją raportowania postępów. Informuje o postępach na trzy sposoby:
_
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;
- 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