Какой самый эффективный способ получить минимум нескольких столбцов в SQL Server 2005?

29

Я нахожусь в ситуации, когда я хочу получить минимальное значение из 6 столбцов.

Я нашел три способа сделать это, но у меня есть проблемы с производительностью этих методов, и я хотел бы знать, какой из них лучше для производительности.

Первый способ - использовать большой регистр . Вот пример с 3 столбцами, основанный на примере по ссылке выше. Мое утверждение будет гораздо длиннее, так как я буду смотреть на 6 столбцов.

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

Второй вариант - использовать UNIONоператор с несколькими операторами выбора . Я бы положил это в UDF, который принимает параметр Id.

select Id, dbo.GetMinimumFromMyTable(Id)
from MyTable

а также

select min(col)
from
(
    select col1 [col] from MyTable where Id = @id
    union all
    select col2 from MyTable where Id = @id
    union all
    select col3 from MyTable where Id = @id
) as t

И третий вариант, который я нашел, состоял в том, чтобы использовать оператор 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

Из-за размера таблицы и частоты, с которой эта таблица запрашивается и обновляется, я обеспокоен влиянием производительности, которое эти запросы будут оказывать на базу данных.

Этот запрос будет фактически использоваться в соединении с таблицей с несколькими миллионами записей, однако количество возвращаемых записей будет уменьшено до примерно сотни записей за раз. Он будет запускаться много раз в течение дня, и 6 колонок, которые я запрашиваю, часто обновляются (они содержат ежедневную статистику). Я не думаю, что есть какие-либо индексы по 6 столбцам, которые я запрашиваю.

Какой из этих методов лучше для производительности при попытке получить минимум из нескольких столбцов? Или есть другой лучший метод, о котором я не знаю?

Я использую SQL Server 2005

Образцы данных и результаты

Если мои данные содержали такие записи:

Идентификатор Col1 Col2 Col3 Col4 Col5 Col6
1 3 4 0 2 1 5
2 2 6 10 5 7 9
3 1 1 2 3 4 5
4 9 5 4 6 8 9

Конечный результат должен быть

Значение идентификатора
1 0
2 2
3 1
4 4
Рейчел
источник

Ответы:

22

Я проверил производительность всех 3 методов, и вот что я нашел:

  • 1 запись: нет заметной разницы
  • 10 записей: нет заметной разницы
  • 1000 записей: нет заметной разницы
  • 10000 записей: UNIONподзапрос был немного медленнее. CASE WHENЗапрос немного быстрее , чем UNPIVOTодин.
  • 100 000 записей: UNIONподзапрос значительно медленнее, но UNPIVOTзапрос становится немного быстрее, чем CASE WHENзапрос
  • 500 000 записей: UNIONподзапрос все еще значительно медленнее, но UNPIVOTстановится намного быстрее, чем CASE WHENзапрос

Таким образом, конечные результаты, кажется,

  • С меньшими наборами записей, кажется, не достаточно разницы, чтобы иметь значение. Используйте все, что легче читать и поддерживать.

  • Как только вы начинаете получать большие наборы записей, UNION ALLподзапрос начинает работать плохо по сравнению с двумя другими методами.

  • В CASEзаявлении выполняет лучше всего только до определенного момента (в моем случае, около 100к строк), и этот момент UNPIVOTзапрос становится наиболее эффективных запросов

Фактическое число, при котором один запрос становится лучше другого, вероятно, изменится в результате вашего оборудования, схемы базы данных, данных и текущей нагрузки на сервер, поэтому обязательно проверяйте свою собственную систему, если вас беспокоит производительность.

Я также провел несколько тестов, используя ответ Микаэля ; однако, это было медленнее, чем все 3 других метода, которые здесь пробовали для большинства размеров набора записей. Единственным исключением было то, что он выполнялся лучше, чем UNION ALLзапрос для очень больших размеров набора записей. Мне нравится тот факт, что он показывает имя столбца в дополнение к наименьшему значению.

Я не dba, поэтому, возможно, я не оптимизировал свои тесты и что-то пропустил. Я тестировал реальные данные, так что это могло повлиять на результаты. Я пытался объяснить это, выполняя каждый запрос несколько раз, но вы никогда не знаете. Мне определенно было бы интересно, если бы кто-то написал чистую проверку этого и поделился своими результатами.

Рейчел
источник
6

Не знаю, что быстрее, но вы можете попробовать что-то вроде этого.

declare @T table
(
  Col1 int,
  Col2 int,
  Col3 int,
  Col4 int,
  Col5 int,
  Col6 int
)

insert into @T values(1, 2, 3, 4, 5, 6)
insert into @T values(2, 3, 1, 4, 5, 6)

select T4.ColName, T4.ColValue
from @T as T1
  cross apply (
                select T3.ColValue, T3.ColName
                from (
                       select row_number() over(order by T2.ColValue) as rn,
                              T2.ColValue,
                              T2.ColName
                       from (
                              select T1.Col1, 'Col1' union all
                              select T1.Col2, 'Col2' union all
                              select T1.Col3, 'Col3' union all
                              select T1.Col4, 'Col4' union all
                              select T1.Col5, 'Col5' union all
                              select T1.Col6, 'Col6'
                            ) as T2(ColValue, ColName)
                     ) as T3
                where T3.rn = 1
              ) as T4

Результат:

ColName ColValue
------- -----------
Col1    1
Col3    1

Если вас не интересует, какой столбец имеет минимальное значение, вы можете использовать его вместо этого.

declare @T table
(
  Id int,
  Col1 int,
  Col2 int,
  Col3 int,
  Col4 int,
  Col5 int,
  Col6 int
)

insert into @T
select 1,        3,       4,       0,       2,       1,       5 union all
select 2,        2,       6,      10,       5,       7,       9 union all
select 3,        1,       1,       2,       3,       4,       5 union all
select 4,        9,       5,       4,       6,       8,       9

select T.Id, (select min(T1.ColValue)
              from (
                      select T.Col1 union all
                      select T.Col2 union all
                      select T.Col3 union all
                      select T.Col4 union all
                      select T.Col5 union all
                      select T.Col6
                    ) as T1(ColValue)
             ) as ColValue
from @T as T

Упрощенный запрос отмены.

select Id, min(ColValue) as ColValue
from @T
unpivot (ColValue for Col in (Col1, Col2, Col3, Col4, Col5, Col6)) as U
group by Id
Микаэль Эрикссон
источник
6

Добавьте постоянный вычисляемый столбец, который использует CASEоператор, чтобы выполнить необходимую логику.

Тогда минимальное значение всегда будет эффективно доступно, когда вам нужно выполнить объединение (или что-либо еще) на основе этого значения.

Значение будет пересчитываться каждый раз при изменении любого из исходных значений ( INSERT/ UPDATE/ MERGE). Я не говорю , что это обязательно является лучшим решением для рабочей нагрузки, я просто предложить его как в растворе, так же как и другие ответы. Только ОП может определить, что лучше для рабочей нагрузки.

Джон Зигель
источник
1

Постановка дела на 6 дат. Чтобы сделать меньше, скопируйте истинную ветвь из первого оператора case. В худшем случае, когда Date1 является наименьшим значением, наилучший случай, когда Date6 является наименьшим значением, поэтому укажите наиболее вероятную дату в Date6. Я написал это из-за ограничений вычисляемых столбцов.

CASE WHEN Date1 IS NULL OR Date1 > Date2 THEN
        CASE WHEN Date2 IS NULL OR Date2 > Date3 THEN
            CASE WHEN Date3 IS NULL OR Date3 > Date4 THEN
                CASE WHEN Date4 IS NULL OR Date4 > Date5 THEN
                    CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                        Date6
                    ELSE
                        Date5
                    END
                ELSE
                    CASE WHEN Date4 IS NULL OR Date4 > Date6 THEN
                        Date6
                    ELSE
                        Date4
                    END
                END
            ELSE
                CASE WHEN Date3 IS NULL OR Date3 > Date5 THEN
                    CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                        Date6
                    ELSE
                        Date5
                    END
                ELSE
                    CASE WHEN Date3 IS NULL OR Date3 > Date6 THEN
                        Date6
                    ELSE
                        Date3
                    END
                END
            END
        ELSE
            CASE WHEN Date2 IS NULL OR Date2 > Date4 THEN
                CASE WHEN Date4 IS NULL OR Date4 > Date5 THEN
                    CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                        Date6
                    ELSE
                        Date5
                    END
                ELSE
                    CASE WHEN Date4 IS NULL OR Date4 > Date5 THEN
                        CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                            Date6
                        ELSE
                            Date5
                        END
                    ELSE
                        CASE WHEN Date4 IS NULL OR Date4 > Date6 THEN
                            Date6
                        ELSE
                            Date4
                        END
                    END
                END
            ELSE
                CASE WHEN Date2 IS NULL OR Date2 > Date5 THEN
                    CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                        Date6
                    ELSE
                        Date5
                    END
                ELSE
                    CASE WHEN Date2 IS NULL OR Date2 > Date6 THEN
                        Date6
                    ELSE
                        Date2
                    END
                END
            END
        END
ELSE
    CASE WHEN Date1 IS NULL OR Date1 > Date3 THEN
        CASE WHEN Date3 IS NULL OR Date3 > Date4 THEN
            CASE WHEN Date4 IS NULL OR Date4 > Date5 THEN
                CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                    Date6
                ELSE
                    Date5
                END
            ELSE
                CASE WHEN Date4 IS NULL OR Date4 > Date6 THEN
                    Date6
                ELSE
                    Date4
                END
            END
        ELSE
            CASE WHEN Date3 IS NULL OR Date3 > Date5 THEN
                CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                    Date6
                ELSE
                    Date5
                END
            ELSE
                CASE WHEN Date3 IS NULL OR Date3 > Date6 THEN
                    Date6
                ELSE
                    Date3
                END
            END
        END
    ELSE
        CASE WHEN Date1 IS NULL OR Date1 > Date4 THEN
            CASE WHEN Date4 IS NULL OR Date4 > Date5 THEN
                CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                    Date6
                ELSE
                    Date5
                END
            ELSE
                CASE WHEN Date4 IS NULL OR Date4 > Date6 THEN
                    Date6
                ELSE
                    Date4
                END
            END
        ELSE
            CASE WHEN Date1 IS NULL OR Date1 > Date5 THEN
                CASE WHEN Date5 IS NULL OR Date5 > Date6 THEN
                    Date6
                ELSE
                    Date5
                END
            ELSE
                CASE WHEN Date1 IS NULL OR Date1 > Date6 THEN
                    Date6
                ELSE
                    Date1
                END
            END
        END
    END
END

Если вы натолкнулись на эту страницу, просто пытаясь сравнить даты и не столь озабочены производительностью или совместимостью, вы можете использовать конструктор табличных значений, который можно использовать везде, где разрешены подвыборы (SQL Server 2008 и выше):

Lowest =    
(
    SELECT MIN(TVC.d) 
    FROM 
    (
        VALUES
            (Date1), 
            (Date2), 
            (Date3), 
            (Date4), 
            (Date5), 
            (Date6)
    ) 
    AS TVC(d)
)
Джесси Адам
источник
1

Ваше caseутверждение не эффективно. Вы делаете 5 сравнений в худшем случае и 2 в лучшем случае; тогда как поиск минимума nдолжен делать в большинстве n-1сравнений.

В среднем для каждой строки вы делаете 3,5 сравнения, а не 2. Таким образом, процесс занимает больше времени и медленнее. Попробуйте свои тесты снова, используя приведенное ниже caseутверждение. Он просто использует 2 сравнения в строке и должен быть более эффективным, чем unpivotи union all.

Select Id, 
       Case 
           When Col1 <= Col2 then case when Col1 <= Col3 Then Col1  else col3 end
            When  Col2 <= Col3 Then Col2  
            Else Col3 
            End As TheMin 
From   YourTableNameHere

union allМетод не так в вашем случае , как вы получаете минимальное значение не для каждой строки , но и для всей таблицы. Кроме того, это не будет эффективно, так как вы собираетесь сканировать одну и ту же таблицу 3 раза. Когда таблица мала, ввод / вывод не будет иметь большого значения, но для больших таблиц это будет. Не используйте этот метод.

UnpivotЭто хорошо, и попробуйте вручную откатить, используя кросс-соединение с вашим столом (select 1 union all select 2 union all select 3). Это должно быть так же эффективно, как unpivot.

Лучшим решением будет наличие вычисляемого постоянного столбца, если у вас нет проблем с пространством. Это увеличит размер строки на 4 байта (я полагаю, у вас будет intтип), что, в свою очередь, увеличит размер таблицы.

Тем не менее, пространство и память являются проблемой в вашей системе, и ЦП не делает их постоянными, а использует простой вычисляемый столбец с помощью оператора case. Это сделает код проще.

Гулли Мил
источник
-1

Я предполагаю, что первый вариант самый быстрый (хотя он не выглядит очень гладким с точки зрения программирования!). Это связано с тем, что он имеет дело ровно с N строками (где N - размер таблицы) и не должен выполнять поиск или сортировку, как в методе 2 или 3.

Тест с большой выборкой должен доказать это.

Еще один вариант, который нужно рассмотреть (как будто вам нужно больше!), - создать материализованное представление вашей таблицы. если ваш размер таблицы в сотнях тысяч или более. Таким образом, минимальное значение вычисляется при изменении строки, и вся таблица не должна обрабатываться при каждом запросе. В SQL Server материализованные представления называются индексированными представлениями.

Без шансов
источник
-1
Create table #temp
   (
    id int identity(1,1),
    Name varchar(30),
    Year1 int,
    Year2 int,
    Year3 int,
    Year4 int
   )

   Insert into #temp values ('A' ,2015,2016,2014,2010)
   Insert into #temp values ('B' ,2016,2013,2017,2018)
   Insert into #temp values ('C' ,2010,2016,2014,2017)
   Insert into #temp values ('D' ,2017,2016,2014,2015)
   Insert into #temp values ('E' ,2016,2016,2016,2016)
   Insert into #temp values ('F' ,2016,2017,2018,2019)
   Insert into #temp values ('G' ,2016,2017,2020,2019)

   Select *, Case 
                 when Year1 >= Year2 and Year1 >= Year3 and Year1 >= Year4 then Year1
                 when Year2 >= Year3 and Year2 >= Year4 and Year2 >= Year1 then Year2
                 when Year3 >= Year4 and Year3 >= Year1 and Year3 >= Year2 then Year3
                 when Year4 >= Year1 and Year4 >= Year2 and Year4 >= Year3 then Year4  
                 else Year1 end as maxscore  
                 from #temp
Ravi
источник
Вы не учитываете NULL - это делает ваше выражение CASE относительно простым. Однако, если хотя бы один из столбцов действительно равен NULL, ваше решение вернется Year1в качестве результата, что не всегда может быть правильным.
Андрей М