Как заменить функционал (много) OUTER APPLY (SELECT * FROM)

Применимо к Microsoft SQL Server 2008 R2.

Проблема в

Если у нас несколько десятков Outer Apply (30) то они начинают работать довольно медленно. В середине Outer Apply у меня есть что-то более сложное, чем простой выбор, представление.

Подробности

Я пишу своего рода атрибуты, назначенные таблицам (в базе данных). Как правило, несколько таблиц содержат ссылку на таблицу атрибутов (ключ, значение).

Псевдоструктура выглядит так:

DECLARE @Lot TABLE (
LotId INT PRIMARY KEY IDENTITY, 
SomeText VARCHAR(8))

INSERT INTO @Lot
OUTPUT INSERTED.*
VALUES ('Hello'), ('World')

DECLARE @Attribute TABLE(
AttributeId INT PRIMARY KEY IDENTITY, 
LotId INT, 
Val VARCHAR(8),
Kind VARCHAR(8))

INSERT INTO @Attribute
OUTPUT INSERTED.* VALUES 
(1, 'Foo1', 'Kind1'), (1, 'Foo2', 'Kind2'), 
(2, 'Bar1', 'Kind1'), (2, 'Bar2', 'Kind2'), (2, 'Bar3', 'Kind3')

LotId       SomeText
----------- --------
1           Hello
2           World

AttributeId LotId       Val      Kind
----------- ----------- -------- --------
1           1           Foo1     Kind1
2           1           Foo2     Kind2
3           2           Bar1     Kind1
4           2           Bar2     Kind2
5           2           Bar3     Kind3

Теперь я могу запустить запрос, например:

SELECT 
[l].[LotId]
  , [SomeText]
  , [Oa1].[AttributeId]
  , [Oa1].[LotId]
  , 'Kind1Val' = [Oa1].[Val]
  , [Oa1].[Kind]
  , [Oa2].[AttributeId]
  , [Oa2].[LotId]
  , 'Kind2Val' = [Oa2].[Val]
  , [Oa2].[Kind]
  , [Oa3].[AttributeId]
  , [Oa3].[LotId]
  , 'Kind3Val' = [Oa3].[Val]
  , [Oa3].[Kind]  
FROM @Lot AS l
OUTER APPLY(SELECT * FROM @Attribute AS la WHERE la.[LotId] = l.[LotId] AND la.[Kind] = 'Kind1') AS Oa1
OUTER APPLY(SELECT * FROM @Attribute AS la WHERE la.[LotId] = l.[LotId] AND la.[Kind] = 'Kind2') AS Oa2
OUTER APPLY(SELECT * FROM @Attribute AS la WHERE la.[LotId] = l.[LotId] AND la.[Kind] = 'Kind3') AS Oa3


LotId       SomeText AttributeId LotId       Kind1Val Kind     AttributeId LotId       Kind2Val Kind     AttributeId LotId       Kind3Val Kind
----------- -------- ----------- ----------- -------- -------- ----------- ----------- -------- -------- ----------- ----------- -------- --------
1           Hello    1           1           Foo1     Kind1    2           1           Foo2     Kind2    NULL        NULL        NULL     NULL
2           World    3           2           Bar1     Kind1    4           2           Bar2     Kind2    5           2           Bar3     Kind3

Простой способ получить сводную таблицу значений атрибутов и результатов для строк лотов, не имеющих атрибута типа Kind3. Я знаю Microsoft PIVOT, и это не просто и не подходит сюда.

Наконец, что будет быстрее и даст те же результаты?


person Tomasito    schedule 12.03.2013    source источник


Ответы (1)


Чтобы получить результат, вы можете развернуть, а затем свести данные.

Есть два способа сделать это. Во-первых, вы можете использовать функции UNPIVOT и PIVOT:

select *
from
(
    select LotId,
        SomeText,
        col+'_'+CAST(rn as varchar(10)) col,
        value
    from
    (
        select l.LotId, 
            l.SomeText,
            cast(a.AttributeId as varchar(8)) attributeid,
            cast(a.LotId as varchar(8)) a_LotId,
            a.Val,
            a.Kind,
            ROW_NUMBER() over(partition by l.lotid order by a.attributeid) rn
        from @Lot l
        left join @Attribute a
            on l.LotId = a.LotId
    ) src
    unpivot
    (
        value
        for col in (attributeid, a_Lotid, val, kind)
    ) unpiv
) d
pivot
(
    max(value)
    for col in (attributeid_1, a_LotId_1, Val_1, Kind_1,
                attributeid_2, a_LotId_2, Val_2, Kind_2,
                attributeid_3, a_LotId_3, Val_3, Kind_3)
) piv

См. SQL Fiddle с демонстрацией.

Или, начиная с SQL Server 2008+, вы можете использовать CROSS APPLY с предложением VALUES, чтобы развернуть данные:

select *
from
(
    select LotId,
        SomeText,
        col+'_'+CAST(rn as varchar(10)) col,
        value
    from
    (
        select l.LotId, 
            l.SomeText,
            cast(a.AttributeId as varchar(8)) attributeid,
            cast(a.LotId as varchar(8)) a_LotId,
            a.Val,
            a.Kind,
            ROW_NUMBER() over(partition by l.lotid order by a.attributeid) rn
        from @Lot l
        left join @Attribute a
            on l.LotId = a.LotId
    ) src
    cross apply
    (
        values ('attributeid', attributeid),('LotId', a_LotId), ('Value', Val), ('Kind', Kind)
    ) c (col, value)
) d
pivot
(
    max(value)
    for col in (attributeid_1, LotId_1, Value_1, Kind_1,
                attributeid_2, LotId_2, Value_2, Kind_2,
                attributeid_3, LotId_3, Value_3, Kind_3)
) piv

См. SQL Fiddle с демонстрацией.

Процесс unpivot берет несколько столбцов для каждого LotID и SomeText и преобразует их в строки, что дает результат:

| LOTID | SOMETEXT |           COL | VALUE |
--------------------------------------------
|     1 |    Hello | attributeid_1 |     1 |
|     1 |    Hello |       LotId_1 |     1 |
|     1 |    Hello |       Value_1 |  Foo1 |
|     1 |    Hello |        Kind_1 | Kind1 |
|     1 |    Hello | attributeid_2 |     2 |

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

Это также можно сделать с помощью динамического SQL:

DECLARE @cols AS NVARCHAR(MAX),
    @query  AS NVARCHAR(MAX)

select @cols = STUFF((SELECT ',' + QUOTENAME(col+'_'+rn) 
                    from 
                    (
                      select 
                        cast(ROW_NUMBER() over(partition by l.lotid order by a.attributeid) as varchar(10)) rn
                      from Lot l
                      left join Attribute a
                          on l.LotId = a.LotId
                    ) t
                    cross apply (values ('attributeid', 1),
                                 ('LotId', 2), 
                                 ('Value', 3), 
                                 ('Kind', 4)) c (col, so)
                    group by col, rn, so
                    order by rn, so
            FOR XML PATH(''), TYPE
            ).value('.', 'NVARCHAR(MAX)') 
        ,1,1,'')


set @query = 'SELECT LotId,
                    SomeText,' + @cols + ' 
             from 
             (
                select LotId,
                    SomeText,
                    col+''_''+CAST(rn as varchar(10)) col,
                    value
                from
                (
                    select l.LotId, 
                        l.SomeText,
                        cast(a.AttributeId as varchar(8)) attributeid,
                        cast(a.LotId as varchar(8)) a_LotId,
                        a.Val,
                        a.Kind,
                        ROW_NUMBER() over(partition by l.lotid order by a.attributeid) rn
                    from Lot l
                    left join Attribute a
                        on l.LotId = a.LotId
                ) src
                cross apply
                (
                    values (''attributeid'', attributeid),(''LotId'', a_LotId), (''Value'', Val), (''Kind'', Kind)
                ) c (col, value)
            ) x
            pivot 
            (
                max(value)
                for col in (' + @cols + ')
            ) p '

execute(@query)

См. SQL Fiddle с демонстрацией.

Все три версии дадут одинаковый результат:

| LOTID | SOMETEXT | ATTRIBUTEID_1 | LOTID_1 | VALUE_1 | KIND_1 | ATTRIBUTEID_2 | LOTID_2 | VALUE_2 | KIND_2 | ATTRIBUTEID_3 | LOTID_3 | VALUE_3 | KIND_3 |
-----------------------------------------------------------------------------------------------------------------------------------------------------------
|     1 |    Hello |             1 |       1 |    Foo1 |  Kind1 |             2 |       1 |    Foo2 |  Kind2 |        (null) |  (null) |  (null) | (null) |
|     2 |    World |             3 |       2 |    Bar1 |  Kind1 |             4 |       2 |    Bar2 |  Kind2 |             5 |       2 |    Bar3 |  Kind3 |
person Taryn    schedule 12.03.2013
comment
Вы — знаток поворотов! ('!-) - person Kaf; 12.03.2013
comment
@Kaf Я предпочитаю ботаников, но да, я в курсе. - person Taryn; 12.03.2013
comment
+1 за отклонение термина компьютерщик, особенно в свете некоторых более красочных исторических употреблений (см. oed.com/view/Entry/77307?rskey=fHnqwr&result=1#eid ) - person David T. Macknet; 12.03.2013
comment
SET STATISTICS IO ON дает: количество сканирований 3, логических чтений 15 VS количество сканирований 1, логических чтений 5 в вашем решении. Сейчас попробую на реальных данных. - person Tomasito; 12.03.2013
comment
Но что, если я: УДАЛИТЬ ИЗ Атрибута; ВСТАВИТЬ В ЗНАЧЕНИЯ Атрибута (2, 'Bar2', 'Kind2'), (2, 'Bar1', 'Kind8'), (2, 'Bar3', 'Kind3'), (1, 'Foo1', 'Kind1' ); Иногда у меня есть Лот с полным набором атрибутов, а иногда только с несколькими. - person Tomasito; 13.03.2013
comment
@Tomasito Я не уверен, о чем ты спрашиваешь. - person Taryn; 13.03.2013
comment
@Kaf Полностью согласен :D Я называю ее Богиня поворотов - person John Woo; 13.03.2013
comment
@bluefeet Посмотрите на sqlfiddle.com/#!3/d41d8/10842 Kind_1 =Kind2, Kind_2 — это Kind8. Это зависит от порядка вставки данных. - person Tomasito; 13.03.2013
comment
@Tomasito Тогда каким должен быть порядок? - person Taryn; 13.03.2013
comment
@bluefeet посмотрите на sqlfiddle.com/#!3/55da1/6, это чего я пытаюсь достичь. Могу ли я сделать это без таблицы AttributeMask? - person Tomasito; 13.03.2013
comment
@Tomasito Мое предложение состоит в том, чтобы опубликовать еще один вопрос с подробностями вашей новой проблемы, это предотвратит обмен мнениями в комментариях, и тогда вы сможете предоставить полную картину своих потребностей. :) - person Taryn; 13.03.2013
comment
@bluefeet Хорошее предложение. stackoverflow.com/questions/15397144/ Спасибо. Вам большое спасибо за помощь до сих пор, был на 100% точен. - person Tomasito; 14.03.2013