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

SQL联接:选择一对多关系中的最后记录,sql,select,join,indexing,greatest-n-per-group,Sql,Select,Join,Indexing,Greatest N Per Group,假设我有一张顾客表和一张购物表。每次购买属于一个客户。我想在一个SELECT语句中获得所有客户及其上次购买的列表。最佳做法是什么?关于建筑索引有什么建议吗 请在回答中使用以下表格/列名: 客户:id,name 采购:id,客户id,商品id,日期 在更复杂的情况下,将最后一次购买放在customer表中,对数据库进行非规范化(性能方面)是否有益 如果(购买)id保证按日期排序,那么可以使用类似于LIMIT 1?的方法简化语句吗这是定期出现在StackOverflow上的每个组最大n个问题的一

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

请在回答中使用以下表格/列名:

  • 客户:
    id
    name
  • 采购:
    id
    客户id
    商品id
    日期
在更复杂的情况下,将最后一次购买放在customer表中,对数据库进行非规范化(性能方面)是否有益


如果(购买)
id
保证按日期排序,那么可以使用类似于
LIMIT 1

的方法简化语句吗这是定期出现在StackOverflow上的
每个组最大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;
选择c.*,p1*
来自客户c
在(c.id=p1.customer\U id)上加入采购p1
左侧外部连接购买p2开启(c.id=p2.customer_id和
(p1.date
说明:给定一行
p1
,不应存在具有相同客户和更晚日期的行
p2
(或者在领带的情况下,不应存在更晚的
id
)。当我们发现这是真的时,
p1
就是该客户最近购买的产品

关于索引,我会在
purchase
列(
customer\u id
date
id
)上创建一个复合索引。这可能允许使用覆盖索引进行外部连接。确保在您的平台上进行测试,因为优化依赖于实现。使用RDBMS的功能分析优化计划。例如,MySQL上的
EXPLAIN



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

您也可以尝试使用子选择来实现这一点

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

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

您尚未指定数据库。如果它允许分析功能,那么使用这种方法可能比逐个分组更快(在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

另一种方法是在加入条件中使用
notexists
条件来测试以后的购买:

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

我发现这个线程可以解决我的问题

但当我尝试它们时,性能很低。下面是我对提高绩效的建议

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 
希望这会有所帮助。

请尝试一下

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;

试试这个,会有帮助的

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

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]
在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()
aggregate函数将确保从每个组中选择最新的购买(但假设日期列的格式是max()给出最新的-通常情况下是这样)。如果您希望处理同一日期的购买,则可以使用
max(p.date,p.id)

在索引方面,我会在购买时使用一个索引(customer_id,date,[您希望在选择中返回的任何其他购买列])


左侧外部联接
(与
内部联接
相反)将确保还包括从未购买过的客户。

如果使用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

请注意,
上的
不同字段(此处为
customer\u id
)必须与
ORDER BY
子句中最左边的字段匹配


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

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

  • 转到具有同一
    客户机的多条记录的
    交易

  • 使用
    group by clientID
    max(transactionDate)

  • internal 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) 
    
  • 您可以使用步骤3中的结果联接任何希望获得不同结果的表


  • 我需要你所需要的,尽管很多年后,我尝试了两个最流行的答案。这些并没有产生想要的果实。这就是我要提供的。。。为了清楚起见,我改了一些名字

    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;
    

    在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
    )
    
    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
    )
    
    SQL Server Fiddle:

    在MySQL上,您可以使用:

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

    总体上是有利的。但这取决于您使用的数据库的品牌,以及数据库中数据的数量和分布。获得准确答案的唯一方法是根据您的数据测试这两种解决方案。如果您想包括从未购买过产品的客户,请将(c.id=p1.customer_id)上的加入购买p1更改为(c.id=p1.customer_id)@russds上的加入购买p1,您需要一些唯一的列来解决问题。在关系数据库中有两个相同的行是没有意义的。“WHERE p2.id为NULL”的目的是什么?此解决方案仅在存在多个采购记录时有效。如果存在1:1链接,则无法工作。这里必须是“WHERE(p2.id为NULL或p1.id=p2.id)是的,它可能值得去规范化(如果它提高了很多性能,这只能通过测试两个版本来发现)。但是去规范化的缺点通常值得避免。相关:这是问题的错误答案