存储过程调用慢于INSERT、executemany无性能提升的原因探究
数据表与存储过程定义
CREATE TABLE `inspect_call` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `task_id` bigint(20) unsigned NOT NULL DEFAULT '0', `cc_number` varchar(63) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `created_at` bigint(20) unsigned NOT NULL DEFAULT '0', `updated_at` bigint(20) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `task_id` (`task_id`) ) ENGINE=InnoDB AUTO_INCREMENT=234031 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci CREATE PROCEDURE inspect_proc(IN task bigint,IN number varchar(63)) INSERT INTO inspect_call(task_id,cc_number) values (task, number)
初始测试困惑
原本预期调用存储过程比直接执行INSERT快很多,但实际测试结果相反:插入10000条数据时,单条INSERT循环耗时约4分钟,调用存储过程循环耗时约15分钟,多次测试结果一致。MySQL服务器配置普通,但无法理解存储过程更慢的原因。测试代码如下(使用mysql-connector-python 8.0.31):
command = ("INSERT INTO inspect_call (task_id,cc_number)" "VALUES (%s, %s)") for i in range(rows): cursor.execute(command, (task_id,f"{cc}{i}")) # cursor.callproc("inspect_proc", (task_id,f"{cc}{i}")) cnx.commit()
注:已知可通过设置innodb_flush_log_at_trx_commit = 2提升插入速度,但暂不打算调整该参数。
更新1:executemany批量插入无性能提升
根据建议尝试executemany批量插入,但性能和单条循环INSERT基本一致:
cursor = cnx.cursor(buffered=True) for i in range(int(rows/1000)): data = [] for j in range(1000): data.append((task_id,f"{cc}{i*1000+j}")) cursor.executemany(command,data) cnx.commit() # 与以下代码性能无差异 cursor = cnx.cursor() for i in range(rows): cursor.execute(command, (task_id,f"{cc}{i}"))
多次测试(尝试过每次批量插入100条),结果两者性能基本一致,这是为什么?
更新2:网络问题解决后executemany仍无提升
最终发现之前插入慢的主因是从笔记本通过外部主机名访问数据库,将脚本上传至服务器内网访问后,速度大幅提升:插入10000条耗时约3-4秒,插入100000条耗时约36秒。但executemany仍未带来性能提升,这是为什么?
1. 存储过程比直接INSERT慢的原因
你的存储过程只是简单封装了单条INSERT语句,调用时依然是单次网络请求执行单条插入,反而额外增加了存储过程的调用开销(比如参数传递、存储过程的解析执行逻辑),所以比直接执行单条INSERT更慢。存储过程的性能优势通常体现在复杂逻辑的批量处理(减少多次网络交互+数据库端批量运算),而非这种简单的单条插入封装。
2. executemany未带来性能提升的原因
mysql-connector-python的executemany默认实现并非真正的批量插入(即生成INSERT INTO ... VALUES (...), (...)这类单条聚合SQL),而是在客户端循环执行单条INSERT,和你手动写for循环调用cursor.execute的逻辑几乎一致,因此性能没有差异。
要实现真正的批量插入,有两种可行方案:
- 手动构造批量INSERT语句:
batch_size = 1000 # 构造对应批量大小的VALUES占位符 command = f"INSERT INTO inspect_call (task_id,cc_number) VALUES {', '.join(['(%s, %s)']*batch_size)}" data = [] for i in range(rows): data.extend([task_id, f"{cc}{i}"]) # 达到批量大小就执行插入 if len(data) >= batch_size * 2: cursor.execute(command, data) data = [] # 处理剩余不足批量的部分 if data: remaining_count = len(data) // 2 command = f"INSERT INTO inspect_call (task_id,cc_number) VALUES {', '.join(['(%s, %s)']*remaining_count)}" cursor.execute(command, data) cnx.commit() - 启用连接器的批量插入优化:
创建连接时指定use_pure=False(使用C扩展实现的客户端),此时executemany会自动生成批量INSERT语句:cnx = mysql.connector.connect( host='你的数据库地址', user='用户名', password='密码', database='数据库名', use_pure=False # 启用C扩展客户端,开启executemany批量优化 ) # 后续使用executemany即可获得性能提升
3. 网络对插入性能的影响
跨公网访问时,单条INSERT的网络往返延迟(RTT)被多次累加,10000次请求的总延迟直接导致耗时极高;内网访问时网络延迟极低,此时客户端的执行逻辑是否真正实现批量,就成为了性能瓶颈——默认的executemany依然是多次单条请求,只是在客户端内部循环,内网低延迟下看起来和单条循环性能接近,但换成真正的批量插入,性能还能进一步提升。
内容的提问来源于stack exchange,提问作者Qiulang

