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


80

给定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列要求重新设计所有查询),这是一个非常优雅的“摆脱困境”卡”。


11
恕我直言,作者的UNPIVOT解决方案优于其他答案。
乔·哈里斯

2
我发现Nizam的解决方案是最精简的解决方案,即使我花了一些时间才开始理解它。精益和非常有用。
帕特里克·奥诺涅兹

Answers:


58

可能有很多方法可以完成此任务。我的建议是使用案例/何时进行。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

6
这是我最初的想法。但是实际查询需要5列,并且列数可能会增加。因此,CASE方法变得有些笨拙。但这确实有效。
stucampbell

2
如果列数可能增加,那么您肯定做错了-请参阅我的文章(为什么不应该以这种方式设置数据库模式的烦恼:-)。
paxdiablo

2
谢谢。正如我在另一条评论中提到的那样。我不是在查询实际表。这些表已正确规范化。该查询是特别复杂的查询的一部分,并且正在处理派生表的中间结果。
stucampbell,

2
在那种情况下,您能否以不同的方式派生它们,使它们看起来标准化?
Kev

3
我遇到一些Cols具有匹配数据的问题时,在@Gmastros上添加了答案,因此我必须添加=符号。我的数据也有可能为null,因此我必须添加or语句来解决这个问题。也许有一种更简单的方法可以执行此操作,但是在过去的6个月中,我一直没有找到一个。感谢参与其中的每个人。选择Id,CaseWhen(Col1 <= Col2 OR Col2为空)并且(Col1 <= Col3 OR Col3为空)然后Col1当(Col2 <= Col1 OR Col1为空)并且(Col2 <= Col3 OR Col3为空)然后Col2其他Col3结尾为来自YourTableNameHe的TheMin
Chad Portman

53

使用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小提琴


看起来很有趣,但我无法解决这个问题。您能否说清楚一点?thx
Patrick Honorez 2015年

2
@iDevlop我在答案中插入了SQL Fiddle
Nizam

我不知道的是标量函数。看来,您的答案也可以使用cross apply。它会增加价值/绩效吗?stackoverflow.com/a/14712024/78522
Patrick Honorez,

@iDevlop如果不提供性能,则会提高可读性。例如,我可以使用类似这样的东西where MinValue > 10,我不能没有CROSS APPLY
Nizam

2
实际上,与此同时,我有机会了解它的“可重用性”优势。谢谢。我今天学到了两件事;-)
Patrick Honorez

25
SELECT ID, Col1, Col2, Col3, 
    (SELECT MIN(Col) FROM (VALUES (Col1), (Col2), (Col3)) AS X(Col)) AS TheMin
FROM Table

1
谢谢你的收获。我错过了那个标签。我实际上不知道,也没有能力对其进行测试。将来检查标签时会更加谨慎。
dsz

3
放下更优雅的解决方案-不知道为什么它没有更多支持。
jwolf

对于内联最大/最小计算,这是迄今为止最好的方法
Saxman

15

在MySQL上,使用以下命令:

select least(col1, col2, col3) FROM yourtable

可能不是SQL语句。
蒂诺·何塞·坦尼帕帕拉

4
但在某些情况下是这样。对于那些人来说,这是一个很棒的答案
Kirby


1
除Microsoft SQL Server之外,几乎所有数据库都支持此非标准SQL扩展。
Mikko Rantalainen

就这样,它也可以在雪花中使用
Ramil Gataullin

10

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

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]

请注意,如果两列或更多列之间存在联系,请<=确保我们CASE尽早退出该语句。


2
使用<=代替,否则,将使用最后一个匹配的最小值代替第一个。
chezy525

6

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

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)

4
鉴于MSSQL中标量函数的性能极低,我不得不针对这种方法提出建议。如果您要走这条路,那么至少要编写一个同时将所有5列作为参数的函数。它仍然会很糟,但至少会少一点坏= /
deroby 2015年

递归会降低性能。但这将满足要求。
蒂诺·何塞·坦尼帕帕拉

6

做到这一点的最好方法可能是这样做-奇怪的是,人们坚持以要求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在其他查询中使用单独对待各个“旧列” 。如果“旧列”的数量增加,这也允许轻松扩展。

这使您的查询变得如此简单。一般准则我倾向于使用,如果你曾经有东西,看起来像在一个数据库行的一个数组,你可能做错事,应该考虑重组数据。


但是,如果由于某种原因您无法更改这些列,建议您使用插入和更新触发器,并添加另一列,这些触发器将其设置为on的最小值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通常都不是性能上的好主意,因为数据仅在插入/更新时发生更改-添加另一列会占用数据库中的更多空间,并且插入和复制的速度会稍慢更新但可以为选择快-首选的方法应该取决于你的重点有,但,如上所述,大多数表是只读远远往往比他们写的。


18
嗯 感谢您的营养。真实数据库已正确规范化。这是一个简单的例子。实际查询很复杂,我感兴趣的5列是派生表的中间结果。
stucampbell,

3
糖尿病仍然很不幸。制作您建议的形式的中间表与制作永久表一样麻烦。事实证明,您必须执行我喜欢的SQL体操才能获得所需的结果。
paxdiablo

如果确实有需要在单个行中使用“数组”的原因,请随时告诉我们,但使用它来选择最小值不是其中之一。
paxdiablo

2
+1为触发建议,以保留原始(如果有缺陷)表结构。
斯科特·弗格森

如果您正在处理自己加入的Hierarchy表,该怎么办?
Nathan Tregillus

5

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

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

2
这行得通,但是当行数增加时性能会下降。
Tomalak

1
谢谢。是的,这可行。正如Tomalak所说,在我的实词查询中,这对于性能而言是非常讨厌的。但是需要+1。:)
stucampbell

3

这是蛮力但有效

 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()仅作用于一列,而不作用于各列。



2

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

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

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


1

如果您能够创建一个存储过程,则它可以采用一个值数组,而您可以调用它。


Oracle具有一个称为LEAST()的函数,它可以完全满足您的需求。
凯夫

感谢您在:)中进行摩擦,我简直不敢相信SQL Server没有等效功能!
stucampbell

我什至要说,“嘿,我最喜欢的pgsql也没有,”但实际上是这样。;)函数本身并不难编写。
Kev

哦,除了T-SQL甚至没有数组支持(???)嗯,我想您可能有一个五参数函数,如果您需要更多功能,只需对其进行扩展...
Kev

1
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

1
这是G Mastros回答的重复,因此如果您想知道:我想这就是不赞成投票的地方。
Tomalak

1

联合查询有些许错误:

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

1

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

这样,您就不会在那么多运算符中迷失方向:)

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

这是你的选择...


好吧,就像我说的那样,这可能会很慢,但是如果您有太多的列(显然是由于数据库设计非常糟糕而造成的!),则值得(至少对于AVG使用)。如果它是一头好还是一头坏牛,您没有给我任何提示:)也许您应该使用上下表决来帮助我弄清楚。
leoinfo

它不是一个好坏;)。我不是数据库专家,所以我只是说“圣牛”,因为这个问题似乎有一个简单的答案。我想这是一个很好的选择,因为您设法为该问题提供了一种灵活,可扩展的解决方案!
dreamlax

1

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

从本质上讲,这类似于联合查询,需要通过的次数相同,但可能会更有效(基于经验,但需要测试)。在这种情况下,效率不是问题(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

1
SELECT [ID],
            (
                SELECT MIN([value].[MinValue])
                FROM
                (
                    VALUES
                        ([Col1]),
                        ([Col1]),
                        ([Col2]),
                        ([Col3])
                ) AS [value] ([MinValue])
           ) AS [MinValue]
FROM Table;

0

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

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

0

我知道这个问题很旧,但是我仍然需要答案,并且对其他答案不满意,因此我不得不设计自己的答案,这与@paxdiablo的答案有所不同。


我来自SAP ASE 16.0,只需要看一下IMHO有效存储在单行不同列中的某些数据的统计信息(它们表示不同的时间-计划到达的时间,预期的到达时间)动作开始了,最后才是实际时间。因此,我已经将列转换为临时表的行,并像往常一样对此执行查询。

注意:不是前面的“一刀切”的解决方案!

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秒,并且仅使用索引数据,因此不是要在时间紧迫的过程中运行,而是要进行一次性数据检查或日末报告等操作很好(但请与您的同行或上级进行验证!)。这种风格的主要奖金对我来说是,我可以很容易地使用更多/更少列和改变分组,过滤等,特别是一旦数据copyied过来。

其他数据(columnNamemaxes,...)是为了帮助我进行搜索,因此您可能不需要它们。我把它们留在这里也许是引起一些想法的:-)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.