1. MySQL查询进阶:聚合与关联的艺术
作为后端开发中最常用的数据库操作,聚合查询和联合查询是每个开发者必须掌握的技能。记得刚入行时,我曾在处理一个简单的销售报表时,因为不熟悉GROUP BY的用法,导致数据统计完全错误,差点造成生产事故。今天我们就来深入剖析这些核心查询技术。
2. 聚合函数:数据统计的瑞士军刀
2.1 五大核心聚合函数解析
MySQL提供了丰富的聚合函数,最常用的五个是:
- COUNT()- 统计行数
- SUM()- 计算总和
- AVG()- 计算平均值
- MAX()- 获取最大值
- MIN()- 获取最小值
这些函数看似简单,但实际使用中有不少细节需要注意。比如COUNT(*)和COUNT(列名)的区别:前者统计所有行,后者只统计该列非NULL的行。
-- 统计员工总数(包括未分配部门的) SELECT COUNT(*) FROM employees; -- 统计已分配部门的员工数 SELECT COUNT(department_id) FROM employees;2.2 聚合函数的进阶用法
聚合函数可以组合使用,也可以配合数学函数进行复杂计算:
-- 计算销售提成(销售额的15%,保留2位小数) SELECT ROUND(SUM(amount) * 0.15, 2) AS commission FROM sales;注意:聚合函数会忽略NULL值,这在计算平均值时需要特别注意。AVG()只计算非NULL值的平均数。
3. GROUP BY:数据分组的魔法
3.1 基础分组查询
GROUP BY将数据按指定列分组,然后对每组应用聚合函数:
-- 按部门统计员工数量和平均薪资 SELECT department_id, COUNT(*) AS employee_count, AVG(salary) AS avg_salary FROM employees GROUP BY department_id;3.2 多列分组与筛选
可以按多列分组,HAVING子句用于过滤分组结果:
-- 按部门和职位分组,筛选平均薪资>10000的组 SELECT department_id, job_title, AVG(salary) AS avg_salary FROM employees GROUP BY department_id, job_title HAVING avg_salary > 10000;关键区别:WHERE在分组前过滤行,HAVING在分组后过滤组。我曾在项目中混淆两者,导致性能问题。
3.3 WITH ROLLUP:小计与总计
ROLLUP选项可以生成分组的小计和总计行:
-- 生成按年和月的销售报表,带小计和总计 SELECT YEAR(order_date) AS year, MONTH(order_date) AS month, SUM(amount) AS total_amount FROM orders GROUP BY YEAR(order_date), MONTH(order_date) WITH ROLLUP;4. 联合查询:数据关系的桥梁
4.1 INNER JOIN:精准匹配
内连接只返回两表中匹配的行:
-- 获取员工及其部门信息 SELECT e.employee_id, e.name, d.department_name FROM employees e INNER JOIN departments d ON e.department_id = d.department_id;4.2 LEFT/RIGHT JOIN:包容性连接
外连接保留某一边的所有行,即使另一边没有匹配:
-- 获取所有员工信息,包括未分配部门的 SELECT e.employee_id, e.name, d.department_name FROM employees e LEFT JOIN departments d ON e.department_id = d.department_id;4.3 多表连接实战
复杂业务往往需要连接多张表:
-- 获取订单详情:客户信息+产品信息 SELECT o.order_id, c.customer_name, p.product_name, oi.quantity, oi.unit_price FROM orders o JOIN customers c ON o.customer_id = c.customer_id JOIN order_items oi ON o.order_id = oi.order_id JOIN products p ON oi.product_id = p.product_id;5. 子查询:查询中的查询
5.1 WHERE子句中的子查询
-- 找出薪资高于平均薪资的员工 SELECT name, salary FROM employees WHERE salary > (SELECT AVG(salary) FROM employees);5.2 FROM子句中的派生表
-- 统计每个部门的最高薪资 SELECT d.department_name, m.max_salary FROM departments d JOIN ( SELECT department_id, MAX(salary) AS max_salary FROM employees GROUP BY department_id ) m ON d.department_id = m.department_id;6. 性能优化与常见陷阱
6.1 索引对聚合查询的影响
合适的索引可以大幅提升聚合查询性能:
- GROUP BY列应该建立索引
- 连接条件列必须建立索引
- 复合索引的顺序应与GROUP BY顺序一致
6.2 大数据量下的分页优化
对于大量数据的分页统计,避免使用OFFSET:
-- 不好的做法(大数据量性能差) SELECT * FROM large_table ORDER BY id LIMIT 10 OFFSET 10000; -- 更好的做法(使用游标) SELECT * FROM large_table WHERE id > last_seen_id ORDER BY id LIMIT 10;6.3 常见错误排查
- GROUP BY遗漏非聚合列:SELECT中的非聚合列必须出现在GROUP BY中
- HAVING误用:过滤单个行用WHERE,过滤组用HAVING
- 连接条件缺失:多表连接时漏写ON条件会导致笛卡尔积
7. 实战案例:销售分析报表
结合所有技术点,我们来实现一个完整的销售分析报表:
-- 按月统计各产品类别的销售额和占比 SELECT DATE_FORMAT(o.order_date, '%Y-%m') AS month, pc.category_name, SUM(oi.quantity * oi.unit_price) AS total_sales, ROUND( SUM(oi.quantity * oi.unit_price) / SUM(SUM(oi.quantity * oi.unit_price)) OVER (PARTITION BY DATE_FORMAT(o.order_date, '%Y-%m')) * 100, 2 ) AS percentage FROM orders o JOIN order_items oi ON o.order_id = oi.order_id JOIN products p ON oi.product_id = p.product_id JOIN product_categories pc ON p.category_id = pc.category_id WHERE o.order_date BETWEEN '2023-01-01' AND '2023-12-31' GROUP BY DATE_FORMAT(o.order_date, '%Y-%m'), pc.category_id ORDER BY month, total_sales DESC;这个查询展示了:
- 日期格式化
- 多表连接
- 聚合计算
- 窗口函数计算占比
- 时间段过滤
- 排序
在实际项目中,这类报表查询通常会配合缓存策略,避免每次请求都执行复杂计算。