29 深入专题:内核机制与内部实现

29 深入专题:内核机制与内部实现

AIO、Direct I/O、共享内存结构、查询执行阶段等内核级细节。

Anti Join 的 join 顺序为什么不能像 inner join 一样随便重排?它的物理实现有哪些?

Anti Join 的右侧只用于否决左侧行,如果把右侧提前或滞后到错误位置,可能改变「哪些左侧行被认为存在匹配」的范围。PostgreSQL 用 SpecialJoinInfo 记录外连接、Semi Join、Anti Join 的约束,保存在 PlannerInfo.join_info_list 中,供 join_is_legal 排除非法 join order,因此复杂查询里反连接会限制搜索空间。JOIN_ANTI 是逻辑算子,可落成 Hash Anti Join、Merge Anti Join、Nested Loop Anti Join、Hash Right Anti Join 等物理形态。执行器语义统一:对当前外侧行查找匹配,命中即丢弃(Hash join 设 HJ_NEED_NEW_OUTER,Nestloop 设 nl_NeedNewOuter),候选查完仍无匹配才输出。成本上「有匹配」可提前停,「无匹配」则必须完整探测,因此 Anti Join 的成本风险主要在确认不存在这一步。

EXISTS、IN、= ANY 这三个写法与 Semi Join 是什么关系?它们的 NULL 语义有何区别?

EXISTS 的结果只取决于子查询是否返回至少一行;IN 等价于 = ANY;ANY 只要任意一行比较为 true 就为 true。解析/分析阶段它们分别对应 EXISTS_SUBLINK、ANY_SUBLINK(IN 是 = ANY 的一种)。三者都可能被优化器识别并转成 JOIN_SEMI。NULL 语义上:IN 和 ANY 在「无匹配但存在 NULL 比较结果」时可能返回 NULL 而不是 false,因此并非任何时候都能等价为二值逻辑的半连接;EXISTS 则按子查询是否返回行判断,NULL 风险更低。这也是实践上「表达存在」优先写 EXISTS 的原因——语义清晰且最贴近 Semi Join 入口。

EXPLAIN ANALYZE 显示 quicksort 和 external merge 有什么区别?如何据此调优 work_mem?

显示 Sort Method: quicksort 说明数据完全在内存完成,Memory 表示峰值内存;显示 Sort Method: external merge Disk 说明输入超过 work_mem 落了盘,产生了临时文件和归并 pass。调优方向:work_mem 太小会产生更多 run 和 merge pass、临时 IO 增加;work_mem 太大则每个排序/哈希节点、每个并行 worker 都有自己的预算,会把并发查询推入内存压力甚至 OOM。排序比较器本身也可能很贵(多列、复杂 collation、宽 tuple、pass-by-reference 类型)。另外注意:SQL 结果需要稳定同 key 顺序时必须显式写 tie-breaker(如追加主键),不要依赖排序算法稳定性。若索引或上游节点已提供目标顺序,优化器可能直接避免显式 Sort。

Incremental Sort 的前提是什么?它的成本模型有什么风险?

前提不是「有索引」,而是下层路径的 pathkeys 覆盖了目标排序键的前缀,顺序可能来自 B-tree Index Scan、GiST 距离扫描、上游 Sort、MergeAppend、Merge Join 外侧等。优化器用 pathkeys_count_contained_in() 判断:目标 pathkeys 被完全覆盖则无需排序,只覆盖前缀且 enable_incremental_sort=on 时构造 IncrementalSortPath。成本模型(cost_incremental_sort)先估算前缀键把输入切成多少个 group(estimate_num_groups),再估算单组 tuplesort 成本并乘以组数,加上分组检测、tuple copy、tuplesort_reset 的额外成本。核心风险是 input_groups 估计:统计信息低估组大小会以为每组很小、实际遇到少数巨型组;高估组数则会低估频繁 reset 的开销。

Incremental Sort(增量排序)解决什么问题?它的核心思想是什么?

它解决「目标排序键有多列,而输入已经按前若干列有序」的问题。普通排序把 N 行按完整 key 从零排,全部排完才能输出;Incremental Sort 则把输入按已有序的前缀键(如索引扫描已按 hundred 输出)切成连续的前缀组,只在每组内补排后缀列,排完一组即可输出一组。收益有三:更低启动延迟(LIMIT 特别受益)、更低峰值内存(更可能落在 work_mem 内)、更低落盘概率。EXPLAIN 里显示 Sort Key(完整目标键)和 Presorted Key(下层已保证的前缀键)。代价是要检测前缀组边界带来 tuple copy/比较成本,小组极多时频繁 reset tuplesort 会抵消收益,且成本模型依赖前缀组数估计,统计信息不准时可能选错计划。

LATERAL JOIN 解决什么问题?它有哪些语义要点和代价?

LATERAL 解决「依赖式 FROM 项」问题:右侧 FROM 项的输入参数来自左侧已产生的行。典型场景包括每行取子表 Top-N、每行调用返回集合的函数(如 jsonb_array_elements、unnest)、每行执行低代价派生查询、用 LEFT JOIN LATERAL … ON true 保留无匹配外侧行。四个语义要点:右侧 LATERAL 项只能引用左侧已可见的 FROM 项(从左到右解析);对每个外侧行右侧重新求值;FROM 中函数参数天然可引用左侧,LATERAL 关键字对函数可选;组合 join 类型必须是 INNER 或 LEFT。代价是依赖关系限制 join 顺序,参数化内侧路径通常在 Nested Loop 内部反复执行,外侧行多、内侧无索引、右侧结果集大时可能把一次大扫描变成大量小扫描。

LATERAL 在 PostgreSQL 优化器里如何建模依赖并生成执行计划?

解析阶段从左到右处理 FROM,新 FROM 项先标记为 lateral_only(只对 LATERAL 表达式可见),处理完整个 FROM 列表才变成普通可见;函数 RTE 只要参数引用了同层外侧变量,即使没写 LATERAL 也会被标记为 lateral。进入优化器后,create_lateral_join_info() 为每个 base relation 填充 direct_lateral_relids、lateral_relids(传递闭包,X 依赖 Y、Y 依赖 Z 则 X 依赖 Z)和 lateral_referencers,用 lateral_relids 限制 join 顺序——被引用的外侧关系必须先算。随后优化器为含 lateral 引用的 RTE 生成参数化路径,这类路径必须由外侧关系提供参数。计划生成阶段 create_nestloop_plan() 把外侧 Var/PlaceHolderVar 替换成 NestLoopParam(PARAM_EXEC),执行期由 Nested Loop 给内侧填参数并重扫。

LEFT JOIN 在什么条件下会被优化器降级为 inner join 或删除?

两类改写。一是外连接降级(reduce_outer_joins):当 WHERE 条件对右表列施加严格条件时,如 LEFT JOIN b ON … WHERE b.y=42,若 = 是严格操作符,b.y 为 NULL 时条件不可能为 true,LEFT JOIN 为未匹配 a 行补出的 NULL 行必然被过滤,故可降级为 inner join;进一步若 WHERE b.z IS NULL 且能证明匹配行的 b.z 必非空,则实际表达的是 anti join。二是无用连接移除(remove_useless_joins):当 LEFT JOIN 右侧列不被上层引用、且能证明右侧在连接键上唯一(否则删除会改变重复行数)时,该 join 不改变结果行数,可整体删除。核心约束是必须先证明语义正确,不能只图看起来优雅。

PostgreSQL 14 为什么移除了 non-fast promotion?

fast promotion 在 9.3 引入后,non-fast promotion 就变成未文档化特性,主要留作调试或 fast promote 有问题时的应急手段。到 PG14 时多个版本已经验证 fast promote 足够稳定,再保留 non-fast promotion 没有意义,因此提交 b5310e4 将其移除。fast promote 只写一个轻量级 end-of-recovery 记录即完成提升,速度更快。

PostgreSQL 14 引入 WaitLatch/WaitEventSet 优化了什么?

PG14 用 WaitLatch 和 WaitEventSet 统一了等待机制:此前 condition variable 等需要为每次等待创建长期 WaitEventSet,或硬编码等待,带来额外 epoll/kqueue 系统调用。引入后 WaitLatch 内部复用类似的 WaitEventSet,避免每次等待都做系统调用,也省去多余的内核描述符。例如 stats collector 的等待改为一个 WaitEventSet 管理 latch、postmaster death、socket 可读等事件。

PostgreSQL 15 引入 MERGE 语法解决什么问题?与 INSERT ON CONFLICT 有何关系?

MERGE 用于 ETL、数据合并等场景,根据匹配条件统一执行 INSERT/UPDATE/DELETE(WHEN MATCHED / WHEN NOT MATCHED 分支),语义比 INSERT … ON CONFLICT 更通用。ON CONFLICT 只能处理基于唯一约束冲突的 upsert,而 MERGE 可基于任意 join 条件做更复杂的同步。PG15 引入基础 MERGE,PG17 进一步支持 RETURNING 和 WHEN NOT MATCHED BY SOURCE。

PostgreSQL 17 有哪些值得期待的重量级特性?

PG17 亮点:pg_basebackup 支持块级增量备份(pg_combinebackup 重构,大库备份不必全量拷贝);逻辑复制 failover/switchover(0 丢失),pg_upgrade 可保留逻辑复制槽;COPY 支持 skip error row(SAVE_ERROR_TO);vacuum 引入 TidStore 打破 dead tuple 上限、省 20 倍内存并提速;btree 倒序扫描优化、brin 并行建索引;WAL 锁优化使高并发写入提升约 2 倍;MERGE 支持 RETURNING 和 WHEN NOT MATCHED BY SOURCE;新增 MAINTAIN 权限等。

PostgreSQL 17 的 TidStore 数据结构改进了什么?

vacuum 需要记录 dead tuple 的 tids,之前用数组存储有上限(对应单表记录数受限,旧说法约 8.9 亿条)。PG17 引入 TidStore 数据结构,只要内存足够,dead tuple ids 不再有固定上限,索引不再需要被多次扫描,相比以往能节省约 20 倍内存,并大幅提升 vacuum 效率,一定程度缓解 xid wraparound 问题。

PostgreSQL 18 的 AIO 子系统在 io_method 上有哪些选择?

PG18 新增 io_method GUC,可选 sync(旧的同步 IO)、worker(专用 IO 工作进程,通过 io_workers 配置数量,主后端入队请求后继续执行)、io_uring(仅 Linux,用内核 io_uring API 提交和完成 IO,无需单独工作进程)。配套参数 io_combine_limit/io_max_combine_limit 控制单次请求可合并多少相邻块,effective_io_concurrency 默认提到 16。可用 pg_aios 视图监控进行中的异步 IO。

PostgreSQL 18 的 AIO 目前覆盖哪些场景,写操作呢?

PG18 中 AIO 覆盖顺序扫描、bitmap heap scan 以及 VACUUM 等维护操作(配合 ReadStream 预读)。写操作目前保持同步。AIO 通过并发发起多个读请求,减少云盘/网络存储高延迟下的 CPU 空闲,冷缓存场景读取性能可提升一倍到三倍,云环境收益尤其明显。

PostgreSQL 18 被谈及最多的三个特性是什么?

  1. 异步 I/O(AIO):让后端并发发起多个读请求,云盘/网络存储下 OLAP 查询性能显著提升;2) UUID v7(uuidv7()):时间戳编码在最高 48 位,分布式无协调生成且时间有序,插入在 B-tree 右侧聚集,索引局部性好、减少 WAL 和页分裂;3) OAuth 2.0 身份验证:pg_hba.conf 新增 oauth 方法,接受 RFC 6750 bearer token,配合 oauth_validator_libraries 验证,支持企业 SSO。

PostgreSQL 为什么需要 TOAST?它如何做到「行不能跨页」却仍能存大字段?

PostgreSQL 使用固定页面大小(常见 8 KB)且不允许物理 tuple 跨多个页面,大字段无法直接无限放进主表行。TOAST(The Oversized-Attribute Storage Technique)把问题改写为主表行保持短小、大值先压缩、必要时切成小 chunk 存到关联的 TOAST 表,主表只保存一个 18 字节的 on-disk 指针。指针(varatt_external)包含 va_rawsize(原始大小)、va_extinfo(外部大小+压缩方法)、va_valueid(chunk_id)、va_toastrelid(TOAST 表 OID)。TOAST 表只有 3 列 chunk_id(oid)、chunk_seq(int4)、chunk_data(bytea),并有唯一 B-tree 索引 (chunk_id, chunk_seq) 供 detoast 按序重组。

PostgreSQL 从库的刷脏策略由哪些参数控制?

从库的脏页刷新分两类:Background Writer 相关(bgwriter_delay、bgwriter_lru_maxpages、bgwriter_lru_multiplier、bgwriter_flush_after)控制常规脏页刷新;Restartpoint 相关(checkpoint_timeout、checkpoint_completion_target、checkpoint_flush_after)控制重启点期间的刷盘。WAL buffer 由 wal_writer_delay 和 wal_writer_flush_after 单独控制。这些参数可在 postgresql.conf 设置,多数 SIGHUP 重载生效。

PostgreSQL 单个 varlena 值为什么上限约 1 GB?什么时候才会真正触发 TOAST?

varlena 头部要用两个 bit 标记短头、压缩、外部指针等特殊形态,导致 TOAST-able 类型单个 datum 的逻辑大小上限变成 2^30-1 字节,即约 1 GB(这不是 TOAST 表总大小上限,表本身可增长到很多 segment)。触发条件不是「字段超过 2 KB 就一定外置」:TOAST 判断的是整行是否过宽(默认 8KB block 下 TOAST_TUPLE_TARGET 约 2KB、TOAST_TUPLES_PER_PAGE=4),当待存行宽超过阈值时才触发,随后按列策略先压缩、再外置,直到行宽降到目标以下或没有收益。短值即使属于 text 也可能完全内联。

PostgreSQL 外部归并排序(External Merge Sort)如何工作?run 是怎么生成的?

当输入超过 work_mem 时,tuplesort 进入外部排序:在 memtuples[] 中积累 tuple,超过 work_mem 后 dumptuples() 把当前内存批次排序成一条有序 run,写入 logical tape(logtape.c),清空内存继续接收下一批;输入结束后对多条 run 做 balanced k-way merge 输出全局有序结果。关键澄清:当前外部排序不是所有阶段都用 mergesort——run 内部由 quicksort 或 radix sort 排序(tuplesort_sort_memtuples 对整数类 leading key 用 radix、单 key fast path 用 qsort_ssup、通用多列用 qsort_tuple),run 之间才是 balanced k-way merge。所以 EXPLAIN ANALYZE 里同时出现 quicksort 和 external merge 并不矛盾。状态机从 TSS_INITIAL 切到 TSS_BUILDRUNS 生成 run,TSS_SORTEDONTAPE 表示最终结果在 tape,TSS_FINALMERGE 表示边归并边返回、省掉最终物化的一轮 IO。

PostgreSQL 大对象(large object)为什么需要?和 bytea 有什么区别?

大对象本质是带 ACID 的文件操作 API 封装。bytea 一行最多存 1GB,而大对象可存超过 1GB 的单个文件;大对象支持 seek 局部读写(普通类型只能整体 replace);支持稀疏写入(seek 到 10GB 只写 1MB 只占 1MB,未写 block 读返回 0)。大对象数据存 pg_largeobjects 表,通过 OID 引用,一个 OID 可被多条记录引用。注意删除记录往往只删引用不删大对象本体,需用 lo_unlink(oid) 或 lo 插件的触发器自动清理。

PostgreSQL 如何在十进制、十六进制、二进制、八进制之间转换?

十进制转十六进制用 to_hex(10)=‘a’,转二进制用 10::bit(4)=‘1010’(注意 bit(n) 长度不足会截断,如 10::bit(1)=‘0’)。十六进制转十进制用 x’A’::int=10,转二进制用 x’A’::bit(4)=‘1010’。二进制转十进制用 B'1010’::int=10,转十六进制用 to_hex(B'1010’::int)=‘a’。varbit 类型如 x’bcd’::varbit 可得到变长二进制串。

PostgreSQL 如何用自关联外键表达树形元数据结构?

在表里让 parent_id 列 REFERENCES 同一张表(self-referential foreign key)。例如 CREATE TABLE tree(node_id int PRIMARY KEY, parent_id int REFERENCES tree, name text),顶层节点的 parent_id 为 NULL,非顶层节点的 parent_id 必须指向已存在的行。可扩展多个自关联外键表达多汇报关系(实线/虚线汇报)。插入不存在的父节点会报违反外键约束。

PostgreSQL 存在哪些单核瓶颈场景?

PG 虽支持并行查询,但仍有一些单核路径:WAL writer、vacuum 单表/单分区、checkpointer、崩溃 recovery、bgwriter。写压力大时可能撞上 data block extend exclusive lock 或 wal insert exclusive lock;大表高并发更新会导致 vacuum 赶不上垃圾产生速度,引发表膨胀;shared_buffer 大且脏页多时 checkpoint 周期长,崩溃恢复需重放大量 WAL。缓解靠拆库、拆表/分区、用更快的 SSD,未来方向是内核支持更并行化的后台任务。

PostgreSQL 孤儿文件(orphaned file)是怎么产生的?如何发现和清理?

孤儿文件类似内存泄露:数据库崩溃时有未提交事务(内含 DDL 和大量导入),重启后 pg_class 等元数据回滚,但对应数据文件未被清理。典型场景:pg_dump 导入中崩溃、事务内建表写大量数据后提交前崩溃。发现时不能用 pg_class.oid 直接对照文件名,因为 table rewrite(vacuum full、cluster、alter table)后 filenode 会变,正确做法是用 pg_relation_filenode(pg_class.oid) 得到真实文件名再对照 pg_ls_dir 列出的文件名。

PostgreSQL 异步提交(synchronous_commit=off)有哪些风险点?

异步提交指事务返回成功后 WAL 可能尚未持久化,风险是数据丢失而非数据损坏:最多丢失约 3 倍 wal_writer_delay 时间内的 WAL(且小于 wal_buffer)。OOM、shutdown immediate、数据库/服务器 crash 都会导致 wal buffer 内容丢失。DDL 和 2PC 事务强制同步提交不受影响。业务有逻辑依赖(后续事务依赖前面事务的提交)时要注意 crash 后「已提交」事务可能回退。任何情况都不要用 fsync=off(那会破坏一致性导致 corruption)。

PostgreSQL 引入 Direct I/O 和 Asynchronous I/O 的动机分别是什么?

Direct I/O 的动机:降低 CPU 开销(避免内核 page cache 到 shared buffer 的拷贝,可用 DMA)、避免 OS cache 与 shared_buffers 双份缓存、更好地控制脏数据写回时机、支持 WAL 并发写。AIO 的动机:没有 AIO 就无法用 DIO;AIO 让启动 IO 与等待结果分离,可并发发起多个 IO 并在等待时执行 CPU 任务,对 fdatasync 这类操作系统无法隐藏延迟的操作用异步发出能显著提升吞吐,尤其是 WAL 写入。

PostgreSQL 有哪些数据扫描方法?分别适用什么场景?

常见扫描类型:Seq Scan 顺序扫描读全表,适合小表或返回行占比较高;Index Scan 索引扫描两步(查索引+回表取行),适合大表取少量行;Bitmap Index Scan + Bitmap Heap Scan 位图扫描,先把匹配的索引页聚成位图再按物理顺序读堆页,适合命中行数中等、多列各自有索引用 AND/OR 连接;Parallel Seq Scan 并行顺序扫描,把表分块给多个 worker 同时扫再 Gather 汇总,适合大表。约返回 10% 行以上时顺序扫描往往更快。

PostgreSQL 物理从库有检查点吗?它叫什么?

物理从库没有自己的 checkpoint,但有类似机制叫 restartpoint(重启点)。从库的 checkpointer 进程在恢复期间执行的是 restartpoint 而非新 checkpoint。它不能创建新的 checkpoint 记录,只能在回放到主库的 checkpoint 记录位置时执行 restartpoint,且受 checkpoint_timeout 时间条件限制,可能跳过。restartpoint 用于回收 WAL 文件、刷新脏页、更新 pg_control,避免崩溃后重扫大量 WAL。

PostgreSQL 的 query rewrite(查询重写)发生在哪几个层次?与规则系统是什么关系?

可分成三层。第一层是「查询规则重写」,位于 src/backend/rewrite,处理视图展开、自定义规则、行安全策略(RLS)、可更新视图改写等,入口是 QueryRewrite(),它位于 parser 之后、planner 之前。第二层是 planner preprocessing,位于 optimizer/prep 与 planner.c,做 ANY/EXISTS 拉起、函数 RTE 内联、子查询 pull-up、外连接降级等,典型调用顺序见 prepjointree.c:pull_up_sublinks → preprocess_function_rtes → pull_up_subqueries → flatten_simple_union_all → reduce_outer_joins → remove_useless_result_rtes。第三层是 planner simplification,在统计信息可用后进一步删减搜索空间(如无用左连接移除、唯一性证明)。规则系统是用户可定义的语义展开,preprocessing 则是 planner 内部的语义保持变换,两者容易被混淆。

PostgreSQL 的 simplehash 和 dynahash 各有什么优缺点?

dynahash 的优点:支持分区(便于共享内存加锁访问)、共享内存哈希启动时分配固定区域且可被其他进程按名称发现、冲突时不移动条目对大条目性能更好、保证稳定指针。simplehash 的优点:模板化生成、无间接函数调用(小条目更快)、开放寻址有更好 CPU cache 行为、类型安全、在 MemoryContext 分配无需单独内存上下文,但不适合共享内存。简言之 dynahash 适合共享内存大条目,simplehash 适合进程内小条目高性能。

PostgreSQL 的 work_mem 到底什么时候释放?

work_mem 不是预分配、也不是申请后一直保留到事务结束的内存。它是排序/哈希等操作可用的软预算:用多少申请多少,超限转 spill 到磁盘,大量内存在 SQL 执行过程中就被提前回收,最晚在语句结束时由 executor 统一回收,一般不会等到事务提交。三类算子释放方式不同:Sort 边做边回收(bounded sort 淘汰、tape 用尽即释放),Hash Join 按 batch reset 批量回收,Hash Agg 在 tuple 级、spill batch 级和节点结束多粒度释放。

PostgreSQL 连接失败常见的排查点有哪些?

从网络到数据库逐层排查:客户端到数据库网络是否通、防火墙是否放行、listen_addresses 是否监听对应网段、客户端认证方法是否与 pg_hba.conf 一致、HBA 是否从上至下第一条匹配规则放行/拒绝、HBA 是否配置了允许该 ip/user/db 登录、用户是否有 login 权限、是否有 login hook 拦截。HBA 规则是自上而下匹配,命中第一条后不再看后续。

Prepared Statement(绑定变量)在连接池事务级复用下为什么可能失效?

事务级连接池复用模式下,每个事务结束后后端连接可能切换给其他会话,下次请求用的可能是不同后端连接。而绑定变量(prepared statement)等会话级属性绑定在特定后端连接的会话上,连接切换后这些会话状态不复存在,导致绑定变量失效。代价是无法复用解析和计划,高并发下性能下降、CPU 上升。这也是 PG 期望内核支持共享会话状态连接池(类似 Oracle shared server)的原因。

SQL 在 PostgreSQL 中运行经过哪五个阶段?

五个阶段:解析 Parsing(文本转解析树,识别 SQL 句法成分但不知语义)→ 分析 Analysis(解析表/列引用、类型检查、权限检查,得到语义验证的查询树)→ 重写 Rewriting(视图展开、RLS 策略注入、自定义规则等自动转换)→ 规划 Planning(选择访问路径、连接顺序和连接算法,生成执行计划)→ 执行 Execution(执行器按计划产出结果)。

Semi Join(半连接)解决什么问题?它与「普通 join + DISTINCT」的区别是什么?

Semi Join 解决「存在性过滤」问题:保留左表中那些在右表能找到至少一个匹配的行,只输出左表行。它与 INNER JOIN + DISTINCT 的差别在于:INNER JOIN 关心「所有匹配行对」,右表一条订单匹配 3 笔支付就产生 3 个行对,业务还得再 DISTINCT 去重;Semi Join 只关心「右表是否至少存在一条匹配」,找到第一条就足够,右表重复不会放大输出。收益有三:避免重复放大、避免无用列传递、给执行器留下「首个匹配即停止」的空间(省 CPU/IO/内存)。牺牲是它不返回右表列,也不告诉匹配了哪一条;需要流水号、最近时间、匹配数量等字段时要改用普通 join、聚合、窗口函数或 LATERAL … LIMIT 1。

TOAST 的四种 storage 策略(PLAIN/EXTENDED/EXTERNAL/MAIN)分别是什么含义?插入更新时的处理顺序?

pg_attribute.attstorage 控制列存储方式:PLAIN 禁止压缩也禁止外置(固定长度或禁止 TOAST);EXTENDED 允许压缩也允许外置,是默认主力策略,先压缩再外置;EXTERNAL 允许外置不压缩,利于大 text/bytea 的切片读取;MAIN 允许压缩但尽量留在主表,实在放不下才外置。插入/更新时 heap_toast_insert_or_update() 按四轮处理:先对 EXTENDED 列尝试内联压缩,若某 EXTENDED/EXTERNAL 列本身极大立即外置;整行仍超目标则继续外置 EXTENDED/EXTERNAL 列;还放不下才对 MAIN 列尝试内联压缩;最后仍放不下才外置 MAIN 列并使用更宽松目标。若 PLAIN 列导致行最终仍放不下,插入或更新会失败。

pg_stat_statements 没有 p95/p99,如何估算 SQL 响应时间的百分位数?

pg_stat_statements 只统计执行次数、min/max/均值、标准差、总和。可假设数据服从某种分布来估算百分位数:正态分布用 X_p = μ + Z_p·σ(P90 Z=1.282、P95 Z=1.645、P99 Z=2.326);对数正态分布 X_p = e^(μ_L + Z_p·σ_L),其中 σ_L=√ln(1+(σ/μ)²)、μ_L=ln(μ)-σ_L²/2;均匀分布 X_p = min + p·(max-min);指数分布 X_p = -ln(1-p)/λ(λ=1/μ)。前提是分布假设合理,否则结果不准。

pgbench 的 random_zipfian 用来生成什么分布的数据?

random_zipfian 生成有界 Zipfian 分布(离散幂律/长尾)随机数,用于模拟长尾模型,如热键集中的场景。parameter 越大分布越偏斜,靠近区间起点的值被抽中越频繁:抽到 k 与 k+1 的概率比为 ((k+1)/k)**parameter。例如 random_zipfian(1,…,2.5) 中 1 出现频率是 2 的 (2/1)**2.5≈5.66 倍。配合 permute 可把热值映射到随机位置,生成贴近真实的热点数据做压测。

为什么 EXISTS 有时变成 Hash Semi Join、有时仍是 SubPlan?子查询拉起的收益和限制是什么?

pull_up_sublinks() 会尝试把顶层 WHERE/JOIN-ON 中的 ANY、EXISTS、NOT EXISTS 改写成 semi join 或 anti join,使子查询内部关系进入 rangetable、相关条件变成 join qual,从而让 planner 可以选择 hash/merge/nested loop、参与 join 顺序搜索和索引选择。但改写范围受限:它只在顶层条件中安全,因为嵌在复杂布尔表达式里的 ANY 涉及 NULL 时可能需返回 FALSE 或 NULL,不能简单改成 join;此外 NOT IN 尤其危险——右侧出现 NULL 时三值逻辑会改变结果,只有能证明 NULL 语义不被破坏才可转为 anti join。相关子查询含易失函数、CTE 物化边界等也会阻止改写,此时保留 SubPlan。

为什么 NOT IN 遇到 NULL 时结果可能是 NULL 而不是 true?

这是三值逻辑问题。官方文档规定 NOT IN 在两种情况返回 NULL:左侧表达式为 NULL;右侧没有相等值但至少有一行让比较结果为 NULL。例如 SELECT 2 NOT IN (1, NULL) 的结果是 NULL,因为 2 和 NULL 比较(2=NULL)得到 unknown,而不是 false。在 WHERE 中 NULL 和 false 一样不保留行,所以 WHERE a.k NOT IN (SELECT b.k FROM b) 会「悄悄丢行」,与 NOT EXISTS 的语义不一致。要安全转成 Anti Join,必须证明外层表达式与子查询输出列都非空、且操作符对非 NULL 输入不会返回 NULL。这也是为什么列有明确 NOT NULL 约束时 NOT IN 才相对安全。

为什么 NOTIFY 即使不在显式事务里也会触发同样的全局锁问题?

因为单条 NOTIFY 语句自带隐式事务(每执行一条语句 PG 会隐式开启并提交一个事务,txid 会递增)。所以即便没有 BEGIN…COMMIT,NOTIFY 仍然在隐式事务的提交阶段获取那个 database 0 的 AccessExclusiveLock,全局锁问题依旧存在。

为什么 PG 高并发短连接、大量连接写小事务性能差?

PG 是进程模型,每个新建连接在服务端 fork 一个新进程对接,频繁建连断开导致 fork/资源开销大。大量连接写小事务还伴随锁竞争和 WAL 竞争。缓解:在业务和数据库之间加连接池(如 pgbouncer),用事务级复用模式。代价是多一跳增加 RT,且事务级复用下会话级属性(绑定变量、临时表、SET 等)无法跨连接保留,可能降低高并发性能。

为什么 PostgreSQL 的 count(*) 查询慢?

PG 没有行数计数器,count 必须真正扫描数据(seq scan/index scan/index only scan/bitmap scan),IO/CPU/内存开销大。不引入计数器是因为:计数器更新会成为并发写入热点影响插入删除性能、通常只能做全表计数场景有限、且计数器无版本信息无法满足 tuple 可见性判断(不符合 ACID)。优化:判断有无记录用 LIMIT 1 而非 count;实时 PV/UV 用 redis/物化视图/流计算;偶尔 count 用并行;静态日志/历史表用列存或 index only scan;行数估算用 reltuples/explain/采样/HLL。

为什么 Recall.ai 说 LISTEN/NOTIFY 在高并发写入下会「吃掉」CPU?

根源是 NOTIFY 在事务提交阶段会获取一个针对整个实例的全局锁:LockSharedObject(DatabaseRelationId, InvalidOid, 0, AccessExclusiveLock),即「object 0 of class 1262 of database 0」,横跨所有数据库所有对象。任何带 NOTIFY 的事务提交都会持有该排它锁,导致所有并发提交串行化。大量并发写入时表现为锁等待激增、CPU/IO 反而下降(都在等锁),造成「CPU 被吃」的假象。高扩展多写场景应避免在事务里用 LISTEN/NOTIFY。

为什么 spill 到磁盘不一定是坏事?

spill 是 PG 在「内存安全」和「执行效率」之间的合理平衡:当排序/哈希工作集超预算时,把部分数据写到磁盘、降低内存压力,而不是让内存无限膨胀导致 OOM。如果看到 spill 就一味调大 work_mem,单个算子确实更少 spill,但复杂 SQL 有多个排序/哈希节点时整体内存峰值可能暴涨,并发一上来反而更危险。因此适度 spill 往往是更稳妥的选择。

为什么分布式场景推荐用 UUID v7 而不是 UUID v4 或自增 ID?

自增整数需要单一事实来源,分片/多主下协调序列号有网络延迟和单点风险。UUID v4 全局唯一、各节点独立生成,但完全随机导致插入分散到整个 B-tree,索引页分裂多、缓存命中差。UUID v7 把毫秒级时间戳放最高 48 位、其余随机,既本地生成无协调,又时间有序——同时刻生成的 ID 相邻,插入集中在 B-tree 右侧,减少 WAL 和页分裂,提高写入和缓存命中率,特别适合分布式水平扩展。

为什么有的 SQL 用 pg_cancel_backend / pg_terminate_backend 都杀不掉?

后端不是瞬间响应中断信号,而是在安全点通过 CHECK_FOR_INTERRUPTS() 检查并处理 QueryCancel(SIGINT)/ProcDie(SIGTERM) 标志。若代码处于 HOLD_INTERRUPTS() … RESUME_INTERRUPTS() 区间(或 critical section),中断被推迟,直到离开 hold 区间后才处理。因此杀不掉往往是因为进程正处在不处理中断信号的阶段。可用 pstack 查看进程卡在哪段代码,确认是否调用了 hold 中断。

为什么用 telnet 探测 PG 端口会出现「invalid length of startup packet」日志?

telnet 只是建立 TCP 连接,不会发送符合 PG 协议的 startup packet,服务端收到不合法长度的启动包就报 08P01 invalid length of startup packet。这属于未遵循 PG 通信协议的探测方式,正常但会污染日志。建议改用 PG 官方探测客户端 pg_isready,它遵循 PG 协议更友好。

为什么说 AIO 之后 PG 未来将大力发展 Direct I/O?

Direct I/O 绕过 OS page cache 层,避免了内核开销和可扩展性限制。实测中开启 debug_io_direct=‘data’ 后大表顺序扫描吞吐从约 3.7GB/s 提升到 6GB/s 且 CPU 占用更低,perf 显示耗时从内核转到 PG 内部。但 DIO 依赖 AIO(否则慢得不可用)、需要显式预取(ReadStream 已应用于顺序扫描/bitmap heap scan/VACUUM,普通索引扫描尚未支持),且 page cache 的写缓冲能力 AIO 目前也不支持写。因此 DIO 是方向但短期不会完全落地。

为什么说 work_mem=64MB 不等于这条 SQL 最多只用 64MB?

work_mem 是每个排序/哈希操作的预算上限,而不是整个 SQL 或整个连接的总预算。一条 SQL 里可能有多个 Sort、多个 Hash Join、多个 Hash Agg,每个节点可分别使用自己的预算;叠加并行执行,实际内存消耗会成倍放大。另外 Hash 类操作还会乘 hash_mem_multiplier。所以内存峰值来自「同时申请预算的节点数量叠加」,这是调优 work_mem 最易踩的坑。

什么是 PG 的 Double Cache 问题?如何缓解?

PG 数据读写走 buffer IO,数据在 OS page cache 和 PG shared_buffers 各缓存一份,形成双重缓存,浪费内存,且 OS 层 bg write 调度不当还会导致 IO hang。缓解手段有限:调大 shared_buffer 并配 huge page、用 pgfincore 把 fd 的 adviceFlag 设为 POSIX_FADV_DONTNEED 尽快淘汰 page。根治要靠内核 DIO(PG16 引入 io_direct 开发者选项,存算分离架构如 PolarDB 用 DIO 解决)。

使用 Direct I/O 有哪些代价和不适用场景?

代价:没有 AIO 时 DIO 慢得无法使用;即便有 AIO 也要改造多处内核逻辑做显式预取;当 shared_buffers 无法设得足够大(如同一主机跑多个实例)时,DIO 性能往往反而不如 buffered IO。因此 DIO 不是无条件更好,需要 AIO 配合、shared_buffers 足够大、以及存储和预取逻辑支持才划算。

如何判断当前 PostgreSQL 数据库是否处于一致状态?

一致状态指所有数据块无 partial write(一半新一半旧),且不存在「此位点之前已提交事务不存在、之后提交事务存在」的错乱。判断方法:若初始化开启了 checksum,停库用 pg_verify_checksums 校验所有块;没开 checksum 则无法主动检测,只能查询到坏块时报错,可用 zero_damaged_pages 跳过。PITR 恢复时看日志是否打印 consistent recovery state reached;崩溃恢复要保证一致性需开启 fsync 和 full page write(COW 文件系统可不开 fpw)。可用 recovery_target=‘immediate’ 在到达一致位点后立即停止恢复。

如何用 RULE + LISTEN/NOTIFY 实现「垂帘听政」式异步消息预警?

在表上建 CREATE RULE,用 WHERE 条件过滤出异常数据(如传感器温度≥60、CPU≥80%),命中时 DO ALSO 执行 pg_notify 向通道发消息;再配合一条 DO INSTEAD NOTHING 规则把正常数据丢弃,从而大幅减少写入量。应用侧 LISTEN 该通道接收异步消息实时预警。相比定时器全量扫描,规则+异步消息避免了为发现问题数据建立大量索引和频繁轮询,实时性好。

异步提交 + 异步流复制时,从库的 WAL 会不会超前于主库?

不会。PG 只会把已经持久化的 WAL 内容发送给从库。因此即使主库异步提交产生大量未持久化的 wal buffer,这些内容也不会被发给从库;主库 crash 重启后从库不会出现 WAL 比主库更前的情况。好处是从库不会因异步提交而出现 WAL 超前,坏处是从库延迟可能更大。

异步提交和 commit_delay(组提交)有什么区别?

异步提交(synchronous_commit=off)在 WAL 落盘前就返回成功,牺牲持久性。commit_delay 其实是同步提交方法:它在事务 flush WAL 前延迟一小段时间,希望多个并发事务合并成一次 flush,均摊 fsync 成本(commit_delay 在异步提交时被忽略)。即异步提交降低持久性换吞吐,commit_delay 保持持久性但用组提交提升吞吐,两者机制和风险完全不同。

把数据库所有 superuser 都改成普通账号后,如何找回超级账号?

用单用户模式直接改系统目录:先 pg_ctl stop -m fast 停库,再 postgres –single 进入单用户 backend,执行 update pg_authid set rolsuper=true where rolname=‘postgres’; 即可把指定角色重新标记为 superuser,最后 pg_ctl start 启动。单用户模式绕过常规认证和权限检查,是找回超级账号的兜底手段。

把数据库服务器系统时间调小(往回调)有什么风险?

主要风险有三类:1) 事务 commit/abort 记录里的时间戳变小,PITR 按时间恢复时可能无法恢复到目标时间段(只能用 xid 指定恢复,因为 xid 始终自增);2) 用 now()/clock_timestamp() 作默认值的字段,在时间追平前,后插入记录的时间戳比先插入的小;3) autovacuum/autoanalyze、统计信息 reset 等系统时间戳短暂不准(影响小)。调大时间无影响。建议用 ntp/chrony 保持时钟准确。

数据库连接长时间空闲有时会自动断开,可能是什么原因?

常见原因是链路层设备(防火墙、负载均衡、NAT)设置了无数据包传输超时断开会话,而空闲连接确实不发包(或在等长 SQL 执行结果)。解决:调大设备超时,或配置数据库 TCP keepalive 心跳包频率。此外数据库侧 statement_timeout、lock_timeout、idle_in_transaction_session_timeout、idle_session_timeout 等参数也可能主动断开超时会话。

用 HLL 类型做滑动窗口 UV 分析为什么能提速上千倍?

传统方案要存明细(每天每 gid 每个 uid),count(distinct) 计算 UV 和新增/流失用户需要扫描海量明细,几十秒到几分钟。HLL(HyperLogLog)类型只存近似基数摘要,每天每 gid 一条,存储从 4GB 缩到 2MB。UV 用 # hll_union_agg 计算,新增/流失用户用 hll_union 做并集后相减,毫秒级完成,精度约 97%~105%。代价是结果是近似值,但换来了存储和速度数量级提升。

窗口函数滑动聚合的 inverse transition(反向转移函数)解决什么问题?代价是什么?

它解决「窗口帧头部移动」带来的重复计算。普通聚合只做 state=sfunc(state,row);窗口聚合作为 window function 时,相邻两行的窗口帧高度重叠(ROWS BETWEEN 59 PRECEDING AND CURRENT ROW 每次只少一条旧行、多一条新行)。没有 inverse transition 时,帧起点一动执行器就必须从头重算当前帧,代价 N×帧宽;有 MINVFUNC/MSFUNC 时,运行时间与输入行数成正比。代价:聚合状态必须支持删掉最早加入的输入值;moving mode 需要独立的 MSTYPE/MSFUNC/MINVFUNC/可选 MFINALFUNC/MINITCOND;反向撤销必须精确恢复状态;有些值无法撤销时 MINVFUNC 返回 NULL 会触发当前帧重算;对浮点、分位数、top-k、去重等聚合「撤销一行」往往不是简单减法。

第 1 条 SQL 执行慢,为什么可能和缓存无关而和 preload libraries 有关?

使用插件功能(类型、函数、操作符、索引等)时需要加载插件库文件,第一次查询若触发加载库,会有额外开销导致首条 SQL 慢。库文件加载方式:shared_preload_libraries 在启动时加载(改需重启)、session_preload_libraries 连接时加载(超户可设)、local_preload_libraries 连接时加载(普通用户可设,库须在 $libdir/plugins)、或手工 LOAD 命令。对类似 PostGIS、pg_jieba 等大库,首次加载耗时明显,可预先 preload 避免首查变慢。

聚合/窗口函数的 FILTER 子句解决什么问题?

FILTER (WHERE …) 允许在聚合或窗口计算前先过滤参与计算的行,用于「排除噪点后的方差/均值」等需要收敛到子集空间的场景。传统 case when 对上下文相关记录(如标准差、平均值)无法正确表达子集语义且结果不一致,还需要多遍扫描。FILTER 语法简单、只扫一遍表、无语义问题,例如 stddev(value) FILTER (WHERE c=‘bee’)。

表达「不存在」的三种写法 NOT EXISTS、LEFT JOIN IS NULL、NOT IN 有什么区别?哪个最稳?

NOT EXISTS 语义最贴近 Anti Join,是推荐入口:把关联条件放在子查询 WHERE 里,优化器能清晰拉平为 JOIN_ANTI。LEFT JOIN … IS NULL 依赖补 NULL 表达无匹配,需保证 IS NULL 的那一列只可能来自未匹配补空,reduce_outer_joins() 能证明匹配行该列必非空时才能降级为 Anti Join,否则可能误判「匹配到但字段为 NULL」的行。NOT IN 最容易踩 NULL 坑:当左侧为 NULL,或右侧没有相等值但存在让比较为 NULL 的行时,结果是 NULL 而非 true,在 WHERE 中 NULL 不保留行,因此 NOT IN 不等价于 NOT EXISTS;只有在能证明两侧都不为 NULL 且操作符安全时才能转 Anti Join。业务表默认允许 NULL 时优先用 NOT EXISTS。