Est-il possible de rechercher chaque colonne de chaque table pour une valeur particulière dans PostgreSQL?
Une question similaire est disponible ici pour Oracle.
Que diriez-vous de vider le contenu de la base de données, puis d'utiliser 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');
Le même utilitaire, pg_dump, peut inclure des noms de colonne dans la sortie. Il suffit de changer --inserts
à --column-inserts
. De cette façon, vous pouvez également rechercher des noms de colonne spécifiques. Mais si je cherchais des noms de colonne, je viderais probablement le schéma au lieu des données.
$ 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');
Voici une fonction pl/pgsql qui localise les enregistrements dans lesquels toute colonne contient une valeur spécifique. Il prend comme arguments la valeur à rechercher au format texte, un tableau de noms de tables dans lequel rechercher (par défaut pour toutes les tables) et un tableau de noms de schéma (par défaut, tous les noms de schéma).
Il renvoie une structure de table avec schéma, nom de table, nom de colonne et pseudo-colonne ctid
(emplacement physique non durable de la ligne dans la table, voir Colonnes système )
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)
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
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;
[~ # ~] éditer [~ # ~] : ce code concerne PG 9.1 ou plus récent. En outre, vous voudrez peut-être que la version github soit basée sur le même principe, mais en ajoutant de la vitesse et en améliorant les rapports.
Exemples d'utilisation dans une base de données de test:
sélectionnez * parmi search_columns ('foobar'); schemaname | nom de table | nom de la colonne | rowctid ------------ + ----------- + ------------ + ------ --- public | s3 | nom d'utilisateur | (0,11) Public | s2 | relname | (7,29) Public | w | corps | (0,2) (3 rangées)
sélectionnez * parmi search_columns ('foobar', '{w}'); schemaname | nom de table | nom de la colonne | rowctid ------------ + ----------- + ------------ + ------ --- public | w | corps | (0,2) (1 rangée)
select * from search_columns ('foobar', array (selectionnez nom_table :: nom depuis information_schema.tables où nom_table ressemble à 's%'), array ['public']); nom_fichier | nom de table | nom de la colonne | rowctid ------------ + ----------- + ------------ + ------ --- public | s2 | relname | (7,29) Public | s3 | nom d'utilisateur | (0,11) (2 rangées)
select * from public.w où ctid = '(0,2)'; titre | corps | tsv ------- + -------- + --------------------- toto | foobar | 'foobar': 2 'toto': 1
Pour tester à nouveau une expression régulière au lieu d'une égalité stricte, comme grep, cette partie de la requête:
SELECT ctid FROM %I.%I WHERE cast(%I as text)=%L
peut être changé en:
SELECT ctid FROM %I.%I WHERE cast(%I as text) ~ %L
Pour les comparaisons insensibles à la casse, vous pouvez écrire:
SELECT ctid FROM %I.%I WHERE lower(cast(%I as text)) = lower(%L)
Le seul outil que je connaisse qui puisse le faire est: SQL Workbench/J: http://www.sql-workbench.net/
Un outil basé sur Java/JDBC qui offre une "commande" SQL spéciale (propriétaire) permettant de rechercher dans toutes les tables (ou uniquement dans celles sélectionnées) d'une base de données:
http://www.sql-workbench.eu/manual/wb-commands.html#command-search-data
http://www.sql-workbench.eu/wbgrepdata_png.html
pour rechercher une valeur particulière dans chaque colonne de chaque table
Cela ne définit pas comment faire correspondre exactement.
Cela ne définit pas non plus exactement ce qu'il faut retourner.
En supposant:
regclass
) et le pointeur de l'élément (ctid
), car c'est le plus simple.Voici un moyen simple, rapide et légèrement sale:
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;
Appel:
SELECT * FROM search_whole_db('mypattern');
Fournissez le modèle de recherche sans inclure %
.
Pourquoi un peu sale?
Si les séparateurs et les décorateurs de la ligne dans la représentation text
peuvent faire partie du modèle de recherche, il peut y avoir des faux positifs:
,
par défaut()
"
\
Peut être ajouté en tant que caractère d'échappementEt la représentation textuelle de certaines colonnes peut dépendre des paramètres locaux - mais cette ambiguïté est inhérente à la question, pas à ma solution.
Chaque ligne qualificative est renvoyée une fois uniquement, même si elle correspond plusieurs fois (par opposition aux autres réponses ici).
Cette recherche dans toute la base de données, à l'exception des catalogues système. Va typiquement prend beaucoup de temps pour terminer. Vous voudrez peut-être restreindre certains schémas/tables (ou même certaines colonnes) comme indiqué dans d'autres réponses. Ou ajoutez des avis et un indicateur de progression, également démontré dans une autre réponse.
Le type d'identificateur d'objet regclass
est représenté sous la forme d'un nom de table, qualifié si nécessaire pour désambiguïser selon le schéma actuel search_path
:
Qu'est-ce que le ctid
?
Vous voudrez peut-être échapper des caractères ayant une signification spéciale dans le modèle de recherche. Voir:
Sans stocker une nouvelle procédure, vous pouvez utiliser un bloc de code et exécuter pour obtenir une table des occurrences. Vous pouvez filtrer les résultats par nom de schéma, de table ou de colonne.
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;
Et si quelqu'un pense que cela pourrait aider. Voici la fonction de @Daniel Vérité, avec un autre paramètre qui accepte les noms de colonnes pouvant être utilisés dans la recherche. De cette façon, le temps de traitement diminue. Au moins dans mon test, ça a beaucoup diminué.
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;
Ci-dessous un exemple d'utilisation de la fonction de recherche créée ci-dessus.
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')
);
- La fonction ci-dessous listera toutes les tables contenant une chaîne spécifique dans la base de données
select TablesCount(‘StringToSearch’);
--Il parcourt toutes les tables de la base de données
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
- Retourne le nombre de tables pour lesquelles la condition est remplie. - Par exemple, si le texte souhaité existe dans l'un des champs de la table, le nombre sera supérieur à 0. Nous pouvons trouver les notifications dans la section Messages du visualiseur de résultats de la base de données 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;
--Obtenir les champs de chaque table. Construit la clause where avec toutes les colonnes d'une table.
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
Voici la fonction de @Daniel Vérité avec la fonctionnalité de rapport d'avancement. Il rend compte des progrès de trois manières:
_
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;
Il existe un moyen d'y parvenir sans créer de fonction ni utiliser un outil externe. En utilisant la fonction query_to_xml()
de Postgres, capable d'exécuter de manière dynamique une requête dans une autre requête, il est possible de rechercher un texte dans plusieurs tables. Ceci est basé sur ma réponse pour récupérer le nombre de lignes pour toutes les tables :
Pour rechercher la chaîne foo
dans toutes les tables d'un schéma, vous pouvez utiliser les éléments suivants:
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
Notez que l'utilisation de xmltable
nécessite Postgres 10 ou une version plus récente. Pour les anciennes versions de Postgres, cela peut également être fait en utilisant 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)
L'expression de table commune (WITH ...
) Est uniquement utilisée à des fins pratiques. Il parcourt toutes les tables du schéma public
. Pour chaque table, la requête suivante est exécutée via la fonction query_to_xml()
:
select to_jsonb(t)
from some_table t
where t::text like '%foo%';
La clause where est utilisée pour s'assurer que la génération coûteuse de contenu XML n'est effectuée que pour les lignes contenant la chaîne de recherche. Cela pourrait retourner quelque chose comme ceci:
<table xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<row>
<table_row>{"id": 42, "some_column": "foobar"}</table_row>
</row>
</table>
La conversion de la ligne complète en jsonb
est terminée, ce qui permet de voir quelle valeur appartient à quelle colonne.
Ce qui précède peut renvoyer quelque chose comme ceci:
table_name | table_row
-------------+----------------------------------------
public.foo | {"id": 1, "some_column": "foobar"}
public.bar | {"id": 42, "another_column": "barfoo"}