Comment rechercher une valeur spécifique dans tous les tableaux (PostgreSQL)?

est-il possible de rechercher dans chaque colonne de chaque tableau pour une valeur particulière dans PostgreSQL?

une question similaire est disponible ici pour Oracle.

67
demandé sur Erwin Brandstetter 2011-03-18 12:25:25

7 réponses

Que Diriez-vous de jeter le contenu de la base de données, puis en utilisant 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 colonnes dans la sortie. Il suffit de remplacer --inserts par --column-inserts . De cette façon, vous pouvez rechercher des noms de colonne, trop. 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');
87
répondu Mike Sherrill 'Cat Recall' 2011-03-19 01:09:18

Voici une fonction pl/pgsql qui localise les enregistrements où n'importe quelle colonne contient une valeur spécifique. Il prend comme arguments la valeur à rechercher dans le format de texte, un tableau des noms de table à rechercher (par défaut à toutes les tables) et un tableau des noms de schéma (par défaut tous les noms de schéma).

il renvoie une structure de table avec Schéma, nom de la table, nom de la colonne et pseudo-colonne ctid (emplacement physique non durable de la ligne dans la table, voir système Colonnes )

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;

EDIT : ce code est pour PG 9.1 ou plus récent. En outre, vous pourriez vouloir la version sur github basé sur le même principe, mais en ajoutant une certaine vitesse et des améliorations de rapport.

exemples d'utilisation dans une base de données d'essai:

recherche dans tous les tableaux du schéma public:

select * from search_columns('foobar');
 schemaname | tablename | columnname | rowctid 
------------+-----------+------------+---------
 public     | s3        | usename    | (0,11)
 public     | s2        | relname    | (7,29)
 public     | w         | body       | (0,2)
(3 rows)

recherche dans un tableau spécifique:

 select * from search_columns('foobar','{w}');
 schemaname | tablename | columnname | rowctid 
------------+-----------+------------+---------
 public     | w         | body       | (0,2)
(1 row)

recherche dans un sous-ensemble de tableaux obtenus à partir d'un select:

select * from search_columns('foobar', array(select table_name::name from information_schema.tables where table_name like 's%'), array['public']);
 schemaname | tablename | columnname | rowctid 
------------+-----------+------------+---------
 public     | s2        | relname    | (7,29)
 public     | s3        | usename    | (0,11)
(2 rows)

obtenir une ligne de résultat avec la table de base correspondante et et ctid:

select * from public.w where ctid='(0,2)';
 title |  body  |         tsv         
-------+--------+---------------------
 toto  | foobar | 'foobar':2 'toto':1

tester à nouveau une expression régulière au lieu d'une égalité stricte, comme grep, ceci:

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

peut être remplacé par:

SELECT ctid FROM %I.%I WHERE cast(%I as text) ~ %L
44
répondu Daniel Vérité 2018-04-06 19:07:31

le seul outil que je connaisse qui puisse faire cela est: SQL Workbench / J: http://www.sql-workbench.net/

un outil basé sur Java / JDBC qui offre une "commande" SQL (propriétaire) spéciale pour rechercher à travers toutes les tables (ou juste sélectionnées) dans 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

9
répondu a_horse_with_no_name 2018-04-25 13:23:43

et si quelqu'un pense que ça pourrait aider. Voici la fonction de @Daniel Vérité, avec un autre param qui accepte les noms de colonnes qui peuvent être utilisées dans la recherche. De cette manière, il diminue le temps de traitement. Au moins dans mon test il réduit beaucoup.

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;

Soufflet est un exemple d'utilisation de la search_function créé 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')
);
4
répondu Daniel A. Martinhao 2014-09-18 14:25:17

sans stocker une nouvelle procédure, vous pouvez utiliser un bloc de code et exécuter pour obtenir une table d'occurences. Vous pouvez filtrer les résultats par schéma, table ou nom 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;
3
répondu profimedica 2017-08-16 22:14:26

Voici la fonction de @Daniel Vérité avec la fonctionnalité de rapport d'étape. Il rend compte des progrès de trois façons:

  1. par SOULEVER AVIS;
  2. par diminution de la valeur de la séquence {progress_seq} fournie {nombre total de colonnes à rechercher dans} 0;
  3. en écrivant le progrès avec les tables trouvées dans le fichier de texte, situé à 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;
2
répondu alexkovelsky 2014-08-22 12:12:42

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

cela ne définit pas comment correspondre exactement.

Il ne définit pas non plus ce qu'il faut retourner exactement.

en supposant:

  • trouver n'importe quelle ligne avec n'importe quelle colonne contenant la valeur donnée dans sa représentation de texte - par opposition à = la valeur donnée.
  • renvoie le nom de la table ( regclass ) et le pointeur d'article ( ctid ), parce que c'est le plus simple.

Voici une voie 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 , 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');

fournit le motif de recherche sans inclure % .

pourquoi un peu sale?

si les séparateurs et les décorateurs pour la rangée dans text la représentation peut faire partie du motif de recherche, il peut y avoir des faux positifs:

  • séparateur de colonne: , par défaut
  • la rangée entière est entre parenthèses: ()
  • certaines valeurs sont incluses entre guillemets "
  • \ peut être ajouté comme escape char

et la représentation textuelle de certaines colonnes peut dépendre de paramètres locaux - mais cette ambiguïté est inhérente à la question, pas à ma solution.

chaque ligne de qualification est retournée une seule fois seulement, même si elle correspond à plusieurs reprises (par opposition à d'autres réponses ici).

ceci recherche la base de données entière à l'exception des catalogues système. Généralement prendre beaucoup de temps pour finir . Vous pouvez restreindre à certains schémas, tableaux (ou même des colonnes) comme démontré dans d'autres réponses. Ou ajouter des avis et un indicateur de progrès, également démontré dans une autre réponse.

le type d'identificateur d'objet regclass est représenté en tant que nom de table, qualifié de schéma lorsque cela est nécessaire pour désambiguer selon l'actuel search_path :

Qu'est-ce que le ctid ?

vous pourriez vouloir échapper aux caractères ayant une signification particulière dans le motif de recherche. Voir:

2
répondu Erwin Brandstetter 2018-04-25 14:21:52