先说点真话
做数据分析项目那会儿,有一次要从一张订单表里统计"一共有多少个客户下过单"。新人直接写了SELECT COUNT(客户id) FROM 订单表,结果数字比实际客户数多了好几倍——因为同一个客户下了多笔单,被重复算了。
还有一次,产品要看"最近 10 笔已付款订单",新人写了SELECT * FROM 订单表 WHERE 状态='已付款',结果一次性返回了几万行。
这两个问题的解法就两个字:去重(DISTINCT)和分页(LIMIT)。看着简单,但组合起来坑不少。今天一次讲透。
💡 「零基础学SQL」系列持续更新中,关注我不迷路,每篇都带标准练习数据,复制就能跑。
一、DISTINCT:去掉重复行
你会什么场景下用到它?
- 统计"公司有几个部门"——但员工表里部门名是重复的
- 看"有哪些客户下过单"——但订单表里同一个客户出现多次
- 检查"有没有重复数据"——先去重看看差多少
基本写法:
-- 意图:看看员工表里到底有几个部门SELECTDISTINCT部门FROM员工表;结果:
| 部门 |
|---|
| 技术部 |
| 销售部 |
不加 DISTINCT 的话,4 个员工会查出 4 行部门(技术部、技术部、销售部、销售部),加了就只留不重复的。
多列去重:
-- 意图:看看"每个部门有哪些不同工资档位"SELECTDISTINCT部门,工资FROM员工表;结果:
| 部门 | 工资 |
|---|---|
| 技术部 | 9000.00 |
| 销售部 | 9200.00 |
注意:DISTINCT 是对整行去重,不是只对第一列。(技术部, 9000)和(销售部, 9200)是两行不同的数据,都保留。
坑 1:DISTINCT 和 COUNT 搭配
想统计"有几个部门",很多人会写成两步:先 DISTINCT 查出来,再数行数。其实一步搞定:
-- 对:COUNT 里直接嵌 DISTINCTSELECTCOUNT(DISTINCT部门)AS部门数FROM员工表;-- 结果:2-- 错:这样写等于先 COUNT 再去重,语法不报错但逻辑完全不对SELECTDISTINCTCOUNT(部门)FROM员工表;-- 结果:4(先数了 4 行,再去重还是 4,毫无意义)记住:COUNT(DISTINCT 列)才是"去重后计数",DISTINCT COUNT(列)是无意义的写法。
坑 2:DISTINCT 对 NULL 的处理
-- 意图:看看任务表里备注字段有几种不同的值SELECTDISTINCT备注FROM任务表;结果:
| 备注 |
|---|
| 完成需求评审 |
| 修复线上bug |
| NULL |
DISTINCT 会把所有 NULL 当作同一个值,只保留一个 NULL 行。如果你不想让 NULL 出现在结果里,加个 WHERE 过滤:
SELECTDISTINCT备注FROM任务表WHERE备注ISNOTNULL;二、LIMIT:限制返回行数(分页)
你会什么场景下用到它?
- “只看前 3 条”——快速预览数据
- “分页显示,每页 2 条,看第 2 页”——列表展示
- “取工资最高的 2 个人”——和 ORDER BY 搭配
基本写法:
-- 意图:只看前 2 条员工数据SELECT姓名,部门,工资FROM员工表LIMIT2;结果:
| 姓名 | 部门 | 工资 |
|---|---|---|
| 张三 | 技术部 | 9000.00 |
| 李四 | 技术部 | 9000.00 |
分页写法(LIMIT + OFFSET):
-- 意图:每页 2 条,看第 2 页(跳过前 2 条)SELECT姓名,部门,工资FROM员工表LIMIT2OFFSET2;结果:
| 姓名 | 部门 | 工资 |
|---|---|---|
| 王五 | 销售部 | 9200.00 |
| 赵六 | 销售部 | 9200.00 |
公式:LIMIT 每页条数 OFFSET (页码-1) × 每页条数
第 1 页:LIMIT 2 OFFSET 0(OFFSET 0 可以省略)
第 2 页:LIMIT 2 OFFSET 2
第 3 页:LIMIT 2 OFFSET 4
坑 3:LIMIT 不加 ORDER BY = 每次结果可能不一样
-- 危险写法:不加 ORDER BY,数据库不保证返回顺序SELECT姓名,工资FROM员工表LIMIT2;-- 这次可能是张三李四,下次可能是王五赵六-- 安全写法:先排序再取前 NSELECT姓名,工资FROM员工表ORDERBY工资DESCLIMIT2;-- 结果一定是工资最高的 2 个人| 姓名 | 工资 |
|---|---|
| 王五 | 9200.00 |
| 赵六 | 9200.00 |
记住:LIMIT 必须和 ORDER BY 搭配才有稳定结果。不加 ORDER BY 的 LIMIT,结果顺序是"数据库心情决定的",每次可能不一样。
三、DISTINCT + LIMIT 组合:去重后取前 N
场景:想看"工资从高到低排,每个部门取第一个人"——这个需要窗口函数,后面单独讲。但"去重后取前 N 条"是可以的:
-- 意图:去重后的部门,只取第 1 个SELECTDISTINCT部门FROM员工表ORDERBY部门LIMIT1;-- 结果:技术部执行顺序:FROM → DISTINCT → ORDER BY → LIMIT。先去重,再排序,最后取前 N 条。
四、速查卡(建议收藏)
| 操作 | 写法 | 典型场景 |
|---|---|---|
| 去重查列 | SELECT DISTINCT 列 FROM 表 | 看有哪些不同的值 |
| 去重计数 | SELECT COUNT(DISTINCT 列) FROM 表 | 统计不重复的值有多少个 |
| 多列去重 | SELECT DISTINCT 列1, 列2 FROM 表 | 看组合不重复的行 |
| 取前N条 | SELECT ... FROM 表 LIMIT N | 预览/取Top N |
| 分页 | SELECT ... FROM 表 LIMIT N OFFSET M | 列表分页展示 |
| 排序后取前N | SELECT ... FROM 表 ORDER BY 列 LIMIT N | 取最高/最低的N条 |
三条铁律:
COUNT(DISTINCT 列)才是去重计数,不要写DISTINCT COUNT(列)- LIMIT 必须配 ORDER BY,不然结果顺序不稳定
- DISTINCT 把多个 NULL 当作同一个值,不想看 NULL 就加
WHERE 列 IS NOT NULL
五、练习题(先自己写,再看答案)
题目 1:统计订单表里有多少个不同的员工下过单。
题目 2:查询订单表里金额最高的 2 笔订单,显示订单id、订单金额、状态。
题目 3:查询任务表里备注字段有多少种不同的非空值。
题目 4:分页查询员工表,每页 2 条,按入职日期从早到晚排序,写出第 2 页的 SQL。
参考答案
题目 1:
SELECTCOUNT(DISTINCT员工id)AS下单员工数FROM订单表;-- 结果:3(员工1、员工2、员工3,员工4没下过单)题目 2:
SELECT订单id,订单金额,状态FROM订单表ORDERBY订单金额DESCLIMIT2;-- 结果:103(4000)、101(3000)题目 3:
SELECTCOUNT(DISTINCT备注)AS不同备注数FROM任务表WHERE备注ISNOTNULL;-- 结果:2("完成需求评审"和"修复线上bug",NULL被排除)题目 4:
SELECT姓名,部门,入职日期FROM员工表ORDERBY入职日期LIMIT2OFFSET2;-- 结果:李四(2020-07-15)、张三(2019-03-01)... 按日期排:王五(2018-01-10)、张三(2019-03-01)是第1页,李四(2020-07-15)、赵六(2021-05-20)是第2页你站哪队?
学 SQL 的时候,你是"先把所有语法过一遍再动手"型,还是"边写边查、踩坑了再记"型?
我当年是后者——踩了无数坑才记住COUNT(DISTINCT 列)和DISTINCT COUNT(列)的区别。评论区聊聊你学 SQL 的方式,我看看哪种人多。
从零数据分析· 十年数据分析经验 · 实战笔记
SQL 从入门到精通 28 篇持续更新中,每篇附标准练习数据,复制即运行。
关注我,把 SQL 学成肌肉记忆。
📋 系列目录:零基础学SQL:从入门到精通完整目录(28篇持续更新)
📖 上篇:篇04 WHERE条件总写错?AND/OR/IN一图搞懂
📖 下篇预告:篇06 数据增删改——INSERT/UPDATE/DELETE 的安全写法(别把生产库删了)
附录:标准练习数据(复制即可运行,全系列通用)
-- 全系列通用四张表,每次运行先 DROP 再 CREATE,不会报"表已存在"DROPTABLEIFEXISTS员工表;CREATETABLE员工表(员工idINTPRIMARYKEY,姓名VARCHAR(20),部门VARCHAR(20),工资DECIMAL(10,2),邮箱VARCHAR(50),手机VARCHAR(20),入职日期DATE);INSERTINTO员工表(员工id,姓名,部门,工资,邮箱,手机,入职日期)VALUES(1,'张三','技术部',9000.00,'zhangsan@demo.com','13800000001','2019-03-01'),(2,'李四','技术部',9000.00,'lisi@demo.com','13800000002','2020-07-15'),(3,'王五','销售部',9200.00,'wangwu@demo.com','13800000003','2018-01-10'),(4,'赵六','销售部',9200.00,'zhaoliu@demo.com','13800000004','2021-05-20');DROPTABLEIFEXISTS订单表;CREATETABLE订单表(订单idINTPRIMARYKEY,员工idINT,订单金额DECIMAL(10,2),下单时间DATETIME,付款时间DATETIME,状态VARCHAR(20));INSERTINTO订单表(订单id,员工id,订单金额,下单时间,付款时间,状态)VALUES(101,1,3000.00,'2026-01-10 10:00:00','2026-01-10 10:05:00','已付款'),(102,1,2500.00,'2026-02-15 14:00:00','2026-02-15 14:10:00','已付款'),(103,3,4000.00,'2026-03-20 09:30:00','2026-03-20 09:40:00','已付款'),(104,2,1500.00,'2026-04-05 16:00:00',NULL,'待付款');DROPTABLEIFEXISTS用户表;CREATETABLE用户表(用户idINTPRIMARYKEY,姓名VARCHAR(20),手机VARCHAR(20),邮箱VARCHAR(50),地址VARCHAR(100));INSERTINTO用户表(用户id,姓名,手机,邮箱,地址)VALUES(1,'张三','13900000001','zhangsan@demo.com','北京市朝阳区'),(2,'李四','13900000002','lisi@demo.com','上海市浦东新区'),(3,'王五','13900000003','wangwu@demo.com','广州市天河区');DROPTABLEIFEXISTS任务表;CREATETABLE任务表(任务idINTPRIMARYKEY,员工idINT,备注VARCHAR(100),状态VARCHAR(20));INSERTINTO任务表(任务id,员工id,备注,状态)VALUES(1,1,'完成需求评审','已完成'),(2,2,NULL,'进行中'),(3,3,'修复线上bug','已完成'),(4,4,NULL,'待分配');