複数の列から最小値を選択するための最良の方法は何ですか?


82

SQL Server2005の次の表を考えます。

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アプローチを試してみました。また、同じ行に2つの最小値があるという事実に対応する必要があるため、回答に記載されているよりも複雑です。

とにかく、私は現在の解決策を投稿すると思いました。これは私の制約を考えると、かなりうまく機能します。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
ニザームの解決策は、理解し始めるのに少し時間がかかったとしても、最も無駄のない解決策だと思います。無駄がなく、とても使いやすい。
Patrick Honorez 2015

回答:


59

これを達成する方法はたくさんあるでしょう。私の提案は、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

6
これが私の最初の考えでした。ただし、実際のクエリには5つの列が必要であり、列の数が増える可能性があります。したがって、CASEアプローチは少し扱いに​​くくなります。しかし、それは機能します。
stucampbell 2008

2
列の数が増える可能性がある場合は、間違いなく間違っています-私の投稿を参照してください(DBスキーマをこのように設定するべきではない理由についての怒り:-)。
paxdiablo 2008

2
ありがとう。別のコメントで述べたように。実際のテーブルをクエリしていません。テーブルは正しく正規化されています。このクエリは特に複雑なクエリの一部であり、派生テーブルからの中間結果を処理しています。
stucampbell 2008

2
その場合、それらを別の方法で導出して、正規化されたように見せることができますか?
Kev

3
一部のColのデータが一致しているという問題が発生したため、@ Gmastrosからの回答に追加するため、=記号を追加する必要がありました。私のデータもnullの可能性があるため、それを説明するためにorステートメントを追加する必要がありました。これを行うためのより簡単な方法があるかもしれませんが、私が探していた過去6か月の間に1つを見つけられませんでした。ここに関係するすべての人に感謝します。Id、CaseWhen(Col1 <= Col2 OR Col2 null)And(Col1 <= Col3 OR Col3 null)Then Col1 When(Col2 <= Col1 OR Col1 null)And(Col2 <= Col3 OR Col3 null)Then Col2 Else Col3 End As TheMin From YourTableNameHere
Chad Portman

55

使用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フィドルを挿入しました
Nizam

私が知らなかったのはスカラー関数でした。あなたの答えはcross apply。なしでも機能するようです。それは価値/パフォーマンスを追加しますか?stackoverflow.com/a/14712024/78522
Patrick Honorez 2015

@iDevlopパフォーマンスが得られない場合は、読みやすさが向上します。たとえば、次のようなものを使用できますがwhere MinValue > 10、これなしでは実行できませんCROSS APPLY
Nizam

2
確かに、その間にそれの「再利用性」の利点を理解する機会がありました。ありがとう。今日は2つのことを学びました
;

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

1
キャッチしてくれてありがとう。そのタグを見逃しました。私は実際には知りませんし、それをテストする能力もありません。今後、タグのチェックがより巧妙になります。
dsz 2016年

3
よりエレガントな解決策を伝えます-なぜそれ以上の賛成票がないのかわかりません。
jwolf

インラインの最大/最小計算の場合、これはこれを行うための最善の方法です
Saxman 2010

素晴らしいソリューション。
ファニ

16

MySQLでは、これを使用します。

select least(col1, col2, col3) FROM yourtable

SQLステートメントではない可能性があります。
Tino Jose Thannippara 2017年

4
しかし、場合によってはそうです。それらにとって、これは素晴らしい答えです
カービー


1
この非標準のSQL拡張機能は、MicrosoftSQLサーバーを除くほとんどすべてのデータベースでサポートされています。
MikkoRantalainen20年

LEAST〜12日前の時点で、最新バージョンのMicrosoft SQLServerマネージドインスタンスで動作します。 reddit.com/r/SQLServer/comments/k0dj2r/...
ジョンZabroski

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]

最初のwhen条件が失敗すると、Col1が最小値ではないことが保証されるため、残りの条件からそれを削除できます。後続の条件についても同様です。5列の場合、クエリは次のようになります。

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]

2つ以上の列の間に同点がある場合は、できるだけ早くステートメント<=を終了するようにしてくださいCASE


2
<=代わりに使用します。それ以外の場合は、最初の値の代わりに最後に一致する最小値が使用されます。
chezy525 2015年

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

再帰するとパフォーマンスが低下します。しかし、それは要件を満たします。
Tino Jose Thannippara 2017年

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、他のクエリで使用することで、個々の「古い列」を個別に処理できます。これにより、「古い列」の数が増えた場合でも簡単に拡張できます。

これにより、クエリ非常に簡単になります。あなたがあれば、私が使用する傾向がある一般的なガイドラインは、あるこれまでのデータベース行の配列のように見えますが、あなたはおそらく何かを間違ってやっていることを何かを持っているし、データの再構築を考える必要があります。


ただし、何らかの理由でこれらの列を変更できない場合、挿入トリガーと更新トリガーを使用して、これらのトリガーが最小に設定されている別の列を追加することをお勧めしCol1/2/3ます。これにより、操作の「コスト」が選択から、それが属する更新/挿入に移動します。私の経験では、ほとんどのデータベーステーブルは、書き込まれるよりもはるかに頻繁に読み取られるため、書き込みにかかるコストは時間の経過とともに効率的になる傾向があります。

つまり、行の最小値は、他の列の1つが変更されたときにのみ変更されるため選択するたびにではなく、計算する必要があります(データが変更されていない場合は無駄になります)。その場合、次のようなテーブルになります。

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内のスペースが多くなり、挿入が少し遅くなります。更新されますが、選択の方がはるかに高速になる可能性があります。推奨されるアプローチは、そこでの優先順位によって異なりますが、前述のように、ほとんどのテーブルは、書き込まれるよりもはるかに頻繁に読み取られます。


18
ええと。diatribeをありがとう。実際のデータベースは適切に正規化されています。これは簡単な例です。実際のクエリは複雑で、私が興味を持っている5つの列は、派生テーブルからの中間結果です。
stucampbell 2008

3
残念ながら、diatribeはまだ立っています。あなたが提案する形式の中間テーブルを作成することは、そのような永続テーブルを作成することと同じくらい問題があります。これは、私がSQL体操と呼んでいるものを実行して、希望する結果を得る必要があるという事実によって証明されています。
paxdiablo 2008

単一の行に「配列」が必要な本当の理由がある場合は、遠慮なく教えてください。ただし、最小値を選択するためにそれを使用することは、それらの1つではありません。
paxdiablo 2008

2
元の(欠陥がある場合)テーブル構造を保持するためのトリガー提案の+1。
スコットファーガソン

1
階層テーブルを処理していて、それ自体に結合されている場合はどうなりますか?
ネイサントレギラス

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 2008

1
ありがとう。はい、これは機能します。Tomalakが言うように、私の本当の言葉のクエリでは、これはパフォーマンスにとってかなり厄介です。しかし、努力のために+1。:)
stucampbell 2008

4

これはブルートフォースですが、機能します

 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()は1つの列でのみ機能し、列間では機能しないためです。


また、MINは暗黙のネストされたループ結合を作成するため、最速になる傾向があります。
JohnZabroski20年

2

この質問この質問の両方に答えようとします。

要約すると、Oracleにはこのための組み込み関数があり、SQL Serverでは、ユーザー定義関数を定義するか、caseステートメントを使用する必要があります。


2

複数の列の場合はCASEステートメントを使用するのが最適ですが、2つの数値列iおよびjの場合は、単純な計算を使用できます。

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

この式は、複数の列の最小値を取得するために使用できますが、2を超えると、min(i、j、k)はmin(i、min(j、k))になります。


1

ストアドプロシージャを作成できる場合は、値の配列を受け取ることができ、それを呼び出すことができます。


Oracleには、LEAST()と呼ばれる関数があり、必要なことを正確に実行します。
Kev

それをこすってくれてありがとう:)SQL Serverに同等のものがないなんて信じられない!
stucampbell 2008

「ねえ、私のお気に入りのpgsqlにもありません」と言うつもりでしたが、実際にはあります。;)関数自体を書くのは難しいことではありません。
Kev

ああ、T-SQLが配列をサポートしていないことを除いて(???)ええと、5つのパラメーター関数を使用できると思います。さらに必要な場合は、それを拡張するだけです...
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
これはGMastrosの回答と重複しているので、不思議に思うかもしれません。それが反対票の出所だと思います。
Tomalak 2008

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

このようにして、多くの演算子で迷子になることはありません:)

ただし、これは他の選択肢よりも遅くなる可能性があります。

それはあなたの選択です...


さて、私が言ったように、これは遅いかもしれませんが、列が多すぎる場合(明らかに、DB設計が非常に悪いためです!)、これを使用する価値があります(少なくともAVGの場合)。それが良い聖牛なのか悪い牛なのか、あなたは私に何のヒントも与えませんでした:)多分あなたは私が理解するのを助けるために賛成/反対票を使うべきです。
leoinfo 2008

それは本当に良いものでも悪いものでもありませんでした;)。私はデータベースの専門家ではないので、質問は些細な答えになるように思えたので、「聖なる牛」と言っていました。問題に対して柔軟で拡張可能なソリューションを提供できたので、これは良い方法だと思います。
dreamlax 2008

1

以下では、一時テーブルを使用して、いくつかの日付の最小値を取得します。最初の一時テーブルは、いくつかの結合テーブルにクエリを実行してさまざまな日付(およびクエリの他の値)を取得し、2番目の一時テーブルは日付列と同じ数のパスを使用してさまざまな列と最小日付を取得します。

これは基本的にユニオンクエリに似ており、同じ数のパスが必要ですが、より効率的である可能性があります(経験に基づいていますが、テストが必要です)。この場合、効率は問題ではありませんでした(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


0

探している値(通常はステータスコード)がわかっている場合は、次のことが役立ちます。

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

0

質問が古いことは知っていますが、私はまだ答えを必要としていて、他の答えに満足していなかったので、@ paxdiabloの答えにひねりを加えた独自の工夫をしなければなりませんでした。


私はSAPASE 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秒かかり、インデックスデータのみを使用したため、タイムクリティカルなプロセスで実行するものではなく、1回限りのデータ検査や1日の終わりのレポートなどの場合に結構です(ただし、これを同僚や上司に確認してください!)。私にとってこのスタイルの主なボーナス利点は、特にデータがコピーされた後、より多くの/より少ない列を簡単に使用し、グループ化、フィルタリングなどを変更できることでした。

追加のデータ(columnNamemaxes、...)は私の検索に役立つため、必要ないかもしれません。いくつかのアイデアを刺激するために、ここに残しました:-)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.