การเข้าร่วม SQL: เลือกระเบียนสุดท้ายในความสัมพันธ์แบบหนึ่ง - ต่อ - กลุ่ม


298

สมมติว่าฉันมีตารางลูกค้าและตารางการซื้อ การซื้อแต่ละครั้งเป็นของลูกค้าหนึ่งราย ฉันต้องการรับรายชื่อลูกค้าทั้งหมดพร้อมกับการซื้อครั้งสุดท้ายในหนึ่งคำสั่ง SELECT การปฏิบัติที่ดีที่สุดคืออะไร? คำแนะนำเกี่ยวกับการสร้างดัชนีใด ๆ

โปรดใช้ชื่อตาราง / คอลัมน์เหล่านี้ในคำตอบของคุณ:

  • ลูกค้า: id, ชื่อ
  • การซื้อ: id, customer_id, item_id, วันที่

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

หากรหัส (ซื้อ) มีการรับประกันว่าจะเรียงตามวันที่งบสามารถจะง่ายขึ้นโดยใช้สิ่งที่ชอบLIMIT 1?


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

2
ที่เกี่ยวข้อง: jan.kneschke.de/projects/mysql/groupwise-max
igorw

คำตอบ:


449

นี่เป็นตัวอย่างของgreatest-n-per-groupปัญหาที่ปรากฏเป็นประจำบน StackOverflow

นี่คือวิธีที่ฉันมักจะแนะนำให้แก้มัน:

SELECT c.*, p1.*
FROM customer c
JOIN purchase p1 ON (c.id = p1.customer_id)
LEFT OUTER JOIN purchase p2 ON (c.id = p2.customer_id AND 
    (p1.date < p2.date OR (p1.date = p2.date AND p1.id < p2.id)))
WHERE p2.id IS NULL;

คำอธิบาย: ให้กับแถวp1ไม่ควรมีแถวที่p2มีลูกค้ารายเดียวกันและวันที่ใหม่กว่า (หรือในกรณีของความสัมพันธ์ภายหลังid) เมื่อเราพบว่าเป็นจริงp1การซื้อล่าสุดสำหรับลูกค้ารายนั้นคือ

เกี่ยวกับดัชนีฉันสร้างดัชนีสารประกอบในpurchaseมากกว่าคอลัมน์ ( customer_id, date, id) ที่อาจอนุญาตให้ทำการรวมภายนอกโดยใช้ดัชนีครอบคลุม อย่าลืมทดสอบบนแพลตฟอร์มของคุณเนื่องจากการเพิ่มประสิทธิภาพนั้นขึ้นอยู่กับการใช้งาน ใช้คุณสมบัติของ RDBMS ของคุณเพื่อวิเคราะห์แผนการปรับให้เหมาะสม เช่นEXPLAINใน MySQL


บางคนใช้แบบสอบถามย่อยแทนวิธีแก้ปัญหาที่ฉันแสดงด้านบน แต่ฉันพบว่าโซลูชันของฉันทำให้การแก้ไขความสัมพันธ์ง่ายขึ้น


3
ในเกณฑ์ดีโดยทั่วไป แต่ขึ้นอยู่กับยี่ห้อของฐานข้อมูลที่คุณใช้และปริมาณและการกระจายของข้อมูลในฐานข้อมูลของคุณ วิธีเดียวที่จะได้คำตอบที่แม่นยำคือให้คุณทดสอบทั้งสองวิธีกับข้อมูลของคุณ
Bill Karwin

27
หากคุณต้องการรวมลูกค้าที่ไม่เคยทำการซื้อให้เปลี่ยน JOIN ซื้อ p1 ON (c.id = p1.customer_id) เป็น LEFT JOIN ซื้อ p1 ON (c.id = p1.customer_id)
GordonM

5
@russds คุณต้องมีคอลัมน์ที่ไม่ซ้ำกันซึ่งคุณสามารถใช้เพื่อแก้ไขปัญหาการผูก มันไม่มีเหตุผลที่จะมีสองแถวที่เหมือนกันในฐานข้อมูลเชิงสัมพันธ์
Bill Karwin

6
วัตถุประสงค์ของ "WHERE p2.id IS NULL" คืออะไร?
clu

3
วิธีนี้ใช้ได้เฉพาะในกรณีที่มีบันทึกการซื้อมากกว่า 1 รายการ ไม่มีลิงก์ 1: 1 มันใช้งานไม่ได้ จะต้องมี "WHERE (p2.id IS NULL หรือ p1.id = p2.id)
Bruno Jennrich

126

คุณสามารถลองทำสิ่งนี้โดยใช้ตัวเลือกย่อย

SELECT  c.*, p.*
FROM    customer c INNER JOIN
        (
            SELECT  customer_id,
                    MAX(date) MaxDate
            FROM    purchase
            GROUP BY customer_id
        ) MaxDates ON c.id = MaxDates.customer_id INNER JOIN
        purchase p ON   MaxDates.customer_id = p.customer_id
                    AND MaxDates.MaxDate = p.date

การเลือกควรเข้าร่วมกับลูกค้าทั้งหมดและวันที่ซื้อล่าสุดของพวกเขา


4
ขอบคุณที่เพิ่งบันทึกฉัน - โซลูชันนี้ดูเหมือนจะสามารถนำมาใช้ใหม่ได้และบำรุงรักษาได้มากกว่าผู้อื่นที่ระบุไว้ + ไม่ใช่ผลิตภัณฑ์เฉพาะ
Daveo

ฉันจะแก้ไขสิ่งนี้ได้อย่างไรหากฉันต้องการรับลูกค้าแม้ว่าจะไม่มีการสั่งซื้อ
clu

3
@clu: เปลี่ยนไปINNER JOIN LEFT OUTER JOIN
Sasha Chedygov

3
ดูเหมือนว่าจะมีการซื้อเพียงครั้งเดียวในวันนั้น หากมีสองคุณจะได้รับสองแถวเอาท์พุทสำหรับลูกค้ารายหนึ่งฉันคิดว่า?
artfulrobot

1
@IstiaqueAhmed - INNER JOIN ล่าสุดรับค่า Max (date) และเชื่อมโยงกลับไปยังตารางต้นทาง หากไม่มีการเข้าร่วมนั้นข้อมูลเดียวที่คุณจะมีจากpurchaseตารางคือวันที่และ customer_id แต่แบบสอบถามจะขอเขตข้อมูลทั้งหมดจากตาราง
หัวเราะเวอร์จิล

26

คุณไม่ได้ระบุฐานข้อมูล ถ้ามันเป็นฟังก์ชั่นที่ช่วยให้การวิเคราะห์มันอาจจะเร็วกว่าที่จะใช้วิธีนี้กว่า GROUP BY one (เร็วกว่าใน Oracle แน่นอนน่าจะเร็วกว่าใน SQL Server รุ่นล่าสุดซึ่งไม่รู้เรื่องอื่น)

ไวยากรณ์ใน SQL Server จะเป็น:

SELECT c.*, p.*
FROM customer c INNER JOIN 
     (SELECT RANK() OVER (PARTITION BY customer_id ORDER BY date DESC) r, *
             FROM purchase) p
ON (c.id = p.customer_id)
WHERE p.r = 1

10
นี่เป็นคำตอบที่ไม่ถูกต้องเนื่องจากคุณใช้ "RANK ()" แทน "ROW_NUMBER ()" อันดับจะยังคงให้ปัญหาความสัมพันธ์แบบเดียวกันกับคุณเมื่อการซื้อสองรายการมีวันที่แน่นอนเหมือนกัน นั่นคือสิ่งที่ฟังก์ชั่นการจัดอันดับไม่; ถ้าการจับคู่ 2 อันดับแรกทั้งคู่จะได้รับการกำหนดค่าเป็น 1 และระเบียนที่ 3 ได้รับค่าเป็น 3 ด้วย Row_Number จะไม่มีการผูกจึงเป็นสิ่งที่ไม่ซ้ำกันสำหรับพาร์ติชันทั้งหมด
MikeTeeVee

4
ลองใช้วิธีของ Bill Karwin เทียบกับวิธีของ Madalina ที่นี่ด้วยแผนการดำเนินการที่เปิดใช้งานภายใต้ sql server 2008 ฉันพบว่าการประเมินของ Bill Karwin มีค่าใช้จ่ายในการสืบค้น 43% ซึ่งตรงข้ามกับวิธีของ Madalina ที่ใช้ 57% จะยังคงชอบเวอร์ชันของ Bill!
Shawson

26

อีกวิธีหนึ่งคือการใช้NOT EXISTSเงื่อนไขในเงื่อนไขการเข้าร่วมเพื่อทดสอบการซื้อในภายหลัง:

SELECT *
FROM customer c
LEFT JOIN purchase p ON (
       c.id = p.customer_id
   AND NOT EXISTS (
     SELECT 1 FROM purchase p1
     WHERE p1.customer_id = c.id
     AND p1.id > p.id
   )
)

คุณอธิบายAND NOT EXISTSส่วนนั้นด้วยคำง่าย ๆ ได้ไหม?
Istiaque Ahmed

ตัวเลือกย่อยจะตรวจสอบว่ามีแถวที่มีรหัสสูงกว่าหรือไม่ คุณจะได้รับแถวในชุดผลลัพธ์ของคุณหากไม่พบใครที่มีรหัสที่สูงกว่า นั่นควรจะเป็นเอกลักษณ์ที่สูงที่สุด
Stefan Haberl

2
นี่สำหรับฉันเป็นทางออกที่อ่านง่ายที่สุด หากเป็นสิ่งสำคัญ
fguillen

:) ขอบคุณ ฉันมักจะพยายามหาวิธีแก้ปัญหาที่อ่านได้มากที่สุดเพราะนั่นเป็นสิ่งสำคัญ
Stefan

19

ฉันพบกระทู้นี้เป็นวิธีแก้ปัญหาของฉัน

แต่เมื่อฉันลองพวกเขาการแสดงก็ต่ำ ร้องเป็นคำแนะนำของฉันเพื่อประสิทธิภาพที่ดีขึ้น

With MaxDates as (
SELECT  customer_id,
                MAX(date) MaxDate
        FROM    purchase
        GROUP BY customer_id
)

SELECT  c.*, M.*
FROM    customer c INNER JOIN
        MaxDates as M ON c.id = M.customer_id 

หวังว่านี่จะเป็นประโยชน์


ที่จะได้รับเพียง 1 ฉันใช้top 1และordered it byMaxDatedesc
Roshna Omer

1
นี่เป็นวิธีที่ง่ายและตรงไปตรงมาในกรณีของฉัน (ลูกค้าจำนวนมากซื้อน้อย) เร็วขึ้น 10% จากนั้นแก้ปัญหาของ
@Sfan Haberl และ

ข้อเสนอแนะที่ยอดเยี่ยมโดยใช้ตารางนิพจน์ทั่วไป (CTE) เพื่อแก้ปัญหานี้ สิ่งนี้ได้ปรับปรุงประสิทธิภาพของคิวรีอย่างมากในหลาย ๆ สถานการณ์
AdamsTips

คำตอบที่ดีที่สุดอ่านง่ายประโยค MAX () ให้ประสิทธิภาพที่ยอดเยี่ยมเรียงลำดับโดย + LIMIT 1
mrj

10

หากคุณใช้ PostgreSQL คุณสามารถใช้DISTINCT ONเพื่อค้นหาแถวแรกในกลุ่ม

SELECT customer.*, purchase.*
FROM customer
JOIN (
   SELECT DISTINCT ON (customer_id) *
   FROM purchase
   ORDER BY customer_id, date DESC
) purchase ON purchase.customer_id = customer.id

PostgreSQL Docs - Distinct On

โปรดทราบว่าDISTINCT ONฟิลด์ - ที่นี่customer_id- จะต้องตรงกับฟิลด์ส่วนใหญ่ด้านซ้ายในส่วนORDER BYคำสั่ง

Caveat: นี่เป็นประโยคที่ไม่เป็นมาตรฐาน


8

ลองสิ่งนี้มันจะช่วย

ฉันใช้มันในโครงการของฉัน

SELECT 
*
FROM
customer c
OUTER APPLY(SELECT top 1 * FROM purchase pi 
WHERE pi.customer_id = c.Id order by pi.Id desc) AS [LastPurchasePrice]

นามแฝง "p" มาจากไหน
TiagoA

มันใช้งานได้ไม่ดีเลย .... เอาไปตลอดซึ่งตัวอย่างอื่น ๆ ที่นี่ใช้เวลา 2 วินาทีในชุดข้อมูลที่ฉันมี ....
Joel_J

3

ทดสอบบน SQLite:

SELECT c.*, p.*, max(p.date)
FROM customer c
LEFT OUTER JOIN purchase p
ON c.id = p.customer_id
GROUP BY c.id

max()ฟังก์ชันการรวมจะให้แน่ใจว่าการซื้อล่าสุดถูกเลือกจากแต่ละกลุ่ม ( แต่สันนิษฐานว่าคอลัมน์วันที่อยู่ในรูปแบบโดยสูงสุด (ก) ให้ล่าสุด - ซึ่งเป็นปกติกรณี) max(p.date, p.id)หากคุณต้องการที่จะจัดการกับการซื้อสินค้ากับวันเดียวกันแล้วคุณสามารถใช้

ในแง่ของดัชนีฉันจะใช้ดัชนีในการซื้อด้วย (customer_id, date, [คอลัมน์การซื้ออื่น ๆ ที่คุณต้องการกลับมาในการเลือกของคุณ])

LEFT OUTER JOIN(เมื่อเทียบกับINNER JOIN) จะให้แน่ใจว่าลูกค้าที่ไม่เคยทำการซื้อรวมอยู่ด้วย


จะไม่ทำงานใน t-sql เป็นตัวเลือก c. * มีคอลัมน์ที่ไม่อยู่ในกลุ่มตามข้อ
Joel_J

1

โปรดลองสิ่งนี้

SELECT 
c.Id,
c.name,
(SELECT pi.price FROM purchase pi WHERE pi.Id = MAX(p.Id)) AS [LastPurchasePrice]
FROM customer c INNER JOIN purchase p 
ON c.Id = p.customerId 
GROUP BY c.Id,c.name;
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.