查看: 4557|回复: 3

PyMySQL连接数据库实战:连接参数、事务提交与批量插入性能避坑

[复制链接]
发表于 7 天前 | 显示全部楼层 |阅读模式
PyMySQL是纯Python实现的MySQL驱动,安装不需要编译C扩展,在Windows、Linux、macOS上表现一致,API也高度兼容MySQLdb,支持Python 3全系,并能配合MySQL 8.x使用;原文还提到MariaDB在驱动层面同样兼容。下面围绕原文的用户表示例,按安装、连接参数、增删改查、事务、连接池和常见报错重组关键代码与排查路径。

一、环境安装与表结构准备
先安装并确认版本,避免多Python环境装错:
  1. pip install pymysql
  2. import pymysql
  3. print(pymysql.__version__)
复制代码
在项目里可用requirements.txt锁版本,例如pymysql==1.1.0。写代码前先确认MySQL服务可用,并用最小权限账号连接。建库、建用户和演示表如下:
  1. CREATE DATABASE demo_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  2. CREATE USER 'demo_user'@'localhost' IDENTIFIED BY 'your_password';
  3. GRANT SELECT, INSERT, UPDATE, DELETE ON demo_db.* TO 'demo_user'@'localhost';
  4. FLUSH PRIVILEGES;
  5. USE demo_db;
  6. CREATE TABLE users (
  7.   id INT AUTO_INCREMENT PRIMARY KEY,
  8.   username VARCHAR(64) NOT NULL,
  9.   email VARCHAR(128),
  10.   age INT,
  11.   created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  12.   UNIQUE KEY uk_username (username)
  13. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
复制代码
字符集统一用utf8mb4,因为MySQL的utf8实际是utf8mb3,无法完整支持emoji和生僻字。权限不要直接GRANT ALL。

二、连接参数与资源管理
最小连接示例如下,注意host使用127.0.0.1而不是localhost,部分系统会把localhost解析到IPv6的::1,而MySQL默认监听IPv4,容易出现Can't connect to MySQL server on 'localhost'。
  1. import pymysql
  2. conn = pymysql.connect(
  3.   host='127.0.0.1',
  4.   port=3306,
  5.   user='demo_user',
  6.   password='your_password',
  7.   database='demo_db',
  8.   charset='utf8mb4'
  9. )
  10. try:
  11.   cursor = conn.cursor()
  12.   cursor.execute('SELECT VERSION()')
  13.   row = cursor.fetchone()
  14.   print('MySQL版本:', row[0])
  15. finally:
  16.   cursor.close()
  17.   conn.close()
复制代码
常用参数中,connect_timeout默认10秒,网络环境差时按需调整;max_allowed_packet默认67108864,插入大数据包时要调大;autocommit默认False,执行INSERT、UPDATE、DELETE后必须手动conn.commit(),否则看似执行成功但数据没有落库。该行为是事务保护机制,不是bug。PyMySQL连接和游标支持上下文管理,推荐用with自动释放:
  1. with pymysql.connect(
  2.   host='127.0.0.1',
  3.   user='demo_user',
  4.   password='your_password',
  5.   database='demo_db',
  6.   charset='utf8mb4'
  7. ) as conn:
  8.   with conn.cursor() as cursor:
  9.     cursor.execute('SELECT id, username, email FROM users LIMIT 5')
  10.     rows = cursor.fetchall()
  11.     for row in rows:
  12.       print(row)
复制代码

三、查询、分页与插入性能
游标提供fetchone()、fetchmany(size)、fetchall()。数据量小用fetchall没问题,几十万行全量加载可能撑爆内存,应使用fetchmany分块或服务端分页。分页查询示例:
  1. page_size = 100
  2. offset = 0
  3. while True:
  4.   cursor.execute(
  5.     'SELECT id, username, email FROM users ORDER BY id LIMIT %s OFFSET %s',
  6.     (page_size, offset)
  7.   )
  8.   rows = cursor.fetchall()
  9.   if not rows:
  10.     break
  11.   for row in rows:
  12.     print(row)
  13.   offset += page_size
复制代码
插入单条可用cursor.lastrowid拿自增ID。批量插入优先executemany,原文本地实测1000条数据,单条循环约1.5秒,executemany约0.2秒,差距约7到10倍;十万级以上可考虑LOAD DATA LOCAL INFILE。示例:
  1. sql = 'INSERT INTO users (username, email, age) VALUES (%s, %s, %s)'
  2. data = []
  3. for i in range(1000):
  4.   data.append((f'user_{i}', f'user_{i}@example.com', 20 + i % 30))
  5. cursor.executemany(sql, data)
  6. conn.commit()
  7. print('批量插入记录数:', cursor.rowcount)
复制代码
字段多时用DictCursor按字段名访问,便于JSON序列化和模板渲染;追求极致性能时默认元组游标略快,但万级以下差异可忽略。超大结果集用SSCursor或SSDictCursor,服务端保留游标、逐行拉取;同一连接上SSCursor是独占的,不能同时开第二个游标执行别的查询。

四、事务控制与自动提交选择
事务用于保证多条SQL要么全部成功、要么全部失败。PyMySQL中推荐try、commit、except、rollback结构:
  1. try:
  2.   conn.autocommit(False)
  3.   cursor.execute('UPDATE users SET age = age + 1 WHERE username = %s', ('user_1',))
  4.   cursor.execute('UPDATE users SET age = age - 1 WHERE username = %s', ('user_2',))
  5.   conn.commit()
  6.   print('事务提交成功')
  7. except Exception as e:
  8.   conn.rollback()
  9.   print('事务回滚:', e)
  10. finally:
  11.   conn.close()
复制代码
autocommit=True适合纯查询、非关键数据写入、日志和统计上报;涉及多表写入、订单支付库存等强一致业务、批量导入需整体回滚时必须关闭自动提交。单条INSERT在InnoDB中也会被包成独立事务,关闭自动提交批量写入可减少redo log同步次数。

五、SQL注入与连接池
动态拼接SQL危险,用户输入会被当成SQL语法执行。正确做法是参数化占位符:
  1. sql = 'SELECT * FROM users WHERE username = %s'
  2. cursor.execute(sql, (username,))
复制代码
PyMySQL会把username当纯数据处理,不参与SQL语法解析。并发高时每次新建连接容易报Too many connections,可用DBUtils的PooledDB:
  1. from dbutils.pooled_db import PooledDB
  2. import pymysql
  3. pool = PooledDB(
  4.   creator=pymysql,
  5.   maxconnections=6,
  6.   mincached=2,
  7.   maxcached=5,
  8.   blocking=True,
  9.   host='127.0.0.1',
  10.   user='demo_user',
  11.   password='your_password',
  12.   database='demo_db',
  13.   charset='utf8mb4'
  14. )
  15. conn = pool.connection()
  16. cursor = conn.cursor()
  17. cursor.execute('SELECT COUNT(*) FROM users')
  18. print(cursor.fetchone())
  19. conn.close()
复制代码
这里conn.close()是归还连接而非真正关闭;blocking=True表示池满时等待。原文压测中连接池比每次新建连接吞吐量约提升3倍。

六、常见报错与唯一键冲突
Can't connect通常查服务是否启动和host是否改127.0.0.1;Access denied查账号密码和GRANT权限;Unknown database查库名和database参数;Table doesn't exist查当前库和表名;Duplicate entry是唯一索引冲突;Incorrect string value多与字符集不支持emoji有关,表结构、连接参数和文件编码要统一utf8mb4;Lost connection和MySQL server has gone away要查max_allowed_packet、wait_timeout、空闲断开和重连保活。唯一键冲突可用一条SQL处理:
  1. sql = '''
  2. INSERT INTO users (username, email, age)
  3. VALUES (%s, %s, %s)
  4. ON DUPLICATE KEY UPDATE
  5. email = VALUES(email),
  6. agE = VALUES(age)
  7. '''
复制代码
排查时建议按顺序确认:MySQL服务是否正常、账号权限是否足够、database和表名是否正确、字符集是否统一、autocommit是否忘记提交、连接数/超时/包大小是否触顶。上述代码与数据均来自原文示例,可按实际环境替换账号、密码和表名。
回复

使用道具 举报

发表于 7 天前 | 显示全部楼层

Re: PyMySQL连接数据库实战:连接参数、事务提交与批量插入性能避坑

这篇整理得挺系统的,尤其是 localhost 和 127.0.0.1 这个坑,我当初第一次用 PyMySQL 就卡在这儿,报错看着像 MySQL 没启动,其实换个 IP 就通了。utf8mb4 和最小权限账号这两点也很值得强调,很多人图省事直接 GRANT ALL,后面换环境或者做审计时才发现麻烦。 autocommit 默认 False 这条确实要反复提醒,我习惯在连接参数里显式写上,需要自动提交的场景单独开连接,别混着用。不然调接口时看着返回成功,查库却一条没有,排查半天才发现是没 commit。 批量插入那块,executemany 快的原因主要是 PyMySQL 对 INSERT 语句会合并成一条多值 INSERT,减少网络往返,所以数据量一大就要留意 max_allowed_packet,单批条数太多容易直接超包报错。我一般按几百到一千条一批做切分,既稳又能保持性能。另外分页那块,LIMIT OFFSET 在 offset 很大时会越来越慢,数据量大时可以考虑用主键游标的方式翻页。 连接池和常见报错那部分正文好像没贴全,挺想看看你怎么处理连接失效和重连的,这块在实际项目里踩坑最多。
回复 支持 反对

使用道具 举报

发表于 7 天前 | 显示全部楼层

Re: PyMySQL连接数据库实战:连接参数、事务提交与批量插入性能避坑

楼主这篇整理得挺实在,几个点都戳中新手容易踩的坑。我特别认同 host 用 127.0.0.1 这一条,之前就遇到过 localhost 被解析成 ::1 导致连不上,排查半天才反应过来,明明是同一台机器。utf8mb4 那段也说得到位,很多人建库时随手写 utf8,后面遇到 emoji 存不进去才发现问题。 autocommit 默认 False 这个坑也值得反复强调,我见过不少人以为 execute 完就完事了,结果程序一退出数据全没落库,还以为是驱动的问题。其实用 with 上下文管理确实省心,不过要注意 with 对连接的作用是关闭连接,提交还得自己来,不能指望它自动 commit。 批量插入的性能差距确实明显,executemany 比循环单条快很多,十万级再上 LOAD DATA 也合理。SSCursor 独占这个细节容易忽略,同一连接开两个游标会报错,最好单独开连接用。 后面事务和连接池的部分好像没发完,期待楼主补上连接池那块,比如用 DBUtils 还是自己封装,以及连接超时和空闲回收怎么配比较稳。
回复 支持 反对

使用道具 举报

发表于 7 天前 | 显示全部楼层

Re: PyMySQL连接数据库实战:连接参数、事务提交与批量插入性能避坑

这篇整理得很实用,尤其是 localhost 和 127.0.0.1 的坑、autocommit 默认 False 需要手动 commit 这两点,很容易让人误以为插入成功。批量插入用 executemany 的对比数据也很直观,十万级再考虑 LOAD DATA LOCAL INFILE 的思路很清晰。字符集统一 utf8mb4、最小权限账号、with 自动释放资源这些细节也值得照着做。事务部分用 try、commit、except、rollback 的思路讲得很到位,SSCursor 独占连接这个提醒也很关键。整体是一篇能直接拿去排查和优化 PyMySQL 使用的实战总结。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-27 18:10 , Processed in 0.024222 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部