web-dev-qa-db-fra.com

SQL: SELECT Toutes les colonnes sauf certaines

Existe-t-il un moyen de SELECT toutes les colonnes d'une table, à l'exception de celles spécifiques? Il serait très pratique de sélectionner toutes les colonnes non blob ou non géométriques d'une table.

Quelque chose comme:

SELECT * -the_geom FROM segments;
  • J'ai entendu une fois que cette fonctionnalité a été délibérément exclue de la norme SQL car la modification de l'ajout de colonnes à la table modifiera les résultats de la requête. Est-ce vrai? L'argument est-il valable?
  • Existe-t-il une solution de contournement, en particulier dans PostgreSQL?
119
Adam Matan

Une telle fonctionnalité n'existe ni dans Postgres ni dans le SQL Standard (AFAIK). Je pense que c'est une question assez intéressante donc j'ai googlé un peu et suis tombé sur un article intéressant sur postgresonline.com .

Ils montrent une approche qui sélectionne les colonnes directement dans le schéma:

SELECT 'SELECT ' || array_to_string(ARRAY(SELECT 'o' || '.' || c.column_name
        FROM information_schema.columns As c
            WHERE table_name = 'officepark' 
            AND  c.column_name NOT IN('officeparkid', 'contractor')
    ), ',') || ' FROM officepark As o' As sqlstmt

Vous pouvez créer une fonction qui fait quelque chose comme ça. Ces sujets ont également été discutés sur les listes de diffusion, mais le consensus général était à peu près le même: interroger le schéma.

Je suis sûr qu'il existe d'autres solutions, mais je pense qu'elles impliqueront toutes une sorte de schéma magique.

BTW: soyez prudent avec SELECT * ... car cela peut entraîner des pénalités de performance

59
DrColossos

La vraie réponse est que vous ne pouvez tout simplement pas en pratique. C'est une fonctionnalité demandée depuis des décennies et les développeurs refusent de l'implémenter.

La réponse populaire suggérant d'interroger les tables de schéma ne pourra pas s'exécuter efficacement car l'optimiseur Postgres considère les fonctions dynamiques comme une boîte noire (voir le cas de test ci-dessous). Cela signifie que les index ne seront pas utilisés et que les jointures ne se feront pas intelligemment. Vous seriez beaucoup mieux avec une sorte de système macro comme m4. Au moins, cela ne confondra pas l'optimiseur (mais il peut encore vous embrouiller.) Sans bifurquer le code et écrire la fonctionnalité vous-même ou en utilisant une interface de langage de programmation, vous êtes coincé.

J'ai écrit une simple preuve de concept ci-dessous montrant à quel point les performances seraient mauvaises avec une exécution dynamique très simple dans plpgsql. Notez également que ci-dessous, je dois contraindre une fonction renvoyant un enregistrement générique dans un type de ligne spécifique et énumérer les colonnes. Cette méthode ne fonctionnera donc pas pour "sélectionner tout mais" sauf si vous souhaitez refaire cette fonction pour toutes vos tables.

test=# create table atest (i int primary key);
CREATE TABLE
test=# insert into atest select generate_series(1,100000);
INSERT 0 100000

test=# create function get_table_column(name text) returns setof record as
$$
    declare r record;
    begin
    for r in execute 'select  * from ' || $1 loop
    return next r;
    end loop;
    return; 
    end; 
$$ language plpgsql; 

test=# explain analyze select i from atest where i=999999;
                                                      QUERY PLAN                                    
----------------------------------------------------------------------------------------------------
-------------------
 Index Only Scan using atest_pkey on atest  (cost=0.29..8.31 rows=1 width=4) (actual time=0.024..0.0
24 rows=0 loops=1)
   Index Cond: (i = 999999)
   Heap Fetches: 0
 Planning time: 0.130 ms
 Execution time: 0.067 ms
(5 rows)

test=# explain analyze
    select * from get_table_column('atest') as arowtype(i int) where i = 999999;
                                                        QUERY PLAN                                  
----------------------------------------------------------------------------------------------------
-----------------------
 Function Scan on get_table_column arowtype  (cost=0.25..12.75 rows=5 width=4) (actual time=92.636..
92.636 rows=0 loops=1)
   Filter: (i = 999999)
   Rows Removed by Filter: 100000
 Planning time: 0.080 ms
 Execution time: 95.460 ms
(5 rows)

Comme vous pouvez le voir, l'appel de fonction a analysé toute la table tandis que la requête directe utilisait l'index (95,46 ms contre 00,07 ms.) Ces types de fonctions chargeaient tout type de requête compliquée qui nécessitait l'utilisation d'index ou joindre des tables dans le bon ordre.

18
user17130

C'est en fait quelque peu possible avec PostgreSQL commençant par 9.4 où JSONB a été introduit. Je réfléchissais à une question similaire sur la façon d'afficher tous les attributs disponibles dans Google Map (via GeoJSON).

johto sur le canal irc a suggéré d'essayer de supprimer l'élément de JSONB.

Voici l'idée

select the_geom,
  to_jsonb(foo) - 'the_geom'::text attributes
from (
  select * from
  segments
) foo

Bien que vous obteniez json au lieu de colonnes individuelles, c'était exactement ce que je voulais. Peut-être que json peut être développé en colonnes individuelles.

14
mlt

La seule façon de le faire (ne dites pas que vous devriez le faire) est d'utiliser des instructions sql dynamiques. Il est facile (comme l'a écrit DrColossos) d'interroger les vues système et de trouver la structure de la table et de créer des instructions appropriées.

PS: Pourquoi voudriez-vous sélectionner toutes/certaines colonnes sans connaître/écrire exactement la structure de votre table?

6
Marian

Dans n commentaire vous expliquez que votre motif est d'avoir la commodité de ne pas afficher le contenu des colonnes avec un contenu long, plutôt que de ne pas afficher la colonne elle-même:

… Parfois, je veux interroger une table avec une colonne géométrique, sans afficher la chaîne de géométrie très longue qui brouille la sortie. Je ne veux pas spécifier toutes les colonnes, car il pourrait y en avoir des dizaines.

C'est possible, à l'aide d'une fonction d'assistance qui remplace le contenu long par null (n'importe quelle colonne text dans mon exemple, mais vous modifieriez cela pour les types que vous souhaitez supprimer):

create table my_table(foo integer, bar integer, baz text);
insert into my_table(foo,bar,baz) values (1,2,'blah blah blah blah blah blah'),(3,4,'blah blah');
select * from my_table;
 foo | bar | baz 
 -: | -: | : ---------------------------- 
 1 | 2 | bla bla bla bla bla bla 
 3 | 4 | bla bla 
create function f(ttype anyelement) returns setof anyelement as
$$
declare
  toid oid;
  tname text;
  nname text;
  cols text;
begin
  --
  select pg_type.oid, pg_namespace.nspname, pg_type.typname
  into toid, nname, tname
  from pg_type join pg_namespace on pg_namespace.oid=pg_type.typnamespace
  where pg_type.oid=pg_typeof(ttype);
  --
  select string_agg((case when data_type<>'text' 
                          then column_name 
                          else 'null::'||data_type||' "'||column_name||'"' end)
                   ,', ' order by ordinal_position)
  into cols
  from information_schema.columns 
  where table_schema=nname and table_name=tname;
  --
  return query execute 'select '||cols||' from '||nname||'.'||tname;
  --
end
$$ language plpgsql;
select * from f(null::my_table);
 foo | bar | baz 
 -: | -: | : --- 
 1 | 2 |  null  
 3 | 4 |  null 

dbfiddle ici

Si votre objectif est de supprimer l'encombrement de l'écran pendant le débogage en n'affichant pas les colonnes avec de grandes valeurs de données, vous pouvez utiliser l'astuce suivante:

(installez le paquet contrib "hstore" si vous ne l'avez pas déjà: "CREATE EXTENSION hstore; ")

Pour un tableau "test" avec col1, col2, col3, vous pouvez définir la valeur de "col2" sur null avant d'afficher:

select (r).* from (select (test #= hstore('col2',null)) as r from test) s;

Ou, définissez deux colonnes sur null avant d'afficher:

select (r).* from (select (test #= hstore('col2',null) #= hstore('col1',null)) as r from test) s;

la mise en garde est que "test" doit être une table (un alias ou une sous-sélection ne fonctionnera pas) car le type d'enregistrement alimentant hstore doit être défini.

3
Sean

Il y a une solution de contournement que je viens de découvrir, mais elle nécessite d'envoyer des requêtes SQL à partir de R. Elle peut être utile aux utilisateurs de R.

Fondamentalement, le package dplyr envoie des requêtes SQL (et spécifiquement PostgreSQL) et accepte l'argument -(column_name).

Ainsi, votre exemple pourrait être écrit comme suit:

select(segments, -(the_geom))
3
Dario Lacan

Dynamiquement comme indiqué ci-dessus est la seule réponse mais je ne le recommanderai pas. Que se passe-t-il si vous ajoutez plus de colonnes à long terme mais qu'elles ne sont pas nécessairement requises pour cette requête?

Vous commenceriez à tirer plus de colonnes que vous n'en avez besoin.

Que faire si la sélection fait partie d'un insert comme dans

Insérer dans le tableauA (col1, col2, col3 .. coln) Sélectionner tout sauf 2 colonnes DU tableauB

La correspondance des colonnes sera incorrecte et votre insertion échouera.

C'est possible, mais je recommande toujours d'écrire chaque colonne nécessaire pour chaque sélection écrite, même si presque chaque colonne est requise.

3
  • Du point de vue de l'application, il s'agit d'une solution paresseuse. Il est peu probable qu'une application sache automatiquement quoi faire avec la ou les nouvelles colonnes.

    Les applications du navigateur de données peuvent interroger les métadonnées pour les données et exclure les colonnes des requêtes en cours d'exécution, ou sélectionner un sous-ensemble des données de la colonne. Les nouveaux BLOB peuvent être exclus lorsqu'ils sont ajoutés. Les données BLOB pour des lignes particulières peuvent être sélectionnées à la demande.

  • Dans toute variante SQL qui prend en charge les requêtes dynamiques, la requête peut être créée à l'aide d'une requête sur les métadonnées des tables. Pour votre intention, j'exclure les colonnes basées sur le type plutôt que sur le nom.

3
BillThor

Vous ne voyez jamais * Dans SQL-VIEWS ... vérifiez \d any_view Dans votre psql. Il existe un (introspectif) prétraitement pour la représentation interne.


Toutes les discussions ici montrent que la proposition de problème (implicite dans la question et les discussions) est un sucre de syntaxe pour les programmeurs, pas un vrai "problème d'optimisation SQL" ... Eh bien, je suppose que c'est pour 80% des programmeurs.

Peut donc être implémenté comme " pré-analyse avec introspection" ... Voir ce que PostgreSQL fait lorsque vous déclarez un SQL-VIEW avec SELECT *: Le constructeur VIEW transforme * Dans une liste de toutes les colonnes (par introspection et au moment où vous exécutez le code source CREATE VIEW).

Implémentation de CREATE VIEW et PREPARE

Il s'agit d'une mise en œuvre viable. Supposons que la table t avec les champs (id serial, name text, the_geom geom).

CREATE VIEW t_full AS SELECT * FROM t;
-- is transformed into SELECT id,name,the_geom FROM t;

CREATE VIEW t_exp_geom AS SELECT * -the_geom FROM t;
-- or other syntax as EXCEPT the_geom
-- Will be transformed into SELECT id,name FROM t;

Idem pour instruction PREPARE .

... donc, c'est possible, et c'est ce dont 80% des programmeurs ont besoin, un sucre de syntaxe pour PREPARE et VIEWS!


NOTE: bien sûr la syntaxe viable n'est peut-être pas - column_name, S'il y a un conflit dans PostgreSQL, nous pouvons donc suggérer EXCEPT column_name,
EXCEPT (column_name1, column_name2, ..., column_nameN) ou autre.

2
Peter Krauss

C'est ma fonction pour sélectionner toutes les colonnes, attendez-en une. J'ai combiné des idées de postgresonline.com et postgresql tuturial et d'autres sources.

CREATE TABLE phonebook(phone VARCHAR(32), firstname VARCHAR(32),
lastname VARCHAR(32), address VARCHAR(64));
INSERT INTO phonebook(phone, firstname, lastname, address) 
VALUES ('+1 123 456 7890', 'John', 'Doe', 'North America'), 
('+1 321 456 7890', 'Matti', 'Meikeläinen', 'Finland'), 
('+1 999 456 7890', 'Maija', 'Meikeläinen', 'Finland'), 
('+9 123 456 7890', 'John', 'Doe', 'Canada'), 
('+1 123 456 7890', 'John', 'Doe', 'Sweden'), 
('+1 123 456 7890', 'John', 'Doe2', 'North America');

drop function all_except_one(text,text);
CREATE OR REPLACE FUNCTION all_except_one(to_remove TEXT, table_name1 TEXT) 
RETURNS void AS $$

 DECLARE 
 rec_row RECORD;
 curs1 refcursor ;

 BEGIN
  --print column names:
  raise notice '%', ('|'|| ARRAY_TO_STRING(ARRAY(SELECT 
  COLUMN_NAME::CHAR(20) FROM INFORMATION_SCHEMA.COLUMNS WHERE
  TABLE_NAME=table_name1 AND COLUMN_NAME NOT IN (to_remove) ), 
  '|') ||'|') ; 

  OPEN curs1 FOR
  EXECUTE 'select table_1  from (SELECT ' || ARRAY_TO_STRING(ARRAY(
  SELECT COLUMN_NAME::VARCHAR(50) FROM INFORMATION_SCHEMA.COLUMNS 
  WHERE TABLE_NAME=table_name1 AND COLUMN_NAME NOT IN (to_remove)    
  ), ', ') || ' FROM ' || table_name1 || ' limit 30)   table_1 ';

  LOOP
  -- fetch row into the rec_row
  FETCH curs1 INTO rec_row;

  -- exit when no more row to fetch
  EXIT WHEN NOT FOUND;

  -- build and print the row output

  raise notice '%',(select'| '|| regexp_replace( array_to_string(
  array_agg(a::char(20)),'|'),'["\(.*\)]+',   '','g') ||'|'  from 
  unnest(string_to_array(replace(replace(replace(trim(rec_row::text,
  '()'),'"',''), ', ','|'),')',' '),',')) as a);

  END LOOP;

  -- Close the cursor

  CLOSE curs1;

  END; $$ LANGUAGE plpgsql;

select  all_except_one('phone','phonebook');

--output:
--NOTICE:  |firstname           |lastname            |address             |
--NOTICE:  | John               |Doe                 |North America       |
--NOTICE:  | Matti              |Meikeläinen         |Finland             |
--NOTICE:  | Maija              |Meikeläinen         |Finland             |
--NOTICE:  | John               |Doe                 |Canada              |
--NOTICE:  | John               |Doe                 |Sweden              |
--NOTICE:  | John               |Doe2                |North America       |
-- all_except_one 
-- ----------------
-- (1 row)
1
Veli-Matti Sorvala