数据变化后重新读一遍规划器的最新计划。

查询变慢时同时检查计划与等待

一条过去很快的查询,可以在数据、统计和访问计划都没有变化时变慢。另一个事务可能持有它需要的锁,存储缓存可能比上次更冷,应用也可能在语句进入 PostgreSQL 之前就开始等待。重新检查计划很有用,但它应当放在完整的耗时比较中:先区分时间是花在执行工作上,还是花在等待获得执行机会。不能从“现在更慢”直接推导出“数据与统计一定都变了”。 保留可比较的基线 记录语句形状、有代表性的参数值、返回行数、相关会话设置,以及应用是否使用预备语句。不同参数可能命中完全不同规模的数据。手工使用常量的查询,不一定复现预备语句采用的通用计划;不同角色或搜索路径,也可能改变实际对象与权限。因此,拿一条看起来相似的语句作为对照,并不保证比较条件相同。 尽量在变慢正在发生时观察活动。pg_stat_activity 可以显示一个状态为运行中的查询正在等待锁;运行中并不等于持续消耗处理器。pg_blocking_pids 列出当前阻塞这个后端的会话。查看其他角色活动需要适当权限,因此字段不可见不等于没有工作发生。交互观察最好使用新的自动提交查询,而不是在同一个长事务中不断读取已经缓存的监控快照。 坏掉的样子 不加判断就运行 ANALYZE,相当于把每次变慢都当成统计陈旧。它可能增加维护工作,却完全没有解除真正的阻塞。一张只有一行的表,即使新增索引,也不能释放其他事务持有的行锁。准确的行数估计和很小的顺序扫描,可以与很长的应用等待同时存在;这两种观察并不矛盾。 EXPLAIN ANALYZE 会真正执行语句,写入也不例外,而且测量本身同样可能被阻塞。从生产环境复制一条大范围更新,不是无害的检查。应先看等待情况和锁的拥有者,再选择安全夹具或副作用已经明确的语句来测量。看到某个进程编号出现在阻塞列表中,也不能立即终止不熟悉的事务,因为它可能正在完成另一个有效业务操作。 快速验证 打开三个新的 psql 会话,分别记作甲、乙、丙,连接同一个可丢弃的 PostgreSQL 18 数据库并使用相同角色,保留通常的自动提交行为。名为 query_recheck_demo 的模式必须尚不存在。务必区分会话:把所有代码块顺序粘贴到同一个连接,无法证明事务之间的阻塞。 在甲中建立夹具,然后开启事务,修改唯一一条记录但暂不提交。 \set ON_ERROR_STOP on CREATE SCHEMA query_recheck_demo; CREATE TABLE query_recheck_demo.items (id integer PRIMARY KEY,value integer); INSERT INTO query_recheck_demo.items VALUES (1,0); ANALYZE query_recheck_demo.items; BEGIN; UPDATE query_recheck_demo.items SET value=10 WHERE id=1; SELECT pg_backend_pid() AS blocker_pid; 在乙中发起带执行测量的更新。它会等待甲结束事务,或者等到六十秒的语句超时。 SET application_name='xbcatm_recheck_waiter'; SET statement_timeout='60s'; EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) UPDATE query_recheck_demo.items SET value=value+1 WHERE id=1; 乙仍在等待且尚未超时时,在丙中执行观察查询。实验专用应用名称用于定位乙,不需要展示整条查询文本。 SELECT pid,state,wait_event_type,wait_event,pg_blocking_pids(pid) AS blockers FROM pg_stat_activity WHERE application_name='xbcatm_recheck_waiter'; 回到甲,撤销尚未提交的变更并释放锁,然后收集乙执行完毕后的计划。 ROLLBACK; 确认乙已经成功结束后,在丙中检查最终数据并清理夹具。 SELECT value FROM query_recheck_demo.items WHERE id=1; DROP SCHEMA query_recheck_demo CASCADE; PostgreSQL 18.6 实测中,乙的状态为运行中,等待类型是 Lock,具体等待事件是 transactionid,阻塞列表包含甲的进程编号。具体事件可能随争用过程变化,核心证据是乙确实被甲阻塞。甲回滚后,乙把原本的零加成一,最后查询返回一,清理语句移除了整个实验模式。 ...

一月 31, 2028 · 1 分钟 · XB Field Notes
真正执行查询以查看每一层的实际行数、耗时与缓冲区。

EXPLAIN ANALYZE 把查询真的跑一遍

EXPLAIN 展示 PostgreSQL 的执行计划,EXPLAIN ANALYZE 还会真正执行语句并测量工作量。最有用的比较,是同一个节点的估计行数与实际行数。两者都不能单独当成结论:估计来自模型,实测则只描述特定数据、缓存、并发和插桩条件下的一次执行。不能把前者称为谎言,也不能把后者当成脱离环境的永恒事实。 先比较同一种量 计划中的 cost 是相对成本单位,不是毫秒。两个成本值分别描述启动成本与完成成本,带 LIMIT 的查询可能更重视尽早得到前几行,因此选择不同计划。actual time 才使用毫秒。即使成本和耗时恰好都打印成一百,也不能把它们直接相减或求比例;它们本来就不表示同一件事。 一个节点被重复执行时,actual rows 和节点耗时是每次循环的平均值,需要结合 loops 理解总工作量。但不能把所有节点耗时简单相加,因为父节点已经包含子节点的工作,重复相加会重复计算。并行执行时更不能用这种方式猜总延迟。先沿树看输入输出、循环次数、过滤条件和行数流向,再决定哪个位置值得调查。 BUFFERS 区分 shared、local 和临时块活动。shared hit 表示 PostgreSQL 在共享缓冲区里找到了块;shared read 只表示需要读入,操作系统仍可能从页缓存满足它,不能直接断言每次都访问了物理磁盘。临时表使用本地缓冲区,排序或哈希溢出则可能读写临时文件。父节点计数包含子节点,逐行把所有计数加起来同样会夸大 I/O。 找到有意义的估计偏差 某个选择性条件估计只返回十行,实际却返回一万行,通常值得先调查,再考虑重写其上方的连接。统计信息陈旧、分布倾斜、列间相关、参数值变化或选择性模型能力不足,都可能造成偏差。ANALYZE 可以刷新统计信息,却不能保证每个估计精确,也不会自动修好所有慢计划。先看真实数据和缺失的信息,再决定是否调整统计目标或增加扩展统计。 行数估计准确也不能证明查询高效。缺少访问路径、昂贵表达式、排序溢出、锁等待和并发争用仍可能存在。反过来,小表或需要返回大量行的查询采用顺序扫描可能正合适,不应把“没用索引”一律判为错误。分析时应保留应用实际使用的参数和查询形状,避免用一个稀有值的漂亮计划替代整个生产分布。 坏掉的样子 最危险的误解,是把 ANALYZE 当成只读预演。UPDATE 会改变行,触发器会执行,锁会被获取,SELECT 也可能调用带副作用的函数。事务末尾执行 ROLLBACK 可以恢复事务性表变化,却不能一般性撤销序列前进或外部系统副作用。对语句影响范围不确定时,应使用可丢弃的数据集,不能靠“最后会回滚”就放心在生产运行。 ...

十二月 16, 2027 · 2 分钟 · XB Field Notes
一个暴露规划器所选执行策略的工具。

EXPLAIN 让你看清 Postgres 会怎么做

EXPLAIN 打印规划器准备采用的执行计划与代价;EXPLAIN ANALYZE 则真的跑一遍语句,把估算变成测到的行数与时间。它们回答的是两个不同问题:规划器认为应该怎样做,以及数据库实际怎样做。排障时先看前者理解意图,再用后者测量现实,别把 cost 当成秒数,也别在生产库上无准备地执行一条会写数据的 ANALYZE。 一份计划不是漂亮的树形报告,而是数据库给出的可审计假设。它告诉你读了哪些表、用了什么索引、预计经过多少行,以及每个节点把多少工作交给下一个节点。把计划保存进工单,和请求参数、统计信息更新时间、数据库版本一起记录,几周后的回归才有可比基线。只贴一句“慢”没有证据;贴出计划才知道到底是哪一个假设失真。 自上而下读形状 文本计划先打印父节点,再缩进打印它的子节点。向下读可以理解节点依赖哪些输入,向上看可以理解结果怎样汇合。执行可能按需发生:上层的 LIMIT 得到足够多的行便会停止索取,下层排序却可能需要读完全部输入才能交出第一行。缩进描述依赖关系,并不是所有节点通用的执行时间顺序。 cost 使用规划器的工作量单位,不是毫秒。父节点总成本包含子节点,不能把整棵树的数字直接求和。启动成本表示返回第一行前预计投入多少工作,总成本表示完成节点的估算。实际时间和实际行数在 loops 大于一时按每次执行取平均;判断重复工作要结合循环次数,同时避免把父节点包含的子节点时间再加一次。 阅读时先问三个问题。第一,计划是否走了预期的访问路径,例如选择性很高的条件是否用了索引。第二,估算行数和实际行数是否在同一个数量级,数量级错了通常比单个节点的 cost 更值得处理。第三,数据流在哪里膨胀或等待,宽大的中间结果会把后面的排序、连接和聚合一起拖慢。沿着这三个问题读,计划就从一堆术语变成了可验证的因果链。 盯着最宽的那个节点 行数最多只是线索,不能直接证明瓶颈。连续读取的大表扫描可能比随机访问索引更便宜,一个每次很快却反复执行数万次的节点也可能主导延迟。先找估算与实际第一次发生明显偏离的地方,再追踪它怎样影响后续连接、排序与内存。嵌套循环并非天然错误:外表很小、内表有匹配索引时它可能最快;危险的是外表被严重低估后仍反复访问内表。 把计划格式化为 JSON 可以让工具比较节点,手工排障先用普通 EXPLAIN 确认访问路径,再按需执行带 ANALYZE、BUFFERS 的测量。缓冲命中、缓冲读取、临时文件读写能帮助定位数据访问与排序落盘。缓冲读取不等于真实磁盘读取,因为操作系统缓存也可能提供数据;看到 read 就宣布磁盘慢,证据还不够。VERBOSE 会显示更多输出列和别名,适合定位复杂视图。 用 ANALYZE 让行数和代价对齐 EXPLAIN ANALYZE 会真实执行查询,同时给出估算与实际行数、节点耗时和循环次数。两者出入很大,可能来自统计过时、分布倾斜或列间相关性;重新收集统计有时能改善,但不能保证修好所有偏差。写入实验放到隔离数据库,并设置语句超时。事务回滚只能撤销可回滚的数据库改动,不能撤销已经分配的序列值、函数的外部调用或触发器发出的消息。SELECT 也可能调用带副作用的函数,不能只根据语句开头判断安全。 估算偏差修好后再判断索引是否值得。可以按需提高相关列的统计目标,或者建立描述相关列的扩展统计,但这些动作也要由测量验证。若同一语句对小租户很快、对大租户很慢,要用应用实际的预备语句路径比较通用计划和定制计划。PostgreSQL 核心并没有通用的 SQL 提示语法,不要凭空加入提示来强行选择节点。每次只改变一个变量,把结果集、计划和延迟并排保存。 ...

七月 8, 2027 · 1 分钟 · XB Field Notes