📖 本篇简介:如果说 DML 是"写",那SELECT就是数据库里出现频率最高的"读"。本篇带你从最基础的"查所有列"开始,逐步掌握列选择、列别名、DISTINCT去重,以及WHERE子句的完整用法——包括比较运算符、逻辑组合、IN/BETWEEN/LIKE三大过滤利器。这一篇是后面所有复杂查询(排序、聚合、多表连接、子查询)的地基。本示例基于 MySQL 8.0,所有 SQL 均按该版本语法书写。
目录
- 1 SELECT 概述与语法结构
- 2 列选择与列别名
- 3 DISTINCT 去重
- 4 WHERE 过滤基础与比较运算符
- 5 逻辑运算符 AND/OR/NOT
- 6 IN / BETWEEN / LIKE 三大过滤
- 7 NULL 值的特殊处理
- 8 SELECT 常用速查表
- 9 常见坑与注意事项
- 10 练习题
1 SELECT 概述与语法结构
SELECT 是 SQL 里最灵活、写法最多的语句。它的完整语法树大致是这样:
-- SELECT 完整语法骨架(8.0 支持 CTE、窗口函数,本篇先不讲)
SELECT [ALL | DISTINCT] 列列表
FROM 表
[WHERE 行过滤条件]
[GROUP BY 分组]
[HAVING 分组后过滤]
[ORDER BY 排序]
[LIMIT 分页];
本篇只覆盖 列选择 + WHERE 过滤 这两块,其他子句在后续章节展开。
最小可用的 SELECT:
-- 查 students 表的所有列、所有行
SELECT * FROM students;
* 代表"所有列"。生产环境禁止用 `*`,原因有三:
1. 表加了新列,应用代码拿到的字段就变多了,可能崩溃;
2. 网络传输浪费(比如一张表有 TEXT 大字段,你根本不需要);
3. 优化器无法使用覆盖索引(后面索引章节会讲)。
正确写法是显式列出需要的列:
-- 只查需要的列
SELECT id, name, city, score FROM students;
2 列选择与列别名
2.1 字面量与表达式
SELECT 后面不只是表的列,还可以是字面量、表达式、函数调用:
-- 查学生姓名,同时查一个固定字符串、一个计算字段
SELECT
name,
'在读' AS status, -- 字面量字符串
age * 12 AS months, -- 表达式:年龄换算成月数
NOW() AS query_time -- 函数调用
FROM students;
这种"在 SELECT 列表里做计算"的能力,是报表查询的基础。
2.2 列别名 AS
AS 用来给列起一个好读的名字。下面三种写法等价:
-- 写法 1:显式 AS(推荐,最清晰)
SELECT name AS 姓名, score AS 分数 FROM students;
-- 写法 2:省略 AS
SELECT name 姓名, score 分数 FROM students;
-- 写法 3:别名含空格/中文特殊字符时,必须用反引号或双引号包起来
SELECT name `学生姓名`, score `考试分数` FROM students;
别名一旦定义,就不能在 WHERE 里用,只能在 ORDER BY、HAVING 里用。原因是 SQL 执行顺序:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
WHERE 在 SELECT 之前执行,此时别名还不存在。后面会反复用到这个执行顺序。
2.3 完全独立于表的 SELECT
MySQL 允许 SELECT 后面不写 FROM,用来做"纯计算":
-- 不查表,直接算
SELECT 1 + 1 AS sum; -- 2
SELECT NOW() AS current_time; -- 当前时间
SELECT VERSION() AS mysql_version; -- 数据库版本
这种写法在调试、快速验证函数行为时非常方便。
3 DISTINCT 去重
3.1 基本用法
DISTINCT 紧跟在 SELECT 后面,对整行组合去重:
-- 查 students 表中出现过哪些城市(重复城市只显示一次)
SELECT DISTINCT city FROM students;
如果不加 DISTINCT,每个学生的城市都会出现一次,北京有 100 个学生就出现 100 次。加了之后北京只出现一次。
3.2 多列组合去重
DISTINCT 是对"后面所有列的组合"去重,不是对每一列单独去重:
-- 查"城市 + 性别"的所有组合,组合相同的只留一行
SELECT DISTINCT city, gender FROM students;
比如北京有男有女,会出现 (北京,M)、(北京,F) 两行。不能理解成"北京出现一次、M 出现一次"。
3.3 DISTINCT 与 COUNT
配合 COUNT() 可以统计"去重后的行数":
-- 统计有多少个不同的城市
SELECT COUNT(DISTINCT city) AS city_count FROM students;
注意 COUNT(*) 是数行数,COUNT(DISTINCT city) 是数 city 不重复的个数。
4 WHERE 过滤基础与比较运算符
WHERE 决定"哪些行进入结果集"。它放在 FROM 之后、GROUP BY 之前。
4.1 比较运算符
| 运算符 | 含义 | 例子 |
|---|---|---|
| = | 等于 | score = 90 |
| <> 或 != | 不等于 | age <> 18 |
| > / < | 大于 / 小于 | score > 60 |
| >= / <= | 大于等于 / 小于等于 | age >= 20 |
-- 查成绩大于 90 分的学生
SELECT name, score FROM students WHERE score > 90;
-- 查不是北京的学生
SELECT name, city FROM students WHERE city <> '北京';
字符串比较默认是不区分大小写的(取决于列的 collation,MySQL 默认 utf8mb4_general_ci,_ci = case insensitive)。如果你要区分大小写,用 BINARY 关键字:
-- 区分大小写比较
SELECT * FROM students WHERE BINARY city = 'Beijing';
4.2 算术与表达式
WHERE 里也可以写表达式:
-- 查年龄大于 20 岁(也就是出生年份早于 2005 年)的学生
SELECT name, age FROM students WHERE age > 20;
-- 查"成绩 * 10"大于 800 的(等价于成绩大于 80)
SELECT name, score FROM students WHERE score * 10 > 800;
不过这种"对列做运算"的写法通常用不到索引(后面索引章节会讲),尽量把运算放到右边常量上。
5 逻辑运算符 AND/OR/NOT
5.1 三种逻辑运算符
| 运算符 | 含义 |
|---|---|
| AND | 两边都为真才为真 |
| OR | 任意一边为真就为真 |
| NOT | 取反 |
-- 查"北京的、成绩大于 80 的男学生"
SELECT name, city, score
FROM students
WHERE city = '北京' AND score > 80 AND gender = 'M';
5.2 AND 与 OR 的优先级
AND 优先级高于 OR。下面两种写法结果完全不同:
-- 写法 A:(北京 AND 成绩>80) OR (上海)
-- 意思是:北京的高分学生,或者上海的所有学生
SELECT * FROM students
WHERE city = '北京' AND score > 80 OR city = '上海';
-- 写法 B:北京 AND (成绩>80 OR 上海)
-- 意思是:北京的、并且(成绩>80 或 城市是上海)
-- 因为 city 不可能同时是北京和上海,所以结果等价于"北京的高分学生"
SELECT * FROM students
WHERE city = '北京' AND (score > 80 OR city = '上海');
实战建议:当条件同时混着 AND 和 OR,一律用括号包起来,别依赖优先级记忆。
5.3 IN 是 OR 的简写
-- 等价写法 A:OR 串联
SELECT * FROM students WHERE city = '北京' OR city = '上海' OR city = '广州';
-- 等价写法 B:IN(推荐,更短更清晰)
SELECT * FROM students WHERE city IN ('北京', '上海', '广州');
6 IN / BETWEEN / LIKE 三大过滤
6.1 IN:集合成员判断
IN 后面跟一个列表,值在列表里就为真:
-- 查来自北上广的学生
SELECT name, city FROM students
WHERE city IN ('北京', '上海', '广州');
-- 取反:不在列表里
SELECT name, city FROM students
WHERE city NOT IN ('北京', '上海', '广州');
IN 也可以接子查询(后面第 8 章会讲),但本章先记住"后面接常量列表"的用法。
6.2 BETWEEN:区间过滤
BETWEEN ... AND ... 是闭区间,包含两端:
-- 查成绩在 60 到 90 之间的学生(含 60 和 90)
SELECT name, score FROM students
WHERE score BETWEEN 60 AND 90;
-- 等价于
WHERE score >= 60 AND score <= 90;
日期也能用:
-- 查 2025 年 9 月入学的学生
SELECT name, enroll_date FROM students
WHERE enroll_date BETWEEN '2025-09-01' AND '2025-09-30';
注意:BETWEEN 用于日期时,'2025-09-30'实际是2025-09-30 00:00:00,如果你列里存的是 datetime,9 月 30 号白天的数据会被漏掉。建议改成>= '2025-09-01' AND < '2025-10-01'。
6.3 LIKE:模糊匹配
LIKE 配合两个通配符:
| 通配符 | 含义 | 例子 |
|---|---|---|
| % | 任意长度(含 0)的任意字符 | '张%' 姓张的 |
| _ | 恰好一个任意字符 | '张_' 姓张且名字 2 个字的 |
-- 姓张的学生
SELECT name FROM students WHERE name LIKE '张%';
-- 名字里包含"三"的
SELECT name FROM students WHERE name LIKE '%三%';
-- 名字恰好 2 个字、姓张的
SELECT name FROM students WHERE name LIKE '张_';
-- 取反:名字不以张开头
SELECT name FROM students WHERE name NOT LIKE '张%';
性能提醒:以 % 开头的 LIKE(比如 '%三%')用不到索引,因为不知道从哪开始。大表上这种查询会全表扫描,生产环境要警惕。后面全文索引、搜索引擎章节会讲怎么优化。
7 NULL 值的特殊处理
NULL 是 SQL 里最容易踩坑的概念。它代表"未知"或"不存在",不等于空字符串、不等于 0、不等于任何值。
7.1 判断 NULL 必须用 IS NULL
-- 错误:= NULL 永远返回 NULL(不是真),查不出任何行
SELECT * FROM students WHERE gender = NULL;
-- 正确:用 IS NULL
SELECT * FROM students WHERE gender IS NULL;
-- 取反:IS NOT NULL
SELECT * FROM students WHERE gender IS NOT NULL;
7.2 NULL 参与运算的结果
任何值与 NULL 做算术、比较,结果都是 NULL:
-- age 是 NULL 时,age + 1 还是 NULL
SELECT NULL + 1; -- NULL
SELECT NULL = NULL; -- NULL(不是 TRUE!)
SELECT NULL <> NULL; -- NULL
这就是为什么 = NULL 查不到东西——它返回 NULL,被 WHERE 当成"不通过"。
7.3 NULL 与 IN/NOT IN 的坑
-- 如果子查询里有 NULL,NOT IN 会返回空结果集(经典坑)
SELECT * FROM students
WHERE city NOT IN (SELECT city FROM other_table);
-- 如果 other_table.city 有一行是 NULL,整个 NOT IN 结果是空
后面子查询章节会专门讲这个坑,先记住:NOT IN 遇到 NULL 全灭。
7.4 用 IFNULL / COALESCE 处理 NULL
-- IFNULL:如果第一个参数是 NULL,返回第二个
SELECT name, IFNULL(gender, '未知') AS gender_display FROM students;
-- COALESCE:返回第一个非 NULL 的参数(更通用)
SELECT name, COALESCE(gender, city, '未知') AS info FROM students;
8 SELECT 常用速查表
| 需求 | 推荐写法 | 备注 |
|---|---|---|
| 查所有列 | SELECT * FROM t; | 生产禁用 |
| 查指定列 | SELECT c1,c2 FROM t; | 推荐 |
| 列起别名 | SELECT c AS 别名 FROM t; | 别名只在 ORDER BY/HAVING 可用 |
| 去重 | SELECT DISTINCT c FROM t; | 对整行组合去重 |
| 去重计数 | SELECT COUNT(DISTINCT c) FROM t; | 统计不重复值个数 |
| 等值过滤 | WHERE c = v | = 不是 == |
| 不等过滤 | WHERE c <> v | 也支持 != |
| 集合过滤 | WHERE c IN (v1,v2) | 等价多个 OR |
| 区间过滤 | WHERE c BETWEEN a AND b | 闭区间 |
| 前缀匹配 | WHERE c LIKE '张%' | 能用到索引 |
| 包含匹配 | WHERE c LIKE '%三%' | 全表扫描 |
| 判断空 | WHERE c IS NULL | 不能 = NULL |
| 空值替换 | IFNULL(c, 默认值) | 或 COALESCE |
9 常见坑与注意事项
坑 1:生产环境用 `SELECT *`。
表加列后应用崩溃、网络浪费、索引用不上。务必显式列名。
坑 2:WHERE 里用列别名。
执行顺序上 WHERE 早于 SELECT,别名还不存在,会报"Unknown column"。
坑 3:AND/OR 混写不加括号。
优先级问题导致结果与预期不符。一律加括号。
坑 4:`= NULL` 查不到数据。
必须用 IS NULL。
坑 5:LIKE 以 `%` 开头导致全表扫描。
大表慎用,考虑全文索引或搜索引擎。
坑 6:BETWEEN 用于 datetime 时漏掉当天数据。
BETWEEN '2025-09-01' AND '2025-09-30' 实际只到 30 号零点,改成 < '2025-10-01'。
坑 7:NOT IN 子查询包含 NULL 时结果为空。
经典坑,后面子查询章节会展开。
坑 8:字符串比较意外区分大小写。
默认 _ci 不区分;要区分时用 BINARY。
坑 9:DISTINCT 作用于多列组合,不是单列去重。
SELECT DISTINCT a,b 是对 (a,b) 组合去重。
10 练习题
练习 1:基础列选择
题目:查询 students 表中所有学生的姓名、城市、成绩,列名显示为"姓名""城市""分数"。
<details>
<summary>参考答案</summary>
SELECT name AS 姓名, city AS 城市, score AS 分数
FROM students;
</details>
练习 2:多条件组合
题目:查询"北京或上海的、成绩大于 80 分的女学生"。
<details>
<summary>参考答案</summary>
SELECT name, city, score
FROM students
WHERE (city = '北京' OR city = '上海')
AND score > 80
AND gender = 'F';
</details>
练习 3:IN 与 BETWEEN 改写
题目:把 WHERE city='北京' OR city='上海' OR city='广州' 改成 IN 写法;把 WHERE score >= 60 AND score <= 90 改成 BETWEEN 写法。
<details>
<summary>参考答案</summary>
-- IN 改写
WHERE city IN ('北京', '上海', '广州');
-- BETWEEN 改写
WHERE score BETWEEN 60 AND 90;
</details>
练习 4:模糊查询
题目:查询姓名包含"张"字、且城市不是"北京"的学生。
<details>
<summary>参考答案</summary>
SELECT name, city
FROM students
WHERE name LIKE '%张%'
AND city <> '北京';
</details>
练习 5:NULL 处理
题目:查询性别字段还没填(NULL)的学生,并把查询结果中的 gender 显示为"未填写"。
<details>
<summary>参考答案</summary>
SELECT name, IFNULL(gender, '未填写') AS gender_display
FROM students
WHERE gender IS NULL;
</details>
练习 6:去重计数
题目:统计 students 表里一共有多少个不同的城市。
<details>
<summary>参考答案</summary>
SELECT COUNT(DISTINCT city) AS city_count
FROM students;
</details>
下一篇预告:第 5 章 排序与分页
文章回复
0 条公开回复