【问题标题】:SQL join: selecting the last records in a one-to-many relationshipSQL join:选择一对多关系中的最后一条记录
【发布时间】:2011-01-07 20:34:26
【问题描述】:

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

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

  • 客户:idname
  • 购买:id,customer_id,item_id,date

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

如果(购买)id 保证按日期排序,是否可以使用LIMIT 1 之类的东西来简化语句?

【问题讨论】:

  • 是的,它可能值得非规范化(如果它大大提高了性能,你只能通过测试两个版本来发现)。但非规范化的缺点通常值得避免。

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


【解决方案1】:

这是 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 的功能来分析优化计划。例如。 EXPLAIN 在 MySQL 上。


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

【讨论】:

  • 总体来说还不错。但这取决于您使用的数据库品牌,以及数据库中数据的数量和分布。获得准确答案的唯一方法是针对您的数据测试这两种解决方案。
  • 如果要包括从未购买过的客户,请将 JOIN purchase p1 ON (c.id = p1.customer_id) 更改为 LEFT JOIN purchase p1 ON (c.id = p1.customer_id)
  • @russds,您需要一些独特的列来解决平局问题。在关系数据库中拥有两个相同的行是没有意义的。
  • “WHERE p2.id IS NULL”的目的是什么?
  • 此解决方案仅适用于购买记录超过 1 条的情况。如果有 1:1 链接,它不起作用。它必须是“WHERE (p2.id IS NULL or p1.id=p2.id)
【解决方案2】:

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

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

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

【讨论】:

  • 谢谢,这救了我 - 这个解决方案似乎比其他列出的解决方案更易于维护和维护 + 它不是特定于产品的
  • 如果我想在没有购买的情况下获得客户,我该如何修改?
  • @clu:将INNER JOIN 更改为LEFT OUTER JOIN
  • 看起来这是假设当天只有一次购买。如果有两个,我想你会为一个客户得到两个输出行?
  • @IstiaqueAhmed - 最后一个 INNER JOIN 采用该 Max(date) 值并将其绑定回源表。如果没有该连接,您从 purchase 表中获得的唯一信息是日期和 customer_id,但查询会询问表中的所有字段。
【解决方案3】:

另一种方法是在您的联接条件中使用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 部分吗?
  • 子选择只是检查是否存在具有更高 id 的行。如果没有找到具有更高 id 的行,您只会在结果集中获得一行。那应该是唯一最高的。
  • 这对我来说是最易读的解决方案。如果这很重要。
  • :) 谢谢。我总是力求最易读的解决方案,因为这很重要。
  • 当 Id 是唯一标识符(guid)时,不能使用。
【解决方案4】:

您尚未指定数据库。如果它是一种允许分析函数的方法,那么使用这种方法可能比使用 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

【讨论】:

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

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

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

【讨论】:

  • psql 的出色且高性能的解决方案。谢谢!
  • 你是我的救星!!
【解决方案6】:

我发现这个帖子可以解决我的问题。

但是当我尝试它们时,性能很低。下面是我对更好性能的建议。

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 by MaxDate desc
  • 这是一个简单直接的解决方案,在我的情况下(很多客户,很少购买)比@Stefan Haberl 的解决方案快 10%,比接受的答案好 10 倍以上
  • 很好的建议使用公用表表达式 (CTE) 来解决这个问题。这极大地提高了许多情况下的查询性能。
  • 最佳答案 imo,易于阅读,与 ORDER BY + LIMIT 1 相比,MAX() 子句具有出色的性能
  • 错误答案。它仅提供表purchase 中的最新日期列。 OP 要求提供完整记录
【解决方案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]

【讨论】:

  • 别名“p”从何而来?
  • 这表现不佳....在我拥有的数据集上其他示例花费了 2 秒的地方永远持续了......
  • 这是我的数据集性能最高的选项。
【解决方案8】:

在 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 子句中
  • 我也发现这在 SQLite 中有效。我搜索了它的文档(非常全面),以获取一些说明它应该可以工作但找不到任何东西的说明。所以不能保证它会在未来的更新中起作用(除非你能找到我错过的东西)。
【解决方案9】:

请试试这个,

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;

【讨论】:

    【解决方案10】:

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

    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;
    

    【讨论】:

    • 谢谢兄弟。这是完美的工作
    • 我有一个条件,我必须加入许多表,并且在 2 处我使用了一对多关系。这实际上解决了我的问题
    【解决方案11】:

    没有先进入代码,逻辑/算法如下:

    1. 转到具有相同client 的多条记录的transaction 表。

    2. 使用group by clientIDmax(transactionDate)选择客户活动的clientIDlatestDate的记录

         select clientID, max(transactionDate) as latestDate 
         from transaction 
         group by clientID
      
    3. inner jointransaction 表与第 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 步的结果来加入您想要获得不同结果的任何表。

    【讨论】:

      【解决方案12】:

      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

      【讨论】:

        【解决方案13】:

        表格:

        Customer => id, name
        Purchase => id, customer_id, item_id, date
        

        查询:

        SELECT C.id, C.name, P.id, P.date
          FROM customer AS C
          LEFT JOIN purchase AS P ON 
            (
              P.customer_id = C.id 
              AND P.id IN (
                SELECT MAX(PP.id) FROM purchase AS PP GROUP BY PP.customer_id
              )
            )
        

        你也可以在sub select查询中指定一些条件

        【讨论】:

          猜你喜欢
          • 2014-06-30
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-09-02
          • 2018-06-26
          • 2016-02-14
          相关资源
          最近更新 更多