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

问题描述 投票:62回答:19

给出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和Col3中的minium值的列)?

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列要求),它是一个非常优雅的“走出监狱卡”。

sql sql-server tsql sql-server-2005 min
19个回答
46
投票

可能有很多方法可以实现这一目标。我的建议是使用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

2
投票

this questionthis question都试图回答这个问题。

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


1
投票

如果你能够创建一个存储过程,它可能需要一组值,你可以调用它。


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
投票

联合查询有点扭曲:

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

这样你就不会迷失在这么多运营商中:)

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

这是你的选择...


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
投票

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

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

这个公式可以用来获得多列的最小值,但是它真的很乱过去2,min(i,j,k)将是min(i,min(j,k))


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的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,......)是为了帮助我进行搜索,所以你可能不需要它们;我把它们留在这里可能引发一些想法:-)。


41
投票

使用CROSS APPLY

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

SQL Fiddle


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

7
投票

您可以使用“强力”方法进行扭曲:

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语句。


7
投票

最好的方法就是不要这样做 - 很奇怪人们坚持以一种需要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时间做出决定的选项在性能方面通常都是一个坏主意,因为数据只会在插入/更新时发生变化 - 添加另一列会占用DB中更多的空间并且插入稍微慢一些和更新,但选择可以更快 - 首选的方法应该取决于你的优先级,但如上所述,大多数表读取的频率远远超过它们的写入。


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)

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

5
投票

用这个:

select least(col1, col2, col3) FROM yourtable

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

© www.soinside.com 2019 - 2024. All rights reserved.