Tham gia SQL: chọn các bản ghi cuối cùng trong mối quan hệ một-nhiều


298

Giả sử tôi có một bảng khách hàng và một bảng mua hàng. Mỗi lần mua thuộc về một khách hàng. Tôi muốn nhận danh sách tất cả khách hàng cùng với lần mua hàng cuối cùng của họ trong một tuyên bố CHỌN. Thực hành tốt nhất là gì? Bất kỳ lời khuyên về xây dựng chỉ số?

Vui lòng sử dụng các tên bảng / cột trong câu trả lời của bạn:

  • khách hàng: id, tên
  • mua hàng: id, customer_id, item_id, ngày

Và trong các tình huống phức tạp hơn, việc sử dụng hiệu quả cơ sở dữ liệu bằng cách đưa việc mua hàng cuối cùng vào bảng khách hàng có ích không?

Nếu id (mua) được đảm bảo sắp xếp theo ngày, các câu lệnh có thể được đơn giản hóa bằng cách sử dụng một cái gì đó như thế LIMIT 1nào không?


Có, nó có thể có giá trị không chuẩn hóa (nếu nó cải thiện hiệu suất rất nhiều, mà bạn chỉ có thể tìm ra bằng cách thử nghiệm cả hai phiên bản). Nhưng nhược điểm của việc không chuẩn hóa thường đáng để tránh.
Vince Bowdren

Câu trả lời:


450

Đây là một ví dụ về greatest-n-per-groupvấn đề đã xuất hiện thường xuyên trên StackOverflow.

Đây là cách tôi thường khuyên bạn nên giải quyết nó:

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;

Giải thích: đưa ra một hàng p1, không nên có một hàng p2với cùng một khách hàng và một ngày sau đó (hoặc trong trường hợp quan hệ, một sau id). Khi chúng tôi thấy điều đó là đúng, thì đó p1là lần mua gần đây nhất cho khách hàng đó.

Về chỉ số, tôi muốn tạo ra một chỉ số hợp chất trong purchasetrên các cột ( customer_id, date, id). Điều đó có thể cho phép kết nối bên ngoài được thực hiện bằng cách sử dụng một chỉ số bao phủ. Hãy chắc chắn kiểm tra trên nền tảng của bạn, bởi vì tối ưu hóa phụ thuộc vào việc triển khai. Sử dụng các tính năng của RDBMS của bạn để phân tích kế hoạch tối ưu hóa. Ví dụ: EXPLAINtrên MySQL.


Một số người sử dụng các truy vấn con thay vì giải pháp tôi trình bày ở trên, nhưng tôi thấy giải pháp của mình giúp giải quyết mối quan hệ dễ dàng hơn.


3
Thuận lợi, nói chung. Nhưng điều đó phụ thuộc vào thương hiệu cơ sở dữ liệu bạn sử dụng, số lượng và phân phối dữ liệu trong cơ sở dữ liệu của bạn. Cách duy nhất để có câu trả lời chính xác là bạn kiểm tra cả hai giải pháp đối với dữ liệu của mình.
Bill Karwin

27
Nếu bạn muốn bao gồm những khách hàng chưa bao giờ mua hàng, hãy thay đổi THAM GIA mua p1 ON (c.id = p1.customer_id) thành LEFT THAM GIA mua p1 ON (c.id = p1.customer_id)
GordonM

5
@russds, bạn cần một số cột duy nhất bạn có thể sử dụng để giải quyết cà vạt. Thật vô nghĩa khi có hai hàng giống nhau trong cơ sở dữ liệu quan hệ.
Bill Karwin

6
Mục đích của "WHERE p2.id LÀ NULL" là gì?
clu

3
giải pháp này chỉ hoạt động, nếu có nhiều hơn 1 hồ sơ mua hàng. ist có liên kết 1: 1, nó KHÔNG hoạt động. ở đó phải là "WHERE (p2.id IS NULL hoặc p1.id = p2.id)
Bruno Jennrich

126

Bạn cũng có thể thử làm điều này bằng cách sử dụng một lựa chọn phụ

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

Việc lựa chọn nên tham gia vào tất cả các khách hàng và ngày mua hàng cuối cùng của họ .


4
Cảm ơn điều này vừa cứu tôi - giải pháp này dường như khả thi và dễ bảo trì hơn sau đó những giải pháp khác được liệt kê + không phải là sản phẩm cụ thể
Daveo

Làm thế nào tôi có thể sửa đổi điều này nếu tôi muốn có được một khách hàng ngay cả khi không có mua hàng?
clu

3
@clu: Thay đổi INNER JOINthành a LEFT OUTER JOIN.
Sasha Chedygov

3
Có vẻ như điều này giả định rằng chỉ có một giao dịch mua vào ngày hôm đó. Nếu có hai bạn sẽ nhận được hai hàng đầu ra cho một khách hàng, tôi nghĩ sao?
artfulrobot

1
@IstiaqueAhmed - INNER THAM GIA cuối cùng lấy giá trị Max (ngày) đó và liên kết nó trở lại bảng nguồn. Nếu không có sự tham gia đó, thông tin duy nhất bạn sẽ có từ purchasebảng là ngày và customer_id, nhưng truy vấn sẽ yêu cầu tất cả các trường từ bảng.
Cười Vergil

26

Bạn chưa chỉ định cơ sở dữ liệu. Nếu nó là một chức năng cho phép các chức năng phân tích, có thể sử dụng phương pháp này nhanh hơn so với NHÓM THEO (chắc chắn là nhanh hơn trong Oracle, rất có thể nhanh hơn trong các phiên bản SQL Server muộn, không biết về các phiên bản khác).

Cú pháp trong SQL Server sẽ là:

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
Đây là câu trả lời sai cho câu hỏi vì bạn đang sử dụng "RANK ()" thay vì "ROW_NUMBER ()". RANK vẫn sẽ cung cấp cho bạn cùng một vấn đề về mối quan hệ khi hai giao dịch mua có cùng một ngày. Đó là những gì chức năng Xếp hạng làm; nếu khớp 2 trên cùng, cả hai đều được gán giá trị 1 và bản ghi thứ 3 có giá trị là 3. Với Row_Number, không có ràng buộc, nó là duy nhất cho toàn bộ phân vùng.
MikeTeeVee

4
Thử cách tiếp cận của Bill Karwin chống lại cách tiếp cận của Madalina tại đây, với các kế hoạch thực hiện được kích hoạt trong máy chủ sql 2008 Tôi thấy ứng dụng của Bill Karwin có chi phí truy vấn là 43% so với cách tiếp cận của Madalina sử dụng 57% - vì vậy, mặc dù cú pháp thanh lịch hơn của câu trả lời này, tôi vẫn sẽ ủng hộ phiên bản của Bill!
Shawson

26

Một cách tiếp cận khác là sử dụng một NOT EXISTSđiều kiện trong điều kiện tham gia của bạn để kiểm tra các lần mua sau:

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

Bạn có thể giải thích AND NOT EXISTSphần bằng những từ dễ dàng?
Istiaque Ahmed

Việc chọn phụ chỉ kiểm tra xem có hàng nào có id cao hơn không. Bạn sẽ chỉ nhận được một hàng trong tập kết quả của mình, nếu không tìm thấy có id cao hơn. Đó phải là cao nhất độc nhất.
Stefan Haberl

2
Điều này đối với tôi là giải pháp dễ đọc nhất. Nếu điều này là quan trọng.
fguillen

:) Cảm ơn. Tôi luôn phấn đấu cho giải pháp dễ đọc nhất, bởi vì đó điều quan trọng.
Stefan Haberl

19

Tôi tìm thấy chủ đề này như là một giải pháp cho vấn đề của tôi.

Nhưng khi tôi thử chúng thì hiệu suất thấp. Dưới đây là đề nghị của tôi cho hiệu suất tốt hơn.

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 

Hy vọng điều này sẽ hữu ích.


để chỉ nhận 1 tôi đã sử dụng top 1ordered it byMaxDatedesc
Roshna Omer

1
Đây là giải pháp dễ dàng và đơn giản, trong trường hợp MY (nhiều khách hàng, ít mua) nhanh hơn 10% sau đó là giải pháp của @Stefan Haberl và tốt hơn 10 lần so với câu trả lời được chấp nhận
Juraj Bezručka

Đề xuất tuyệt vời sử dụng biểu thức bảng chung (CTE) để giải quyết vấn đề này. Điều này đã cải thiện đáng kể hiệu suất của các truy vấn trong nhiều tình huống.
AdamsTips

Câu trả lời hay nhất, dễ đọc, mệnh đề MAX () mang lại hiệu suất tuyệt vời được chuyển sang ĐẶT HÀNG B + NG + GIỚI HẠN 1
mrj

10

Nếu bạn đang sử dụng PostgreSQL, bạn có thể sử dụng DISTINCT ONđể tìm hàng đầu tiên trong một nhóm.

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

Tài liệu PostgreSQL - Nổi bật

Lưu ý rằng DISTINCT ON(các) trường - ở đây customer_id- phải khớp với hầu hết các trường bên trái trong ORDER BYmệnh đề.

Hãy cẩn thận: Đây là một điều khoản không chuẩn.


8

Hãy thử điều này, nó sẽ giúp.

Tôi đã sử dụng điều này trong dự án của tôi.

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]

Trường hợp bí danh "p" đến từ đâu?
TiagoA

điều này không hoạt động tốt .... mất mãi mãi khi các ví dụ khác ở đây mất 2 giây trên tập dữ liệu tôi có ....
Joel_J

3

Đã thử nghiệm trên 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

Hàm max()tổng hợp sẽ đảm bảo rằng giao dịch mua mới nhất được chọn từ mỗi nhóm (nhưng giả sử rằng cột ngày có định dạng theo đó max () đưa ra giá trị mới nhất - thường là như vậy). Nếu bạn muốn xử lý mua hàng trong cùng một ngày thì bạn có thể sử dụng max(p.date, p.id).

Về mặt chỉ mục, tôi sẽ sử dụng một chỉ mục khi mua hàng với (customer_id, ngày, [bất kỳ cột mua hàng nào khác bạn muốn trả lại trong lựa chọn của bạn]).

Các LEFT OUTER JOIN(như trái ngược với INNER JOIN) sẽ đảm bảo rằng khách hàng mà chưa bao giờ thực hiện mua hàng cũng được bao gồm.


sẽ không chạy trong t-sql vì chọn c. * có các cột không nằm trong nhóm theo mệnh đề
Joel_J

1

Hãy thử nó

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