SQL连接:选择一对多关系中的最后一条记录


298

假设我有一个客户表和一个采购表。每次购买都属于一个客户。我想在一个SELECT语句中获得所有客户的列表以及他们的最后一次购买。最佳做法是什么?关于建立索引有什么建议吗?

请在您的答案中使用这些表/列名称:

  • 客户:身份证,姓名
  • 购买:id,customer_id,item_id,日期

并且在更复杂的情况下,通过将最后一次购买放入客户表中来对数据库进行非规范化(在性能方面)是否有益?

如果保证(购买)ID按日期排序,可以使用类似的语句简化语句LIMIT 1吗?


是的,可能值得对它进行非规范化(如果它可以大大提高性能,则只能通过测试两个版本才能发现)。但是非正规化的弊端通常是可以避免的。
文斯·鲍德伦

Answers:


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_iddateid)。这可能允许使用覆盖索引完成外部联接。确保优化在您的平台上进行测试,因为优化取决于实现。使用RDBMS的功能来分析优化计划。例如EXPLAIN在MySQL上。


有些人使用子查询来代替我上面显示的解决方案,但是我发现我的解决方案使解决联系更加容易。


3
一般而言,这是有利的。但这取决于您所使用的数据库的品牌以及数据库中数据的数量和分布。获得精确答案的唯一方法是针对您的数据测试这两种解决方案。
Bill Karwin 2010年

27
如果要包括从未进行过购买的客户,则将JOIN购买p1 ON(c.id = p1.customer_id)更改为LEFT JOIN购买p1 ON(c.id = p1.customer_id)
GordonM 2010年

5
@russds,您需要一些唯一的列可用于解决关系。在关系数据库中具有两个相同的行是没有意义的。
Bill Karwin 2012年

6
“ WHERE p2.id IS NULL”的目的是什么?
clu 2015年

3
仅当购买记录超过1条时,此解决方案才有效。ist有1:1链接,它不起作用。那里必须是“ WHERE(p2.id为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 2012年

如果即使没有购物也想获得客户,我将如何修改呢?
CLU

3
@clu:将更INNER JOIN改为LEFT OUTER JOIN
Sasha Chedygov,2015年

3
看起来这是假设当天只有一次购买。如果有两个,您会为一个客户得到两个输出行,我想呢?
artfulrobot

1
@IstiaqueAhmed-最后一个INNER JOIN获取该Max(date)值并将其绑定到源表。如果没有该联接,那么您从purchase表中获得的唯一信息就是日期和customer_id,但是查询会询问表中的所有字段。
Laughing Vergil,

26

您尚未指定数据库。如果是允许分析功能的方法,则使用此方法的速度可能比使用GROUP BY的方法更快(在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()”。当两次购买的日期完全相同时,RANK仍然会给您带来相同的领带问题。那就是Rank函数的作用;如果前2个匹配,则它们都被分配值为1,而第3个记录都被分配值为3。对于Row_Number,没有关系,它对于整个分区都是唯一的。
MikeTeeVee 2012年

4
在这里尝试使用Bill Karwin的方法与Madalina的方法进行对比,并在sql server 2008下启用了执行计划,我发现Bill Karwin的方法的查询成本为43%,而Madalina的方法使用了57%,因此尽管此答案的语法更为优雅,但我仍然会支持比尔的版本!
肖森

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

子选择仅检查是否存在具有较高ID的行。如果未找到具有更高ID的结果,则只会在结果集中获得一行。那应该是独一无二的最高的。
Stefan Haberl

2
对我来说,这是最易读的解决方案。如果这很重要。
fguillen

:) 谢谢。我一直在争取最易读的解决方案,因为这重要。
Stefan Haberl

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 1ordered it byMaxDatedesc
Roshna Omer

1
这是一个简单明了的解决方案,在我的情况下(许多客户,很少购买)比@Stefan Haberl的解决方案快10%,比公认的答案好10倍以上
JurajBezručka18年

很好的建议使用公用表表达式(CTE)解决此问题。在许多情况下,这极大地提高了查询的性能。
AdamsTips '18

最佳答案imo,易于阅读,MAX()子句可提供与ORDER BY + LIMIT 1
相称的

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文件-不同

请注意,DISTINCT ON此处的字段customer_id必须与ORDER BY子句中最左边的字段匹配。

注意:这是一个非标准条款。


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

这表现不好。...花了很多时间,这里的其他示例对我拥有的数据集花费了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()给出了最新的-这通常情况下)。如果您要处理同一日期的购买,则可以使用max(p.date, p.id)

在索引方面,我会在采购时使用索引(customer_id,日期,[您要在选择中返回的任何其他采购列])。

LEFT OUTER JOIN(相对于INNER JOIN)将确保也包括那些从未购买的客户。


不会在t-sql中运行,因为select c。*的列不在group by子句中
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;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.