# 失控查询、锁与事务

LLMS 索引： [llms.txt](/llms.txt)

---

“杀慢查询”不是故障判型。一个耗时最长的 session 可能是阻塞根节点、也可能是等待者；
可能正在做有价值的恢复，也可能已经超过用户 deadline；可能可以安全 cancel，也可能
已经写入海量 WAL 和死版本，terminate 也不会把这些成本自动抹掉。动作必须绑定
query、transaction、application、owner 与业务语义。

## 34.3.1 识别高消耗查询和阻塞根节点 {#item-34-3-1}

### 当前现场与历史重查询分开

`pg_stat_activity` 说明此刻有哪些 backend、状态与等待；`pg_stat_statements` 聚合的是
一段时间内同类语句的执行统计。前者适合回答“谁现在占着资源”，后者适合回答“哪类
语句长期贡献最多”。不能用累计榜单代替当前事故现场。

一个不导出完整 SQL 文本的当前投影：

```sql
SELECT pid,
       usename,
       application_name,
       state,
       wait_event_type,
       wait_event,
       clock_timestamp() - query_start AS query_age,
       clock_timestamp() - xact_start AS xact_age,
       backend_xid,
       backend_xmin,
       query_id
FROM pg_stat_activity
WHERE backend_type = 'client backend'
  AND pid <> pg_backend_pid()
ORDER BY xact_start NULLS LAST, query_start NULLS LAST;
```

对历史工作量，可按目标排序，而不是永远按 total time：

```sql
SELECT queryid,
       calls,
       total_exec_time,
       mean_exec_time,
       rows,
       shared_blks_read,
       shared_blks_written,
       temp_blks_read,
       temp_blks_written,
       wal_bytes
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
```

版本、扩展列和统计起点应随报告一起记录。统计 reset 或重启后的短窗口不能与一周基线
直接比较。

### 找根阻塞者，不要只杀等待者

```sql
WITH RECURSIVE lock_tree AS (
  SELECT a.pid,
         a.application_name,
         a.xact_start,
         pg_blocking_pids(a.pid) AS blockers,
         ARRAY[a.pid] AS path
  FROM pg_stat_activity AS a
  WHERE cardinality(pg_blocking_pids(a.pid)) > 0

  UNION ALL

  SELECT b.pid,
         b.application_name,
         b.xact_start,
         pg_blocking_pids(b.pid),
         t.path || b.pid
  FROM lock_tree AS t
  CROSS JOIN LATERAL unnest(t.blockers) AS p(pid)
  JOIN pg_stat_activity AS b ON b.pid = p.pid
  WHERE NOT b.pid = ANY(t.path)
)
SELECT * FROM lock_tree;
```

根 blocker 可能显示 `idle in transaction`，因为它已经执行完持锁语句，正在等客户端下
一条命令。仅筛选 `state='active'` 会漏掉它。也要排除 autovacuum、logical worker、
备份和维护工作等不同 `backend_type`，不要把每个 PID 都当作应用会话。

### 高消耗不是自动有罪

取消前回答：

```text
is this the root blocker or a victim?
is its client deadline already expired?
is it OLTP, migration, maintenance, backup, recovery, or batch?
what locks and objects does it own?
what rows/WAL/temp/I/O has it already produced?
does it carry a business idempotency key?
who owns the decision?
```

对 query_id 做执行计划分析时，转到第 10、11 章的方法；在线事故中不要在主库上无界
执行 `EXPLAIN ANALYZE` 复现一条未知重查询。

## 34.3.2 cancel、terminate 与中止后成本 {#item-34-3-2}

### 两个函数的边界

```sql
SELECT pg_cancel_backend(:pid);
SELECT pg_terminate_backend(:pid);
```

`pg_cancel_backend` 向目标 backend 发送取消当前 query 的请求。session 通常仍存在；
当前事务会进入错误状态，客户端需要 `ROLLBACK`。`pg_terminate_backend` 终止整个
session，连接断开，未提交事务由服务器回滚。两者都需要相应权限；不要通过给应用
超级用户来获得事故处置能力。

优先级一般是：

```text
application cooperative cancel/deadline
  -> pg_cancel_backend exact PID
      -> wait and verify
          -> pg_terminate_backend exact PID when justified
```

“exact”至少绑定：

```text
pid + backend_start
database + user + application_name
query_id / transaction age
incident run id or ticket
```

PID 会复用。先查 PID、过几分钟再裸 terminate，可能命中完全不同的新连接。执行动作的
SQL 应在同一事务/语句里重验识别字段。

### cancel 不等于立即释放全部资源

- query 可能在到达可中断点前继续运行；
- client 可能自动重试同一工作；
- 事务未 rollback 前仍可能持有锁；
- parallel workers 与 leader 的收敛需要时间；
- remote/extension 调用的中断语义取决于组件；
- query 已经产生的 WAL、temp 或脏页不会凭空消失。

terminate 也不是免费的“更强 cancel”，但要准确理解 PostgreSQL 的代价：普通事务
中止不会通过物理 undo 逐行撤销已经写过的 tuple。backend 仍需响应信号、执行中止清理
并释放锁和本地资源；已经产生的 WAL、脏页、复制延迟和死版本不会消失，死版本通常要
由后续 vacuum 回收。因此不能套用“按修改量做数小时物理回滚”的模型，也不能因为
session 已消失就宣称资源影响全部结束。

### 动作后必须复核

```sql
SELECT pid, backend_start, state, wait_event_type, wait_event
FROM pg_stat_activity
WHERE pid = :pid;
```

同时观察：

```text
root blocker disappeared?
dependent waiters made progress?
pool stopped recreating the work?
abort cleanup、vacuum 或 replica catch-up 是否仍在消耗 I/O？
user success and tail latency recovered?
```

如果应用立刻重建同一 session，数据库端 cancel 只是短暂擦除症状，真正控制点在 admission
和 retry。

## 34.3.3 长事务和大事务结束前先评估后果 {#item-34-3-3}

### “长”与“大”是两个维度

```text
long but small
  idle transaction holds snapshot/locks for hours

short but large
  bulk UPDATE changes millions of rows in minutes

long and large
  migration/ETL both retains old state and creates rollback work
```

长事务主要风险是 lock、`backend_xmin`、vacuum 回收与连接占用；大事务还带来 WAL、
dirty buffers、replication lag、终止响应与后续清理成本。prepared transaction 即使
没有活动 session，也可长期保留锁和 XID 状态：

```sql
SELECT gid,
       prepared,
       owner,
       database,
       clock_timestamp() - prepared AS age
FROM pg_prepared_xacts
ORDER BY prepared;
```

不要看到 prepared transaction 就 `ROLLBACK PREPARED`。它属于两阶段提交协议，必须先
与 transaction manager/业务 ledger 对账，判断应 commit 还是 rollback。

### 结束前的后果清单

```yaml
identity:
  pid_backend_start: ...
  application_owner: ...
  transaction_or_job_id: ...
business:
  partial_external_effects: ...
  idempotency_or_reconciliation: ...
database:
  locks: ...
  xmin_retention: ...
  rows_wal_temp_estimate: ...
  replicas_and_archive_effect: ...
abort_and_cleanup:
  expected_resource_cost: ...
  observation_query: ...
  escalation_timeout: ...
```

若事务包含数据库外部副作用，PostgreSQL rollback 只能撤销数据库内未提交状态，不能
撤回已经发出的邮件、支付或消息。此时需要业务补偿，不是更强的 terminate。

### 更好的预防

- 为交互式事务设置合理的 `idle_in_transaction_session_timeout`；
- 为不同工作负载设置 statement/lock timeout，而不是一个全局极小值；
- 大批处理分块提交，并让每块有可恢复 checkpoint；
- schema change 使用受控 lock timeout 和发布门；
- 统一 application_name、query tag 与业务 job id；
- 为重要操作保留可对账 token。

timeout 是保护栏，不是容量。设置后还要验证应用如何处理取消、事务错误和重试。

---

[上一节：连接风暴与排队失控](../02/) · [返回本章目录](../) · [下一节：CPU、内存、I/O 与 OOM](../04/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
