SELECT DISTINCT für mehrere Spalten

23

Angenommen, wir haben eine Tabelle mit vier Spalten (a,b,c,d)desselben Datentyps.

Ist es möglich, alle unterschiedlichen Werte innerhalb der Daten in den Spalten auszuwählen und als einzelne Spalte zurückzugeben, oder muss ich eine Funktion erstellen, um dies zu erreichen?

Fabrizio Mazzoni
quelle
7
Du meinst SELECT a FROM tablename UNION SELECT b FROM tablename UNION SELECT c FROM tablename UNION SELECT d FROM tablename ;?
ypercubeᵀᴹ
Ja. Das würde reichen, aber ich müsste 4 Abfragen ausführen. Wäre es nicht ein Leistungsengpass?
Fabrizio Mazzoni
6
Das ist eine Abfrage, nicht 4.
ypercubeᵀᴹ
1
Ich sehe verschiedene Möglichkeiten, um die Abfrage zu schreiben, die je nach verfügbaren Indizes usw. eine unterschiedliche Leistung haben können. Ich kann mir jedoch nicht vorstellen, wie eine Funktion helfen würde
ypercubeᵀᴹ
1
OKAY. Probieren Sie es aus mitUNION
Fabrizio Mazzoni

Antworten:

24

Update: Alle 5 Abfragen in SQLfiddle mit 100K-Zeilen (und 2 separaten Fällen, einer mit wenigen (25) unterschiedlichen Werten und einer mit Lots (ca. 25K-Werten) getestet .

Eine sehr einfache Abfrage wäre zu verwenden UNION DISTINCT. Ich denke, es wäre am effizientesten, wenn es einen separaten Index für jede der vier Spalten gäbe. Es wäre effizient, wenn Postgres eine Optimierung mit losem Index-Scan implementiert hätte , was nicht der Fall ist. Daher ist diese Abfrage nicht effizient, da 4 Scans der Tabelle erforderlich sind (und kein Index verwendet wird):

-- 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 ;

Eine andere wäre erstmal zu UNION ALLbenutzen und dann DISTINCT. Dies erfordert auch 4 Tabellenscans (und keine Verwendung von Indizes). Kein schlechter Wirkungsgrad, wenn die Werte gering sind und mit mehr Werten der schnellste in meinem (nicht umfangreichen) Test wird:

-- 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 ;

Die anderen Antworten bieten mehr Optionen mit Array-Funktionen oder der LATERALSyntax. Jacks Abfrage ( 187 ms, 261 ms) hat eine angemessene Leistung, aber AndriyMs Abfrage scheint effizienter ( 125 ms, 155 ms) zu sein. Beide führen einen sequentiellen Scan der Tabelle durch und verwenden keinen Index.

Tatsächlich sind Jacks Abfrageergebnisse ein bisschen besser als oben gezeigt (wenn wir die entfernen order by) und können weiter verbessert werden, indem die 4 internen entfernt distinctund nur die externe gelassen werden.


Wenn - und nur wenn - die eindeutigen Werte der 4 Spalten relativ gering sind, können Sie den WITH RECURSIVEauf der obigen Seite "Loose Index Scan" beschriebenen Hack / die Optimierung verwenden und alle 4 Indizes mit einem bemerkenswert schnellen Ergebnis verwenden! Getestet mit denselben 100K-Zeilen und ungefähr 25 unterschiedlichen Werten, die auf die 4 Spalten verteilt sind (dauert nur 2 ms!), Während es mit 25K-unterschiedlichen Werten mit 368 ms am langsamsten ist:

-- 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


Zusammenfassend ist die rekursive Abfrage der absolute Gewinner, wenn es nur wenige eindeutige Werte gibt, während die Abfragen von Jack (verbesserte Version unten) und AndriyM mit vielen Werten die besten Ergebnisse erzielen.


Späte Ergänzungen, eine Variation der ersten Abfrage, die trotz der besonders ausgeprägten Operationen viel besser als die ursprüngliche erste und nur geringfügig schlechter als die zweite ist:

-- 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 ;

und Jacks verbessert:

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

Sie könnten LATERAL verwenden, wie in dieser Abfrage :

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

Mit dem Schlüsselwort LATERAL kann die rechte Seite des Joins auf Objekte von der linken Seite verweisen. In diesem Fall ist die rechte Seite ein VALUES-Konstruktor, der aus den Spaltenwerten, die Sie in eine einzelne Spalte einfügen möchten, eine einspaltige Teilmenge erstellt. Die Hauptabfrage verweist einfach auf die neue Spalte und wendet DISTINCT darauf an.

Andriy M
quelle
10

Um es klar auszudrücken , würde ich verwenden, unionwie es ypercube vorschlägt , aber es ist auch mit Arrays möglich:

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 hier

Jack Douglas
quelle
7

Kürzeste

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

Eine weniger ausführliche Version von Andriys Idee ist nur etwas länger, aber eleganter und schneller.
Für viele eindeutige / wenige doppelte Werte:

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

Am schnellsten

Mit einem Index für jede beteiligte Spalte!
Für wenige eindeutige / viele doppelte Werte:

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;

Dies ist eine andere rCTE-Variante, die der bereits veröffentlichten @ ypercube-Variante ähnelt, die ich jedoch verwende, ORDER BY 1 LIMIT 1stattdessen min(a)ist sie in der Regel etwas schneller. Ich brauche auch kein zusätzliches Prädikat, um NULL-Werte auszuschließen.
Und LATERALanstelle einer korrelierten Unterabfrage, weil sie sauberer ist (nicht unbedingt schneller).

Detaillierte Erklärung in meiner Antwort für diese Technik:

Ich habe die SQL-Geige von ypercube aktualisiert und meiner Wiedergabeliste hinzugefügt.

Erwin Brandstetter
quelle
Können Sie mit testen EXPLAIN (ANALYZE, TIMING OFF), um die beste Gesamtleistung zu verifizieren? (Best of 5, um Caching-Effekte auszuschließen.)
Erwin Brandstetter
Interessant. Ich dachte, ein Komma-Join wäre in jeder Hinsicht gleichbedeutend mit einem CROSS JOIN, also auch in Bezug auf die Leistung. Ist der Unterschied spezifisch für die Verwendung von LATERAL?
Andriy M
Oder vielleicht habe ich falsch verstanden. Wenn Sie "schneller" über die weniger ausführliche Version meines Vorschlags sagten, meinten Sie damit schneller als meine oder schneller als die SELECT DISTINCT mit unnest?
Andriy M
1
@AndriyM: Das Komma ist äquivalent (mit der Ausnahme, dass die explizite CROSS JOIN-Syntax beim Auflösen der Join-Sequenz stärker bindet). Ja, ich meine deine Idee mit VALUES ...ist schneller als unnest(ARRAY[...]). LATERAList implizit für set-return-Funktionen in der FROMListe.
Erwin Brandstetter
Danke für die Verbesserungen! Ich habe die Order / Limit-1-Variante ausprobiert, aber es gab keinen merklichen Unterschied. Mit LATERAL ist es ziemlich cool, die mehrfachen IS NOT NULL-Prüfungen zu vermeiden. Sie sollten diese Variante den Postgres-Leuten vorschlagen, um sie auf der Loose-Index-Scan-Seite hinzuzufügen.
ypercubeᵀᴹ
3

Sie können, aber als ich die Funktion schrieb und testete, fühlte ich mich falsch. Es ist eine Verschwendung von Ressourcen.
Verwenden Sie einfach bitte eine Gewerkschaft und wählen Sie mehr aus. Einziger Vorteil (wenn ja), ein einziger Scan vom Haupttisch.

In SQL Fiddle müssen Sie das Trennzeichen von $ in etwas anderes ändern , wie /

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
quelle
Sie haben tatsächlich recht, da eine Funktion immer noch eine Gewerkschaft verwenden würde. In jedem Fall +1 für den Aufwand.
Fabrizio Mazzoni
2
Warum machst du diese Array- und Cursor-Magie? @ ypercubes Lösung erledigt die Arbeit und es ist sehr einfach, sich in eine SQL-Sprachfunktion zu integrieren.
Dezso
Entschuldigung, ich konnte Ihre Funktion nicht kompilieren lassen. Ich habe wahrscheinlich etwas Dummes getan. Wenn Sie es schaffen, dass es hier funktioniert , geben Sie mir bitte einen Link und ich werde meine Antwort mit den Ergebnissen aktualisieren, damit wir sie mit den anderen Antworten vergleichen können.
ypercubeᵀᴹ
@ypercube Die bearbeitete Lösung muss funktionieren. Denken Sie daran, das Trennzeichen in Geige zu ändern. Ich habe meine lokale Datenbank mit Tabellenerstellung getestet und funktioniert einwandfrei.
user_0