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)
相关推荐
_千思_1 小时前
【小白说】数据库系统概念 5
数据库
014-code1 小时前
Redis 分布式锁:从 0 到 1 完整演变
数据库·redis·分布式
落羽的落羽2 小时前
【Linux系统】磁盘ext文件系统与软硬链接
linux·运维·服务器·数据库·c++·人工智能·机器学习
WJX_KOI2 小时前
保姆级教程:Apache Flink CDC(standalone 模式)部署 MySQL CDC、PostgreSQL CDC 及使用方法
java·大数据·mysql·postgresql·flink
树码小子2 小时前
Mybatis(17)Mybatis-Plus条件构造器(2)& 自定义 SQL
数据库·sql·mybatis-plus
橘子132 小时前
redis主从复制
数据库·redis·缓存
白太岁2 小时前
Redis:(5) 分布式锁实现:原子性设置锁与 Lua 释放锁
数据库·redis·分布式
zhu62019762 小时前
Postgres数据库docker快速安装
数据库·docker·容器
数据知道3 小时前
PostgreSQL:详解 PostgreSQL 与Hadoop与Spark的集成
hadoop·postgresql·spark