Pymysql批量插入避坑指南:executemany常见错误与ON DUPLICATE KEY UPDATE的正确用法
Pymysql批量插入避坑指南executemany常见错误与ON DUPLICATE KEY UPDATE的正确用法最近在做一个数据同步项目需要把上游系统每天产生的几百万条记录高效地写入MySQL。一开始我天真地用了最熟悉的cursor.execute()结果跑了一个通宵还没完成十分之一。团队里一位资深工程师看了一眼我的代码只说了句“试试executemany吧再结合ON DUPLICATE KEY UPDATE处理重复数据。” 我照做了性能提升了上百倍但随之而来的是一堆意想不到的语法错误和逻辑陷阱。这篇文章就是把我踩过的那些坑、调试到半夜才搞明白的细节以及最终稳定运行的解决方案毫无保留地分享给各位正在与数据库打交道的开发者。无论你是要处理用户行为日志、同步订单数据还是构建实时数据管道掌握executemany与ON DUPLICATE KEY UPDATE的联用技巧都能让你事半功倍。1. 理解executemany不仅仅是“批量”那么简单很多开发者对executemany的理解停留在“批量执行多条SQL”这其实低估了它的价值也埋下了错误的种子。从数据库驱动层面看executemany与循环调用execute有本质区别。当你使用循环execute时Python代码和MySQL服务器之间会发生多次完整的“请求-响应”交互。每一次插入都伴随着网络传输、SQL解析、权限验证、执行计划生成等一系列开销。而executemany在底层通常采用“预处理语句批量参数传递”的机制。以PyMySQL为例它会先将SQL语句如INSERT INTO table VALUES (%s, %s)发送到MySQL服务器进行预处理得到一个statement handler。然后它将所有要插入的数据一个参数列表一次性或分批次发送给这个预处理好的语句执行。这极大地减少了网络往返次数和服务器端的重复解析开销。一个关键但常被忽略的细节是参数格式。executemany的第二个参数必须是一个序列的序列最常见的是列表套元组。# 正确列表内每个元素是一个元组代表一行数据 data_correct [ (101, Alice, aliceexample.com), (102, Bob, bobexample.com), (103, Charlie, charlieexample.com) ] # 错误列表内是普通值或其他结构 data_wrong1 [101, Alice, aliceexample.com] # 不是序列的序列 data_wrong2 [(101, Alice, aliceexample.com)] # 虽然正确但只有一行用executemany意义不大 data_wrong3 [[101, Alice, aliceexample.com]] # 列表套列表PyMySQL通常也接受但非标准元组形式注意确保你的数据生成逻辑比如从CSV读取、从API获取最终产出的是这种List[Tuple]格式。我遇到过因为用了列表推导式生成列表而忘记转换成元组导致executemany静默失败插入0行的情况。另一个性能相关的坑是批量大小。虽然executemany一次性发送所有数据但MySQL服务器和客户端驱动对单次传输的数据包大小都有限制max_allowed_packet。如果你试图一次性插入100万行很可能会遇到Packet too large的错误。一个稳健的做法是进行分批处理def batch_insert(cursor, sql, data, batch_size10000): 分批执行批量插入 for i in range(0, len(data), batch_size): batch data[i:i batch_size] cursor.executemany(sql, batch) print(f已插入 {ilen(batch)} / {len(data)} 行) # 注意conn.commit() 需要在所有批次执行完后调用一次下表对比了不同插入方式在10万条数据量级下的近似性能差异环境本地MySQL中等配置插入方式核心机制预估耗时网络请求次数适用场景循环 execute逐行交互非常高 5分钟N数据行数极少量数据或每行逻辑复杂独立executemany预处理批量传参低约 2-3秒1或少量分批主流批量插入场景LOAD DATA INFILE文件直接加载极低 1秒1文件传输超大数据量千万级允许中间文件2. ON DUPLICATE KEY UPDATE 的机制与常见误解ON DUPLICATE KEY UPDATE是MySQL提供的一个非常强大的语法糖用于处理“如果记录存在则更新不存在则插入”的场景。它依赖于表上的唯一键约束主键或唯一索引。当插入的数据违反某个唯一键约束时MySQL不会报错而是转而执行UPDATE子句。但它的行为有几个反直觉的地方是很多错误的根源“受影响行数”的含义特殊cursor.rowcount返回的值。如果是新插入一行返回1。如果是一行被更新返回2。如果一行被更新但更新的值和原有值完全相同即实际上没变返回0。 如果你用这个值来统计“成功处理”的行数需要小心处理。自增ID的“跳跃”问题对于带有AUTO_INCREMENT主键的表即使最终执行的是UPDATE操作INSERT部分也会尝试申请一个新的自增ID。虽然这个ID不会被使用因为行已存在但它已经被消耗掉了会导致自增序列出现不连续的“空洞”。这在一些对ID连续性有严格要求的业务中需要注意。更新值的来源这是与executemany结合时最容易出错的地方。在普通的INSERT ... ON DUPLICATE KEY UPDATE语句中你可以在UPDATE子句里引用VALUES(column_name)来获取本次试图插入的值。-- 单条插入时的标准写法 INSERT INTO users (id, name, login_count) VALUES (1, Alice, 1) ON DUPLICATE KEY UPDATE name VALUES(name), -- 使用试图插入的‘Alice‘ login_count login_count 1; -- 使用原有值1关键在于VALUES(column_name)指的是当前这条INSERT语句试图插入的值。当executemany把多条数据打包成一批时这个机制依然有效但你必须保证SQL语句的写法能正确匹配到每一行数据对应的值。3. executemany与ON DUPLICATE KEY UPDATE联用的核心陷阱这是本文要解决的核心难题。当你把这两个功能组合时一个经典的错误写法是这样的# 错误示例 sql_wrong INSERT INTO products (sku, name, stock) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE name %s, -- 错误这里的%s无法正确绑定到批量数据中对应行的值 stock %s; -- 错误 data [(SKU001, ProductA, 100), (SKU002, ProductB, 200)] cursor.executemany(sql_wrong, data)这段代码看起来逻辑很通顺插入三个值更新两个字段。但executemany在处理时会将data列表中的每个元组依次替换SQL中所有的%s占位符。对于第一行数据(SKU001, ProductA, 100)它期望SQL语句中有3个%s来对应。然而上面的错误SQL中实际有5个%s3个在VALUES里2个在UPDATE里。这会导致参数数量不匹配的错误或者更糟糕地引发难以察觉的逻辑错误比如用stock的值错误地更新了name字段。正确的做法是在ON DUPLICATE KEY UPDATE子句中绝对不要使用%s作为占位符而必须使用VALUES(column_name)函数来引用你试图插入的那一列的值。# 正确示例 sql_correct INSERT INTO products (sku, name, stock) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE name VALUES(name), -- 引用试图插入的name值 stock VALUES(stock); -- 引用试图插入的stock值 cursor.executemany(sql_correct, data)这样executemany只需要为每一行数据提供3个参数对应VALUES里的三个%s。UPDATE子句中的VALUES(name)和VALUES(stock)会由MySQL自动从同一行的插入值中获取。进阶场景基于旧值更新有时我们的需求不是简单覆盖而是基于原有值计算比如“库存累加”。这时VALUES()函数依然是你最好的伙伴。sql_update_with_old INSERT INTO sales_statistics (date, product_id, sales_volume) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE sales_volume sales_volume VALUES(sales_volume); -- 将新值累加到旧值上 # 假设data是[(‘2023-10-27‘, 1, 5), (‘2023-10-27‘, 2, 3)] # 如果(‘2023-10-27‘, 1)的记录已存在且sales_volume为10执行后该记录会变为15。4. 实战构建一个健壮的数据去重更新工具理论说完了我们动手写一个在实际项目中可复用的工具函数。这个函数需要处理分批插入、重复数据更新、事务控制、以及基本的错误处理。假设我们有一个user_activity表结构如下以(user_id, activity_date)为复合唯一键CREATE TABLE user_activity ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, activity_date DATE NOT NULL, login_count INT DEFAULT 0, last_active TIMESTAMP, UNIQUE KEY uk_user_date (user_id, activity_date) );我们的工具函数需要将一批用户活动数据插入或更新到这个表中。import pymysql from typing import List, Tuple, Optional import logging logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) def upsert_user_activity_batch( connection_params: dict, data: List[Tuple[int, str, int, str]], # (user_id, activity_date, login_count, last_active) batch_size: int 5000, table_name: str ‘user_activity‘ ) - Optional[int]: 批量插入或更新用户活动数据。 参数: connection_params: 数据库连接参数字典 data: 数据列表每个元组格式为 (user_id, activity_date, login_count, last_active) batch_size: 每批处理的数据量 table_name: 目标表名 返回: 成功处理的总行数失败则返回None if not data: logger.warning(输入数据为空无需操作。) return 0 sql f“““ INSERT INTO {table_name} (user_id, activity_date, login_count, last_active) VALUES (%s, %s, %s, %s) ON DUPLICATE KEY UPDATE login_count login_count VALUES(login_count), -- 登录次数累加 last_active GREATEST(last_active, VALUES(last_active)); -- 取最晚的活动时间 ”““ conn None cursor None total_affected 0 try: conn pymysql.connect(**connection_params) cursor conn.cursor() # 分批处理 for i in range(0, len(data), batch_size): batch data[i:i batch_size] try: cursor.executemany(sql, batch) # rowcount是这一批次受影响的行数总和1,2,0的累加 # 注意它不完全等于“处理的行数”但可以作为参考 affected cursor.rowcount total_affected affected conn.commit() # 每批提交一次平衡性能与数据安全 logger.info(f批次 {i//batch_size 1} 完成本批受影响行数: {affected} 累计: {total_affected}) except pymysql.Error as e: conn.rollback() # 当前批次回滚 logger.error(f批次 {i//batch_size 1} 插入失败错误: {e} 已回滚。失败数据起始索引: {i}) # 这里可以根据业务决定是终止还是跳过本批继续 # 例如记录错误批次稍后重试或人工处理 raise # 重新抛出异常终止函数 logger.info(f批量upsert操作完成总受影响行数参考: {total_affected}) return total_affected except pymysql.Error as e: logger.error(f数据库操作发生错误: {e}) return None finally: if cursor: cursor.close() if conn: conn.close() # 使用示例 if __name__ ‘__main__‘: config { ‘host‘: ‘localhost‘, ‘user‘: ‘your_username‘, ‘password‘: ‘your_password‘, ‘database‘: ‘your_database‘, ‘charset‘: ‘utf8mb4‘ } # 模拟数据user_id, activity_date (YYYY-MM-DD), login_count, last_active (YYYY-MM-DD HH:MM:SS) sample_data [ (1001, ‘2023-10-27‘, 2, ‘2023-10-27 14:30:00‘), (1002, ‘2023-10-27‘, 1, ‘2023-10-27 09:15:00‘), (1001, ‘2023-10-27‘, 1, ‘2023-10-27 22:00:00‘), # 与第一条user_id和date相同会触发UPDATE (1003, ‘2023-10-27‘, 1, ‘2023-10-27 12:00:00‘), ] result upsert_user_activity_batch(config, sample_data, batch_size1000) if result is not None: print(f操作完成受影响行数参考: {result})这个工具函数包含了几个关键设计分批处理避免单次数据包过大。每批独立事务一批失败只回滚当前批次不影响已成功的批次根据业务容忍度你也可以改为全部成功后才提交。明确的更新逻辑login_count累加last_active取最大值这比简单覆盖更符合业务逻辑。基础日志方便追踪执行过程和定位问题。5. 性能调优与高级注意事项当你处理的数据量从几万上升到几百万甚至更多时一些额外的优化手段就变得至关重要。1. 调整数据库参数在客户端代码优化之外数据库本身的配置也影响巨大。两个关键参数max_allowed_packet控制MySQL服务器和客户端之间通信包的最大尺寸。如果批量数据太大需要调高此值例如设置为256M。可以在MySQL配置文件如my.cnf中修改或通过SQL会话设置但重启后失效。-- 查看当前值 SHOW VARIABLES LIKE ‘max_allowed_packet‘; -- 临时设置会话级 SET GLOBAL max_allowed_packet 256*1024*1024;innodb_buffer_pool_size对于InnoDB引擎这是最重要的缓存区。它的大小应设置为可用物理内存的50%-70%以确保批量插入的索引和数据更新能在内存中高效进行。2. 禁用索引和约束检查高风险需谨慎对于一次性导入海量历史数据的场景可以在导入前暂时禁用非唯一索引和外键约束导入后再重建这能大幅提升速度。-- 导入前 ALTER TABLE your_large_table DISABLE KEYS; -- 执行你的批量插入/更新操作... -- 导入后 ALTER TABLE your_large_table ENABLE KEYS;警告DISABLE KEYS只对非唯一索引有效唯一索引包括主键仍会被维护。此外在禁用键期间查询性能会严重下降。此操作必须在维护窗口或没有其他业务访问时进行。3. 使用LOAD DATA INFILE作为备选方案当executemany的性能仍无法满足要求时例如上亿条数据LOAD DATA INFILE是终极武器。它通过将数据写入文本文件再由MySQL直接从文件加载绕过了SQL解析和网络传输的绝大部分开销。PyMySQL可以通过执行SQL命令来使用它def bulk_load_from_csv(conn, filepath, table_name): 通过LOAD DATA LOCAL INFILE快速导入CSV文件 sql f“““ LOAD DATA LOCAL INFILE %s INTO TABLE {table_name} FIELDS TERMINATED BY ‘,‘ OPTIONALLY ENCLOSED BY ‘“‘ LINES TERMINATED BY ‘\n‘ IGNORE 1 ROWS; -- 如果CSV有标题头忽略第一行 ”““ cursor conn.cursor() try: cursor.execute(sql, (filepath,)) conn.commit() print(f从 {filepath} 导入了 {cursor.rowcount} 行数据。) except Exception as e: conn.rollback() print(f导入失败: {e}) finally: cursor.close() # 注意需要在连接时启用LOCAL_INFILE功能且MySQL服务器和客户端配置允许此操作。4. 监控与问题排查在运行大规模批量操作时建议监控以下指标MySQL进程列表查看是否有长时间运行的INSERT或UPDATE语句。SHOW PROCESSLIST;InnoDB状态关注插入缓冲、锁等待情况。SHOW ENGINE INNODB STATUS\G网络与IO使用iostat,vmstat等工具监控服务器磁盘IO和网络流量确保不是硬件瓶颈。最后记得在正式对生产环境运行超大批量操作前一定要在同等数据量和配置的测试环境进行充分验证。测试不仅包括功能正确性更要评估执行时间、对数据库负载的影响CPU、IO、锁等待以及是否有触发主从延迟的风险。我自己的习惯是在测试环境用1/10或1/100的数据量跑一遍记录耗时然后按比例预估生产环境时间并预留至少两倍的缓冲时间。

相关新闻

一站式解决Windows运行时组件问题:VisualCppRedist AIO全功能指南

一站式解决Windows运行时组件问题:VisualCppRedist AIO全功能指南

一站式解决Windows运行时组件问题:VisualCppRedist AIO全功能指南 【免费下载链接】vcredist AIO Repack for latest Microsoft Visual C Redistributable Runtimes 项目地址: https://gitcode.com/gh_mirrors/vc/vcredist 当您的Windows系统弹出"无法启…

2026/5/17 10:11:06 阅读更多 →
NotaGen场景应用:为短视频、游戏、影视生成原创配乐

NotaGen场景应用:为短视频、游戏、影视生成原创配乐

NotaGen场景应用:为短视频、游戏、影视生成原创配乐 1. 引言:当古典音乐遇见现代创作 想象一下,你正在为一个历史题材的短视频寻找背景音乐,或者为你的独立游戏设计一段符合中世纪氛围的配乐。你打开音乐库,却发现要…

2026/5/17 10:11:04 阅读更多 →
EcomGPT-中英文-7B电商模型C盘清理后遗症解决:模型依赖库丢失与虚拟环境恢复

EcomGPT-中英文-7B电商模型C盘清理后遗症解决:模型依赖库丢失与虚拟环境恢复

EcomGPT-中英文-7B电商模型C盘清理后遗症解决:模型依赖库丢失与虚拟环境恢复 你是不是也遇到过这种情况?为了给C盘腾出宝贵的空间,一顿操作猛如虎,清理了各种“临时文件”和“无用程序”。结果,之前跑得好好的EcomGPT…

2026/5/17 9:40:12 阅读更多 →

最新新闻

Windows 10/11 移动热点与第三方软件对比:5 项指标实测与 2 个典型错误排查

Windows 10/11 移动热点与第三方软件对比:5 项指标实测与 2 个典型错误排查

Windows热点与第三方工具深度评测:抓包环境搭建的5大核心指标与实战避坑指南当移动开发者需要调试App网络请求或安全工程师分析流量时,一个稳定的抓包环境如同外科医生的手术刀。本文将带您深入比较Windows原生热点与第三方工具在抓包场景下的真实表现&a…

2026/7/6 23:28:33 阅读更多 →
Unity 协程 IEnumerator 状态机解析:从 C# yield 到 IL2CPP 的 3 层转换

Unity 协程 IEnumerator 状态机解析:从 C# yield 到 IL2CPP 的 3 层转换

Unity 协程 IEnumerator 状态机解析:从 C# yield 到 IL2CPP 的 3 层转换当你在 Unity 中写下yield return null时,背后发生的魔法远比表面看起来复杂。这个简单的语句触发了从 C# 语法糖到 IL 中间语言,再到 IL2CPP 转换的三层技术栈转换。本…

2026/7/6 23:24:28 阅读更多 →
74HC32+MKV44F128VLH16实现高效按键矩阵与硬件去抖

74HC32+MKV44F128VLH16实现高效按键矩阵与硬件去抖

1. 项目背景与核心需求解析 在嵌入式系统开发中,按键输入是最基础的人机交互方式之一。传统方案通常直接将机械按键连接到微控制器的GPIO引脚,但这种做法存在两个显著问题:一是机械按键的触点抖动会导致误触发,二是大量按键会占用…

2026/7/6 23:24:28 阅读更多 →
显示器 Overdrive 技术原理:从 60Hz 到 240Hz 的响应时间优化算法

显示器 Overdrive 技术原理:从 60Hz 到 240Hz 的响应时间优化算法

显示器 Overdrive 技术原理:从 60Hz 到 240Hz 的响应时间优化算法当你在玩一款快节奏的射击游戏时,屏幕上快速移动的敌人是否会出现模糊的拖影?或者当你在观看体育赛事时,高速运动的球体是否变得难以追踪?这些现象的背…

2026/7/6 23:22:26 阅读更多 →
开源火箭仿真平台OpenRocket:从设计到飞行的全链路解决方案

开源火箭仿真平台OpenRocket:从设计到飞行的全链路解决方案

开源火箭仿真平台OpenRocket:从设计到飞行的全链路解决方案 【免费下载链接】openrocket Model-rocketry aerodynamics and trajectory simulation software 项目地址: https://gitcode.com/GitHub_Trending/op/openrocket 在航空航天工程领域,传…

2026/7/6 23:18:20 阅读更多 →
Obi Fluid v4.0.2 实战:Unity 2022.3 实现复杂管道流体交互(附3个关键参数)

Obi Fluid v4.0.2 实战:Unity 2022.3 实现复杂管道流体交互(附3个关键参数)

Obi Fluid v4.0.2 深度实战:打造工业级管道流体交互系统在Unity中实现逼真的管道流体效果一直是技术美术和程序员的挑战。传统方法如UV动画或粒子路径移动虽然简单,但难以模拟真实流体的物理特性。本文将基于Obi Fluid 4.0.2插件,带你从零构建…

2026/7/6 23:16:17 阅读更多 →

日新闻

H2 与 MySQL 单元测试兼容性:5 个关键 SQL 语句差异与规避方案

H2 与 MySQL 单元测试兼容性:5 个关键 SQL 语句差异与规避方案

H2与MySQL单元测试兼容性:5个关键SQL语句差异与规避方案1. 单元测试中的数据库兼容性挑战在Java开发领域,单元测试是保证代码质量的重要环节。当应用涉及数据库操作时,测试环境的搭建往往成为开发者的痛点。H2数据库因其轻量级、内存模式和快…

2026/7/6 0:01:17 阅读更多 →
Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘

Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘

Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘 【免费下载链接】rbtray A fork of RBTray from http://sourceforge.net/p/rbtray/code/. 项目地址: https://gitcode.com/gh_mirrors/rb/rbtray 你是否厌倦了Windows任务栏上密密麻麻的图标&…

2026/7/6 0:01:17 阅读更多 →
Visual C++ 运行时库一键安装终极指南:告别DLL缺失烦恼

Visual C++ 运行时库一键安装终极指南:告别DLL缺失烦恼

Visual C 运行时库一键安装终极指南:告别DLL缺失烦恼 【免费下载链接】vcredist AIO Repack for latest Microsoft Visual C Redistributable Runtimes 项目地址: https://gitcode.com/gh_mirrors/vc/vcredist 你是否曾经遇到过这样的情况:下载了…

2026/7/6 0:05:19 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/6 8:11:50 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/6 8:11:52 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/6 6:52:56 阅读更多 →

月新闻