Wybierz DISTINCT w wielu kolumnach

23

Załóżmy, że mamy tabelę z czterema kolumnami (a,b,c,d)tego samego typu danych.

Czy można wybrać wszystkie odrębne wartości w danych w kolumnach i zwrócić je jako pojedynczą kolumnę, czy muszę utworzyć funkcję, aby to osiągnąć?

Fabrizio Mazzoni
źródło
7
Masz na myśli SELECT a FROM tablename UNION SELECT b FROM tablename UNION SELECT c FROM tablename UNION SELECT d FROM tablename ;?
ypercubeᵀᴹ
Tak. To by wystarczyło, ale musiałbym uruchomić 4 zapytania. Czy nie byłoby to wąskim gardłem wydajności?
Fabrizio Mazzoni
6
To jedno zapytanie, a nie 4.
ypercubeᵀᴹ
1
Widzę kilka sposobów na napisanie zapytania, które może mieć różną wydajność, w zależności od dostępnych indeksów itp. Ale nie mogę sobie wyobrazić, jak funkcja by pomogła
ypercubeᵀᴹ
1
DOBRZE. WypróbujUNION
Fabrizio Mazzoni

Odpowiedzi:

24

Aktualizacja: Przetestowano wszystkie 5 zapytań w SQLfiddle ze 100 000 wierszy (i 2 oddzielnymi przypadkami, jedno z kilkoma (25) odrębnymi wartościami i drugie z partiami (około 25 000 wartości).

Można użyć bardzo prostego zapytania UNION DISTINCT. Myślę, że byłoby najbardziej wydajne, gdyby istniał osobny indeks w każdej z czterech kolumn. Byłoby wydajne z osobnym indeksem w każdej z czterech kolumn, gdyby Postgres zaimplementował optymalizację Loose Index Scan , czego nie ma. To zapytanie nie będzie wydajne, ponieważ wymaga 4 skanów tabeli (i nie jest używany indeks):

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

Innym byłoby najpierw, UNION ALLa następnie użyć DISTINCT. Będzie to również wymagać 4 skanów tabel (i bez użycia indeksów). Niezła wydajność, gdy wartości są nieliczne, a przy większej wartości staje się najszybsza w moim (nie obszernym) teście:

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

Inne odpowiedzi zawierają więcej opcji przy użyciu funkcji tablicowych lub LATERALskładni. Zapytanie Jacka ( 187 ms, 261 ms) ma rozsądną wydajność, ale zapytanie AndriyM wydaje się bardziej wydajne ( 125 ms, 155 ms). Obaj wykonują jeden sekwencyjny skan tabeli i nie używają żadnego indeksu.

W rzeczywistości wyniki zapytania Jacka są nieco lepsze niż pokazano powyżej (jeśli usuniemy order by) i można je ulepszyć, usuwając 4 wewnętrzne distincti pozostawiając tylko zewnętrzne.


Wreszcie, jeśli - i tylko jeśli - odrębne wartości 4 kolumn są względnie nieliczne, możesz użyć WITH RECURSIVEhack / optymalizacji opisanej na powyższej stronie Loose Index Scan i użyć wszystkich 4 indeksów, z wyjątkowo szybkim wynikiem! Testowany z tymi samymi 100 000 wierszami i około 25 odrębnymi wartościami rozłożonymi na 4 kolumny (działa tylko 2 ms!), Natomiast z 25 000 odrębnymi wartościami jest najwolniejszy z 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


Podsumowując, gdy odrębnych wartości jest niewiele, zapytanie rekurencyjne jest absolutnym zwycięzcą, podczas gdy z dużą ilością wartości, moja druga, Jack (poprawiona wersja poniżej) i zapytania AndriyM są najlepsze.


Późne dodawanie, odmiana pierwszego zapytania, które pomimo bardzo wyraźnych operacji, działa znacznie lepiej niż pierwotne pierwsze i tylko nieznacznie gorsze niż drugie:

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

a Jack poprawił:

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

Możesz użyć LATERAL, tak jak w tym zapytaniu :

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

Słowo kluczowe LATERAL pozwala prawej stronie złączenia odwoływać się do obiektów z lewej strony. W tym przypadku po prawej stronie znajduje się konstruktor VALUES, który buduje podzbiór jednokolumnowy z wartości kolumn, które chcesz umieścić w jednej kolumnie. Główne zapytanie po prostu odwołuje się do nowej kolumny, również stosując do niej DISTINCT.

Andriy M.
źródło
10

Dla jasności użyłbym,union jak sugeruje ypercube , ale jest to również możliwe w przypadku tablic:

select distinct unnest( array_agg(distinct a)||
                        array_agg(distinct b)||
                        array_agg(distinct c)||
                        array_agg(distinct d) )
from t
order by 1;
| niespokojny |
| : ----- |
| 0 |
| 1 |
| 2 |
| 3 |
| 5 |
| 6 |
| 8 |
| 9 |

dbfiddle tutaj

Jack Douglas
źródło
7

Najkrótszy

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

Mniej szczegółowa wersja pomysłu Andrija jest tylko nieco dłuższa, ale bardziej elegancka i szybsza.
W przypadku wielu różnych / kilku zduplikowanych wartości:

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

Najszybszy

Z indeksem w każdej zaangażowanej kolumnie!
W przypadku kilku różnych / wielu zduplikowanych wartości:

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;

To kolejny wariant rCTE, podobny do już opublikowanego @ypercube , ale używam go ORDER BY 1 LIMIT 1zamiast tego, min(a)który jest zwykle nieco szybszy. Nie potrzebuję też żadnych dodatkowych predykatów, aby wykluczyć wartości NULL.
I LATERALzamiast skorelowanego podkwerendy, ponieważ jest on czystszy (niekoniecznie szybszy).

Szczegółowe wyjaśnienie w mojej odpowiedzi na tę technikę:

Zaktualizowałem SQL Fiddle w ypercube i dodałem mój do listy odtwarzania.

Erwin Brandstetter
źródło
Czy możesz przetestować, EXPLAIN (ANALYZE, TIMING OFF)aby sprawdzić najlepszą ogólną wydajność? (Najlepsze z 5, aby wykluczyć efekty buforowania.)
Erwin Brandstetter
Ciekawy. Myślałem, że łączenie przecinkiem będzie pod każdym względem równoważne do CROSS JOIN, czyli pod względem wydajności. Czy różnica jest specyficzna dla używania LATERAL?
Andriy M
A może źle zrozumiałem. Kiedy powiedziałeś „szybciej” o mniej szczegółowej wersji mojej sugestii, czy miałeś na myśli szybszą niż moja czy szybszą niż WYBIERZ ODRÓŻNIANIE z nieuczciwością?
Andriy M,
1
@AndriyM: Przecinek jest równoważny (z wyjątkiem tego, że jawna składnia `CROSS JOIN` wiąże się silniej podczas rozwiązywania sekwencji łączenia). Tak, mam na myśli, że twój pomysł VALUES ...jest szybszy niż unnest(ARRAY[...]). LATERALjest niejawny dla funkcji zwracających zestaw na FROMliście.
Erwin Brandstetter
Dziękujemy za ulepszenia! Wypróbowałem wariant order / limit-1, ale nie było żadnej zauważalnej różnicy. Używanie LATERAL jest całkiem fajne, unikanie wielokrotnych czeków IS NOT NULL, świetnie. Powinieneś zasugerować ten wariant chłopakom Postgres, który zostanie dodany na stronie Loose-Index-Scan.
ypercubeᵀᴹ
3

Możesz, ale kiedy napisałem i przetestowałem funkcję, poczułem się źle. To marnotrawstwo zasobów.
Po prostu skorzystaj ze związku i wybierz więcej. Jedyna zaleta (jeśli jest), jedno skanowanie z głównej tabeli.

W sql fiddle musisz zmienić separator z $ na coś innego, takiego jak /

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();
użytkownik_0
źródło
Masz rację, ponieważ funkcja nadal używa związku. W każdym razie +1 za wysiłek.
Fabrizio Mazzoni
2
Dlaczego robisz magię tablic i kursorów? Rozwiązanie @ ypercube działa i bardzo łatwo jest zawinąć w funkcję języka SQL.
dezso
Przepraszamy, nie mogłem zmusić twojej funkcji do kompilacji. Prawdopodobnie zrobiłem coś głupiego. Jeśli uda Ci się, aby działał tutaj , podaj mi link, a ja zaktualizuję swoją odpowiedź o wyniki, abyśmy mogli porównać z innymi odpowiedziami.
ypercubeᵀᴹ
@ypercube Edytowane rozwiązanie musi działać. Pamiętaj, aby zmienić separator na skrzypce. Testowałem na mojej lokalnej bazie danych z tworzeniem tabel i działa dobrze.
użytkownik_0