从几列中选择最小值的最佳方法是啥?

Posted

技术标签:

【中文标题】从几列中选择最小值的最佳方法是啥?【英文标题】:What's the best way to select the minimum value from several columns?从几列中选择最小值的最佳方法是什么? 【发布时间】:2010-09-26 23:16:38 【问题描述】:

鉴于 SQL Server 2005 中的下表:

ID   Col1   Col2   Col3
--   ----   ----   ----
1       3     34     76  
2      32    976     24
3       7    235      3
4     245      1    792

编写产生以下结果的查询的最佳方法是什么(即产生最后一列的查询 - 一列包含 Col1、Col2 和 Col 3 每行的最小值)?

ID   Col1   Col2   Col3  TheMin
--   ----   ----   ----  ------
1       3     34     76       3
2      32    976     24      24
3       7    235      3       3
4     245      1    792       1

更新:

为了澄清(正如我在评论中所说),在实际场景中,数据库已正确规范化。这些“数组”列不在实际表中,而是在报表所需的结果集中。而新的要求是报表也需要这个 MinValue 列。我无法更改底层结果集,因此我在寻找 T-SQL 以获得方便的“越狱卡”。

我尝试了下面提到的 CASE 方法,它确实有效,虽然它有点麻烦。它也比答案中所说的更复杂,因为您需要满足同一行中有两个最小值的事实。

无论如何,我想我会发布我当前的解决方案,考虑到我的限制,它工作得很好。它使用 UNPIVOT 运算符:

with cte (ID, Col1, Col2, Col3)
as
(
    select ID, Col1, Col2, Col3
    from TestTable
)
select cte.ID, Col1, Col2, Col3, TheMin from cte
join
(
    select
        ID, min(Amount) as TheMin
    from 
        cte 
        UNPIVOT (Amount for AmountCol in (Col1, Col2, Col3)) as unpvt
    group by ID
) as minValues
on cte.ID = minValues.ID

我会提前说,我不希望它提供最佳性能,但考虑到这种情况(我不能只为新的 MinValue 列要求重新设计所有查询),它是一个非常优雅的“get出狱卡”。

【问题讨论】:

恕我直言,作者的 UNPIVOT 解决方案优于其他答案。 我发现 Nizam 的解决方案是最精简的解决方案,即使我花了一段时间才开始理解它。精益且非常实用。 【参考方案1】:

可能有很多方法可以做到这一点。我的建议是使用 Case/When 来做。有3列,还不错。

Select Id,
       Case When Col1 < Col2 And Col1 < Col3 Then Col1
            When Col2 < Col1 And Col2 < Col3 Then Col2 
            Else Col3
            End As TheMin
From   YourTableNameHere

【讨论】:

这是我最初的想法。但真正的查询需要 5 列,并且列数可能会增加。所以 CASE 方法变得有点笨拙。但它确实有效。 如果列数可以增加,那么您肯定做错了 - 请参阅我的帖子(关于为什么不应该设置数据库架构的咆哮)方式:-)。 谢谢。正如我在另一条评论中提到的。我不是在查询实际的表。表格已正确标准化。此查询是一个特别复杂的查询的一部分,正在处理派生表的中间结果。 在这种情况下,您能否以不同的方式推导它们以使它们看起来标准化? 添加了来自@Gmastros 的答案,因为我遇到了一些 Cols 具有匹配数据的问题,因此我不得不添加 = 符号。我的数据也有可能为 null,所以我必须添加 or 语句来说明这一点。可能有一种更简单的方法可以做到这一点,但在过去的 6 个月里我一直在寻找一个。感谢所有参与其中的人。 Select Id, CaseWhen (Col1 【参考方案2】:

这是蛮力但有效

 select case when col1 <= col2 and col1 <= col3 then col1
           case when col2 <= col1 and col2 <= col3 then col2
           case when col3 <= col1 and col3 <= col2 then col3
    as 'TheMin'
           end

from Table T

...因为 min() 仅适用于一列而不是跨列。

【讨论】:

它也往往是最快的,因为 MIN 创建了隐式嵌套循环连接。【参考方案3】:

最好的方法可能是这样做 - 人们坚持以需要 SQL“体操”来提取有意义的信息的方式存储数据,这很奇怪,当有远如果您只是更好地构建架构,则可以更轻松地实现所需结果:-)

在我看来,正确的方法是使用下表:

ID    Col    Val
--    ---    ---
 1      1      3
 1      2     34
 1      3     76

 2      1     32
 2      2    976
 2      3     24

 3      1      7
 3      2    235
 3      3      3

 4      1    245
 4      2      1
 4      3    792

ID/Col 作为主键(可能Col 作为额外键,取决于您的需要)。然后您的查询变成一个简单的select min(val) from tbl,您仍然可以通过在其他查询中使用where col = 2 来分别处理各个“旧列”。如果“旧列”的数量增加,这也可以轻松扩展。

这使您的查询如此更加容易。我倾向于使用的一般准则是,如果您曾经在数据库行中有一些看起来像数组的东西,那么您可能做错了什么,应该考虑重组数据。


但是,如果由于某种原因您无法更改这些列,我建议使用插入和更新触发器并添加这些触发器设置的 另一个Col1/2/3 的最小值。这会将操作的“成本”从选择转移到它所属的更新/插入 - 根据我的经验,大多数数据库表的读取频率远远高于写入频率,因此随着时间的推移,写入成本往往会更有效率。

换句话说,一行的最小值仅在其他列之一发生变化时才会发生变化,因此 这是您应该计算它的时间,而不是每次选择时(如果数据不变)。然后你会得到一个像这样的表:

ID   Col1   Col2   Col3   MinVal
--   ----   ----   ----   ------
 1      3     34     76        3
 2     32    976     24       24
 3      7    235      3        3
 4    245      1    792        1

必须在select 时间做出决定的任何其他选项在性能方面通常是一个坏主意,因为数据仅在插入/更新时更改 - 添加另一列会占用数据库中的更多空间,并且将插入和更新的速度稍慢,但选择的速度可能快得多 - 首选方法应取决于您在那里的优先级,但如前所述,大多数表的读取频率比他们写的要多。

【讨论】:

嗯。谢谢你的谩骂。真实数据库已正确规范化。这是一个简单的例子。实际查询很复杂,我感兴趣的 5 列是派生表的中间结果。 不幸的是,谩骂仍然存在。制作您建议的形式的中间表与制作这样的永久表一样有问题。这一点可以从以下事实得到证明:您必须执行我喜欢称之为 SQL 体操的操作才能获得您想要的结果。 如果有真正的原因需要单行中的'数组',请随时启发我们,但使用它来选择最小值不是其中之一。 +1 用于触发器建议以保留原始(如果有缺陷)表结构。 如果您正在处理一个层次表,并自行连接到它上面怎么办?【参考方案4】:

如果你能够创建一个存储过程,它可以接受一个值数组,你可以直接调用它。

【讨论】:

Oracle 有一个名为 LEAST() 的函数,它完全符合您的要求。 感谢您在 :) 中的摩擦,我不敢相信 SQL Server 没有等价物! 我什至想说,“嘿,我最喜欢的 pgsql 也没有,”但它确实有。 ;) 虽然函数本身并不难写。 哦,除了 T-SQL 甚至没有数组支持 (???) 好吧,我想你可以有一个五参数函数,如果你需要更多,只需扩展它...... 【参考方案5】:
select *,
case when column1 < columnl2 And column1 < column3 then column1
when columnl2 < column1 And columnl2 < column3 then columnl2
else column3
end As minValue
from   tbl_example

【讨论】:

这是 G Mastros 回答的副本,所以如果您想知道:我想这就是投票否决的来源。【参考方案6】:

您也可以使用联合查询来执行此操作。随着列数的增加,您将需要修改查询,但至少这是一个直接的修改。

Select T.Id, T.Col1, T.Col2, T.Col3, A.TheMin
From   YourTable T
       Inner Join (
         Select A.Id, Min(A.Col1) As TheMin
         From   (
                Select Id, Col1
                From   YourTable

                Union All

                Select Id, Col2
                From   YourTable

                Union All

                Select Id, Col3
                From   YourTable
                ) As A
         Group By A.Id
       ) As A
       On T.Id = A.Id

【讨论】:

这可行,但当行数增加时性能会下降。 谢谢。是的,这行得通。正如 Tomalak 所说,在我的实数查询中,这对性能来说是非常糟糕的。但是为努力+1。 :)【参考方案7】:

如果你使用 SQL 2005,你可以做一些简洁的事情:

;WITH    res
          AS ( SELECT   t.YourID ,
                        CAST(( SELECT   Col1 AS c01 ,
                                        Col2 AS c02 ,
                                        Col3 AS c03 ,
                                        Col4 AS c04 ,
                                        Col5 AS c05
                               FROM     YourTable AS cols
                               WHERE    YourID = t.YourID
                             FOR
                               XML AUTO ,
                                   ELEMENTS
                             ) AS XML) AS colslist
               FROM     YourTable AS t
             )
    SELECT  YourID ,
            colslist.query('for $c in //cols return min(data($c/*))').value('.',
                                            'real') AS YourMin ,
            colslist.query('for $c in //cols return avg(data($c/*))').value('.',
                                            'real') AS YourAvg ,
            colslist.query('for $c in //cols return max(data($c/*))').value('.',
                                            'real') AS YourMax
    FROM    res

这样你就不会迷失在这么多运算符中:)

但是,这可能比其他选择慢。

这是你的选择......

【讨论】:

好吧,就像我说的,这可能会很慢,但是如果你有太多的列(显然,由于数据库设计非常糟糕!),它可能值得使用(至少对于平均)。你没有给我任何提示,如果它是一头好圣牛还是一头坏牛:)也许你应该使用赞成/反对票来帮助我弄清楚。 这不是一个真正的好或坏的;)。我不是数据库专家,所以我只是说“天哪”,因为这个问题似乎有一个微不足道的答案。我想这是一个很好的解决方案,因为您设法为该问题提供了一种灵活、可扩展的解决方案!【参考方案8】:

this question 并this question 尝试回答这个问题。

总结一下,Oracle 对此有一个内置函数,使用 Sql Server,您要么定义用户定义函数,要么使用 case 语句。

【讨论】:

【参考方案9】:

联合查询的一个小转折:

DECLARE @Foo TABLE (ID INT, Col1 INT, Col2 INT, Col3 INT)

INSERT @Foo (ID, Col1, Col2, Col3)
VALUES
(1, 3, 34, 76),
(2, 32, 976, 24),
(3, 7, 235, 3),
(4, 245, 1, 792)

SELECT
    ID,
    Col1,
    Col2,
    Col3,
    (
        SELECT MIN(T.Col)
        FROM
        (
            SELECT Foo.Col1 AS Col UNION ALL
            SELECT Foo.Col2 AS Col UNION ALL
            SELECT Foo.Col3 AS Col 
        ) AS T
    ) AS TheMin
FROM
    @Foo AS Foo

【讨论】:

【参考方案10】:

如果列是您的示例中的整数,我将创建一个函数:

create function f_min_int(@a as int, @b as int) 
returns int
as
begin
    return case when @a < @b then @a else coalesce(@b,@a) end
end

然后当我需要使用它时,我会这样做:

select col1, col2, col3, dbo.f_min_int(dbo.f_min_int(col1,col2),col3)

如果你有 5 个列,那么上面就变成了

select col1, col2, col3, col4, col5,
dbo.f_min_int(dbo.f_min_int(dbo.f_min_int(dbo.f_min_int(col1,col2),col3),col4),col5)

【讨论】:

鉴于 MSSQL 中标量函数的糟糕表现,我不得不反对这种方法。如果您要走这条路,至少要编写一个函数,一次将所有 5 列作为参数。它仍然会很糟糕,但至少不会那么糟糕 =/ 递归会降低性能。但它会满足要求。【参考方案11】:

如果您知道要查找的值(通常是状态代码),则以下内容可能会有所帮助:

select case when 0 in (PAGE1STATUS ,PAGE2STATUS ,PAGE3STATUS,
PAGE4STATUS,PAGE5STATUS ,PAGE6STATUS) then 0 else 1 end
FROM CUSTOMERS_FORMS

【讨论】:

【参考方案12】:

mysql 上,使用这个:

select least(col1, col2, col3) FROM yourtable

【讨论】:

可能不是 SQL 语句。 但在某些情况下确实如此。对于那些人来说,这是一个很好的答案 自 8.1 起在 Postgres 中可用:postgresql.org/docs/8.1/functions-conditional.html#AEN12704 几乎所有数据库都支持这种非标准 SQL 扩展,除了 Microsoft SQL 服务器。 对于任何使用 SQLLite 的读者,min 函数的工作方式类似于 SQLLite 中的 least,但如果 any 值为 NULL,则为 NULL。见this related question。【参考方案13】:

你可以使用“蛮力”的方法来扭转:

SELECT CASE
    WHEN Col1 <= Col2 AND Col1 <= Col3 THEN Col1
    WHEN                  Col2 <= Col3 THEN Col2
    ELSE                                    Col3
END AS [Min Value] FROM [Your Table]

当第一个条件失败时,它保证 Col1 不是最小值,因此您可以从其余条件中消除它。对于后续条件也是如此。对于五列,您的查询变为:

SELECT CASE
    WHEN Col1 <= Col2 AND Col1 <= Col3 AND Col1 <= Col4 AND Col1 <= Col5 THEN Col1
    WHEN                  Col2 <= Col3 AND Col2 <= Col4 AND Col2 <= Col5 THEN Col2
    WHEN                                   Col3 <= Col4 AND Col3 <= Col5 THEN Col3
    WHEN                                                    Col4 <= Col5 THEN Col4
    ELSE                                                                      Col5
END AS [Min Value] FROM [Your Table]

请注意,如果两列或更多列之间存在关联,则&lt;= 可确保我们尽早退出CASE 语句。

【讨论】:

请改用&lt;=,否则将使用最后一个匹配的最小值而不是第一个。【参考方案14】:

使用CROSS APPLY

SELECT ID, Col1, Col2, Col3, MinValue
FROM YourTable
CROSS APPLY (SELECT MIN(d) AS MinValue FROM (VALUES (Col1), (Col2), (Col3)) AS a(d)) A

SQL Fiddle

【讨论】:

看起来很有趣,但我无法让它工作。你能说清楚一点吗?谢谢 @iDevlop 我在答案中插入了 SQL Fiddle 我不知道的是标量函数。看来您的答案在没有cross apply 的情况下也有效。它是否增加了价值/性能? ***.com/a/14712024/78522 @iDevlop 如果它不能提供性能,它会增加可读性。例如,我可以使用where MinValue &gt; 10 之类的东西,如果没有CROSS APPLY,我就无法做到这一点 确实,与此同时,我有机会了解它的“可重用性”优势。谢谢。我今天学到了两件事;-)【参考方案15】:

下面我使用一个临时表来获取几个日期的最小值。第一个临时表查询几个连接表以获取各种日期(以及查询的其他值),然后第二个临时表使用与日期列一样多的通道获取各种列和最小日期。

这本质上类似于联合查询,需要相同数量的通过,但可能更有效(基于经验,但需要测试)。在这种情况下,效率不是问题(8,000 条记录)。可以索引等。

--==================== this gets minimums and global min
if object_id('tempdb..#temp1') is not null
    drop table #temp1
if object_id('tempdb..#temp2') is not null
    drop table #temp2

select r.recordid ,  r.ReferenceNumber, i.InventionTitle, RecordDate, i.ReceivedDate
, min(fi.uploaddate) [Min File Upload], min(fi.CorrespondenceDate) [Min File Correspondence]
into #temp1
from record r 
join Invention i on i.inventionid = r.recordid
left join LnkRecordFile lrf on lrf.recordid = r.recordid
left join fileinformation fi on fi.fileid = lrf.fileid
where r.recorddate > '2015-05-26'
 group by  r.recordid, recorddate, i.ReceivedDate,
 r.ReferenceNumber, i.InventionTitle



select recordid, recorddate [min date]
into #temp2
from #temp1

update #temp2
set [min date] = ReceivedDate 
from #temp1 t1 join #temp2 t2 on t1.recordid = t2.recordid
where t1.ReceivedDate < [min date] and  t1.ReceivedDate > '2001-01-01'

update #temp2 
set [min date] = t1.[Min File Upload]
from #temp1 t1 join #temp2 t2 on t1.recordid = t2.recordid
where t1.[Min File Upload] < [min date] and  t1.[Min File Upload] > '2001-01-01'

update #temp2
set [min date] = t1.[Min File Correspondence]
from #temp1 t1 join #temp2 t2 on t1.recordid = t2.recordid
where t1.[Min File Correspondence] < [min date] and t1.[Min File Correspondence] > '2001-01-01'


select t1.*, t2.[min date] [LOWEST DATE]
from #temp1 t1 join #temp2 t2 on t1.recordid = t2.recordid
order by t1.recordid

【讨论】:

【参考方案16】:
SELECT ID, Col1, Col2, Col3, 
    (SELECT MIN(Col) FROM (VALUES (Col1), (Col2), (Col3)) AS X(Col)) AS TheMin
FROM Table

【讨论】:

感谢您的收获。我错过了那个标签。我其实不知道,也没有能力测试它。今后会更加努力地检查标签。 放弃更优雅的解决方案 - 不知道为什么它没有更多的赞成票。 对于内联最大/最小计算,这是迄今为止最好的方法 绝妙的解决方案。 精彩且非常轻松【参考方案17】:

对于多个列,最好使用 CASE 语句,但是对于两个数字列 i 和 j,您可以使用简单的数学运算:

min(i,j) = (i+j)/2 - abs(i-j)/2

此公式可用于获取多列的最小值,但它在 2 之后确实很混乱,min(i,j,k) 将是 min(i,min(j,k))

【讨论】:

【参考方案18】:
SELECT [ID],
            (
                SELECT MIN([value].[MinValue])
                FROM
                (
                    VALUES
                        ([Col1]),
                        ([Col1]),
                        ([Col2]),
                        ([Col3])
                ) AS [value] ([MinValue])
           ) AS [MinValue]
FROM Table;

【讨论】:

【参考方案19】:

我知道这个问题很老了,但我仍然需要答案,并且对其他答案不满意,所以我不得不设计自己的答案,这是对 @paxdiablo 的 answer 的一个转折。


我来自 SAP ASE 16.0,我只需要查看某些数据的统计信息,恕我直言,这些数据有效地存储在单行的不同列中(它们代表不同的时间 - 计划到达的时间,它是什么预计行动开始时,最后是什么实际时间)。因此,我已将列转置到临时表的行中,并像往常一样对此进行查询。

注意不是万能的解决方案!

CREATE TABLE #tempTable (ID int, columnName varchar(20), dataValue int)

INSERT INTO #tempTable 
  SELECT ID, 'Col1', Col1
    FROM sourceTable
   WHERE Col1 IS NOT NULL
INSERT INTO #tempTable 
  SELECT ID, 'Col2', Col2
    FROM sourceTable
   WHERE Col2 IS NOT NULL
INSERT INTO #tempTable 
  SELECT ID, 'Col3', Col3
    FROM sourceTable
   WHERE Col3 IS NOT NULL

SELECT ID
     , min(dataValue) AS 'Min'
     , max(dataValue) AS 'Max'
     , max(dataValue) - min(dataValue) AS 'Diff' 
  FROM #tempTable 
  GROUP BY ID

这在 630000 行的源集上花费了大约 30 秒,并且只使用了索引数据,因此不是在时间关键的过程中运行的东西,而是用于一次性数据检查或一天结束报告之类的东西你可能没问题(但请与你的同事或上级核实这一点!)。 这种风格对我来说的主要好处是我可以轻松地使用更多/更少的列并更改分组、过滤等,尤其是在复制数据后。

附加数据(columnNamemaxes、...)用于帮助我进行搜索,因此您可能不需要它们;我把它们留在这里也许是为了激发一些想法:-)。

【讨论】:

【参考方案20】:

案例 当 Col1 else null 以 'MIN' 结尾

【讨论】:

这是我成功使用 3 列空值的唯一方法

以上是关于从几列中选择最小值的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

Pandas GroupBy 并选择特定列中具有最小值的行

从时间戳中提取时间分辨率以获取列中特定值的最佳方法是啥?

如何获取列中多个最小值的索引?

在列中查找最小值

R - 创建几列非零最小值的新列

sql 查询一行内几列的平均值,最大值,最小值,怎么写