WHERE 筛行,HAVING 筛组. 条件不涉及聚合函数,放 WHERE 更高效(减少参与分组的数据量);条件涉及 COUNT/SUM 等聚合结果,只能放 HAVING.
多列分组
GROUP BY 可以同时按多个列分组,形成更细的粒度:
-- 每个城市中,VIP 和非 VIP 用户各有多少?平均余额多少? SELECT city, is_vip, COUNT(*) AS cnt, ROUND(AVG(balance), 2) AS avg_balance FROM users GROUPBY city, is_vip ORDERBY city, is_vip DESC;
-- 每个分类下,各商品的平均评分 SELECT c.name AS category, p.name AS product, ROUND(AVG(r.rating), 1) AS avg_rating, COUNT(r.id) AS review_count FROM products p JOIN categories c ON c.id = p.category_id LEFTJOIN reviews r ON r.product_id = p.id GROUPBY c.name, p.name HAVINGCOUNT(r.id) >0 ORDERBY category, avg_rating DESC;
条件聚合
用 FILTER (WHERE ...) 或 CASE WHEN 在一次查询中同时计算多个有条件的聚合值,避免写多个子查询:
-- 每个用户:总订单数,已完成订单数,已取消订单数 SELECT u.username, COUNT(o.id) AS total_orders, COUNT(o.id) FILTER (WHERE o.status ='done') AS done_orders, COUNT(o.id) FILTER (WHERE o.status ='cancelled') AS cancelled_orders FROM users u LEFTJOIN orders o ON o.user_id = u.id GROUPBY u.username ORDERBY total_orders DESC;
FILTER 是 PostgreSQL 扩展
FILTER (WHERE ...) 是 SQL 标准(SQL:2003)语法,但 MySQL 不支持. 如果需要兼容 MySQL,用等价的 CASE WHEN 写法:COUNT(CASE WHEN status = 'done' THEN 1 END)
-- CASE WHEN 等价写法 SELECT u.username, COUNT(o.id) AS total_orders, SUM(CASEWHEN o.status ='done'THEN1ELSE0END) AS done_orders, SUM(CASEWHEN o.status ='cancelled'THEN1ELSE0END) AS cancelled_orders FROM users u LEFTJOIN orders o ON o.user_id = u.id GROUPBY u.username;
-- 统计每个分类的:商品数,均价,最高价,库存总量 SELECT c.name AS category, COUNT(p.id) AS product_count, ROUND(AVG(p.price), 2) AS avg_price, MAX(p.price) AS max_price, SUM(p.stock) AS total_stock FROM categories c LEFTJOIN products p ON p.category_id = c.id GROUPBY c.name ORDERBY avg_price DESC;
-- 有多少个不同城市的用户下过单? SELECTCOUNT(DISTINCT u.city) AS city_count FROM users u JOIN orders o ON o.user_id = u.id; -- 4(北京,上海,广州,深圳都有用户下单)
-- 每个分类被多少个不同用户购买过? SELECT c.name AS category, COUNT(DISTINCT o.user_id) AS buyer_count FROM categories c JOIN products p ON p.category_id = c.id JOIN order_items oi ON oi.product_id = p.id JOIN orders o ON o.id = oi.order_id GROUPBY c.name ORDERBY buyer_count DESC;
-- 每个用户的总消费金额和平均订单金额(只统计已完成的订单) SELECT u.username, u.city, COUNT(o.id) AS order_count, SUM(o.total) AS total_spent, ROUND(AVG(o.total), 2) AS avg_order FROM users u LEFTJOIN orders o ON o.user_id = u.id AND o.status ='done' GROUPBY u.id, u.username, u.city ORDERBY total_spent DESCNULLS LAST;
-- eve 上海 2 23064.00 11532.00 -- alice 北京 2 18597.00 9298.50 -- grace 北京 1 7266.00 7266.00 -- bob 上海 1 228.00 228.00 -- dave 广州 1 256.00 256.00 -- carol 北京 1 139.00 139.00 -- henry 广州 1 168.00 168.00 -- frank 深圳 0 (null) (null)
LEFT JOIN + 条件放在 ON vs WHERE
上面的 AND o.status = 'done' 放在 ON 子句中. 如果放在 WHERE 中,没有 done 订单的用户会被整行过滤掉(frank 就不会出现).
-- 每个商品分类的平均评分和评价数 SELECT c.name AS category, COUNT(r.id) AS review_count, ROUND(AVG(r.rating), 2) AS avg_rating FROM categories c JOIN products p ON p.category_id = c.id LEFTJOIN reviews r ON r.product_id = p.id GROUPBY c.name ORDERBY avg_rating DESCNULLS LAST;
-- 正确做法:分别聚合后再 JOIN SELECT u.username, COALESCE(oc.cnt, 0) AS order_count, COALESCE(rc.cnt, 0) AS review_count FROM users u LEFTJOIN (SELECT user_id, COUNT(*) AS cnt FROM orders GROUPBY user_id) oc ON oc.user_id = u.id LEFTJOIN (SELECT user_id, COUNT(*) AS cnt FROM reviews GROUPBY user_id) rc ON rc.user_id = u.id;