Birden çok sütun üzerinde DISTINCT SEÇ


23

Diyelim ki (a,b,c,d)aynı veri tipinde dört sütunlu bir tablomuz var .

Sütunlardaki verilerdeki tüm farklı değerleri seçmek ve bunları tek bir sütun olarak döndürmek mümkün mü yoksa bunu elde etmek için bir işlev mi oluşturmalıyım?


7
Demek SELECT a FROM tablename UNION SELECT b FROM tablename UNION SELECT c FROM tablename UNION SELECT d FROM tablename ;istiyorsun
ypercubeᵀᴹ

Evet. Bu olurdu ama 4 sorgu çalıştırmak zorunda kaldım. Performans darboğazı olmaz mıydı?
Fabrizio Mazzoni

6
Bu bir sorgu değil, 4 değil.
ypercubeᵀᴹ

1
Ben, farklı bir performansa sahip olabilir sorguyu yazmak için çeşitli yollar bkz vb mevcut endeksler, bağlı Ama bir işlev yardımcı olacağını nasıl öngörülüyor olamaz olabilir
ypercubeᵀᴹ

1
TAMAM. Onunla gitmekUNION
Fabrizio Mazzoni

Yanıtlar:


24

Güncelleme: SQLfiddle'da 5 sorgunun tümünü 100K satırlarla (ve 2 ayrı durum, biri birkaç (25) farklı değerde, diğeri de lotlu (yaklaşık 25K değer) test etti .

Çok basit bir sorgu kullanmak olacaktır UNION DISTINCT. Dört sütunun her birinde ayrı bir dizin olması en etkili olacağını düşünüyorum. Eğer Postgres'in sahip olmadığı Gevşek İndeks Taraması optimizasyonunu uygulamış olsaydı, dört sütunun her birinde ayrı bir endeks ile verimli olur . Bu nedenle, bu sorgu, tablonun 4 taramasını gerektirdiğinden etkili olmayacaktır (ve dizin kullanılmamaktadır):

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

Başka bir ilk önce UNION ALLkullanmak ve sonra kullanmak olacaktır DISTINCT. Bu aynı zamanda 4 masa taraması gerektirecektir (ve indeks kullanmaz). Değerler az olduğunda verim düşük değil ve daha fazla değere sahip (kapsamlı değil) testimde en hızlı olanı:

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

Diğer cevaplar, dizi işlevlerini veya LATERALsözdizimini kullanarak daha fazla seçenek sağlamıştır . Jack'in sorgusu ( 187 ms, 261 ms) makul bir performansa sahip ancak AndriyM'in sorgusu daha verimli görünüyor ( 125 ms, 155 ms). Her ikisi de tablonun bir sıralı taramasını yapar ve herhangi bir dizin kullanmaz.

Aslında Jack'in sorgu sonuçları yukarıda gösterilenden biraz daha iyidir (eğer kaldırırsak order by) ve 4 tane içini çıkartıp distinctsadece hariciini bırakarak daha da iyileştirilebilir .


Son olarak, eğer - ve sadece - eğer 4 sütununun belirgin değerleri göreceli olarak WITH RECURSIVEazsa, yukarıdaki Gevşek İndeks Tarama sayfasında açıklanan hack / optimizasyonu kullanabilir ve 4 dizinin hepsini de oldukça hızlı bir şekilde kullanabilirsiniz! Aynı 100K satırları ve 4 sütuna yayılmış yaklaşık 25 farklı değerle test edilmiştir (sadece 2 ms'de çalışır!), 25K farklı değerlerle ise 368 ms'de en yavaş değer:

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


Özetlemek gerekirse, farklı değerler az olduğunda özyinelemeli sorgu mutlak kazanırken, birçok değere sahip olduğumda, ikinci olanım, Jack'in (aşağıdaki geliştirilmiş sürüm) ve AndriyM'in sorguları en iyi performans gösterenlerdir.


Ekstra belirgin işlemlere rağmen, ilk sorgudaki bir varyasyon olan geç eklemeler, orijinal 1’den çok daha iyi performans gösterirken,

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

ve Jack gelişmiş:

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

12

Bu sorguda olduğu gibi LATERAL kullanabilirsiniz :

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

LATERAL anahtar sözcüğü, birleşimin sağ tarafının sol taraftan başvuru nesnelerine izin verir. Bu durumda, sağ taraf, tek bir sütuna koymak istediğiniz sütun değerlerinden tek bir sütun altkümesi oluşturan bir VALUES yapıcısıdır. Ana sorgu basitçe DISTINCT de uygulayan yeni sütuna başvurur.


10

Açıkçası, ypercube'un önerdiğiunion gibi kullanırdım , ancak dizilerle de mümkündür:

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 burada


7

En kısa

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

Andriy'nin fikrinin daha az ayrıntılı bir sürümü sadece biraz daha uzun, ama daha zarif ve daha hızlı.
İçin birçok farklı / birkaç yinelenen değerler:

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

En hızlı

Her ilgili sütunda bir indeks ile!
İçin birkaç farklı / birçok yinelenen değerler:

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;

Bu, daha önce yayınlanan bir @ypercube'e benzeyen başka bir rCTE değişkenidir, ancak ORDER BY 1 LIMIT 1bunun yerine kullanıyorum min(a), genellikle biraz daha hızlı. NULL değerleri dışlamak için ek bir yüklemeye de ihtiyacım yok.
Ve LATERALilişkili bir alt sorgu yerine, çünkü daha temiz (zorunlu olarak daha hızlı değil).

Bu tekniğe vereceğim cevapta ayrıntılı açıklama:

Ypercube'un SQL Fiddle'ı güncelledim ve çalma listesine benimkileri ekledim.


En EXPLAIN (ANALYZE, TIMING OFF)iyi genel performansı doğrulamak için test edebilir misiniz ? (Önbellekleme etkilerini dışlamak için
5’in

İlginç. Bir virgül katılımının her bakımdan, yani performans açısından bir CROSS JOIN'e eşdeğer olacağını düşündüm. Fark LATERAL kullanmaya özgü mü?
Andriy M

Ya da belki yanlış anladım. Önerimin daha az ayrıntılı versiyonuyla ilgili "daha hızlı" derken, benimkilerden daha mı hızlı, yoksa SELECT DISTINCT'den daha dürüst değil miydin?
Andriy M

1
@AndriyM: virgül olan eşdeğeri (yani açık Geçmeye JOIN` sözdizimi bağlar hariç sekansı birleştirme çözümlerken güçlü). Evet, senin fikrin ile VALUES ...daha hızlı demek istiyorum unnest(ARRAY[...]). LATERALBu, FROMlistedeki set-return işlevlerinde gizlidir.
Erwin Brandstetter

İyileştirmeler için Thnx! Order / limit-1 değişkenini denedim ancak gözle görülür bir fark yoktu. LATERAL kullanarak, çok sayıdaki IS NOT NULL çeklerden kaçınılarak oldukça harika. Bu değişkeni, Gevşek İndeks-Tarama sayfasına eklenecek Postgres adamlarına önermelisin.
ypercubeᵀᴹ

3

Yapabilirsin, ancak fonksiyonu yazıp test ederken yanlış hissettim. Bu bir kaynak israfıdır.
Lütfen bir sendika kullanın ve daha fazlasını seçin. Yalnızca avantaj (öyleyse), ana tablodan tek bir tarama.

Sql fiddle'da separator'ı $ ' dan başkasına çevirmelisiniz /

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

Aslında bir fonksiyon hala bir sendika kullanır gibi haklısın. Her durumda çaba için +1.
Fabrizio Mazzoni

2
Neden bu dizi ve imleç büyüsünü yapıyorsun? @ ypercube'un çözümü bu işi yapar ve bir SQL dil fonksiyonuna sarılması çok kolaydır.
dezso

Maalesef, işlevinizi derlemek için yapamadım. Muhtemelen aptalca bir şey yaptım. Burada çalışmasını sağlamayı başarırsanız , lütfen bana bir bağlantı verin, ben de cevabımı sonuçlar ile güncelleyeceğim, böylece diğer cevapları karşılaştırabiliriz.
ypercubeᵀᴹ

@ypercube Düzenlenmiş çözüm çalışması gerekir. Ayırıcıyı keman biçiminde değiştirmeyi unutmayın. Yerel db'mde tablo oluşturma ile test ettim ve iyi çalışıyor.
user_0
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.