1. 这个项目到底在解决什么问题:Python与MySQL之间的那座桥
做Python开发的人,迟早要跟数据库打交道。我见过太多初学者卡在这一步:代码写到一半,需要把数据存下来,或者从数据库里查数据,结果卡在“怎么连”“怎么查”“怎么处理报错”上。网上教程确实不少,但要么只讲个demo,要么跳过了大量关键细节,看完还是不知道怎么用到真实项目里。
PyMySQL就是Python和MySQL之间那座最直接的桥。它的作用一句话就能说清:让Python代码能连接MySQL数据库,执行增删改查,拿到结果继续处理。这个库是纯Python实现的,不依赖C扩展库,安装简单,跨平台,可以说是我现在最推荐给初学者的MySQL操作库。而且它的API风格和MySQLdb非常接近,以后如果转用其他框架,心智负担也很小。
这篇内容适合所有正在学Python、准备做Web后端、写爬虫落地数据、做数据分析,或者想把自己的小工具接上数据库的朋友。不管你是刚装好Python还没碰过数据库的小白,还是已经写过几年代码但一直用ORM没手写过SQL的老手,这篇内容都能帮你在PyMySQL这条路上少踩几个坑。
我会从环境准备、连接配置、CRUD操作、参数化查询、事务处理、性能优化,一直讲到真实项目里最常见的报错和排查思路。所有代码我都标注了完整的上下文,你复制到自己的环境里改掉用户名密码就能直接跑通。这是一篇标准的“看完就能干活”的实操指南。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开发环境准备:Python、MySQL、PyMySQL三者怎么配才顺
2.1 先装Python:别用最新版,用稳定版
Python安装这块,我见过太多人在第一步就踩坑。官方下载页面一打开就是3.13、3.14这种大版本,很多人直接装最新版,结果后面装依赖包时发现一堆坑——不是这个库不支持,就是那个编译报错。我的建议是不要刻意追求最新版本,选当前生态里兼容性最好的稳定版本。
具体操作就是:打开Python官网,Downloads页面里找“Stable Releases”下的版本,比如3.11.x或3.12.x,下载对应系统的安装包。需要特别提醒的是Windows安装时一定要勾选“Add Python to PATH”,这个选项默认是关闭的,漏了这一步,后面在命令行执行python就提示“不是内部或外部命令”,还得自己改环境变量,纯属浪费时间。
装完验证方式很简单,打开终端或命令行,输入:
bash复制python --version
pip --version
能正常输出版本号,就说明解释器和包管理器都就位了。
2.2 安装MySQL:最基础的服务端配置
MySQL的安装比Python麻烦一点。Windows用户推荐直接下载MySQL Installer,选Server Only或者Developer Default都行。安装过程中会让你设置root密码,这里别设太复杂的,但也不要真用root/root这种。开发环境用root很常见,但至少给个稍微像样的密码,不然习惯养成后上生产环境还要再改一遍,纯给自己找事。
Linux用户(Ubuntu/Debian系)可以用:
bash复制sudo apt update
sudo apt install mysql-server
安装完成后,检查一下服务是否启动:
bash复制sudo systemctl status mysql
注意MySQL 8.x和5.7的认证方式有区别,这一点非常关键。MySQL 8.0+默认使用caching_sha2_password认证插件,而PyMySQL老版本和很多其他客户端工具默认走的是mysql_native_password。如果你用的PyMySQL是新版本(1.0+)通常没问题,但如果之前装的是特别老的环境,后面连的时候就会报认证协议不匹配。这个我后面在常见问题章节会专门展开讲。
2.3 安装PyMySQL:一条命令的事
PyMySQL安装非常简单:
bash复制pip install pymysql
国内网络环境慢的话,可以加清华镜像源:
bash复制pip install pymysql -i https://pypi.tuna.tsinghua.edu.cn/simple
装完后验证一下:
bash复制python -c "import pymysql; print(pymysql.__version__)"
能输出版本号就说明安装成功了。需要提一句,PyMySQL是纯Python库,不依赖编译环境,这点比MySQLdb好很多。以前用MySQLdb时,Windows上光是编译那个C扩展就能劝退一堆人,还要装一堆MSVC工具链。PyMySQL完全没这个问题,这也是我推荐它的核心原因之一——装上就能用,不会把时间耗在配环境上。
3. 最核心的日常操作:连接、游标、增删改查
3.1 建立数据库连接:connect参数逐个说清楚
PyMySQL的connect方法参数不算多,但每个都值得理解清楚。我用一个实际例子来说明:
python复制import pymysql
conn = pymysql.connect(
host='localhost',
user='root',
password='your_password',
database='test_db',
port=3306,
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor,
autocommit=False
)
参数逐一说一下:
host:数据库服务器地址。本机就是localhost或127.0.0.1,远程服务器填IP或域名。user和password:登录账号密码。database:要连接的数据库名。如果还没有这个库,先不填,连接后再用SQL创建。port:MySQL默认端口3306,如果改过就填实际端口。charset:强烈推荐utf8mb4而不是utf8。因为utf8在MySQL里最多只支持3字节,存不了emoji和一些特殊字符,而utf8mb4是完整版UTF-8。这个坑我踩过,网页表单里用户输入了一个emoji,结果插入时直接报错,排查了半天才发现是字符集问题。cursorclass:指定游标类型为DictCursor,这样查询结果返回的是字典列表,每一行数据可以像row['name']这样通过字段名访问,比默认的元组可读性强太多。autocommit:是否自动提交事务。建议设成False,配合显式commit()和rollback(),事务边界更清晰。
3.2 游标的本质是什么
游标(cursor)这个概念很多初学者不理解。你可以把它想象成一个“指针”,指向查询结果集中的当前行。每次执行SQL,服务端返回一组数据,游标负责帮你一行一行地取数据,或者一次性取全部。
PyMySQL中游标的使用模式非常固定:
python复制cursor = conn.cursor() # 创建游标
cursor.execute(sql) # 执行SQL
results = cursor.fetchall() # 获取所有结果
cursor.close() # 关闭游标
注意游标用完后要关闭,释放数据库资源。但严格说,游标是否一定要手动close取决于连接是否关闭——如果连接是短连接,用完就关,游标也会随之释放;如果是长连接,频繁建游标不关就会积累资源,所以养成良好的习惯总是对的。
3.3 建库建表与插入:先跑通最小流程
连接建立后,第一步是建库建表。假设我要建一个用户表:
python复制import pymysql
# 先连接服务器(不指定database)
conn = pymysql.connect(
host='localhost',
user='root',
password='your_password',
charset='utf8mb4'
)
cursor = conn.cursor()
# 创建数据库
cursor.execute("CREATE DATABASE IF NOT EXISTS test_db DEFAULT CHARACTER SET utf8mb4")
# 切换到test_db
conn.select_db('test_db')
# 创建用户表
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
age INT,
email VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
conn.commit()
cursor.close()
conn.close()
有几个细节我特别想强调:
IF NOT EXISTS是开发期救命的写法。脚本重复跑不会直接报错“表已存在”。ENGINE=InnoDB是必须的。InnoDB支持事务、支持外键,而MyISAM不支持事务。如果你只是临时做着玩无所谓,但只要是正经项目,一律InnoDB。DEFAULT CHARSET=utf8mb4配合前面连接时的charset=utf8mb4,双保险防止中文乱码。
插入数据的代码:
python复制cursor = conn.cursor()
sql = "INSERT INTO users (name, age, email) VALUES (%s, %s, %s)"
cursor.execute(sql, ('张三', 25, 'zhangsan@example.com'))
conn.commit()
print(cursor.lastrowid) # 获取自增主键ID
cursor.close()
这里有一个新手非常容易踩的坑:PyMySQL的占位符是%s,不是MySQL原生SQL风格的?。有些从其他语言转过来的朋友习惯写?,结果执行直接报语法错误,还以为是SQL写错了,其实是占位符风格不对。
还有一个特别重要但容易被忽略的细节:execute之后一定要commit()。如果你忘记提交,代码不报错,数据也没写进去。我见过太多人问“为什么我insert了数据库里没有数据”,十有八九就是没提交。即使你在连接参数里设了autocommit=False(我推荐的设置),每次写操作后都需要手动提交。
3.4 查询、更新、删除的完整套路
查询是日常用得最多的操作,分几种情况来看。
查询所有行:
python复制cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
rows = cursor.fetchall()
for row in rows:
print(row['id'], row['name'], row['age'])
查询单行:
python复制cursor.execute("SELECT * FROM users WHERE id = %s", (1,))
row = cursor.fetchone()
if row:
print(row['name'])
这里注意(1,)——单个参数的元组一定要带逗号。这是Python语法里的常见坑:(1)不是元组,只是整数外面的括号,运行时会报参数数量不匹配。
带条件的更新:
python复制sql = "UPDATE users SET age = %s WHERE name = %s"
cursor.execute(sql, (26, '张三'))
conn.commit()
print(f"影响行数: {cursor.rowcount}")
删除:
python复制sql = "DELETE FROM users WHERE id = %s"
cursor.execute(sql, (1,))
conn.commit()
关于影响行数rowcount,这个属性在PyMySQL里返回的是“匹配行数”还是“受影响行数”,在MySQL默认配置下通常是实际被修改的行数。但要注意,如果更新的值和原来一样,比如把age从25改成25,MySQL实际没有修改行数据,rowcount可能是0。这和“有没有查到数据”是两码事,业务逻辑里如果要根据rowcount判断是否存在记录,需要用SELECT先查,而不是看UPDATE的rowcount。
3.5 连接管理的最佳实践:上下文管理器是底线
我一开始写代码时都是手工conn.close(),但写着写着就会漏。后来统一改用with上下文管理器,省心很多:
python复制from contextlib import closing
import pymysql
with closing(pymysql.connect(
host='localhost',
user='root',
password='your_password',
database='test_db',
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)) as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT * FROM users")
rows = cursor.fetchall()
print(rows)
closing会自动调用conn.close(),即便中间代码抛了异常也会执行。游标部分使用with conn.cursor()也能自动关闭。唯一要注意的是,事务的commit()不会自动执行,仍然需要你显式写。所以完整写法里,with块结束前需要手动conn.commit()。
在Django、Flask这类Web框架里,连接管理会更复杂一些,每个请求都要保证连接正确打开和释放。后面我用Flask举例时会专门展示一个连接池方案,那个才是Web场景下的长期解法。
4. 进阶操作:参数化查询、事务、并发与安全
4.1 为什么必须用参数化查询而不是字符串拼接
教程里演示增删改查时,很多人会偷懒用f-string拼SQL,比如:
python复制# 错误示范,千万别学
sql = f"SELECT * FROM users WHERE name = '{name}'"
cursor.execute(sql)
这个写法最大的问题就是SQL注入漏洞。用户的输入一旦包含恶意字符,比如输入' OR '1'='1,拼出来的SQL就变成:
sql复制SELECT * FROM users WHERE name = '' OR '1'='1'
这个条件永远为真,直接把整个表数据全查出来。再极端一点,输入'; DROP TABLE users; --,后果就是删库跑路现场。
正确做法是参数化查询:
python复制sql = "SELECT * FROM users WHERE name = %s"
cursor.execute(sql, (name,))
参数化查询的原理是:SQL语句的结构先被解析,参数只是作为纯数据处理,不会改变SQL语义。这就从根上堵死了注入漏洞。**任何从用户那里拿到的数据,无论前端做过什么校验,后端一旦进入SQL,都必须走参数化查询。**这个习惯从第一天就要养成,别等项目上线了被别人扫描出注入漏洞再后悔。
4.2 事务处理:commit、rollback与savepoint的完整认知
事务是数据库一致性的核心保障。一个转账场景最经典:A账户扣钱、B账户加钱,如果第二步失败,第一步也必须撤回,否则钱就凭空消失了。
PyMySQL里的事务控制是靠连接对象的方法实现的。我把事务的完整模式写出来:
python复制import pymysql
conn = pymysql.connect(
host='localhost',
user='root',
password='your_password',
database='test_db',
charset='utf8mb4',
autocommit=False
)
cursor = conn.cursor()
try:
# 开启事务:在autocommit=False模式下,第一条SQL执行即默认开启事务
cursor.execute("UPDATE accounts SET balance = balance - 100 WHERE user_id = 1")
cursor.execute("UPDATE accounts SET balance = balance + 100 WHERE user_id = 2")
# 这里故意模拟一个可能失败的场景
cursor.execute("SELECT * FROM users WHERE id = 99999")
if cursor.fetchone() is None:
raise ValueError("用户不存在,回滚所有操作")
conn.commit() # 事务提交,所有修改生效
print("事务提交成功")
except Exception as e:
conn.rollback() # 出现异常,全部回滚
print("事务回滚:", e)
finally:
cursor.close()
conn.close()
这段代码的核心逻辑是:try块里所有SQL都成功,才commit();任何一步异常,进入except执行rollback(),数据库回到事务开始前的状态。
rollback()回滚的是整个事务。如果事务很大,中间已经执行了一堆操作,只想回滚到某个中间点,可以用savepoint:
python复制cursor.execute("UPDATE accounts SET balance = balance - 100 WHERE user_id = 1")
cursor.execute("SAVEPOINT sp1") # 设置保存点
cursor.execute("UPDATE accounts SET balance = balance - 50 WHERE user_id = 2")
# 发现问题,回滚到sp1
cursor.execute("ROLLBACK TO SAVEPOINT sp1")
这个场景实际用得比较少,但理解它能帮你更好地把握事务边界。
另外,同一个连接对象的事务是共享的。如果在一次事务里执行了多个execute,直到commit()或rollback()之前,对数据库的修改对其他连接都是不可见的(在默认隔离级别REPEATABLE READ下)。这也是事务隔离的意义所在。
4.3 事务隔离级别的坑:默认配置下你可能读不到最新数据
MySQL默认的隔离级别是REPEATABLE READ,这意味着在同一个事务里,你多次查询看到的是同一个快照。这在某些场景下会造成“明明别的连接已经更新了数据,我这里查不到”的错觉。
举个例子:
python复制# 连接1:开启事务读数据
conn1 = pymysql.connect(...)
cur1 = conn1.cursor()
cur1.execute("SELECT age FROM users WHERE id = 1")
print(cur1.fetchone()) # 假设输出25
# 连接2:修改数据并提交
conn2 = pymysql.connect(...)
cur2 = conn2.cursor()
cur2.execute("UPDATE users SET age = 30 WHERE id = 1")
conn2.commit()
# 连接1:再次查询,仍然是25,而不是30
cur1.execute("SELECT age FROM users WHERE id = 1")
print(cur1.fetchone()) # 输出25
这不是Bug,是InnoDB在REPEATABLE READ下的快照读机制。如果业务要求每次都读到最新已提交数据,可以在查询SQL里加FOR UPDATE(锁定读),或者把连接隔离级别改成READ COMMITTED。有些团队在代码里遇到这种诡异问题,排查半天最后发现是隔离级别没搞明白。
PyMySQL里设置隔离级别可以在初始化时用isolation_level参数控制,或者用conn.begin()手动开始事务时指定模式,但一般改全局默认就够用,这是DBA或者架构层面要考虑的事情,自己写脚本的话知道有这回事就行。
4.4 并发写入安全:连接别共享,事务要短
多线程操作数据库时,有一个原则必须守住:每个线程用独立的连接,不要共享同一个连接对象。
PyMySQL的Connection对象不是线程安全的。多个线程共用同一个连接会有很多诡异问题:游标状态冲突、事务错乱、数据串了等。正确做法是每个线程自己创建连接,用完关闭。如果觉得创建连接开销大,就直接用连接池,后面我会专门写一节。
另外,事务要短。事务里不要写耗时的网络请求、文件IO、time.sleep(),这些操作会一直持有行锁或间隙锁,拖累整体并发性能。
5. 性能优化:从单条插入到批量写入、连接池与索引
5.1 executemany:批量插入的正确姿势
我见过很多人在循环里执行execute做批量插入,比如遍历一万条数据,一条条insert。这种写法性能极差。MySQL每次执行都是一次网络往返,一万条就是一万次,数据量一大基本上可以泡杯茶等结果了。
PyMySQL提供了executemany方法,可以在一条SQL中批量执行多条参数:
python复制import pymysql
conn = pymysql.connect(
host='localhost',
user='root',
password='your_password',
database='test_db',
charset='utf8mb4',
autocommit=False
)
cursor = conn.cursor()
data = [
('Alice', 20, 'alice@example.com'),
('Bob', 22, 'bob@example.com'),
('Charlie', 21, 'charlie@example.com'),
]
sql = "INSERT INTO users (name, age, email) VALUES (%s, %s, %s)"
cursor.executemany(sql, data)
conn.commit()
print(f"插入行数: {cursor.rowcount}")
cursor.close()
conn.close()
executemany底层会把多条参数组合成多行VALUES,一条SQL发给MySQL服务端执行。性能比循环execute高一个数量级。数据量在几千、几万条时效果特别明显。
5.2 万级数据导入推荐方案:分批+事务
如果你想导入几十万条数据,一个大的executemany执行所有数据,也不是最优方案。MySQL对单条SQL的长度有限制(max_allowed_packet),数据过大容易把包撑爆。正确做法是分批执行,每批几千条,每批一个独立事务:
python复制import pymysql
conn = pymysql.connect(...)
cursor = conn.cursor()
batch_size = 5000
sql = "INSERT INTO users (name, age, email) VALUES (%s, %s, %s)"
data = [] # 假设这是从外部文件或接口读到的数据
for i in range(0, len(data), batch_size):
batch = data[i:i+batch_size]
cursor.executemany(sql, batch)
conn.commit() # 每批提交一次
print(f"已插入 {i + len(batch)} 条")
cursor.close()
conn.close()
这种“小事务”的好处是:
- 不会占用过长事务,避免锁等待和undo log膨胀。
- 如果某一批失败,只会回滚当前批,已经成功提交的不受影响,方便断点续传。
5.3 连接池:Web应用必须掌握的方案
每次pymysql.connect()都是一次完整的TCP握手、MySQL认证、权限校验,这个开销在Web场景下是巨大的。一个每秒钟处理上百个请求的接口,每次都新建连接,后端数据库很快就扛不住了。
解决方案是连接池。常见的选择是DBUtils库,它是Web站点用Python连MySQL时最经典的连接池方案。写这篇内容的时候推荐使用dbutils包下的PooledDB:
bash复制pip install dbutils
使用示例:
python复制from dbutils.pooled_db import PooledDB
import pymysql
pool = PooledDB(
creator=pymysql,
maxconnections=10, # 连接池最大连接数
mincached=2, # 初始化时最少空闲连接数
maxcached=5, # 最多空闲连接数
blocking=True, # 连接不足时是否阻塞等待
maxusage=None, # 一个连接最大复用次数
setsession=[], # 每个连接创建时执行的SQL
ping=1, # 检查连接是否可用,1表示每次从池中取连接时ping一下
host='localhost',
user='root',
password='your_password',
database='test_db',
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
def query_user(user_id):
conn = pool.connection()
try:
with conn.cursor() as cursor:
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
return cursor.fetchone()
finally:
conn.close() # 不是真正关闭连接,而是归还到连接池
注意这里conn.close()的语义变了——对于连接池中的连接,close()是归还连接而不是销毁连接。连接池会在后台维护这几个连接,你需要使用时取出一个,用完还回去,别人继续用。这就是为什么连接池能显著降低连接创建开销。
参数里ping=1需要说一下:MySQL服务端有个wait_timeout配置,默认8小时,空闲超过这个时间连接会被服务端主动断开。如果不做心跳检测,从连接池里拿到一个已经断开的连接,执行SQL时就会报“MySQL server has gone away”。ping=1会在每次取连接时自动检测连接是否还活着,断开的就重连,这个参数能让你的应用稳定很多。
5.4 别忘了索引和表结构优化
PyMySQL本身不负责优化SQL,但优化最终还得落到SQL和表设计上。数据库性能80%以上靠索引。这里说几个常见原则:
- WHERE、ORDER BY、GROUP BY后面的字段要建索引。
- 不要对索引字段做函数运算,比如
WHERE DATE(created_at) >= '2024-01-01'会导致索引失效,应该写成WHERE created_at >= '2024-01-01 00:00:00'。 - 避免
SELECT *,只查需要的字段,减少网络传输和InnoDB回表。 - 大表分页
LIMIT 100000, 20不是好办法,越到后面越慢,可以用WHERE id > 100000 LIMIT 20来代替。
索引设计是个大话题,但日常开发中至少得懂这些基础原则。
6. 结合项目实践:把PyMySQL用进爬虫、Web和自动化脚本
6.1 爬虫数据落地:一个带去重的爬虫存储方案
爬虫是Python使用PyMySQL最常见的场景之一。爬下来的数据怎么存,很多人一开始是写CSV或JSON,但数据一多、要查重、要增量更新时就发现文件方案不够用,数据库才是正道。
我曾经爬过一个商品列表页,每页几十条数据,需要持续抓取并新增或更新。当时用一个简单的“先查再插或更新”的逻辑:
python复制import pymysql
def upsert_product(cursor, product):
# 检查是否已存在
cursor.execute(
"SELECT id FROM products WHERE source_url = %s",
(product['url'],)
)
row = cursor.fetchone()
if row:
# 已存在则更新价格和更新时间
cursor.execute(
"UPDATE products SET name = %s, price = %s, updated_at = NOW() WHERE source_url = %s",
(product['name'], product['price'], product['url'])
)
else:
# 不存在则插入
cursor.execute(
"INSERT INTO products (name, price, source_url) VALUES (%s, %s, %s)",
(product['name'], product['price'], product['url'])
)
这里用source_url做唯一标识,配合数据库里的唯一索引,即使程序并发跑,也不会插入重复数据。表设计时对source_url加上UNIQUE KEY,这是个很实用的技巧。
批量爬数据时,把所有商品先攒到列表里,最后统一executemany写入,再配合事务提交,效率很高。爬虫的数据量通常不如后端高并发接口那么大,所以用长连接逐条操作也可以,但批量操作永远是更好的习惯。
6.2 定时任务与自动化脚本:写入日志、统计数据
很多自动化脚本需要把运行状态记录下来。比如我写过一个定时监控脚本,每5分钟检查服务器磁盘使用率,发现超过80%就写入数据库并告警。用PyMySQL操作非常顺:
python复制import pymysql
import datetime
def write_monitor_log(disk_usage, alarm_level):
conn = pymysql.connect(...)
with conn.cursor() as cursor:
cursor.execute(
"INSERT INTO monitor_logs (disk_usage, alarm_level, created_at) VALUES (%s, %s, %s)",
(disk_usage, alarm_level, datetime.datetime.now())
)
conn.commit()
conn.close()
定时任务里有一个常见问题:脚本跑久了连接会断。我之前遇到过几次“任务跑了一周,某天突然报MySQL server has gone away”,原因就是连接空闲超过8小时被服务端断开。解决方案就是每次脚本启动时重新创建连接,或者启动时ping一下:
python复制try:
conn.ping(reconnect=True)
except pymysql.Error:
conn = pymysql.connect(...)
6.3 在Web框架中整合PyMySQL:Flask接口示例
Web后端是PyMySQL最广泛的应用场景。很多人会用SQLAlchemy ORM,但如果你项目简单,只想写原生的SQL,PyMySQL就够用了。我写一个Flask接口的小例子,展示真实项目中怎么结合使用:
python复制from flask import Flask, jsonify, request
from dbutils.pooled_db import PooledDB
import pymysql
app = Flask(__name__)
pool = PooledDB(
creator=pymysql,
maxconnections=10,
mincached=2,
maxcached=5,
blocking=True,
ping=1,
host='localhost',
user='root',
password='your_password',
database='test_db',
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
conn = pool.connection()
try:
with conn.cursor() as cursor:
cursor.execute("SELECT id, name, age, email FROM users WHERE id = %s", (user_id,))
user = cursor.fetchone()
if not user:
return jsonify({'error': '用户不存在'}), 404
return jsonify(user)
finally:
conn.close()
@app.route('/api/users', methods=['POST'])
def create_user():
data = request.get_json()
name = data.get('name')
age = data.get('age', 0)
email = data.get('email', '')
if not name:
return jsonify({'error': 'name不能为空'}), 400
conn = pool.connection()
try:
with conn.cursor() as cursor:
cursor.execute(
"INSERT INTO users (name, age, email) VALUES (%s, %s, %s)",
(name, age, email)
)
user_id = cursor.lastrowid
conn.commit()
return jsonify({'id': user_id, 'name': name}), 201
except pymysql.IntegrityError:
conn.rollback()
return jsonify({'error': '数据已存在或字段格式错误'}), 400
finally:
conn.close()
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
这里有几个细节值得学习:
- 每个请求从连接池拿连接,用完
finally归还,保证连接不泄漏。 - 参数化查询,用户输入不会进SQL,防注入。
- 捕获
IntegrityError返回友好错误信息,而不是让500错误暴露给前端。 - 连接池参数
maxconnections=10表示最多10个并发数据库连接。如果你的接口并发超过10,多出来的请求会等待空闲连接(blocking=True),这是可接受的行为。
6.4 调用MySQL存储过程:PyMySQL里的callproc
有些公司业务逻辑写在存储过程里,你用PyMySQL调用存储过程也很方便。PyMySQL提供了callproc方法:
python复制cursor = conn.cursor()
# 调用名为get_user_stats的存储过程,传入user_id,输出参数列表
cursor.callproc('get_user_stats', (1,))
# 获取结果集
result = cursor.fetchall()
存储过程里的OUT参数如何获取?PyMySQL的callproc有个特性,它会把存储过程的返回结果以多个结果集的形式暴露。通常你需要连续调用cursor.nextset()来获取后续的结果集。实际使用时我会这样处理:
python复制cursor.callproc('get_user_stats', (1, 0))
# 先拿第一个结果集
results = cursor.fetchall()
# 再拿OUT参数
cursor.execute("SELECT @_get_user_stats_1, @_get_user_stats_2")
param_values = cursor.fetchone()
@_存储过程名_参数索引是MySQL会话变量的命名方式,索引从0开始,对应传入参数的位置。这个技巧在文档里不显眼,但实际用到时非常关键。需要说明的是,如果你用的存储过程只是简单返回一个查询结果,直接用callproc然后fetchall就够了,不需要管OUT参数的事。
存储过程的管理在数据库工具(如Navicat)里通常会提供可视化操作,但归根结底还是SQL逻辑,这里不展开。
6.5 数据分析场景:PyMySQL + pandas 快速取数
做数据分析的人,数据经常在MySQL里。用pandas直接读MySQL非常顺:
python复制import pandas as pd
import pymysql
conn = pymysql.connect(
host='localhost',
user='root',
password='your_password',
database='test_db',
charset='utf8mb4'
)
df = pd.read_sql("SELECT * FROM users", conn)
print(df.head())
pandas的read_sql底层可以自动使用PyMySQL连接,它会把查询结果直接转成DataFrame,省去手写遍历和组装。反过来,如果要把DataFrame写回MySQL,用df.to_sql很方便,但要注意pandas的to_sql默认依赖SQLAlchemy,需要安装对应依赖:
bash复制pip install sqlalchemy
一个常见的坑是:pandas的to_sql写入时对中文和字符集的兼容性,如果写入报编码错误,先检查charset='utf8mb4'是否配置好,以及目标表字符集是否为utf8mb4。
7. 常见问题与排查技巧实录:这些坑我都替你踩过了
7.1 问题一:Access denied for user 'root'@'localhost'
出现这个报错,俗称“认证被拒绝”。原因分三类:
- 密码错误。检查
password参数是否与MySQL用户实际密码一致。 - 用户没有从当前主机访问的权限。MySQL的用户权限是“用户@主机”绑定的,比如'root'@'localhost'只能从本机登录,如果你的脚本从远程连,需要另建授权账户。
- 端口不对,连接到了别的服务上。检查
port是否是3306。
排查命令是在MySQL命令行里执行:
sql复制SELECT user, host FROM mysql.user;
看看root对应的host是什么。如果是localhost,而PyMySQL在服务器本机运行,那说明密码问题可能性最大。
7.2 问题二:Authentication plugin 'caching_sha2_password' cannot be loaded
这个错误在MySQL 8.0中非常常见。原因我前面提过:MySQL 8.0默认认证插件是caching_sha2_password,而旧版本PyMySQL或某些客户端还不支持这个协议。
解决办法有几种:
方法一:升级PyMySQL到1.0以上版本。
bash复制pip install --upgrade pymysql
新版本PyMySQL已经原生支持caching_sha2_password,这是最简单直接的方案。
方法二:修改MySQL用户认证插件为mysql_native_password。
sql复制ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password';
FLUSH PRIVILEGES;
这个方法在本地开发环境非常管用,但MySQL官方已经在逐步淘汰mysql_native_password,如果是新项目,优先升级客户端而不是改数据库配置。
7.3 问题三:中文乱码:Incorrect string value: '\xE5\xBC\xA0...'
插入中文时报编码错误,一般就是字符集问题。排查顺序:
- 数据库、表、字段的字符集是不是
utf8mb4。 - 连接参数
charset='utf8mb4'有没有配。 - 终端或文件本身的编码是否UTF-8(Windows下cmd默认gbk,容易出这个问题)。
我之前踩过的坑是:数据库建表时忘了指定DEFAULT CHARSET=utf8mb4,表默认继承了服务器级别的latin1,插入中文直接报Incorrect string value。改法:
sql复制ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
这个命令可以把表所有字段都转成utf8mb4。生产环境执行前一定要备份,数据量大时这个操作会锁表,要在低峰期做。
7.4 问题四:MySQL server has gone away
这个报错80%的情况是连接空闲时间过长,MySQL服务端主动断开了连接。wait_timeout默认8小时,如果你的长连接隔了很长时间没使用,再执行SQL就会报这个错。
解决方案:
- 每次使用前
conn.ping(reconnect=True)。 - 用连接池并设置
ping=1。 - 如果脚本批量任务中间要长时间处理其他逻辑,建议用一个线程定期发送
SELECT 1保持心跳。
一个容易忽略的细节是:MySQL的max_allowed_packet设置过小,执行大SQL(比如一次插入几MB的数据)也会触发“gone away”。顺便把配置调大:
sql复制SET GLOBAL max_allowed_packet = 64 * 1024 * 1024;
7.5 问题五:pymysql.err.ProgrammingError: You have an error in your SQL syntax
SQL语法报错时,先别急着改代码。我的排查步骤是:
- 先把SQL打印出来,用数据库客户端(如Navicat)手动执行一遍,确认是不是SQL本身有问题。
- 检查占位符是不是
%s,而不是?。 - 注意表名、字段名是否用了MySQL关键字,比如
order、group、desc,需要加反引号`。
尤其最后一个,排错时特别容易遗漏。字段名取名为order之类的,SQL写SELECT order FROM orders直接报语法错,因为ORDER是SQL关键字。要么改名,要么加反引号:
sql复制SELECT `order` FROM orders
7.6 问题六:数据库连接太多:Too many connections
这个错误说明连接数已经达到MySQL的max_connections上限。排查思路:
- 看代码里连接是否没关闭,最常见的是
conn.close()写在了return之后,异常路径不执行,连接全泄漏了。 - 检查连接池配置,
maxconnections是否设得过大。 - 临时调大MySQL上限:
sql复制SET GLOBAL max_connections = 500;
但根本解决办法是排查应用层连接泄漏,把所有conn和cursor都放进try/finally或with里保证释放。
7.7 问题七:pandas的to_sql报错ModuleNotFoundError
严格说这个不是PyMySQL直接报的错,而是pandas的to_sql依赖SQLAlchemy和sqlalchemy的MySQL驱动。需要安装:
bash复制pip install sqlalchemy pymysql
然后用create_engine创建连接串:
python复制from sqlalchemy import create_engine
import pandas as pd
engine = create_engine('mysql+pymysql://root:password@localhost:3306/test_db?charset=utf8mb4')
df.to_sql('users', con=engine, if_exists='append', index=False)
连接串里mysql+pymysql://意思是使用PyMySQL作为SQLAlchemy的数据库驱动,用户名、密码、主机、端口、库名、字符集都在URL里,非常直观。日常用pandas读MySQL时,我常用pd.read_sql(sql, engine),比原生的PyMySQL封装更好用。
8. 一些实际操作中的体会和小技巧
这部分想分享一些我平时积累的、很难在官方文档里看到的经验。
第一,调试SQL时,利用cursor._last_executed可以拿到实际执行的SQL语句。参数化查询时我们传的是%s占位符和参数列表,如果想看真正发到MySQL的SQL长什么样,可以在execute之后打印这个属性:
python复制cursor.execute("SELECT * FROM users WHERE id = %s", (1,))
print(cursor._last_executed)
这在排查“怎么查不到数据”“SQL哪里不对”的时候非常有用。
第二,生产环境记得把autocommit设为True,除非是明确的批量事务操作。Web接口场景下,每个请求都是一次独立操作,手动管理事务很容易漏提交导致线上数据没写进去。我自己写脚本时会设autocommit=False做显式事务管理,但Web接口里多用autocommit=True,避免因为异常路径忘了提交带来的麻烦。
第三,不要把敏感信息硬编码在代码里。数据库账号密码这些,要么用环境变量管理,要么放在配置文件里。我之前就见过同事把数据库密码直接提交到Git仓库,结果整个仓库需要重置历史记录,非常折腾。最低成本的做法是用环境变量:
python复制import os
import pymysql
conn = pymysql.connect(
host=os.getenv('DB_HOST', 'localhost'),
user=os.getenv('DB_USER', 'root'),
password=os.getenv('DB_PASSWORD', ''),
database=os.getenv('DB_NAME', 'test_db'),
charset='utf8mb4'
)
第四,数据库表设计阶段的投入永远比事后优化划算。字段类型的选择、是否为空、默认值、索引的设计,这些都要在一开始想清楚。PyMySQL只是执行你写的SQL,但SQL是否高效,取决于表设计。比如用户ID用BIGINT还是INT,邮箱字段要不要加UNIQUE KEY,状态字段用TINYINT还是ENUM,这些决策在数据量小时无所谓,到千万级时就是天壤之别。
第五,学会用EXPLAIN看执行计划。写一条慢查询,先用EXPLAIN SELECT ...看MySQL是怎么执行这条SQL的,有没有走索引、扫描多少行。很多时候程序慢不是PyMySQL的问题,而是SQL本身没写好。这个习惯能帮你从“随缘优化”变成“有的放矢”。
最后再分享一个小技巧:写操作多、读操作更多的时候,把SQL日志打开,观察慢查询。MySQL的慢查询日志配置很简单,在my.cnf里设置:
ini复制slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
然后定期看slow.log,凡是超过1秒的SQL都值得优化。我的经验是,把这些慢查询一条条用EXPLAIN分析,再用索引、改写SQL方式处理掉,数据库性能会得到立竿见影的提升。PyMySQL本身性能足够稳定,真正的瓶颈永远在SQL、架构和数据模型上。
