CHỌN DISTINCT trên nhiều cột


23

Giả sử chúng ta có một bảng có bốn cột (a,b,c,d)cùng loại dữ liệu.

Có thể chọn tất cả các giá trị riêng biệt trong dữ liệu trong các cột và trả về chúng dưới dạng một cột hay tôi phải tạo một hàm để đạt được điều này?


7
Ý bạn là SELECT a FROM tablename UNION SELECT b FROM tablename UNION SELECT c FROM tablename UNION SELECT d FROM tablename ;sao?
ypercubeᵀᴹ

Vâng. Điều đó sẽ làm nhưng tôi phải chạy 4 truy vấn. Nó sẽ không phải là một nút cổ chai hiệu suất?
Fabrizio Mazzoni

6
Đó là một truy vấn, không phải 4.
ypercubeᵀᴹ

1
Tôi có thể thấy một số cách để viết truy vấn có thể có hiệu suất khác nhau, tùy thuộc vào các chỉ mục có sẵn, v.v. Nhưng tôi không thể hình dung được một chức năng sẽ giúp như thế nào
ypercubeᵀᴹ

1
ĐƯỢC. Hãy đồng hành cùngUNION
Fabrizio Mazzoni

Câu trả lời:


24

Cập nhật: Đã kiểm tra tất cả 5 truy vấn trong SQLfiddle với 100K hàng (và 2 trường hợp riêng biệt, một có ít (25) giá trị khác biệt và một truy vấn khác có nhiều (khoảng 25K giá trị).

Một truy vấn rất đơn giản sẽ được sử dụng UNION DISTINCT. Tôi nghĩ sẽ hiệu quả nhất nếu có một chỉ mục riêng biệt cho mỗi trong bốn cột Nó sẽ hiệu quả với một chỉ mục riêng trên mỗi bốn cột, nếu Postgres đã thực hiện tối ưu hóa Quét chỉ mục lỏng lẻo , điều này không có. Vì vậy, truy vấn này sẽ không hiệu quả vì nó yêu cầu 4 lần quét bảng (và không có chỉ mục nào được sử dụng):

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

Một cái khác sẽ là đầu tiên UNION ALLvà sau đó sử dụng DISTINCT. Điều này cũng sẽ yêu cầu 4 lần quét bảng (và không sử dụng chỉ mục). Hiệu quả không tồi khi các giá trị còn ít và với nhiều giá trị trở thành nhanh nhất trong thử nghiệm (không bao quát) của tôi:

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

Các câu trả lời khác đã cung cấp nhiều tùy chọn hơn bằng cách sử dụng các hàm mảng hoặc LATERALcú pháp. Truy vấn của Jack ( 187 ms, 261 ms) có hiệu suất hợp lý nhưng truy vấn của AndriyM có vẻ hiệu quả hơn ( 125 ms, 155 ms). Cả hai đều thực hiện quét tuần tự bảng và không sử dụng bất kỳ chỉ mục nào.

Trên thực tế, kết quả truy vấn của Jack tốt hơn một chút so với hiển thị ở trên (nếu chúng tôi xóa order by) và có thể được cải thiện hơn nữa bằng cách xóa 4 nội bộ distinctvà chỉ để lại bên ngoài.


Cuối cùng, nếu - và chỉ khi - các giá trị riêng biệt của 4 cột tương đối ít, bạn có thể sử dụng WITH RECURSIVEhack / tối ưu hóa được mô tả trong trang Loose Index Scan ở trên và sử dụng cả 4 chỉ mục, với kết quả nhanh đáng kể! Đã thử nghiệm với cùng 100K hàng và khoảng 25 giá trị riêng biệt trải đều trên 4 cột (chỉ chạy trong 2 ms!) Trong khi với 25K giá trị khác biệt, nó chậm nhất với 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


Tóm lại, khi các giá trị riêng biệt còn ít, truy vấn đệ quy là người chiến thắng tuyệt đối trong khi với rất nhiều giá trị, lần thứ 2 của tôi, Jack (phiên bản cải tiến bên dưới) và truy vấn của AndriyM là những người thực hiện tốt nhất.


Bổ sung muộn, một biến thể của truy vấn thứ 1, mặc dù có các hoạt động khác biệt hơn, hoạt động tốt hơn nhiều so với truy vấn ban đầu và chỉ kém hơn một chút so với truy vấn thứ 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 ;

và Jack đã cải thiện:

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

12

Bạn có thể sử dụng LATITH, như trong truy vấn này :

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

Từ khóa LATITH cho phép phía bên phải của phép nối tham chiếu các đối tượng từ phía bên trái. Trong trường hợp này, phía bên phải là một hàm tạo GIÁ TRỊ xây dựng một tập hợp con một cột trong số các giá trị cột bạn muốn đặt vào một cột. Truy vấn chính chỉ cần tham chiếu cột mới, cũng áp dụng DISTINCT cho cột đó.


10

Để rõ ràng, tôi sẽ sử dụng unionnhư ypercube gợi ý , nhưng cũng có thể với các mảng:

select distinct unnest( array_agg(distinct a)||
                        array_agg(distinct b)||
                        array_agg(distinct c)||
                        array_agg(distinct d) )
from t
order by 1;
| không tốt nhất |
| : ----- |
| 0 |
| 1 |
| 2 |
| 3 |
| 5 |
| 6 |
| 8 |
| 9 |

dbfiddle ở đây


7

Ngắn nhất

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

Một phiên bản ít dài hơn của ý tưởng của Irina chỉ dài hơn một chút, nhưng thanh lịch hơn và nhanh hơn.
Đối với nhiều giá trị khác nhau / vài giá trị trùng lặp:

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

Nhanh nhất

Với một chỉ số trên mỗi cột liên quan!
Đối với một vài giá trị khác biệt / nhiều trùng lặp:

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;

Đây là một biến thể rCTE khác, tương tự như một @ypercube đã được đăng , nhưng tôi sử dụng ORDER BY 1 LIMIT 1thay vì min(a)thường nhanh hơn một chút. Tôi cũng không cần thêm biến vị ngữ để loại trừ các giá trị NULL.
LATERALthay vì một truy vấn con tương quan, bởi vì nó sạch hơn (không nhất thiết phải nhanh hơn).

Giải thích chi tiết trong câu trả lời của tôi cho kỹ thuật này:

Tôi đã cập nhật SQL Fiddle của ypercube và thêm tôi vào danh sách phát.


Bạn có thể kiểm tra EXPLAIN (ANALYZE, TIMING OFF)để xác minh hiệu suất tổng thể tốt nhất? (Tốt nhất trong số 5 để loại trừ các hiệu ứng bộ đệm.)
Erwin Brandstetter

Hấp dẫn. Tôi nghĩ rằng một dấu phẩy tham gia sẽ tương đương với CROSS THAM GIA ở mọi khía cạnh, tức là về mặt hiệu suất. Là sự khác biệt cụ thể để sử dụng LATITH?
Andriy M

Hoặc có thể tôi hiểu lầm. Khi bạn nói "nhanh hơn" về phiên bản ít dài dòng hơn của đề xuất của tôi, bạn có nghĩa là nhanh hơn của tôi hoặc nhanh hơn so với CHỌN DISTINCT không cần thiết?
Andriy M

1
@AndriyM: Dấu phẩy tương đương (ngoại trừ cú pháp `CROSS THAM GIA 'rõ ràng liên kết mạnh hơn khi giải quyết chuỗi tham gia). Vâng, ý tôi là ý tưởng của bạn VALUES ...nhanh hơn unnest(ARRAY[...]). LATERALlà ẩn cho các hàm trả về trong FROMdanh sách.
Erwin Brandstetter

Thnx cho những cải tiến! Tôi đã thử biến thể order / giới hạn-1 nhưng không có sự khác biệt đáng chú ý nào. Sử dụng LATITH khá tuyệt, tránh được nhiều kiểm tra ISLL NULL, thật tuyệt. Bạn nên đề xuất biến thể này cho những kẻ Postgres, để được thêm vào trang Loose-Index-Scan.
ypercubeᵀᴹ

3

Bạn có thể, nhưng khi tôi viết và kiểm tra chức năng tôi cảm thấy sai. Đó là một sự lãng phí tài nguyên.
Chỉ cần vui lòng sử dụng một liên minh và lựa chọn nhiều hơn. Chỉ có lợi thế (nếu có), một lần quét từ bảng chính.

Trong fiddle sql, bạn cần thay đổi dấu phân cách từ $ sang thứ khác, như /

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

Bạn thực sự đúng như một chức năng vẫn sẽ sử dụng một liên minh. Trong mọi trường hợp +1 cho nỗ lực.
Fabrizio Mazzoni

2
Tại sao bạn làm ma thuật mảng và con trỏ này? Giải pháp của @ ypercube thực hiện công việc và rất dễ dàng để kết hợp với chức năng ngôn ngữ SQL.
dezso

Xin lỗi, tôi không thể thực hiện chức năng của bạn để biên dịch. Tôi có lẽ đã làm một cái gì đó ngớ ngẩn. Nếu bạn quản lý để nó hoạt động ở đây , vui lòng cung cấp cho tôi một liên kết và tôi sẽ cập nhật câu trả lời của mình với kết quả, để chúng tôi có thể so sánh với các câu trả lời khác.
ypercubeᵀᴹ

@ypercube Giải pháp chỉnh sửa phải hoạt động. Nhớ thay đổi dấu phân cách trong fiddle. Tôi đã thử nghiệm trên db cục bộ của mình với việc tạo bảng và hoạt động tốt.
user_0 29/05/2015
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.