SELEZIONA DISTINTO su più colonne
-
26-09-2020 - |
Domanda
Supponiamo di avere una tabella con quattro colonne (a,b,c,d)
dello stesso tipo di dati.
È possibile selezionare tutti i valori distinti all'interno dei dati nelle colonne e restituirli come un'unica colonna o devo creare una funzione per raggiungere questo obiettivo?
Soluzione
Aggiornamento: Testato tutte e 5 le query in SQLfiddle con 100.000 righe (e 2 casi separati, uno con pochi (25) valori distinti e un altro con lotti (circa 25.000 valori).
Una query molto semplice sarebbe quella da utilizzare UNION DISTINCT
. Penso che sarebbe più efficiente se ci fosse un indice separato su ciascuna delle quattro colonne Sarebbe efficiente con un indice separato su ciascuna delle quattro colonne, se Postgres lo avesse implementato Scansione dell'indice sciolto ottimizzazione, cosa che non ha.Quindi questa query non sarà efficiente poiché richiede 4 scansioni della tabella (e non viene utilizzato alcun indice):
-- 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 altro sarebbe il primo UNION ALL
e poi utilizzare DISTINCT
.Ciò richiederà anche 4 scansioni di tabelle (e nessun utilizzo di indici).Non male l'efficienza quando i valori sono pochi, e con più valori diventa il più veloce nel mio (non approfondito) test:
-- 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 ;
Le altre risposte hanno fornito più opzioni utilizzando le funzioni di array o il file LATERAL
sintassi.La domanda di Jack (187 ms, 261 ms
) ha prestazioni ragionevoli ma la query di AndriyM sembra più efficiente (125 ms, 155 ms
).Entrambi eseguono una scansione sequenziale della tabella e non utilizzano alcun indice.
In realtà i risultati della query di Jack sono leggermente migliori di quelli mostrati sopra (se rimuoviamo il file order by
) e può essere ulteriormente migliorato rimuovendo i 4 interni distinct
e lasciando solo quello esterno.
Infine, se - e solo se - i valori distinti delle 4 colonne sono relativamente pochi, puoi utilizzare il file WITH RECURSIVE
hack/ottimizzazione descritti nella pagina Loose Index Scan sopra e utilizza tutti e 4 gli indici, con risultati straordinariamente veloci!Testato con le stesse 100.000 righe e circa 25 valori distinti distribuiti sulle 4 colonne (viene eseguito in soli 2 ms!) mentre con 25.000 valori distinti è il più lento con 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 ;
Per riassumere, quando i valori distinti sono pochi, la query ricorsiva è la vincitrice assoluta mentre con molti valori, la mia seconda, le query di Jack (versione migliorata di seguito) e AndriyM hanno le prestazioni migliori.
Aggiunte tardive, una variazione della prima query che, nonostante le operazioni più distinte, funziona molto meglio della prima query originale e solo leggermente peggiore della seconda:
-- 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 ;
e Jack è migliorato:
-- Query 4b. (104 ms, 128 ms)
select distinct unnest( array_agg(a)||
array_agg(b)||
array_agg(c)||
array_agg(d) )
from t ;
Altri suggerimenti
Puoi usare laterale, come in Questa richiesta :
SELECT DISTINCT
x.n
FROM
atable
CROSS JOIN LATERAL (
VALUES (a), (b), (c), (d)
) AS x (n)
;
.
La parola chiave laterale consente il lato destro del join agli oggetti di riferimento dal lato sinistro.In questo caso, il lato destro è un costruttore di valori che costruisce un sottoinsieme a colonna singola dai valori della colonna che si desidera inserire in una singola colonna.La query principale riferisce semplicemente la nuova colonna, applicando anche distinti ad esso.
Per essere chiari, utilizzerei union
come Ypercube suggerisce , ma è anche possibile con gli array:
..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 |.
dbfidddle qui
il più breve
SELECT DISTINCT n FROM observations, unnest(ARRAY[a,b,c,d]) n;
.
Una versione meno verbosa di L'idea di Andriy è solo leggermente più lunga, ma più elegante e più veloce.
Per molti distinti / pochi valori duplicati :
SELECT DISTINCT n FROM observations, LATERAL (VALUES (a),(b),(c),(d)) t(n);
.
più veloce
con un indice su ciascuna colonna coinvolta!
Per pochi distinti / molti valori duplicati :
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;
.
Questa è un'altra variante RCTE, simile a quella @ypercube già pubblicato , ma uso ORDER BY 1 LIMIT 1
invece di min(a)
che è in genere un po 'più veloce. Non ho anche bisogno di un predicato aggiuntivo per escludere valori nullo.
E LATERAL
invece di una sottoquery correlata, perché è più pulito (non necessariamente più veloce).
Spiegazione dettagliata nel mio go-tows Rispondi per questa tecnica:
Ho aggiornato il SQL Fiddle e ha aggiunto il mio alla playlist.
puoi, ma come ho scritto e testato la funzione che ho sentito sbagliato.È uno spreco di risorse.
solo per favore usa un'unione e più seleziona.Solo vantaggio (se lo è), una singola scansione dalla tabella principale.
In SQL Fiddle è necessario modificare il separatore da $ a qualcos'altro, come /
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();
.