捋一捋到底SQL慢了是哪里的原因,说SQL需要优化的可以退下了
❝开头还是介绍一下群,如果感兴趣PolarDB ,MongoDB ,MySQL ,PostgreSQL ,Redis, OceanBase, Sql Server等有问题,有需求都可以加群加群请联系 liuaustin3 ,(共3400人左右 1 + 2 + 3 + 4 +5 + 6 + 7 + 8 )(1 2 3 4 5 6 7 8群已经爆满 9群 为纯聊天群,默认不加入不得发广告,自己公众号文章链接等,发一次直接踢,默认加入8群,开10群PolarDB专业学习群115+)
今天我们将开启一个新的系列,我想探究一些数据库更深层次运行的原理。
摘要 当一条 SQL 突然慢下来,绝大多数人的第一反应是「SQL 写得不好」。但真正棘手的问题往往不是 SQL 本身,而是数据库内核在统计信息、基数估算、JOIN 顺序、执行计划、MVCC、存储引擎这一整条链路上某一步「猜错了」。
1「数据库为什么会变慢?我们用一个故事开始: 某天凌晨,监控告警炸了。一条跑了三年的报表 SQL,平时只要 200ms,突然变成了 45 秒。
开发同学第一反应是:「SQL 我又没动过,数据量也才涨了 5%,怎么就慢成这样?」
排查链路非常典型:
索引还在?✅ 执行计划变了?❌(变成 Nested Loop 了,原来是 Hash Join)
统计信息是新的?❌(ANALYZE 还没跑)
为什么计划变了?❌(基数估算从「100 万」变成了「100」)
看到这里你大概已经明白:问题不在 SQL,而在优化器「猜错了基数」。一条 SQL 的快与慢,从你按下回车那一刻起,就被一连串内核决策决定了。这条决策链,就是本文要拆解的对象。
SQL 文本
↓
解析 / 重写
↓
统计信息
↓
基数估算 (Cardinality Estimation)
↓
成本模型 (Cost Model)
↓
JOIN 顺序 / JOIN 算法
↓
执行计划
↓
执行器
↓
存储引擎 (Heap / B-tree)
↓ MVCC
↓
IO / Buffer
02 统计信息:优化器的「天气预报」 PostgreSQL 优化器在生成执行计划时,并不真正去扫描数据——它没法承担那个代价。它依赖的是 pg_statistic 里的一堆「统计摘要」:
MVCC 表的 行数估计(reltuples)
直方图(most_common_vals / histogram_bounds)
distinct 值数量(ndistinct)
列的相关性(correlation,影响索引扫描代价估算)
-- 看一张表目前的统计快照
SELECT relname, relpages, reltuples
FROM pg_class
WHERE relname = 'orders';
-- 看某列的直方图与 MCV
SELECT attname, n_distinct,
most_common_vals, most_common_freqs,
histogram_bounds
FROM pg_stats
WHERE tablename = 'orders' AND attname = 'status';
关键点:统计信息是一个「采样摘要」,它永远不等于真实数据分布。当数据分布倾斜、或统计信息过期,优化器就拿着一份错的「地图」去做决策。
注意 default_statistics_target 默认是 100,意味着直方图只有 100 个桶。如果某列数据高度倾斜(例如订单状态列 99% 是 'PAID'),100 个桶可能根本拟合不出真实分布。
统计信息为什么会失真?
大批量 UPDATE / INSERT 后还没 ANALYZE autovacuum 触发条件没达到(默认 analyze_scale_factor = 0.1,10% 才会自动分析);
数据倾斜严重,默认采样桶数不够;
跨列相关性无法表达(ndistinct 只统计单列,多列组合时优化器只能假设独立);
函数表达式结果分布没有统计(PG 14 之后的 extended statistics 才部分解决)。
统计信息一旦失真,下一步——基数估算——就开始出错。
基数估算:数据库最危险的一步 基数估算(Cardinality Estimation)回答的是:"这个过滤条件之后,还剩多少行?"
SELECT *
FROM orders
WHERE status = 'PAID'
AND created_at >= '2026-01-01';
优化器需要猜:status = 'PAID' 剩多少行?created_at >= '2026-01-01' 剩多少行?两个条件组合之后剩多少行?(假设独立 → 相乘)这一步是整个链路最危险的,因为后面所有的决策都建立在它之上.
错误基数估算
↓
错误成本估算
↓
错误 JOIN 顺序
↓
错误 JOIN 算法
↓
错误执行计划
↓
SQL 突然变慢
一个典型翻车场景:
实际:100 万行
估算:100 行
优化器选了 Nested Loop + Index Scan(适合小结果集)实际跑出来:100 万次索引回表,磁盘随机 IO 爆炸核心问题 基数估算错一点,后面全部错。这不是"SQL 写得不好",而是优化器拿着一张错地图导航。
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE status = 'PAID'
AND created_at >= '2026-01-01';
--如果 rows=100 而 actual rows=1000000,那就是基数估算翻车了。
PostgreSQL 的基数估算逻辑分布在 costsize.c 里,主要函数是 set_baserel_size_estimates() 和 estimate_selectivity()。它本质上是一套基于统计摘要和独立性假设的公式。
多列相关的查询(例如 WHERE country = 'CN' AND city = 'Beijing')会让"独立性假设"彻底失效——这是 PG 14 引入 CREATE STATISTICS 扩展统计的根本原因。
04 JOIN 顺序:为什么 10 张表 JOIN 还行,30 张表就崩了?
当基数估算出来的"每张表剩下多少行"交给优化器之后,下一个问题是:N 张表 JOIN,应该按什么顺序连?
这个问题看似无害,实则是 PostgreSQL 优化器最复杂的部分之一。N 张表 JOIN,理论上有 N! 种排列方式(如果不考虑 JOIN 树形状)。这个增长速度极其恐怖——它是阶乘级,比指数还猛
动态规划:PostgreSQL 的默认策略当表数量较少时,PG 用动态规划(DP)枚举所有可能的 JOIN 顺序,并为每种组合算一个代价,最后挑最便宜的。
DP 的核心思想是:
把"找 N 张表最优 JOIN 顺序"拆成"找每张子集的最优顺序";子问题的解可以复用;复杂度大约是 O(3^N)(实际还要看 JOIN 图形状)。 动态规划的好处是精确——它一定能找到"代价模型下"的最优计划。坏处是当 N 一大,复杂度爆炸。当 JOIN 的表数超过一个阈值,PostgreSQL 会自动切换到 GEQCO(Genetic Query Optimization)——一种基于遗传算法的启发式搜索。
这个阈值由参数控制:
geqo_threshold = 12 -- 默认值:JOIN 表数 ≥ 12 时切到 GEQO
GEQO 不再穷举,而是用遗传算法在搜索空间里"随机游走 + 优胜劣汰",找到一个"还不错"的 JOIN 顺序就交差。GEQO 的代价 它不保证最优。也就是说,同样一条 SQL,两次执行计划可能不同。这是为什么很多 DBA 在生产环境直接把 geqo 关掉或者调高 geqo_threshold。
建一个有 20~30 张表的 JOIN,对比 geqo=on vs geqo=off,可以明显看到: 计划生成时间差异巨大(DP 慢但精确,GEQO 快但不稳定) 实际执行时间可能相差 10 倍以上 两次 EXPLAIN 输出可能不同(GEQO 是随机的)
那么GEQO怎么调,GEQO是什么,怎么用,咱们下期继续,
PostgreSQL 版本升级方法总结,具体pg_upgrade怎么操作
PostgerSQL 14-17备份的变化 PG17更贴近商业数据库 与 实际命令
PostgreSQL 怎么用好高版本的PG调优--PG14-PG18
同学问 PG17 的备份比老的版本 好哪了? 你给总结总结 !!
算法领主与数据农奴:AI时代的不能说的问题-- 此文为AI临时工所做与公众号作者无关
从亚马逊 AGI 部门裁员看 AI 商业逻辑的必然转向 -- 资本不会给AGI 半点脸
比起简单的Skill技能,我更想建立Agent Skill的系统思维能力--- 感谢本书作者答疑解惑
MongoDB 全文索引 与 展示查询数据的一部分,提高性能
体现价值-我们靠PostgreSQL迁移PolarDB,给公司省下了100万 “巨款”
《告别迁移焦虑:OceanBase MySQL 模式能否兼容 DBA 的“祖传”运维 SQL?》
干数据库不是买白菜:光盯着License几毛钱,看不见300台机器的电费?
一个秘密,不是你 SQL 写对了,是优化器帮“擦了屁股” 客户问迁移后为什么快了--迁移到PolarDB后的故事
AI 引入后,MySQL 列权限控制,插入,更新,读取,删除 --有了AI 真是越帮越忙
PostgreSQL 大表改字段卡死的问题解决了吗? 解决了方案在此
AI 引入DBA 工作,造成工作量增加,忙不过来,根本忙不过来!!!
三无项目导致MongoDB 持续1406% CPU 问题解决