PILIH PERBEDAAN pada beberapa kolom

23

Misalkan kita memiliki tabel dengan empat kolom (a,b,c,d)dari tipe data yang sama.

Apakah mungkin untuk memilih semua nilai yang berbeda di dalam data di kolom dan mengembalikannya sebagai satu kolom atau apakah saya harus membuat fungsi untuk mencapai ini?

Fabrizio Mazzoni
sumber
7
Maksudmu SELECT a FROM tablename UNION SELECT b FROM tablename UNION SELECT c FROM tablename UNION SELECT d FROM tablename ;?
ypercubeᵀᴹ
Iya nih. Itu bisa dilakukan tetapi saya harus menjalankan 4 query. Bukankah itu akan menjadi hambatan kinerja?
Fabrizio Mazzoni
6
Itu satu permintaan, bukan 4.
ypercubeᵀᴹ
1
Saya dapat melihat beberapa cara untuk menulis kueri yang mungkin memiliki kinerja yang berbeda, tergantung pada indeks yang tersedia, dll. Tetapi saya tidak dapat membayangkan bagaimana suatu fungsi akan membantu
ypercubeᵀᴹ
1
BAIK. CobalahUNION
Fabrizio Mazzoni

Jawaban:

24

Pembaruan: Menguji semua 5 kueri dalam SQLfiddle dengan 100K baris (dan 2 kasus terpisah, satu dengan beberapa (25) nilai berbeda dan lainnya dengan banyak (sekitar nilai 25K).

Permintaan yang sangat sederhana adalah menggunakan UNION DISTINCT. Saya pikir akan lebih efisien jika ada indeks terpisah pada masing-masing dari empat kolom. Ini akan efisien dengan indeks terpisah pada masing-masing dari empat kolom, jika Postgres telah menerapkan optimisasi pemindaian indeks longgar , yang belum. Jadi kueri ini tidak akan efisien karena membutuhkan 4 pemindaian tabel (dan tidak ada indeks yang digunakan):

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

Lain akan menjadi pertama UNION ALLdan kemudian digunakan DISTINCT. Ini juga akan membutuhkan 4 scan tabel (dan tidak menggunakan indeks). Bukan efisiensi yang buruk ketika nilainya sedikit, dan dengan lebih banyak nilai menjadi yang tercepat dalam pengujian (tidak luas) saya:

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

Jawaban lain telah memberikan lebih banyak opsi menggunakan fungsi array atau LATERALsintaks. Permintaan Jack ( 187 ms, 261 ms) memiliki kinerja yang masuk akal tetapi permintaan AndriyM tampaknya lebih efisien ( 125 ms, 155 ms). Keduanya melakukan satu pemindaian berurutan dari tabel dan tidak menggunakan indeks apa pun.

Sebenarnya hasil permintaan Jack sedikit lebih baik daripada yang ditunjukkan di atas (jika kita menghapus order by) dan dapat lebih ditingkatkan dengan menghapus 4 internal distinctdan hanya menyisakan yang eksternal.


Akhirnya, jika - dan hanya jika - nilai yang berbeda dari 4 kolom relatif sedikit, Anda dapat menggunakan WITH RECURSIVEretas / optimisasi yang dijelaskan di halaman Pemindaian Indeks Lepas di atas dan menggunakan semua 4 indeks, dengan hasil yang sangat cepat! Diuji dengan baris 100K yang sama dan sekitar 25 nilai berbeda yang tersebar di 4 kolom (berjalan hanya dalam 2 ms!) Sedangkan dengan nilai berbeda 25K itu paling lambat dengan 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


Untuk meringkas, ketika nilai-nilai yang berbeda sedikit, kueri rekursif adalah pemenang mutlak sementara dengan banyak nilai, nilai ke-2 saya, Jack (versi yang ditingkatkan di bawah) dan kueri AndriyM adalah yang berkinerja terbaik.


Penambahan yang terlambat, variasi pada kueri ke-1 yang meskipun memiliki operasi yang sangat berbeda, berkinerja jauh lebih baik daripada yang pertama dan hanya sedikit lebih buruk daripada yang ke-2:

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

dan Jack membaik:

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

Anda dapat menggunakan LATERAL, seperti dalam kueri ini :

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

Kata kunci LATERAL memungkinkan sisi kanan gabungan untuk referensi objek dari sisi kiri. Dalam kasus ini, sisi kanan adalah konstruktor VALUES yang membangun subset satu kolom dari nilai kolom yang ingin Anda masukkan ke dalam satu kolom. Query utama hanya mereferensikan kolom baru, juga menerapkan DISTINCT.

Andriy M
sumber
10

Agar jelas, saya akan menggunakan unionseperti yang disarankan ypercube , tetapi juga dimungkinkan dengan 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;
| paling tidak |
| : ----- |
| 0 |
| 1 |
| 2 |
| 3 |
| 5 |
| 6 |
| 8 |
| 9 |

Aku di sini

Jack Douglas
sumber
7

Terpendek

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

Versi ide Andriy yang kurang jelas hanya sedikit lebih panjang, tetapi lebih elegan dan lebih cepat.
Untuk banyak berbeda / beberapa nilai ganda:

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

Tercepat

Dengan indeks pada setiap kolom yang terlibat!
Untuk beberapa nilai duplikat yang berbeda / banyak :

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;

Ini adalah varian rCTE lain, mirip dengan yang @ypercube sudah diposting , tapi saya menggunakan ORDER BY 1 LIMIT 1bukannya min(a)yang biasanya sedikit lebih cepat. Saya juga tidak memerlukan predikat tambahan untuk mengecualikan nilai NULL.
Dan LATERALbukannya subquery yang berkorelasi, karena lebih bersih (belum tentu lebih cepat).

Penjelasan terperinci dalam jawaban masuk saya untuk teknik ini:

Saya memperbarui SQL Fiddle ypercube dan menambahkan milik saya ke daftar putar.

Erwin Brandstetter
sumber
Bisakah Anda menguji dengan EXPLAIN (ANALYZE, TIMING OFF)untuk memverifikasi kinerja keseluruhan terbaik? (Terbaik dari 5 untuk mengecualikan efek caching.)
Erwin Brandstetter
Menarik. Saya pikir koma join akan setara dengan CROSS JOIN dalam segala hal, yaitu dalam hal kinerja juga. Apakah perbedaannya khusus untuk menggunakan LATERAL?
Andriy M
Atau mungkin saya salah paham. Ketika Anda mengatakan "lebih cepat" tentang versi saran saya yang kurang jelas, apakah maksud Anda lebih cepat daripada milik saya atau lebih cepat daripada SELECT DISTINCT dengan undest?
Andriy M
1
@AndriyM: koma The adalah setara (kecuali bahwa eksplisit `mengikat LINTAS JOIN` sintaks kuat ketika menyelesaikan bergabung urutan). Ya, maksud saya ide Anda VALUES ...lebih cepat daripada unnest(ARRAY[...]). LATERALtersirat untuk fungsi set-return dalam FROMdaftar.
Erwin Brandstetter
Thnx untuk perbaikan! Saya mencoba varian order / limit-1 tetapi tidak ada perbedaan nyata. Menggunakan LATERAL ada yang cukup keren, menghindari beberapa cek BUKAN NULL, bagus. Anda harus menyarankan varian ini kepada orang-orang Postgres, untuk ditambahkan di halaman Loose-Index-Scan.
ypercubeᵀᴹ
3

Anda bisa, tetapi ketika saya menulis dan menguji fungsi saya merasa salah. Ini adalah pemborosan sumber daya.
Cukup gunakan serikat pekerja dan lebih banyak pilih. Hanya keuntungan (jika ya), satu pemindaian tunggal dari tabel utama.

Dalam sql fiddle Anda perlu mengubah pemisah dari $ ke sesuatu yang lain, seperti /

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();
pengguna_0
sumber
Anda sebenarnya benar karena suatu fungsi masih akan menggunakan gabungan. Bagaimanapun, +1 untuk upaya ini.
Fabrizio Mazzoni
2
Mengapa Anda melakukan array dan magic kursor ini? Solusi @ ypercube berfungsi dan sangat mudah untuk menggunakan fungsi bahasa SQL.
dezso
Maaf, saya tidak dapat membuat fungsi Anda untuk dikompilasi. Saya mungkin melakukan sesuatu yang konyol. Jika Anda berhasil membuatnya berfungsi di sini , harap berikan saya tautan dan saya akan memperbarui jawaban saya dengan hasil, sehingga kami dapat membandingkan dengan jawaban lainnya.
ypercubeᵀᴹ
Solusi @ypercube Diedit harus bekerja. Ingatlah untuk mengganti pemisah dengan biola. Saya menguji db lokal saya dengan membuat tabel dan berfungsi dengan baik.
user_0