MySQL服务端采用流式发送机制,边查边发,首行延迟取决于是否能early termination;阻塞场景包括无索引ORDER BY、GROUP BY等需全量计算的操作;客户端须主动读取,否则触发net_write_timeout断连;大字段分帧传输、缓冲区与超时参数需协同调优。

MySQL服务端不是等结果集生成完才发,而是边查边发
只要存储引擎返回第一行(比如 InnoDB 从 B+ 树读出一条记录),服务端就立刻序列化成 Row Data Packet,写入 socket 发送缓冲区。客户端调用 fetchone() 或 mysql_fetch_row() 就可能拿到它。
这意味着:如果 SQL 带 LIMIT 10 且能 early termination(比如主键范围扫描),服务端很可能只读 10 行就停;但若用了 ORDER BY col 且 col 没索引,就得先把全部百万行排序完,再取前 N 行——这时首行要等到整个排序结束才发出。
常见阻塞首行的场景包括:GROUP BY、WINDOW FUNCTION、DISTINCT、临时表类操作,它们必须攒够数据才能计算,无法流式输出。
客户端必须主动读取,否则服务端会卡在 write() 阻塞
MySQL 协议是半双工的:服务端发数据时,客户端必须持续调用读取方法(如 cursor.fetchone()),否则服务端会在 send buffer 满后阻塞在 write() 系统调用上,最终触发 net_write_timeout(默认 60 秒)断连,报错 Lost connection to MySQL server during query。
尤其注意 C API:mysql_real_query() 只发请求,真正拉数据必须显式调 mysql_store_result()(全缓存)或 mysql_use_result()(流式),漏掉就会报 Commands out of sync。
Python 驱动(如 PyMySQL)默认用的是缓存模式,fetchall() 会把百万行全加载进内存;若只想逐行处理,应改用 fetchone() 循环,避免 OOM。
单行太大或字段含 BLOB/TEXT,会导致分帧和传输延迟
MySQL 协议按帧传输,每帧以 3 字节长度 + 1 字节序号开头。大字段(如 TEXT、BLOB)会被拆成多个 Row Data Packet 子帧,客户端需拼接还原。
这带来两个实际影响:
-
max_allowed_packet必须同时设在客户端和服务端,且 ≥ 单行最大可能字节数,否则报Packet too large - 即使只取前 10 行,若某一行含 2MB 的
LONGTEXT,也会触发多次小包发送,Wireshark 可见大量1500B左右的 TCP 包,首行延迟明显 - Python 中启用
use_unicode=False可减少 UTF-8 编码开销,对含大量中文字段的查询有实测提速
网络层和缓冲区参数直接影响百万行传输体验
关键配置项不是可有可无的调试项,而是直接决定能否完成传输:
net_buffer_length 控制服务端内部单行初始缓冲区大小(默认 16KB)。百万行中若平均行宽 2KB,它基本够用;但若有几行超 100KB,就会频繁 realloc,拖慢发送节奏。建议调到 64K~256K。
max_allowed_packet 是硬上限(默认 4MB)。若估算单行最大为 3MB,此值至少设为 4M,且客户端连接时也要显式传参,例如 PyMySQL 中加 max_allowed_packet=4194304。
最容易被忽略的是:服务端 wait_timeout 和客户端 socket 超时必须匹配。如果客户端 fetch 循环处理慢(比如每行做 HTTP 请求),而服务端 wait_timeout=30,连接会在第 31 秒无声断开,错误表现常为 MySQL server has gone away,而非明确超时提示。


















