Cross-Tab Query for SQL Server
Back to TutorialsA common issue I run into is the need to display data in a chart that is to perform a Cross-Tab query. Lets take for example the North Wind sample database in Microsoft DB products and assume that we need to create a report that will list the number qty of products sold for each product category during each month of the year. The data to gather this information is stored in several tables.
Northwind Database Tables Used
Categories
|
Products
|
Orders
|
Order Details
|
So to get a list of Qty Sold for each category for each month and year the following query will gather this for us.
SELECT CategoryName, month(OrderDate) as Month ,
year(OrderDate) as Year, sum(quantity) as QTY
FROM orders inner join
[order details] od on od.orderID = orders.orderid inner join
products prod on prod.productID = od.ProductID inner join
categories cat on cat.CategoryID = prod.CategoryID
GROUP BY CategoryName, year(orderdate), month(orderdate)
This gives us a result similar to this:
| CategoryName | Month | Year | Qty |
|---|---|---|---|
| Beverages | 7 | 1996 | 272 |
| Beverages | 8 | 1996 | 347 |
| Beverages | 9 | 1996 | 285 |
| Beverages | 10 | 1996 | 301 |
| Beverages | 11 | 1996 | 332 |
| Beverages | 12 | 1996 | 305 |
| Condiments | 7 | 1996 | 139 |
| Condiments | 8 | 1996 | 154 |
| Condiments | 9 | 1996 | 110 |
| Condiments | 10 | 1996 | 183 |
| Condiments | 11 | 1996 | 294 |
| Condiments | 12 | 1996 | 82 |
This table gives us the raw data that we are looking for, but often is is easier to view the data in this manner
| Category | Jul 1996 | Aug 1996 | Sep 1996 | Oct 1996 | Nov 1996 | Dec 1996 |
|---|---|---|---|---|---|---|
| Beverages |
272 |
347 | 285 | 301 | 332 | 305 |
| Condiments | 139 | 154 | 110 | 183 | 294 | 82 |
This version shows the same data, but is much easier to read.
So how do we convert from the first version to the second?
The method described here is called The Rozenshtein Method after the mathmetician who created it. By using this query, we get almost exactly what we are after.
SELECT CategoryName, year(orders.orderdate) as Year,
SUM(Quantity*(1-ABS(SIGN(DatePart(mm,OrderDate)-1)))) AS Jan,
SUM(Quantity*(1-ABS(SIGN(DatePart(mm,OrderDate)-2)))) AS Feb,
SUM(Quantity*(1-ABS(SIGN(DatePart(mm,OrderDate)-3)))) AS Mar,
SUM(Quantity*(1-ABS(SIGN(DatePart(mm,OrderDate)-4)))) AS Apr,
SUM(Quantity*(1-ABS(SIGN(DatePart(mm,OrderDate)-5)))) AS May,
SUM(Quantity*(1-ABS(SIGN(DatePart(mm,OrderDate)-6)))) AS Jun,
SUM(Quantity*(1-ABS(SIGN(DatePart(mm,OrderDate)-7)))) AS Jul,
SUM(Quantity*(1-ABS(SIGN(DatePart(mm,OrderDate)-8)))) AS Aug,
SUM(Quantity*(1-ABS(SIGN(DatePart(mm,OrderDate)-9)))) AS Sep,
SUM(Quantity*(1-ABS(SIGN(DatePart(mm,OrderDate)-10)))) AS Oct,
SUM(Quantity*(1-ABS(SIGN(DatePart(mm,OrderDate)-11)))) AS Nov,
SUM(Quantity*(1-ABS(SIGN(DatePart(mm,OrderDate)-12)))) AS Dec
FROM Orders INNER JOIN
[Order Details] od on od.OrderID = Orders.OrderID INNER JOIN
Products on products.productID = od.ProductID INNER JOIN
Categories on categories.CategoryID = products.CategoryID
GROUP BY categories.CategoryName, year(orders.orderdate)
ORDER BY year(orders.orderdate),Categories.CategoryName
This gives us results like this.
| Category Name | Year | Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Beverages | 1996 | 0 | 0 | 0 | 0 | 0 | 0 | 272 | 347 | 285 | 301 | 332 | 305 |
| Condiments | 1996 | 0 | 0 | 0 | 0 | 0 | 0 | 139 | 154 | 110 | 183 | 394 | 82 |
| Confections | 1996 | 0 | 0 | 0 | 0 | 0 | 0 | 245 | 170 | 248 | 187 | 267 | 240 |
| Beverages | 1997 | 330 | 220 | 471 | 268 | 322 | 313 | 327 | 334 | 174 | 491 | 248 | 498 |
| Condiments | 1997 | 271 | 393 | 150 | 259 | 300 | 95 | 300 | 206 | 222 | 270 | 172 | 257 |
| Confections | 1997 | 623 | 425 | 265 | 394 | 291 | 107 | 210 | 335 | 371 | 390 | 255 | 471 |
So how does this work?
The key is in the following line (and the ones like it).
SUM(Quantity*(1-ABS(SIGN(DatePart(mm,OrderDate)-7)))) AS Jul
What exactly is this line doing?
Lets start in the middle and work out from there.
(1-ABS(SIGN(DatePart(mm,OrderDate)-7)))
-
DatePart(mm,OrderDate) gives us the month number of the order date, so an order in January would return a 1, February a 2 and so on.
-
DatePart(mm,OrderDate)-7 simply subtracts seven from the month number notice that this is changed the the same as the month number for each line.
-
Sign() is a sql function that returns 1 for positive numbers, 0 for zero and -1 for negative numbers. So looking at it in this code, if the order date is in July then the value returned here is:
DatePart(mm,OrderDate) will equal 7, then we will be looking at SIGN(7-7) or SIGN(0)which equals 0.
Now lets say the order was in March.
DatePart(mm,OrderDate) will equal 3. This will give us SIGN(3-7) or SIGN(-4) which equals -1
Lastly we’ll look at a October order.
DatePart(mm,OrderDate) will equal 10. This will give us SIGN(10-7) or SIGN(3) which equals 1 -
The ABS function returns the absolue value of a number,
in this case the value returned by the SIGN function.
Absolute value in simple terms means that we drop the sign on a numeric value
so ABS(15) = 15 and ABS(-15) = 15. ABS(0) will equal 0.
Since the SIGN fuction is returning 1, 0 or -1 this means that the returned
value here will be 0 if the month of the order is the same as the line being calculated
or 1 if any other month.
- Subtracting the value returned by the ABS function from 1 will give us the opposite value of the value returned by the ABS function. So if the order were in July, the ABS function will return a 0. 1–0=1 so the value at this point is 1. If the order were in any other month the the ABS function will return a 1. 1-1=0 so the value would be 0.
So the above section of code simply returns a 1 if the order is from the month in question or a 0 if it is not. Now we multiply the Qty by that value which will result in the Qty if the order is in the specified month or 0 if it is not.
Sum() is a sql function to total the values of the given field(s) for all records. So the Sum() function here will total the Quantity field for every record, however since the Quantity field is multiplied by 1 if it is for the correct month, and 0 if not then the total will only be the sum of the quantities for the month in question.