Practice Topics
- Common Table Expressions (CTEs)
- Top N
- Final Comprehensive Query Practice
Common Table Expressions (CTEs)
Overview
In this Week 3 post, you were introduced to subqueries
. Subqueries
are really ugly when included in a complex query. They are hard to read, especially when other things are going on around them.
The good news is that common table expressions (CTEs)
exist. They are exactly the same thing as a subquery
, but you just define them at the top of a query using a WITH
statement. Once defined, you can treat them like a table throughout the remainder of your query. Multiple CTEs
are separated with a comma. This has multiple benefits:
- Easy to read: When they are defined at the top of a query, they are much easier to read since they aren’t intermingled with everything else.
- Unlimited CTEs allowed: You aren’t restricted on the number of
CTEs
that you can use in a single query. This is amazing in longer queries because they can be chunked up in bite-sized segments. - Sequential processing:
CTEs
are processed from the top down. This means that you can actually reference oneCTE
in anotherCTE
as you move through the query. - Usable everywhere: A
CTE
is usable everywhere later in the query (including inSELECT
,FROM
,WHERE
, and of course in otherCTEs
). - Treat them like a table: As mentioned earlier, once you have defined a
CTE
, it acts just like a table. Instead of listingDatabaseName.SchemaName.TableName
in theFROM
statement, list the name assigned to aCTE
later in the query where you want to reference it.
Example: Using the AdventureWorks2019.Sales.SalesOrderDetail
table, calculate the % of Total Sales
for each ProductID
per SalesOrderID
. Total sales can be determined by using aggregation within a CTE
. In the output, include the Name
from the AdventureWorks2019.Production.Product
table and give it an alias name of ProductName
. You haven’t yet learned how to do math in SQL. To determine the % of Total Sales
, you can divide the LineTotal
by the TotalSales
that you will derive in your CTE
. Division works exactly the same as it does in Excel; use /
.
WITH TotalSalesCTE AS ( SELECT SalesOrderID, SUM(LineTotal) AS TotalSales FROM AdventureWorks2019.Sales.SalesOrderDetail GROUP BY SalesOrderID ) SELECT a.SalesOrderID, a.ProductID, b.Name AS ProductName, c.TotalSales, a.LineTotal / c.TotalSales AS "Percent of Total Sales" FROM AdventureWorks2019.Sales.SalesOrderDetail a INNER JOIN AdventureWorks2019.Production.Product b ON a.ProductID = b.ProductID INNER JOIN TotalSalesCTE c ON a.SalesOrderID = c.SalesOrderID;
Example: Now, add on to the prior example and include a metric that shows the OrderQty
and % of Total Quantity
for each ProductID
per SalesOrderID
using the AdventureWorks2019.Sales.SalesOrderDetail
table.
When you divide a whole number by a whole number, the result is a whole number (either 1 or 0 depending on rounding). You avoid this by converting either the numerator or denominator to a decimal. I typically handle this by converting the denominator to a float
. A float is the same thing as a decimal. This will ensure you get a decimal as intended. For example: a.OrderQty / cast(d.TotalQty as float)
WITH TotalSalesCTE AS ( SELECT SalesOrderID, SUM(LineTotal) AS TotalSales FROM AdventureWorks2019.Sales.SalesOrderDetail GROUP BY SalesOrderID ), TotalQtyCTE AS ( SELECT SalesOrderID, SUM(OrderQty) AS TotalQty FROM AdventureWorks2019.Sales.SalesOrderDetail GROUP BY SalesOrderID ) SELECT a.SalesOrderID, a.ProductID, b.Name AS ProductName, c.TotalSales, a.LineTotal / c.TotalSales AS "Percent of Total Sales", d.TotalQty, a.OrderQty / cast(d.TotalQty as float) AS "Percent of Total Quantity" FROM AdventureWorks2019.Sales.SalesOrderDetail a INNER JOIN AdventureWorks2019.Production.Product b ON a.ProductID = b.ProductID INNER JOIN TotalSalesCTE c ON a.SalesOrderID = c.SalesOrderID INNER JOIN TotalQtyCTE d ON a.SalesOrderID = d.SalesOrderID;
Self Practice
- Write a query to identify all the names of employees by selecting
FirstName
andLastName
from theAdventureWorks2019.Person.Person
table whereBusinessEntityID
is in a common table expression (CTE) that selectsBusinessEntityID
from theAdventureWorks2019.HumanResources.Employee
table. - Write a CTE to calculate the total sales for each customer from the
AdventureWorks2019.Sales.SalesOrderHeader
table, then select the top 5 customers from your CTE based on total sales amount.
WITH EmployeeCTE AS ( SELECT BusinessEntityID FROM AdventureWorks2019.HumanResources.Employee ) SELECT p.FirstName, p.LastName FROM AdventureWorks2019.Person.Person p INNER JOIN EmployeeCTE ON p.BusinessEntityID = EmployeeCTE.BusinessEntityID;
WITH CustomerSales AS ( SELECT CustomerID, SUM(TotalDue) AS TotalSales FROM AdventureWorks2019.Sales.SalesOrderHeader GROUP BY CustomerID ) SELECT TOP 5 CustomerID, TotalSales FROM CustomerSales ORDER BY TotalSales DESC;
Top N
Overview
Sometimes, you need to display the Top N
records from a query result. For example, if you are asked to produce the top 10 customers by total sales, you are going to get thousands of records unless you specifically request the top 10 records. To return the top 10, you need to do two things:
- Add
TOP 10
right after theSELECT
:SELECT TOP 10
- Add an
ORDER BY
on the total sales descending:ORDER BY TotalSales DESC
Example: Identify the top 10 CustomerID
values from the AdventureWorks2019.Sales.SalesOrderHeader
table in order of TotalDue
descending.
SELECT TOP 10 a.CustomerID, SUM(a.TotalDue) AS TotalSales FROM AdventureWorks2019.Sales.SalesOrderHeader a GROUP BY a.CustomerID ORDER BY TotalSales DESC;
Self Practice
- Write a SQL query to retrieve the top 5 products with the highest cumulative (
LineTotal
) from theAdventureWorks2019.Sales.SalesOrderDetail
table. Include theProductID
, sum ofLineTotal
, andName
of each product from theAdventureWorks2019.Production.Product
table in the output.
SELECT TOP 5 p.ProductID, p.Name, SUM(sod.LineTotal) AS TotalSales FROM AdventureWorks2019.Sales.SalesOrderDetail sod INNER JOIN AdventureWorks2019.Production.Product p ON sod.ProductID = p.ProductID GROUP BY p.ProductID, p.Name ORDER BY TotalSales DESC;
Final Comprehensive Query Practice
- Write a query to find the top 5 products by
OrderQty
in theAdventureWorks2019.Sales.SalesOrderDetail
table, joined with theAdventureWorks2019.Production.Product
table to get the productName
. - Write a query to find all customers from the
AdventureWorks2019.Sales.Customer
table who did not make a purchase in 2013 in theAdventureWorks2019.Sales.SalesOrderHeader
table. Hint, use anegative join
. - Write a query to calculate the total sales for each
SalesPersonID
for the year 2014 from theAdventureWorks2019.Sales.SalesOrderHeader
table, including theirFirstName
andLastName
from theAdventureWorks2019.Person.Person
table. Use appropriate joins and date functions. - Write a query to list all
CustomerID
values who have placed orders from theAdventureWorks2019.Sales.SalesOrderHeader
table, in both 2013 and 2014. Use subqueries or joins as needed.
SELECT TOP 5 sod.ProductID, p.Name, SUM(sod.OrderQty) AS TotalOrderQty FROM AdventureWorks2019.Sales.SalesOrderDetail sod INNER JOIN AdventureWorks2019.Production.Product p ON sod.ProductID = p.ProductID GROUP BY sod.ProductID, p.Name ORDER BY TotalOrderQty DESC;
SELECT c.CustomerID, c.PersonID, c.StoreID, c.TerritoryID, c.AccountNumber FROM AdventureWorks2019.Sales.Customer c LEFT JOIN AdventureWorks2019.Sales.SalesOrderHeader soh ON c.CustomerID = soh.CustomerID AND YEAR(soh.OrderDate) = 2013 WHERE soh.SalesOrderID IS NULL;
SELECT soh.SalesPersonID AS SalesPersonID, p.FirstName, p.LastName, SUM(soh.TotalDue) AS TotalSales FROM AdventureWorks2019.Sales.SalesOrderHeader soh INNER JOIN AdventureWorks2019.Person.Person p ON soh.SalesPersonID = p.BusinessEntityID WHERE YEAR(soh.OrderDate) = 2014 GROUP BY soh.SalesPersonID, p.FirstName, p.LastName;
SELECT c.CustomerID FROM AdventureWorks2019.Sales.Customer c WHERE c.CustomerID IN ( SELECT soh2013.CustomerID FROM AdventureWorks2019.Sales.SalesOrderHeader soh2013 WHERE YEAR(soh2013.OrderDate) = 2013 ) AND c.CustomerID IN ( SELECT soh2014.CustomerID FROM AdventureWorks2019.Sales.SalesOrderHeader soh2014 WHERE YEAR(soh2014.OrderDate) = 2014 );
Conclusion
As we wrap up this week’s SQL homework assignments, we’ve delved into very advanced aspects of SQL, including common table expressions (CTEs)
, top N
, and tying all four weeks together. Once you are familiar with these concepts, you will have a very strong foundation of knowledge. All crazy queries are just variations of these smaller topics so don’t get overwhelmed.
Remember, consistent practice is the key to mastering SQL. If you find yourself needing extra help or wanting to deepen your understanding, don’t hesitate to sign up for our private or group training sessions. These sessions offer personalized guidance and a collaborative learning environment to further enhance your SQL skills. Stay tuned for more articles, and keep pushing yourself to learn more!