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

时间:2010-01-21 17:29:51

标签: sql select join indexing greatest-n-per-group

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

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

  • customer:id,name
  • 购买:id,customer_id,item_id,日期

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

如果(购买)ID保证按日期排序,可以使用LIMIT 1之类的内容简化陈述吗?

12 个答案:

答案 0 :(得分:376)

这是在StackOverflow上定期出现的greatest-n-per-group问题的示例。

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

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的功能来分析优化计划。例如。关于MySQL的EXPLAIN


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

答案 1 :(得分:106)

你也可以尝试使用子选择

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

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

答案 2 :(得分: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

答案 3 :(得分:18)

另一种方法是在您的加入条件中使用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
   )
)

答案 4 :(得分:11)

我发现这个帖子是我问题的解决方案。

但是当我尝试它们时,性能很低。贝娄是我提出更好表现的建议。

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 

希望这会有所帮助。

答案 5 :(得分:4)

试试这个,这会有所帮助。

我在我的项目中使用了这个。

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]

答案 6 :(得分: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,date,[您想在选择中返回的任何其他购买列])。

LEFT OUTER JOIN(与INNER JOIN相对)将确保包含从未购买过的客户。

答案 7 :(得分: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;

答案 8 :(得分:1)

如果您使用的是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子句中最左边的字段匹配。

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

答案 9 :(得分:0)

不首先进入代码,逻辑/算法如下:

  1. 转到transaction表,其中包含同一client的多个记录。

  2. 使用clientIDlatestDate

    选择客户活动的group by clientIDmax(transactionDate)记录

       select clientID, max(transactionDate) as latestDate 
       from transaction 
       group by clientID
    
  3. inner join transaction表以及步骤2的结果,那么您将拥有transaction表的完整记录,并且只有每个客户的最新记录。

       select * from 
       transaction t 
       inner join (
         select clientID, max(transactionDate) as latestDate
         from transaction 
         group by clientID) d 
       on t.clientID = d.clientID and t.transactionDate = d.latestDate) 
    
  4. 您可以使用第3步中的结果来联接要获得不同结果的任何表。

答案 10 :(得分:0)

我需要你需要的东西,尽管是多年以后,并尝试了两个最受欢迎的答案。这些没有产生想要的果实。所以这就是我必须提供的...为清楚起见,我更改了一些名称。

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;

答案 11 :(得分:0)

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
)

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
)

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