摘要:掌握 SQL 查询是数据库应用的核心。本文以实战外卖骑手数据为例,系统讲解 SELECT 查询的完整技能栈:从基础查询、条件过滤、聚合统计、分组排序,到多表关联设计。通过 20+ 个真实案例,带你从零到一掌握数据查询的精髓,让你能从任何数据库中精准捞出想要的数据。
一、 DQL 是什么
DQL(Data Query Language),数据查询语言。关键字就一个:SELECT。在一个真实业务系统里,查询占了数据库操作的 80% 以上。你打开淘宝搜"篮球鞋",后端发了一条 SELECT 去数据库;你看订单列表,又是一条 SELECT;后台老板想看今天的销售额报表,还是 SELECT。
一条完整的 DQL 语句长这样:
SELECT -- 要查哪些字段 FROM -- 从哪张表查 WHERE -- 什么条件下查 GROUP BY -- 怎么分组 HAVING -- 分组后过滤 ORDER BY -- 怎么排序 LIMIT -- 取多少条今天按这个顺序逐个拆解。先建一张练习用的表。
准备测试数据
用外卖场景——骑手表。
创建数据库和测试表:
CREATE DATABASE IF NOT EXISTS delivery_db; USE delivery_db; CREATE TABLE rider ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '骑手ID', username VARCHAR(20) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(32) DEFAULT '123456' COMMENT '密码', name VARCHAR(10) NOT NULL COMMENT '真实姓名', gender TINYINT UNSIGNED NOT NULL COMMENT '性别: 1男 2女', phone CHAR(11) COMMENT '手机号', level TINYINT UNSIGNED COMMENT '骑手等级: 1青铜 2白银 3黄金 4钻石', join_date DATE COMMENT '入职日期', city VARCHAR(20) DEFAULT '北京' COMMENT '所在城市', score DECIMAL(3,1) DEFAULT 5.0 COMMENT '评分', create_time DATETIME NOT NULL COMMENT '创建时间', update_time DATETIME NOT NULL COMMENT '修改时间' ) COMMENT '骑手表';插入一些测试数据:
INSERT INTO rider (username, name, gender, phone, level, join_date, city, score, create_time, update_time) VALUES ('wangqiang', '王强', 1, '13800001111', 4, '2020-03-15', '北京', 4.9, NOW(), NOW()), ('liumei', '刘梅', 2, '13800002222', 2, '2022-07-01', '上海', 4.5, NOW(), NOW()), ('zhangwei', '张伟', 1, '13800003333', 3, '2021-01-20', '北京', 4.7, NOW(), NOW()), ('chenjing', '陈静', 2, '13800004444', 1, '2023-11-05', '深圳', 4.2, NOW(), NOW()), ('zhaolei', '赵磊', 1, '13800005555', 4, '2019-06-10', '广州', 4.8, NOW(), NOW()), ('sunli', '孙丽', 2, '13800006666', 3, '2021-09-12', '上海', 4.6, NOW(), NOW()), ('zhoudong', '周东', 1, '13800007777', 2, '2022-02-28', '北京', 4.3, NOW(), NOW()), ('wujing', '吴静', 2, '13800008888', 1, '2024-04-01', '深圳', 4.0, NOW(), NOW()), ('zhengyang', '郑洋', 1, '13800009999', 4, '2018-11-20', '杭州', 4.9, NOW(), NOW()), ('huangli', '黄丽', 2, '13800010000', 2, '2023-05-15', '广州', 4.4, NOW(), NOW()), ('xuming', '徐明', 1, '13800011111', 3, '2020-08-08', '北京', 4.5, NOW(), NOW()), ('linfang', '林芳', 2, '13800012222', 1, '2024-01-10', '上海', 3.8, NOW(), NOW()), ('maochao', '毛超', 1, '13800013333', 2, '2021-12-25', '杭州', 4.1, NOW(), NOW()), ('heping', '何平', 1, '13800014444', 3, '2019-04-18', '深圳', 4.6, NOW(), NOW()), ('guyan', '顾燕', 2, '13800015555', 4, '2020-10-01', '北京', 5.0, NOW(), NOW()), ('xiaofeng', '肖峰', 1, '13800016666', 2, '2022-09-03', '广州', 3.9, NOW(), NOW()), ('fanjie', '范洁', 2, '13800017777', 1, '2023-03-20', NULL, 4.2, NOW(), NOW()), ('daishan', '代山', 1, NULL, 3, '2020-05-06', '上海', 4.3, NOW(), NOW()), ('leiyu', '雷宇', 1, '13800019999', NULL, '2021-08-14', '深圳', 3.5, NOW(), NOW()), ('dengna', '邓娜', 2, '13800020000', 2, '2022-11-30', '北京', 4.7, NOW(), NOW());20 条数据,故意留了几个坑:范洁的 city 是 NULL,雷宇的 level 是 NULL,代山的 phone 是 NULL。后面讲条件查询时会用到。
二、基本查询
2.1 查指定字段
SELECT name, join_date FROM rider;只返回姓名和入职日期两列。用哪个字段就写哪个字段,别上来就 SELECT *——返回一堆不需要的列,浪费带宽和内存。
2.2 查所有字段
SELECT * FROM rider;* 是通配符。快速看看表里有什么数据时可以用,但正式代码里别写 *——表结构一变,你的代码读到预期之外的列,排序和分页都可能出 bug。
2.3 起别名
-- 三种写法等价 SELECT name AS 姓名, join_date AS 入职日期 FROM rider; SELECT name AS '姓 名', join_date AS '入职日期' FROM rider; SELECT name AS "姓名", join_date AS "入职日期" FROM rider;别名里有空格时,用单引号或双引号包起来。别名只影响显示,不改变表结构。
2.4 去重
-- 看看骑手都在哪些城市 SELECT DISTINCT city FROM rider;结果:北京、上海、深圳、广州、杭州、NULL。DISTINCT 对整行去重。如果写SELECT DISTINCT city, gender FROM rider,只有城市和性别完全相同的行才会被合并。
三、条件查询(WHERE)
这才是查询的灵魂。
语法:
SELECT 字段 FROM 表 WHERE 条件;3.1 比较运算符
| 运算符 | 功能 | 示例 |
|---|---|---|
| > >= < <= = | 比较 | score > 4.5 |
| <> 或 != | 不等于 | city <> '北京' |
| BETWEEN a AND b | 在范围内(含边界) | score BETWEEN 4.0 AND 4.9 |
| IN (v1, v2, ...) | 在列表中 | level IN (3, 4) |
| LIKE '模式' | 模糊匹配 | name LIKE '王%' |
| IS NULL / IS NOT NULL | 判空 | phone IS NULL |
3.2 逻辑运算符
| 运算符 | 功能 |
|---|---|
| AND 或 && | 并且 |
| OR 或 || | 或者 |
| NOT 或 ! | 取反 |
3.3 实战案例
用 rider 表逐个试:
-- 1. 评分高于 4.5 的骑手 SELECT name, score FROM rider WHERE score > 4.5; -- 2. 非北京的骑手 SELECT name, city FROM rider WHERE city <> '北京'; -- 3. 评分在 4.0 到 4.5 之间(含边界) SELECT name, score FROM rider WHERE score BETWEEN 4.0 AND 4.5; -- 4. 黄金或钻石等级的骑手 SELECT name, level FROM rider WHERE level IN (3, 4); -- 5. 姓"王"的骑手 —— % 匹配任意个字符 SELECT name FROM rider WHERE name LIKE '王%'; -- 6. 姓"王"且名字是两个字 —— _ 匹配单个字符 SELECT name FROM rider WHERE name LIKE '王_'; -- 7. 没填手机号的骑手 SELECT name, phone FROM rider WHERE phone IS NULL; -- 8. 有等级的骑手 SELECT name, level FROM rider WHERE level IS NOT NULL;四、聚合统计
当我们需要对数据进行汇总计算时,就需要用到聚合函数。
4.1 常用聚合函数
| 函数 | 功能 | 示例 |
|---|---|---|
| COUNT() | 统计行数 | SELECT COUNT(*) FROM rider; |
| SUM() | 求和 | SELECT SUM(score) FROM rider; |
| AVG() | 求平均值 | SELECT AVG(score) FROM rider; |
| MAX() | 求最大值 | SELECT MAX(score) FROM rider; |
| MIN() | 求最小值 | SELECT MIN(score) FROM rider; |
4.2 实战案例
-- 1. 统计骑手总数 SELECT COUNT(*) AS 总人数 FROM rider; -- 2. 统计有手机号的骑手数量 SELECT COUNT(phone) AS 有手机号人数 FROM rider; -- 3. 计算平均评分 SELECT AVG(score) AS 平均评分 FROM rider; -- 4. 找出最高评分和最低评分 SELECT MAX(score) AS 最高分, MIN(score) AS 最低分 FROM rider; -- 5. 统计各城市骑手数量 SELECT city, COUNT(*) AS 人数 FROM rider GROUP BY city;五、分组查询(GROUP BY)
GROUP BY 用于将数据按指定字段分组,通常与聚合函数一起使用。
5.1 基本语法
SELECT 分组字段, 聚合函数(字段) FROM 表名 WHERE 条件 GROUP BY 分组字段;5.2 实战案例
-- 1. 按城市分组,统计每个城市的骑手数量 SELECT city, COUNT(*) AS 人数 FROM rider GROUP BY city; -- 2. 按等级分组,计算每个等级的平均评分 SELECT level, AVG(score) AS 平均评分 FROM rider GROUP BY level; -- 3. 按性别和城市分组,统计人数 SELECT gender, city, COUNT(*) AS 人数 FROM rider GROUP BY gender, city;5.3 HAVING 子句
HAVING 用于对分组后的结果进行过滤,类似于 WHERE,但作用于分组后的数据。
-- 找出平均评分大于 4.5 的城市 SELECT city, AVG(score) AS 平均评分 FROM rider GROUP BY city HAVING AVG(score) > 4.5; -- 找出骑手数量超过 3 人的城市 SELECT city, COUNT(*) AS 人数 FROM rider GROUP BY city HAVING COUNT(*) > 3;六、排序(ORDER BY)
ORDER BY 用于对查询结果进行排序。
6.1 基本语法
SELECT 字段 FROM 表名 ORDER BY 排序字段 [ASC|DESC];ASC:升序(默认),DESC:降序。
6.2 实战案例
-- 1. 按评分降序排列 SELECT name, score FROM rider ORDER BY score DESC; -- 2. 按入职日期升序排列 SELECT name, join_date FROM rider ORDER BY join_date ASC; -- 3. 多字段排序:先按城市升序,再按评分降序 SELECT name, city, score FROM rider ORDER BY city ASC, score DESC;七、分页(LIMIT)
LIMIT 用于限制查询结果的数量,常用于分页查询。
7.1 基本语法
SELECT 字段 FROM 表名 LIMIT 偏移量, 记录数;或者:
SELECT 字段 FROM 表名 LIMIT 记录数 OFFSET 偏移量;7.2 实战案例
-- 1. 查询前5条记录 SELECT * FROM rider LIMIT 5; -- 2. 查询第6-10条记录(每页5条,第2页) SELECT * FROM rider LIMIT 5, 5; -- 3. 按评分降序排列,取前3名 SELECT name, score FROM rider ORDER BY score DESC LIMIT 3;八、多表关系设计
实际业务中,数据通常分布在多张表中,需要通过关联查询获取完整信息。
8.1 表关系类型
- 一对一:一个表中的一条记录对应另一个表中的一条记录
- 一对多:一个表中的一条记录对应另一个表中的多条记录
- 多对多:需要中间表来建立关联
8.2 创建关联表
继续使用外卖场景,创建订单表:
CREATE TABLE orders ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID', order_no VARCHAR(20) NOT NULL UNIQUE COMMENT '订单号', rider_id INT UNSIGNED COMMENT '骑手ID', amount DECIMAL(10,2) NOT NULL COMMENT '订单金额', status TINYINT UNSIGNED DEFAULT 1 COMMENT '状态: 1待接单 2配送中 3已完成 4已取消', create_time DATETIME NOT NULL COMMENT '创建时间', FOREIGN KEY (rider_id) REFERENCES rider(id) ON DELETE SET NULL ) COMMENT '订单表';8.3 关联查询(JOIN)
JOIN 用于将多个表中的数据关联起来。
8.3.1 INNER JOIN(内连接)
只返回两个表中匹配的记录。
-- 查询骑手及其配送的订单 SELECT r.name AS 骑手姓名, o.o