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?
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 DISTINCT
. Je 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 ;
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 ;
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.
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
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);
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.
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();