เลือกแถวแรกในแต่ละกลุ่มจำแนกตามกลุ่ม?


1321

เป็นชื่อแนะนำ, GROUP BYฉันต้องการที่จะเลือกแถวแรกของชุดของแต่ละแถวมีการจัดกลุ่ม

โดยเฉพาะถ้าฉันมีpurchasesตารางที่มีลักษณะเช่นนี้:

SELECT * FROM purchases;

เอาท์พุทของฉัน:

id | ลูกค้า | รวม
--- + + ---------- ------
 1 | โจ | 5
 2 | แซลลี่ | 3
 3 | โจ | 2
 4 | แซลลี่ | 1

ฉันต้องการค้นหาidการซื้อที่ใหญ่ที่สุด ( total) จากแต่ละcustomerรายการ บางสิ่งเช่นนี้

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

ผลลัพธ์ที่คาดหวัง:

FIRST (id) | ลูกค้า | FIRST (รวม)
---------- ---------- + + -------------
        1 | โจ | 5
        2 | แซลลี่ | 3

เนื่องจากคุณเป็นเพียงการมองหาแต่ละคนที่ใหญ่ที่สุดทำไมไม่แบบสอบถามสำหรับMAX(total)?
phil294

4
@ phil294 การสืบค้นสูงสุด (ผลรวม) จะไม่เชื่อมโยงผลรวมนั้นกับค่า 'id' ของแถวที่เกิดขึ้น
gwideman

คำตอบ:


1115

บน Oracle 9.2+ (ไม่ใช่ 8i + ตามที่ระบุไว้เดิม), SQL Server 2005+, PostgreSQL 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

รองรับฐานข้อมูลใด ๆ :

แต่คุณต้องเพิ่มตรรกะเพื่อทำลายความสัมพันธ์:

  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 ยังรองรับฟังก์ชั่นหน้าต่าง (CTE จำเป็นต้องแปลงเป็นตารางที่ได้รับมา) และ Firebird 3.0 จะรองรับฟังก์ชั่น Window ด้วย
a_horse_with_no_name

37
ROW_NUMBER() OVER(PARTITION BY [...])พร้อมกับการเพิ่มประสิทธิภาพอื่น ๆ ช่วยให้ฉันได้รับแบบสอบถามจาก 30 วินาทีถึงไม่กี่มิลลิวินาที ขอบคุณ! (9.2 PostgreSQL)
แซม

8
หากมีการสั่งซื้อหลายรายที่มีอย่างเท่าเทียมกันสูงสุดtotalสำหรับลูกค้ารายหนึ่งที่1แบบสอบถามส่งกลับโดยพลผู้ชนะ (ขึ้นอยู่กับการใช้งานรายละเอียดนั้นidสามารถเปลี่ยนทุกการดำเนินการ!) โดยทั่วไป (ไม่เสมอไป) คุณต้องการหนึ่งแถวต่อลูกค้ากำหนดโดยเกณฑ์เพิ่มเติมเช่น "แถวที่เล็กที่สุดid" ในการแก้ไขปัญหาผนวกidไปรายการORDER BY row_number()จากนั้นคุณจะได้รับผลลัพธ์เช่นเดียวกับแบบสอบถามที่2ซึ่งไม่มีประสิทธิภาพมากสำหรับกรณีนี้ นอกจากนี้คุณต้องมีแบบสอบถามย่อยอีกคอลัมน์สำหรับทุกคอลัมน์เพิ่มเติม
Erwin Brandstetter

2
BigQuery ของ Google รองรับคำสั่ง ROW_NUMBER () ของข้อความค้นหาแรก ทำงานเหมือนเสน่ห์สำหรับเรา
Praxiteles

2
โปรดทราบว่ารุ่นแรกที่มีฟังก์ชั่นหน้าต่างทำงานเหมือนรุ่น SQLite 3.25.0: sqlite.org/windowfunctions.html#history
brianz

1147

ในPostgreSQLโดยทั่วไปแล้วจะง่ายและเร็วขึ้น (การเพิ่มประสิทธิภาพประสิทธิภาพด้านล่าง):

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

หรือสั้นกว่า (หากไม่ชัดเจน) พร้อมหมายเลขลำดับของคอลัมน์เอาต์พุต:

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

หากtotalสามารถเป็น NULL ได้ (จะไม่เกิดความเสียหาย แต่อย่างใด แต่คุณต้องการจับคู่ดัชนีที่มีอยู่ ):

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

ประเด็นสำคัญ

  • DISTINCT ONเป็นส่วนขยาย PostgreSQL ของมาตรฐาน (โดยกำหนดเฉพาะDISTINCTในSELECTรายการทั้งหมด)

  • แสดงรายการจำนวนนิพจน์ใด ๆ ในDISTINCT ONประโยคค่าแถวที่รวมกันจะนิยามรายการที่ซ้ำกัน คู่มือ:

    เห็นได้ชัดว่าสองแถวถือว่าแตกต่างกันหากพวกเขาแตกต่างกันอย่างน้อยหนึ่งค่าคอลัมน์ ค่า Null นั้นถือว่าเท่ากันในการเปรียบเทียบนี้

    เหมืองเน้นหนัก

  • DISTINCT ONORDER BYสามารถใช้ร่วมกับ นิพจน์ชั้นนำORDER BYต้องอยู่ในชุดของนิพจน์DISTINCT ONแต่คุณสามารถจัดเรียงลำดับใหม่ได้อย่างอิสระ ตัวอย่าง. คุณสามารถเพิ่มนิพจน์เพิ่มเติมORDER BYเพื่อเลือกแถวเฉพาะจากกลุ่มเพื่อนแต่ละกลุ่ม หรือตามที่คู่มือวางไว้ :

    DISTINCT ONแสดงออก (s) จะต้องตรงกับด้านซ้ายของORDER BY การแสดงออก (s) ส่วนORDER BYคำสั่งจะมีนิพจน์เพิ่มเติมที่กำหนดลำดับความสำคัญของแถวที่ต้องการภายในแต่ละDISTINCT ONกลุ่ม

    ฉันได้เพิ่มidเป็นรายการสุดท้ายที่จะทำลายความสัมพันธ์:
    "เลือกแถวที่มีขนาดเล็กที่สุดidจากแต่ละกลุ่มที่แชร์มากที่สุดtotal"

    ORDER BYเพื่อผลการสั่งซื้อในทางที่ไม่เห็นด้วยกับการเรียงลำดับการกำหนดกลุ่มต่อแรกคุณสามารถซ้อนเหนือแบบสอบถามในแบบสอบถามด้านนอกอีกด้วย ตัวอย่าง.

  • หากtotalสามารถเป็น NULL ได้คุณอาจต้องการแถวที่มีค่าไม่เป็นศูนย์มากที่สุด เพิ่มNULLS LASTเช่นแสดงให้เห็นถึง ดู:

  • SELECTรายการไม่ได้ จำกัด โดยนิพจน์ในDISTINCT ONหรือORDER BYในทางใดทางหนึ่ง (ไม่จำเป็นในกรณีง่ายด้านบน):

    • คุณไม่จำเป็นต้องรวมถึงการใด ๆ ของการแสดงออกในหรือDISTINCT ONORDER BY

    • คุณสามารถรวมการแสดงออกอื่น ๆ ในSELECTรายการ นี่เป็นเครื่องมือสำหรับแทนที่คิวรีที่ซับซ้อนมากขึ้นด้วยเคียวรี่ย่อยและฟังก์ชันการรวม / หน้าต่าง

  • ฉันทดสอบกับ Postgres เวอร์ชัน 8.3 - 12 แต่ฟีเจอร์นี้มีอยู่ตั้งแต่เวอร์ชัน 7.1 เป็นอย่างน้อย

ดัชนี

สมบูรณ์แบบดัชนีสำหรับการค้นหาดังกล่าวข้างต้นจะเป็นดัชนีคอลัมน์หลายทอดทั้งสามคอลัมน์ในการจับคู่ตามลำดับและมีการจับคู่การเรียงลำดับ:

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

อาจมีความเชี่ยวชาญมากเกินไป แต่ใช้ถ้าอ่านประสิทธิภาพสำหรับการสืบค้นเฉพาะเป็นสิ่งสำคัญ ถ้าคุณมีDESC NULLS LASTในแบบสอบถามใช้เหมือนกันในดัชนีเพื่อให้เรียงลำดับการจับคู่และดัชนีใช้

ประสิทธิผล / การเพิ่มประสิทธิภาพ

ชั่งน้ำหนักค่าใช้จ่ายและผลประโยชน์ก่อนสร้างดัชนีที่ปรับให้เหมาะกับแต่ละแบบสอบถาม ศักยภาพของดัชนีดังกล่าวข้างต้นส่วนใหญ่ขึ้นอยู่กับการกระจายข้อมูล

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

เกณฑ์มาตรฐาน

ฉันมีมาตรฐานง่ายๆที่นี่ซึ่งล้าสมัยไปแล้วในตอนนี้ ฉันแทนที่มันด้วยมาตรฐานที่มีรายละเอียดในคำตอบแยกนี้


28
นี่เป็นคำตอบที่ดีสำหรับขนาดฐานข้อมูลส่วนใหญ่ แต่ฉันต้องการชี้ให้เห็นว่าเมื่อคุณเข้าใกล้ ~ ล้านแถวDISTINCT ONจะช้ามาก การนำไปใช้จะเรียงลำดับตารางทั้งหมดเสมอและสแกนหารายการที่ซ้ำกันโดยไม่สนใจดัชนีทั้งหมด (แม้ว่าคุณจะสร้างดัชนีหลายคอลัมน์ที่ต้องการ) ดูexplextended.com/2009/05/03/postgresql-optimizing-distinctสำหรับวิธีการแก้ไขที่เป็นไปได้
Meekohi

14
การใช้คำสั่งเพื่อ "ทำให้รหัสสั้นลง" เป็นความคิดที่แย่มาก วิธีการเกี่ยวกับการปล่อยชื่อคอลัมน์ไว้เพื่อให้สามารถอ่านได้?
KOTJMF

13
@KOTJMF: ฉันขอแนะนำให้คุณไปกับการตั้งค่าส่วนตัวของคุณแล้ว ฉันแสดงตัวเลือกทั้งสองเพื่อให้ความรู้ การจดชวเลขไวยากรณ์จะมีประโยชน์สำหรับนิพจน์แบบยาวในSELECTรายการ
Erwin Brandstetter

1
@jangorecki: มาตรฐานเดิมมาจาก 2011 ฉันไม่ได้ติดตั้งอีกต่อไป แต่มันก็เป็นเวลาที่จะทำการทดสอบด้วย pg 9.4 และ pg 9.5 อยู่ดี ดูรายละเอียดในคำตอบที่เพิ่มเข้ามา . คุณอาจเพิ่มความคิดเห็นด้วยผลจากการติดตั้งของคุณด้านล่าง?
Erwin Brandstetter

2
@PirateApp: ไม่ได้มาจากด้านบนของหัวของฉัน DISTINCT ONดีสำหรับการรับหนึ่งแถวต่อกลุ่มเพื่อน
Erwin Brandstetter

134

เกณฑ์มาตรฐาน

การทดสอบผู้สมัครที่น่าสนใจมากที่สุดกับ Postgres 9.4และ9.5พร้อมโต๊ะจริงครึ่งหนึ่งของ200k แถวในpurchasesและ10k ที่แตกต่างกันcustomer_id ( เฉลี่ย. 20 แถวต่อลูกค้า )

สำหรับ Postgres 9.5 ฉันทำการทดสอบครั้งที่ 2 กับลูกค้าที่แตกต่าง 86446 ราย ดูด้านล่าง ( เฉลี่ย 2.3 แถวต่อลูกค้า )

ติดตั้ง

ตารางหลัก

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

ฉันใช้serial(เพิ่มข้อ จำกัด PK ด้านล่าง) และจำนวนเต็มcustomer_idเนื่องจากเป็นการตั้งค่าทั่วไป เพิ่มsome_columnไปยังทำขึ้นสำหรับคอลัมน์เพิ่มเติมโดยทั่วไป

ข้อมูลจำลอง, PK, ดัชนี - ตารางทั่วไปยังมีสิ่งอันดับที่ตายแล้ว:

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 ตาราง - สำหรับการสืบค้นที่เหนือกว่า

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;

ในของฉันทดสอบครั้งที่สอง 9.5 ผมใช้การตั้งค่าเดียวกัน แต่มีrandom() * 100000การสร้างที่จะได้รับเพียงไม่กี่แถวต่อcustomer_idcustomer_id

ขนาดวัตถุสำหรับตาราง purchases

สร้างด้วยแบบสอบถามนี้

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

แบบสอบถาม

1. row_number()ใน CTE ( ดูคำตอบอื่น ๆ )

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()ในแบบสอบถามย่อย (การเพิ่มประสิทธิภาพของฉัน)

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( ดูคำตอบอื่น ๆ )

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

4. rCTE พร้อมLATERALแบบสอบถามย่อย ( ดูที่นี่ )

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. customerตารางที่มีLATERAL( ดูที่นี่ )

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()กับORDER BY( ดูคำตอบอื่น ๆ )

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

ผล

เวลาดำเนินการสำหรับการค้นหาข้างต้นด้วยEXPLAIN ANALYZE(และตัวเลือกทั้งหมดออก ) ที่ดีที่สุดของ 5 ลาน

ทุกคำสั่งที่ใช้เฉพาะดัชนีสแกนบนpurchases2_3c_idx(ในขั้นตอนอื่น ๆ ) บางส่วนใช้สำหรับดัชนีที่มีขนาดเล็กลง แต่บางรายการก็มีประสิทธิภาพมากกว่า

A. Postgres 9.4 กับ 200k แถวและ ~ 20 ต่อ 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. เช่นเดียวกับ 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. เหมือนกับ B. แต่มี ~ 2.3 แถวต่อ 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

มาตรฐานที่เกี่ยวข้อง

นี่คือการทดสอบ "ogr" ใหม่ด้วยแถว 10M และลูกค้า "60" ที่ไม่ซ้ำ 60kในPostgres 11.5 (ปัจจุบัน ณ วันที่ 25 ก.ย. 2019) ผลลัพธ์ยังคงสอดคล้องกับสิ่งที่เราได้เห็น:

มาตรฐานดั้งเดิม (ล้าสมัย) จาก 2011

ฉันรันการทดสอบสามรายการกับ PostgreSQL 9.1บนตารางชีวิตจริงของ 65579 แถวและดัชนี btree แบบคอลัมน์เดียวในแต่ละคอลัมน์สามคอลัมน์ที่เกี่ยวข้องและใช้เวลาดำเนินการที่ดีที่สุด5 ครั้ง
การเปรียบเทียบข้อความค้นหาแรกของ @OMGPonies ( A) กับโซลูชันด้านบนDISTINCT ON ( B):

  1. เลือกทั้งตารางผลลัพธ์ใน 5958 แถวในกรณีนี้

    A: 567.218 ms
    B: 386.673 ms
  2. ใช้เงื่อนไขWHERE customer BETWEEN x AND yส่งผลให้ 1,000 แถว

    A: 249.136 ms
    B:  55.111 ms
  3. WHERE customer = xเลือกลูกค้าเดียวกับ

    A:   0.143 ms
    B:   0.072 ms

การทดสอบเดียวกันซ้ำกับดัชนีที่อธิบายในคำตอบอื่น ๆ

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
ขอบคุณสำหรับมาตรฐานที่ยอดเยี่ยม ฉันสงสัยว่าการสืบค้นข้อมูลเหตุการณ์ที่คุณมีการประทับเวลาแทนที่จะเป็นผลรวมจะได้รับประโยชน์จากดัชนี BRIN ใหม่หรือไม่ สิ่งนี้อาจทำให้เพิ่มความเร็วสำหรับการค้นหาชั่วคราว
jangorecki

3
@angorecki: ตารางขนาดใหญ่ใด ๆ ที่มีข้อมูลที่จัดเรียงทางร่างกายสามารถทำกำไรได้จากดัชนี BRIN
Erwin Brandstetter

@ErwinBrandstetter ในตัวอย่าง2. row_number()และ5. customer table with LATERALอะไรที่ทำให้แน่ใจได้ว่า id จะเล็กที่สุด?
Artem Novikov

@ArtemNovikov: ไม่มีอะไร โดยมีวัตถุประสงค์เพื่อดึงต่อแถวที่มีสูงสุดcustomer_id totalมันเป็นเรื่องบังเอิญที่ทำให้เข้าใจผิดในข้อมูลการทดสอบของคำถามที่ว่าidในแถวที่เลือกนั้นเกิดขึ้นน้อยที่สุดcustomer_idเช่นกัน
Erwin Brandstetter

1
@ArtemNovikov: เพื่ออนุญาตการสแกนเฉพาะดัชนี
Erwin Brandstetter

55

นี่เป็นเรื่องปกติ ปัญหาซึ่งได้ผ่านการทดสอบมาอย่างดีและได้รับการปรับปรุงอย่างเหมาะสมแล้ว โดยส่วนตัวแล้วฉันชอบโซลูชันการเข้าร่วมด้านซ้ายโดย Bill Karwin ( โพสต์ดั้งเดิมที่มีโซลูชันอื่น ๆ มากมาย )

โปรดทราบว่าการแก้ปัญหาที่พบบ่อยนี้สามารถพบได้ในแหล่งที่เป็นทางการมากที่สุดคู่มือ MySQL ! ดูตัวอย่างของการสืบค้นทั่วไป :: แถวถือสูงสุดกลุ่มที่ชาญฉลาดของคอลัมน์บาง


22
คู่มือ MySQL ในทางใดทาง "เป็นทางการ" สำหรับคำถาม Postgres / SQLite (ไม่พูดถึง SQL) นอกจากนี้ต้องมีความชัดเจนในDISTINCT ONรุ่นที่สั้นมากง่ายและโดยทั่วไปดำเนินการที่ดีขึ้นใน Postgres กว่าทางเลือกด้วยตนเองหรือกึ่งต่อต้านการเข้าร่วมกับLEFT JOIN NOT EXISTSนอกจากนี้ยังเป็น "ทดสอบอย่างดี"
Erwin Brandstetter

3
นอกจากนี้ในสิ่งที่ Erwin เขียนฉันจะบอกว่าการใช้ฟังก์ชั่นหน้าต่าง (ซึ่งเป็นฟังก์ชั่น SQL ทั่วไปในปัจจุบัน) เกือบจะเร็วกว่าการใช้การเข้าร่วมกับตารางที่ได้รับ
a_horse_with_no_name

6
การอ้างอิงที่ดี ฉันไม่รู้ว่าสิ่งนี้ถูกเรียกว่าปัญหาที่ยิ่งใหญ่ที่สุด -n- ต่อกลุ่ม ขอบคุณ.
David Mann

คำถามไม่ได้เช่นเดียวกับที่ยิ่งใหญ่ที่สุด n ต่อกลุ่ม แต่แรก n
reinierpost

1
ในกรณีที่ฉันพยายามสั่งซื้อสองรายการ "ออกจากการเข้าร่วมแก้ปัญหาโดย Bill Karwin" ให้ประสิทธิภาพต่ำ ดูความคิดเห็นของฉันด้านล่างstackoverflow.com/a/8749095/684229
Johnny Wong

30

ใน Postgres คุณสามารถใช้array_aggสิ่งนี้:

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

สิ่งนี้จะให้การidซื้อที่ใหญ่ที่สุดของลูกค้าแต่ละราย

บางสิ่งที่ควรทราบ:

  • array_aggเป็นฟังก์ชั่นรวมดังนั้นจึงใช้งานGROUP BYได้
  • array_aggช่วยให้คุณระบุขอบเขตการสั่งซื้อเพียงเพื่อตัวเองดังนั้นจึงไม่ จำกัด โครงสร้างของแบบสอบถามทั้งหมด นอกจากนี้ยังมีไวยากรณ์สำหรับวิธีที่คุณเรียงลำดับ NULL หากคุณต้องการทำสิ่งที่แตกต่างจากค่าเริ่มต้น
  • เมื่อเราสร้างอาร์เรย์เราจะใช้องค์ประกอบแรก (อาร์เรย์ Postgres นั้นมีดัชนี 1 ดัชนีไม่ใช่ดัชนี 0)
  • คุณสามารถใช้array_aggวิธีการที่คล้ายกันสำหรับคอลัมน์ผลลัพธ์ที่สามของคุณ แต่max(total)ง่ายกว่า
  • ซึ่งแตกต่างจากการDISTINCT ONใช้array_aggช่วยให้คุณเก็บไว้GROUP BYในกรณีที่คุณต้องการด้วยเหตุผลอื่น

14

วิธีการแก้ปัญหาไม่ได้มีประสิทธิภาพมากตามที่ Erwin ชี้เนื่องจากมี SubQs

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

ขอบคุณใช่เห็นด้วยกับคุณการเข้าร่วมระหว่างแบบสอบถามย่อยและแบบสอบถามภายนอกใช้เวลานานกว่าจริง ๆ "In" จะไม่เป็นปัญหาที่นี่เนื่องจาก subq จะส่งผลให้แถวเดียว BTW คุณมีข้อผิดพลาดทางไวยากรณ์อะไรบ้าง?
user2407394

ohh .. เคยใช้กับ "Teradata" .. แก้ไขแล้วตอนนี้ .. ไม่ว่าจะผูกความสัมพันธ์แบบใดก็ตามที่นี่ก็จำเป็นต้องหายอดรวมสูงสุดสำหรับลูกค้าแต่ละราย ..
user2407394

คุณทราบหรือไม่ว่าคุณได้รับหลายแถวสำหรับลูกค้ารายเดียวในกรณีที่เสมอกัน? ขึ้นอยู่กับความต้องการที่แน่นอน ปกติแล้วมันไม่ใช่ สำหรับคำถามที่อยู่ในชื่อค่อนข้างชัดเจน
Erwin Brandstetter

นี่ไม่ชัดเจนจากคำถามหากลูกค้ารายเดียวกันซื้อ = สูงสุดสำหรับรหัสที่ต่างกัน 2 รหัสฉันคิดว่าเราควรแสดงทั้งสอง
user2407394

10

ฉันใช้วิธีนี้ (เฉพาะ 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
);

จากนั้นตัวอย่างของคุณควรจะทำงานได้เกือบจะเหมือนเดิม:

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

ถ้ำ: มันไม่สนใจแถว NULL


แก้ไข 1 - ใช้ส่วนขยาย postgres แทน

ตอนนี้ฉันใช้วิธีนี้: http://pgxn.org/dist/first_last_agg/

วิธีติดตั้งบน 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'

มันเป็นส่วนเสริม postgres ที่ให้ฟังก์ชั่นแรกและฟังก์ชั่นสุดท้าย เห็นได้ชัดว่าเร็วกว่าวิธีข้างต้น


แก้ไข 2 - การสั่งซื้อและการกรอง

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

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

ตัวอย่างที่เทียบเท่ากับการสั่งซื้อจะเป็นดังนี้:

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

แน่นอนคุณสามารถสั่งซื้อและกรองตามที่คุณเห็นว่าเหมาะสมภายในการรวม; มันเป็นไวยากรณ์ที่มีประสิทธิภาพมาก


ใช้ฟังก์ชั่นที่กำหนดเองนี้เช่นกัน สากลเพียงพอและง่าย ทำไมสิ่งที่ซับซ้อนนี่เป็นวิธีแก้ปัญหาที่มีประสิทธิภาพน้อยกว่าอย่างอื่นหรือไม่?
Sergey Shcherbakov

9

คำค้นหา:

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

มันทำงานอย่างไร! (ฉันเคยไปที่นั่น)

เราต้องการให้แน่ใจว่าเรามียอดรวมสูงสุดสำหรับการซื้อแต่ละครั้งเท่านั้น


บางทฤษฎีสิ่ง (ข้ามส่วนนี้ถ้าคุณต้องการที่จะเข้าใจแบบสอบถาม)

ให้ผลรวมเป็นฟังก์ชัน T (ลูกค้า, id) ซึ่งจะส่งกลับค่าตามชื่อและรหัสเพื่อพิสูจน์ว่าผลรวมที่ให้ (T (ลูกค้า, id)) เป็นค่าสูงสุดที่เราต้องพิสูจน์ว่าเราต้องการพิสูจน์เช่นกัน

  • ∀x T (ลูกค้า, id)> T (ลูกค้า, x) (ผลรวมนี้สูงกว่าผลรวมอื่นทั้งหมดสำหรับลูกค้ารายนั้น)

หรือ

  • ¬∃x T (ลูกค้า, id) <T (ลูกค้า, x) (ไม่มีลูกค้าทั้งหมดรวมสูงกว่า)

วิธีแรกจะต้องการให้เราได้รับบันทึกทั้งหมดสำหรับชื่อที่ฉันไม่ชอบ

อันที่สองจะต้องมีวิธีที่ชาญฉลาดในการบอกว่าจะไม่มีบันทึกสูงกว่าอันนี้


กลับไปที่ SQL

หากเราออกจากตารางไปยังชื่อและผลรวมนั้นน้อยกว่าตารางที่เข้าร่วม:

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

เราตรวจสอบให้แน่ใจว่าบันทึกทั้งหมดที่มีบันทึกอื่นที่มียอดรวมสูงกว่าเพื่อให้ผู้ใช้รายเดียวกันเข้าร่วม:

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

ซึ่งจะช่วยให้เรากรองยอดรวมสูงสุดสำหรับการซื้อแต่ละครั้งโดยไม่ต้องจัดกลุ่ม:

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

และนั่นคือคำตอบที่เราต้องการ


8

ทางออกที่รวดเร็วมาก

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

และเร็วมาก ๆ ถ้าตารางถูกทำดัชนีโดย id:

create index purchases_id on purchases (id);

ประโยคการใช้เป็นมาตรฐานมาก เป็นเพียงว่าระบบฐานข้อมูลย่อยบางระบบไม่มี
Holger Jakobs

2
ไม่พบการซื้อของลูกค้าที่มียอดรวมสูงสุด
Johnny Wong

7

ใน SQL Server คุณสามารถทำสิ่งนี้:

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

คำอธิบาย: ที่นี่ จัดกลุ่มตามจะทำบนพื้นฐานของลูกค้าแล้วสั่งซื้อโดยรวมแล้วแต่ละกลุ่มดังกล่าวจะได้รับหมายเลขประจำสินค้าเป็น StRank และเราจะทำการดึงลูกค้า 1 รายแรกที่มี StRank เป็น 1


ขอบคุณ! สิ่งนี้ทำงานได้อย่างสมบูรณ์และง่ายต่อการเข้าใจและนำไปใช้
ruohola


4

ใน PostgreSQL ความเป็นไปได้อีกอย่างหนึ่งคือการใช้first_valueฟังก์ชันหน้าต่างร่วมกับSELECT DISTINCT:

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

ฉันสร้างคอมโพสิต(id, total)ดังนั้นค่าทั้งสองจะถูกส่งกลับโดยรวมเดียวกัน แน่นอนว่าคุณสามารถสมัครได้first_value()สองครั้งเสมอ


3

โซลูชัน "รองรับฐานข้อมูลใด ๆ " ของ OMG Ponies ที่ยอมรับนั้นมีความเร็วที่ดีจากการทดสอบของฉัน

ที่นี่ฉันให้วิธีการเดียวกัน แต่สมบูรณ์มากขึ้นและสะอาดโซลูชันฐานข้อมูลใด ๆ การพิจารณาความสัมพันธ์ (สมมติว่าต้องการรับเพียงหนึ่งแถวสำหรับลูกค้าแต่ละรายแม้จะมีหลายระเบียนสำหรับยอดรวมสูงสุดต่อลูกค้า) และเขตการซื้ออื่น ๆ (เช่น purchase_payment_id) จะถูกเลือกสำหรับแถวการจับคู่จริงในตารางการซื้อ

รองรับฐานข้อมูลใด ๆ :

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

แบบสอบถามนี้มีความรวดเร็วพอสมควรโดยเฉพาะอย่างยิ่งเมื่อมีดัชนีคอมโพสิตเช่น (ลูกค้าทั้งหมด) บนตารางการซื้อ

สังเกต:

  1. t1, t2 เป็นนามแฝงย่อยที่สามารถลบได้ขึ้นอยู่กับฐานข้อมูล

  2. ข้อแม้คือusing (...)ข้อขณะนี้ไม่ได้รับการสนับสนุนใน MS-SQL และ Oracle ฐานข้อมูลเป็นของการแก้ไขนี้เมื่อ ม.ค. 2017 คุณจะต้องขยายตัวเองให้เช่นon t2.id = purchase.idฯลฯ การใช้งานในไวยากรณ์ SQLite, MySQL และ PostgreSQL


2

Snowflake / Teradata รองรับQUALIFYส่วนคำสั่งที่ใช้งานได้กับHAVINGฟังก์ชั่นหน้าต่าง:

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

1
  • หากคุณต้องการเลือกแถวใด ๆ (ตามเงื่อนไขบางอย่างของคุณ) จากชุดของแถวที่รวม

  • หากคุณต้องการที่จะใช้อีก ( sum/avgฟังก์ชั่นการรวม) max/minนอกเหนือไปจาก ดังนั้นคุณไม่สามารถใช้เงื่อนงำด้วยDISTINCT ON

คุณสามารถใช้แบบสอบถามย่อยถัดไป:

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

คุณสามารถแทนที่amount = MAX( tf.amount )ด้วยเงื่อนไขใด ๆ ที่คุณต้องการด้วยข้อ จำกัด เดียว: แบบสอบถามย่อยนี้จะต้องไม่ส่งคืนมากกว่าหนึ่งแถว

แต่ถ้าคุณต้องการทำสิ่งต่าง ๆ คุณอาจต้องการฟังก์ชั่นหน้าต่าง


1

สำหรับเซิร์ฟเวอร์ SQl วิธีที่มีประสิทธิภาพที่สุดคือ:

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

และอย่าลืมสร้างดัชนีคลัสเตอร์สำหรับคอลัมน์ที่ใช้แล้ว

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