外层语句预算矩形包含执行、等锁、执行、等待四块;两个等待块下分别用红括号表示逐次计时。

事务拿锁时别的请求会礼貌等待

等待锁可能是正常协调,也可能让连接和请求持续排队。PostgreSQL 的 lock_timeout 可以让调用方停止等待,但它不会解释持锁者为什么慢,更不会自动决定业务是否可以重试。应从操作能够接受的延迟出发设定预算,同时找到实际阻塞者;单纯延长超时或增加尝试次数,可能只是扩大同一条等待队列。 坏掉的样子 锁超时分别作用于每一次锁获取尝试,包括语句隐式申请的锁。它测量的是等待当前锁的时间,而不是整条语句的总运行时间。查询已经执行了一段时间,随后又申请一把锁,仍然可能触发这个限制。反过来,多次较短的等待也可能累计超过一个配置间隔。把锁超时理解成“只有语句刚开始时检查一次”,或把它当成整次查询的总截止时间,都会漏掉真实的等待范围。 statement_timeout 限制整条语句执行,其中也包括等锁。如果它更短,可能先于锁超时取消语句。空闲事务会话超时处理的则是另一种状态:事务还开着,但服务器正在等待客户端发下一条命令。卡在更新内部等待锁的后端通常仍是活动状态,等待事件属于锁;不能看到连接排队,就直接把所有等待者叫作空闲事务。真正闲着不提交的持锁者和正在等待的受阻者,需要分别定位。 PostgreSQL 十八还提供事务超时,事务持续过久时会终止整个会话。这与锁超时仅取消语句的失败边界不同,连接池必须丢弃已经断开的连接。两种设置都不是跨多次尝试的应用总截止时间;事务开始前的排队,以及重试退避期间的耗时,仍应计入调用方预算。不要把一个数据库内计时器当成完整请求的时间保证。 配置超时与 SQLSTATE 说明作用范围;先回滚再重试。配置毫秒数不是测得的客户端延迟。 快速验证 准备同一个 PostgreSQL 18 可丢弃数据库,开启甲、乙两个全新的 psql -X -v ON_ERROR_STOP=1 会话。使用同一角色并具备创建模式权限,确认 timeout_demo 尚不存在,显式事务外保持自动提交。实验使用默认的零值锁超时,且没有其他会提前干预的超时设置。下面的毫秒参数只用于快速复现,不是生产建议值。甲先建立独立模式和计数表: CREATE SCHEMA timeout_demo; CREATE TABLE timeout_demo.counter (id integer PRIMARY KEY,n integer); INSERT INTO timeout_demo.counter VALUES (1,0); 甲在显式事务中把数字改成十,暂时不提交,也不回滚,以便继续持有目标行的锁: ...

二月 16, 2028 · 1 分钟 · XB Field Notes
一行从 closed 改为 archived,前后都不进入 open 子集,但更改谓词列仍会阻止 PostgreSQL 18 B-tree 示例中的 HOT。

部分索引的写入成本

部分索引筛选的是行,不是裁剪列。给 id 建索引并加上 WHERE status = 'open',保存的是未关闭行的标识符;状态列决定哪些行进入索引,却不因此成为搜索键。估算维护成本时,这个区别很重要:即使某一列的值没有存进索引键,它仍可能影响更新需要付出的代价。 如果订单表大部分是已关闭的历史记录,把这些行排除在索引之外,确实可能省下大量索引空间。但这不代表历史行可以免费写入,也不保证查询一定更快,更不会消除数据表本身的清理工作。判断收益时,要同时看结构缩小了多少,以及维持正确状态仍需承担哪些写入。 成员资格决定哪些行入索引;写入成本还取决于键变化、谓词列依赖和同页 HOT 条件。 区分成员资格与更新成本 新插入的行不满足谓词时,这一个部分索引不需要为它新增条目。谓词判断、表内存储、其他索引以及正常事务处理仍然存在。把一行从子集外改到子集内,需要新增条目;把它改出子集,则会改变当前查询能看见的成员。旧行版本与过时索引条目的物理回收可能发生在之后,不能把逻辑上退出子集理解成立即释放所有存储空间。 成员资格没有变化,也不等于可以跳过索引维护。未关闭订单的标识符改变了,索引键就需要更新。即使只修改一列与索引无关的备注,只要数据库无法采用堆内元组更新,也就是 HOT,新行版本仍可能需要新的索引条目。因此,“谓词结果不变”不足以证明这次更新很便宜。 对本文讨论的 PostgreSQL 18 B-tree 索引,HOT 要求所有索引定义引用的列值保持不变,包括表达式引用的列和部分索引谓词引用的列;原来那张堆页面还必须容得下新行版本。把状态从已关闭改成已归档,两种状态虽然都不满足谓词,仍然改变了索引依赖的状态列,会阻止 HOT。只修改没有被任何索引引用的备注列,则可能满足条件。 ...

二月 15, 2028 · 2 分钟 · XB Field Notes
十月条件分叉到九月、十月和十一月;外侧两张分区被划去,十月保留。

分区裁剪跳过查询不可能用到的整张分区

分区裁剪排除的是整张不可能包含匹配行的分区。它与在剩余分区内部利用索引寻找行不同,也不是按页工作的缓存技巧。PostgreSQL 可以在查询规划阶段完成排除,也可以等参数值在执行过程中可用后再裁剪。要判断它是否生效,需要查看实际访问的分区,而不是只看最终返回了多少行。 证明哪些分区不可能命中 声明式分区的边界来自分区键。把日期键限制在十月,可以证明九月和十一月与查询无关。裁剪依赖这些边界,而不是估计其他分区“可能很少匹配”。统计信息仍影响其他规划决策,但行数估计不准,不会让分区归属变成概率判断;这两个层面的信息应分开解释。 裁剪不要求先建索引。排除无关分区后,如果剩余分区的大部分行都需要读取,PostgreSQL 完全可能选择顺序扫描。所以,出现 Index Scan 并不是裁剪成功的判据。应先检查物理关系名和计划结构,再单独评价剩余每张表内部的访问方式。顺序扫描一张相关分区,仍然可能已经省掉其他分区的大量工作。 条件必须能对应分区键及其比较语义。把日期列转换成文本,可能隐藏直接的边界关系,即使某个字符串条件恰好仍选出相同行。应使用有类型的参数和直接表达目标区间的条件,不能假定分区键外面套任意表达式都保留裁剪能力,甚至不能默认它保留原来的日期语义。 参数也能在执行时触发裁剪 常量可以支持规划期裁剪,使计划只留下相关子表。预备语句的通用计划无法提前知道未来每次传入的参数值,但这不表示它必须扫描全部分区:执行初始化时已经知道的值,也可以用于执行期裁剪。看见通用计划中原本存在多个分支,还不足以判断最终都会读取。 EXPLAIN ANALYZE 用 Subplans Removed 报告初始化阶段排除的分区。另一些情况,例如嵌套循环内侧参数在执行期间变化,可以在实际执行过程中继续裁剪。分析这些场景时,需要查看循环次数和子计划是否运行。某个节点显示从未执行,也可能是外层没有输入等其他原因,不能脱离完整计划单独当成裁剪证据。 因此,不能把这些规则简化成“只要不是常量表达式,就会失去裁剪”。参数何时可用,以及表达式是否受支持,共同决定何时能够排除分区。定制计划和通用计划可能不同,但返回同样答案。尤其要注意,同一个通用计划参数可以支持运行时分区裁剪,却不满足部分索引在规划期要求的谓词证明。这两种优化使用的信息时点不同,不能把一个实验的限制直接套到另一个机制上。真正值得确认的是:负载使用真实参数时,哪些分区实际被访问了。 坏掉的样子 只过滤无关字段,可能让所有分区仍然相关;把键转换成无法对应边界的表达式,也可能产生同样结果。调整分区布局前,应检查准确条件和 enable_partition_pruning 设置。手工添加相互矛盾的 CHECK 约束并不是修复方法;普通本地声明式分区会执行自己的边界规则,而基于 CHECK 的约束排除是另一种优化机制。 裁剪也不保证没有锁或规划成本可以忽略。执行初始化阶段排除的分区,在执行开始时仍然会被锁定。大型分区层级可能消耗规划时间和会话内的元数据内存,即使最后只扫描少数子表。应根据真实查询和维护模式选择层级,而不是追求尽可能多的分区数量。 最后,即使裁掉两个分区,第三个分区内部仍可能有几千条无关行。合适的局部索引可能帮助第二阶段。最终返回一百行,不证明只检查了一百行;缓冲命中也不等于物理磁盘读取。必须沿着计划节点看清扫描、过滤与输出之间的区别。 分区边界证明、初始化裁剪与分区内工作分开。历史计划观察仅限该夹具;排除不证明没有锁。 快速验证 把下面内容保存为 partition-pruning.sql,在可丢弃的 PostgreSQL 18 数据库中执行 psql -X -v ON_ERROR_STOP=1 -f partition-pruning.sql。脚本在事务内启用裁剪,并用通用预备计划展示初始化阶段的行为。临时分区上没有建立任何索引。 ...

二月 13, 2028 · 2 分钟 · XB Field Notes
错开的九月与十月区间共用十月首日边界线;实心起点与空心终点表示包含下界、不含上界。

表分区按范围或键拆分数据

范围分区把边界定义变成写入路由规则。PostgreSQL 按键值把行放到能够接受它的分区,无论这条数据准时到达,还是晚了几个月。任何查询获得裁剪收益之前,都需要先定义正确边界、使用匹配的时间类型,并安排缺失范围的处理流程。分区建表成功,只是这些条件中的一部分。 使用相邻的左闭右开范围 范围分区包含下界,不包含上界。九月可以从九月一日开始,一直到十月一日之前;十月正好从同一边界开始,双方不会重叠。不要靠猜测一个月最后可表示的时间戳来定义范围。相邻的左闭右开区间,不依赖时间字段究竟保留多少位小数,也能避免边界遗漏。 查询同样需要准确表达区间。大于等于下界、小于上界的条件,适合描述一个月的数据。BETWEEN 包含两个端点,因此,把下月第一瞬间写成其上界,可能把下个月的数据也查进来。分区路由只能执行数据布局规则,不能替一个请求了错误区间的条件修正业务含义。 分区键类型与时区也是定义的一部分。timestamptz 代表瞬间,不带时区的 timestamp 则代表没有对应时区解释的本地日期时间。示例为每个边界显式写出 UTC 偏移。如果业务月份按另一个时区划分,应根据那个时区的规则推导边界,而不是假定所有本地日期都对应相同的实际时长。 缺失范围是一种运维状态 没有分区接受新键值时,插入会报错,除非 DEFAULT 分区能够接住它。日历进入新月份,并不会让 PostgreSQL 自动创建下一张月表。应在写入需要之前创建范围,同时考虑迟到数据和旧日期修正。输入键不必单调增长,路由照样能够工作;关键是相应目标范围仍然存在。 DEFAULT 可以接收显式范围之外的值,减少立即发生的插入失败,但如果不检查默认表,也会掩盖分区管理遗漏。持续增长的默认分区可能混入多个互不相关的时间范围,让后续整理更困难。应让“有意保留的例外数据”与“忘记创建分区的日常数据”保持可辨认,不能把插入暂时不报错当成管理正常。 新增显式分区,会改变默认分区能够接受的值。如果新范围对应的行还留在 DEFAULT 中,PostgreSQL 会拒绝变更,而不会允许两处同时拥有这批数据。应先把这些行迁移到合适的暂存表或按规则处理,再挂载新分区。边界是被执行的数据规则,不是可以随意标错的说明标签。 坏掉的样子 下一月分区未创建时,部署可能恰好在跨月瞬间出现写入失败。补建分区又可能因为 DEFAULT 已经积累对应行而失败。这两种问题都属于路由和数据位置,不是规划器统计陈旧。应先检查被拒绝的键和当前边界,再决定操作,不能直接运行 ANALYZE 或改索引,希望它们修复缺失范围。 ATTACH 可能扫描候选表以验证边界,也可能检查默认分区中是否存在冲突数据。合适且已验证的约束能省去部分扫描,但操作仍需要锁,并要求表结构兼容。PostgreSQL 十八的 ATTACH 在父表上取得 SHARE UPDATE EXCLUSIVE,在候选表和存在的 DEFAULT 表上取得 ACCESS EXCLUSIVE。省掉验证扫描,并不等于省掉这些取锁要求。活跃表上迁移数据时,需要协调写入者和锁获取;一个隔离环境中的事务示例,不是完整的在线迁移操作方案。 ...

二月 10, 2028 · 2 分钟 · XB Field Notes
同样四个分数,位置为 1、2、3、4,竞赛排名为 1、2、2、4,密集排名为 1、2、2、3。

窗口函数在不合并组的前提下排名

排名函数为分区内的每一条输入行附加位置,不会把整个团队、部门或客户的数据压缩成一行聚合结果。真正需要选择的是并列的含义:每行占一个独立位置、允许并列且保留空位的竞赛名次,还是按不同分数组连续计数的名次。三种规则解决的问题不同,应先确定业务规则再写窗口。 三个问题对应三种排名 row_number() 从一开始给每行分配连续整数。如果两行的排序值完全相同,而窗口没有其他区分条件,它们的相对序号没有保证。需要恰好选出两位获胜者时,可以按分数降序,再按稳定且唯一的标识排序。这意味着同分时由标识决定谁占据剩余名额,不能把这个结果当成不影响业务的技术细节。 rank() 给同组并列行分配该组第一行的位置。分数为一百、九十、九十、八十时,名次依次为一、二、二、四。两位第二名占用了两个位置,因此下一档分数前面会留下空位。dense_rank() 则计算同分组的数量,对同一批数据给出一、二、二、三,不会因并列人数增加而跳号。 并列组由窗口 ORDER BY 中的全部表达式决定。如果把唯一标识也加入 rank 的窗口,即使本意只是稳定展示顺序,也会让原来的同分行不再并列。row_number 需要标识裁决、rank 需要保留同分时,应使用不同窗口定义。最终结果的展示顺序可以由外层 ORDER BY 控制,不必因此改掉排名所依据的并列关系。 明确分区与过滤时机 PARTITION BY 让每个组从头编号。不写分区条件时,窗口看到的所有输入行属于同一个分区。这和窗口帧是不同概念:排名依据分区顺序和并列组,不是对某段移动聚合帧计数。添加 ROWS 边界,不会把 rank 变成“这个边界内有多少行”的函数。 窗口看到的是 FROM、WHERE、分组和 HAVING 处理后剩余的行。因此,在同层查询中过滤,可能先删掉部分竞争者,再计算名次。如果需要先给所有人排名,再挑前几个位置,应在子查询或 CTE 中生成窗口值,然后到外层过滤。PostgreSQL 不允许直接把窗口函数放进 WHERE,这也提醒读者区分排名输入与排名输出。 “前两名”必须有明确含义。位置不大于二,每个分区最多返回两行;竞赛名次不大于二,会保留全部并列第二名,结果可能超过两行;密集名次不大于二,则选择前两个不同分数组。当第一档分数本身就有多人并列时,密集排名与竞赛排名的筛选结果也可能不同。这些是不同的结果集合,不是可以随意互换的性能写法。 排名保留的是当前查询提供的行,不保证原始实体恰好一行。一对多连接可能在编号之前复制参赛者,导致同一实体占据多个位置。应先说明输入粒度并解决非预期的连接膨胀,再筛选前若干名。事后添加 DISTINCT 即使删掉重复输出,也不一定修复已经计算的名次;连接产生的行数本身就是排名语义的一部分。 坏掉的样子 row_number 缺少确定的并列裁决条件时,同分选手可能在重复查询中交换位置。反过来,在所有窗口都添加标识,又可能悄悄消除 rank 本来要保留的并列。测试时必须放入恰好卡在截止名次上的同分数据,不能只测每个分数都不同的情况。如果分数允许 NULL,还应明确空值顺序;PostgreSQL 降序默认把空值放前面,需要时应显式指定 NULLS LAST。 ...

二月 8, 2028 · 2 分钟 · XB Field Notes
一千旧版本的条越过新门槛 150,却未达到旧门槛 2050,并注明上限禁用或至少 2050,表示符合条件而非完成清理。

自动清理资格不等于执行完成

繁忙表持续产生旧行版本,维护速度需要与这种产生速度匹配。降低自动回收门槛,可以让表更早具备被处理的资格,但“符合条件”并不等于“已经完成”。工作者仍需轮到这张表、获得所需锁,并在积压继续增长之前完成足够的有效清理。调优应观察这个完整循环,不能把一个参数值当成后台吞吐的保证。 坏掉的样子 普通更新和删除的触发条件,由固定门槛加上表行数估计乘比例因子构成;PostgreSQL 18 在启用上限时,还会应用最大触发门槛。插入触发清理、由事务年龄驱动的冻结,又有各自的运行原因。因此,只看一个死元组比例,会漏掉其他维护需求;每看见一次清理就断言更新删除门槛被触发,也可能误判真正原因。 把门槛调低,不能释放仍被旧快照需要的版本,也不能创造额外的磁盘吞吐。增加工作者或降低成本延迟,还可能加剧与前台查询的资源竞争。让死元组估计在每个瞬间都接近零,既不是通用的现实目标,也不能充分说明业务性能良好。真正需要维持的是可持续的清理能力、可接受的文件增长、请求延迟和维护成本。 2050 到 150 的计算要求有效上限禁用或至少为 2050。资格、自动执行与有效完成仍分开;运行待验证。 快速验证 本轮没有重跑原样保留的后台工作者集成。先前的自动清理观察属于历史记录,手动或单用户清理不能代替它。 使用 PostgreSQL 18 可丢弃实例,确认自动回收与统计收集已启用、有可用工作者,并使用正常的一分钟数据库检查间隔。通过一个全新的 psql -X -v ON_ERROR_STOP=1 连接执行下面的脚本。它最多等待一百三十秒,核对真实自动执行;如果超时,应把这次观察记为失败,不能手动清理一次就冒充自动任务已经运行。 CREATE SCHEMA churn_demo; CREATE TABLE churn_demo.jobs (id integer PRIMARY KEY,n integer) WITH (autovacuum_enabled=false, autovacuum_vacuum_threshold=50, autovacuum_vacuum_scale_factor=0.2); INSERT INTO churn_demo.jobs SELECT g,0 FROM generate_series(1,10000) AS g; SELECT pg_stat_force_next_flush(); VACUUM (ANALYZE) churn_demo.jobs; UPDATE churn_demo.jobs SET n=1 WHERE id<=1000; SELECT pg_stat_force_next_flush(); SELECT n_live_tup,n_dead_tup,vacuum_count,autovacuum_count FROM pg_stat_user_tables WHERE relid='churn_demo.jobs'::regclass; SELECT 50+0.2*reltuples AS old_threshold,50+0.01*reltuples AS new_threshold FROM pg_class WHERE oid='churn_demo.jobs'::regclass; ALTER TABLE churn_demo.jobs SET (autovacuum_enabled=true,autovacuum_vacuum_scale_factor=0.01); DO $$ DECLARE deadline timestamptz := clock_timestamp()+interval '130 seconds'; BEGIN LOOP PERFORM pg_stat_clear_snapshot(); EXIT WHEN (SELECT autovacuum_count>0 FROM pg_stat_user_tables WHERE relid='churn_demo.jobs'::regclass); IF clock_timestamp()>deadline THEN RAISE EXCEPTION 'no automatic vacuum observed within the experiment budget'; END IF; PERFORM pg_sleep(0.25); END LOOP; END $$; SELECT pg_stat_clear_snapshot(); SELECT n_dead_tup,last_autovacuum IS NOT NULL AS ran,autovacuum_count>0 AS counted FROM pg_stat_user_tables WHERE relid='churn_demo.jobs'::regclass; SELECT count(*),count(*) FILTER (WHERE n=1) FROM churn_demo.jobs; DO $$ BEGIN IF (SELECT count(*) FROM churn_demo.jobs)<>10000 OR (SELECT count(*) FROM churn_demo.jobs WHERE n=1)<>1000 THEN RAISE EXCEPTION 'maintenance changed application data'; END IF; END $$; DROP SCHEMA churn_demo CASCADE; 测试表最初只关闭自己的常规自动回收,让初始化与受控更新先完成。手动清理并分析建立统计,同时清掉初始化插入积压,然后再更新一千行。显式统计刷新调用用于同步这个短实验,避免待发布的写入增量落在手动维护更新估计值之后;它不是建议在线业务频繁调用的调优开关。 ...

二月 4, 2028 · 1 分钟 · XB Field Notes
用户邮箱改变时,订单 1 仍引用用户编号 1。Identity 生成、键约束与访问授权分别承担不同职责。

代理键把身份与含义分开

代理主键把一行记录的身份与可能变化的业务值分开。客户修改邮箱时,订单仍引用同一个客户编号,而不必跟着邮箱一起改引用。这对于易变或较宽的业务键很有用,但稳定性来自数据库约束与应用规则,不是“自动生成”四个字本身。一个生成的数字不会天然不可修改、保密、全局唯一,也不能代替访问授权。 稳定标识来自规则与约束。生成机制、合法存储值、业务规则、引用和访问决定需要分别保证。 生成身份与约束唯一性 PostgreSQL identity 列从关联序列取得默认值。GENERATED ALWAYS AS IDENTITY 会拒绝普通显式赋值,GENERATED BY DEFAULT 则允许调用方提供值。即使使用 ALWAYS,导入时仍可通过 OVERRIDING SYSTEM VALUE 显式覆盖。两种形式都不会单独保证值唯一;需要拒绝重复标识时,仍须声明 PRIMARY KEY 或 UNIQUE 约束。 主键同时保证唯一与非空,并提供可供引用使用的索引。Identity 负责怎样生成默认值,约束负责哪些最终值允许存储,这两项职责不能混淆。导入时尤其如此:显式写入一个很大的编号,不会自动把序列推进到那个位置。若没有协调序列状态,后续生成值可能最终与导入行相撞,直到主键报错才暴露问题。 单独保留业务唯一规则 代理编号不会阻止相同业务实体重复出现。如果邮箱是唯一登录标识,应继续保留明确的唯一约束,并先决定规范化规则。普通 text 的 UNIQUE 使用该列的比较语义,不会暗中替应用处理大小写折叠、空白或各种邮箱等价规则。迁移之前就要明确这些行为,否则数据库认为不同的值,应用可能认为是同一账号,或者反过来。 子表如果不能出现悬空引用,就需要真实的外键约束。邮箱不是引用键时,修改它不会改变订单指向的客户,因为订单保存的是 id。这缩小了这一项修改的范围,却不能承诺所有业务改名都变成简单操作:搜索索引、审计记录、外部集成和缓存仍可能依赖邮箱,需要按实际调用链更新。数据模型只解决它真正覆盖的关系。 ...

二月 2, 2028 · 2 分钟 · XB Field Notes
会话甲持锁,连接会话乙的线被挡住,乙等待;观察者丙指向等待侧。

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

一条过去很快的查询,可以在数据、统计和访问计划都没有变化时变慢。另一个事务可能持有它需要的锁,存储缓存可能比上次更冷,应用也可能在语句进入 PostgreSQL 之前就开始等待。重新检查计划很有用,但它应当放在完整的耗时比较中:先区分时间是花在执行工作上,还是花在等待获得执行机会。不能从“现在更慢”直接推导出“数据与统计一定都变了”。 保留可比较的基线 记录语句形状、有代表性的参数值、返回行数、相关会话设置,以及应用是否使用预备语句。不同参数可能命中完全不同规模的数据。手工使用常量的查询,不一定复现预备语句采用的通用计划;不同角色或搜索路径,也可能改变实际对象与权限。因此,拿一条看起来相似的语句作为对照,并不保证比较条件相同。 尽量在变慢正在发生时观察活动。pg_stat_activity 可以显示一个状态为运行中的查询正在等待锁;运行中并不等于持续消耗处理器。pg_blocking_pids 列出当前阻塞这个后端的会话。查看其他角色活动需要适当权限,因此字段不可见不等于没有工作发生。交互观察最好使用新的自动提交查询,而不是在同一个长事务中不断读取已经缓存的监控快照。 坏掉的样子 不加判断就运行 ANALYZE,相当于把每次变慢都当成统计陈旧。它可能增加维护工作,却完全没有解除真正的阻塞。一张只有一行的表,即使新增索引,也不能释放其他事务持有的行锁。准确的行数估计和很小的顺序扫描,可以与很长的应用等待同时存在;这两种观察并不矛盾。 EXPLAIN ANALYZE 会真正执行语句,写入也不例外,而且测量本身同样可能被阻塞。从生产环境复制一条大范围更新,不是无害的检查。应先看等待情况和锁的拥有者,再选择安全夹具或副作用已经明确的语句来测量。看到某个进程编号出现在阻塞列表中,也不能立即终止不熟悉的事务,因为它可能正在完成另一个有效业务操作。 没有锁等待的计划样本,也不等于端到端延迟。EXPLAIN ANALYZE 不会像普通查询那样把结果行发送给客户端;未指定 SERIALIZE 时,还不包含输出转换。TIMING OFF 减少逐节点计时开销,但仍保留总执行时间。大量结果传输、应用解码和连接池排队,需要在计划之外单独观察,不能从这里的执行时间中直接读出。 多会话过程归属于历史报告,未找回原始日志;打印观察需人工检查,超时是独立失败结果。 快速验证 打开三个新的 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,阻塞列表包含甲的进程编号。具体事件可能随争用过程变化,核心证据是乙确实被甲阻塞。该报告称甲回滚后,乙把零加成一,最后查询返回一,清理移除了模式。必须人工确认丙恰好识别出乙、阻塞列表包含甲报告的 pid,且最终值为一;SQL 只是打印这些观察,没有自动断言。 ...

一月 31, 2028 · 1 分钟 · XB Field Notes