SELEZIONA DISTINCT su più colonne


23

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 una singola colonna o devo creare una funzione per raggiungere questo obiettivo?


7
Intendi SELECT a FROM tablename UNION SELECT b FROM tablename UNION SELECT c FROM tablename UNION SELECT d FROM tablename ;?
ypercubeᵀᴹ

Sì. Ciò farebbe ma dovrei eseguire 4 query. Non sarebbe un collo di bottiglia nelle prestazioni?
Fabrizio Mazzoni,

6
Questa è una query, non 4.
ypercubeᵀᴹ

1
Riesco a vedere diversi modi per scrivere la query che può avere prestazioni diverse, a seconda degli indici disponibili, ecc. Ma non riesco a immaginare come una funzione possa aiutare
ypercubeᵀᴹ

1
OK. Dare una prova conUNION
Fabrizio Mazzoni,

Risposte:


24

Aggiornamento: testate tutte e 5 le query in SQLfiddle con 100K righe (e 2 casi separati, uno con pochi (25) valori distinti e un altro con lotti (circa 25K valori).

Una query molto semplice sarebbe da usare 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 avesse implementato l' ottimizzazione di Scansione indice libera , cosa che non ha fatto. Quindi questa query non sarà efficiente in quanto 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 prima di tutto UNION ALLe poi usarlo DISTINCT. Ciò richiederà anche 4 scansioni di tabelle (e nessun uso di indici). Non cattiva efficienza quando i valori sono pochi e con più valori diventa il più veloce nel mio test (non esteso):

-- 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 usando le funzioni dell'array o la LATERALsintassi. La query 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 order by) e possono essere ulteriormente migliorati rimuovendo i 4 interni distincte lasciando solo quello esterno.


Infine, se - e solo se - i valori distinti delle 4 colonne sono relativamente pochi, puoi usare l' WITH RECURSIVEhack / l'ottimizzazione descritta nella pagina Scansione indice sciolta sopra e usare tutti e 4 gli indici, con un risultato notevolmente veloce! Testato con le stesse righe da 100 K e circa 25 valori distinti distribuiti su 4 colonne (funziona in soli 2 ms!) Mentre con 25 K 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 ;

SQLfiddle


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 sono le migliori.


Aggiunte tardive, una variazione sulla prima query che, nonostante le operazioni extra distinte, offre prestazioni molto migliori rispetto alla prima originale e solo leggermente peggiori 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 ;

12

È possibile utilizzare LATERAL, come in questa query :

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

La parola chiave LATERAL consente al lato destro del join di fare riferimento a oggetti dal lato sinistro. In questo caso, il lato destro è un costruttore VALUES che crea un sottoinsieme a colonna singola a partire dai valori della colonna che si desidera inserire in una singola colonna. La query principale fa semplicemente riferimento alla nuova colonna, applicando anche DISTINCT ad essa.


10

Per essere chiari, usereiunion come suggerisce ypercube , 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;
| inutile |
| : ----- |
| 0 |
| 1 |
| 2 |
| 3 |
| 5 |
| 6 |
| 8 |
| 9 |

dbfiddle qui


7

Più breve

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

Una versione meno dettagliata dell'idea di Andriy è solo leggermente più lunga, ma più elegante e più veloce.
Per molti valori distinti / pochi duplicati:

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

più veloce

Con un indice su ogni colonna coinvolta!
Per pochi valori distinti / molti 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 di rCTE, simile a quella di @ypercube già pubblicata , ma io uso ORDER BY 1 LIMIT 1invece di quella min(a)che è in genere un po 'più veloce. Inoltre non ho bisogno di un predicato aggiuntivo per escludere i valori NULL.
E LATERALinvece di una sottoquery correlata, perché è più pulita (non necessariamente più veloce).

Spiegazione dettagliata nella mia risposta per questa tecnica:

Ho aggiornato il violino SQL di ypercube e ho aggiunto il mio alla playlist.


Puoi provare con EXPLAIN (ANALYZE, TIMING OFF)per verificare le migliori prestazioni complessive? (Il migliore di 5 per escludere effetti di memorizzazione nella cache.)
Erwin Brandstetter,

Interessante. Ho pensato che una virgola sarebbe equivalente a una CROSS JOIN sotto tutti gli aspetti, vale a dire anche in termini di prestazioni. La differenza è specifica nell'uso di LATERAL?
Andriy M,

O forse ho frainteso. Quando hai detto "più veloce" della versione meno dettagliata del mio suggerimento, volevi dire più veloce del mio o più veloce di SELECT DISTINCT con il più inutile?
Andriy M,

1
@AndriyM: la virgola è equivalente (tranne che la sintassi esplicita `CROSS JOIN` si lega più forte quando si risolve la sequenza di join). Sì, voglio dire che la tua idea VALUES ...è più veloce di unnest(ARRAY[...]). LATERALè implicito per le funzioni di restituzione di set FROMnell'elenco.
Erwin Brandstetter,

Grazie per i miglioramenti! Ho provato la variante order / limit-1 ma non c'erano differenze evidenti. L'uso di LATERAL è piuttosto interessante, evitando i molteplici controlli IS NOT NULL, fantastico. Dovresti suggerire questa variante ai ragazzi di Postgres, da aggiungere nella pagina Loose-Index-Scan.
ypercubeᵀᴹ

3

Puoi, ma mentre scrivevo e testavo la funzione mi sentivo male. È uno spreco di risorse.
Basta usare un sindacato e più selezionare. Unico vantaggio (se lo è), una singola scansione dalla tabella principale.

Nel violino sql devi cambiare 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();

In realtà hai ragione perché una funzione userebbe ancora un'unione. In ogni caso +1 per lo sforzo.
Fabrizio Mazzoni,

2
Perché stai facendo questo array e cursore magico? La soluzione di @ ypercube fa il suo lavoro ed è molto semplice inserire una funzione del linguaggio SQL.
dezso,

Spiacenti, impossibile compilare la tua funzione. Probabilmente ho fatto qualcosa di stupido. Se riesci a farlo funzionare qui , ti prego di fornirmi un link e aggiornerò la mia risposta con i risultati, in modo che possiamo confrontare con le altre risposte.
ypercubeᵀᴹ

@ypercube La soluzione modificata deve funzionare. Ricorda di cambiare il separatore in violino. Ho provato sul mio db locale con la creazione della tabella e funziona benissimo.
user_0
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.