SELECT DISTINCT sur plusieurs colonnes

23

Supposons que nous ayons une table avec quatre colonnes (a,b,c,d)du même type de données.

Est-il possible de sélectionner toutes les valeurs distinctes dans les données des colonnes et de les renvoyer comme une seule colonne ou dois-je créer une fonction pour y parvenir?

Fabrizio Mazzoni
la source
7
Tu veux dire SELECT a FROM tablename UNION SELECT b FROM tablename UNION SELECT c FROM tablename UNION SELECT d FROM tablename ;?
ypercubeᵀᴹ
Oui. Cela ferait l'affaire mais je devrais exécuter 4 requêtes. Ne serait-ce pas un goulot d'étranglement en termes de performances?
Fabrizio Mazzoni
6
C'est une requête, pas 4.
ypercubeᵀᴹ
1
Je peux voir plusieurs façons d'écrire la requête qui peuvent avoir des performances différentes, selon les index disponibles, etc. Mais je ne peux pas imaginer comment une fonction pourrait aider
ypercubeᵀᴹ
1
D'ACCORD. Essayez-leUNION
Fabrizio Mazzoni

Réponses:

24

Mise à jour: Test des 5 requêtes dans SQLfiddle avec 100K lignes (et 2 cas séparés, un avec quelques (25) valeurs distinctes et un autre avec beaucoup (environ 25K valeurs).

Une requête très simple serait d'utiliser UNION DISTINCT. Je pense que ce serait plus efficace s'il y avait un index séparé sur chacune des quatre colonnes. Ce serait efficace avec un index séparé sur chacune des quatre colonnes, si Postgres avait implémenté l' optimisation Loose Index Scan , ce qu'il n'a pas. Cette requête ne sera donc pas efficace car elle nécessite 4 analyses de la table (et aucun index n'est utilisé):

-- Query 1. (334 ms, 368ms) 
SELECT a AS abcd FROM tablename 
UNION                           -- means UNION DISTINCT
SELECT b FROM tablename 
UNION 
SELECT c FROM tablename 
UNION 
SELECT d FROM tablename ;

Une autre consisterait à utiliser d'abord UNION ALLet ensuite DISTINCT. Cela nécessitera également 4 analyses de table (et aucune utilisation d'index). Pas mal d'efficacité quand les valeurs sont peu nombreuses, et avec plus de valeurs devient la plus rapide dans mon test (pas extensif):

-- Query 2. (87 ms, 117 ms)
SELECT DISTINCT a AS abcd
FROM
  ( SELECT a FROM tablename 
    UNION ALL 
    SELECT b FROM tablename 
    UNION ALL
    SELECT c FROM tablename 
    UNION ALL
    SELECT d FROM tablename 
  ) AS x ;

Les autres réponses ont fourni plus d'options en utilisant les fonctions de tableau ou la LATERALsyntaxe. La requête de Jack ( 187 ms, 261 ms) a des performances raisonnables, mais la requête d'AndriyM semble plus efficace (125 ms, 155 ms ). Les deux effectuent un balayage séquentiel de la table et n'utilisent aucun index.

En fait, les résultats de la requête de Jack sont un peu meilleurs que ceux indiqués ci-dessus (si nous supprimons le order by) et peuvent être encore améliorés en supprimant les 4 internes distinctet en ne laissant que les externes.


Enfin, si - et seulement si - les valeurs distinctes des 4 colonnes sont relativement peu nombreuses, vous pouvez utiliser le WITH RECURSIVEhack / optimisation décrit dans la page Loose Index Scan ci-dessus et utiliser les 4 index, avec un résultat remarquablement rapide! Testé avec les mêmes 100K lignes et environ 25 valeurs distinctes réparties sur les 4 colonnes (s'exécute en seulement 2 ms!) Tandis qu'avec 25K valeurs distinctes, c'est la plus lente avec 368 ms:

-- Query 3.  (2 ms, 368ms)
WITH RECURSIVE 
    da AS (
       SELECT min(a) AS n  FROM observations
       UNION ALL
       SELECT (SELECT min(a) FROM observations
               WHERE  a > s.n)
       FROM   da AS s  WHERE s.n IS NOT NULL  ),
    db AS (
       SELECT min(b) AS n  FROM observations
       UNION ALL
       SELECT (SELECT min(b) FROM observations
               WHERE  b > s.n)
       FROM   db AS s  WHERE s.n IS NOT NULL  ),
   dc AS (
       SELECT min(c) AS n  FROM observations
       UNION ALL
       SELECT (SELECT min(c) FROM observations
               WHERE  c > s.n)
       FROM   dc AS s  WHERE s.n IS NOT NULL  ),
   dd AS (
       SELECT min(d) AS n  FROM observations
       UNION ALL
       SELECT (SELECT min(d) FROM observations
               WHERE  d > s.n)
       FROM   db AS s  WHERE s.n IS NOT NULL  )
SELECT n 
FROM 
( TABLE da  UNION 
  TABLE db  UNION 
  TABLE dc  UNION 
  TABLE dd
) AS x 
WHERE n IS NOT NULL ;

SQLfiddle


Pour résumer, lorsque les valeurs distinctes sont peu nombreuses, la requête récursive est la gagnante absolue tandis qu'avec beaucoup de valeurs, ma deuxième, celle de Jack (version améliorée ci-dessous) et celle d'AndriyM sont les plus performantes.


Les ajouts tardifs, une variation sur la 1ère requête qui, malgré les opérations supplémentaires distinctes, fonctionne bien mieux que la 1ère originale et seulement légèrement pire que la 2ème:

-- Query 1b.  (85 ms, 149 ms)
SELECT DISTINCT a AS n FROM observations 
UNION 
SELECT DISTINCT b FROM observations 
UNION 
SELECT DISTINCT c FROM observations 
UNION 
SELECT DISTINCT d FROM observations ;

et Jack amélioré:

-- Query 4b.  (104 ms, 128 ms)
select distinct unnest( array_agg(a)||
                        array_agg(b)||
                        array_agg(c)||
                        array_agg(d) )
from t ;
ypercubeᵀᴹ
la source
12

Vous pouvez utiliser LATERAL, comme dans cette requête :

SELECT DISTINCT
  x.n
FROM
  atable
  CROSS JOIN LATERAL (
    VALUES (a), (b), (c), (d)
  ) AS x (n)
;

Le mot clé LATERAL permet au côté droit de la jointure de référencer des objets du côté gauche. Dans ce cas, le côté droit est un constructeur VALUES qui crée un sous-ensemble à colonne unique à partir des valeurs de colonne que vous souhaitez mettre dans une seule colonne. La requête principale fait simplement référence à la nouvelle colonne, en lui appliquant également DISTINCT.

Andriy M
la source
10

Pour être clair, j'utiliserais unioncomme le suggère ypercube , mais c'est également possible avec les tableaux:

select distinct unnest( array_agg(distinct a)||
                        array_agg(distinct b)||
                        array_agg(distinct c)||
                        array_agg(distinct d) )
from t
order by 1;
| unnest |
| : ----- |
| 0 |
| 1 |
| 2 |
| 3 |
| 5 |
| 6 |
| 8 |
| 9 |

dbfiddle ici

Jack Douglas
la source
7

Le plus court

SELECT DISTINCT n FROM observations, unnest(ARRAY[a,b,c,d]) n;

Une version moins verbeuse de l'idée d' Andriy n'est que légèrement plus longue, mais plus élégante et plus rapide.
Pour de nombreux distinctes / quelques valeurs en double:

SELECT DISTINCT n FROM observations, LATERAL (VALUES (a),(b),(c),(d)) t(n);

Le plus rapide

Avec un index sur chaque colonne impliquée!
Pour quelques valeurs distinctes / nombreuses en double:

WITH RECURSIVE
  ta AS (
   (SELECT a FROM observations ORDER BY a LIMIT 1)  -- parentheses required!
   UNION ALL
   SELECT o.a FROM ta t
    , LATERAL (SELECT a FROM observations WHERE a > t.a ORDER BY a LIMIT 1) o
   )
, tb AS (
   (SELECT b FROM observations ORDER BY b LIMIT 1)
   UNION ALL
   SELECT o.b FROM tb t
    , LATERAL (SELECT b FROM observations WHERE b > t.b ORDER BY b LIMIT 1) o
   )
, tc AS (
   (SELECT c FROM observations ORDER BY c LIMIT 1)
   UNION ALL
   SELECT o.c FROM tc t
    , LATERAL (SELECT c FROM observations WHERE c > t.c ORDER BY c LIMIT 1) o
   )
, td AS (
   (SELECT d FROM observations ORDER BY d LIMIT 1)
   UNION ALL
   SELECT o.d FROM td t
    , LATERAL (SELECT d FROM observations WHERE d > t.d ORDER BY d LIMIT 1) o
   )
SELECT a
FROM  (
       TABLE ta
 UNION TABLE tb
 UNION TABLE tc
 UNION TABLE td
 ) sub;

Il s'agit d'une autre variante de rCTE, similaire à celle de @ypercube déjà publiée , mais j'utilise à la ORDER BY 1 LIMIT 1place, min(a)ce qui est généralement un peu plus rapide. Je n'ai également besoin d'aucun prédicat supplémentaire pour exclure les valeurs NULL.
Et LATERALau lieu d'une sous-requête corrélée, car elle est plus propre (pas nécessairement plus rapide).

Explication détaillée dans ma réponse à cette technique:

J'ai mis à jour SQL Fiddle de ypercube et ajouté le mien à la liste de lecture.

Erwin Brandstetter
la source
Pouvez-vous tester avec EXPLAIN (ANALYZE, TIMING OFF)pour vérifier les meilleures performances globales? (Le meilleur de 5 pour exclure les effets de mise en cache.)
Erwin Brandstetter
Intéressant. Je pensais qu'une jointure par virgule serait équivalente à une CROSS JOIN à tous égards, c'est-à-dire en termes de performances également. La différence est-elle spécifique à l'utilisation de LATERAL?
Andriy M
Ou peut-être que j'ai mal compris. Quand vous avez dit "plus vite" à propos de la version moins verbeuse de ma suggestion, vouliez-vous dire plus vite que la mienne ou plus vite que le SELECT DISTINCT avec unnest?
Andriy M
1
@AndriyM: la virgule est équivalente (sauf que la syntaxe explicite `CROSS JOIN` se lie plus fort lors de la résolution de la séquence de jointure). Oui, je veux dire que votre idée avec VALUES ...est plus rapide que unnest(ARRAY[...]). LATERALest implicite pour les fonctions renvoyant un ensemble dans la FROMliste.
Erwin Brandstetter
Merci pour les améliorations! J'ai essayé la variante order / limit-1 mais il n'y avait pas de différence notable. En utilisant LATERAL, il est assez cool, en évitant les multiples contrôles IS NOT NULL, super. Vous devriez suggérer cette variante aux gars de Postgres, à ajouter dans la page Loose-Index-Scan.
ypercubeᵀᴹ
3

Vous pouvez, mais pendant que j'écrivais et testais la fonction, je me sentais mal. C'est un gaspillage de ressources.
Veuillez simplement utiliser une union et plus sélectionner. Seul avantage (si c'est le cas), un seul scan depuis la table principale.

Dans sql fiddle, vous devez changer le séparateur de $ en autre chose, comme /

CREATE TABLE observations (
    id         serial
  , a int not null
  , b int not null
  , c int not null
  , d int not null
  , created_at timestamp
  , foo        text
);

INSERT INTO observations (a, b, c, d, created_at, foo)
SELECT (random() * 20)::int        AS a          -- few values for a,b,c,d
     , (15 + random() * 10)::int 
     , (10 + random() * 10)::int 
     , ( 5 + random() * 20)::int 
     , '2014-01-01 0:0'::timestamp 
       + interval '1s' * g         AS created_at -- ascending (probably like in real life)
     , 'aöguihaophgaduigha' || g   AS foo        -- random ballast
FROM generate_series (1, 10) g;               -- 10k rows

CREATE INDEX observations_a_idx ON observations (a);
CREATE INDEX observations_b_idx ON observations (b);
CREATE INDEX observations_c_idx ON observations (c);
CREATE INDEX observations_d_idx ON observations (d);

CREATE OR REPLACE FUNCTION fn_readuniqu()
  RETURNS SETOF text AS $$
DECLARE
    a_array     text[];
    b_array     text[];
    c_array     text[];
    d_array     text[];
    r       text;
BEGIN

    SELECT INTO a_array, b_array, c_array, d_array array_agg(a), array_agg(b), array_agg(c), array_agg(d)
    FROM observations;

    FOR r IN
        SELECT DISTINCT x
        FROM
        (
            SELECT unnest(a_array) AS x
            UNION
            SELECT unnest(b_array) AS x
            UNION
            SELECT unnest(c_array) AS x
            UNION
            SELECT unnest(d_array) AS x
        ) AS a

    LOOP
        RETURN NEXT r;
    END LOOP;

END;
$$
  LANGUAGE plpgsql STABLE
  COST 100
  ROWS 1000;

SELECT * FROM fn_readuniqu();
user_0
la source
Vous avez en fait raison, car une fonction utiliserait toujours une union. En tout cas +1 pour l'effort.
Fabrizio Mazzoni
2
Pourquoi faites-vous ce tableau et la magie du curseur? La solution de @ ypercube fait le travail et il est très facile d'envelopper dans une fonction de langage SQL.
dezso
Désolé, je n'ai pas pu compiler votre fonction. J'ai probablement fait quelque chose de stupide. Si vous parvenez à le faire fonctionner ici , veuillez me fournir un lien et je mettrai à jour ma réponse avec les résultats, afin que nous puissions comparer avec les autres réponses.
ypercubeᵀᴹ
La solution éditée @ypercube doit fonctionner. N'oubliez pas de changer le séparateur en violon. J'ai testé sur ma base de données locale avec la création de table et fonctionne très bien.
user_0