March 14 , 2011
Since 2004 there is buzz in SQL circles about CROSS APPLY, a new feature of SQL Server 2005. Actually, CROSS APPLY is quite simple: a restricted "INNER JOIN" between a table (outer query) and a table-valued function (common usage), or derived table from correlated subquery. The table-valued function is evaluated only for the paramater values supplied by the outer query. Here is a quick example for CROSS APPLY usage applying a DMV and a DMF:
-- SQL Cross Apply quick syntax - SELECT * last ran stats
SELECT deqs.last_execution_time AS LastRun, dest.TEXT AS QueryText, *
FROM sys.dm_exec_query_stats AS deqs
CROSS APPLY sys.dm_exec_sql_text(deqs.sql_handle) AS dest
WHERE LEFT(dest.TEXT,8)='SELECT *'
ORDER BY LastRun DESC
The results generated by CROSS APPLY queries can be achieved without using CROSS APPLY by
applying a temporary table, common table expression or table variables. However, that is a bit tedious. The
application of CROSS APPLY in conjunction with a table-valued
function yields a neat and very powerful solution in a single well structured query.When we take the AdventureWorks2008 sample database product table we see that the ProductID-s are not continuous by color. The following exercise finds the missing ID-s for each color to make it a perfect sequence with the application of CROSS APPLY and nested CTE-s. The fnRange table-valued UDF generates a sequence between two integers. We use LEFT JOIN to find the "gap" numbers.
CREATE FUNCTION fnRange ( @Start int, @End int)
RETURNS TABLE AS
RETURN (SELECT * FROM (SELECT SEQ=ROW_NUMBER() OVER (ORDER BY (SELECT 1))
FROM MASTER.dbo.spt_values a
-- CROSS JOIN MASTER.dbo.spt_values b -- uncomment for more range
) x
WHERE SEQ BETWEEN @Start AND @End)
GO
;WITH CTE (Color, MinID, MaxID) AS
(SELECT Color, MIN(ProductID), MAX(ProductID)
FROM Production.Product
WHERE Color is not null GROUP BY Color),
cteSEQ AS (SELECT Color, SEQ FROM CTE
CROSS APPLY dbo.fnRange (MinID, MaxID) as R)
SELECT *, P.Color, I.SEQ
FROM cteSEQ I LEFT JOIN Production.Product P
ON I.SEQ = P.ProductID AND I.Color = P.Color
AND P.Color is not null
WHERE P.ProductID is null
ORDER BY I.Color, I.SEQ
/* Missing from continuous ID sequence
Color SEQ
Black 317
Black 318
Black 319
Black 320
Black 321
.... */
In another article I mentioned that CROSSTAB reports are excellent
career boosters for database developers and DBA-s. So are CROSS APPLY reports.
In the first three T-SQL examples we use derived table (as opposed to table-valued function) for CROSS APPLY.
In the first three T-SQL examples we use derived table (as opposed to table-valued function) for CROSS APPLY.
------------
-- T-SQL column aliasing with CROSS APPLY
------------
SELECT TranID, ProdID, Qty, Cost, TotalCost=Qty * Cost
FROM AdventureWorks2008.Production.TransactionHistory
CROSS APPLY
(SELECT TranID = TransactionID
,ProdID = ProductID
,Qty = Quantity
,Cost = ActualCost ) x
WHERE Qty > 10 and Cost > 0.0
ORDER BY TranID, ProdID
/* TranID ProdID Qty Cost TotalCost
100154 864 14 41.275 577.85
100157 869 24 45.4935 1091.844
100198 869 16 45.4935 727.896
....
*/
-- T-SQL column aliasing with CROSS APPLY
------------
SELECT TranID, ProdID, Qty, Cost, TotalCost=Qty * Cost
FROM AdventureWorks2008.Production.TransactionHistory
CROSS APPLY
(SELECT TranID = TransactionID
,ProdID = ProductID
,Qty = Quantity
,Cost = ActualCost ) x
WHERE Qty > 10 and Cost > 0.0
ORDER BY TranID, ProdID
/* TranID ProdID Qty Cost TotalCost
100154 864 14 41.275 577.85
100157 869 24 45.4935 1091.844
100198 869 16 45.4935 727.896
....
*/
------------
------------
-- CROSS APPLY using GROUP BY derived table from correlated subquery
------------
USE AdventureWorks;
DECLARE @Year INT,
@Month INT
SET @Year = 2003;
SET @Month = 2
-- SQL cross apply - SQL group by - SQL correlated subquery
SELECT [Customer] = s.Name,
-- Special money data type currency formatting option
[Total$ Sales] = '$' +
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
-- The inner query is a correlated GROUP BY subquery
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
-- This is the 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
(132 row(s) affected)
Customer Total$ Sales
Ace Bicycle Supply $647.99
Affordable Sports Equipment $50,953.32
Alpine Ski House $939.59
Basic Sports Equipment $159.56
Bicycle Lines Distributors $22,243.33
*/
/**** OUTER APPLY results: (701 row(s) affected) ********/
------------
-- CROSS APPLY using derived table from correlated subquery
------------
-- 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 Deposit (
DepositId INT IDENTITY PRIMARY KEY,
AccountID INT,
Amount MONEY,
DepositDate SMALLDATETIME DEFAULT (getdate()))
GO
INSERT Account(AccountName) VALUES ('Charles Mills')
INSERT Account(AccountName) VALUES ('Miranda Vegas')
INSERT Account(AccountName) VALUES ('Corner Hardware')
INSERT Account(AccountName) VALUES ('Laptop Land')
INSERT Account(AccountName) VALUES ('Cellphone City')
SELECT * FROM Account
/*
AccountID AccountName CreateDate IsActive
1 Charles Mills 2015-01-25 08:38:00 1
2 Miranda Vegas 2015-01-25 08:38:00 1
3 Corner Hardware 2015-01-25 08:38:00 1
4 Laptop Land 2015-01-25 08:38:00 1
5 Cellphone City 2015-01-25 08:38:00 1
*/
INSERT Deposit (AccountID, Amount) VALUES(1, 1400.0)
INSERT Deposit (AccountID, Amount) VALUES(1, 1200.0)
INSERT Deposit (AccountID, Amount) VALUES(1, 1300.0)
INSERT Deposit (AccountID, Amount) VALUES(1, 1100.0)
INSERT Deposit (AccountID, Amount) VALUES(2, 400.0)
INSERT Deposit (AccountID, Amount) VALUES(2, 200.0)
INSERT Deposit (AccountID, Amount) VALUES(2, 300.0)
INSERT Deposit (AccountID, Amount) VALUES(2, 900.0)
INSERT Deposit (AccountID, Amount) VALUES(3, 33400.0)
INSERT Deposit (AccountID, Amount) VALUES(3, 11200.0)
INSERT Deposit (AccountID, Amount) VALUES(3, 22300.0)
INSERT Deposit (AccountID, Amount) VALUES(3, 12100.0)
GO
-- CROSS APPLY returns only matching data - SQL cross apply
-- Cross apply T-SQL - Cross apply mssql - derived table - SQL correlated subquery
SELECT a.AccountName,
TopDeposit=tt.Amount
FROM ACCOUNT a
CROSS APPLY (SELECT TOP ( 1 ) Amount
FROM Deposit t
WHERE t.AccountID = a.AccountID
ORDER BY Amount DESC) tt
ORDER BY AccountName
/*
AccountName TopDeposit
Corner Hardware 33400.00
Miranda Vegas 900.00
Charles Mills 1400.00
*/
-- Cleanup
DROP TABLE tempdb.dbo.Account
DROP TABLE tempdb.dbo.Deposit
------------
In the following example, the table-valued inline function returns the top
5 (highest TotalDue) orders for a store or individual customer, provided
there are 5 orders. The SELECT query itself is restricted to stores. CROSS
APPLY joins the store information with the top 5 orders information
produced by the table-valued function.
USE AdventureWorks;
USE AdventureWorks;
GO
-- SQL inline function
-- User-defined inline function
CREATE FUNCTION Sales.fnTopNOrders (
@CustomerID AS INT,
@n AS INT )
RETURNS TABLE
AS
RETURN
SELECT
TOP(@n) SalesOrderID,
ShipDate = convert(char(10), ShipDate,112),
-- SQL currency formatting
TotalDue=convert(varchar,TotalDue,1)
FROM AdventureWorks.Sales.SalesOrderHeader
WHERE CustomerID = @CustomerID
ORDER BY TotalDue DESC
GO
-- SQL cross apply - SQL Server cross apply
SELECT
StoreName=s.Name,
[Top].ShipDate,
[Top].SalesOrderID,
TotalDue='$'+[Top].TotalDue
FROM AdventureWorks.Sales.Store AS s
JOIN AdventureWorks.Sales.Customer AS c
ON s.CustomerID = c.CustomerID
CROSS APPLY
AdventureWorks.Sales.fnTopNOrders(c.CustomerID, 5) AS [Top]
WHERE CustomerType='S'
ORDER BY StoreName, convert(money,TotalDue) DESC
GO
Partial result set:
|
-- SQL cross apply - Cross apply mssql
-- SQL dynamic management view - dmv - SQL dynamic management function - dmf
SELECT LastExecutionTime = max(last_execution_time),
Query = Text
FROM sys.dm_exec_query_stats AS eqs
CROSS APPLY sys.dm_exec_sql_text(eqs.sql_handle) AS SQL
WHERE Text LIKE ('%AdventureWorks%')
AND Text NOT LIKE ('%fullText%')
GROUP BY Text
ORDER BY Query
GO
/* Partial results
LastExecutionTime Query
2009-01-24 09:42:23.217 SELECT EmployeeID, StaffName = LastName+',....
2009-01-24 10:31:49.170 SELECT e.EmployeeID, e.Title, StaffName = ....
*/
-- SQL Server cross apply
SELECT
PlanSource=sql.text,
plans.*
FROM sys.dm_exec_cached_plans plans
CROSS APPLY sys.dm_exec_sql_text (plans.plan_handle) AS sql
-- SQL cross apply
SELECT
RequestSource = sql.text,
er.*
FROM
sys.dm_exec_requests er
CROSS APPLY
sys.dm_exec_sql_text(sql_handle) AS sqlGO
USE tempdb;
GO
-- 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 * FROM Employee
-- SQL table-valued function - user-defined function UDF - Tree processing function
CREATE FUNCTION fnOrgChartSubTree(@EmployeeID AS INT)
RETURNS @TREE TABLE (
EmployeeID INT
,EmployeeName VARCHAR(35)
,Department VARCHAR(30)
,ManagerID INT
,ManagerName VARCHAR(35)
,OrgChartLevel INT )
AS
BEGIN
-- SQL common table expression - CTE - SQL recursive CTE
WITH cteOrgChartSubTree(EmployeeID, EmployeeName, Department,
ManagerID, ManagerName, OrgChartLevel)
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.OrgChartLevel+1
FROM Employee e1
INNER JOIN Employee e2
ON e1.ManagerID = e2.EmployeeID
JOIN cteOrgChartSubTree AS cte
ON e1.ManagerID = cte.EmployeeID
)
-- Return results
INSERT INTO @TREE
SELECT * FROM cteOrgChartSubTree;
RETURN
END
GO
-- Find CEO
SELECT CEO=StaffName, CEOid =EmployeeID
FROM Employee
WHERE ManagerID is NULL
GO
/* Results
CEO CEOid
Sánchez, Ken 109
*/
-- Test UDF - user-defined function
-- Generate orgchart starting with CEO
SELECT EmpID = EmployeeID, EmpName=EmployeeName, Department,
MgrID=ManagerID, MgrName=ManagerName, OCLvl=OrgChartLevel
FROM dbo.fnOrgChartSubTree(109)
GO
/* 290 rows - Partial results
EmpID EmpName Department MgrID MgrName OCLvl
109 Sánchez, Ken Executive NULL NULL 0
6 Bradley, David Marketing 109 Sánchez, Ken 1
12 Duffy, Terri Engineering 109 Sánchez, Ken 1
42 Trenary, Jean Informa… 109 Sánchez, Ken 1
140 Norman, Laura Executive 109 Sánchez, Ken 1
148 Hamilton, James Production 109 Sánchez, Ken 1
273 Welcker, Brian Sales 109 Sánchez, Ken 1
268 Jiang, Stephen Sales 273 Welcker, Brian 2
284 Alberts, Amy Sales 273 Welcker, Brian 2
*/
-- Test UDF for supervisor Miller, Dylan
SELECT EmpID = EmployeeID, EmpName=EmployeeName, Department,
MgrID=ManagerID, MgrName=ManagerName, OCLvl=OrgChartLevel
FROM dbo.fnOrgChartSubTree(158)
GO
/* Results
R & D = Research and Development
EmpID EmpName Department MgrID MgrName OCLvl
158 Miller, Dylan R & D 3 Tamburello… 0
79 Margheim, Diane R & D 158 Miller, Dylan 1
114 Matthew, Gigi R & D 158 Miller, Dylan 1
217 Raheem, Michael R & D 158 Miller, Dylan 1
*/
-- Test UDF for staff Margheim, Diane (leaf level on orgchart tree)
SELECT EmpID = EmployeeID, EmpName=EmployeeName, Department,
MgrID=ManagerID, MgrName=ManagerName, OCLvl=OrgChartLevel
FROM dbo.fnOrgChartSubTree(79)
GO
/* Results
EmpID EmpName Department MgrID MgrName OCLvl
79 Margheim, Diane Research and... 158 Miller, Dylan 0
*/
-- Get top-level executives - Level 1 when root is CEO
-- SQL select into create temporary table - SQL cross apply
SELECT ExecName=oc.EmployeeName, oc.EmployeeID, oc.Department
INTO #EXECS
FROM Employee e
CROSS APPLY dbo.fnOrgChartSubTree(e.EmployeeID) AS oc
WHERE e.EmployeeID = 109
and oc.OrgChartLevel=1;
SELECT * FROM #EXECS
GO
/* Results
ExecName EmployeeID Department
Bradley, David 6 Marketing
Duffy, Terri 12 Engineering
Trenary, Jean 42 Information Services
Norman, Laura 140 Executive
Hamilton, James 148 Production
Welcker, Brian 273 Sales
*/
-- Get orgchart by executives
-- Note: AdventureWorks database does not have Department Manager info
-- SQL cross apply - SQL IN operator
SELECT Executive=e.StaffName, e.Title, oc.Department,
Staff=oc.EmployeeName, Supervisor = oc.ManagerName
FROM Employee e
CROSS APPLY dbo.fnOrgChartSubTree(e.EmployeeID) AS oc
WHERE e.EmployeeID IN (Select EmployeeID FROM #EXECS)
ORDER by Executive, Department, Supervisor, Staff
GO
/* 289 rows CEO Ken Sanchez not included - Partial results
VPS = Vice President of Sales
WB = Welcker, Brian
Dept = Department
Executive Title Dept Staff Supervisor
WB VPS Sales Tsoflias, Lynn Abbas, Syed
WB VPS Sales Pak, Jae Alberts, Amy
WB VPS Sales Valdez, Rachel Alberts, Amy
WB VPS Sales Varkey C.., Ranjit Alberts, Amy
WB VPS Sales Ansman-W.., Pamela Jiang, Stephen
WB VPS Sales Blythe, Michael Jiang, Stephen
WB VPS Sales Campbell, David Jiang, Stephen
WB VPS Sales Carson, Jillian Jiang, Stephen
WB VPS Sales Ito, Shu Jiang, Stephen
WB VPS Sales Mensa-A.., Tete Jiang, Stephen
WB VPS Sales Mitchell, Linda Jiang, Stephen
WB VPS Sales Reiter, Tsvi Jiang, Stephen
WB VPS Sales Saraiva, José Jiang, Stephen
WB VPS Sales Vargas, Garrett Jiang, Stephen
WB VPS Sales Welcker, Brian Sánchez, Ken
WB VPS Sales Abbas, Syed Welcker, Brian
WB VPS Sales Alberts, Amy Welcker, Brian
WB VPS Sales Jiang, Stephen Welcker, Brian
*/
-- Cleanup
DROP TABLE tempdb.dbo.Employee
DROP TABLE #EXECS
GO
-- SQL create function
USE AdventureWorks;
GO
CREATE FUNCTION dbo.fnGetTotalSalesByCustomer(@CustID int)
RETURNS TABLE
AS
RETURN
SELECT Store = s.Name,
TotalSales = '$'+convert(varchar,TotalSales,1)
FROM
(
SELECT CustomerID = @CustID, TotalSales=sum(SubTotal)
FROM Sales.SalesOrderHeader
WHERE CustomerID =@CustID
) soh
INNER JOIN Sales.Store s
ON soh.CustomerID = s.CustomerID
GO
-- SQL cross apply use - SQL Server cross apply
SELECT Store, TotalSales
FROM Sales.Customer AS c
CROSS APPLY dbo.fnGetTotalSalesByCustomer (c.CustomerID) tsc
WHERE c.CustomerID < 100
ORDER BY Store
GO
/* Partial results
Store TotalSales
A Bike Store $102,351.80
Advanced Bike Components $433,942.38
Aerobic Exercise Company $3,301.21
Associated Bikes $9,384.45
Bicycle Exporters $37,684.82
Bicycle Warehouse Inc. $7,959.01
Bike World $112,601.32
*/
In the following example we calculate financial
statistics using Aggregate Functions for each dealer which sells
AdventureWorks mountain bikes and associated products. Here is the listing:
USE tempdb
GO
-- drop FUNCTION dbo.fnOrderFingerprint
CREATE FUNCTION dbo.fnOrderFingerprint
(@CustomerID AS INT)
RETURNS TABLE
AS
RETURN
SELECT Label = 'Maximum $',
TotalDue = max(TotalDue)
FROM AdventureWorks.Sales.SalesOrderHeader
WHERE CustomerID = @CustomerID
UNION
SELECT Label = 'Average $',
TotalDue = avg(TotalDue)
FROM AdventureWorks.Sales.SalesOrderHeader
WHERE CustomerID = @CustomerID
UNION
SELECT Label = 'Minimum $',
TotalDue = min(TotalDue)
FROM AdventureWorks.Sales.SalesOrderHeader
WHERE CustomerID = @CustomerID
UNION
SELECT Label = 'Order Count',
TotalDue = count(TotalDue)
FROM AdventureWorks.Sales.SalesOrderHeader
WHERE CustomerID = @CustomerID
UNION
SELECT Label = 'Standard Deviation $',
TotalDue = stdev(TotalDue)
FROM AdventureWorks.Sales.SalesOrderHeader
WHERE CustomerID = @CustomerID
GO
SELECT Customer = S.Name,
F.Label,
[Total Due] = left(convert(VARCHAR,convert(MONEY,F.TotalDue),1),
len(convert(VARCHAR,convert(MONEY,F.TotalDue),1)) - 3)
FROM AdventureWorks.Sales.Store AS S
JOIN AdventureWorks.Sales.Customer AS C
ON S.CustomerID = C.CustomerID
CROSS APPLY tempdb.dbo.fnOrderFingerprint(C.CustomerID) AS F
ORDER BY Customer ASC,
Label DESC
GO
This is the partial report:
Customer | Label | TotalDue |
Excellent Riding Supplies | Standard Deviation $ | 34,246 |
Excellent Riding Supplies | Order Count | 12 |
Excellent Riding Supplies | Minimum $ | 34,102 |
Excellent Riding Supplies | Maximum $ | 150,167 |
Excellent Riding Supplies | Average $ | 94,562 |
Exceptional Cycle Services | Standard Deviation $ | 3,978 |
Exceptional Cycle Services | Order Count | 4 |
Exceptional Cycle Services | Minimum $ | 4,012 |
Exceptional Cycle Services | Maximum $ | 13,016 |
Exceptional Cycle Services | Average $ | 7,913 |
Exchange Parts Inc. | Standard Deviation $ | 3,965 |
Exchange Parts Inc. | Order Count | 4 |
Exchange Parts Inc. | Minimum $ | 34,610 |
Exchange Parts Inc. | Maximum $ | 43,277 |
Exchange Parts Inc. | Average $ | 38,982 |
Exclusive Bicycle Mart | Standard Deviation $ | 576 |
Exclusive Bicycle Mart | Order Count | 6 |
Exclusive Bicycle Mart | Minimum $ | 429 |
Exclusive Bicycle Mart | Maximum $ | 1,943 |
Exclusive Bicycle Mart | Average $ | 1,035 |
Source : sqlusa
No comments:
Post a Comment