DeepSeek辅助生成的PostgreSQL 执行计划分析幻灯片脚本

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)
相关推荐
乌鸦乌鸦你的小虎牙3 小时前
qt 5.12.8 配置报错(交叉编译环境)
开发语言·数据库·qt
一只大袋鼠3 小时前
Redis 安装+基于短信验证码登录功能的完整实现
java·开发语言·数据库·redis·缓存·学习笔记
Anastasiozzzz3 小时前
深入研究Redis的ZSet底层数据结构:从 Ziplist 的级联更新到 Listpack 的完美救场
数据结构·数据库·redis
菠萝蚊鸭3 小时前
x86 平台使用 buildx 基于源码构建 MySQL Wsrep 5.7.44 镜像
数据库·mysql·galera·wsrep
沙漏无语6 小时前
(二)TIDB搭建正式集群
linux·数据库·tidb
姚不倒6 小时前
三节点 TiDB 集群部署与负载均衡搭建实战
运维·数据库·分布式·负载均衡·tidb
隔壁小邓6 小时前
批量更新方式与对比
数据库
数据知道6 小时前
MongoDB复制集架构原理:Primary、Secondary 与 Arbiter 的角色分工
数据库·mongodb·架构
人道领域6 小时前
苍穹外卖:菜品新增功能全流程解析
数据库·后端·状态模式
修行者Java6 小时前
(七)从 “非结构化数据难存储” 到 “MongoDB 灵活赋能”——MongoDB 实战进阶指南
数据库·mongodb