以下内容有AI辅助整理,介意的话,勿看。
一、先说ClickHouse
1. ORDER BY 跟查询条件对着干
先看一个例子。某个表,如果建表语句是这样的,然后业务方经常按 event_type 查,会有什么问题?
-- 建表时的排序键ORDER BY (timestamp, user_id, event_type)-- 业务方的查询SELECT count() FROM events WHERE event_type = 'login'
ClickHouse 的索引不是 B+树,是稀疏索引。它会把你写入的数据按 ORDER BY 的列顺序排序,然后每 8192 行记录一个"标记"。查询的时候,必须先匹配 ORDER BY 的第一列,才能用到索引。
上面的例子,ORDER BY 的第一列是 timestamp,但查询条件里根本没有 timestamp——只有 event_type。那 ClickHouse 就傻了:我不知道 timestamp 的范围,没法用索引定位,只能把所有数据扫一遍。
怎么改?把高频查的列放前面:
ORDER BY (event_type, timestamp, user_id)同一个查询,改了之后可能只扫几千个颗粒(granule),而不是全表几百万行。效果就是几十秒变成零点几秒。
联合索引/排序键的原理都一样——查询条件不命中第一列,后面的列全白瞎。

2. 分区太大或太小都不行
一个按天分区的表,单天 5 亿条日志。业务方查"最近一周的数据":
SELECT count() FROM logsWHERE log_date BETWEEN '2025-07-05' AND '2025-07-11'
看起来带了时间条件没错,但每一天是一个分区,分区里面有 5 亿行。就算只扫 7 个分区,加起来也要扫 35 亿行。毛估估也得跑几十秒。
反过来,如果按小时分区呢?一天的日志数据变成 24 个分区,一个月就 720 个分区。分区粒度过细也有问题——ClickHouse 记录每个分区的元数据,分区太多了,光是"确定要查哪些分区"就要花不少时间。可以理解成要在一千个文件夹里找一个文件,光翻目录都翻半天。
建议:日志类数据按天分区基本够用;业务数据(日增百万级)按月。
3. 后台有人在搞事情——Mutation 和 Merge
你正查着数据,感觉忽然比平时慢了好几倍。一查 system.mutations:
SELECT database, table, command, parts_to_do, is_doneFROM system.mutationsWHERE is_done = 0
结果有一条:
database | table | command | parts_to_do | is_done---------|------------|----------------------------------|-------------|--------mydb | events | DELETE WHERE id % 100 = 0 | 1500 | 0
什么意思?有人在跑一个 DELETE,需要重写 1500 个 part 文件。每个 part 是一个独立的数据片段,重写一个 part 就是要把它全读出来,删掉符合条件的行,再写回去。1500 个 part 正在排队等处理,磁盘 IO 被占得七七八八。查询跟它抢磁盘,快不了。
另外还有一个场景——高频小写入。
想象一下:应用每秒调几百次 INSERT,每次都只写几行。每次 INSERT 都会在磁盘上生成一个新的 part 文件。一分钟后可能已经有几千个细碎的 part 了。
ClickHouse 后台有个 merge 线程,专门负责把这些小 part 合并成大 part。但 merge 要是跟不上写入速度,积压就越来越严重。这会导致两个问题:一是查询时要打开几千个小文件,文件句柄不够用;二是 merge 本身大量读写磁盘,相当耗资源。
一个典型的飙慢路径就是:小批量高频写入 → part 碎片堆积 → merge 满载 → 查询被挤压。
4. 内存不够,落到磁盘跑
举个 GROUP BY 的例子:
SELECT user_id, count() FROM events GROUP BY user_id假设 events 表有 5 亿行,user_id 的取值有几千万种。GROUP BY 要维护一个 hash 表,把每个 user_id 对应到一个计数器的内存地址。
ClickHouse 给单个查询分配的内存是有上限的(参数叫 max_bytes_before_external_group_by)。如果内存装不下这个 hash 表了,怎么办?它不会报错让你重来,而是把装不下的部分先临时写进磁盘,等你继续查的时候再从磁盘读回来。
内存在跑就是毫秒级的事。一旦开始读写临时文件,性能直接断崖式下跌。就好像:本来在桌子上摊开作业写,桌子不够大了,开始蹲在地上写,还得不停站起来去桌上拿这拿那。

怎么看有没有溢写:去 system.query_log 看,如果查询执行时间很长但没报 OOM,而且 peak_memory_usage 摸到配置的上限——大概在偷偷走磁盘。
ORDER BY、JOIN、DISTINCT 同理,内存满了就溢写,溢写就变慢。
5. 分布式集群——查询被"放大"了
连的是一个分布式表,这个表下面挂了 4 个分片(shard),每个分片上有一张本地表存实际数据。然后跑了一条:
SELECT count() FROM distributed_events这条查询的实际执行路径是:发起节点收到请求,把 SELECT count() 原封不动地发到 4 个分片上去执行。4 个分片各自扫完自己的本地表,返回一个数字。发起节点再把 4 个数字加起来给你。
数据量小的时候没事。但如果每个分片都存了几亿行,4 个分片同时全表扫,磁盘 IO 直接叠加——这个就叫"查询放大"。
怎么缓解:带分区键条件去查,或者用本地表直接查单个分片。
还有一个坑是 GLOBAL IN:
SELECT * FROM distributed_AWHERE user_id GLOBAL IN (SELECT user_id FROM distributed_B WHERE ...)
子查询的结果要先从各节点汇总到发起节点,再分发回各节点。如果子查询的结果集有几百万条……发起节点内存直接炸了。

二、关系型数据库
再看下MySQL这类关系型数据库,作为扩展。
1. 索引——看不见不代表它没失效
很多人觉得"加了索引就应该快"。但索引用不上的场景比较多:
对索引列做了函数
SELECT * FROM orders WHERE DATE(create_time) = '2025-07-11'create_time 列上明明有索引。但是用 DATE() 函数把它包了一层。MySQL 不认识 DATE(create_time) 等于什么,它必须把每一行的 create_time 都算一遍 DATE(),才能过滤。索引全程没用。
改成这样就走了:
SELECT * FROM ordersWHERE create_time >= '2025-07-11 00:00:00'AND create_time < '2025-07-12 00:00:00'
类型对不上——隐式转换
手机号字段 phone 是 varchar(20),你的查询写成:
SELECT * FROM users WHERE phone = 13800138000字符串列查数字——MySQL 内部会把 phone 列的值逐行转成数字再去比较。索引又废了。
前缀模糊
SELECT * FROM articles WHERE title LIKE '%面试%'B+树索引是从左往右匹配的。% 在最前面,MySQL 不知道从哪开始。除非上全文索引。

2. 分页翻到后面越翻越慢
翻页功能,前几页秒开,翻到一百多页开始转圈。原因:
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10MySQL 想要第 100 万行开始的 10 条数据。但它没有"直接跳到第 100 万行"的能力,只能从第 1 行开始数,数到 100 万行,丢掉,再取 10 条。相当于为了拿 10 条记录,扫了 100 万条。
3. 锁——不是查询慢,是被人堵住了
-- 事务A(不提交)BEGIN;UPDATE inventory SET stock = stock - 1 WHERE product_id = 12345;-- 跑去接水了……10分钟没回来-- 事务BSELECT * FROM inventory WHERE product_id = 12345 FOR UPDATE;-- 干等着,因为事务A拿着这行的排他锁
从监控上看,事务B的查询跑了 5 分钟还没完。但它不是真的在执行——它就是在排队等锁。这种"慢"用 EXPLAIN 分析是看不出来的。
长事务是锁竞争的温床。开了一个事务,中间写了几行没提交,后面的人全排着。
三、其他非关系型数据库
看看两个常见的NoSQL。
MongoDB —— 文档嵌套是双刃剑
假设把用户和它的订单存成一个文档:
{”user_id”: ”U001”,”name”: ”张三”,”orders”: [{ ”order_id”: ”O001”, ”amount”: 100, ... },{ ”order_id”: ”O002”, ”amount”: 200, ... },// ... 这个用户买了几万单,全塞在这个数组里]}
现在要查"所有用户中,有没有一笔订单金额是 999 元"。数据库必须把每个用户的整个文档(包括那个几万条订单的巨型数组)全部加载到内存里,再一条一条翻订单。一个用户就一个巨型文档,扫完全表,加载的数据量可能是真正需要的几百倍。
这就是"数据模型和查询方式不匹配"——存的时候想着"订单归用户管",但查的时候是按订单维度查的,模型就对不上了。
Redis——单线程怕重活
Redis 是单线程模型,所有命令排队处理。
KEYS order:*假设有 100 万个 key。KEYS 会遍历整个 key 空间进行正则匹配。执行期间,其他所有请求全部排队干等。生产环境跑这个命令,基本等同于自己给自己挂了一个几秒甚至十几秒的阻塞。
还有一种情况。一个 Hash 结构:
HSET user:info:10086 field1 val1 field2 val2 ... field5000000 val5000000然后某天执行:
HGETALL user:info:10086500 万个 field 的一次性拉取,网络传输加上内存复制,服务在这期间没法响应其他请求。
用 SCAN 替代 KEYS,用 HSCAN 替代 HGETALL 分批拿——思路一样,都是化整为零。
四、面试时怎么答
AI说了这么多,现场不可能全背啊。我的建议,也准备以后这样做:
第一步:接住语境
如果前面一直在聊你的项目,就说项目中用到的对应数据库,然后其他类型的数据库有不一样的地方,再展开说。比如我应该:"我的项目中主要用到ClickHouse,刚才我们也一直在聊这个数据库,慢SQL可能会是以下几种情况(总结性说):
1、ORDER BY 跟查询条件对着干;
2、分区设置不合理,太大或太小都不行;
3、后台在抢资源,Mutation 和 Merge;
4、内存不够了,落到磁盘跑;
5.、分布式集群,查询被"放大"了。"
第二步:挑两个有印象最深刻的
比如ClickHouse, 索引排序键和后台 mutation,各举一个能讲清楚的例子:"我们线上遇到过,ORDER BY 把 timestamp 放第一列,但大多数查询是按 event_type 查的,索引用不上,几十秒起步。还有就是有同事跑了一条 DELETE,后台重写了几千个 parts,那段时间全表的查询都慢了。"
第三步:补充知道的其他数据库类型
一笔带过其他类型的数据库,面试官感兴趣的话,会追问细节。最后加一句排查思路,体现下实战经验。
"其实除了ClickHouse,产品中还经常用到PostgreSQL,像关系型数据库,我了解到的,索引失效和深分页是比较常见的慢sql原因。NoSQL 的话,模型和查询不匹配会导致慢sql,比如 MongoDB 文档嵌套太深。一般排查的思路都可以归结为——先看是不是索引/键的问题,再看是不是别人在抢资源。"
End.希望有帮助。

夜雨聆风