一、PostgreSQL查询执行的生命周期

PostgreSQL处理一条SQL查询的过程,就像做蛋糕的完整流程:先看懂配方(解析)、调整配方(重写)、选择最快的制作方法(规划)、实际动手做(执行)。每个步骤环环相扣,任何一步出错都会影响最终结果。

1.1 解析阶段:从SQL字符串到“可理解的步骤”

当你输入一条SQL(比如SELECT * FROM users WHERE age > 30;),PostgreSQL首先要**“看懂”这句话**——这就是解析阶段的任务。

  • 词法分析:把SQL字符串拆成一个个“关键词”(比如SELECTFROMWHERE)和“值”(比如usersage30),就像把“鸡蛋+面粉+糖”拆成单独的食材。
  • 语法分析:检查这些关键词的顺序是否符合SQL语法规则(比如SELECT后面必须跟列或*FROM后面必须跟表名),生成解析树(Parse Tree)——类似把“先打鸡蛋,再混合面粉”写成树状步骤。

如果语法错误(比如把FROM写成FORM),PostgreSQL会立刻报错:ERROR: syntax error at or near "FORM"

1.2 重写阶段:处理“隐藏的规则”

解析后的SQL可能包含视图(View)规则(Rule),重写阶段会把这些“隐藏的逻辑”展开成原始SQL。
比如你创建了一个视图:

CREATE VIEW adult_users AS SELECT * FROM users WHERE age >= 18;

当你查询SELECT * FROM adult_users WHERE age > 30;时,重写阶段会把视图替换成原始条件,变成:

SELECT * FROM users WHERE age >= 18 AND age > 30;

这就像recipe里写“用准备好的蛋糕糊”,重写阶段会把它替换成“鸡蛋+面粉+糖混合后的糊”——让规划器能看到完整的逻辑。

1.3 规划阶段:选择“最快的执行方法”

规划阶段是查询优化的核心。PostgreSQL的查询规划器(Planner)会生成多个可能的执行计划,然后计算每个计划的成本(Cost),选择成本最低的那个。

比如查询SELECT * FROM users WHERE age > 30;,可能的计划有两种:

  1. 全表扫描(Seq Scan):逐行读取整个users表,过滤出age > 30的行。
  2. 索引扫描(Index Scan):如果age列有索引,先通过索引找到age > 30的行的位置,再去表中读取对应的数据。

规划器会计算这两个计划的成本(比如全表扫描成本是100,索引扫描是20),然后选成本低的索引扫描。

1.4 执行阶段:按计划“动手做事”

执行阶段由**执行器(Executor)**负责,它会严格按照规划器选好的计划运行,就像按recipe步骤烤蛋糕:

  • 如果是索引扫描,执行器会先读索引文件,找到符合条件的行的id,再去表文件中读取完整的行数据。
  • 如果是全表扫描,执行器会逐行读取表文件,过滤出符合条件的行。

执行器还会处理并发控制(比如锁)和结果返回(把数据传给客户端)。

二、代价模型:PostgreSQL选计划的“计算器”

PostgreSQL为什么选索引扫描而不是全表扫描?因为它有一套成本计算规则——代价模型(Cost Model)。成本越低,计划越好。

2.1 成本的两大组成:IO vs CPU

PostgreSQL的成本分为两类:

  1. IO成本:从磁盘读取数据的时间(比如读一个数据页需要1个单位成本)。
  2. CPU成本:处理数据的时间(比如计算一行的条件需要0.01个单位成本)。

比如全表扫描的成本公式:

全表扫描成本 = (表的总数据页数 × 1) + (表的总行数 × 0.01)

索引扫描的成本公式:

索引扫描成本 = (索引的总数据页数 × 1) + (索引的总行数 × 0.01) + (符合条件的行数 × 1)

(最后一项是“回表读”的IO成本——从索引找到行位置后,再去表中读数据。)

2.2 统计信息:成本计算的“情报源”

代价模型的准确性,完全依赖统计信息(Statistics)——就像做蛋糕前要知道“冰箱里有多少鸡蛋”。
PostgreSQL把统计信息存在pg_statistic表中,包含:

  • 列的distinct值数量(比如age列有多少不同的年龄);
  • 列的最频值(Most Common Values, MCV)(比如age列最常见的年龄是25);
  • 列的直方图(比如age在18-30之间的行占比多少)。

如果统计信息过时(比如表新增了10万行但没更新统计信息),规划器会算错成本。比如:

  • 实际users表有100万行,但统计信息显示只有1万行;
  • 规划器会认为全表扫描成本是100(1万行×0.01 + 1万页×1),但实际成本是10000(100万行×0.01 + 100万页×1);
  • 这时候规划器会错误地选择全表扫描,导致查询变慢。

2.3 如何看成本?用EXPLAIN ANALYZE

想知道规划器选了什么计划、成本是多少,用EXPLAIN ANALYZE命令。比如:

EXPLAIN ANALYZE SELECT * FROM users WHERE age > 30;

输出结果(简化版):

Index Scan using idx_users_age on users  (cost=0.29..8.30 rows=4000 width=58) (actual time=0.015..0.600 rows=4000 loops=1)
  Index Cond: (age > 30)
Planning Time: 0.120 ms
Execution Time: 0.800 ms
  • cost=0.29..8.30:计划的预估成本(0.29是启动成本,8.30是总执行成本);
  • actual time=0.015..0.600实际执行时间(毫秒);
  • rows=4000预估返回行数(实际返回4000行)。

三、优化的核心理念:用“最小代价”拿结果

查询优化的本质,就是让规划器选成本最低的计划。核心思路有三个:

3.1 减少数据扫描量:用索引“精准定位”

索引的作用就像书的目录——找第100页不用翻完整本书,直接看目录。PostgreSQL支持多种索引(B-tree、GiST、GIN等),其中最常用的是B-tree索引(适合范围查询和等值查询)。

比如在users表的age列建索引:

CREATE INDEX idx_users_age ON users (age);

查询age > 30时,规划器会选索引扫描,扫描的行数从100万变成40万,成本大幅降低。

注意:索引不是越多越好!每个索引会增加写操作的成本(比如插入行时要更新索引)。只给常用查询的条件列建索引。

3.2 选对连接方式:Nested Loop vs Hash Join vs Merge Join

当查询涉及多个表连接(比如SELECT * FROM users JOIN orders ON users.id = orders.user_id;),规划器会选三种连接方式之一:

  1. 嵌套循环连接(Nested Loop):适合小表连大表(比如users是小表,orders是大表)。逻辑是:先遍历小表的每一行,再去大表中找对应的行(类似“先拿一个用户,再找他的所有订单”)。
  2. 哈希连接(Hash Join):适合两个大表连接。逻辑是:先把小表的数据做成哈希表(比如把users.id做成key),再遍历大表的每一行,用哈希表快速找对应的行(类似“先把用户做成字典,再快速查订单对应的用户”)。
  3. 合并连接(Merge Join):适合两个已排序的表。逻辑是:把两个表按连接键排序,然后一一对应(类似“把用户和订单都按id排序,然后依次配对”)。

比如users有1万行,orders有100万行,规划器会选嵌套循环连接——因为小表遍历快,大表用索引找行也快。

3.3 避免不必要的计算:Early Pruning与Predicate Pushdown

PostgreSQL会尽可能早地过滤数据,减少后续计算量:

  • Early Pruning:在扫描表时,提前跳过不符合条件的分区(比如按年份分区的表,查询2023年的数据,直接跳过2022年的分区)。
  • Predicate Pushdown:把过滤条件“推”到数据源(比如查询视图时,把age > 30推到视图的原始表中,而不是先查视图再过滤)。

比如查询SELECT * FROM adult_users WHERE age > 30;,重写阶段会把age > 30合并到视图的age >= 18条件中,变成age > 30——减少扫描的行数。

四、课后Quiz:巩固你的理解

问题1:为什么统计信息过时会导致查询性能下降?

答案解析
PostgreSQL的规划器依赖统计信息计算成本。如果统计信息过时(比如表行数从1万变成100万,但统计信息没更新),规划器会算错成本——比如误以为全表扫描的成本是100,但实际是10000。这会导致规划器选择成本更低但实际更慢的计划(比如全表扫描而不是索引扫描),最终查询变慢。

解决办法:定期运行ANALYZE命令更新统计信息(比如ANALYZE users;)。

问题2:索引越多越好吗?为什么?

答案解析
不是。索引会增加写操作的成本——比如插入一行数据时,不仅要写表文件,还要更新所有相关的索引文件。如果一个表有10个索引,插入一行的时间会比没有索引时慢10倍。

建议:只给常用查询的条件列建索引(比如users表的age列如果经常被用来过滤,就建索引)。

五、常见报错解决方案

报错1:ERROR: relation "users" does not exist

原因

  1. 表名拼写错误(比如写成user而不是users);
  2. 表不在当前schema中(比如表在app schema下,但当前schema是public);
  3. 表没有被创建。

解决办法

  • dt命令查看当前数据库中的表(psql中);
  • 用schema限定表名(比如app.users);
  • 如果表没创建,执行CREATE TABLE语句。

预防建议:使用显式的schema名(比如app.users),避免依赖search_path(搜索路径)。

报错2:ERROR: syntax error at or near "SELECT"

原因
SQL语句有语法错误(比如两个SELECT之间没有分号或UNION)。例如:

SELECT id FROM users WHERE age > 30 SELECT name FROM orders;

解决办法

  • 检查SQL语句的语法,确保每个语句正确结束(用分号);
  • 用pgAdmin的“语法检查”功能(点击“检查语法”按钮)。

预防建议:写SQL时逐步测试(比如先写SELECT id FROM users;,再添加WHERE条件),避免一次性写复杂语句。

参考链接

  • 代价模型与规划器:www.postgresql.org/docs/17/pla…
  • 统计信息:www.postgresql.org/docs/17/pla…
  • EXPLAIN的使用:www.postgresql.org/docs/17/usi…
  • 常见错误代码:www.postgresql.org/docs/17/err…

余下文章内容请点击跳转至 个人博客页面 或者 扫码关注或者微信搜一搜:编程智域 前端至全栈交流与成长,阅读完整的文章:PostgreSQL处理SQL居然像做蛋糕?解析到执行的4步里藏着多少查询优化的小心机?

往期文章归档
  • PostgreSQL备份不是复制文件?物理vs逻辑咋选?误删还能精准恢复到1分钟前? - cmdragon's Blog
  • 转账不翻车、并发不干扰,PostgreSQL的ACID特性到底有啥魔法? - cmdragon's Blog
  • 银行转账不白扣钱、电商下单不超卖,PostgreSQL事务的诀窍是啥? - cmdragon's Blog
  • PostgreSQL里的PL/pgSQL到底是啥?能让SQL从“说目标”变“讲步骤”? - cmdragon's Blog
  • PostgreSQL视图不存数据?那它怎么简化查询还能递归生成序列和控制权限? - cmdragon's Blog
  • PostgreSQL索引这么玩,才能让你的查询真的“飞”起来? - cmdragon's Blog
  • PostgreSQL的表关系和约束,咋帮你搞定用户订单不混乱、学生选课不重复? - cmdragon's Blog
  • PostgreSQL查询的筛子、排序、聚合、分组?你会用它们搞定数据吗? - cmdragon's Blog
  • PostgreSQL数据类型怎么选才高效不踩坑? - cmdragon's Blog
  • 想解锁PostgreSQL查询从基础到进阶的核心知识点?你都get了吗? - cmdragon's Blog
  • PostgreSQL DELETE居然有这些操作?返回数据、连表删你试过没? - cmdragon's Blog
  • PostgreSQL UPDATE语句怎么玩?从改邮箱到批量更新的避坑技巧你都会吗? - cmdragon's Blog
  • PostgreSQL插入数据还在逐条敲?批量、冲突处理、返回自增ID的技巧你会吗? - cmdragon's Blog
  • PostgreSQL的“仓库-房间-货架”游戏,你能建出电商数据库和表吗? - cmdragon's Blog
  • PostgreSQL 17安装总翻车?Windows/macOS/Linux避坑指南帮你搞定? - cmdragon's Blog
  • 能当关系型数据库还能玩对象特性,能拆复杂查询还能自动管库存,PostgreSQL凭什么这么香? - cmdragon's Blog
  • 给接口加新字段又不搞崩老客户端?FastAPI的多版本API靠哪三招实现? - cmdragon's Blog
  • 流量突增要搞崩FastAPI?熔断测试是怎么防系统雪崩的? - cmdragon's Blog
  • FastAPI秒杀库存总变负数?Redis分布式锁能帮你守住底线吗 - cmdragon's Blog
  • FastAPI的CI流水线怎么自动测端点,还能让Allure报告美到犯规? - cmdragon's Blog
  • 如何用GitHub Actions为FastAPI项目打造自动化测试流水线? - cmdragon's Blog
  • 如何用Git Hook和CI流水线为FastAPI项目保驾护航? - cmdragon's Blog
  • FastAPI如何用契约测试确保API的「菜单」与「菜品」一致?
  • 为什么TDD能让你的FastAPI开发飞起来? - cmdragon's Blog
  • 如何用FastAPI玩转多模块测试与异步任务,让代码不再“闹脾气”? - cmdragon's Blog
  • 如何在FastAPI中玩转“时光倒流”的数据库事务回滚测试?
  • 如何在FastAPI中优雅地模拟多模块集成测试? - cmdragon's Blog
  • 多环境配置切换机制能否让开发与生产无缝衔接? - cmdragon's Blog
  • 如何在 FastAPI 中巧妙覆盖依赖注入并拦截第三方服务调用? - cmdragon's Blog
  • 为什么你的单元测试需要Mock数据库才能飞起来? - cmdragon's Blog
  • 如何在FastAPI中巧妙隔离依赖项,让单元测试不再头疼? - cmdragon's Blog
  • 如何在FastAPI中巧妙隔离依赖项,让单元测试不再头疼? - cmdragon's Blog
  • 测试覆盖率不够高?这些技巧让你的FastAPI测试无懈可击! - cmdragon's Blog
  • 为什么你的FastAPI测试覆盖率总是低得让人想哭? - cmdragon's Blog
  • 如何让FastAPI测试不再成为你的噩梦? - cmdragon's Blog
  • FastAPI测试环境配置的秘诀,你真的掌握了吗? - cmdragon's Blog
  • 全链路追踪如何让FastAPI微服务架构的每个请求都无所遁形? - cmdragon's Blog
  • 如何在API高并发中玩转资源隔离与限流策略? - cmdragon's Blog
  • 任务分片执行模式如何让你的FastAPI性能飙升? - cmdragon's Blog
  • 冷热任务分离:是提升Web性能的终极秘籍还是技术噱头? - cmdragon's Blog
  • 如何让FastAPI在百万级任务处理中依然游刃有余? - cmdragon's Blog
  • 如何让FastAPI与消息队列的联姻既甜蜜又可靠? - cmdragon's Blog
免费好用的热门在线工具
  • 智能提词器 - 应用商店 | By cmdragon
  • 魔法简历 - 应用商店 | By cmdragon
  • Image Puzzle Tool - 图片拼图工具 | By cmdragon
  • 字幕下载工具 - 应用商店 | By cmdragon
  • 歌词生成工具 - 应用商店 | By cmdragon
  • 网盘资源聚合搜索 - 应用商店 | By cmdragon
  • ASCII字符画生成器 - 应用商店 | By cmdragon
  • JSON Web Tokens 工具 - 应用商店 | By cmdragon
  • Bcrypt 密码工具 - 应用商店 | By cmdragon
  • GIF 合成器 - 应用商店 | By cmdragon
  • GIF 分解器 - 应用商店 | By cmdragon
  • 文本隐写术 - 应用商店 | By cmdragon
  • CMDragon 在线工具 - 高级AI工具箱与开发者套件 | 免费好用的在线工具
  • 应用商店 - 发现1000+提升效率与开发的AI工具和实用程序 | 免费好用的在线工具
  • CMDragon 更新日志 - 最新更新、功能与改进 | 免费好用的在线工具
  • 支持我们 - 成为赞助者 | 免费好用的在线工具
  • AI文本生成图像 - 应用商店 | 免费好用的在线工具
  • 临时邮箱 - 应用商店 | 免费好用的在线工具
  • 二维码解析器 - 应用商店 | 免费好用的在线工具
  • 文本转思维导图 - 应用商店 | 免费好用的在线工具
  • 正则表达式可视化工具 - 应用商店 | 免费好用的在线工具
  • 文件隐写工具 - 应用商店 | 免费好用的在线工具
  • IPTV 频道探索器 - 应用商店 | 免费好用的在线工具
  • 快传 - 应用商店 | 免费好用的在线工具
  • 随机抽奖工具 - 应用商店 | 免费好用的在线工具
  • 动漫场景查找器 - 应用商店 | 免费好用的在线工具
  • 时间工具箱 - 应用商店 | 免费好用的在线工具
  • 网速测试 - 应用商店 | 免费好用的在线工具
  • AI 智能抠图工具 - 应用商店 | 免费好用的在线工具
  • 背景替换工具 - 应用商店 | 免费好用的在线工具
  • 艺术二维码生成器 - 应用商店 | 免费好用的在线工具
  • Open Graph 元标签生成器 - 应用商店 | 免费好用的在线工具
  • 图像对比工具 - 应用商店 | 免费好用的在线工具
  • 图片压缩专业版 - 应用商店 | 免费好用的在线工具
  • 密码生成器 - 应用商店 | 免费好用的在线工具
  • SVG优化器 - 应用商店 | 免费好用的在线工具
  • 调色板生成器 - 应用商店 | 免费好用的在线工具
  • 在线节拍器 - 应用商店 | 免费好用的在线工具
  • IP归属地查询 - 应用商店 | 免费好用的在线工具
  • CSS网格布局生成器 - 应用商店 | 免费好用的在线工具
  • 邮箱验证工具 - 应用商店 | 免费好用的在线工具
  • 书法练习字帖 - 应用商店 | 免费好用的在线工具
  • 金融计算器套件 - 应用商店 | 免费好用的在线工具
  • 中国亲戚关系计算器 - 应用商店 | 免费好用的在线工具
  • Protocol Buffer 工具箱 - 应用商店 | 免费好用的在线工具
  • IP归属地查询 - 应用商店 | 免费好用的在线工具
  • 图片无损放大 - 应用商店 | 免费好用的在线工具
  • 文本比较工具 - 应用商店 | 免费好用的在线工具
  • IP批量查询工具 - 应用商店 | 免费好用的在线工具
  • 域名查询工具 - 应用商店 | 免费好用的在线工具
  • DNS工具箱 - 应用商店 | 免费好用的在线工具
  • 网站图标生成器 - 应用商店 | 免费好用的在线工具
  • XML Sitemap
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]