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

net*_*ope 268 sql indexing select join greatest-n-per-group

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

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

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

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

如果(购买)ID保证按日期排序,是否可以通过使用类似的方式简化语句LIMIT 1

Bil*_*win 416

这是greatest-n-per-groupStackOverflow上经常出现的问题的一个示例.

以下是我通常建议解决的方法:

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;
Run Code Online (Sandbox Code Playgroud)

说明:给定一行p1,不应该有p2相同客户的行和更晚的日期(或者在关系的情况下,稍后id).当我们发现这是真的时,则p1是该客户最近的购买.

对于指数,我会在创建复合指数purchase在列(customer_id,date,id).这可以允许使用覆盖索引来完成外连接.请务必在您的平台上进行测试,因为优化与实现有关.使用RDBMS的功能来分析优化计划.例如EXPLAIN在MySQL上.


有些人使用子查询而不是我上面显示的解决方案,但我发现我的解决方案可以更容易地解决关系.

  • 如果您想包括从未进行过购买的客户,请将JOIN购买p1 ON(c.id = p1.customer_id)改为LEFT JOIN购买p1 ON(c.id = p1.customer_id) (21认同)
  • 它与性能的子选择相比如何? (8认同)
  • "WHERE p2.id IS NULL"的目的是什么? (6认同)
  • @russds,你需要一些独特的专栏来解决这个问题.在关系数据库中有两个相同的行是没有意义的. (4认同)
  • @b.lit我相信“WHERE p2.id IS NULL”的目的是隔离购买表中的最后一条记录。当我们到达表的末尾时,p1 指向最后一条记录,p2 指向下一条记录。最后一条记录没有下一条记录,因此该记录的 id 为空。 (4认同)
  • 总的来说,有利的是.但这取决于您使用的数据库品牌,以及数据库中数据的数量和分布.获得精确答案的唯一方法是根据数据测试两种解决方案. (3认同)
  • 仅当购买记录超过1条时,此解决方案才有效。ist有1:1链接,它不起作用。那里必须是“哪里”(p2.id为NULL或p1.id = p2.id) (2认同)

Adr*_*der 111

您也可以尝试使用子选择执行此操作

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
Run Code Online (Sandbox Code Playgroud)

选择应加入所有客户及其上次购买日期.

  • 谢谢这只是救了我 - 这个解决方案似乎比其他列出的更具可行性和可维护性+它不是产品特定的 (4认同)
  • @clu:将`INNER JOIN`改为`LEFT OUTER JOIN`. (3认同)
  • 看起来这是假设当天只有一次购买。如果有两个,您将为一个客户获得两个输出行,我想呢? (3认同)

Mad*_*mir 24

您尚未指定数据库.如果它是允许分析函数的那个​​,那么使用这种方法可能比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
Run Code Online (Sandbox Code Playgroud)

  • 这是问题的错误答案,因为您使用的是"RANK()"而不是"ROW_NUMBER()".当两次购买具有完全相同的日期时,RANK仍然会给你相同的关系问题.这就是排名功能的作用; 如果前2个匹配,则它们都被赋值为1,第3个记录的值为3.对于Row_Number,没有平局,它对于整个分区是唯一的. (9认同)
  • 在这里尝试了Bill Karwin对Madalina方法的处理方法,在sql server 2008下启用了执行计划我发现Bill Karwin的apprach的查询成本为43%,而Madalina的方法使用了57% - 所以尽管这个答案的语法更优雅,我仍然会赞成比尔的版本! (4认同)

Ste*_*erl 23

另一种方法是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
   )
)
Run Code Online (Sandbox Code Playgroud)

  • 对我来说,这是“最具可读性”的解决方案。如果这很重要。 (2认同)
  • 当Id是唯一标识符(guid)时,不能使用它。 (2认同)

Tat*_*ton 23

如果您使用的是 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
Run Code Online (Sandbox Code Playgroud)

PostgreSQL Docs - Distinct On

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

警告:这是一个非标准条款。

  • psql 的出色且高性能的解决方案。谢谢! (2认同)

Mat*_*hee 16

我发现这个线程是我问题的解决方案.

但是当我尝试它们时,性能很低.贝娄是我提高表现的建议.

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 
Run Code Online (Sandbox Code Playgroud)

希望这会有所帮助.

  • 错误的答案。它仅提供“购买”表中的最新日期列。OP要求完整记录 (3认同)
  • 这是简单直接的解决方案,就我而言(很多客户,很少购买)比 @Stefan Haberl 的解决方案快 10%,比接受的答案好 10 倍以上 (2认同)

Rah*_*ari 7

试试这个,会有所帮助。

我在项目中使用了这个。

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]
Run Code Online (Sandbox Code Playgroud)


Dan*_*eks 7

我需要你所需要的东西,尽管已经过去很多年了,并且尝试了两个最流行的答案。这些都没有结出想要的果实。这就是我必须提供的...为了清楚起见,我更改了一些名称。

SELECT 
  cc.pk_ID AS pk_Customer_ID, 
  cc.Customer_Name AS Customer_Name, 
  IFNULL(pp.pk_ID, '') AS fk_Purchase_ID,
  IFNULL(pp.fk_Customer_ID, '') AS fk_Customer_ID,
  IFNULL(pp.fk_Item_ID, '') AS fk_Item_ID,
  IFNULL(pp.Purchase_Date, '') AS Purchase_Date
FROM customer cc
LEFT JOIN purchase pp ON (
  SELECT zz.pk_ID 
  FROM purchase zz 
  WHERE cc.pk_ID = zz.fk_Customer_ID 
  ORDER BY zz.Purchase_Date DESC LIMIT 1) = pp.pk_ID
ORDER BY cc.pk_ID;
Run Code Online (Sandbox Code Playgroud)

  • 谢谢兄弟。这工作完美 (4认同)
  • 我有一个情况,我必须连接许多表,并且有 2 个表我使用了一对多关系。这实际上解决了我的问题 (2认同)

cel*_*owm 7

在SQL Server上您可以使用:

SELECT *
FROM customer c
INNER JOIN purchase p on c.id = p.customer_id
WHERE p.id = (
    SELECT TOP 1 p2.id
    FROM purchase p2
    WHERE p.customer_id = p2.customer_id
    ORDER BY date DESC
)
Run Code Online (Sandbox Code Playgroud)

SQL Server 小提琴:http://sqlfiddle.com/#!18/262fd /2

在MySQL上你可以使用:

SELECT c.name, date
FROM customer c
INNER JOIN purchase p on c.id = p.customer_id
WHERE p.id = (
    SELECT p2.id
    FROM purchase p2
    WHERE p.customer_id = p2.customer_id
    ORDER BY date DESC
    LIMIT 1
)
Run Code Online (Sandbox Code Playgroud)

MySQL小提琴:http://sqlfiddle.com/#!9/202613/7


Mar*_*ark 5

在 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
Run Code Online (Sandbox Code Playgroud)

聚合max()函数将确保从每个组中选择最新的购买(但假设日期列的格式是 max() 给出最新的 - 通常是这种情况)。如果您想处理同一日期的购买,那么您可以使用max(p.date, p.id)

就索引而言,我将在购买时使用索引(customer_id、日期、[您想要在选择中返回的任何其他购买列])。

LEFT OUTER JOIN与 相对INNER JOIN)将确保从未购买过的客户也包括在内。

  • 不会在 t-sql 中运行,因为 select c.* 的列不在 group by 子句中 (2认同)
  • 我还发现这在 SQLite 中也有效。我在它的文档(非常全面)中搜索了一些注释,说它应该可以工作,但找不到任何东西。所以不能保证它会在未来的更新中起作用(除非你能找到我错过的东西)。 (2认同)