Chọn hàng đầu tiên trong mỗi nhóm THEO NHÓM?


1325

Như tiêu đề cho thấy, tôi muốn chọn hàng đầu tiên của mỗi bộ hàng được nhóm với a GROUP BY.

Cụ thể, nếu tôi có một purchasesbảng trông như thế này:

SELECT * FROM purchases;

Đầu ra của tôi:

id | khách hàng | toàn bộ
--- + ---------- + ------
 1 | Joe | 5
 2 | Sally | 3
 3 | Joe | 2
 4 | Sally | 1

Tôi muốn truy vấn idmua hàng lớn nhất ( total) được thực hiện bởi mỗi người customer. Một cái gì đó như thế này:

SELECT FIRST(id), customer, FIRST(total)
FROM  purchases
GROUP BY customer
ORDER BY total DESC;

Đầu ra dự kiến:

ĐẦU TIÊN (id) | khách hàng | ĐẦU TIÊN (tổng cộng)
---------- + ---------- + -------------
        1 | Joe | 5
        2 | Sally | 3

vì bạn chỉ tìm kiếm mỗi cái lớn nhất, tại sao không truy vấn MAX(total)?
phil294

4
@ phil294 truy vấn tối đa (tổng cộng) sẽ không liên kết tổng số đó với giá trị 'id' của hàng mà nó xảy ra.
gwideman

Câu trả lời:


1116

Trên Oracle 9.2+ (không phải 8i + như đã nêu ban đầu), SQL Server 2005+, PostgreQuery 8.4+, DB2, Firebird 3.0+, Teradata, Sybase, Vertica:

WITH summary AS (
    SELECT p.id, 
           p.customer, 
           p.total, 
           ROW_NUMBER() OVER(PARTITION BY p.customer 
                                 ORDER BY p.total DESC) AS rk
      FROM PURCHASES p)
SELECT s.*
  FROM summary s
 WHERE s.rk = 1

Được hỗ trợ bởi bất kỳ cơ sở dữ liệu:

Nhưng bạn cần thêm logic để phá vỡ mối quan hệ:

  SELECT MIN(x.id),  -- change to MAX if you want the highest
         x.customer, 
         x.total
    FROM PURCHASES x
    JOIN (SELECT p.customer,
                 MAX(total) AS max_total
            FROM PURCHASES p
        GROUP BY p.customer) y ON y.customer = x.customer
                              AND y.max_total = x.total
GROUP BY x.customer, x.total

2
Informix 12.x cũng hỗ trợ các chức năng của cửa sổ (CTE cần phải được chuyển đổi thành bảng dẫn xuất). Và Firebird 3.0 cũng sẽ hỗ trợ các chức năng Window
a_horse_with_no_name

37
ROW_NUMBER() OVER(PARTITION BY [...])cùng với một số tối ưu hóa khác đã giúp tôi có được một truy vấn giảm từ 30 giây xuống vài mili giây. Cảm ơn! (PostgreSQL 9.2)
Sam

8
Nếu có nhiều giao dịch mua với mức cao nhất totalcho một khách hàng, truy vấn thứ nhất trả về một người chiến thắng tùy ý (tùy thuộc vào chi tiết triển khai; idcó thể thay đổi cho mỗi lần thực hiện!). Thông thường (không phải luôn luôn) bạn sẽ muốn một hàng cho mỗi khách hàng, được xác định bởi các tiêu chí bổ sung như "hàng có giá trị nhỏ nhất id". Để sửa chữa, hãy thêm idvào ORDER BYdanh sách row_number(). Sau đó, bạn nhận được kết quả tương tự như với truy vấn thứ 2 , rất không hiệu quả cho trường hợp này. Ngoài ra, bạn cần một truy vấn con khác cho mỗi cột bổ sung.
Erwin Brandstetter

2
Google BigQuery cũng hỗ trợ lệnh ROW_NUMBER () của truy vấn đầu tiên. Làm việc như một cơ duyên đối với chúng tôi
Praxiteles

2
Lưu ý rằng phiên bản đầu tiên có chức năng cửa sổ hoạt động như phiên bản SQLite 3.25.0: sqlite.org/windowfifts.html#history
brianz

1149

Trong PostgreSQL, việc này thường đơn giản và nhanh hơn (tối ưu hóa hiệu suất nhiều hơn bên dưới):

SELECT DISTINCT ON (customer)
       id, customer, total
FROM   purchases
ORDER  BY customer, total DESC, id;

Hoặc ngắn hơn (nếu không rõ ràng) với số thứ tự của cột đầu ra:

SELECT DISTINCT ON (2)
       id, customer, total
FROM   purchases
ORDER  BY 2, 3 DESC, 1;

Nếu totalcó thể là NULL (sẽ không bị tổn thương theo cách nào, nhưng bạn sẽ muốn khớp với các chỉ mục hiện có ):

...
ORDER  BY customer, total DESC NULLS LAST, id;

Những điểm chính

  • DISTINCT ONlà một phần mở rộng PostgreSQL của tiêu chuẩn (chỉ có DISTINCTtrong toàn bộ SELECTdanh sách được xác định).

  • Liệt kê bất kỳ số lượng biểu thức trong DISTINCT ONmệnh đề, giá trị hàng kết hợp xác định trùng lặp. Hướng dẫn sử dụng:

    Rõ ràng, hai hàng được coi là khác biệt nếu chúng khác nhau về ít nhất một giá trị cột. Giá trị Null được coi là bằng nhau trong so sánh này.

    Nhấn mạnh đậm của tôi.

  • DISTINCT ONcó thể kết hợp với ORDER BY. Các biểu thức dẫn đầu ORDER BYphải nằm trong tập hợp các biểu thức trong DISTINCT ON, nhưng bạn có thể sắp xếp lại thứ tự giữa các biểu thức đó một cách tự do. Thí dụ. Bạn có thể thêm các biểu thức bổ sungORDER BY để chọn một hàng cụ thể từ mỗi nhóm đồng nghiệp. Hoặc, như hướng dẫn đặt nó :

    (Các) DISTINCT ONbiểu thức phải khớp với ORDER BY (các) biểu thức ngoài cùng bên trái . Các ORDER BYđiều khoản thông thường sẽ chứa thêm biểu thức (s) để xác định ưu tiên mong muốn các hàng trong mỗi DISTINCT ONnhóm.

    Tôi đã thêm vào idnhư mục cuối cùng để phá vỡ mối quan hệ:
    "Chọn hàng có giá trị nhỏ nhất idtừ mỗi nhóm chia sẻ mức cao nhất total".

    Để sắp xếp kết quả theo cách không đồng ý với thứ tự sắp xếp xác định đầu tiên cho mỗi nhóm, bạn có thể lồng trên truy vấn trong một truy vấn bên ngoài với truy vấn khác ORDER BY. Thí dụ.

  • Nếu totalcó thể là NULL, rất có thể bạn muốn hàng có giá trị khác không lớn nhất. Thêm NULLS LASTnhư thể hiện. Xem:

  • Các SELECTdanh sách không bị hạn chế bởi các biểu thức trong DISTINCT ONhoặc ORDER BYtrong bất kỳ cách nào. (Không cần thiết trong trường hợp đơn giản ở trên):

    • Bạn không cần phải bao gồm bất kỳ biểu thức nào trong DISTINCT ONhoặc ORDER BY.

    • Bạn có thể bao gồm bất kỳ biểu thức khác trong SELECTdanh sách. Đây là công cụ để thay thế các truy vấn phức tạp hơn nhiều bằng các truy vấn con và các hàm tổng hợp / cửa sổ.

  • Tôi đã thử nghiệm với Postgres phiên bản 8.3 - 12. Nhưng tính năng đã có ở đó ít nhất là từ phiên bản 7.1, vì vậy về cơ bản là luôn luôn.

Mục lục

Các hoàn hảo chỉ mục cho các truy vấn trên sẽ là một chỉ số đa cột kéo dài cả ba cột trong phù hợp với trình tự và với phù hợp với thứ tự sắp xếp:

CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id);

Có thể là quá chuyên ngành. Nhưng sử dụng nó nếu hiệu suất đọc cho truy vấn cụ thể là rất quan trọng. Nếu bạn có DESC NULLS LASTtrong truy vấn, hãy sử dụng tương tự trong chỉ mục để sắp xếp thứ tự khớp và chỉ mục được áp dụng.

Hiệu quả / Tối ưu hóa hiệu suất

Cân nhắc chi phí và lợi ích trước khi tạo các chỉ mục phù hợp cho từng truy vấn. Tiềm năng của chỉ số trên phần lớn phụ thuộc vào phân phối dữ liệu .

Chỉ mục được sử dụng vì nó cung cấp dữ liệu được sắp xếp trước. Trong Postgres 9.2 trở lên, truy vấn cũng có thể được hưởng lợi từ chỉ mục chỉ quét nếu chỉ mục nhỏ hơn bảng bên dưới. Chỉ số phải được quét toàn bộ, mặc dù.

Điểm chuẩn

Tôi đã có một điểm chuẩn đơn giản ở đây đã lỗi thời. Tôi đã thay thế nó bằng một điểm chuẩn chi tiết trong câu trả lời riêng biệt này .


28
Đây là một câu trả lời tuyệt vời cho hầu hết các kích thước cơ sở dữ liệu, nhưng tôi muốn chỉ ra rằng khi bạn tiếp cận ~ triệu hàng DISTINCT ONtrở nên cực kỳ chậm. Việc triển khai luôn sắp xếp toàn bộ bảng và quét qua nó để tìm các bản sao, bỏ qua tất cả các chỉ mục (ngay cả khi bạn đã tạo chỉ mục nhiều cột cần thiết). Xem Explextends.com/2009/05/03/postgresql-optimizing-distinc để biết giải pháp có thể.
Meekohi

14
Sử dụng các lệnh để "làm cho mã ngắn hơn" là một ý tưởng tồi tệ. Làm thế nào về việc để lại tên cột để làm cho nó dễ đọc?
KOTJMF

13
@KOTJMF: Tôi khuyên bạn nên đi với sở thích cá nhân của bạn sau đó. Tôi chứng minh cả hai lựa chọn để giáo dục. Tốc ký cú pháp có thể hữu ích cho các biểu thức dài trong SELECTdanh sách.
Erwin Brandstetter

1
@jangorecki: Điểm chuẩn ban đầu là từ năm 2011, tôi không còn thiết lập nữa. Nhưng đã đến lúc chạy thử nghiệm với pg 9,4 và pg 9,5. Xem chi tiết trong câu trả lời thêm. . Bạn có thể thêm một bình luận với kết quả từ cài đặt của bạn dưới đây?
Erwin Brandstetter

2
@PirateApp: Không phải từ đỉnh đầu của tôi. DISTINCT ONchỉ tốt khi nhận được một hàng cho mỗi nhóm đồng nghiệp.
Erwin Brandstetter

134

Điểm chuẩn

Kiểm tra hầu hết các thí sinh thú vị với Postgres 9.49,5 với một bảng nửa đường thực tế của 200k hàng trong purchases10k biệtcustomer_id ( trung bình. 20 hàng mỗi khách hàng ).

Đối với Postgres 9.5, tôi đã chạy thử nghiệm lần 2 với 86446 khách hàng khác biệt. Xem bên dưới ( trung bình 2,3 hàng cho mỗi khách hàng ).

Thiết lập

Bàn chính

CREATE TABLE purchases (
  id          serial
, customer_id int  -- REFERENCES customer
, total       int  -- could be amount of money in Cent
, some_column text -- to make the row bigger, more realistic
);

Tôi sử dụng một serial(ràng buộc PK được thêm vào bên dưới) và một số nguyên customer_idvì đó là một thiết lập điển hình hơn. Cũng được thêm vào some_columnđể bù cho các cột thường hơn.

Dữ liệu giả, PK, chỉ mục - một bảng thông thường cũng có một số bộ dữ liệu chết:

INSERT INTO purchases (customer_id, total, some_column)    -- insert 200k rows
SELECT (random() * 10000)::int             AS customer_id  -- 10k customers
     , (random() * random() * 100000)::int AS total     
     , 'note: ' || repeat('x', (random()^2 * random() * random() * 500)::int)
FROM   generate_series(1,200000) g;

ALTER TABLE purchases ADD CONSTRAINT purchases_id_pkey PRIMARY KEY (id);

DELETE FROM purchases WHERE random() > 0.9; -- some dead rows

INSERT INTO purchases (customer_id, total, some_column)
SELECT (random() * 10000)::int             AS customer_id  -- 10k customers
     , (random() * random() * 100000)::int AS total     
     , 'note: ' || repeat('x', (random()^2 * random() * random() * 500)::int)
FROM   generate_series(1,20000) g;  -- add 20k to make it ~ 200k

CREATE INDEX purchases_3c_idx ON purchases (customer_id, total DESC, id);

VACUUM ANALYZE purchases;

customer bảng - cho truy vấn cấp trên

CREATE TABLE customer AS
SELECT customer_id, 'customer_' || customer_id AS customer
FROM   purchases
GROUP  BY 1
ORDER  BY 1;

ALTER TABLE customer ADD CONSTRAINT customer_customer_id_pkey PRIMARY KEY (customer_id);

VACUUM ANALYZE customer;

Trong thử nghiệm thứ hai của tôi cho 9.5 tôi đã sử dụng cùng một thiết lập, nhưng với random() * 100000để tạo ra customer_idchỉ nhận được vài hàng mỗi customer_id.

Kích thước đối tượng cho bảng purchases

Tạo với truy vấn này .

               what                | bytes/ct | bytes_pretty | bytes_per_row
-----------------------------------+----------+--------------+---------------
 core_relation_size                | 20496384 | 20 MB        |           102
 visibility_map                    |        0 | 0 bytes      |             0
 free_space_map                    |    24576 | 24 kB        |             0
 table_size_incl_toast             | 20529152 | 20 MB        |           102
 indexes_size                      | 10977280 | 10 MB        |            54
 total_size_incl_toast_and_indexes | 31506432 | 30 MB        |           157
 live_rows_in_text_representation  | 13729802 | 13 MB        |            68
 ------------------------------    |          |              |
 row_count                         |   200045 |              |
 live_tuples                       |   200045 |              |
 dead_tuples                       |    19955 |              |

Truy vấn

1. row_number()trong CTE, ( xem câu trả lời khác )

WITH cte AS (
   SELECT id, customer_id, total
        , row_number() OVER(PARTITION BY customer_id ORDER BY total DESC) AS rn
   FROM   purchases
   )
SELECT id, customer_id, total
FROM   cte
WHERE  rn = 1;

2. row_number()trong truy vấn con (tối ưu hóa của tôi)

SELECT id, customer_id, total
FROM   (
   SELECT id, customer_id, total
        , row_number() OVER(PARTITION BY customer_id ORDER BY total DESC) AS rn
   FROM   purchases
   ) sub
WHERE  rn = 1;

3. DISTINCT ON( xem câu trả lời khác )

SELECT DISTINCT ON (customer_id)
       id, customer_id, total
FROM   purchases
ORDER  BY customer_id, total DESC, id;

4. rCTE với LATERALtruy vấn con ( xem tại đây )

WITH RECURSIVE cte AS (
   (  -- parentheses required
   SELECT id, customer_id, total
   FROM   purchases
   ORDER  BY customer_id, total DESC
   LIMIT  1
   )
   UNION ALL
   SELECT u.*
   FROM   cte c
   ,      LATERAL (
      SELECT id, customer_id, total
      FROM   purchases
      WHERE  customer_id > c.customer_id  -- lateral reference
      ORDER  BY customer_id, total DESC
      LIMIT  1
      ) u
   )
SELECT id, customer_id, total
FROM   cte
ORDER  BY customer_id;

5. customerbảng với LATERAL( xem tại đây )

SELECT l.*
FROM   customer c
,      LATERAL (
   SELECT id, customer_id, total
   FROM   purchases
   WHERE  customer_id = c.customer_id  -- lateral reference
   ORDER  BY total DESC
   LIMIT  1
   ) l;

6. array_agg()với ORDER BY( xem câu trả lời khác )

SELECT (array_agg(id ORDER BY total DESC))[1] AS id
     , customer_id
     , max(total) AS total
FROM   purchases
GROUP  BY customer_id;

Các kết quả

Thời gian thực hiện cho các truy vấn trên với EXPLAIN ANALYZE(và tất cả các tùy chọn tắt ), tốt nhất trong 5 lần chạy .

Tất cả các truy vấn đã sử dụng một Index Chỉ Quét trên purchases2_3c_idx(trong số những bước khác). Một số trong số họ chỉ cho kích thước nhỏ hơn của chỉ số, những người khác hiệu quả hơn.

A. Postgres 9,4 với 200k hàng và ~ 20 mỗi customer_id

1. 273.274 ms  
2. 194.572 ms  
3. 111.067 ms  
4.  92.922 ms  
5.  37.679 ms  -- winner
6. 189.495 ms

B. Tương tự với Postgres 9.5

1. 288.006 ms
2. 223.032 ms  
3. 107.074 ms  
4.  78.032 ms  
5.  33.944 ms  -- winner
6. 211.540 ms  

C. Giống như B., nhưng với ~ 2,3 hàng mỗi customer_id

1. 381.573 ms
2. 311.976 ms
3. 124.074 ms  -- winner
4. 710.631 ms
5. 311.976 ms
6. 421.679 ms

Điểm chuẩn liên quan

Dưới đây là một thử nghiệm mới bằng cách "ogr" thử nghiệm với 10 triệu hàng và 60k "khách hàng" duy nhất trên Postgres 11,5 (hiện tại kể từ tháng 9 năm 2019). Kết quả vẫn phù hợp với những gì chúng ta đã thấy cho đến nay:

Điểm chuẩn gốc (lỗi thời) từ năm 2011

Tôi đã chạy ba thử nghiệm với PostgreSQL 9.1 trên bảng thực tế gồm 65579 hàng và chỉ mục btree cột đơn trên mỗi ba cột có liên quan và mất thời gian thực hiện tốt nhất trong 5 lần chạy.
So sánh truy vấn đầu tiên của @OMGPonies ( A) với giải pháp trênDISTINCT ON ( B):

  1. Chọn toàn bộ bảng, kết quả trong 5958 hàng trong trường hợp này.

    A: 567.218 ms
    B: 386.673 ms
  2. Sử dụng điều kiện WHERE customer BETWEEN x AND ydẫn đến 1000 hàng.

    A: 249.136 ms
    B:  55.111 ms
  3. Chọn một khách hàng duy nhất với WHERE customer = x.

    A:   0.143 ms
    B:   0.072 ms

Kiểm tra tương tự lặp lại với chỉ số được mô tả trong câu trả lời khác

CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id);

1A: 277.953 ms  
1B: 193.547 ms

2A: 249.796 ms -- special index not used  
2B:  28.679 ms

3A:   0.120 ms  
3B:   0.048 ms

5
Cảm ơn cho một điểm chuẩn tuyệt vời. Tôi đã tự hỏi nếu truy vấn dữ liệu sự kiện trong đó bạn có dấu thời gian thay vì tổng số sẽ được hưởng lợi từ chỉ số BRIN mới. Điều này có khả năng cung cấp tốc độ cho các truy vấn tạm thời.
jangorecki

3
@jangorecki: Bất kỳ bảng lớn nào có dữ liệu được sắp xếp vật lý đều có thể thu lợi từ chỉ số BRIN.
Erwin Brandstetter

@ErwinBrandstetter Trong 2. row_number()5. customer table with LATERAL ví dụ, điều gì đảm bảo id sẽ nhỏ nhất?
Artem Novikov

@ArtemNovikov: Không có gì. Mục tiêu là để lấy, trên mỗi customer_id hàng với mức cao nhất total. Đó là một sự trùng hợp sai lệch trong dữ liệu kiểm tra của câu hỏi rằng idtrong các hàng được chọn xảy ra cũng là nhỏ nhất trên mỗi customer_id.
Erwin Brandstetter

1
@ArtemNovikov: Để cho phép quét chỉ mục.
Erwin Brandstetter

55

Điều này là phổ biến vấn đề, đã có thử nghiệm tốt và giải pháp tối ưu hóa cao . Cá nhân tôi thích giải pháp tham gia trái của Bill Karwin ( bài gốc với rất nhiều giải pháp khác ).

Lưu ý rằng một loạt các giải pháp cho vấn đề phổ biến này có thể được tìm thấy một cách đáng ngạc nhiên trong một trong những nguồn chính thức nhất, hướng dẫn sử dụng MySQL ! Xem ví dụ về các truy vấn phổ biến :: Các hàng giữ mức tối đa theo nhóm của một cột nhất định .


22
Làm thế nào là hướng dẫn sử dụng MySQL theo bất kỳ cách nào "chính thức" cho các câu hỏi Postgres / SQLite (không đề cập đến SQL)? Ngoài ra, để rõ ràng, DISTINCT ONphiên bản ngắn hơn nhiều, đơn giản hơn và thường hoạt động tốt hơn trong Postgres so với các lựa chọn thay thế với chế độ tự LEFT JOINhoặc bán chống tham gia NOT EXISTS. Nó cũng là "thử nghiệm tốt".
Erwin Brandstetter

3
Ngoài ra với những gì Erwin đã viết, tôi nói rằng việc sử dụng một hàm cửa sổ (là chức năng SQL phổ biến hiện nay) hầu như luôn nhanh hơn so với sử dụng phép nối với bảng dẫn xuất
a_horse_with_no_name

6
Tài liệu tham khảo tuyệt vời. Tôi không biết đây được gọi là vấn đề lớn nhất của mỗi nhóm. Cảm ơn bạn.
David Mann

Câu hỏi không dành cho n lớn nhất trong mỗi nhóm mà là n đầu tiên .
Revierpost

1
Trong trường hợp hai trường theo thứ tự tôi đã thử, "giải pháp tham gia trái của Bill Karwin" cho hiệu suất kém. Xem bình luận của tôi dưới đây stackoverflow.com/a/8749095/684229
Johnny Wong

30

Trong Postgres bạn có thể sử dụng array_aggnhư thế này:

SELECT  customer,
        (array_agg(id ORDER BY total DESC))[1],
        max(total)
FROM purchases
GROUP BY customer

Điều này sẽ cung cấp cho bạn id mua hàng lớn nhất của mỗi khách hàng.

Một số điều cần lưu ý:

  • array_agg là một hàm tổng hợp, vì vậy nó hoạt động với GROUP BY .
  • array_aggcho phép bạn chỉ định một thứ tự trong phạm vi chính nó, vì vậy nó không ràng buộc cấu trúc của toàn bộ truy vấn. Ngoài ra còn có cú pháp cho cách bạn sắp xếp NULL, nếu bạn cần làm gì đó khác với mặc định.
  • Khi chúng ta xây dựng mảng, chúng ta lấy phần tử đầu tiên. (Mảng Postgres là 1 chỉ mục, không phải 0 chỉ mục).
  • Bạn có thể sử dụng array_aggtheo cách tương tự cho cột đầu ra thứ ba của mình, nhưng max(total)đơn giản hơn.
  • Không giống như DISTINCT ON, sử dụng array_aggcho phép bạn giữ của bạn GROUP BY, trong trường hợp bạn muốn điều đó vì lý do khác.

14

Giải pháp này không hiệu quả như Erwin đã chỉ ra, vì sự hiện diện của SubQs

select * from purchases p1 where total in
(select max(total) from purchases where p1.customer=customer) order by total desc;

Cảm ơn, có đồng ý với bạn, việc tham gia giữa subq và truy vấn bên ngoài thực sự mất nhiều thời gian hơn. "Trong" sẽ không là vấn đề ở đây vì subq sẽ chỉ dẫn đến một hàng. BTW, lỗi cú pháp nào bạn đang chỉ vào ??
dùng2407394

ohh .. được sử dụng để "Teradata" .. được chỉnh sửa ngay bây giờ..tất cả các mối quan hệ không cần thiết ở đây vì nó cần tìm tổng số cao nhất cho mỗi khách hàng ..
user2407394

Bạn có biết rằng bạn nhận được nhiều hàng cho một khách hàng trong trường hợp hòa không? Cho dù đó là mong muốn phụ thuộc vào yêu cầu chính xác. Thông thường, nó không phải là. Đối với câu hỏi trong tầm tay, tiêu đề khá rõ ràng.
Erwin Brandstetter

Điều này không rõ ràng từ câu hỏi, nếu cùng một khách hàng có mua = Tối đa cho 2 id khác nhau, tôi nghĩ chúng ta nên hiển thị cả hai.
dùng2407394

10

Tôi sử dụng theo cách này (chỉ postgresql): https://wiki.postgresql.org/wiki/First/last_%28aggregate%29

-- Create a function that always returns the first non-NULL item
CREATE OR REPLACE FUNCTION public.first_agg ( anyelement, anyelement )
RETURNS anyelement LANGUAGE sql IMMUTABLE STRICT AS $$
        SELECT $1;
$$;

-- And then wrap an aggregate around it
CREATE AGGREGATE public.first (
        sfunc    = public.first_agg,
        basetype = anyelement,
        stype    = anyelement
);

-- Create a function that always returns the last non-NULL item
CREATE OR REPLACE FUNCTION public.last_agg ( anyelement, anyelement )
RETURNS anyelement LANGUAGE sql IMMUTABLE STRICT AS $$
        SELECT $2;
$$;

-- And then wrap an aggregate around it
CREATE AGGREGATE public.last (
        sfunc    = public.last_agg,
        basetype = anyelement,
        stype    = anyelement
);

Sau đó, ví dụ của bạn sẽ hoạt động gần như là:

SELECT FIRST(id), customer, FIRST(total)
FROM  purchases
GROUP BY customer
ORDER BY FIRST(total) DESC;

CAVEAT: Nó bỏ qua các hàng NULL


Chỉnh sửa 1 - Thay vào đó, hãy sử dụng tiện ích mở rộng postgres

Bây giờ tôi sử dụng cách này: http://pgxn.org/dist/first_last_agg/

Để cài đặt trên Ubuntu 14.04:

apt-get install postgresql-server-dev-9.3 git build-essential -y
git clone git://github.com/wulczer/first_last_agg.git
cd first_last_app
make && sudo make install
psql -c 'create extension first_last_agg'

Đó là một phần mở rộng postgres cung cấp cho bạn các chức năng đầu tiên và cuối cùng; rõ ràng nhanh hơn cách trên.


Chỉnh sửa 2 - Đặt hàng và lọc

Nếu bạn sử dụng các hàm tổng hợp (như thế này), bạn có thể sắp xếp các kết quả mà không cần phải có dữ liệu đã được đặt hàng:

http://www.postgresql.org/docs/current/static/sql-expressions.html#SYNTAX-AGGREGATES

Vì vậy, ví dụ tương đương, với thứ tự sẽ là một cái gì đó như:

SELECT first(id order by id), customer, first(total order by id)
  FROM purchases
 GROUP BY customer
 ORDER BY first(total);

Tất nhiên bạn có thể đặt hàng và lọc khi bạn thấy phù hợp trong tổng hợp; đó là cú pháp rất mạnh.


Sử dụng phương pháp tiếp cận chức năng tùy chỉnh này là tốt. Đủ phổ quát và đơn giản. Tại sao lại phức tạp hóa mọi thứ, đây có phải là giải pháp ít hiệu quả hơn những thứ khác?
Serge Shcherbakov

9

Truy vấn:

SELECT purchases.*
FROM purchases
LEFT JOIN purchases as p 
ON 
  p.customer = purchases.customer 
  AND 
  purchases.total < p.total
WHERE p.total IS NULL

LÀM THẾ NÀO ĐỂ LÀM VIỆC! (Tôi đã từng ở đó)

Chúng tôi muốn đảm bảo rằng chúng tôi chỉ có tổng số cao nhất cho mỗi lần mua.


Một số công cụ lý thuyết (bỏ qua phần này nếu bạn chỉ muốn hiểu truy vấn)

Đặt Total là hàm T (khách hàng, id) trong đó nó trả về một giá trị được đặt tên và id Để chứng minh rằng tổng (T (khách hàng, id)) là cao nhất chúng tôi phải chứng minh rằng chúng tôi muốn chứng minh

  • X T (khách hàng, id)> T (khách hàng, x) (tổng số này cao hơn tất cả các tổng số khác cho khách hàng đó)

HOẶC LÀ

  • X T (khách hàng, id) <T (khách hàng, x) (không tồn tại tổng số cao hơn cho khách hàng đó)

Cách tiếp cận đầu tiên sẽ cần chúng tôi lấy tất cả các hồ sơ cho tên mà tôi không thực sự thích.

Cách thứ hai sẽ cần một cách thông minh để nói rằng không thể có bản ghi nào cao hơn bản này.


Quay lại SQL

Nếu chúng ta rời khỏi tham gia bảng trên tên và tổng số ít hơn bảng tham gia:

      LEFT JOIN purchases as p 
      ON 
      p.customer = purchases.customer 
      AND 
      purchases.total < p.total

chúng tôi đảm bảo rằng tất cả các bản ghi có một bản ghi khác có tổng số cao hơn cho cùng một người dùng được tham gia:

purchases.id, purchases.customer, purchases.total, p.id, p.customer, p.total
1           , Tom           , 200             , 2   , Tom   , 300
2           , Tom           , 300
3           , Bob           , 400             , 4   , Bob   , 500
4           , Bob           , 500
5           , Alice         , 600             , 6   , Alice   , 700
6           , Alice         , 700

Điều đó sẽ giúp chúng tôi lọc tổng số cao nhất cho mỗi lần mua mà không cần nhóm:

WHERE p.total IS NULL

purchases.id, purchases.name, purchases.total, p.id, p.name, p.total
2           , Tom           , 300
4           , Bob           , 500
6           , Alice         , 700

Và đó là câu trả lời chúng ta cần.


8

Giải pháp rất nhanh

SELECT a.* 
FROM
    purchases a 
    JOIN ( 
        SELECT customer, min( id ) as id 
        FROM purchases 
        GROUP BY customer 
    ) b USING ( id );

và thực sự rất nhanh nếu bảng được lập chỉ mục bởi id:

create index purchases_id on purchases (id);

Điều khoản USING là rất nhiều tiêu chuẩn. Chỉ là một số hệ thống cơ sở dữ liệu nhỏ không có nó.
Holger Jakobs

2
Điều này không tìm thấy giao dịch mua của khách hàng với tổng số tiền lớn nhất
Johnny Wong

7

Trong SQL Server, bạn có thể làm điều này:

SELECT *
FROM (
SELECT ROW_NUMBER()
OVER(PARTITION BY customer
ORDER BY total DESC) AS StRank, *
FROM Purchases) n
WHERE StRank = 1

Giải thích: Tại đây, Nhóm được thực hiện trên cơ sở khách hàng và sau đó đặt hàng tổng cộng sau đó mỗi nhóm như vậy được cấp số sê-ri là StRank và chúng tôi sẽ loại 1 khách hàng đầu tiên có StRank là 1


Cảm ơn bạn! Điều này làm việc hoàn hảo và rất dễ hiểu và thực hiện.
ruohola


4

Trong PostgreSQL, một khả năng khác là sử dụng first_valuechức năng cửa sổ kết hợp với SELECT DISTINCT:

select distinct customer_id,
                first_value(row(id, total)) over(partition by customer_id order by total desc, id)
from            purchases;

Tôi đã tạo một hỗn hợp (id, total), vì vậy cả hai giá trị được trả về bởi cùng một tổng hợp. Tất nhiên bạn luôn có thể áp dụng first_value()hai lần.


3

Giải pháp "Được hỗ trợ bởi bất kỳ cơ sở dữ liệu" nào của OMG Ponies được chấp nhận có tốc độ tốt từ thử nghiệm của tôi.

Ở đây tôi cung cấp một cách tiếp cận tương tự, nhưng đầy đủ hơn và làm sạch bất kỳ giải pháp cơ sở dữ liệu nào. Các mối quan hệ được xem xét (giả sử mong muốn chỉ nhận được một hàng cho mỗi khách hàng, thậm chí nhiều bản ghi cho tổng số tối đa trên mỗi khách hàng) và các trường mua khác (ví dụ: buy_payment_id) sẽ được chọn cho các hàng khớp thực trong bảng mua hàng.

Được hỗ trợ bởi bất kỳ cơ sở dữ liệu:

select * from purchase
join (
    select min(id) as id from purchase
    join (
        select customer, max(total) as total from purchase
        group by customer
    ) t1 using (customer, total)
    group by customer
) t2 using (id)
order by customer

Truy vấn này khá nhanh, đặc biệt là khi có một chỉ số tổng hợp như (khách hàng, tổng số) trên bảng mua hàng.

Ghi chú:

  1. t1, t2 là các bí danh truy vấn con có thể bị xóa tùy thuộc vào cơ sở dữ liệu.

  2. Hãy cẩn thận : using (...)mệnh đề hiện không được hỗ trợ trong MS-SQL và Oracle db kể từ lần chỉnh sửa này vào tháng 1 năm 2017. Bạn phải tự mở rộng nó để on t2.id = purchase.idv.v. Cú pháp USING hoạt động trong SQLite, MySQL và PostgreQuery.


2

QUALIFYMệnh đề hỗ trợ Snowdrops / Teradata hoạt động như HAVINGđối với các hàm được cửa sổ:

SELECT id, customer, total
FROM PURCHASES
QUALIFY ROW_NUMBER() OVER(PARTITION BY p.customer ORDER BY p.total DESC) = 1

1
  • Nếu bạn muốn chọn bất kỳ hàng nào (theo một số điều kiện cụ thể của bạn) từ tập hợp các hàng tổng hợp.

  • Nếu bạn muốn sử dụng sum/avghàm tổng hợp ( ) khác ngoài max/min. Vì vậy, bạn không thể sử dụng đầu mối vớiDISTINCT ON

Bạn có thể sử dụng truy vấn con tiếp theo:

SELECT  
    (  
       SELECT **id** FROM t2   
       WHERE id = ANY ( ARRAY_AGG( tf.id ) ) AND amount = MAX( tf.amount )   
    ) id,  
    name,   
    MAX(amount) ma,  
    SUM( ratio )  
FROM t2  tf  
GROUP BY name

Bạn có thể thay thế amount = MAX( tf.amount )bằng bất kỳ điều kiện nào bạn muốn bằng một hạn chế: Truy vấn con này không được trả lại nhiều hơn một hàng

Nhưng nếu bạn muốn làm những việc như vậy, có lẽ bạn đang tìm kiếm các chức năng của cửa sổ


1

Đối với Máy chủ SQl, cách hiệu quả nhất là:

with
ids as ( --condition for split table into groups
    select i from (values (9),(12),(17),(18),(19),(20),(22),(21),(23),(10)) as v(i) 
) 
,src as ( 
    select * from yourTable where  <condition> --use this as filter for other conditions
)
,joined as (
    select tops.* from ids 
    cross apply --it`s like for each rows
    (
        select top(1) * 
        from src
        where CommodityId = ids.i 
    ) as tops
)
select * from joined

và đừng quên tạo chỉ mục cụm cho các cột được sử dụng

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.