Максимум из нескольких колонок

by Alexey Knyazev 2. декабря 2011 23:08

Ещё один вопрос, который регулярно появляется на тематических форумах: "Как получить максимум из нескольких колонок одной таблицы?".
Любую задачу можно решить несколькими способами, эта не исключение. Я попытался собрать несколько вариантов для решения этого вопроса в этой заметке для разных версий SQL Server.
Кроме того, проведу небольшие замеры производительности всех рассмотренных примеров.

Буду рад, если вы предложите ещё несколько вариантов решения этой задачи и с удовольствием дополню эту статью позже.

Начнем с наполнения таблицы тестовыми данными. Для теста я буду использовать небольшую табличку с 5 колонками (a,b,c,d,e). При этом достаточно 5 записей.

if object_id ( N'dbo.test_table', N'U' ) is not null
  drop table dbo.test_table;
go

create table dbo.test_table ( id int identity
                            , a int
                            , b int
                            , c int
                            , d int
                            , e int
                            );
go

insert into dbo.test_table
select 10, 5, 12, 56, 2
union all
select 100, 45, 78, 6, 122
union all
select 150, 50, 127, 596, 22
union all
select 3410, 55, 0, -45, 90
union all
select -10, -5, 0, -56, -2;
go


Рис 01. Таблица с тестовыми данными

А теперь несколько примеров, как найти максимум в каждой строке по всем колонкам.

1. Выражение CASE (любая версия)
select *
     , max_val = case when ( a >= b ) and ( a >= c ) and ( a >= d ) and ( a >= e )
                      then a
                      when ( b >= a ) and ( b >= c ) and ( b >= d ) and ( b >= e )
                      then b
                      when ( c >= a ) and ( c >= b ) and ( c >= d ) and ( c >= e )
                      then c
                      when ( d >= a ) and ( d >= b ) and ( d >= c ) and ( d >= e )
                      then d
                      else e
                 end 
  from dbo.test_table


2. Подзапрос и оператор UNION (любая версия)
select *
     , max_val = ( select max(i) 
                     from ( select a 
                            union all 
                            select b 
                            union all 
                            select c
                            union all 
                            select d
                            union all 
                            select e
                          ) t(i)
                 )
  from dbo.test_table


3. Подзапрос и предложение VALUES (SQL Server 2008 и выше)
select *
     , max_val = ( select max(i) 
                     from ( values (a)
                                  ,(b)
                                  ,(c)
                                  ,(d)
                                  ,(e) 
                          ) t(i)
                 )
  from dbo.test_table


4. Оператор UNPIVOT (SQL Server 2005 и выше)
select id, max(val) as max_val
  from ( select * 
          from dbo.test_table t
            unpivot ( val for col in ( [a],[b],[c],[d],[e]) ) up
       
       ) t
  group by id


5. Оператор UNPIVOT и ранжирующие функции (SQL Server 2005 и выше)
select top 1 with ties id, val 
  from dbo.test_table t
    unpivot ( val for col in ( [a],[b],[c],[d],[e]) ) up
  order by row_number() over ( partition by id order by val desc )


6. Оператор CROSS APPLY (SQL Server 2005 и выше)
select * 
  from dbo.test_table t
    cross apply ( select top 1 val 
	                  from ( values (a),(b),(c),(d),(e) ) t (val)
                    order by val desc
                ) ca (max_val)

--Ещё вариант

select id, a, b, c, d, e, max_val
  from dbo.test_table t
    cross apply ( select row_number() over (order by val desc) i, val
	                  from ( values (a),(b),(c),(d),(e) ) t (val)
                ) ca (i, max_val)
  where ca.i = 1


7. Inline-функция и CTE (SQL Server 2005 и выше)
if object_id ( N'dbo.f_get_max', N'IF' ) is not null
  drop function dbo.f_get_max;
go
create function dbo.f_get_max ( 
    @a int
  , @b int
  , @c int
  , @d int
  , @e int
) returns table
as
return
with cte
as
( select val from 
    ( values (@a),(@b),(@c),(@d),(@e) 
    ) t(val)
)
select max(val) as max_val from cte;
go 

select * from dbo.test_table t
  cross apply dbo.f_get_max ( a, b, c, d, e )
go


А теперь посмотрим на производительность этих запросов, чтобы определить, какой из вариантов более предпочтителен. Тест буду проводить так же на 5 колонках таблицы с 10 млн. записей.
set nocount on;
go

if object_id ( N'dbo.test_table', N'U' ) is not null
  drop table dbo.test_table;
go

create table dbo.test_table ( id int identity primary key clustered
                            , a int
                            , b int
                            , c int
                            , d int
                            , e int
                            );
go

--Генерим 10 млн строк
with cte 
as
( 
  select 1 i
  union all
  select i+1 from cte where i < 10000000
)
insert into dbo.test_table
select checksum ( newid() ) % 1000 as a
     , checksum ( newid() ) % 1000 as b
     , checksum ( newid() ) % 1000 as c
     , checksum ( newid() ) % 1000 as d
     , checksum ( newid() ) % 1000 as e
  from cte
option ( maxrecursion 0 );

declare @val int;

print '1. Выражение CASE (любая версия)';
set statistics time on;

select @val =  case when ( a >= b ) and ( a >= c ) and ( a >= d ) and ( a >= e )
                    then a
                    when ( b >= a ) and ( b >= c ) and ( b >= d ) and ( b >= e )
                    then b
                    when ( c >= a ) and ( c >= b ) and ( c >= d ) and ( c >= e )
                    then c
                    when ( d >= a ) and ( d >= b ) and ( d >= c ) and ( d >= e )
                    then d
                    else e
                 end 
  from dbo.test_table;

set statistics time off;

print '2. Подзапрос и оператор UNION (любая версия)';
set statistics time on;

select @val = ( select max(i) 
                     from ( select a 
                            union all 
                            select b 
                            union all 
                            select c
                            union all 
                            select d
                            union all 
                            select e
                          ) t(i)
                 )
  from dbo.test_table;

set statistics time off;

print '3. Подзапрос и предложение VALUES (SQL Server 2008 и выше)';
set statistics time on;

select @val = ( select max(i) 
                  from ( values (a)
                              ,(b)
                              ,(c)
                              ,(d)
                              ,(e) 
                      ) t(i)
              )
  from dbo.test_table;

set statistics time off;

print '4. Оператор UNPIVOT (SQL Server 2005 и выше)';
set statistics time on;

select @val = max(val)
  from ( select * 
          from dbo.test_table t
            unpivot ( val for col in ( [a],[b],[c],[d],[e]) ) up
       
       ) t
  group by id;

set statistics time off;

print '5. Оператор UNPIVOT и ранжирующие функции (SQL Server 2005 и выше)';
set statistics time on;

select top 1 with ties @val = val 
  from dbo.test_table t
    unpivot ( val for col in ( [a],[b],[c],[d],[e]) ) up
  order by row_number() over ( partition by id order by val desc );

set statistics time off;

print '6. Оператор CROSS APPLY (SQL Server 2005 и выше)';

print 'CROSS APPLY 1';
set statistics time on;
select @val = max_val 
  from dbo.test_table t
    cross apply ( select top 1 val 
	                  from ( values (a),(b),(c),(d),(e) ) t (val)
                    order by val desc
                ) ca (max_val);

print 'CROSS APPLY 2';
select @val = max_val
  from dbo.test_table t
    cross apply ( select row_number() over (order by val desc) i, val
	                  from ( values (a),(b),(c),(d),(e) ) t (val)
                ) ca (i, max_val)
  where ca.i = 1;

set statistics time off;

print '7. Inline-функция и CTE (SQL Server 2005 и выше)';
set statistics time on;

select @val = max_val from dbo.test_table t  
  cross apply dbo.f_get_max ( a, b, c, d, e );

set statistics time off;
go

Результат ниже:
1. Выражение CASE (любая версия)

SQL Server Execution Times:
   CPU time = 3416 ms,  elapsed time = 3416 ms.
----------------------------------------------------------------------

2. Подзапрос и оператор UNION (любая версия)

SQL Server Execution Times:
   CPU time = 12777 ms,  elapsed time = 3848 ms.
----------------------------------------------------------------------

3. Подзапрос и предложение VALUES (SQL Server 2008 и выше)

SQL Server Execution Times:
   CPU time = 13182 ms,  elapsed time = 3968 ms.
----------------------------------------------------------------------

4. Оператор UNPIVOT (SQL Server 2005 и выше)

SQL Server Execution Times:
   CPU time = 22246 ms,  elapsed time = 15966 ms.
----------------------------------------------------------------------

5. Оператор UNPIVOT и ранжирующие функции (SQL Server 2005 и выше)

SQL Server Execution Times:
   CPU time = 153957 ms,  elapsed time = 239071 ms.
----------------------------------------------------------------------

6. Оператор CROSS APPLY (SQL Server 2005 и выше)

CROSS APPLY 1

SQL Server Execution Times:
   CPU time = 22963 ms,  elapsed time = 6273 ms.

CROSS APPLY 2

SQL Server Execution Times:
   CPU time = 33244 ms,  elapsed time = 9270 ms.
----------------------------------------------------------------------

7. Inline-функция и CTE (SQL Server 2005 и выше)

SQL Server Execution Times:
   CPU time = 12246 ms,  elapsed time = 3787 ms.

Tags: ,

SQL Server

Комментарии (9) -

user89
user89 Russia
16.02.2012 22:34:50 #

Алексей, спасибо за статью, добавил в избранное.
1-й вариант с case действительно самый быстрый, но надо добавить обработку null
Если вставить примерно такую строку
insert into dbo.test_table
   select 10, 5, 12, 56, null


то максимальное значение будет null.

Reply

Alexey Knyazev
Alexey Knyazev Russia
17.02.2012 1:55:46 #

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

Reply

Игорь
Игорь Russia
28.02.2012 14:02:35 #

универсальный вариант поиска максимума (без испольнования названия колонок):
DECLARE @test_table TABLE (id INT IDENTITY, a INT, b INT, c INT, d INT, e INT)  
DECLARE @X XML  
INSERT INTO @test_table
SELECT   10,  5, 12,  56,  2 UNION ALL
SELECT  100, 45, 78,   6,122 UNION ALL
SELECT  150, 50,127, 596, 22 UNION ALL
SELECT 3410, 55,  0, -45, 90 UNION ALL
SELECT -10,  -5,  0, -56, -2

SET @X = (SELECT * FROM @test_table FOR XML PATH(''), ROOT('T'))
SELECT MAX(X.value('.[local-name() != "id"]', 'int')) FROM @X.nodes('*/*') T(X)

Reply

kDn
kDn Ukraine
12.03.2012 17:02:36 #

Игорь видимо имел в виду следующее:

SELECT  t1.id
      , t1.a
      , t1.b
      , t1.c
      , t1.d
      , t1.e
      , t2.max_val
FROM    (
          SELECT    *
                  , CAST((
                           SELECT t2.* FROM @test_table t2 WHERE t1.id= t2.id
                         FOR
                           XML PATH('')
                         ) AS XML) xd
          FROM      @test_table t1
        ) t1
CROSS APPLY (
              SELECT    MAX(x.z.value('.[local-name() != "id"]', 'int')) max_val
              FROM      xd.nodes('/*') x ( z )
            ) t2

Reply

Pegoopik
Pegoopik Russia
16.06.2012 22:21:23 #

По поводу кейсов.

Довольно легко выйти на:
Internal error: An expression services limit has been reached. Please look for potentially complex expressions in your query, and try to simplify them.

У меня как раз подобная задача была, только нужно было из 7ми значений найти максимум и следующее после него значение. Кейсами 2008 r2 не вынесSmile
В итоге сделал через row_number+n in(1,2), так быстрее всего получилось, правда у меня довольно частный пример. Но перепробовал многие варианты отсюда, за статью спасибо!

Reply

Мурат
Мурат Kyrgyzstan
02.10.2012 17:33:35 #

Зачем нагружать CPU 1-й метод надо упростить - улучшить
отбрасывая то что уже меньше, проверьте в ms обратно


1. Выражение CASE (любая версия)

select *
     , max_val = case when ( a >= b ) and ( a >= c ) and ( a >= d ) and ( a >= e )
                      then a
                      when ( b >= c ) and ( b >= d ) and ( b >= e )
                      then b
                      when ( c >= d ) and ( c >= e )
                      then c
                      when ( d >= e )
                      then d
                      else e
                 end
  from dbo.test_table

Reply

dus
dus Russia
06.12.2012 2:49:17 #

Огромное спасибо за статью.
Пытался найти максимум из 6 значений с исключением NULL через case, но гораздо проше сделать через описаный здесь UNION с указанием WHERE i IS NOT NULL

Reply

pegoopik
pegoopik Russia
05.03.2013 11:22:23 #

Для произвольного числа колонок, вариант по скорости почти как с кейсами:

Для 50ти столбцов:

SELECT t.*, m49
FROM test_table t
  CROSS APPLY(SELECT CASE WHEN x1>x2 THEN x1 ELSE x2 END m1)c1
  CROSS APPLY(SELECT CASE WHEN x2>x3 THEN x2 ELSE x3 END m2)c2
  CROSS APPLY(SELECT CASE WHEN x3>x4 THEN x3 ELSE x4 END m3)c3
  ~~~~~~~~~~~~
  CROSS APPLY(SELECT CASE WHEN x48>x49 THEN x48 ELSE x49 END m48)c48
  CROSS APPLY(SELECT CASE WHEN x49>x50 THEN x49 ELSE x50 END m49)c49

Reply

Alexey Knyazev
Alexey Knyazev Russia
25.06.2013 19:55:04 #

Красивый, но медленный вариант через XML:

select t.*
     , (select t.* for xml path(''), type).value( 'max(*)', 'int' ) [sum]
from dbo.test_table as t

Reply

Добавить комментарий

  Country flag

biuquote
  • Комментарий
  • Предпросмотр
Loading