web-dev-qa-db-fra.com

SELECT DISTINCT sur plusieurs colonnes

Supposons que nous ayons un tableau à 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?

23
Fabrizio Mazzoni

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

Une requête très simple serait d'utiliser UNION DISTINCTJe pense que ce serait plus efficace s'il y avait un index séparé sur chacune des quatre colonnes Il serait efficace avec un index séparé sur chacune des quatre colonnes, si Postgres avait implémenté l'optimisation Loose Index Scan , ce qui n'est pas le cas. 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 ;

Un autre serait de commencer par UNION ALL puis utilisez 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 syntaxe LATERAL. 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 peut être encore amélioré en supprimant les 4 distinct internes et 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 RECURSIVE hack/optimisation décrit dans la page ci-dessus Loose Index Scan et utilisez 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 de 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 ;
24
ypercubeᵀᴹ

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.

12
Andriy M

Pour être clair, j'utiliserais union comme ypercube le suggère , mais c'est aussi possible avec des 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

Le plus court

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

Une version moins verbeuse de idée d'Andriy n'est que légèrement plus longue, mais plus élégante et plus rapide.
Pour beaucoup distinct/ peu 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 peu distinct/ beaucoup valeurs 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;

Ceci est une autre variante rCTE, similaire à celle @ ypercube déjà publiée , mais j'utilise ORDER BY 1 LIMIT 1 Au lieu de min(a) 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 LATERAL au lieu d'une sous-requête corrélée, car c'est plus propre (pas nécessairement plus rapide).

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

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

7

Vous pouvez, mais pendant que j'écrivais et testais la fonction, je me sentais mal. C'est un gaspillage de ressources.
Veuillez simplement utiliser un syndicat 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();
3
user_0