本文将深入探讨 MySQL 高级查询技巧,重点讲解 GROUP BY
、HAVING
、各种聚合函数、子查询以及分页查询(LIMIT
语法)的使用。文章内容涵盖实际应用中最常见的报表需求和分页实现技巧,适合有一定 SQL 基础的开发者进一步提升技能。
在进入高级部分之前,我们先简要回顾一些 SQL 查询的基本组成部分,便于后续内容的理解:
SELECT [字段列表]
FROM [表名]
WHERE [条件]
GROUP BY [分组字段]
HAVING [聚合条件]
ORDER BY [排序字段]
LIMIT [偏移量, 行数]
GROUP BY
用于将查询结果按某个或某些字段进行分组。配合聚合函数(如 COUNT()
、SUM()
、AVG()
等)使用,可以实现对每个分组的统计。
示例:统计每个部门的员工数量
SELECT department_id, COUNT(*) AS employee_count
FROM employees
GROUP BY department_id;
示例:统计每个部门中每个职位的员工数量
SELECT department_id, job_id, COUNT(*) AS employee_count
FROM employees
GROUP BY department_id, job_id;
多字段分组适用于需要“交叉”维度分析的场景,比如不同区域+不同产品的销售统计。
聚合函数用于对一组数据进行计算,常用于 GROUP BY
分组后。
函数 | 说明 |
---|---|
COUNT() | 统计数量 |
SUM() | 求和 |
AVG() | 平均值 |
MAX() | 最大值 |
MIN() | 最小值 |
GROUP_CONCAT() | 将组内字段连接为字符串 |
SELECT department_id, COUNT(*) AS total_employees
FROM employees
GROUP BY department_id;
SELECT department_id, SUM(salary) AS total_salary
FROM employees
GROUP BY department_id;
SELECT department_id, GROUP_CONCAT(first_name) AS employee_names
FROM employees
GROUP BY department_id;
GROUP_CONCAT()
在报表中经常用于“拼接多个名称为一列”,如列出参与某个项目的所有人名。
WHERE
是对 原始数据 进行筛选HAVING
是对 分组后的结果 进行筛选SELECT department_id, COUNT(*) AS employee_count
FROM employees
GROUP BY department_id
HAVING COUNT(*) > 5;
虽然在 SELECT
中定义了别名 employee_count
,但在 HAVING
中引用聚合函数更安全。
HAVING COUNT(*) > 5 -- 推荐
-- HAVING employee_count > 5 -- 有些版本不支持
子查询是指嵌套在主查询内部的 SELECT 查询。可以出现在 SELECT、FROM、WHERE 等多个位置。
示例:查询每位员工的平均工资差值
SELECT employee_id, salary,
salary - (SELECT AVG(salary) FROM employees) AS diff_from_avg
FROM employees;
示例:查询工资高于公司平均值的员工
SELECT *
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
用于将子查询临时当作一个“表”来使用。
SELECT department_id, avg_salary
FROM (
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
) AS dept_avg
WHERE avg_salary > 10000;
示例:查找至少有一位员工的部门
SELECT department_id, department_name
FROM departments
WHERE department_id IN (
SELECT DISTINCT department_id FROM employees
);
在构建分页接口或展示数据列表时,LIMIT
是非常关键的 SQL 工具。
SELECT * FROM employees
LIMIT 10; -- 取前10条
SELECT * FROM employees
LIMIT 10 OFFSET 20; -- 从第21条开始,取10条
等价写法:
SELECT * FROM employees
LIMIT 20, 10;
-- page = 3, pageSize = 10
SELECT * FROM employees
ORDER BY hire_date DESC
LIMIT 20, 10;
分页核心逻辑:LIMIT (page - 1) * pageSize, pageSize
以下为结合 GROUP BY
、聚合函数、子查询与分页的常见报表查询场景。
SELECT department_id,
DATE_FORMAT(hire_date, '%Y-%m') AS month,
SUM(salary) AS total_salary
FROM employees
GROUP BY department_id, month
ORDER BY department_id, month;
SELECT employee_id, first_name, salary
FROM employees
ORDER BY salary DESC
LIMIT 3;
SELECT *
FROM employees e
WHERE salary = (
SELECT MAX(salary)
FROM employees
WHERE department_id = e.department_id
);
分页性能在大数据量下成为瓶颈,以下是常见优化方法。
-- 仅查询主键或索引字段
SELECT employee_id
FROM employees
ORDER BY hire_date DESC
LIMIT 100000, 10;
-- 第一步:查主键
SELECT employee_id
FROM employees
ORDER BY hire_date DESC
LIMIT 100000, 10;
-- 第二步:再查详情
SELECT *
FROM employees
WHERE employee_id IN (…);
-- 假设上次最后一条ID是 120
SELECT * FROM employees
WHERE employee_id > 120
ORDER BY employee_id
LIMIT 10;
技巧 | 应用场景 |
---|---|
GROUP BY | 数据分组统计、分类汇总 |
聚合函数 | 报表、指标计算(如总数、平均值等) |
HAVING | 分组结果过滤 |
子查询 | 复杂条件过滤、嵌套数据分析 |
LIMIT | 分页列表、Top N 取值 |
分页优化 | 大数据分页响应慢时的优化方案 |
本项目适用于后台管理系统、电商用户中心、SaaS 用户模块等场景,特别适合开发者进行实战演练与面试准备。
我们将构建一个基础版的用户管理系统,具备以下业务功能:
涉及的核心业务对象包括:用户、角色、权限、日志等。
users
)CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
email VARCHAR(100),
status TINYINT DEFAULT 1 COMMENT '0:禁用, 1:启用',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
roles
)CREATE TABLE roles (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(255)
);
permissions
)CREATE TABLE permissions (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL UNIQUE,
code VARCHAR(50) NOT NULL UNIQUE COMMENT '用于权限标识,如 user:view'
);
user_role
)CREATE TABLE user_role (
user_id INT,
role_id INT,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (role_id) REFERENCES roles(id)
);
role_permission
)CREATE TABLE role_permission (
role_id INT,
permission_id INT,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES roles(id),
FOREIGN KEY (permission_id) REFERENCES permissions(id)
);
login_logs
)CREATE TABLE login_logs (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
ip_address VARCHAR(45),
login_time DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
INSERT INTO roles(name, description) VALUES ('admin', '系统管理员'), ('user', '普通用户');
INSERT INTO permissions(name, code) VALUES
('查看用户', 'user:view'),
('新增用户', 'user:create'),
('删除用户', 'user:delete');
-- 分配权限给角色
INSERT INTO role_permission(role_id, permission_id) VALUES
(1, 1), (1, 2), (1, 3), -- admin 拥有全部权限
(2, 1); -- user 仅能查看用户
INSERT INTO users(username, password, email) VALUES
('alice', 'hashed_pwd1', 'alice@example.com'),
('bob', 'hashed_pwd2', 'bob@example.com');
-- 分配角色
INSERT INTO user_role(user_id, role_id) VALUES
(1, 1), -- alice 为管理员
(2, 2); -- bob 为普通用户
SELECT u.id, u.username, r.name AS role
FROM users u
JOIN user_role ur ON u.id = ur.user_id
JOIN roles r ON ur.role_id = r.id
WHERE u.status = 1;
SELECT p.name, p.code
FROM users u
JOIN user_role ur ON u.id = ur.user_id
JOIN role_permission rp ON ur.role_id = rp.role_id
JOIN permissions p ON rp.permission_id = p.id
WHERE u.username = 'alice';
SELECT u.username, l.ip_address, l.login_time
FROM login_logs l
JOIN users u ON l.user_id = u.id
WHERE l.login_time >= NOW() - INTERVAL 7 DAY
ORDER BY l.login_time DESC;
SELECT *
FROM users
WHERE username LIKE '%bob%'
ORDER BY created_at DESC
LIMIT 0, 10;
在角色授权或用户注册等业务流程中,可以使用事务来确保数据完整性。
START TRANSACTION;
INSERT INTO users(username, password, email) VALUES('charlie', 'hashed_pwd3', 'charlie@example.com');
SET @uid = LAST_INSERT_ID();
INSERT INTO user_role(user_id, role_id) VALUES(@uid, 2); -- 默认赋普通角色
COMMIT;
START TRANSACTION;
-- 假设某权限不存在导致失败
INSERT INTO role_permission(role_id, permission_id) VALUES(1, 999);
-- 失败时回滚
ROLLBACK;
users.username
:用于登录验证、搜索login_logs.user_id
:日志查询user_role.user_id
/ role_permission.role_id
:JOIN 优化CREATE INDEX idx_username ON users(username);
CREATE INDEX idx_user_log ON login_logs(user_id);
EXPLAIN SELECT u.username, r.name FROM users u JOIN user_role ur ON u.id = ur.user_id JOIN roles r ON ur.role_id = r.id;
可查看索引是否使用、JOIN 类型、Rows 扫描数量等