Comment rechercher une valeur spécifique dans toutes les tables (PostgreSQL)?

111

Est-il possible de rechercher dans chaque colonne de chaque table une valeur particulière dans PostgreSQL?

Une question similaire est disponible ici pour Oracle.

Sandro Munda
la source
Vous recherchez un outil ou une implémentation des procédures indiquées dans la question liée?
a_horse_with_no_name
Non, c'est simplement le moyen le plus simple de trouver une valeur spécifique dans tous les champs / tables.
Sandro Munda
Vous ne souhaitez donc pas utiliser d'outil externe?
a_horse_with_no_name
1
Si c'est le moyen le plus simple => ok pour un outil externe :-)
Sandro Munda

Réponses:

131

Que diriez-vous de vider le contenu de la base de données, puis de l'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. Changez simplement --insertspour --column-inserts. De cette façon, vous pouvez également rechercher des noms de colonnes spécifiques. Mais si je cherchais des noms de colonnes, 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');
Mike Sherrill 'Rappel de chat'
la source
5
+1 gratuit et simple. Et si vous voulez une structure, pg_dump peut le faire aussi. Aussi, si grep n'est pas votre truc, utilisez l'outil de recherche de contenu de fichier que vous voulez sur les structures et / ou données vidées.
Kuberchaun
Si vous voulez grep des données texte (qui sont généralement encodées dans des versions plus récentes de postgres), vous devrez peut-être le faire ALTER DATABASE your_db_name SET bytea_output = 'escape';sur la base de données (ou une copie de celle-ci) avant de le vider. (Je ne vois pas de moyen de spécifier cela uniquement pour une pg_dumpcommande.)
phils
pouvez-vous expliquer en détail ..? Comment rechercher la chaîne «ABC» dans toutes les tables?
M. Bhosale
1
Si vous utilisez IntelliJ, vous pouvez simplement cliquer avec le bouton droit de la souris sur votre base de données et sélectionner "Dump with 'pg_dump'" ou "Dump data to file (s)"
Laurens
3
En quoi est-ce une solution valide pour une base de données suffisamment grande pour que vous ne puissiez pas la vider sur votre disque?
Govind Parmar
76

Voici une fonction pl / pgsql qui localise les enregistrements où toute colonne contient une valeur spécifique. Il prend comme arguments la valeur à rechercher au format texte, un tableau de noms de table à rechercher (par défaut sur 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)
        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;

Voir aussi la version sur github basée sur le même principe mais en ajoutant quelques améliorations de vitesse et de reporting.

Exemples d'utilisation dans une base de données de test:

  • Rechercher dans toutes les tables du schéma public:
sélectionnez * dans search_columns ('foobar');
 schemaname | nomtable | nom_colonne | rowctid
------------ + ----------- + ------------ + ---------
 public | s3 | nom d'utilisateur | (0,11)
 public | s2 | relname | (7,29)
 public | w | corps | (0,2)
(3 rangées)
  • Rechercher dans un tableau spécifique:
 select * from search_columns ('foobar', '{w}');
 schemaname | nomtable | nom_colonne | rowctid
------------ + ----------- + ------------ + ---------
 public | w | corps | (0,2)
(1 rangée)
  • Rechercher dans un sous-ensemble de tables obtenu à partir d'une sélection:
select * from search_columns ('foobar', array (select table_name :: name from information_schema.tables where table_name like 's%'), array ['public']);
 schemaname | nomtable | nom_colonne | rowctid
------------ + ----------- + ------------ + ---------
 public | s2 | relname | (7,29)
 public | s3 | nom d'utilisateur | (0,11)
(2 rangées)
  • Obtenez une ligne de résultat avec la table de base correspondante et et ctid:
sélectionnez * dans public.w où ctid = '(0,2)';
 titre | corps | tsv         
------- + -------- + ---------------------
 toto | foobar | 'foobar': 2 'toto': 1

Variantes

  • Pour tester par rapport à une expression régulière au lieu de l'é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)

Daniel Vérité
la source
ERREUR: erreur de syntaxe à ou près de "default" LINE 3: haystack_tables name [] default '{}' (Utilisation de PostgreSQL 8.2.17 et impossible de mettre à jour)
Henno
@Henno: oui, il nécessite PG-9.1. Modifié maintenant pour rendre cela explicite. Pour l'utiliser avec des versions plus anciennes, vous devrez l'adapter.
Daniel Vérité
1
@Rajendra_Prasad: l'opérateur d'expression régulière a une variante insensible à la casse: ~*plus adéquate que lower (). Mais de toute façon, cela t.*ne fait pas partie de la réponse ci-dessus. La recherche colonne par colonne n'est pas la même chose que la recherche de la ligne en tant que valeur en raison des séparateurs de colonnes.
Daniel Vérité
2
Cela ne renvoie qu'une seule ligne par colonne de table-schéma.
theGtknerd
1
Merci beaucoup. Cette solution fonctionne parfaitement pour moi. J'ai dû localiser une table dans une liste de plus de 1000 tables contenant une URL spécifique. Vous avez sauvé ma journée !.
Sunil
7

pour rechercher dans chaque colonne de chaque table une valeur particulière

Cela ne définit pas comment faire correspondre exactement.
Il ne définit pas non plus ce qu'il faut renvoyer exactement.

En supposant:

  • Trouvez n'importe quelle ligne avec n'importe quelle colonne contenant la valeur donnée dans sa représentation textuelle - par opposition à l' égalisation de la valeur donnée.
  • Renvoyez le nom de la table ( regclass) et l'ID de tuple ( 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 textreprésentation peuvent faire partie du modèle de recherche, il peut y avoir des faux positifs:

  • séparateur de colonne: ,par défaut
  • la ligne entière est entre parenthèses:()
  • certaines valeurs sont entre guillemets "
  • \ peut être ajouté comme caractère d'échappement

Et 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 de qualification est renvoyée une seule fois , même si elle correspond plusieurs fois (par opposition aux autres réponses ici).

Ceci recherche l'ensemble de la base de données à l'exception des catalogues système. Prendra généralement beaucoup de temps pour terminer . Vous voudrez peut-être vous limiter à certains schémas / tables (ou même à des colonnes) comme illustré dans d'autres réponses. Ou ajoutez des avis et un indicateur de progression, également démontré dans une autre réponse.

Le regclasstype d'identificateur d'objet est représenté sous forme de nom de table, qualifié de schéma si nécessaire pour lever l'ambiguïté en fonction du courant search_path:

Quel est le ctid?

Vous souhaiterez peut-être échapper des caractères ayant une signification particulière dans le modèle de recherche. Voir:

Erwin Brandstetter
la source
Cette excellente solution est encore meilleure avec lower () - 'SELECT $ 1, ctid FROM% st WHERE lower (t :: text) ~~ lower (% L)'
Georgi Bonchev
5

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 utilisables dans la recherche. De cette façon, il réduit le temps de traitement. Au moins dans mon test, il 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 search_function 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')
);
Daniel A. Martinhao
la source
5

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;
profimedica
la source
Où spécifiez-vous la chaîne de recherche? Ou est-ce simplement le dumping de la base de données entière, table par table?
jimtut
1
Je n'ai pas créé de paramètre pour la chaîne. Vous pouvez soit le coder en dur et l'exécuter directement en tant que bloc, soit en créer une procédure stockée. Dans tous les cas, la chaîne à rechercher va ici entre les deux signes de pourcentage: WHERE UPPER (', rec1. "Nom_colonne",') LIKE UPPER ('' ',' %% ',' '')
profimedica
5

Il existe un moyen d'y parvenir sans créer de fonction ni utiliser un outil externe. En utilisant la query_to_xml()fonction de Postgres qui peut exécuter dynamiquement une requête dans une autre requête, il est possible de rechercher un texte dans de nombreuses 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 foodans toutes les tables d'un schéma, les éléments suivants peuvent être utilisés:

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 xmltablenécessite Postgres 10 ou plus récent. 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 ...) n'est utilisée que par commodité. Il parcourt toutes les tables du publicschéma. Pour chaque table, la requête suivante est exécutée via la query_to_xml()fonction:

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 renvoyer 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 jsonbest effectuée, de sorte que dans le résultat, on puisse 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"}

Exemple en ligne pour Postgres 10+

Exemple en ligne pour les anciennes versions de Postgres

un cheval sans nom
la source
J'essaie d'exécuter le code pour les anciennes versions de PostgreSQL et j'obtiens l'erreur suivanteERROR: 42883: function format("unknown", information_schema.sql_identifier, information_schema.sql_identifier) does not exist
Matt
Vous devrez probablement les lancer:format('%I.%I', table_schema::text, table_name::text)
a_horse_with_no_name
Ok, fait ça, maintenant j'aiERROR: 42883: function format("unknown", character varying, character varying) does not exist
Matt
Ensuite, beaucoup de votre version de Postgres est si ancienne, cet identifiant n'a même pas la format()fonction
a_horse_with_no_name
Je pense que Redshift est basé sur 8.3?
Matt
3

Voici la fonction de @Daniel Vérité avec la fonctionnalité de rapport de progression. Il rend compte des progrès de trois manières:

  1. par RAISE NOTICE;
  2. en diminuant la valeur de la séquence {progress_seq} fournie de {nombre total de colonnes à rechercher} à 0;
  3. en écrivant la progression avec les tables trouvées dans un fichier texte, situé dans 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
la source
3

- La fonction ci-dessous listera toutes les tables qui contiennent une chaîne spécifique dans la base de données

 select TablesCount(‘StringToSearch’);

--Itère 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

- Renvoie le nombre de tables pour lesquelles la condition est remplie. - Par exemple, si le texte prévu existe dans l'un des champs de la table, - alors le nombre sera supérieur à 0. Nous pouvons trouver les notifications - dans la section Messages de la visionneuse de résultats dans 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;

- Obtenez 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
Ganesh
la source