如何在 T/SQL 中按一个列进行分组,并检索另一列中最小值的行?

14

我知道这是一个相当愚蠢的问题,但是(如标题所述)我想知道如何执行以下操作:

我有一个像这样的表格:

ID Foo Bar Blagh
----------------
1  10  20  30
2  10  5   1
3  20  50  40
4  20  75  12

我想按Foo分组,然后提取Bar最小的行,即我想要以下结果:

ID Foo Bar Blagh
----------------
2  10  5   1
3  20  50  40

我真的无法弄清楚正确的SQL语句来检索这个。 我想要的是:

SELECT ID, Foo, Bar, Blagh
FROM Table
GROUP BY Foo
HAVING(MIN(Bar))

然而,这显然不起作用,因为那是完全无效的 HAVING 语法,而且 Foo、Bar 和 Blagh 没有被聚合。

我做错了什么?

6个回答

12

这个链接几乎是相同的问题,但已经有了一些答案!

以下是我模拟的表格:

declare @Borg table (
    ID int,
    Foo int,
    Bar int,
    Blagh int
)
insert into @Borg values (1,10,20,30)
insert into @Borg values (2,10,5,1)
insert into @Borg values (3,20,50,70)
insert into @Borg values (4,20,75,12)

那么你可以进行匿名内连接来获取所需的数据。

select B.* from @Borg B inner join 
(
    select Foo,
        MIN(Bar) MinBar 
    from @Borg 
    group by Foo
) FO
on FO.Foo = B.Foo and FO.MinBar = B.Bar

编辑Adam Robinson 指出,“此解决方案有可能在 Bar 的最小值重复时返回多行,并且会消除任何值为null的foo”

根据您的用例,当 Bar 重复时具有重复值可能是有效的-如果您想找到所有 Bar 最小的 Borg 值,则同时拥有两个结果似乎是正确的方法。

如果您需要捕获在聚合字段中的NULLs(在此情况下通过MIN聚合),则可以将 NULL 与可接受的高(或低)值进行coalesce(这是一种hack):

...
MIN(coalesce(Bar,1000000)) MinBar -- A suitably high value if you want to exclude this row, make it suitably low to include
...

或者你可以使用UNION将所有这样的值附加到结果集的底部。

on FO.Foo = B.Foo and FO.MinBar = B.Bar
union select * from @Borg where Bar is NULL

由于不知道如何在相同的Foo值之间进行选择,因此后者将不会将@Borg中具有相同Foo值的值分组。


2
顽皮。我在评论时编辑。 - gbn
1
@gbn 我正在为我的回答“增值”! :) - butterchicken
1
嘿,我很欣赏这种增值的贡献!!:) - ljs
4
请记住,此解决方案可能会在 Bar 的最小值重复时返回多行,并且会消除 bar 为 null 的任何 foo 值。 - Adam Robinson
哦,我不知道那些注意事项;@butterchicken - 或许值得将它们添加到解决方案中,以便其他用户知道? - ljs
显示剩余2条评论

3
select 
    ID, 
    Foo, 
    Bar, 
    Blagh
from Table
join (
    select 
    ID, 
    (row_number() over (order by foo, bar) - rank() over (order by foo)) as rowNumber
) t on t.ID = Table.ID and t.rowNumber = 0 

这将再次加入表格,但这次会为bar的值添加一个相对行号,就像在每个foo的值内按升序排序一样。通过过滤rowNumber = 0,它仅选择每个foo值的最低bar值。这也有效地消除了group by子句,因为您现在只检索每个foo的一行。


值得一提的是,在引入row_number()函数之前的MSSQL2005版本中,此方法不可行。 - butterchicken

1

我的理解是你不能一次性完成这个。

select Foo, min(Bar) from table group by Foo

使用“GROUP BY”可以获取每个不同Foo的最小Bar。但是,你无法将该最小值与特定ID相对应,因为可能会有多个具有相同Bar值的行。

您可以尝试类似以下的方法:

select * from Table t
join (
   select Foo, min(Bar) as minbar
   from Table group by Foo
) tt on t.Foo=tt.Foo and t.Bar=tt.minbar

请注意,如果有多行具有最小的 Bar 值,则使用上述查询将获取它们所有。
现在,我并不是声称自己是 SQL 大师,而且我所在的地方已经很晚了,可能会漏掉一些东西,但这就是我的看法。

1

这可能会有所帮助:

DECLARE @Table TABLE(
        ID INT,
        Foo FLOAT,
        Bar FLOAT,
        Blah FLOAT
)

INSERT INTO @Table (ID,Foo,Bar,Blah) SELECT 1, 10 ,20 ,30
INSERT INTO @Table (ID,Foo,Bar,Blah) SELECT 2, 10 ,5 ,1
INSERT INTO @Table (ID,Foo,Bar,Blah) SELECT 3, 20 ,50 ,40
INSERT INTO @Table (ID,Foo,Bar,Blah) SELECT 4, 20 ,75 ,12

SELECT  t.*
FROM    @Table t INNER JOIN
        (
            SELECT  Foo,
                    MIN(Bar) MINBar
            FROM    @Table
            GROUP BY Foo
        ) Mins ON t.Foo = Mins.Foo
                AND t.Bar = Mins.MINBar

1

另一个选择可能是以下内容:

DECLARE @TEST TABLE(    ID int,    Foo int,    Bar int,    Blagh int)
INSERT INTO @TEST VALUES (1,10,20,30)
INSERT INTO @TEST VALUES (2,10,5,1)
INSERT INTO @TEST VALUES (3,20,50,70)
INSERT INTO @TEST VALUES (4,20,75,12)

SELECT Id, Foo, Bar, Blagh 
FROM (
      SELECT id, Foo, Bar, Blagh, CASE WHEN (Min(Bar) OVER(PARTITION BY FOO) = Bar) THEN 1 ELSE 0 END as MyRow
      FROM @TEST) t
WHERE MyRow = 1

虽然这仍需要一个子查询,但消除了连接的需要。

只是另一个选项。


-1
declare @Borg table (
    ID int,
    Foo int,
    Bar int,
    Blagh int
)
insert into @Borg values (1,10,20,30)
insert into @Borg values (2,10,5,1)
insert into @Borg values (3,20,50,70)
insert into @Borg values (4,20,75,12)

select * from @Borg

select Foo,Bar,Blagh from @Borg b 
where Bar = (select MIN(Bar) from @Borg c where c.Foo = b.Foo)

不要复制他人的答案。 - Mohammed mansoor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接