“Don’t drown yourself with details. Look at the whole.”
—MARSHAL FERDINAND FOCH COMMANDER-IN-CHIEF, ALLIED ARMIES IN FRANCE
Chapter 12, “Simple Totals,” explained how to use the aggregate functions (COUNT, MIN, MAX, AVG, and SUM) to ask SQL to calculate a value across all the rows in the table defined in your FROM and WHERE clauses. I pointed out, however, that after you include any value expression that contains an aggregate function in your SELECT clause, all your value expressions must either be a literal constant or contain an aggregate function. This characteristic is useful if you want to see only one row of totals across a result set, but what if you want to see some subtotals? In this chapter, I’ll show you how to ask for subtotals by grouping your data. I’ll show you how to do more complex subtotals in Chapter 21, “Performing Complex Calculations on Groups.”
When you’re working in the Sales Orders database, finding out the number of orders (COUNT), the total sales (SUM), the average of sales (AVG), the smallest order (MIN), or the largest order (MAX) is useful, indeed. And if you want to calculate any of these values by customer, order date, or product, you can add a filter (WHERE) to fetch the rows for one particular customer or product. But what if you want to see subtotals for all customers, displaying the customer name along with the subtotals? To do that, you need to ask your database system to group the rows.
Likewise, in the Entertainment Agency database, it’s easy to find out the number of contracts, the total contract price, the smallest contract price, or the largest contract price for all contracts. You can even filter the rows so that you see these calculations for one particular entertainer, one particular customer, or across a specific range of dates. Again, if you want to see one total row for each customer or entertainer, you must group the rows.
Are you starting to get the idea? When you ask your database system to group rows on column values or expressions, it forms subsets of rows based on matching values. You can then ask your database to calculate aggregate values on each group. Let’s look at a simple example from the Entertainment Agency database. First, I need to build a query that fetches the columns of interest—entertainer name and contract price. Here’s the SQL:
|
SELECT Entertainers.EntStageName, Engagements.ContractPrice FROM Entertainers INNER JOIN Engagements ON Entertainers.EntertainerID = Engagements.EntertainerID ORDER BY EntStageName |
The result looks like the following table. (In the sample database, I saved this request as CH13_Entertainers_And_ContractPrices.)
ContractPrice |
|
Carol Peacock Trio |
$140.00 |
Carol Peacock Trio |
$1,670.00 |
Carol Peacock Trio |
$770.00 |
Carol Peacock Trio |
$1,670.00 |
Carol Peacock Trio |
$1,670.00 |
Carol Peacock Trio |
$320.00 |
Carol Peacock Trio |
$1,400.00 |
Carol Peacock Trio |
$680.00 |
Carol Peacock Trio |
$410.00 |
Carol Peacock Trio |
$1,940.00 |
Carol Peacock Trio |
$410.00 |
Caroline Coie Cuartet |
$1,250.00 |
Caroline Coie Cuartet |
$2,450.00 |
Caroline Coie Cuartet |
$1,490.00 |
Caroline Coie Cuartet |
$1,370.00 |
<< more rows here >> |
You already know that you can count all the rows, or find the smallest, largest, sum, or average of the ContractPrice column—as long as you eliminate the EntStageName column. However, you can keep this column if you ask your database to group on it. If you ask to group on entertainer stage name, your database will form one group containing the first eleven rows (“Carol Peacock Trio”), a second group containing the next eleven rows (“Caroline Coie Cuartet”), and so on through the entire table. You can now ask for the COUNT of the rows or the SUM, MIN, MAX, or AVG of the ContractPrice column, and you will get one aggregate row per entertainment group. The result looks like the following table.
NumContracts |
TotPrice |
MinPrice |
MaxPrice |
AvgPrice |
|
Carol Peacock Trio |
11 |
$11,080.00 |
$140.00 |
$1,940.00 |
$1,007.27 |
Caroline Coie Cuartet |
11 |
$15,070.00 |
$290.00 |
$2,450.00 |
$1,370.00 |
Coldwater Cattle Company |
8 |
$14,875.00 |
$350.00 |
$3,800.00 |
$1,859.38 |
Country Feeling |
15 |
$34,080.00 |
$275.00 |
$14,105.00 |
$2,272.00 |
Jazz Persuasion |
7 |
$5,480.00 |
$500.00 |
$1,670.00 |
$782.86 |
Jim Glynn |
9 |
$3,030.00 |
$110.00 |
$770.00 |
$336.67 |
Julia Schnebly |
8 |
$4,345.00 |
$275.00 |
$875.00 |
$543.13 |
JV & the Deep Six |
10 |
$17,150.00 |
$950.00 |
$3,650.00 |
$1,715.00 |
Modern Dance |
10 |
$14,600.00 |
$650.00 |
$2,930.00 |
$1,460.00 |
Saturday Revue |
9 |
$11,550.00 |
$290.00 |
$2,930.00 |
$1,283.33 |
Susan McLain |
6 |
$2,670.00 |
$230.00 |
$800.00 |
$445.00 |
Topazz |
7 |
$6,620.00 |
$590.00 |
$1,550.00 |
$945.71 |
<< more rows here >> |
Looks interesting, doesn’t it? I bet you’d like to know how I did that! I’ll show you all the details in the following sections.
Note: Remember in the Introduction that I warned you that results from each database system
won’t necessarily match the sort order you see in examples in this book unless you
include an ORDER BY clause. Even when you include that specification, the system might
return results in columns not included in the ORDER BY clause in a different sequence
because of different optimization techniques.
If you’re running the examples in Microsoft SQL Server, simply selecting the rows from the view does not honor any ORDER BY clause specified in the view. You must open the design of the view and execute it from there for the ORDER BY clause to be honored.
As you discovered in Chapter 12, you can find out all sorts of interesting information by using aggregate functions. However, you might have noticed that all the examples I gave you applied the aggregate functions across all the rows returned by the FROM and WHERE clauses. You could filter the result set down to one group using the WHERE clause, but there was really no way to look at the results from multiple groups in one request. To accomplish this summarizing by group in a single request, I need to add one more major clause to your SQL vocabulary—GROUP BY.
Let’s take a close look at the GROUP BY clause. Figure 13-1 shows the basic diagram for a SELECT statement with GROUP BY added.
As you recall from earlier chapters, you define the tables that are the source of your data in the FROM clause. Your FROM clause can be as simple as a single table name or as complex as a JOIN of multiple tables. As discussed in Chapter 8, “INNER JOINs,” you can even embed an entire table subquery (a SELECT statement) as a table reference. Next, you can optionally provide a WHERE clause to include or exclude certain rows supplied by the FROM clause. I covered the WHERE clause in detail in Chapter 6, “Filtering Your Data.”
When you add a GROUP BY clause, you specify the columns in the logical table formed by the FROM and WHERE clauses that you want your database system to use as the definition for groups of rows. Rows that have the same values in the list of columns you specify will be gathered into a group. You can use the columns that you list in the GROUP BY clause in value expressions in your SELECT clause, and you can use any of the aggregate functions I discussed in the previous chapter to perform calculations across each group.
Note: When you use GROUP BY, you’ll often see the results returned by your database system
as though they are sorted by the columns you specified. This happens because some
optimizers first sort the data internally to make it faster to process your GROUP
BY. Keep in mind that if you want a specific sort order, you must also include an
ORDER BY clause.
Let’s apply the GROUP BY clause to see how you can calculate information about contract prices by entertainment group—the sample I tantalized you with earlier. Figure 13-2 shows the tables needed to solve this problem.
Note: Throughout this chapter, I use the “Request/Translation/Clean Up/SQL” technique introduced
in Chapter 4, “Creating a Simple Query.”
“Show me for each entertainment group the group name, the count of contracts for the group, the total price of all the contracts, the lowest contract price, the highest contract price, and the average price of all the contracts.”
(Hint: When you see a request that wants the count, total, smallest, largest, or average of values at a detail level [contracts] for each value at a higher level [entertainers], you are going to need to use aggregate functions and grouping in your request. Remember that for each entertainer there are most likely many contracts.)
Translation |
Select entertainer name, the count of contracts, the sum of the contract price, the lowest contract price, the highest contract price, and the average contract price from the entertainers table joined with the engagements table on entertainer ID, grouped by entertainer name |
Clean Up |
Select entertainer name, the count of (*) contracts, the sum of the (contract price), the lowest min(contract price), the highest max(contract price), and the average avg(contract price) from the entertainers table inner joined with the engagements table on entertainers.entertainer ID in the entertainers table matches = engagements.entertainer ID in the engagements table, grouped by entertainer name |
|
|
Note that I substituted MIN for “lowest,” MAX for “highest,” and AVG for “average,” as I showed you in the previous chapter. I also asked for COUNT(*) because I want to count all the engagement (contract) rows regardless of any Null values. Adding the GROUP BY clause is what gets me the aggregate calculations per entertainment group. It also allows me to include the entertainer name in the SELECT clause. (I saved this request as CH13_Aggregate_Contract_Info_By_Entertainer in the sample database.)
Do you suppose the above query returns a row for each entertainer? What about entertainers who have never been booked? If you remember what you learned in Chapter 9 about OUTER JOIN, you might be tempted to solve the problem like this:
|
|
One interesting point about all the aggregate functions is that they ignore rows that have a Null value. The above query will return a blank or Null value for TotPrice, MinPrice, MaxPrice, and AvgPrice for the one entertainer who has no engagements, but you’ll find that NumContracts is 1! How can that be? Well, this SQL asks for COUNT(*)—count any row returned. The OUTER JOIN returns exactly one row for the entertainer with no booking, so the count of 1 is correct. (I saved this request as CH13_Aggregate_Contract_Info_All_Entertainers_WRONG in the sample database.) However, if you remember from the previous chapter, you can also COUNT(value expression), and that tells your database system to add to the count only if it finds a non-Null value in the value expression or column name you specify. Let’s tweak the query one more time.
|
Because the EntertainerID column from the Engagements table for the one entertainer who has no bookings is Null, nothing gets counted. If you run this query, you should see the correct value 0 in NumContracts for the one entertainer who has no engagements. (I saved this request as CH13_Aggregate_Contract_Info_All_Entertainers in the sample database.)
What if you want (or need) to group on more than one value? Let’s look at this same problem, but from the perspective of customers rather than entertainers, and let’s assume you want to display in your result set both the customer’s last name and first name. Figure 13-3 shows the necessary tables.
“Show me for each customer the customer first and last names, the count of contracts for the customer, the total price of all the contracts, the lowest contract price, the highest contract price, and the average price of all the contracts.”
Select customer last name, customer first name, the count of contracts, the sum of the contract price, the lowest contract price, the highest contract price, and the average contract price from the customers table joined with the engagements table on customer ID, grouped by customer last name and customer first name |
|
Clean Up |
Select customer last name, customer first name, the count of (*) contracts, the sum of the (contract price), the lowest min(contract price), the highest max(contract price), and the average avg(contract price) from the customers table inner joined with the engagements table on customers.customer ID in the customers table matches = engagements.customer ID in the engagements table, grouped by customer last name, and customer first name |
|
|
The result looks like the following table. (In the Entertainment Agency sample database, I saved this request as CH13_Aggregate_ Contract_Info_By_Customer.)
CustLast Name |
CustFirst Name |
Num Contracts |
TotPrice |
MinPrice |
MaxPrice |
AvgPrice |
Berg |
Matt |
9 |
$13,170.00 |
$200.00 |
$2,675.00 |
$1,463.33 |
Brehm |
Peter |
7 |
$7,250.00 |
$290.00 |
$3,800.00 |
$1,035.71 |
Ehrlich |
Zachary |
13 |
$12,455.00 |
$230.00 |
$1,550.00 |
$958.08 |
Elizabeth |
8 |
$25,585.00 |
$410.00 |
$14,105.00 |
$3,198.13 |
|
Hartwig |
Doris |
8 |
$10,795.00 |
$140.00 |
$2,750.00 |
$1,349.38 |
Keyser |
Liz |
7 |
$4,685.00 |
$200.00 |
$1,490.00 |
$669.29 |
McCrae |
Dean |
11 |
$11,800.00 |
$290.00 |
$2,570.00 |
$1,072.73 |
Patterson |
Kerry |
7 |
$6,815.00 |
$110.00 |
$2,930.00 |
$973.57 |
<< more rows here >> |
Because it takes two columns to display the customer name, I had to include them both in the GROUP BY clause. Remember that if you want to include a column in the output that is not the result of an aggregate calculation, you must also include it in the GROUP BY clause. I did not include ContractPrice in the GROUP BY clause because that’s the column I’m using in many of the aggregate function expressions. If I had included ContractPrice, I would have gotten unique groups of customers and prices. MIN, MAX, and AVG will all return that grouped price. COUNT will be greater than one only if more than one contract with the same price exists for a given customer. If you think about it, though, grouping by customer and price and asking for a COUNT would be a good way to find customers who have multiple contracts at the same price.
Do you suppose this query includes customers who have no bookings? If you answered “No,” you’re correct! To fetch data for all customers regardless of whether they’ve booked an engagement, you must use an OUTER JOIN and be careful to COUNT one of the columns from the Engagements table. The solution is similar to the problem discussed earlier for entertainers and engagements.
Suppose you want to list the customer name as one output column, the full customer address as another output column, the last engagement date, and the sum of engagement contract prices. The customer name is in two columns: CustFirstName and CustLastName. The columns you need for a full address are CustStreetAddress, CustCity, CustState, and CustZipCode. Let’s see how you should construct the SQL for this request. (I saved this request as CH13_Customers_Last_Booking in the Entertainment Agency sample database.)
“Show me for each customer the customer full name, the customer full address, the latest contract date for the customer, and the total price of all the contracts.”
Translation |
Select customer last name and customer first name as CustomerFullName; street address, city, state, and ZIP Code as CustomerFullAddress; the latest contract start date; and the sum of the contract price from the customers table joined with the engagements table on customer ID, grouped by customer last name, customer first name, customer street address, customer city, customer state, and customer ZIP Code |
Clean Up |
Select customer last name and || ‘, ’ || customer first name as CustomerFullName, street address, || ‘, ‘ || city, || ‘, ‘ || state, and || ‘ ‘ || ZIP Code as CustomerFullAddress, the latest max(contract start date) as latest date, and the sum of the (contract price) as total contract price from the customers table inner joined with the engagements table on customers.customer ID in the customers table matches = engagements.customer ID in the engagements table grouped by customer last name, customer first name, customer street address, customer city, customer state, and customer ZIP Code |
|
|
Notice that I had to list every one of the columns that I used in an output expression that did not include an aggregate function. I used StartDate and ContractPrice in aggregate expressions, so I don’t need to list them in the GROUP BY clause. In fact, it doesn’t make sense to group on either StartDate or ContractPrice because I want to use these in an aggregate calculation across multiple customers. If, for example, I grouped on StartDate, MAX(StartDate) would return the grouping value, and the expression SUM(ContractPrice) would return only the sum of contract prices for a customer on any given date. You wouldn’t get the sum of more than one contract unless a customer had more than one contract for a given date—not likely.
In Chapter 11, “Subqueries,” I introduced the COUNT and MAX aggregate functions to show how to filter rows using an aggregate value fetched with a subquery. In Chapter 12 I showed how to use MIN, AVG, and SUM in a subquery filter as well. Let’s look at a request that requires both a subquery with an aggregate function and a GROUP BY clause in the subquery:
“Display the engagement contract whose price is greater than the sum of all contracts for any other customer.”
Translation |
Select customer first name, customer last name, engagement start date, and engagement contract price from the customers table joined with the engagements table on customer ID where the contract price is greater than the sum of all contract prices from the engagements table for customers other than the current customer, grouped by customer ID |
Clean Up |
Select customer first name, customer last name, engagement start date, and engagement contract price from the customers table inner joined with the engagements table on customers.customer ID in the customers table matches = engagements.customer ID in the engagements table where the contract price is greater than > ALL (select the sum of all (contract prices) from the engagements table as E2 for where E2.customers ID <> other than the current customers.customer ID, grouped by E2.customer ID) |
|
Let’s analyze what the subquery is doing. For each engagement that the query looks at in the JOIN of the Customers and Engagements tables, the subquery calculates the SUM of all contract prices for all other customers and groups them by customer ID. Because there are multiple customers in the database, the subquery will return multiple SUM values—one for each of the other customers. For this reason, I cannot ask for a simple greater than (>) comparison. I can, however, use the quantified greater than all (> ALL) comparison to check a set of values as you learned in Chapter 11. If you run this query in the sample Entertainment Agency database for this chapter (I saved it as CH13_Biggest_Big_Contract), you’ll find that one contract fits the bill, as shown here:
CustFirstName |
CustLastName |
StartDate |
ContractPrice |
Elizabeth |
Hallmark |
2018-01-22 |
$14,105.00 |
Did it occur to you that you can use a GROUP BY clause and not include any aggregate functions in your SELECT clause? Sure you can! When you do this, you get the same effect as using the DISTINCT keyword covered in Chapter 4. (See the “Eliminating Duplicate Rows” section in that chapter.)
Let’s look at a simple request that requires unique values and solve it using both techniques:
“Show me the unique city names from the customers table.”
Translation 1 |
Select the unique city names from the customers table |
Clean Up |
Select the unique distinct city names from the customers table |
|
|
Translation 2 |
Select city name from the customers table, grouped by city name |
Clean Up |
Select city name from the customers table, grouped by city name |
|
|
Remember that GROUP BY groups all the rows on the grouping column(s) you specify and returns one row per group. This is a slightly different way to get to the same result that you obtain with the DISTINCT keyword. Which one is better? I think that DISTINCT might be a clearer statement of what you want if all you want is unique rows, but you might find that your database system solves the problem faster when you use GROUP BY. In addition, GROUP BY lets you obtain more information about your data. Consider the following query:
|
|
With this query, you not only fetch the unique city names but also find out how many customers are in each city. Is that cool or what?
I already mentioned that adding a GROUP BY clause places certain restrictions on constructing your request. Let’s review those restrictions to make sure you don’t fall into common traps.
When you add a GROUP BY clause, you’re asking your database system to form unique groups of rows from those returned by the tables defined in your FROM clause and filtered by your WHERE clause. You can use as many aggregate expressions as you like in your SELECT clause, and these expressions can use any of the columns in the table defined by the FROM and WHERE clauses. As I pointed out in an earlier example, it probably does not make sense to reference a column in an aggregate expression and also include that column in your grouping specification.
If you choose to also include expressions that reference columns but do not include an aggregate function, you must list all columns you use this way in the GROUP BY clause. One of the most common mistakes is to assume that you can reference columns in nonaggregate expressions as long as the columns come from unique rows. For example, let’s look at an incorrect request that includes a primary key value—something that I know by definition is unique:
“Display the customer ID, customer full name, and the total of all engagement contract prices.”
Translation |
Select customer ID, customer first name, and customer last name as CustFullName, and the sum of contract prices as TotalPrice from the customers table joined with the engagements table on customer ID, grouped by customer ID |
Clean Up |
Select customer ID, customer first name and || ‘ ‘ || customer last name as CustFullName, and the sum of (contract price)s as TotalPrice from the customers table inner joined with the engagements table on customers.customer ID in the customers table matches = engagements.customer ID in the engagements table, grouped by customer ID |
|
|
I know that CustomerID is unique per customer. Grouping on CustomerID alone should be sufficient to fetch unique customer first and last name information within the groups formed by CustomerID. However, SQL is a language based on syntax, not semantics. In other words, SQL does not take into account any knowledge that could be implied by the design of your database tables—including whether columns are primary keys. SQL demands that your request be syntactically “pure” and translatable without any knowledge of the underlying table design. So, the above SQL statement will fail on a database system that is fully compliant with the SQL Standard because I’ve included columns in the SELECT clause that are not in an aggregate function and are also not in the GROUP BY clause (CustFirstName and CustLastName). (Surprisingly, a query constructed like the above does work in MySQL and PostgreSQL.) The correct SQL request is as follows:
|
|
This might seem like overkill, but it’s the correct way to do it!
Note: In some database systems, you must exactly duplicate the expressions you use in the SELECT clause in the GROUP BY clause. Oracle and Microsoft Office
Access are examples of systems that either support or require this. (Microsoft Office
Access lets you do it either way.) In my example, instead of listing the separate
columns, you would have to end the SQL with this:
GROUP BY Customers.CustomerID,
Customers.CustFirstName || ' ' ||
Customers.CustLastName
This isn’t compliant with the SQL Standard, but you might find that this is the only way you can get your request to work on your system.
I showed you earlier some correct examples of creating expressions that do not include aggregate functions. One of the most common mistakes is to attempt to group on the expression you create in the SELECT clause rather than on the individual columns. Remember that the GROUP BY clause must refer to columns created by the FROM and WHERE clauses. It cannot use an expression you create in your SELECT clause.
Let’s take another look at an example I solved earlier to show you what I mean, but this time, let’s make the mistake. (I’m skipping the Translation and Clean Up steps here because I covered them earlier.)
“Show me for each customer in the state of Washington the customer full name, the customer full address, the latest contract date for the customer, and the total price of all the contracts.”
|
|
Some database systems will let you get away with this, but it’s not correct. The CustomerFullName and CustomerFullAddress columns don’t exist until after your database system has evaluated the FROM, WHERE, and GROUP BY clauses. The GROUP BY clause won’t find these columns in the result created in the FROM and WHERE clauses, so on a database system that strictly adheres to the SQL Standard you’ll get a syntax error.
I showed you earlier one correct way to solve this: You must list all the columns you use in both the CustomerFullName and CustomerFullAddress expressions. Another way is to make the FROM clause generate the calculated columns by embedding a table subquery. Here’s what it looks like:
|
|
This works now because I’ve generated the CustomerFullName and CustomerFullAddress columns as output in the FROM clause. You have to admit, though, that this makes the query very complex. In truth, it’s better to just list all the individual columns you plan to use in nonaggregate expressions rather than try to generate the expressions as columns inside the FROM clause. I saved this last request as CH13_Customers_Total_Contract in the Entertainment Agency sample database.
At this point, you should have a fairly good understanding of how to ask for subtotals across groups using aggregate functions and the GROUP BY clause. The best way to give you an idea of the wide range of uses for GROUP BY is to list some problems you can solve with this new clause and then present a robust set of examples in the “Sample Statements” section:
“Show me each vendor and the average by vendor of the number of days to deliver products.”
“Display for each product the product name and the total sales.”
“List for each customer and order date the customer full name and the total cost of items ordered on each date.”
“Display each entertainment group ID, entertainment group member, and the amount of pay for each member based on the total contract price divided by the number of members in the group.”
“Show each agent name, the sum of the contract price for the engagements booked, and the agent’s total commission.”
“For completed classes, list by category and student the category name, the student name, and the student’s average grade in all classes taken in that category.”
“Display by category the category name and the count of classes offered.”
“List each staff member and the count of classes each is scheduled to teach.”
“Show me for each tournament and match the tournament ID, the tournament location, the match number, the name of each team, and the total of the handicap score for each team.”
“Display for each bowler the bowler name and the average of the bowler’s raw game scores.”
“Show me how many recipes exist for each class of ingredient.”
“If I want to cook all the recipes in my cookbook, how much of each ingredient must I have on hand?”
You now know the mechanics of constructing queries using a GROUP BY clause and have seen some of the types of requests you can answer. Let’s take a look at a set of samples, all of which request that the information be grouped. These examples come from each of the sample databases.
I’ve also included sample result sets that would be returned by these operations and placed them immediately after the SQL syntax line. The name that appears immediately above a result set is the name I gave each query in the sample data on the companion website for this book, www.informit.com/title/9780134858333. I stored each query in the appropriate sample database (as indicated within the example), and I prefixed the names of the queries relevant to this chapter with “CH13.” You can follow the instructions in the Introduction of this book to load the samples onto your computer and try them.
Note: Remember that all the column names and table names used in these examples are drawn
from the sample database structures shown in Appendix B, “Schema for the Sample Databases.” To simplify the process, I have combined the
Translation and Clean Up steps for all the examples.
These samples assume you have thoroughly studied and understood the concepts covered in previous chapters, especially the chapters on JOINs and subqueries.
“List for each customer and order date the customer full name and the total cost of items ordered on each date.”
Select customer first name and || ‘ ‘ || customer last name as CustFullName, order date, and the sum of (quoted price times * quantity ordered) as TotalCost from the customers table inner joined with the orders table on customers.customer ID in the customers table matches = orders.customer ID in the orders table, and then inner joined with the order details table on orders.order number in the orders table matches = order_details.order number in the order details table, grouped by customer first name, customer last name, and order date |
|
|
|
CH13_Order_Totals_By_Customer_And_Date (847 rows)
CustFullName |
OrderDate |
TotalCost |
Alaina Hallmark |
2017-09-03 |
$4,699.98 |
Alaina Hallmark |
2017-09-15 |
$4,433.95 |
Alaina Hallmark |
2017-09-22 |
$353.25 |
Alaina Hallmark |
2017-09-23 |
$3,951.90 |
Alaina Hallmark |
2017-10-01 |
$10,388.68 |
Alaina Hallmark |
2017-10-13 |
$3,088.00 |
Alaina Hallmark |
2017-10-23 |
$6,775.06 |
Alaina Hallmark |
2017-10-31 |
$15,781.10 |
<< more rows here >> |
“Display each entertainment group ID, entertainment group member, and the amount of pay for each member based on the total contract price divided by the number of members in the group.”
Note: This one is really tricky because each member might belong to more than one entertainer
group. You must sum the contract prices for each entertainer and then divide by the
count of members in that group (assuming each member gets equal pay). Fetching the
count requires a subquery filtered on the current entertainer ID (the ID of the group,
not the ID of the member), which means you also must group by entertainer ID. Oh yes,
and don’t forget to exclude members who are not active (Status = 3).
Translation/Clean Up |
Select entertainer ID, member first name, member last name, and the sum of (contract price)s divided by / the (select count(*) of active members from entertainer members as EM2 in the current entertainer group where status is not equal to <> not active 3 and the EM2 table entertainer ID equals = the entertainer members table entertainer ID) from the members table inner joined with the entertainer members table on members.member ID in the members table matches = entertainer_members.member ID in the entertainer members table, then inner joined with the entertainers table on entertainers.entertainer ID in the entertainers table matches = entertainer_members.entertainer ID in the entertainer members table, and finally inner joined with the engagements table on entertainers.entertainer ID in the entertainers table matches = engagements.entertainer ID in the engagements table, where member status is not equal to <> not active 3, grouped by entertainer ID, member first name, and member last name, sorted order by member last name |
|
|
CH13_Member_Pay (39 rows)
EntertainerID |
MbrFirstName |
MbrLastName |
MemberPay |
1010 |
Kendra |
Bonnicksen |
$2,887.50 |
1013 |
Kendra |
Bonnicksen |
$3,767.50 |
1007 |
Robert |
Brown |
$2,975.00 |
1008 |
Robert |
Brown |
$6,816.00 |
1008 |
George |
Chavez |
$6,816.00 |
1013 |
George |
Chavez |
$3,767.50 |
1010 |
Caroline |
Coie |
$2,887.50 |
1013 |
Caroline |
Coie |
$3,767.50 |
<< more rows here >> |
“For completed classes, list by category and student the category name, the student name, and the student’s average grade of all classes taken in that category.”
Translation/Clean Up |
Select category description, student first name, student last name, and the average AVG(of grade) as AvgOfGrade from the categories table inner joined with the subjects table on categories.category ID in the categories table matches = subjects.category ID in the subjects table, then inner joined with the classes table on subjects.subject ID in the subjects table matches = classes.subject ID in the classes table, then inner joined with the student schedules table on classes.class ID in the classes table matches = student_schedules.class ID in the student schedules table, then inner joined with the student class status table on student_class_status.class status in the student class status table matches = student_schedules.class status in the student schedules table, and finally inner joined with the students table on students.student ID in the students table matches = student_schedules.student ID in the student schedules table where class status description is = ‘Completed,’ grouped by category description, student first name, and student last name |
|
|
CH13_Student_GradeAverage_By_Category (63 rows)
Category Description |
StudFirst Name |
StudLast Name |
AvgOfGrade |
Accounting |
Doris |
Hartwig |
80.51 |
Accounting |
Elizabeth |
Hallmark |
91.12 |
Accounting |
Kendra |
Bonnicksen |
88.50 |
Accounting |
Richard |
Lum |
79.61 |
Accounting |
Sarah |
Thompson |
77.34 |
Art |
Doris |
Hartwig |
82.19 |
Art |
George |
Chavez |
83.63 |
Art |
John |
Kennedy |
87.65 |
Art |
Kerry |
Patterson |
99.83 |
Art |
Michael |
Viescas |
73.37 |
<< more rows here >> |
“Show me for each tournament and match the tournament ID, the tournament location, the match number, the name of each team, and the total of the handicap score for each team.”
Select tourney ID, tourney location, match ID, team name, and the sum of (handicap score) as TotHandiCapScore from the tournaments table inner joined with the tourney matches table on tournaments.tourney ID in the tournaments table matches = tourney_matches.tourney ID in the tourney matches table, then inner joined with the match games table on tourney_matches.match ID in the tourney matches table matches = match_games.match ID in the match games table, then inner joined with the bowler scores table on match_games.match ID in the match games table matches = bowler_scores.match ID in the bowler scores table and match_games.game number in the match games table matches = bowler_scores.game number in the bowler scores table, then inner joined with the bowlers table on bowlers.bowler ID in the bowlers table matches = bowler_scores.bowler ID in the bowler scores table, and finally inner joined with the teams table on teams.team ID in the teams table matches = bowlers.team ID in the bowlers table, grouped by tourney ID, tourney location, match ID, and team name |
|
|
|
As you can see, the difficult part of this request is assembling the complex JOIN clauses to link all the tables in the correct manner.
CH13_Tournament_Match_Team_Results (112 rows)
Tourney ID |
Tourney Location |
MatchID |
TeamName |
TotHandi CapScore |
|
1 |
Red Rooster Lanes |
1 |
Marlins |
2351 |
|
1 |
Red Rooster Lanes |
1 |
Sharks |
2348 |
|
1 |
Red Rooster Lanes |
2 |
Barracudas |
2289 |
|
1 |
Red Rooster Lanes |
2 |
Terrapins |
2391 |
|
1 |
Red Rooster Lanes |
3 |
Dolphins |
2389 |
|
1 |
Red Rooster Lanes |
3 |
Orcas |
2395 |
|
1 |
Red Rooster Lanes |
4 |
Manatees |
2292 |
|
1 |
Red Rooster Lanes |
4 |
Swordfish |
2353 |
|
2 |
Thunderbird Lanes |
5 |
Marlins |
2297 |
|
2 |
Thunderbird Lanes |
5 |
Terrapins |
2279 |
|
<< more rows here >> |
“Display the highest raw score for each bowler.”
Translation/Clean Up |
Select bowler first name, bowler last name, and the maximum (raw score) as HighScore from the bowlers table inner joined with the bowler scores table on bowlers.bowler ID in the bowlers table matches = bowler_scores.bowler ID in the bowler scores table, grouped by bowler first name, and bowler last name |
|
CH13_Bowler_High_Score_Group (32 rows)
BowlerFirstName |
BowlerLastName |
HighScore |
Alaina |
Hallmark |
180 |
Alastair |
Black |
164 |
Angel |
Kennedy |
194 |
Ann |
Patterson |
165 |
Bailey |
Hallmark |
164 |
Barbara |
Fournier |
164 |
Caleb |
Viescas |
193 |
Carol |
Viescas |
150 |
David |
Cunningham |
180 |
David |
Fournier |
178 |
<< more rows here >> |
“Show me how many recipes exist for each class of ingredient.”
Note: The challenge here is that you don’t want to count a particular recipe class more
than once per recipe. For example, if a recipe contains multiple herbs or dairy ingredients,
that recipe should be counted only once per class. Sounds like it’s time to use COUNT(DISTINCT
value expression), doesn’t it?
Select ingredient class description, and the unique count of (distinct recipe ID) as CountOfRecipeID from the ingredient classes table inner joined with the ingredients table on ingredient_classes.ingredient class ID in the ingredient classes table matches = ingredients.ingredient class ID in the ingredients table, and then inner joined with the recipe ingredients table on ingredients.ingredient ID in the ingredients table matches = recipe_ingredients.ingredient ID in the recipe ingredients table, grouped by ingredient class description |
|
|
|
CH13_IngredientClass_Distinct_Recipe_Count (19 rows)
IngredientClassDescription |
CountOfRecipeID |
||
Bottle |
1 |
||
Butter |
3 |
||
Cheese |
2 |
||
Chips |
1 |
||
Condiment |
2 |
||
Dairy |
2 |
||
Fruit |
1 |
||
Grain |
2 |
||
Herb |
1 |
||
<< more rows here >> |
Note: Because Microsoft Access does not support COUNT DISTINCT, you’ll find that the query
in the Access sample database first selects the DISTINCT values of RecipeID using
a table subquery in the FROM clause and then counts the resulting rows.
I began the chapter by explaining to you why you might want to group data to get multiple subtotals from a result set. After tantalizing you with an example, I proceeded to show how to use the GROUP BY clause to solve the example problem and several others. I also showed how to mix column expressions with aggregate functions.
I next explored an interesting example of using GROUP BY in a subquery that acts as a filter in a WHERE clause. I subsequently pointed out that constructing a query using GROUP BY and no aggregate functions is the same as using DISTINCT in your SELECT clause. Then I warned you to carefully construct your GROUP BY clause to include the columns and not the expressions.
I wrapped up my discussion of the GROUP BY clause by explaining some common pitfalls. I showed that SQL does not consider any knowledge of primary keys. I also explained common mistakes you might make when using column expressions in your SELECT clause.
I summarized why the GROUP BY clause is useful and gave you a sample list of problems you can solve using GROUP BY. The rest of the chapter provided examples of how to build requests that require the GROUP BY clause.
The following section presents several requests that you can work out on your own.
Below, I show you the request statement and the name of the solution query in the sample databases. If you want some practice, you can work out the SQL you need for each request and then check your answer with the query I saved in the samples. Don’t worry if your syntax doesn’t exactly match the syntax of the queries I saved—as long as your result set is the same.
1. “Show me each vendor and the average by vendor of the number of days to deliver products.”
(Hint: Use the AVG aggregate function and group on vendor.)
You can find the solution in CH13_Vendor_Avg_Delivery (10 rows).
2. “Display for each product the product name and the total sales.”
(Hint: Use SUM with a calculation of quantity times price and group on product name.)
You can find the solution in CH13_Sales_By_Product (38 rows).
3. “List all vendors and the count of products sold by each.”
You can find the solution in CH13_Vendor_Product_Count_Group (10 rows).
4. Challenge: Now solve problem 3 by using a subquery.
You can find the solution in CH13_Vendor_Product_Count_Subquery (10 rows).
1. “Show each agent’s name, the sum of the contract price for the engagements booked, and the agent’s total commission.”
(Hint: You must multiply the sum of the contract prices by the agent’s commission. Be sure to group on the commission rate as well!)
You can find the solution in CH13_Agent_Sales_And_Commissions (8 rows).
1. “Display by category the category name and the count of classes offered.”
(Hint: Use COUNT and group on category name.)
You can find the solution in CH13_Category_Class_Count (15 rows).
2. “List each staff member and the count of classes each is scheduled to teach.”
(Hint: Use COUNT and group on staff name.)
You can find the solution in CH13_Staff_Class_Count (22 rows).
3. Challenge: Now solve problem 2 by using a subquery.
You can find the solution in CH13_Staff_Class_Count_Subquery (27 rows).
4. Can you explain why the subquery solution returns five more rows? Is it possible to modify the query in question 2 to return 27 rows? If so, how would you do it?
(Hint: Think about using an OUTER JOIN.)
1. “Display for each bowler the bowler name and the average of the bowler’s raw game scores.”
(Hint: Use the AVG aggregate function and group on bowler name.)
You can find the solution in CH13_Bowler_Averages (32 rows).
2. “Calculate the current average and handicap for each bowler.”
(Hint: This is a “friendly” league, so the handicap is 90 percent of 200 minus the raw average. Be sure to round the raw average and convert it to an integer before subtracting it from 200, and then round and truncate the final result. Although the SQL Standard doesn’t define a ROUND function, most commercial database systems provide one. Check your product documentation for details.)
You can find the solution in CH13_Bowler_Average_Handicap (32 rows).
3. Challenge: “Display the highest raw score for each bowler,” but solve it by using a subquery.
You can find the solution in CH13_Bowler_High_Score_Subquery (32 rows).
1. “If I want to cook all the recipes in my cookbook, how much of each ingredient must I have on hand?”
(Hint: Use SUM and group on ingredient name and measurement description.)
You can find the solution in CH13_Total_Ingredients_Needed (65 rows).
2. “List all meat ingredients and the count of recipes that include each one.”
You can find the solution in CH13_Meat_Ingredient_Recipe_Count_Group (4 rows).
3. Challenge: Now solve problem 2 by using a subquery.
You can find the solution in CH13_Meat_Ingredient_Recipe_Count_Subquery (11 rows).
4. Can you explain why the subquery solution returns seven more rows? Is it possible to modify the query in question 2 to return 11 rows? If so, how would you do it?
(Hint: Think about using an OUTER JOIN.)