# 从会话到语句定位范围

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

---

事件边界确定后，先回答“backend 此刻在做什么”，再回答“过去一段时间哪个查询族消耗最大”。前者来自动态 activity snapshot，后者来自累计查询统计；把两者混为一谈，会拿一个瞬时会话解释一小时预算，或拿一小时均值解释当前阻塞。

## 8.2.1 活跃、等待、阻塞与空闲事务 {#item-8-2-1}

下面的只读查询保留会话身份、事务年龄、当前状态、等待与直接 blocker：

```sql
SELECT
    clock_timestamp() AT TIME ZONE 'UTC' AS captured_at_utc,
    pid,
    backend_start,
    datname,
    usename,
    application_name,
    client_addr,
    state,
    CASE WHEN state = 'active'
         THEN clock_timestamp() - query_start
    END AS active_for,
    CASE WHEN xact_start IS NOT NULL
         THEN clock_timestamp() - xact_start
    END AS xact_age,
    wait_event_type,
    wait_event,
    pg_blocking_pids(pid) AS blocking_pids,
    query_id,
    left(regexp_replace(query, '[[:space:]]+', ' ', 'g'), 240)
        AS query_excerpt
FROM pg_stat_activity
WHERE backend_type = 'client backend'
  AND datname = current_database()
ORDER BY
    (state = 'active') DESC,
    query_start NULLS LAST;
```

读取时必须联合解释 `state` 与 wait：

| state / wait | 证据含义 | 不能直接推出 |
|---|---|---|
| `active` / NULL | 正在执行，采样瞬间未报告等待 | 一定消耗 CPU；一定健康 |
| `active` / `Lock/*` | 查询执行中，正在等 heavyweight lock | 当前等待者就是根因 |
| `active` / `IO/*` | 正在某个 I/O wait point | 磁盘一定坏；全部时间都在 I/O |
| `active` / `Client/ClientWrite` | server 正等待把数据写给客户端 | 查询计算本身慢 |
| `idle` / `Client/ClientRead` | backend 等客户端发下一条命令 | 一条“慢 SQL”正在跑 |
| `idle in transaction` | 事务打开但当前没有语句执行 | 没有影响；必须立刻终止 |

PostgreSQL 明确把 state 与 wait 定义为相互独立的维度。采样也可能遇到短暂不一致，所以重要结论应跨数个短间隔采样，而不是冻结一行就下结论。

`idle in transaction` 尤其需要看 `xact_start`、`backend_xmin`、锁和业务上下文。它可能保留锁、阻碍 vacuum 清理旧版本、延长事务边界，却不是“运行很久的当前 query”；`query` 字段此时是上一条语句。治理措施应优先修复应用事务边界，并使用经过评估的 `idle_in_transaction_session_timeout`，而非定时粗暴终止所有 idle session。

阻塞关系使用：

```sql
SELECT
    waiting.pid AS waiting_pid,
    waiting.backend_start AS waiting_backend_start,
    blocker_pid,
    blocker.application_name AS blocker_application,
    blocker.state AS blocker_state,
    blocker.xact_start AS blocker_xact_start,
    blocker.wait_event_type AS blocker_wait_type,
    blocker.wait_event AS blocker_wait_event
FROM pg_stat_activity AS waiting
CROSS JOIN LATERAL unnest(pg_blocking_pids(waiting.pid))
    AS edge(blocker_pid)
JOIN pg_stat_activity AS blocker
  ON blocker.pid = edge.blocker_pid
WHERE waiting.datname = current_database();
```

等待最久的 PID 未必是 root blocker；它可能也是链中受害者。先建立边，再沿边找到不再被别人阻塞的上游会话。第 5 章已给出锁模式与事务语义，本章强调诊断身份。

若必须缓解，先保存证据，再精确重查：

```sql
SELECT pg_cancel_backend(pid)
FROM pg_stat_activity
WHERE pid = :captured_pid
  AND backend_start = :'captured_backend_start'::timestamptz
  AND datname = :'captured_database'
  AND application_name = :'captured_application';
```

PID 会复用，单凭截图里的数字取消有伤及无关会话的风险。`pg_cancel_backend` 取消当前 query，`pg_terminate_backend` 结束 session；后者影响事务和客户端重连，不能当默认按钮。生产动作还要经过本地权限、SOP 和风险分级。

普通用户只能完整看到自己的会话；调查角色通常需要内置角色 `pg_read_all_stats`，但这也会暴露 SQL 与活动信息。权限应授予受控诊断角色，不应为了面板方便把业务用户升为 superuser。

最后注意视图一致性：累计统计可能延迟刷新，并在事务内缓存；activity 信息也会在同一事务首次读取后形成一致快照。交互调查若持续开着事务重复查询，先结束事务或按需调用 `pg_stat_clear_snapshot()`，否则可能把旧快照当实时状态。

## 8.2.2 按调用、总时长、均值和尾延迟排序 {#item-8-2-2}

`pg_stat_statements` 把结构相同、常量不同的语句归一化，适合回答“哪些查询族消耗了累计预算”。先确认扩展已加载、目标数据库有 view，并记录 reset 起点：

```sql
SELECT stats_reset
FROM pg_stat_statements_info;
```

不要为一次调查执行全局 `pg_stat_statements_reset()`：它会破坏其他人正在使用的基线。更好的做法是保存两个时点的快照并计算 counter delta，或让监控系统持续抓取 counter。

一次基础排序：

```sql
SELECT
    userid,
    dbid,
    queryid,
    calls,
    total_exec_time,
    total_exec_time / NULLIF(calls, 0) AS mean_from_total_ms,
    mean_exec_time,
    max_exec_time,
    stddev_exec_time,
    rows,
    rows::numeric / NULLIF(calls, 0) AS rows_per_call,
    shared_blks_hit,
    shared_blks_read,
    temp_blks_written,
    wal_bytes,
    left(query, 240) AS query
FROM pg_stat_statements
WHERE calls > 0
ORDER BY total_exec_time DESC
LIMIT 30;
```

至少从四种视角排序：

- `total_exec_time`：谁吃掉最多执行时间预算，适合容量与总体收益；
- `mean_exec_time` / `max_exec_time` / `stddev_exec_time`：谁单次慢或波动大；
- `calls`：谁极高频，单次少量改善也可能有大收益；
- blocks、temp、WAL、rows：谁制造 I/O、spill、写放大或大结果。

一个总时间第一的查询可能只是每次 2 ms、调用数巨大；一个均值第一的查询可能每天只跑一次；一个调用数第一的查询可能返回 0 行且预算很小。索引、缓存、批处理、限流和 SQL 改写针对的是不同问题，不能只保留一个“Top SQL”榜。

`pg_stat_statements` 记录 min/max/mean/stddev，但不保存每次执行的完整分布，也没有原生 p95/p99 列。因此：

- `max_exec_time` 不是 p99；
- 不能从 mean/stddev 假定任意分布再可靠推算 p99；
- 尾延迟要来自请求 histogram、trace、采样日志或保存单次事件的系统；
- 累计 max 可能来自很久以前，必须结合 `stats_reset`/快照窗口。

执行统计只覆盖 PostgreSQL server 侧的一部分时间。连接池等待不在其中；结果传输造成的 server wait 与驱动计时也未必与 `total_exec_time` 完全同口径。把查询榜与应用 SLI 对齐是下一步，不是直接宣布榜首为根因。

## 8.2.3 查询指纹、参数与时间窗口 {#item-8-2-3}

查询身份不是只有一段 SQL 文本。建议至少保存：

```text
cluster/instance/database
userid/dbid/toplevel/queryid
normalized query text
application/release/route
representative parameter class
UTC window and stats reset boundary
```

`queryid` 来自 parse analysis 后的结构 hash。它比字符串更适合在同一环境中关联，但保证有限：

- 同一文本可能因 `search_path` 或对象含义不同而分成多个 ID；
- 常量通常被归一化，hot tenant 与 cold tenant 可能落在同一 query family；
- drop/recreate 对象、catalog OID 与平台架构会影响身份；
- 不应假设跨 PostgreSQL major version 稳定；
- 物理复制节点通常可对应，逻辑复制环境不能据此保证对应；
- 极低概率仍可能 hash collision。

因此长期证据键通常是 `(cluster identity, major version, dbid, userid, toplevel, queryid)` 加归一化文本 hash，而不是一个裸 `queryid`。升级、重建或迁移后应建立新的 epoch。

归一化是优点也是盲区。第 7 章的 tenant 实验中，同一语句：

```sql
... WHERE tenant_id = $1
```

参数 1 返回 90000 行，参数 1001 只返回 10 行。累计均值可以同时掩盖两端。要恢复参数语义，使用经过脱敏的 trace tag、业务参数分桶、受控日志采样或可重放 fixture；不要把 token、密码、个人数据和任意 payload 倾倒进日志。

时间窗口也必须对齐三种数据：

1. `pg_stat_activity` 是采样瞬间；
2. `pg_stat_statements` 是自 reset/entry 起的累计量；
3. Prometheus/日志/trace 是各自 scrape、采样或保留窗口。

例如事故发生五分钟，直接查询累计三个月的 mean 会稀释变化。正确做法是从监控 counter 计算事故窗口 delta/rate，或在事故前后保存两份 snapshot：

```text
calls_delta
total_exec_time_delta
rows_delta
blocks/temp/WAL delta
mean_in_window = total_exec_time_delta / calls_delta
```

counter reset、entry eviction、实例重启和 failover 都会造成不连续，计算前应检查 reset/instance identity，不能把负 delta 当真实负负载。

本节最终应得到一个优先调查列表，而不是一个榜首判决：

```text
query family + parameter class + affected window
current state/wait/blocker evidence
window calls/total/mean/resource deltas
identity and reset boundaries
two or three competing explanations
```

下一节把这份列表与日志、主机指标、计划和变更事件放到同一时间轴。

---

[上一节：先定义“慢”](../01/) · [返回本章目录](../) · [下一节：关联日志、指标与计划](../03/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
