Який найкращий спосіб вибрати мінімальне значення з кількох стовпців?


82

Враховуючи таку таблицю в 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, і він працює, хоча і трохи громіздко. Це також складніше, ніж зазначено у відповідях, оскільки вам потрібно враховувати той факт, що в одному рядку є два значення min.

У будь-якому випадку, я думав опублікувати своє поточне рішення, яке, враховуючи мої обмеження, працює досить добре. Він використовує оператор 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
IMHO авторське рішення UNPIVOT перевершує інші відповіді.
Джо Гарріс

2
Я вважаю, що рішення Нізама є найскладнішим рішенням, навіть якщо мені знадобився час, щоб я почав його розуміти. Худий і дуже корисний.
Patrick Honorez

Відповіді:


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

2
Якщо кількість стовпців може зростати, ви, безумовно, робите це неправильно - див. Мій пост (аргументація щодо того, чому у вас не повинна бути налаштована ваша схема БД таким чином :-).
paxdiablo

2
Дякую. Як я вже згадував в іншому коментарі. Я не запитую фактичні таблиці. Таблиці нормалізовані правильно. Цей запит є частиною особливо складного запиту і працює над проміжними результатами з похідних таблиць.
stucampbell

2
У такому випадку, чи можете ви їх вивести по-різному, щоб вони виглядали нормалізованими?
Кев

3
Додайте до відповіді від @Gmastros, коли я зіткнувся з проблемою, що деякі Cols мають відповідні дані, тому мені довелося додати знак =. Мої дані також мали потенціал null, тому мені довелося додати заяву or або, щоб це врахувати. Можливо, є простіший спосіб зробити це, але я не знайшов жодного за останні 6 місяців, які я шукав. Дякуємо всім, хто тут бере участь. Виберіть ідентифікатор, CaseWhen (Col1 <= Col2 АБО Col2 є нульовим) І (Col1 <= Col3 АБО Col3 є нульовим) Потім Col1 Коли (Col2 <= Col1 АБО Col1 є нульовим) І (Col2 <= Col3 АБО Col3 є нульовим) Тоді Col2 Else Col3 End As The Minin from YourTableNameHere
Чад Портман

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
Патрік Гонорес

2
@iDevlop Я вставив скрипту SQL у свою відповідь
Nizam

Що я не знав, це скалярна функція. Здається, ваша відповідь працює і без cross apply. Це додає вартості / продуктивності? stackoverflow.com/a/14712024/78522
Patrick Honorez

@iDevlop Якщо це не забезпечує продуктивність, це збільшує читабельність. Наприклад, я міг використати щось подібне where MinValue > 10, без чого я не міг обійтисяCROSS APPLY
Нізам

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

3
Передайте більш елегантне рішення - не впевнені, чому воно не має більше голосів.
jwolf

Для вбудованих макс. / Хв calcs це, безумовно, найкращий спосіб це зробити
Saxman 02

Чудове рішення.
Phani

16

На MySQL використовуйте це:

select least(col1, col2, col3) FROM yourtable

Можливо, це не оператор SQL.
Тіно Хосе Танніппара

4
але в деяких випадках це так. для тих, це прекрасна відповідь
Кірбі


1
Це нестандартне розширення SQL підтримується майже будь-якою базою даних, крім сервера Microsoft SQL.
Мікко Ранталайнен

LEASTпрацює в останній версії керованих екземплярів Microsoft SQL Server станом на ~ 12 днів тому. reddit.com/r/SQLServer/comments/k0dj2r/…
Джон Заброські

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

Рекурсія знизить продуктивність. Але це буде відповідати вимогам.
Тіно Хосе Танніппара

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. Це змістить "вартість" операції з вибраного на оновлення / вставку, де вона належить - більшість таблиць баз даних на моєму досвіді читаються набагато частіше, ніж написані, тому витрати на запис, як правило, з часом ефективніші.

Іншими словами, мінімум для рядка змінюється лише тоді, коли змінюється один з інших стовпців, тому саме тоді вам слід обчислювати його, а не кожного разу, коли ви вибираєте (що витрачається даремно, якщо дані не змінюються). Потім ви отримаєте таблицю, таку як:

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 для пропозиції тригера зберегти оригінальну (якщо вона має недоліки) структуру таблиці.
Скотт Фергюсон,

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
Це працює, але продуктивність погіршується, коли кількість рядків зростає.
Томалак

1
Дякую. Так, це працює. Як каже Томалак, у моєму реальному запиті це було б дуже неприємно для виконання. Але +1 за зусилля. :)
stucampbell

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 () працює лише в одному стовпці, а не в кожному стовпці.


Це також, як правило, найшвидше, оскільки MIN створює неявне приєднання вкладеного циклу.
Джон Заброські

2

І це питання, і це питання намагаються відповісти на це.

Підсумок полягає в тому, що Oracle має вбудовану функцію для цього, завдяки Sql Server ви застрягли або у визначенні користувацької функції, або у використанні заяв на регістр.


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 теж цього немає", але насправді є. ;) Саму функцію не складно написати, хоча.
Кев

О, за винятком того, що T-SQL навіть не підтримує масив (???) Ну, я думаю, ви могли б мати п'ятипараметричну функцію, і якщо вам потрібно більше, просто розширити її ...
Кев

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, тому, якщо вам цікаво: я думаю, саме звідси бере свій початок голосування.
Томалак

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

Нижче я використовую часову таблицю, щоб отримати мінімум декількох дат. Перша тимчасова таблиця запитує кілька об’єднаних таблиць, щоб отримати різні дати (а також інші значення для запиту), друга тимчасова таблиця отримує різні стовпці та мінімальну дату, використовуючи стільки проходів, скільки стовпців дати.

Це, по суті, як запит об’єднання, потрібна однакова кількість проходів, але може бути більш ефективною (на основі досвіду, але потребує тестування). Ефективність у цьому випадку не була проблемою (8000 записів). Можна було б індексувати тощо.

--==================== 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 .


Я приїхав із країни 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

Це зайняло близько 30 секунд для набору джерел із 630000 рядків і використовували лише дані індексу, тому не те, що запускатиметься в критично важливому для часу процесі, але для таких речей, як одноразова перевірка даних або звіт про кінець дня, ви можете чудово (але уточніть це у своїх однолітків або начальства, будь ласка!). Головним бонусом цього стилю для мене було те, що я міг легко використовувати більше / менше стовпців і змінювати групування, фільтрацію тощо, особливо після копіювання даних.

Додаткові дані ( columnName, наприклад max, ...) мали допомогти мені у пошуку, тож вони можуть вам не знадобитися; Я залишив їх тут, щоб, можливо, викликати деякі ідеї :-).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.