Labels

Friday, December 30, 2011

CROSS APPLY VS OUTER APPLY

The terminology used with CROSS APPLY and OUTER APPLY is somewhat misleading. A better name for CROSS APPLY would be INNER APPLY. The APPLY ( CROSS APPLY, OUTER APPLY) operator allows value(s) from the main (outer) query to be used as input to a table-valued function. The rows from the main query and table-valued function are combined. As an example, the orgchart subtree table-valued function may return 290 rows in subtree format for the CEO of AdventureWorks but only 10 rows for the marketing manager. The following T-SQL example scripts will demonstrate how CROSS APPLY and OUTER APPLY can be used in practical database applications.

In the following example we demonstrate CROSS APPLY & OUTER APPLY with group by derived table.

USE AdventureWorks;

DECLARE  @Year  INT,
         @Month INT
SET @Year = 2003;
SET @Month = 

-- SQL cross apply  
-- SQL Server cross apply
-- SQL group by
-- SQL correlated subquery

SELECT [Customer] = s.Name,

       -- Special money data type currency formatting option
       [TotalSalesforMonth] = '$' +
        Convert(VARCHAR,Convert(MONEY,SalesAmount.OrderTotal),1)
FROM     Sales.Customer AS c
         -- The customer name is in this table
                       INNER JOIN Sales.Store AS s
           ON s.CustomerID = c.CustomerID
         -- Cross apply
         CROSS APPLY (SELECT   soh.CustomerId,
                               Sum(sod.LineTotal) AS OrderTotal
                      FROM     Sales.SalesOrderHeader AS soh
                               INNER JOIN Sales.SalesOrderDetail AS sod
                                 ON sod.SalesOrderId = soh.SalesOrderId
                      -- Correlation to the outer query
                     WHERE soh.CustomerId = c.CustomerId
                                             -- Filter data
                               AND Year(OrderDate) = @Year
                               AND Month(OrderDate) = @Month
                      GROUP BY soh.CustomerId) AS SalesAmount
ORDER BY [Customer]
             
/* Partial results  
(174 row(s) affected)

Customer                      TotalSalesforMonth
Accessories Network           $283.95
Acclaimed Bicycle Company     $3,405.17
Action Bicycle Specialists    $92,278.05
Advanced Bike Components      $68,906.65
Aerobic Exercise Company      $48.59
*/
             
-- SQL outer apply
-- SQL Server outer apply 

SELECT [Customer] = s.Name,
                     -- Special money data type currency formatting option
       [TotalSalesforMonth] = '$' +
                      Convert(VARCHAR,Convert(MONEY,SalesAmount.OrderTotal),1)
FROM     Sales.Customer AS c
                       -- The customer name is in this table
                       INNER JOIN Sales.Store AS s
           ON s.CustomerID = c.CustomerID
         OUTER APPLY (SELECT   soh.CustomerId,
                               Sum(sod.LineTotal) AS OrderTotal
                      FROM     Sales.SalesOrderHeader AS soh
                               INNER JOIN Sales.SalesOrderDetail AS sod
                                 ON sod.SalesOrderId = soh.SalesOrderId

                      -- Correlation to the outer query
                     WHERE soh.CustomerId = c.CustomerId
                               -- Filter data
                               AND Year(OrderDate) = @Year
                               AND Month(OrderDate) = @Month
                      GROUP BY soh.CustomerId) AS SalesAmount
ORDER BY [Customer]
GO
             
/* Partial results
             
(701 row(s) affected)

Customer                      TotalSalesforMonth
Acceptable Sales & Service    NULL
Accessories Network           $283.95
Acclaimed Bicycle Company     $3,405.17
Ace Bicycle Supply            NULL
Action Bicycle Specialists    $92,278.05
Active Cycling                NULL

*/
              In the following example we demonstrate CROSS APPLY & OUTER APPLY with table-valued function. We start with creating a table-valued function. In the second step, we will create data and store it in a table variable.

USE AdventureWorks;

GO

-- Minimum function

-- Maximum function

-- SQL minmax function

-- T-SQL table-valued function

CREATE FUNCTION fnMinimumMaximum

               (@Input1 MONEY,

                @Input2 MONEY)

RETURNS @MinMax  TABLE (Minimum money, Maximum money)

AS

BEGIN

  IF @Input1 is NULL and @Input2 is NULL RETURN

  INSERT @MinMax

    SELECT CASE

              WHEN @Input1 < @Input2 THEN @Input1

              WHEN @Input2 < @Input1 THEN @Input2

              ELSE COALESCE(@Input1,@Input2)

            END AS Minimum,

            CASE

              WHEN @Input1 > @Input2 THEN @Input1

              WHEN @Input2 > @Input1 THEN @Input2

              ELSE COALESCE(@Input1,@Input2)

            END AS Maximum;

  RETURN

END -- function



GO

-- SELECT * FROM dbo.fnMinimumMaximum(NULL, NULL) 

-- SQL table variable create and population

-- Cross or outer apply will select the maximum dimension

DECLARE @Product TABLE (ProductName varchar(30), Width int, Height int) 

INSERT @Product VALUES ( 'PortraitFrame', 8, 11)

INSERT @Product VALUES ( 'LandscapeFrame', 12, 7)

INSERT @Product VALUES ( 'Notepad Computer', NULL, NULL)

INSERT @Product VALUES ( 'CircleFrame', 10, 10)

INSERT @Product VALUES ( 'Ringbinder', NULL, NULL)



-- OUTER APPLY includes nomatches (null data)

-- SQL outer apply

-- Outer apply T-SQL

-- Outer apply mssql

-- SQL outer apply with table-valued function

SELECT ProductName, MaxDimension=Maximum

FROM @Product p

OUTER APPLY dbo.fnMinimumMaximum (Width, Height)

/*

ProductName             MaxDimension

PortraitFrame           11.00

LandscapeFrame          12.00

Notepad Computer        NULL

CircleFrame             10.00

Ringbinder              NULL

*/



-- CROSS APPLY excludes nomatches (null data)

-- SQL cross apply

-- Cross apply T-SQL

-- Cross apply mssql

-- SQL cross apply with table-valued function

SELECT ProductName, MaxDimension=Maximum

FROM @Product p

CROSS APPLY dbo.fnMinimumMaximum (Width, Height)

GO

/*

ProductName             MaxDimension

PortraitFrame           11.00

LandscapeFrame          12.00

CircleFrame             10.00

*/

------------

The following example applies a derived table from a correlated subquery instead of table-valued function (UDF).

------------

-- OUTER APPLY versus CROSS APPLY using derived table

------------

-- Create and populate tables for testing

USE tempdb;

GO

SET NOCOUNT ON;

CREATE TABLE Account (

  AccountID   INT    IDENTITY    PRIMARY KEY,

  AccountName VARCHAR(50),

  CreateDate  SMALLDATETIME    DEFAULT (CURRENT_TIMESTAMP),

  IsActive    BIT    DEFAULT (1))



GO



CREATE TABLE [Transaction] (

  TransactionId   INT    IDENTITY    PRIMARY KEY,

  AccountID       INT,

  Amount          MONEY,

  TransactionDate SMALLDATETIME    DEFAULT (getdate()))



GO

INSERT Account(AccountName) VALUES ('Roger Smith')

INSERT Account(AccountName) VALUES ('Linda White')

INSERT Account(AccountName) VALUES ('Corner Hardware')

INSERT Account(AccountName) VALUES ('Laptop Land')

INSERT Account(AccountName) VALUES ('Cellphone City')



INSERT [Transaction] (AccountID, Amount) VALUES(1, 1400.0)

INSERT [Transaction] (AccountID, Amount) VALUES(1, 1200.0)

INSERT [Transaction] (AccountID, Amount) VALUES(1, 1300.0)

INSERT [Transaction] (AccountID, Amount) VALUES(1, 1100.0)

INSERT [Transaction] (AccountID, Amount) VALUES(2, 400.0)

INSERT [Transaction] (AccountID, Amount) VALUES(2, 200.0)

INSERT [Transaction] (AccountID, Amount) VALUES(2, 300.0)

INSERT [Transaction] (AccountID, Amount) VALUES(2, 900.0)

INSERT [Transaction] (AccountID, Amount) VALUES(3, 33400.0)

INSERT [Transaction] (AccountID, Amount) VALUES(3, 11200.0)

INSERT [Transaction] (AccountID, Amount) VALUES(3, 22300.0)

INSERT [Transaction] (AccountID, Amount) VALUES(3, 12100.0)

GO



-- CROSS APPLY returns only matching data

-- SQL cross apply

-- SQL derived table

-- SQL correlated subquery

SELECT   a.AccountName,

         TopAmount=tt.Amount

FROM     ACCOUNT a

CROSS APPLY (SELECT   TOP ( 1 ) Amount

             FROM     [Transaction] t

             WHERE    t.AccountID = a.AccountID

             ORDER BY Amount DESC) tt

ORDER BY AccountName

/*

AccountName       TopAmount

Corner Hardware   33400.00

Linda White       900.00

Roger Smith       1400.00

*/



-- OUTER APPLY returns  matching and non-matching data

-- SQL outer apply

SELECT   a.AccountName,

         TopAmount=tt.Amount

FROM     ACCOUNT a

OUTER APPLY (SELECT   TOP ( 1 ) Amount

             FROM     [Transaction] t

             WHERE    t.AccountID = a.AccountID

             ORDER BY Amount DESC) tt

ORDER BY AccountName

/*

AccountName       TopAmount

Cellphone City    NULL

Corner Hardware   33400.00

Laptop Land       NULL

Linda White       900.00

Roger Smith       1400.00

*/



-- Cleanup

DROP TABLE tempdb.dbo.Account

DROP TABLE tempdb.dbo.[Transaction]

------------

The following example applies AdventureWorks data. Following is the T-SQL code sample script to create a function to be used with "APPLY":

USE AdventureWorks;

GO

-- SQL user-defined function

-- SQL table-valued function

CREATE FUNCTION Sales.fnCustomerOrderTotal

               (@CustomerID INT)

RETURNS @Result TABLE(OrderTotal MONEY)

AS

  BEGIN

    INSERT @Result

    SELECT   SUM(sod.LineTotal) AS OrderTotal

    FROM     Sales.SalesOrderHeader AS soh

             JOIN Sales.SalesOrderDetail AS sod

               ON sod.SalesOrderID = soh.SalesOrderID

    WHERE    soh.CustomerID = @CustomerID

    GROUP BY soh.CustomerID

    

    RETURN

  END



GO

OUTER APPLY will return rows also for customers without order as nulls. In the example below null is translated into 0.0. This is logical businesswise.

-- SQL outer apply

-- SQL Server outer apply

-- Outer apply T-SQL

SELECT   STORE = s.Name,

         OrderTotal = convert(VARCHAR,isnull(cot.OrderTotal,0.0),1)

FROM     Sales.Store AS s

         JOIN Sales.Customer AS c

           ON s.CustomerID = c.CustomerID

         OUTER APPLY Sales.fnCustomerOrderTotal(c.CustomerID) AS cot

ORDER BY STORE


This is the partial result set:

Store OrderTotal
A Bicycle Association 0
A Bike Store 85,177.08
A Cycle Shop 0
A Great Bicycle Company 9,055.29
A Typical Bike Shop 83,457.11
Acceptable Sales & Service 1,258.38
Accessories Network 2,165.79
Acclaimed Bicycle Company 7,300.83

Following is the way to use "CROSS APPLY". The result set will not contain 0.0 (null) orders.

-- SQL cross apply

-- SQL Server cross apply

-- Cross apply mssql

SELECT   STORE = s.Name,

         OrderTotal = convert(VARCHAR,isnull(cot.OrderTotal,0.0),1)

FROM     Sales.Store AS s

         JOIN Sales.Customer AS c

           ON s.CustomerID = c.CustomerID

         CROSS APPLY Sales.fnCustomerOrderTotal(c.CustomerID) AS cot

ORDER BY STORE

This is the partial result set:

Store OrderTotal
A Bike Store 85,177.08
A Great Bicycle Company 9,055.29
A Typical Bike Shop 83,457.11
Acceptable Sales & Service 1,258.38
Accessories Network 2,165.79
Acclaimed Bicycle Company 7,300.83
Ace Bicycle Supply 3,749.13
Action Bicycle Specialists 321,752.83
Active Cycling 1,805.45
Active Life Toys 200,013.37
Active Systems 639.98
Active Transport Inc. 88,245.87
Activity Center 42,650.40



------------

In the following example the supervisory subtree function returns the entire orgchart for a supervisory personnel at any level. For CEO, Ken Sanchez, it returns 290 employees including himself. For non-supervisory staff the table-valued function returns an empty table.

CROSS APPLY returns only correlated data. Therefore, for marketing staff Mary Gibson it returns nothing.

OUTER APPLY will return non-correlated data as well, placing NULLs into the missing cells.

USE tempdb;

GO

-- Create table for demonstration

-- SQL select into create table

-- SQL inner join

SELECT e.EmployeeID,

       e.Title,

       StaffName = LastName + ', ' + FirstName,

       ManagerID,

       Department = d.Name

INTO   Employee

FROM   AdventureWorks.HumanResources.Employee e

       INNER JOIN AdventureWorks.HumanResources.EmployeeDepartmentHistory edh

         ON e.EmployeeID = edh.EmployeeID

       INNER JOIN AdventureWorks.HumanResources.Department d

         ON edh.DepartmentID = d.DepartmentID

       INNER JOIN AdventureWorks.Person.Contact c

         ON e.ContactID = c.ContactID

WHERE  edh.EndDate IS NULL

GO



SELECT COUNT(*) FROM Employee

GO

-- 290 - Entire staff of AdventureWorks Cycles



-- Function to return OrgChart for supervisors only

-- For non-supervisory staff it will have empty return

-- SQL table-valued function

-- SQL user-defined function UDF

-- Tree processing function

CREATE FUNCTION fnSupervisorySubTree

              (@EmployeeID AS INT)

RETURNS @TREE TABLE(EmployeeID   INT,

                    EmployeeName VARCHAR(35),

                    Department   VARCHAR(30),

                    ManagerID    INT,

                    ManagerName  VARCHAR(35),

                    SubTreeLevel INT)

AS

  BEGIN

    -- SQL common table expression - CTE

    -- SQL recursive CTE

    WITH cteSubTree(EmployeeID,EmployeeName,Department,ManagerID,ManagerName,SubTreeLevel)

         AS (-- Anchor (root) node

             SELECT e1.EmployeeID,

                    e1.StaffName,

                    e1.Department,

                    e1.ManagerID,

                    e2.StaffName,

                    0

             FROM   Employee e1

                    LEFT JOIN Employee e2

                      ON (e1.ManagerID = e2.EmployeeID)

             WHERE  e1.EmployeeID = @EmployeeID

             UNION ALL

             -- Recursive nodes to leaf level

             SELECT e1.EmployeeID,

                    e1.StaffName,

                    e1.Department,

                    e1.ManagerID,

                    e2.StaffName,

                    cte.SubTreeLevel + 1

             FROM   Employee e1

                    INNER JOIN Employee e2

                      ON e1.ManagerID = e2.EmployeeID

                    JOIN cteSubTree AS cte

                      ON e1.ManagerID = cte.EmployeeID)

    -- Return results

    INSERT INTO @TREE

    SELECT *

    FROM   cteSubTree

    -- IF NOTE SUPERVISOR, RETURN AN EMPTY SET (TABLE)

    IF (SELECT COUNT(* )  FROM   @TREE) = 1

      DELETE @TREE

    

    RETURN

  END



GO



-- Test UDF - user-defined function

-- Generate orgchart for Marketing, David Bradley manager

SELECT EmpID = EmployeeID,

       EmpName = EmployeeName,

       Department,

       MgrID = ManagerID,

       MgrName = ManagerName,

       SubLvl = SubTreeLevel

FROM   dbo.fnSupervisorySubTree(6)

GO

/* Results



EmpID EmpName           Department  MgrID MgrName           SubLvl

6     Bradley, David    Marketing   109   Sánchez, Ken      0

2     Brown, Kevin      Marketing   6     Bradley, David    1

46    Harnpa…, Sariya   Marketing   6     Bradley, David    1

106   Gibson, Mary      Marketing   6     Bradley, David    1

119   Williams, Jill    Marketing   6     Bradley, David    1

203   Eminhizer, Terry  Marketing   6     Bradley, David    1

269   Benshoof, Wanida  Marketing   6     Bradley, David    1

271   Wood, John        Marketing   6     Bradley, David    1

272   Dempsey, Mary     Marketing   6     Bradley, David    1

*/



-- CROSS APPLY returns no row for staff Mary Gibson

-- SQL cross apply

SELECT Employee = e.StaffName,

       e.Title,

       oc.Department,

       Staff = oc.EmployeeName,

       Supervisor = oc.ManagerName

FROM   Employee e

       CROSS APPLY dbo.fnSupervisorySubTree(e.EmployeeID) AS oc

WHERE  e.EmployeeID = 106

/*

Employee          Title                   Department  Staff Supervisor

*/



-- OUTER APPLY returns a row for staff Mary Gibson

-- SQL outer apply

SELECT   Employee = e.StaffName,

         e.Title,

         oc.Department,

         Staff = oc.EmployeeName,

         Supervisor = oc.ManagerName

FROM     Employee e

         OUTER APPLY dbo.fnSupervisorySubTree(e.EmployeeID) AS oc

WHERE    e.EmployeeID = 106

GO

/*

Employee          Title                   Department  Staff Supervisor

Gibson, Mary      Marketing Specialist    NULL        NULL  NULL

*/
             

-- Cleanup

DROP TABLE tempdb.dbo.Employee

DROP FUNCTION dbo.fnSupervisorySubTree

GO

No comments:

Post a Comment