聚合 SQL 函数以仅从每个组中获取第一个

Posted

技术标签:

【中文标题】聚合 SQL 函数以仅从每个组中获取第一个【英文标题】:Aggregate SQL Function to grab only the first from each group 【发布时间】:2010-10-20 21:07:15 【问题描述】:

我有 2 个表 - 一个 Account 表和一个 Users 表。每个帐户可以有多个用户。我有一个场景,我想对这两个表执行一个查询/连接,但我想要所有的帐户数据 (Account.*) 和只有 first 组用户数据(特别是他们的名字)。

我不想在我的聚合组上做一个“最小”或“最大”,而是做一个“第一个”。但是,显然,TSQL 中没有“First”聚合函数。

关于如何获取此查询的任何建议?显然,很容易得到 Account x Users 的笛卡尔积:

 SELECT User.Name, Account.* FROM Account, User
 WHERE Account.ID = User.Account_ID

但是我怎么可能只根据他们的 User.ID 的顺序从产品中获取第一个用户呢?

【问题讨论】:

SQL Server 的情况更糟,因为它没有 FIRST。我还没有听到关于为什么它在 SQL Server 中不存在的令人信服的解释。有时它们的顺序无关紧要(如果它们在特定组的列中都具有相同的值),有时它会(并且它们是有序的)。无论哪种方式 FIRST() 都有用处。 【参考方案1】:
SELECT (SELECT TOP 1 Name 
        FROM User 
        WHERE Account_ID = a.AccountID 
        ORDER BY UserID) [Name],
       a.*
FROM Account a

【讨论】:

但是,这种方法将为每个帐户行执行另一个选择语句。如果你有1000个账户,你的查询会执行1001条独立的select语句) 对于小桌子来说没什么大不了的,但你的解决方案更好:)【参考方案2】:

与其分组,不如这样进行......

select
    *

from account a

join (
    select 
        account_id, 
        row_number() over (order by account_id, id) - 
            rank() over (order by account_id) as row_num from user
     ) first on first.account_id = a.id and first.row_num = 0

【讨论】:

有趣,我没有意识到你可以做类似 first.row_num = 0 的事情 我看到你在这里使用了 Rank(),然后从 Row_Number() 中减去它,然后寻找 0。我将只使用 Row_Number()(按 Account_ID 分区)并在 Row_Num = 上过滤1.结果将是相同的(并且可能在技术上更快)。请参阅@AaronLS 的示例:***.com/a/9220232/555798 @MikeTeeVee 同意;这是一个更好的解决方案,如果我今天解决了这个问题,我会想出这个解决方案。 我不明白 row_num = 0 / 1 部分。我解决了删除这样的部分并在外部查询中使用 where 子句【参考方案3】:

有很多方法可以做到这一点,这里有一种快速而肮脏的方法。

Select (SELECT TOP 1 U.Name FROM Users U WHERE U.Account_ID = A.ID) AS "Name,
    A.*
FROM Account A

【讨论】:

【参考方案4】:

定义“第一”。您首先想到的巧合通常与聚集索引顺序有关,但不应依赖(您可以设计破坏它的示例)。

你不使用 MAX() 或 MIN() 是对的。虽然很诱人,但请考虑您的名字和姓氏在不同字段中的情况。您可能会从不同的记录中获得名称。

因为听起来您真正关心的是您只为每个组获得一个任意记录,您可以做的只是 MIN 或 MAX 该记录的 ID 字段,然后将该表加入到该 ID 的查询中.

【讨论】:

他首先根据他们的用户 ID 说【参考方案5】:

Sql Server 2005 或 2008 中不存在 First 和 Last,但 Sql Server 2012 中有 First_Value, Last_Value 函数。我尝试为 Sql Server 2005 实现聚合 First 和 Last 并遇到障碍,即 sql server 确实保证以定义的顺序计算聚合。 (见属性 SqlUserDefinedAggregateAttribute.IsInvariantToOrder 属性,未实现。)这可能是因为查询分析器尝试在多个线程上执行聚合的计算并组合结果,这加快了执行速度,但不保证顺序聚合了哪些元素。

【讨论】:

欢迎来到 Stack Overflow!在发布多个问题的复制和粘贴样板/逐字答案时要小心,这些往往被社区标记为“垃圾邮件”。如果您这样做,则通常意味着问题是重复的,因此请标记它们。【参考方案6】:
Select *
From Accounts a
Left Join (
    Select u.*, 
    row_number() over (Partition By u.AccountKey Order By u.UserKey) as Ranking
    From Users u
  ) as UsersRanked
  on UsersRanked.AccountKey = a.AccountKey and UsersRanked.Ranking = 1

这可以通过使用 Partition By 子句来简化。在上面,如果一个帐户有三个用户,那么子查询将它们编号为 1、2 和 3,对于不同的 AccountKey,它将重置编号。这意味着对于每个唯一的 AccountKey,总会有一个 1,可能还有 2、3、4 等。

因此,您在 Ranking=1 上进行过滤以从每个组中获取第一个。

这将为您每个帐户提供一行,如果该帐户至少有一个用户,那么它将为您提供具有最低键的用户(因为我使用左连接,您将始终获得一个帐户列表即使没有用户存在)。如果您希望按字母顺序或其他标准选择第一个用户,请将 Order By u.UserKey 替换为另一个字段。

【讨论】:

【参考方案7】:

我知道我的回答有点晚了,但这可能对其他人有所帮助。有一种方法可以在 SQL Server 中实现 First() 和 Last(),这里是:

Stuff(Min(Convert(Varchar, DATE_FIELD, 126) + Convert(Varchar, DESIRED_FIELD)), 1, 23, '')

对 First() 使用 Min(),对 Last() 使用 Max()。 DATE_FIELD 应该是确定它是第一条记录还是最后一条记录的日期。 DESIRED_FIELD 是您想要的第一个或最后一个值的字段。它的作用是:

    在字符串开头添加 ISO 格式的日期(23 个字符长) 将 DESIRED_FIELD 附加到该字符串 获取该字段的 MIN/MAX 值(因为它以日期开头,您将获得第一条或最后一条记录) 填充连接字符串以删除前 23 个字符(日期部分)

给你!

编辑:我遇到了第一个公式的问题:当 DATE_FIELD 的毫秒数为 .000 时,SQL Server 将日期作为字符串返回,根本没有毫秒数,因此从 DESIRED_FIELD 中删除了前 4 个字符。我只是将格式更改为“20”(没有毫秒),效果很好。唯一的缺点是,如果您有两个在同一秒内创建的字段,则排序可能会很混乱……在这种情况下,您可以将格式恢复为“126”。

Stuff(Max(Convert(Varchar, DATE_FIELD, 20) + Convert(Varchar, DESIRED_FIELD)), 1, 19, '')

编辑 2:我最初的意图是返回最后一个(或第一个)NON NULL 行。我被问到如何返回最后一行或第一行,无论它是否为空。只需将 ISNULL 添加到 DESIRED_FIELD。当您使用 + 运算符连接两个字符串时,当其中一个为 NULL 时,结果为 NULL。所以使用以下内容:

Stuff(Max(Convert(Varchar, DATE_FIELD, 20) + IsNull(Convert(Varchar, DESIRED_FIELD), '')), 1, 19, '')

【讨论】:

我注意到使用 min() 而不是使用前 1 的嵌套选择会显着降低性能。我认为原因是 min 迭代整个数据集,其中前 1 只取第一个它遇到了。【参考方案8】:

Dominic Goulet 的 STUFF 反应非常巧妙。但是,如果您的 DATE_FIELD 是 SMALLDATETIME(而不是 DATETIME),那么 ISO 8601 长度将为 19 而不是 23(因为 SMALLDATETIME 没有毫秒) - 因此请相应地调整 STUFF 参数,否则 STUFF 函数的返回值将不正确(缺少前四个字符)。

【讨论】:

感谢您的评论!几周前我也注意到了,更新了我的答案。当您的日期时间以 .000 为毫秒时也会发生这种情况,它们只是被剥离并且您丢失了前 4 个字符。我将格式从 126 更改为 20 以始终减少毫秒,现在效果很好!【参考方案9】:

您可以使用 OUTER APPLY,请参阅documentation。

SELECT User1.Name, Account.* FROM Account
OUTER APPLY 
    (SELECT  TOP 1 Name 
    FROM [User]
    WHERE Account.ID = [User].Account_ID
    ORDER BY Name ASC) User1

【讨论】:

【参考方案10】:

我已经对所有方法进行了基准测试,实现这一目标的最简单和最快的方法是使用外部/交叉应用

SELECT u.Name, Account.* FROM Account
OUTER APPLY (SELECT TOP 1 * FROM User WHERE Account.ID = Account_ID ) as u

CROSS APPLY 的工作方式类似于 INNER JOIN 并获取两个表相关的行,而 OUTER APPLY 的工作方式类似于 LEFT OUTER JOIN 并从左表中获取所有行(此处为帐户)

【讨论】:

此查询可能会给出不一致的结果。没有 SORT BY 的 SELECT TOP 1 可以返回任何匹配的查询,这取决于 SqlServer 引擎。因此,这样的结果可以给出“随机结果”。【参考方案11】:

(有点离题,但是)我经常运行聚合查询来列出异常摘要,然后我想知道为什么客户会出现在结果中,所以使用 MIN 和 MAX 给出 2 个半随机样本,我可以详细看看例如

SELECT Customer.Id, COUNT(*) AS ProblemCount
      , MIN(Invoice.Id) AS MinInv, MAX(Invoice.Id) AS MaxInv
FROM Customer
INNER JOIN Invoice on Invoice.CustomerId = Customer.Id
WHERE Invoice.SomethingHasGoneWrong=1
GROUP BY Customer.Id

【讨论】:

【参考方案12】:

使用返回每个帐户的第一个用户的子选择“FirstUser”创建和加入

SELECT User.Name, Account.* 
FROM Account, User, 
 (select min(user.id) id,account_id from User group by user.account_id) as firstUser
WHERE Account.ID = User.Account_ID 
 and User.id = firstUser.id and Account.ID = firstUser.account_id

【讨论】:

以上是关于聚合 SQL 函数以仅从每个组中获取第一个的主要内容,如果未能解决你的问题,请参考以下文章

使用 activerecord 发出请求以仅从组中获取用户,而不从其他人中获取

sql 取聚合函数的值和每个分组的第一个元素

窗口函数从每个组中获取第一行和最后一行

pandas使用groupby.first函数groupby.nth函数获取每个组中的第一个值实战:groupby.first函数和groupby.nth函数对比(对待NaN的差异)

如何仅从 React 中的 API 获取第一个数据?

Apache Spark Group By(获取组中的第一个和最后一个值)