เลือก DISTINCT ในหลายคอลัมน์


23

สมมติว่าเรามีตารางที่มีสี่คอลัมน์(a,b,c,d)ของชนิดข้อมูลเดียวกัน

เป็นไปได้หรือไม่ที่จะเลือกค่าที่แตกต่างทั้งหมดภายในข้อมูลในคอลัมน์และส่งกลับเป็นคอลัมน์เดียวหรือฉันต้องสร้างฟังก์ชันเพื่อให้ได้สิ่งนี้?


7
คุณหมายถึงSELECT a FROM tablename UNION SELECT b FROM tablename UNION SELECT c FROM tablename UNION SELECT d FROM tablename ;อะไร
ypercubeᵀᴹ

ใช่. นั่นจะทำ แต่ฉันต้องเรียกใช้แบบสอบถาม 4 ครั้ง มันจะไม่ใช่คอขวดของประสิทธิภาพหรือไม่
Fabrizio Mazzoni

6
นั่นคือข้อความค้นหาหนึ่งข้อไม่ใช่ 4
ypercubeᵀᴹ

1
ฉันสามารถดูวิธีการเขียนแบบสอบถามที่อาจมีประสิทธิภาพแตกต่างกันหลายวิธีขึ้นอยู่กับดัชนีที่มี ฯลฯ แต่ฉันไม่สามารถจินตนาการได้ว่าฟังก์ชันจะช่วยได้อย่างไร
ypercubeᵀᴹ

1
ตกลง. ให้มันเป็นไปด้วยUNION
Fabrizio Mazzoni

คำตอบ:


24

อัปเดต:ทดสอบการค้นหาทั้งหมด 5 รายการในSQLfiddle ที่มีแถว 100K (และ 2 กรณีแยกกันหนึ่งรายการมีค่าที่แตกต่างกันสองสาม (25) ค่าและอีกรายการหนึ่งมีจำนวนมาก (ประมาณ 25K ค่า)

UNION DISTINCTแบบสอบถามที่ง่ายมากที่จะใช้ ฉันคิดว่ามันจะมีประสิทธิภาพมากที่สุดหากมีดัชนีแยกกันในแต่ละสี่คอลัมน์มันจะมีประสิทธิภาพด้วยดัชนีแยกต่างหากในแต่ละคอลัมน์ทั้งสี่ถ้า Postgres ได้ใช้การปรับให้เหมาะสมของดัชนีสแกนแบบหลวมซึ่งมันไม่มี ดังนั้นแบบสอบถามนี้จะไม่มีประสิทธิภาพเนื่องจากต้องใช้การสแกน 4 ตาราง (และไม่มีการใช้ดัชนี):

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

อีกก็จะไปครั้งแรกที่ใช้แล้วUNION ALL DISTINCTสิ่งนี้จะต้องมีการสแกน 4 ตาราง (และไม่ใช้ดัชนี) ประสิทธิภาพไม่เลวเมื่อค่าน้อยและมีค่ามากขึ้นจะกลายเป็นเร็วที่สุดในการทดสอบของฉัน (ไม่ครอบคลุม):

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

คำตอบอื่น ๆ มีให้กับตัวเลือกเพิ่มเติมโดยใช้ฟังก์ชั่นอาร์เรย์หรือLATERALไวยากรณ์ ข้อความค้นหาของแจ็ค ( 187 ms, 261 ms) มีประสิทธิภาพที่สมเหตุสมผล แต่ข้อความค้นหาของ AndriyM นั้นมีประสิทธิภาพมากกว่า ( 125 ms, 155 ms) ทั้งคู่ทำการสแกนตามลำดับหนึ่งตารางและไม่ใช้ดัชนีใด ๆ

ที่จริงแล้วผลการสืบค้นของแจ็คดีกว่าที่แสดงด้านบนเล็กน้อย (ถ้าเราลบออกorder by) และสามารถปรับปรุงให้ดียิ่งขึ้นได้โดยการลบ 4 ภายในdistinctและทิ้งเฉพาะภายนอกเท่านั้น


ท้ายที่สุดถ้า - และหาก - ค่าที่แตกต่างของคอลัมน์ 4 มีค่าค่อนข้างน้อยคุณสามารถใช้WITH RECURSIVEแฮ็ค / การเพิ่มประสิทธิภาพที่อธิบายไว้ในหน้า Loose Index Scan ข้างต้นและใช้ดัชนีทั้ง 4 ด้วยผลลัพธ์ที่รวดเร็วอย่างน่าทึ่ง! ทดสอบด้วยแถว 100K ที่เหมือนกันและค่าที่แตกต่างกันประมาณ 25 ค่าแผ่กระจายไปทั่วทั้ง 4 คอลัมน์ (ทำงานใน 2 มิลลิวินาทีเท่านั้น!) ในขณะที่ค่า 25K แตกต่างกันนั้นช้าที่สุดด้วย 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


เพื่อสรุปเมื่อค่าที่แตกต่างกันน้อยแบบสอบถามแบบเรียกซ้ำเป็นผู้ชนะแน่นอนในขณะที่มีจำนวนมากค่าที่สองของฉันแจ็ค (รุ่นที่ปรับปรุงด้านล่าง) และแบบสอบถามของ AndriyM เป็นนักแสดงที่ดีที่สุด


การเพิ่มเติมล่าช้าการเปลี่ยนแปลงในแบบสอบถามครั้งที่ 1 ซึ่งแม้จะมีการดำเนินการที่แตกต่างกันเป็นพิเศษจะทำงานได้ดีกว่ารุ่นแรกและยิ่งแย่กว่ารุ่นที่ 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 ;

และแจ็คได้รับการปรับปรุง:

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

12

คุณสามารถใช้ LATERAL เหมือนในแบบสอบถามนี้ :

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

คำหลัก LATERAL ช่วยให้ด้านขวาของการเข้าร่วมเพื่ออ้างอิงวัตถุจากด้านซ้าย ในกรณีนี้ทางด้านขวาคือตัวสร้างค่าที่สร้างชุดย่อยคอลัมน์เดี่ยวจากค่าคอลัมน์ที่คุณต้องการใส่ลงในคอลัมน์เดียว แบบสอบถามหลักเพียงแค่อ้างอิงคอลัมน์ใหม่นอกจากนี้ยังใช้ DISTINCT กับมัน


10

เพื่อความชัดเจนฉันจะใช้unionเป็นypercube แนะนำแต่ก็เป็นไปได้ด้วยอาร์เรย์:

select distinct unnest( array_agg(distinct a)||
                        array_agg(distinct b)||
                        array_agg(distinct c)||
                        array_agg(distinct d) )
from t
order by 1;
| ผิดพลาด
| : ----- |
| 0 |
| 1 |
| 2 |
| 3 |
| 5 |
| 6 |
| 8 |
| 9 |

dbfiddle ที่นี่


7

ที่สั้นที่สุด

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

ความคิดที่น้อยกว่าของไอเดียของ Andriy นั้นมีความยาวเพียงเล็กน้อยเท่านั้น แต่มีความสง่างามและรวดเร็วกว่า
สำหรับค่าที่ซ้ำกัน/ ไม่ซ้ำกันจำนวนมาก :

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

ที่เร็วที่สุด

ด้วยดัชนีในแต่ละคอลัมน์ที่เกี่ยวข้อง!
สำหรับไม่กี่ที่แตกต่างกัน / หลายค่าที่ซ้ำกัน:

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;

นี่เป็นอีกหนึ่งตัวแปร rCTE คล้ายกับหนึ่ง@ypercube ที่โพสต์แล้วแต่ฉันใช้ORDER BY 1 LIMIT 1แทนmin(a)ซึ่งโดยทั่วไปแล้วจะเร็วขึ้นเล็กน้อย ฉันยังไม่จำเป็นต้องใช้เพรดิเคตเพิ่มเติมเพื่อยกเว้นค่า NULL
และLATERALแทนที่จะเป็นคำถามย่อยที่สัมพันธ์กันเพราะมันสะอาดกว่า (ไม่จำเป็นต้องเร็วกว่า)

คำอธิบายโดยละเอียดในคำตอบของฉันสำหรับเทคนิคนี้:

ฉันอัปเดตSQL Fiddleของ ypercube และเพิ่มของฉันลงในเพลย์ลิสต์


คุณสามารถทดสอบEXPLAIN (ANALYZE, TIMING OFF)เพื่อตรวจสอบประสิทธิภาพโดยรวมที่ดีที่สุดได้หรือไม่ (ดีที่สุด 5 อันดับเพื่อยกเว้นเอฟเฟ็กต์แคช)
Erwin Brandstetter

น่าสนใจ ฉันคิดว่าการเข้าร่วมด้วยเครื่องหมายจุลภาคจะเทียบเท่ากับ CROSS JOIN ทุกประการเช่นในแง่ของประสิทธิภาพเช่นกัน ความแตกต่างของการใช้ LATERAL นั้นเฉพาะเจาะจงหรือไม่
Andriy M

หรือบางทีฉันเข้าใจผิด เมื่อคุณพูดว่า "เร็วขึ้น" เกี่ยวกับคำแนะนำของฉันในเวอร์ชัน verbose น้อยกว่าคุณหมายถึงเร็วกว่าของฉันหรือเร็วกว่าเลือก DISTINCT ที่ไม่ผิดเพี้ยนหรือไม่
Andriy M

1
@AndriyM: เครื่องหมายจุลภาคเป็นเทียบเท่า (ยกเว้นที่ชัดเจน `CROSS JOIN` ไวยากรณ์ผูกแข็งแกร่งเมื่อการแก้ปัญหาร่วมลำดับ) ใช่ผมหมายถึงความคิดของคุณกับจะเร็วกว่าVALUES ... มีความหมายสำหรับฟังก์ชั่น set-return ในรายการ unnest(ARRAY[...])LATERALFROM
Erwin Brandstetter

ขอบคุณสำหรับการปรับปรุง! ฉันลองใช้คำสั่ง / จำกัด -1 ตัวแปร แต่ไม่มีความแตกต่างที่น่าสังเกต การใช้ LATERAL นั้นยอดเยี่ยมมาก ๆ คุณควรแนะนำตัวแปรนี้ให้กับกลุ่มผู้ชาย Postgres ที่จะเพิ่มในหน้า Loose-Index-Scan
ypercubeᵀᴹ

3

คุณทำได้ แต่เมื่อฉันเขียนและทดสอบฟังก์ชั่นฉันรู้สึกผิด มันเป็นทรัพยากรที่สิ้นเปลือง
เพียงแค่ใช้สหภาพและเลือกเพิ่มเติม ข้อได้เปรียบเท่านั้น (ถ้าเป็น) การสแกนหนึ่งครั้งจากตารางหลัก

ในซอร์ฟแวร์ sql คุณต้องเปลี่ยนตัวคั่นจาก$เป็นอย่างอื่นเช่น/

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

คุณพูดถูกจริงๆเพราะฟังก์ชั่นยังคงใช้สหภาพ ไม่ว่าในกรณีใด +1 สำหรับความพยายาม
Fabrizio Mazzoni

2
ทำไมคุณถึงต้องทำอาเรย์และเคอร์เซอร์นี้? โซลูชันของ @ ypercube ทำงานได้และมันง่ายมากที่จะรวมเข้ากับฟังก์ชันภาษา SQL
dezso

ขออภัยฉันไม่สามารถรวบรวมฟังก์ชั่นของคุณได้ ฉันอาจทำอะไรที่โง่ หากคุณจัดการให้มันทำงานได้ที่นี่โปรดให้ลิงก์กับฉันและฉันจะอัปเดตคำตอบของฉันกับผลลัพธ์เพื่อให้เราสามารถเปรียบเทียบกับคำตอบอื่น ๆ
ypercubeᵀᴹ

@ypercube แก้ไขแล้วต้องทำงาน อย่าลืมเปลี่ยนตัวคั่นในซอ ฉันทดสอบฐานข้อมูลท้องถิ่นของฉันด้วยการสร้างตารางและทำงานได้ดี
user_0
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.