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ąć?


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 ;

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.


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


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.


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

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.