Aggregate: turn rows into numbers
GROUP BY: one number per group
Last updated
An aggregate over a whole table gives one number. Most of the time you want one number per group: revenue per category, orders per customer, sign-ups per month. GROUP BY does this. It splits the rows into groups, then runs the aggregate inside each group.
SELECT category, COUNT(*) AS n
FROM products
GROUP BY category;Read it as: for each category, count the rows. The rule to remember is that every column in SELECT is either inside an aggregate or named in GROUP BY.
How many products are in each category?
CREATE TABLE customers ( id INTEGER PRIMARY KEY, name TEXT, city TEXT, signup_date TEXT ); INSERT INTO customers VALUES (1, 'Aisyah Rahman', 'Kuala Lumpur', '2023-01-12'), (2, 'Lim Wei Jie', 'Penang', '2023-02-03'), (3, 'Arjun Pillai', 'Johor Bahru', '2023-02-20'), (4, 'Nurul Huda', 'Kuala Lumpur', '2023-03-15'), (5, 'Tan Mei Ling', 'Ipoh', '2023-05-01'), (6, 'Faiz Hassan', 'Penang', '2023-06-10'); CREATE TABLE products ( id INTEGER PRIMARY KEY, name TEXT, category TEXT, price REAL ); INSERT INTO products VALUES (1, 'Standard License', 'Software', 199.0), (2, 'Pro License', 'Software', 499.0), (3, 'Onboarding Workshop', 'Service', 1200.0), (4, 'Support Plan', 'Service', 300.0), (5, 'Data Pack', 'Add-on', 89.0); CREATE TABLE orders ( id INTEGER PRIMARY KEY, customer_id INTEGER, order_date TEXT, status TEXT ); INSERT INTO orders VALUES (1001, 1, '2023-03-01', 'paid'), (1002, 1, '2023-04-12', 'paid'), (1003, 2, '2023-04-15', 'paid'), (1004, 3, '2023-05-02', 'refunded'), (1005, 4, '2023-05-20', 'paid'), (1006, 2, '2023-06-01', 'pending'), (1007, 5, '2023-06-18', 'paid'); CREATE TABLE order_items ( order_id INTEGER, product_id INTEGER, quantity INTEGER ); INSERT INTO order_items VALUES (1001, 1, 2), (1001, 5, 1), (1002, 2, 1), (1003, 1, 1), (1003, 4, 1), (1004, 3, 1), (1005, 2, 2), (1005, 5, 3), (1006, 1, 1), (1007, 3, 1), (1007, 4, 2);
SELECT category, COUNT(*) AS n_products FROM products GROUP BY category;
Group the orders by status to see the shape of the pipeline.
CREATE TABLE customers ( id INTEGER PRIMARY KEY, name TEXT, city TEXT, signup_date TEXT ); INSERT INTO customers VALUES (1, 'Aisyah Rahman', 'Kuala Lumpur', '2023-01-12'), (2, 'Lim Wei Jie', 'Penang', '2023-02-03'), (3, 'Arjun Pillai', 'Johor Bahru', '2023-02-20'), (4, 'Nurul Huda', 'Kuala Lumpur', '2023-03-15'), (5, 'Tan Mei Ling', 'Ipoh', '2023-05-01'), (6, 'Faiz Hassan', 'Penang', '2023-06-10'); CREATE TABLE products ( id INTEGER PRIMARY KEY, name TEXT, category TEXT, price REAL ); INSERT INTO products VALUES (1, 'Standard License', 'Software', 199.0), (2, 'Pro License', 'Software', 499.0), (3, 'Onboarding Workshop', 'Service', 1200.0), (4, 'Support Plan', 'Service', 300.0), (5, 'Data Pack', 'Add-on', 89.0); CREATE TABLE orders ( id INTEGER PRIMARY KEY, customer_id INTEGER, order_date TEXT, status TEXT ); INSERT INTO orders VALUES (1001, 1, '2023-03-01', 'paid'), (1002, 1, '2023-04-12', 'paid'), (1003, 2, '2023-04-15', 'paid'), (1004, 3, '2023-05-02', 'refunded'), (1005, 4, '2023-05-20', 'paid'), (1006, 2, '2023-06-01', 'pending'), (1007, 5, '2023-06-18', 'paid'); CREATE TABLE order_items ( order_id INTEGER, product_id INTEGER, quantity INTEGER ); INSERT INTO order_items VALUES (1001, 1, 2), (1001, 5, 1), (1002, 2, 1), (1003, 1, 1), (1003, 4, 1), (1004, 3, 1), (1005, 2, 2), (1005, 5, 3), (1006, 1, 1), (1007, 3, 1), (1007, 4, 2);
SELECT status, COUNT(*) AS n_orders FROM orders GROUP BY status;
Your turn
Count how many orders each customer has placed. Return customer_id and a count named n_orders.
Orders per customer.
CREATE TABLE customers ( id INTEGER PRIMARY KEY, name TEXT, city TEXT, signup_date TEXT ); INSERT INTO customers VALUES (1, 'Aisyah Rahman', 'Kuala Lumpur', '2023-01-12'), (2, 'Lim Wei Jie', 'Penang', '2023-02-03'), (3, 'Arjun Pillai', 'Johor Bahru', '2023-02-20'), (4, 'Nurul Huda', 'Kuala Lumpur', '2023-03-15'), (5, 'Tan Mei Ling', 'Ipoh', '2023-05-01'), (6, 'Faiz Hassan', 'Penang', '2023-06-10'); CREATE TABLE products ( id INTEGER PRIMARY KEY, name TEXT, category TEXT, price REAL ); INSERT INTO products VALUES (1, 'Standard License', 'Software', 199.0), (2, 'Pro License', 'Software', 499.0), (3, 'Onboarding Workshop', 'Service', 1200.0), (4, 'Support Plan', 'Service', 300.0), (5, 'Data Pack', 'Add-on', 89.0); CREATE TABLE orders ( id INTEGER PRIMARY KEY, customer_id INTEGER, order_date TEXT, status TEXT ); INSERT INTO orders VALUES (1001, 1, '2023-03-01', 'paid'), (1002, 1, '2023-04-12', 'paid'), (1003, 2, '2023-04-15', 'paid'), (1004, 3, '2023-05-02', 'refunded'), (1005, 4, '2023-05-20', 'paid'), (1006, 2, '2023-06-01', 'pending'), (1007, 5, '2023-06-18', 'paid'); CREATE TABLE order_items ( order_id INTEGER, product_id INTEGER, quantity INTEGER ); INSERT INTO order_items VALUES (1001, 1, 2), (1001, 5, 1), (1002, 2, 1), (1003, 1, 1), (1003, 4, 1), (1004, 3, 1), (1005, 2, 2), (1005, 5, 3), (1006, 1, 1), (1007, 3, 1), (1007, 4, 2);
SELECT customer_id, FROM orders ;
SELECT customer_id, COUNT(*) AS n_orders FROM orders GROUP BY customer_id;
Every column inSELECTmust be either inside an aggregate or named inGROUP BY. Some databases reject the query if you forget; others return an arbitrary value and let the mistake slip through.
In a grouped query, where must a plain column (not wrapped in an aggregate) appear?
Any column you select without aggregating has to be in
GROUP BY, so the database knows how to fold the rows together.