3. SQL查询语句的优化技巧
说实话,我排查过上百个OLAP慢查询,十有八九的问题都出在SQL本身。很多人一上来就调参数、加机器,其实先把SQL写好,性能能提升一大截。这一章我就把自己常用的优化套路分享给你。
核心原则:OLAP的SQL优化,本质上是减少数据扫描量和计算量。别让数据库做它不该做的事。
3.1 分区裁剪:别扫全表
我最常看到的问题就是——查询没带分区条件。你想想看,一张表几百个分区,全扫一遍,不慢才怪。
-- ❌ 错误写法:全表扫描
SELECT user_id, SUM(amount)
FROM order_fact
WHERE dt >= '2024-01-01';
-- ✅ 正确写法:指定分区键
SELECT user_id, SUM(amount)
FROM order_fact
WHERE dt >= '2024-01-01'
AND province_id = '110000'; -- 分区键
我的习惯:写查询前,先用 SHOW PARTITIONS table_name 看一眼分区结构。确保WHERE条件里包含了高基数的分区键。
3.2 列裁剪:别用 SELECT *
嗯,这个错误太普遍了。我记得有一次帮一个团队排查,他们的查询跑了20分钟,我改成只选需要的列,直接降到3分钟。
-- ❌ 错误写法:读取所有列
SELECT * FROM user_profile
WHERE register_date > '2024-01-01';
-- ✅ 正确写法:只读需要的列
SELECT user_id, user_name, register_date
FROM user_profile
WHERE register_date > '2024-01-01';
为什么会这样?因为OLAP大多是列式存储,你少选一列,它就少读一列的数据。数据量大的时候,差别是几何级的。
3.3 聚合下推:先过滤再聚合
这个技巧说白了就是——能提前过滤的数据,别等到聚合完再过滤。我在项目中遇到过好几次,有人把过滤条件写在HAVING里,导致聚合计算量暴增。
-- ❌ 错误写法:先聚合再过滤
SELECT user_id, COUNT(*) AS cnt
FROM click_log
GROUP BY user_id
HAVING cnt > 1000;
-- ✅ 正确写法:先过滤再聚合
SELECT user_id, COUNT(*) AS cnt
FROM click_log
WHERE event_time > '2024-06-01'
GROUP BY user_id
HAVING cnt > 1000;
避坑指南:我曾经见过一个查询,WHERE条件里写了子查询,结果子查询没走分区裁剪,导致外层也扫了全表。记住:子查询也要做分区裁剪和列裁剪。
3.4 JOIN优化:小表驱动大表
OLAP里JOIN是重灾区。我个人的经验是,能用Broadcast Join就别用Shuffle Join。说白了,就是把小表广播到每个节点,避免数据shuffle。
| JOIN类型 | 适用场景 | 性能特点 |
|---|---|---|
| Broadcast Join | 小表 < 10MB | 无shuffle,极快 |
| Shuffle Join | 大表对大表 | 有shuffle,较慢 |
| Bucket Join | 两表都已分桶 | 局部JOIN,中等 |
-- 显式指定Broadcast Join(以Spark SQL为例)
SELECT /*+ BROADCAST(dim_user) */
f.order_id, u.user_name, f.amount
FROM order_fact f
JOIN dim_user u ON f.user_id = u.user_id;
我的建议:写JOIN之前,先评估一下表的大小。如果小表超过几百MB,Broadcast反而会撑爆内存。这时候可以考虑先对小表做一次聚合或过滤。
3.5 避免数据倾斜
数据倾斜是OLAP的隐形杀手。我记得有一次排查一个跑了6小时的查询,发现某个key的数据量是其他key的100倍,所有计算都压在一个节点上。
常见的解决思路:
- 加盐(Salting):给热点key加随机后缀,分散到多个reduce
- 两阶段聚合:先局部聚合,再全局聚合
- 调整并行度:适当增大reduce数量
-- 两阶段聚合示例(解决count distinct倾斜)
-- 第一阶段:加随机前缀,局部聚合
SELECT prefix, city, COUNT(DISTINCT user_id) AS partial_cnt
FROM (
SELECT
CONCAT(CAST(RAND()*10 AS INT), '_', city) AS prefix,
city,
user_id
FROM user_log
WHERE dt = '2024-06-01'
) t
GROUP BY prefix, city;
-- 第二阶段:去掉前缀,全局聚合
SELECT city, SUM(partial_cnt) AS total_cnt
FROM stage1_result
GROUP BY city;
3.6 善用物化视图
如果你的查询模式很固定,比如每天都要跑一次月度汇总,那就别每次都从头算。物化视图就是预计算好的结果,查询时直接读。
-- 创建物化视图(以ClickHouse为例)
CREATE MATERIALIZED VIEW mv_monthly_sales
ENGINE = SummingMergeTree
PARTITION BY toYYYYMM(sale_date)
ORDER BY (product_id, sale_date)
AS SELECT
product_id,
toStartOfMonth(sale_date) AS sale_month,
sum(amount) AS total_amount,
count(*) AS order_count
FROM sales
GROUP BY product_id, sale_month;
注意:物化视图不是银弹。它会占用存储空间,而且数据更新有延迟。我建议只对高频、固定的查询创建物化视图。
3.7 查询执行计划分析
写完了SQL,怎么知道它快不快?看执行计划。我每次优化前都会先跑一遍EXPLAIN,看看是不是走了全表扫描、有没有数据倾斜、JOIN类型对不对。
-- 查看执行计划
EXPLAIN ANALYZE
SELECT user_id, SUM(amount)
FROM order_fact
WHERE dt = '2024-06-01'
GROUP BY user_id;
重点关注几个指标:
- 扫描行数:是不是比预期多?
- shuffle数据量:是不是很大?
- 每个stage的耗时:哪个stage最慢?
3.8 本章知识体系
下面这张图是我自己整理的SQL优化决策流程,每次遇到慢查询,我就按这个顺序排查:
这张图我用了很多年,每次排查都按这个顺序走,基本不会漏掉关键点。你把它记下来,遇到慢查询就照着做。
3.9 总结一下
SQL优化其实没那么玄乎。核心就三件事:少读数据、少算数据、别让数据倾斜。你把这三点刻在脑子里,写出来的SQL不会差到哪里去。