EXPLAIN 命令家族和可视化工具、优化实战流程。
python
from manim import *
from manim_slides import Slide
# 配置中文字体,请根据系统环境调整
config.tex_template.add_to_preamble(r"\usepackage{ctex}")
config.tex_template.add_to_preamble(r"\usepackage{xcolor}")
class ExplainAnalysis(Slide):
"""PostgreSQL 执行计划分析专题幻灯片"""
def construct(self):
# 统一代码样式配置
code_config = {
"language": "sql",
"formatter_style": "fruity",
"background": "window",
"add_line_numbers": True,
"paragraph_config": {
"font": "Milky Han Mono SC",
"font_size": 20
}
}
# ---------- 标题页 ----------
title = Text("PostgreSQL 执行计划分析", font_size=48, color=BLUE)
subtitle = Text("第四专题:使用EXPLAIN深度优化", font_size=36, color=GRAY)
authors = Text("少查·快连·精索·常析·避坑", font_size=28, color=GREEN)
VGroup(title, subtitle, authors).arrange(DOWN, buff=0.5)
self.play(Write(title))
self.play(FadeIn(subtitle, shift=UP))
self.play(FadeIn(authors, shift=UP))
self.wait(1)
self.next_slide()
# 清除当前画面
self.clear()
# ---------- 1. EXPLAIN家族对比 ----------
explain_title = Text("1. EXPLAIN 命令家族", font_size=40, color=YELLOW).to_edge(UP)
self.play(Write(explain_title))
# EXPLAIN对比表格
explain_table = Table(
[["EXPLAIN", "显示预估执行计划", "快速查看计划,不执行SQL"],
["EXPLAIN ANALYZE", "实际执行并统计", "最准确,但会实际执行SQL"],
["EXPLAIN (BUFFERS)", "显示缓冲区使用", "分析IO和缓存命中"],
["EXPLAIN (TIMING)", "显示耗时细节", "默认开启,可关闭减少开销"],
["EXPLAIN (VERBOSE)", "显示详细信息", "输出列信息等额外内容"]],
col_labels=[Text("命令"), Text("作用"), Text("适用场景")],
include_outer_lines=True,
line_config={"stroke_width": 1, "color": GRAY},
element_to_mobject_config={"font_size": 22}
).scale(0.6).shift(UP*0.5)
self.play(Create(explain_table))
explain_note = Text(
"日常调优最常用: EXPLAIN ANALYZE + BUFFERS",
font_size=24, color=BLUE
).to_edge(DOWN).shift(UP*0.5)
self.play(Write(explain_note))
self.wait(2)
self.next_slide()
# ---------- 2. 执行计划关键字段 ----------
self.clear()
fields_title = Text("2. 执行计划关键字段解读", font_size=40, color=YELLOW).to_edge(UP)
self.play(Write(fields_title))
# 关键字段表格
fields_table = Table(
[["type", "访问方式", "Seq Scan(全表)/Index Scan(索引)/Hash Join等"],
["rows", "预估返回行数", "实际与预估偏差大时统计信息可能过时"],
["width", "平均行宽度(字节)", "影响IO和内存开销"],
["cost", "启动成本..总成本", "成本单位(磁盘页读取次数)"],
["actual time", "实际执行时间(ms)", "仅EXPLAIN ANALYZE显示"],
["loops", "节点执行次数", "多次执行时总时间要乘以loops"]],
col_labels=[Text("字段"), Text("含义"), Text("说明")],
include_outer_lines=True,
line_config={"stroke_width": 1, "color": GRAY},
element_to_mobject_config={"font_size": 22}
).scale(0.55).shift(UP*0.5)
self.play(Create(fields_table))
self.wait(2)
self.next_slide()
# ---------- 3. type字段详解 ----------
self.clear()
type_title = Text("3. type字段:访问方式详解", font_size=40, color=YELLOW).to_edge(UP)
self.play(Write(type_title))
# 访问方式表格
type_table = Table(
[["Seq Scan", "全表扫描", "❌ 危险信号", "无索引或索引失效"],
["Index Scan", "索引扫描", "✅ 理想", "通过索引定位,回表获取数据"],
["Index Only Scan", "仅索引扫描", "✅ 最佳", "索引包含所有需要的数据"],
["Bitmap Index Scan", "位图索引扫描", "✅ 适合大量数据", "多索引组合时使用"],
["Bitmap Heap Scan", "位图堆扫描", "✅ 配合位图索引", "将多个索引结果合并"],
["Hash Join", "哈希连接", "⚡ 适合大表", "一张表建哈希表,另一张表探测"],
["Nested Loop", "嵌套循环", "⚠️ 小表驱动", "外层循环每行匹配内层"],
["Merge Join", "归并连接", "⚡ 有序输入", "两个有序结果集合并"]],
col_labels=[Text("type类型"), Text("说明"), Text("评价"), Text("典型场景")],
include_outer_lines=True,
line_config={"stroke_width": 1, "color": GRAY},
element_to_mobject_config={"font_size": 22}
).scale(0.55).shift(DOWN*0.5)
self.play(Create(type_table))
self.wait(2)
self.next_slide()
# ---------- 4. Extra字段警告标志 ----------
self.clear()
extra_title = Text("4. Extra字段:需要警惕的标志", font_size=40, color=YELLOW).to_edge(UP)
self.play(Write(extra_title))
# Extra警告表格
extra_table = Table(
[["Using filesort", "需要额外排序", "⚠️ 内存排序, 数据量大时影响性能", "增加索引避免排序"],
["Using temporary", "使用临时表", "⚠️ GROUP BY无索引", "优化索引或SQL"],
["Using where", "使用WHERE过滤", "✅ 正常", "但需配合索引使用"],
["Using index", "使用覆盖索引", "✅ 最佳", "无需回表"],
["Using index condition", "索引条件下推", "✅ 5.6+优化", "减少回表次数"],
["Using MRR", "多范围读取", "✅ 优化", "顺序IO优化"],
["Using join buffer", "使用连接缓冲区", "⚠️ 无索引JOIN", "必须添加索引"]],
col_labels=[Text("Extra信息"), Text("含义"), Text("影响"), Text("优化建议")],
include_outer_lines=True,
line_config={"stroke_width": 1, "color": GRAY},
element_to_mobject_config={"font_size": 22}
).scale(0.55).shift(DOWN*0.5)
self.play(Create(extra_table))
self.wait(2)
self.next_slide()
# ---------- 5. 实际案例:好计划vs坏计划 ----------
self.clear()
case_title = Text("5. 实战案例:优劣执行计划对比", font_size=40, color=YELLOW).to_edge(UP)
self.play(Write(case_title))
# 准备测试表
setup_code = Code(
code_string='''
-- 准备测试数据
CREATE TABLE users (
id SERIAL PRIMARY KEY,
status INT,
email VARCHAR(100),
created_at TIMESTAMP
);
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_users_created ON users(created_at);
-- 插入100万条测试数据
INSERT INTO users (status, email, created_at)
SELECT
random() * 10, -- status 0-10
'user' || i || '@test.com',
now() - (random() * interval '365 days')
FROM generate_series(1, 1000000) i;
''',
**code_config
).scale(0.5).shift(UP*0.5)
self.play(FadeIn(setup_code, shift=UP))
self.wait(2)
self.next_slide()
self.clear()
# 好计划示例
good_plan_title = Text("✅ 好的执行计划", font_size=32, color=GREEN).shift(UP*3+LEFT*3)
self.play(Write(good_plan_title))
good_plan = Code(
code_string='''
-- 使用索引的查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, email
FROM users
WHERE status = 5
AND created_at > now() - interval '30 days';
--------------------------------------------------------------------
Bitmap Heap Scan on users (cost=12.34..156.78 rows=890 width=27)
Recheck Cond: (status = 5)
Filter: (created_at > (now() - '30 days'::interval))
-> Bitmap Index Scan on idx_users_status
(cost=0.00..12.12 rows=1245 width=0)
Index Cond: (status = 5)
Planning Time: 0.234 ms
Execution Time: 1.456 ms
''',
**code_config
).scale(0.5).shift(LEFT*3)
self.play(FadeIn(good_plan, shift=LEFT))
# 坏计划示例
bad_plan_title = Text("❌ 坏的执行计划", font_size=32, color=RED).shift(UP*3+RIGHT*3)
self.play(Write(bad_plan_title))
bad_plan = Code(
code_string='''
-- 索引失效的查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, email
FROM users
WHERE status::text = '5' -- 隐式转换导致索引失效
AND date(created_at) = current_date; -- 函数包裹导致索引失效
--------------------------------------------------------------------
Seq Scan on users (cost=0.00..23456.78 rows=12345 width=27)
Filter: ((status::text = '5'::text)
AND (date(created_at) = CURRENT_DATE))
Rows Removed by Filter: 987654
Planning Time: 0.345 ms
Execution Time: 234.567 ms
''',
**code_config
).scale(0.5).shift(RIGHT*3)
self.play(FadeIn(bad_plan, shift=RIGHT))
# 对比说明
compare_note = Text(
"好计划: Bitmap Index Scan + Bitmap Heap Scan (1.4ms)\n"
"坏计划: Seq Scan 全表扫描 (234ms), 扫描100万行",
font_size=20, color=YELLOW,
line_spacing=1.5
).to_edge(DOWN)
self.play(Write(compare_note))
self.wait(3)
self.next_slide()
# ---------- 6. 识别危险信号 ----------
self.clear()
warning_title = Text("6. 执行计划中的危险信号", font_size=40, color=YELLOW).to_edge(UP)
self.play(Write(warning_title))
# 危险信号列表
warnings = VGroup(
Text("🔴 Seq Scan 全表扫描 (大表)", font_size=28, color=RED),
Text("🔴 Using filesort 文件排序 (数据量大时)", font_size=28, color=RED),
Text("🔴 Using temporary 临时表 (GROUP BY无索引)", font_size=28, color=RED),
Text("🔴 rows 预估偏差巨大 (统计信息过时)", font_size=28, color=ORANGE),
Text("🔴 Using join buffer (JOIN无索引)", font_size=28, color=RED),
Text("🔴 Nested Loop 驱动表过大 (小表驱动大表原则)", font_size=28, color=ORANGE),
).arrange(DOWN, aligned_edge=LEFT, buff=0.3).shift(DOWN*0.5)
for warning in warnings:
self.play(Write(warning, lag_ratio=0.1))
self.wait(0.2)
self.wait(2)
self.next_slide()
# ---------- 7. 统计信息维护 ----------
self.clear()
stats_title = Text("7. 统计信息:执行计划的基石", font_size=40, color=YELLOW).to_edge(UP)
self.play(Write(stats_title))
stats_code = Code(
code_string='''
-- 查看统计信息
SELECT schemaname, tablename, n_live_tup, n_dead_tup, last_vacuum
FROM pg_stat_user_tables
WHERE tablename = 'users';
-- 手动更新统计信息
ANALYZE users;
-- 查看表的统计信息详情
SELECT attname, n_distinct, correlation, null_frac
FROM pg_stats
WHERE tablename = 'users'
ORDER BY attname;
-- 配置自动统计信息阈值
ALTER TABLE users SET (autovacuum_vacuum_scale_factor = 0.05);
ALTER TABLE users SET (autovacuum_analyze_scale_factor = 0.02);
-- 查看当前统计信息设置
SELECT relname, reloptions
FROM pg_class
WHERE relname = 'users';
''',
**code_config
).scale(0.5).shift(UP*0.5)
self.play(FadeIn(stats_code, shift=UP))
stats_note = Text(
"统计信息过时 -> 错误执行计划 -> 性能下降",
font_size=24, color=BLUE
).to_edge(DOWN)
self.play(Write(stats_note))
self.wait(2)
self.next_slide()
# ---------- 8. 执行计划可视化工具 ----------
self.clear()
tools_title = Text("8. 执行计划可视化工具", font_size=40, color=YELLOW).to_edge(UP)
self.play(Write(tools_title))
tools_table = Table(
[["pgAdmin", "内置可视化", "图形化显示执行计划树"],
["explain.depesz.com", "在线工具", "颜色标记高成本节点"],
["PEV (Postgres Explain Visualizer)", "Web工具", "交互式浏览"],
["explain.tensor.ru", "俄罗斯开发", "详细的指标分析"],
["auto_explain", "内置插件", "自动记录慢查询计划"]],
col_labels=[Text("工具"), Text("类型"), Text("特点")],
include_outer_lines=True,
line_config={"stroke_width": 1, "color": GRAY},
element_to_mobject_config={"font_size": 22}
).scale(0.6).shift(UP*0.5)
self.play(Create(tools_table))
# 生成执行计划JSON的示例
json_example = Code(
code_string='''
-- 生成JSON格式的执行计划(便于工具分析)
EXPLAIN (FORMAT JSON)
SELECT * FROM users WHERE status = 5;
-- 生成YAML格式
EXPLAIN (FORMAT YAML)
SELECT * FROM users WHERE status = 5;
-- 生成XML格式
EXPLAIN (FORMAT XML)
SELECT * FROM users WHERE status = 5;
''',
**code_config
).scale(0.5).to_edge(DOWN).shift(DOWN*0.5)
self.play(FadeIn(json_example, shift=UP))
self.wait(2)
self.next_slide()
# ---------- 9. 优化实战流程 ----------
self.clear()
process_title = Text("9. EXPLAIN优化实战流程", font_size=40, color=YELLOW).to_edge(UP)
self.play(Write(process_title))
# 优化流程图
steps = VGroup(
Text("1️⃣ 定位慢SQL (pg_stat_statements/日志)", font_size=26),
Text("2️⃣ EXPLAIN ANALYZE 获取真实执行计划", font_size=26),
Text("3️⃣ 识别危险信号 (ALL/filesort/temp)", font_size=26),
Text("4️⃣ 分析cost/rows与实际偏差", font_size=26),
Text("5️⃣ 检查索引使用情况", font_size=26),
Text("6️⃣ 优化SQL或添加索引", font_size=26),
Text("7️⃣ 再次EXPLAIN验证优化效果", font_size=26),
Text("8️⃣ 持续监控,定期ANALYZE", font_size=26),
).arrange(DOWN, aligned_edge=LEFT, buff=0.2).shift(UP*0.5)
# 添加箭头连接步骤
for i in range(len(steps)-1):
arrow = Arrow(
steps[i].get_bottom(),
steps[i+1].get_top(),
buff=0.1,
color=BLUE,
stroke_width=2
)
#self.play(Create(arrow), run_time=0.3)
for step in steps:
self.play(Write(step, lag_ratio=0.1), run_time=0.3)
self.wait(2)
self.next_slide()
# ---------- 10. 总结 ----------
self.clear()
summary_title = Text("EXPLAIN分析五大法则", font_size=44, color=YELLOW).to_edge(UP)
self.play(Write(summary_title))
rules = VGroup(
Text("1️⃣ 先ANALYZE: 用EXPLAIN ANALYZE获取真实数据", font_size=28),
Text("2️⃣ 盯关键: type, rows, actual time, Extra", font_size=28),
Text("3️⃣ 识危险: ALL全表扫描, filesort排序, temp临时表", font_size=28),
Text("4️⃣ 对偏差: 实际rows与预估偏差大要更新统计信息", font_size=28),
Text("5️⃣ 可视化: 善用工具直观分析执行计划", font_size=28),
).arrange(DOWN, aligned_edge=LEFT, buff=0.3).shift(UP*1)
for rule in rules:
self.play(Write(rule, lag_ratio=0.1))
self.wait(0.3)
# 最终原则
final_principle = Text(
"少查、快连、精索、常析、避坑",
font_size=36, color=GREEN
).shift(DOWN*1.5)
self.play(Write(final_principle))
self.wait(3)
# ---------- 下一专题预告 ----------
self.clear()
next_title = Text("下一专题预告", font_size=48, color=BLUE)
topic_name = Text("避免全表扫描与常见性能陷阱", font_size=36, color=GREEN)
topic_points = VGroup(
Text("• NULL值导致索引失效", font_size=28),
Text("• LIKE '%abc'无法走索引", font_size=28),
Text("• JOIN字段未建索引", font_size=28),
).arrange(DOWN, aligned_edge=LEFT, buff=0.2)
VGroup(next_title, topic_name, topic_points).arrange(DOWN, buff=0.5)
self.play(
Write(next_title),
Write(topic_name),
*[Write(point) for point in topic_points]
)
self.wait(3)