### 问题: 现在整个项目是sql 优化, 所以需要非常确切的时间标准,所以是不是在执行完sql, 同一个会话, 不重新建连, 然后断连;然后执行下一个sql 再链接么, 还是一个session 完整执行完 1000多条sql, 这里会不会因为会有已有缓存所以时间不准确的现象么 ### 你要的“非常确切时间标准”,先把口径定死 做 SQL 优化时,通常要区分两种指标(建议两套都产出): - A. 纯 SQL 执行耗时(推荐主指标):只衡量“服务器执行这条 SQL”要多久,不包含建连/鉴权/握手。这才能反映优化(索引/改写)本身的收益。 - B. 端到端耗时:包含建连等所有开销。更贴近业务请求,但噪声大、受网络/连接池影响强,不适合作为优化对比的唯一标准。 你现在项目目标是“SQL 优化”,所以主指标应是 A(不含建连);建连可以作为单独字段记录即可。 ------ ### 一条 SQL 的最佳执行方式(我建议的“标准流程”) 对每一条 SQL,最稳的方式是: - 建一次连接(一个 session) - 在这个 session 里: - SET SESSION ...(如超时、optimizer 参数等) - 跑 1 次 warm-up(不计入统计) - 再跑 N 次计时(你们现在是 5 次取后 2 次均值,这个思路可以) - 紧接着用同一个连接跑 EXPLAIN ANALYZE(保证会话参数一致) - 断开连接 - 进入下一条 SQL 再新建连接 这样做的好处是: - 同一条 SQL 的执行和 explain 在同一会话,避免 session 参数不一致导致不可比 - 不同 SQL 之间 session 状态完全隔离(不会被上一条 SQL 的 SET/临时表/@变量/事务 污染) - 计时口径天然是 不含建连(因为你计的是 execute,建连发生在外面) ------ ### “一个 session 跑完 1000 条”会不会因为缓存导致不准确? 会出现你担心的现象,但核心点是:这不是“不准确”,而是“测的是 warm-cache 场景”。 - 无法完全避免缓存影响:即使你每条 SQL 都新建连接,MySQL 服务器的 buffer pool/文件系统页缓存 仍然会越来越热;新连接并不会清空这些缓存。 - 一个 session 跑完 1000 条 还会额外引入: - session 级状态残留(SET、用户变量、临时表、事务上下文) - 更容易出现“某条 SQL 改了会话参数影响后续”的不可控因素 所以结论是: - 不建议一个 session 连续跑完 1000 条 当作严谨基准。 - 你想要“确切标准”,应该把标准定义为: - steady-state(热缓存)性能:先统一 warm-up,再测;这是生产更常见的状态 - 以及可选的 cold-ish(相对冷启动)性能:隔离环境、重启实例/重置缓存(通常成本高,难自动化) ------ ### 怎么让 1000+ 条的对比更“公平”(比纠结连接更重要) 真正影响公平性的通常是 对比顺序带来的热身偏置。建议: - 先做统一 warm-up pass:把 1000 条先跑一遍不计时,让系统进入稳定态 - 对比两种版本(原 SQL vs 优化 SQL)要交错/随机化: - 最理想:对每个 query,按相同规则测原版和优化版(顺序可 AB/BA 交替),减少“后跑的一方天然更热”的偏差 - 每条 SQL 多次采样,丢掉首轮:你们“5 次取后 2 次均值”就是在做这个 - 现在 execute_query 的 5 次计时:计的是 cursor.execute(...) 这一段(你们代码里明确把 fetchall() 排除在计时外),不包含建连时间。 - EXPLAIN ANALYZE 输出里的时间:是数据库在执行“分析计划/执行节点”时的耗时统计,不可能包含客户端建连/握手/鉴权的时间。建连发生在发出 SQL 之前,EXPLAIN 结果里只反映语句本身的执行与分析。 所以:即使你为 EXPLAIN 单独新建连接,EXPLAIN 文本里的时间也不会把建连算进去;变化只会来自“不同 session 状态/缓存热度/参数差异”这些因素,而不是建连本身。 # pymysql 查询性能基准测试时间测量问题总结(2026年2月版) 针对 pymysql 脚本测量大结果集查询时间严重失真的问题,梳理核心矛盾、根因及最准确的测量/优化方法。 **重点解决:命令行 mysql / Workbench 1s,pymysql 脚本 16s 的典型差距。** **核心结论**( 1. **差距主要不是 SQL 本身慢,而是客户端实现差异**: - mysql 终端(C 实现)取数/解码极快,默认 buffered 全量读取。 - pymysql(纯 Python)在结果集大时,类型转换、解码、内存分配开销巨大。 - **默认 Cursor (pymysql 的游标对象)是 buffered**:`execute()` 就已经把结果集全部读回 Python 内存,导致即使不调用 `fetchall()`,execute 时间也包含全量传输开销。 2. **最准确测量方式**: - 使用 **SSCursor**(Server Side Cursor) + 迭代器方式取数。 - 分开测量 execute(服务器执行 + 元数据)与 fetch(客户端传输 + 解码)。 - 用 `time.perf_counter()` 高精度计时。 - 服务器端真实耗时用 `performance_schema.events_statements_history` 查询 TIMER_WAIT。 3. **推荐驱动**: PyMySQL 适合开发/调试,但性能基准测试建议换成 **mysqlclient**(C 扩展,通常快 2–10x)。 如果实际业务也用不同语言,python/java 本身问题依旧存在 ## 1. 核心问题清单 | 序号 | 问题描述 | 表现形式 | 根因 | 严重程度 | | ---- | -------------------------------------------- | ----------------------------------- | ------------------------------------------------------------ | -------- | | 1 | pymysql execute() 时间远超预期 | execute 10–16s,命令行 1s | 默认 Cursor buffered,execute() 已全读结果集 + Python 解码慢 | ★★★★★ | | 2 | fetchall() 开销巨大,但 execute 已包含大部分 | fetchall 反而没那么夸张 | buffered 模式下 execute 已预读全量数据 | ★★★★☆ | | 3 | 加 LIMIT 500 后执行计划变化,担心结果不准 | 扫描行数/时间大幅减少,但可能缺失组 | 优化器利用 ORDER BY + LIMIT 提前终止,可能导致聚合不完整(数据倾斜时) | ★★★★☆ | | 4 | Python 整体时间与 C 客户端差距大 | Python 脚本慢 10x+ | 纯 Python 实现 + 类型转换 + 内存管理 vs C 高效解码/缓冲 | ★★★★☆ | 使用 **SSCursor** + **迭代器取数** + **perf_counter** 高精度计时,确保 execute 时间干净(不含全量 fetch)。 --- ## 2. v3/tdsql_client 驱动入口(PyMySQL 与 mysqlclient) `v3/core/tdsql_client` 已支持双驱动入口,并统一读取 `config.ini` 的 `[tdsql]`。 ### 2.1 配置方式(config.ini) ```ini [tdsql] ### 2.2 运行时覆盖(sql_exe.py) ```bash python v3/core/tdsql_client/sql_exe.py --driver mysqlclient ``` 优先级: 1. `sql_exe.py --driver` 2. `config.ini [tdsql] driver` 3. 默认 `pymysql` ### 2.3 当前执行时间口径(v3) - 主字段:`execution_time`(仅 execute 阶段) - 辅助字段:`fetch_time`、`total_time` - 默认执行模式:`fetch_mode=none`(不返回结果集,避免大结果集传输/解码开销污染主指标) qlclient 快速自检脚本 ```bash python v3/core/tdsql_client/mysqlclient_smoke_test.py ``` 脚本行为: - 强制使用 mysqlclient 驱动 - 读取 `config.ini [tdsql]` - 执行最小查询并输出 `execution_time/fetch_time/total_time`