Search code examples
sql-serversql-server-2008pivotunpivot

How to replace a functional (many) OUTER APPLY (SELECT * FROM)


Applies to Microsoft SQL Server 2008 R2.

The problem is

If we have a few dozen Outer Apply (30) then they begin to work pretty slowly. In the middle of the Outer Apply I have something more complicated than a simple select, a view.

Details

I'm writing a sort of attributes assigned to tables (in the database). Generally, a few tables, holds a reference to a table of attributes (key, value).

Pseudo structure looks like this:

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

I can now run a query such as:

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

The simple way to get the pivot table of attribute values ​​and results for Lot rows that do not have attribute such a Kind3. I know Microsoft PIVOT and it is not simple and do not fits here.

Finally, what will be faster and will give the same results?


Solution

  • In order to get the result you can unpivot and then pivot the data.

    There are two ways that you can perform this. First, you can use the UNPIVOT and the PIVOT function:

    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
    

    See SQL Fiddle with Demo.

    Or starting in SQL Server 2008+, you can use CROSS APPLY with a VALUES clause to unpivot the data:

    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
    

    See SQL Fiddle with Demo.

    The unpivot process takes the multiple columns for each LotID and SomeText and converts it into rows giving the result:

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

    I added a row_number() to the inner subquery to be used to create the new column names to pivot. Once the names are created the pivot can be applied to the new columns giving the final result

    This could also be done using dynamic 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)
    

    See SQL Fiddle with Demo

    All three versions will give the same result:

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