20th Apr 2023 10 minutes read CTE vs. Subquery in SQL: What’s the Difference? Jill Thornhill subqueries Common Table Expressions Table of Contents What Is a Subquery? And What Are CTEs? What Are the Differences Between CTEs and Subqueries? Subqueries Only: Using Subqueries in WHERE Subqueries Only: Using Subqueries in UPDATE CTEs Make a Complex Query More Readable What Are Recursive CTEs? CTE vs. Subquery What are Common Table Expressions (CTEs)? Are they the same as subqueries? When would you need to use CTEs? This article looks at the similarities and differences between CTE vs subquery. When I introduce a student to Common Table Expressions, their first reaction is “That’s just a subquery! Why do I need to learn that?”. Let’s answer this question by looking at what you can do with an SQL subquery and what extra advantages there are in using a CTE. If you decide after reading the article that you would like to learn how to use SQL CTEs, LearnSQL.com offers a comprehensive Common Table Expressions course that gives in-depth coverage of the topic. Beginning with simple examples, you'll work through to difficult concepts like processing trees and graphs. Each topic has step-by-step explanations and examples. With 114 interactive exercises, you'll get plenty of practice. You'll probably need about 18 hours to complete the course. What Is a Subquery? A subquery is a query nested inside the main query; this is best explained by looking at an example. All examples in this article use SQL Server; the syntax may be slightly different in other SQL dialects. Suppose management wants to offer a discount to all customers whose annual purchases were higher than the average for all customers. The query to extract a list of these customers could look like this: SELECT account_no, name FROM customers WHERE annual_purchases > (SELECT AVG(annual_purchases) FROM customers); The main query lists the customers, and the subquery calculates the average annual purchases for all customers. The subquery doesn't need a name (except when you're working in PostgreSQL). If you're not familiar with subqueries, you may like to have a look at LearnSQL.com's SQL Basics course, which has a section devoted to subqueries. You can read more about subqueries in our Beginner’s Guide to the SQL Subquery. How to Practice SQL Subqueries gives you some ideas on how to put these concepts into practice. And What Are CTEs? Common Table Expressions are named result sets that are defined at the front of a query and can be accessed by the query just as if they were tables. Suppose we wanted to compare employees' salaries to the average salary for their role. The query may look like this: WITH avg_salary AS ( SELECT role, avg(salary) AS average FROM employee GROUP BY role ) SELECT employee.role, name, salary, avg_salary FROM employee JOIN avg_salary ON avg_salary.role = employee.role ORDER BY role, name CTEs are introduced by the keyword WITH, and the results are stored in a named temporary table. In this example, the results of the CTE are stored in the table avg_salary, and this is used by the main query to pick up the average salary for each role. In fact, simple CTEs like this one could equally well be written as subqueries. The article "What Is a CTE?" explains CTEs in more detail. What Are the Differences Between CTEs and Subqueries? I'll start by briefly listing the major differences, then I'll look at some of them in more detail. CTEs are defined at the front of the query, whereas subqueries are defined inline. CTEs must always be named. Only PostgreSQL insists that subqueries must have a name. CTEs can be used recursively. I'll explain this later in the article. CTEs are much more readable than subqueries when you're writing a complex report. A CTE can be used many times within a query, whereas a subquery can only be used once. This can make the query definition much shorter, but it won't necessarily result in improved performance. Subqueries can be used in a WHERE clause in conjunction with the keywords IN or EXISTS, but you can't do this with CTEs. Subqueries can be used to pick up a single piece of data from one table in order to update a value in another table. Some functionality is limited to subqueries only. I’ll give two examples where subqueries cannot be replaced with CTEs. In the first example, the subquery forms part of the WHERE clause. In the second, it’s used to pick up a single value from one table in order to update another. As I mentioned above, SQL doesn’t allow you to use CTEs for these tasks. Subqueries Only: Using Subqueries in WHERE In the first example, a bank holds details of all the transactions for the day in a table called daily_trans. The data in this table includes an account number, a transaction code, and an amount. The database also has a table called transaction_types, and its columns include the transaction code and an indicator called debit_credit, which is set to 1 for transaction types that credit the customer's account, and 2 for those that debit the account. If the bank wants a list of all credit transactions for the day, the query could look like this: SELECT account_no, tran_code, amount FROM daily_trans WHERE tran_code IN (SELECT tran_code FROM transaction_types WHERE debit_credit = 1); This type of query can only be written using a subquery. You couldn’t replace the subquery with a CTE here. Subqueries Only: Using Subqueries in UPDATE In the second example, the same bank has a table called customer, whose columns include an account number, a customer name, and the employee number of their designated customer support person. The bank has done some reshuffling of responsibilities and wants to re-assign the support person for some customers. To do this, they've created a table called reassignments that holds the old support employee's ID number and the employee number of the support person who will take over their responsibilities. To carry out the reassignments, we could write a query like this: UPDATE customer SET support_person = (SELECT new_employee FROM reassignments WHERE old_employee = customer.support_person); CTEs Make a Complex Query More Readable To illustrate how CTEs can make things easier to understand, let's take a query that uses multiple subqueries and recode it using CTEs. Suppose we have a store that sells three types of product: books, music, and videos. The manager wants to know how much each customer has purchased in each category. The report may look something like this: CustomerTotalBooksMusicVideos M. Mouse150604050 F. Flintstone90102060 Here’s a query that uses subqueries to produce the report: SELECT customer, sum(purchases) AS Total, total_books AS Books, total_music AS Music, total_videos AS Videos FROM sales JOIN (SELECT account_no, sum(purchases) AS total_books FROM sales WHERE product_type = 'Books' GROUP BY account_no) books ON books.account_no = sales.account_no JOIN (SELECT account_no, sum(purchases) AS total_music FROM sales WHERE product_type = 'Music' GROUP BY account_no) music ON music.account_no = sales.account_no JOIN (SELECT account_no, sum(purchases) AS total_videos FROM sales WHERE product_type = 'Videos' GROUP BY account_no) videos ON videos.account_no = sales.account_no GROUP BY customer ORDER BY customer As you can see, it's quite complicated. And it’s hard to follow if someone else needs to make changes to it later. Now let's see what this query might look like if we rewrite it using Common Table Expressions: WITH books AS ( SELECT customer, sum(purchases) AS total_books FROM sales WHERE product_type = 'Books' GROUP BY customer ), music AS ( SELECT customer, sum(purchases) AS total_music FROM sales WHERE product_type = 'Music' GROUP BY customer ), videos as ( SELECT customer, sum(purchases) AS total_videos FROM sales WHERE product_type = 'Videos' GROUP BY customer ) SELECT customer, sum(purchases) AS Total, total_books AS Books, total_music AS Music, total_videos AS Videos FROM sales JOIN books ON books.customer = sales.customer JOIN music ON music.customer = sales.customer JOIN videos ON videos.customer = sales.customer GROUP BY customer ORDER BY customer Most people will find this second version much easier to understand, although both queries produce exactly the same results. What Are Recursive CTEs? I said earlier that only CTEs can be recursive. So what's a recursive query? Recursive queries allow you to navigate hierarchical data, and produce reports suitable for tree- and graph-type data. Examples of hierarchical data include: In an organization, an employee may report to a sub-manager; the sub-manager reports to a manager, and the manager reports to the b In manufacturing, a product may be made of several components. Each component could also be made of many sub-components, and the sub-components could be made from various raw materials. Let's look at an example. An ice cream parlour has several items on the menu. Each menu item may be made from several ingredients: a banana split is made up of bananas, chocolate sauce, and ice cream. But the chocolate sauce also has several ingredients. These may include cocoa powder, sugar and some other things. The owner wants a complete list of each menu item followed by all its ingredients. Part of the list may look like this: ItemIngredient Menu150 Menu > Banana SplitBanana Split Menu > Banana Split > BananaBanana Menu > Banana Split > Chocolate SauceChocolate Sauce Menu > Banana Split > Chocolate Sauce > CocoaCocoa Menu > Banana Split > Chocolate Sauce > MargarineMargarine Menu > Banana Split > Chocolate Sauce > SugarSugar Menu > Banana Split > Ice cream - AmericanIce cream - American Menu > Banana Split > Ice cream - American > CreamCream Menu > Banana Split > Ice cream - American > MilkMilk Menu > Banana Split > Ice cream - American > SugarSugar Menu > Banana Split > Ice cream - American > Vanilla ExtractVanilla Extract Menu > Choc Nut SundaeChoc Nut Sundae Menu > Choc Nut Sundae > Chocolate SauceChocolate Sauce Menu > Choc Nut Sundae > Chocolate Sauce > CocoaCocoa Menu > Choc Nut Sundae > Chocolate Sauce > MargarineMargarine Menu > Choc Nut Sundae > Chocolate Sauce > SugarSugar Menu > Choc Nut Sundae > Ice cream - RichIce cream - Rich Menu > Choc Nut Sundae > Ice cream - Rich > CreamCream Menu > Choc Nut Sundae > Ice cream - Rich > EggEgg Menu > Choc Nut Sundae > Ice cream - Rich > SugarSugar Menu > Choc Nut Sundae > Ice cream - Rich > Vanilla ExtractVanilla Extract Menu > Choc Nut Sundae > Mixed NutsMixed Nuts In the database, we have two tables: The Item table holds a list of each item in the menu and each ingredient. The Bill_of_materials table holds links between each item and its ingredients. The Items table holds this information: idDescriptionunitprice 15MenuNULLNULL 14EggEach0.1 13BananaEach0.2 12Banana SplitEach2 11MargarineKg4 10CocoaKg10 9Chocolate SauceLitre8 8Mixed NutsKg2 7Choc Nut SundaeEach1.5 6Ice Cream - RichLitre6 5SugarKg2 4Vanilla ExtractBottle1 3MilkLitre1.5 2CreamLitre4 1Ice Cream - AmericanLitre5 Here are some examples of entries in the Bill_of_materials table. The column item_id holds a link to the parent item in the Items table, whereas component_id holds a link to one of its ingredients. The first entry, therefore, shows that Item 10: Cocoa is an ingredient of Item 9: Chocolate Sauce. iditem_idcomponent_idquantity 19100.25 29110.25 3950.25 4120.5 Here’s the recursive query used to traverse this information. The query was written in SQL Server; other dialects would be slightly different. WITH menu_ingredients (id, path, description, item_id) AS ( SELECT CAST (id AS bigint), CAST (description as varchar (255)), CAST ('' AS varchar(40)), CAST (id AS bigint) FROM items WHERE description = 'Menu' UNION ALL SELECT CAST (bom.component_id AS bigint), CAST (m.path + ' > ' + i.description AS varchar(255)), i.description, CAST (bom.item_id AS bigint) FROM menu_ingredients m, bill_of_materials bom JOIN items i ON i.id = bom.component_id WHERE bom.item_id = m.id ) SELECT path, description FROM menu_ingredients ORDER BY path A full explanation of what’s going on here is outside the scope of this article, but I’ll quickly explain the basics. Briefly, when a row is added to the result set in a recursive query, it can 'see' the previous row and use it to pick up a piece of information that can be used to find the next row. This query begins by picking up the top level entry in the bill of materials: the menu itself. From there, it can cycle through all its 'child' rows – the ingredients it's made from. And each ingredient can pick up its own child rows, if it has any. For a more detailed explanation of recursion, check out What Is a Recursive CTE in SQL?. And as I mentioned earlier, recursive queries are fully covered in LearnSQL.com's Common Table Expressions course. CTE vs. Subquery In summary, choose a CTE when: You want to make a complex query more readable. You need to use a recursive query. Choose a subquery when: You're using the WHERE clause keywords IN or EXISTS to pick up the selection criteria from another table. You want to select a single piece of data from another table as the new value for a field in an UPDATE statement. In all other circumstances, it's your choice: you can use whichever you feel most comfortable with. If you want more examples of where to use Subquery and where to use CTE - check out this tutorial. And if you need some practice with either CTEs or subqueries, you might like to try the SQL Practice Set, which has hundreds of interactive exercises to help you consolidate your skills. Happy learning! Tags: subqueries Common Table Expressions