关于 wherewent
wherewent 记录 Python 作业中的 SQLAlchemy 活动,并报告高成本查询模式、调用位置和修复建议,适合排查缓慢批处理作业,也支持分析未完成的运行。
主要功能
- 01无需改代码即可记录 SQL
- 02按查询形态分组并计数
- 03定位应用代码调用行
- 04基于规则生成发现与修复建议
- 05支持部分运行和异步 SQLAlchemy
- 06报告单元成本及变化趋势
适用场景
- 01查找拖慢批处理的重复快速查询
- 02发现逐行提交过于频繁
- 03不中断长时间作业并检查运行情况
- 04识别处理单元增加时上升的查询成本
MCP 相关
其 stdio MCP 服务提供 `analyze_job`,用于运行并分析作业;`explain_run` 可读取已保存 JSON 报告中的发现。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
wherewent
时间都去哪儿了?一条命令,马上查明。
零配置记录器,帮你解答“为什么这个 Python 批处理任务要跑这么久?”
在 shell 中运行——也可以将它用作 MCP 服务,由 AI Agent 直接调用。
wherewent run python your_job.py
耗时 5 分钟的 0.4ms 查询
单次查询可能很快——只要 0.4ms——但仍会拖垮你的任务,因为它在一行代码中被调用了 500,000 次。你的应用在往返通信上耗费了 300 秒,而 Postgres 本身只运行了 80 秒。你试过的每个性能分析器都只告诉你*“时间花在 psycopg 上”*,然后就没下文了。
wherewent 会展示调用模式。 它按查询形状分组,统计每种形状的运行次数,汇总总耗时,并指出你自己的代码中触发查询的确切 file:line——然后用通俗易懂的语言和清楚的计算过程告诉你该怎么做。
====================================================================================================
wherewent — SQL flight recorder
----------------------------------------------------------------------------------------------------
wall: 26.15s cpu: 25.41s (97% CPU busy) queries: 20,004 commits: 20,001 rollbacks: 1
in-DB time: 5.46s (20.9% of wall; app-observed: includes network+driver+server)
commit time: 9.06s total rows: 20,000
recording added ~1.81s (~6.9% of wall)
====================================================================================================
QUERY GROUP CALLS TOTAL MEDIAN CALL SITE
----------------------------------------------------------------------------------------------------
INSERT INTO events (name, value) VALUES (?, ?) 20,000 5.46s 0.24ms demo/naive_job.py:65 in main
SELECT count(*) AS count_1 FROM events 1 0.00s 0.16ms demo/naive_job.py:71 in main
====================================================================================================
FINDINGS
----------------------------------------------------------------------------------------------------
1. [R1+R2] commit-per-row loop
20,000 calls x 0.24ms median ~= 5.5s = 21% of 26.1s wall, at demo/naive_job.py:65. Batch it.
20,001 commits for 20,000 rows (1.0 rows/commit), 9.1s in commit = 35% of wall. Batch to 1,000+ rows/txn.
~= 14.5s attributable
====================================================================================================
与众不同之处
| 采样分析器 | APM / 链路追踪 | wherewent | |
|---|---|---|---|
| 无需修改代码 | ✅ | ❌ | ✅ |
| 按查询形状分组 | ❌ | ⚠️ | ✅ |
| 定位到你的调用位置 | ⚠️ | ⚠️ | ✅ |
| 告诉你如何修复 | ❌ | ❌ | ✅ |
| 随处运行,无需服务端 | ✅ | ❌ | ✅ |
| 支持按 Ctrl-C 中断后的部分运行 | ❌ | ⚠️ | ✅ |
安装
pip install wherewent
就这样——记录器只使用标准库。你只需要 SQLAlchemy,因为你的任务已经在用它了。
使用方法
用它包装任意命令。你的脚本会完全不做修改地运行——无需导入、装饰器或配置:
wherewent run python your_job.py --some arg
wherewent run python -m your_package
wherewent run --save run.json python your_job.py # also dump machine-readable JSON
- 报告会在退出时输出到 stderr;任务自身的 stdout/stderr 会原样通过。
- 按 Ctrl-C 仍会生成报告。 采集一个 14 小时任务最初 5 分钟的数据是主要用例——重点就是获取部分数据。
- 无需停止即可查看。 发送
SIGUSR1(kill -USR1 <pid>)可在运行期间获取部分快照;也可以设置WHEREWENT_INTERVAL=30,每隔 30 秒输出一次。任务会继续运行。 - 支持异步 SQLAlchemy。 查询在 greenlet 中运行,堆栈上没有用户帧,因此简单的堆栈遍历会无法定位;wherewent 仍能将其归因到实际调用位置(
AsyncSession/AsyncConnection)。 - 绝不会导致任务崩溃或数据损坏。 每个钩子函数体都经过包装,记录器即使失败也会静默处理,不会中断运行。
- 绝不会记录你的数据。 只保留查询形状和计数——在存储任何内容之前,都会剥离字面值和绑定参数。
试试内置演示
git clone https://github.com/habibafaisal/wherewent && cd wherewent
pip install -e ".[dev]"
wherewent run python demo/naive_job.py # watch the R1+R2 finding fire
python demo/benchmark.py # naive vs fixed, with the overhead gate
将其用作 MCP 服务(原生支持 Agent)
wherewent 提供 Model Context Protocol(MCP)服务,因此
AI Agent 可以在任务变慢时立即直接调用它,并获取机器可读的发现结果
——无需读取原始查询日志并自行推理得出相同结论。它已列入官方
MCP Registry,名称为
io.github.habibafaisal/wherewent。
使用 [mcp] 附加项安装(它会引入 MCP SDK;核心记录器仍仅依赖标准库),
然后运行 stdio 服务:
uvx --from 'wherewent[mcp]' wherewent-mcp
# or: pip install 'wherewent[mcp]' && wherewent-mcp
传输方式: stdio。提供的工具:
| MCP 工具 | 功能 |
|---|---|
analyze_job(command, unit_function?, timeout_s=600) | 在 wherewent 下运行 Python/SQLAlchemy 任务,并以结构化字段返回任务变慢的原因——确切调用位置、查询数量和修复建议。超时后会返回部分结果(timed_out: true)。 |
explain_run(path) | 返回已由 wherewent run --save 生成的 JSON 文件中的增强版发现结果——无需重新运行。 |
每项发现都包含 fix、call_site、calls、wall_fraction,以及 Agent 可据此采取行动并引用的 evidence 对象。通过配置将其接入任意 MCP 客户端(例如 Claude Desktop):
{
"mcpServers": {
"wherewent": {
"command": "uvx",
"args": ["--from", "wherewent[mcp]", "wherewent-mcp"]
}
}
}
仓库根目录下的 Dockerfile 可为基于容器的 MCP 主机构建相同的 stdio 服务。
为工作单元命名
“81,749 次查询”很难判断。**“每个应收账款 135 次查询”**能让工程师 立刻看出架构存在过多请求。为任务处理的工作单元命名,wherewent 就会报告 单个工作单元的成本情况——中位时长、每个单元的查询数/提交数/行数,以及 成本随运行推进的变化趋势:
# Zero-config: name a function; every top-level call is one unit
wherewent run --unit-function myapp.jobs:process_receivable python run.py
# Or mark the unit in code (same machinery, same report)
import wherewent
for receivable in book:
with wherewent.unit("receivable"):
process(receivable)
UNIT: myapp.jobs:process_receivable (1,203 units)
----------------------------------------------------------------------------------------------------
median duration 341 ms queries/unit 135 (median)
commits/unit 1.0 rows/unit 46.0
GROWTH
units 1–100 220 ms/unit
units (last 100) 379 ms/unit
queries 1–100 98 queries/unit
queries (last 100) 171 queries/unit
trend +72% slower over the run
query trend +74% more queries/unit over the run ← R6 fires
R6 在任一趋势斜率触发时都会生效。这对计算密集型任务很重要:如果运行时间保持不变,但
每个单元的查询数不断增加,时长趋势会显示为 flat,只有查询趋势能揭示问题——
因此 wherewent 会同时报告两种趋势,并明确指出这种模式存在可扩展性风险,而非当前的
运行时瓶颈。
增长趋势也说明了为什么采样运行更能反映真实情况:它会显示每个单元的成本不断上升, 因此你会知道完整运行的实际成本会高于线性外推结果——而仅统计总量的分析器 永远无法告诉你这一点。即使异步单元并发运行,每个单元的计数也都准确;记录的内容仅限 模式和计数。
工作原理
- 通过
PYTHONPATH中的 sitecustomize 垫片将自身注入目标进程——无需 修改代码,也无需导入包装器。 - 在类级别监听——
event.listen(sqlalchemy.engine.Engine, ...)——因此 会自动捕获应用创建的所有引擎,无需配置。 - 将每条语句规范化为一个查询组:字面量、绑定参数、
IN列表 和多行VALUES都会合并,因此一百万条不同的插入语句会汇总成一条如实的记录。 - 通过沿调用栈跳过库代码帧,将调用位置解析到你自己的代码中的首行——开销很低: 按文件名缓存,每个组仅对前 5 个样本收集完整调用栈,因此热路径开销足够低,能满足其预算。
- 根据三条规则触发确定性发现,每条规则都会展示其计算过程。
发现引擎
| 规则 | 触发条件 | 说明 |
|---|---|---|
| R1 — 频繁调用组 | 调用次数 > 1,000,耗时占墙钟时间 > 10%,中位数 < 5ms | 快速查询被调用得太频繁——将其批量处理(executemany / IN 列表 / JOIN)。 |
| R2 — 每行提交 | 提交次数 > 100,每次提交的行数 < 10,提交耗时占墙钟时间 > 5% | 你在逐行提交——将每个事务的批量大小增至 1,000 行以上。 |
| R3 — 受数据库等待限制 | 数据库内耗时占墙钟时间 > 60%,CPU 忙碌时间 < 30% | 该任务受往返延迟限制,而非计算能力限制。 |
| R4 — 同时出现的模式 | ≥ 2 个查询组由同一个函数触发,且该模式会扩展——在多次迭代中,每次迭代都有许多查询,或者排除一次性设置后耗时占墙钟时间 > 10% | 每次迭代都会一起触发多个查询(SELECT + UPDATE + INSERT)——将它们合并为一次往返。按函数而非行聚类,因此即使某个辅助函数在三行不同的代码中执行语句,也仍会被视为一个操作。排除只执行一次的语句(calls == 1)——它们属于固定开销,由 R5 检查。报告估算的每次迭代查询数,即使有界运行的计时掩盖了某些模式,也会标记可扩展的模式。 |
| R5 — 一次性高开销语句 | 单条 calls==1 语句耗时占墙钟时间 > 15% 或绝对耗时 > 10s | 一条语句就产生了巨大的固定开销。R1/R3/R4 都在查找频繁调用,因此会漏掉它——R5 则能捕获最值得优化的单行代码。绝对耗时下限很重要:无论 20s 占采样运行的 24%,还是占完整运行的 1%,都值得优化。 |
| R6 — 单位成本上升 | 从前 100 个单位到最后 100 个单位,每单位耗时 或 查询数/单位上升 ≥ 1.5 倍(需要 --unit-function/wherewent.unit()) | 随着运行推进,每个项目的成本不断增长——可能是状态累积、历史读取未批量处理,或列表在每轮循环中不断增长。报告增长斜率(早期与后期的查询数/单位),因此即使任务受计算能力限制,只要查询成本在增长,也能被发现。 |
具有相同根本原因的发现会合并(例如 R1+R2);耗时占墙钟时间不足 5% 的发现会被隐藏,最多显示排名靠前的 3 项——按其所占用的秒数排序。R4 能捕获单组阈值无法发现的情况:N+1 模式分散在 SELECT + UPDATE + INSERT 中,单独来看都不显眼,却会在每次循环中作为一个整体触发。从 v0.3 开始,即使一次性设置开销让模式在短时采样运行中看起来很小,只要模式可扩展,R4 也会触发。
**每个数字都真实可信。**查询耗时会标注为应用观测值(包含网络、驱动程序和服务器耗时——不只是 Postgres)。任何无法测量的内容都会显示 —,绝不会猜测。wherewent 甚至会测量自身钩子的耗时,并报告它增加的开销。
路线图 — 欢迎贡献 🙌
wherewent 的设计目标是超越 SQLAlchemy。它的八个模块中,有七个——
规范化、调用点解析、统计模型、规则引擎、报告、CLI 和注入垫片——已经与框架无关。它们处理的是纯粹的
RunSnapshot 查询事件快照。只有绑定 SQLAlchemy 事件系统的 recorder.py 与框架相关。
**这意味着新增后端是一项边界清晰的贡献:**从另一个驱动捕获查询
开始/结束/行数/事务事件,传入相同的 RunSnapshot,整个发现与报告流程便可直接复用。适合作为首个后端的选项:
- Async SQLAlchemy — 通过 greenlet 边界进行调用点归因 (v0.2.0)
- 感知工作单元的性能分析 — 按单元统计开销 + 增长趋势 (v0.3.0)
- 执行模式发现 (下一个重要功能 — 欢迎贡献) — 目前 wherewent
会对每次迭代中一起触发的查询进行聚类(R4)。下一步:重建这些查询背后有序的、
可能嵌套的工作流并为其命名,例如
这是真正超越普通 N+1 检测的一步(Sentry/Scout 会发现重复的单一形态 查询;此功能则能发现跨越多种 SQL 形态和函数的多操作工作流): 读取→修改→写入循环、每个条目依次序列化→插入→提交、每次变更后都获取完整状态快照、不断增长的历史记录扫描,以及随条目位置上升的 CPU 开销。需要按单元记录有序事件日志 + 挖掘重复子序列,并将开销控制在门槛以内。For each receivable: For each audit event ×23: SELECT chain_state → SELECT payload → INSERT payload → INSERT audit_event → UPDATE chain_state Finding: serialized audit-append loop — 23 repetitions/receivable, ≈115 statements/receivable, 58% of DB activity, at process_receivable → emit_firing → append_event. - 原生
psycopg/psycopg2— 游标子类或连接工厂钩子 - 原生
asyncpg(SQLAlchemy 之外)— 异步执行路径 - Django ORM — 通过
connection.execute_wrapper - 通用 DB-API 2.0 — 无需 monkeypatch 的
Cursor代理 - 更多发现规则(锁等待、顺序扫描启发式规则)
请参阅 CONTRIBUTING.md,了解后端接口约定,以及每条捕获路径都必须通过的 < 15% 开销
门槛。
当前的局限
- SQLAlchemy 2.x(同步和异步 ORM/Core;1.4 可能也能用)。目前尚未归因 SQLAlchemy 之外的原始
asyncpg调用。 - 单进程——不支持多进程扇出。
- 查询耗时按应用观察结果计算(网络 + 驱动程序 + 服务端),这是有意为之。
- 提交耗时通过包装方言的提交操作获取;如果包装失败,则显示
—。 - 每次迭代的比率是估算值(标记为
≈),根据同时出现的查询数量推断——只有信号明确时才显示,绝不猜测。 - 按单元统计的数量(
--unit-function/wherewent.unit())即使在并发异步单元下也准确;按单元统计的耗时是墙钟时间,单元并发运行时可能重叠——常见的顺序循环场景下则准确。 - R6 归因的秒数是确定性的下限估算值,不是测量值。每个单元的超额查询数按本次运行的单次查询平均 DB 耗时折算,因此,如果额外查询比平均值更快,真实成本就更高(反之亦然)。它根据准确的整数查询数量计算,而非读取时钟,因此每次运行的结果都可复现——但 R6 关注的是斜率,而不是秒数。
- ORM flush 归因。
session.flush()/commit()发出的所有查询都会归到同一个调用位置,因此 R4 可能会把无关写入归为同一个“工作流”。当某个集群的写入共用一行源码时,wherewent 会将其标记为可能来自单次 flush,而不会声称你可以合并它们——它不会建议你批量处理已经批量处理过的内容。 - 每组中位数是有上限的样本中位数(每组最多保留 5,000 次执行),因此即使运行包含数百万次查询,内存占用也能保持稳定。
calls和total_time仍然是准确值。 - 提交耗时和回滚耗时会分别报告。 SQLAlchemy 的连接池会在每次连接归还时执行回滚,因此回滚耗时会标记为*(包括连接池重置)*,绝不会计入提交耗时。
- 分析结果会说明耗时所在位置及其扩展方式——如果运行受 CPU 限制,就会明确指出,而不是暗示修复 SQL 就能加快本次运行。
这些是验证原型需要坦诚面对的边界,并非永久限制——请参阅路线图。
贡献
非常欢迎贡献——包括新后端、新规则、文档和错误报告。请先阅读 CONTRIBUTING.md,对于任何较大的改动,请先提交 issue 讨论,并在推送前运行 pytest && python demo/benchmark.py。
许可证
MIT © 2026 Habiba Faisal
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。