Python操作MySQL的最全使用教程
前言先说一个不成立的说法「Python 内置了 MySQL 支持」。标准库里跟数据库沾边的模块只有sqlite3内置的嵌入式数据库引擎和dbm一类的键值存储MySQL 驱动一律要额外安装纯 Python 实现和 C 扩展实现都一样。第二个常见的混淆是把驱动driver和 ORM对象关系映射Object-Relational Mapping当成一回事。驱动负责按 MySQL 的线协议连上服务器、把 SQL 发过去、把结果取回来ORM 负责把数据表和 Python 类互相映射。ORM 底下一定还压着一个驱动。这一层分清楚选型才不乱。这两个误解决定了本文的写法先讲清主流驱动各自站在什么位置再讲它们共同遵守的契约——DB-API 2.0PEP 249最后落到事务语义、结果集处理与连接池。本文不逐条演示增删改查目标是让你读完能判断换个驱动哪些代码不用动、哪些必须改。另外网上大量 MySQL 教程还是 Python 2 时代的产物。Python 2.7 已于 2020 年 1 月 1 日停止维护那些print ...语句形式、dict.iteritems()、xrange在 Python 3 下都已失效。一、驱动全景与选型驱动导入名实现方式定位与取舍PyMySQLpymysql纯 Python无编译环境、脚本、教学跨平台一致代价是纯 Python 带来的额外开销mysqlclientMySQLdbC 扩展本机有 C 编译工具链与 MySQL 开发库时可用很多老代码依赖import MySQLdbmysql-connector-pythonmysql.connectorOracle 官方维护、自包含想和官方生态对齐自带连接池另有可选的 C 扩展SQLAlchemysqlalchemy工具层 / ORM不是驱动必须搭配上面任一个要模型映射、要跨库切换时用asyncmyasyncmyCython 实现的 asyncio 驱动asyncio 栈Windows 上编译扩展需要 C 构建工具aiomysqlaiomysql复用 PyMySQL 的 asyncio 驱动asyncio 栈API 形态接近 PyMySQL需要特别说明的是前四个是同步驱动都对外声明遵循 DB-API 2.0asyncmy 和 aiomysql 是异步驱动连接、执行、取结果都要await不在 PEP 249 的那份同步契约之内。二、DB-API 2.0同步驱动共有的契约PEP 249 解决的问题是同步驱动实现各不相同但对外暴露的名字和语义是一份约定。只要驱动声明自己合规换驱动时大部分调用代码就能原样保留。规范要求模块级必须有三个常量apilevel——支持的接口级别只允许1.0或2.0。threadsafety——0 表示线程间不能共享模块1 表示可共享模块但不能共享连接2 表示模块和连接都可共享3 表示模块、连接、游标都可共享。paramstyle——SQL 文本里的占位符风格。paramstyle是迁移时最要命的一个它决定 SQL 文本长什么样值形式例子qmark问号... WHERE name?numeric数字位置... WHERE name:1named命名... WHERE name:nameformatANSI C printf 风格... WHERE name%spyformatPython 扩展风格... WHERE name%(name)s各驱动实际声明的值如下可用驱动名.paramstyle读出核对驱动apilevelthreadsafetyparamstylePyMySQL2.01pyformatmysqlclientMySQLdb2.01formatmysql-connector-python2.01pyformat标准库sqlite3对照用2.0随底层构建而变qmark注意pyformat是format的超集声明pyformat的驱动既接受%s参数传序列也接受%(name)s参数传映射而声明format的驱动只承诺%s。这是跨驱动迁移时最容易踩的一处。连接对象必须提供的方法只有四个close()、commit()、rollback()、cursor()。游标对象必须提供execute(operation[, parameters])、executemany(operation, seq_of_parameters)、fetchone()、fetchmany([size])、fetchall()、close()以及只读属性rowcount、description和可读写属性arraysize默认1fetchmany()不带参数时就按它取行。用标准库的sqlite3可以离线验证这套公共契约——它同样是 DB-API 2.0 实现但不需要数据库服务器# 适用于 Python 3.8import sqlite3conn sqlite3.connect(:memory:)cur conn.cursor()cur.execute(CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT))cur.executemany(INSERT INTO t (name) VALUES (?), [(a,), (b,), (c,)])conn.commit()cur.execute(SELECT id, name FROM t ORDER BY id)print(rowcount:, cur.rowcount) # 规范允许为 -1表示「无法确定」print(fetchone:, cur.fetchone())print(fetchmany(2):, cur.fetchmany(2))print(fetchall:, cur.fetchall()) # 前面已经取完这里是空列表conn.close()这些名字都是 PEP 249 的公共接口换到 PyMySQL 上完全一样唯一要动的是占位符?改成%s。那么「换驱动」到底要改什么代码换驱动后connect(...)的调用必须改规范明说连接参数是「数据库相关」的不做规定SQL 文本里的占位符必须改由paramstyle决定%s与?不通用依赖驱动私有异常码的分支可能要改公共异常树一致私有错误码与子类名不一定有cur.lastrowid、conn.autocommit等不保证存在属于可选扩展用之前先确认cursor()/execute()/fetch*()/commit()/rollback()/close()不用改它们是规范的核心同一件事不同驱动怎么写把上表落到代码上真正会绊住迁移的是 SQL 文本本身Python 侧的调用形状是一样的。# 适用于 Python 3.8# 同一个查询两种 paramstyle —— 变的只有 SQL 文本# sqlite3: cur.execute(SELECT id, name FROM metrics WHERE score ?, (60,))# pymysql: cur.execute(SELECT id, name FROM metrics WHERE score %s, (60,))# pyformat 驱动还接受命名占位符参数传映射下面两行等价# cur.execute(SELECT id, name FROM metrics WHERE name %s, (cpu,))# cur.execute(SELECT id, name FROM metrics WHERE name %(name)s, {name: cpu})# 连接构造也是同一个形状但参数名不统一跨驱动得逐个查文档# pymysql.connect(host127.0.0.1, userapp, passwordpwd, databasedemo)# MySQLdb.connect(host127.0.0.1, userapp, passwdpwd, dbdemo)# mysql.connector.connect(host127.0.0.1, userapp, passwordpwd, databasedemo)参数名不是公约mysqlclient 沿用的是passwd/db这两个老名字换成另外两家就得改回password/database。三、事务隔离级别与提交语义PEP 249 对提交的要求非常直接.commit()提交任何待处理的事务并且如果数据库支持自动提交接口的初始状态必须是关闭自动提交。这就是「为什么 DB-API 非要你显式commit()」的根源——手动提交是规范定的默认值不是某个驱动的脾气。配套的两条规则同样写在规范里。一是.close()时若还有未提交的修改会触发隐式回滚——「没 commit 就直接关连接」不会报错但改动作废而且这个兜底行为不能依赖驱动和配置不同结果可能不一样。二是.rollback()在规范里标为可选不支持事务的数据库可以不实现它此时调用可能抛NotSupportedError。Connection.autocommit属于可选扩展读到True表示连接处在自动提交非事务模式。但 PEP 249 给了一条弃用提示——通过写这个属性切换模式已被弃用理由是写属性可能触发 I/O 及相关异常在异步环境里很难实现要开自动提交优先用建立连接时的构造参数。接下来是隔离级别。它和commit/rollback不在一个层次前者管「别的连接能看见你的什么」后者管「你自己的修改什么时候生效、什么时候撤销」。而且隔离级别是数据库层面的概念DB-API 不规定——驱动只负责把 SQL 发过去-- 隔离级别在会话/服务器层设置与用哪个 Python 驱动无关SET TRANSACTION ISOLATION LEVEL READ COMMITTED;MySQL 的 InnoDB 支持标准的四档隔离级别隔离级别脏读不可重复读幻读READ UNCOMMITTED可能可能可能READ COMMITTED不会可能可能REPEATABLE READ不会不会标准定义下可能SERIALIZABLE不会不会不会REPEATABLE READ是 InnoDB 的默认隔离级别MySQL 在这一档上额外用 next-key 锁抑制幻读效果比标准定义的底线更强。但「默认级别是哪个」随数据库和版本而变不要拿驱动文档去推断隔离级别去查目标数据库的文档。四、结果集、连接池与生产实践批量结果集executemany 与流式读取批量写用executemany(operation, seq_of_parameters)。规范允许驱动用「多次 execute」或「数组操作一次性提交」来实现两种都合规但规范明确写了这个方法的返回值未定义不要拿它当「插入了 N 行」的依据。对会产生结果集的语句调用executemany也属于未定义行为。读大结果集时别一次fetchall()全塞进内存。规范给出的通用工具是arraysize配fetchmany()arraysize决定fetchmany()不带参数时每次取多少行默认是1。# 适用于 Python 3.8import sqlite3conn sqlite3.connect(:memory:)cur conn.cursor()cur.execute(CREATE TABLE sample (id INTEGER PRIMARY KEY, val TEXT))cur.executemany(INSERT INTO sample (val) VALUES (?),[(fv{i},) for i in range(5)],)conn.commit()cur.arraysize 2 # 让下面不带参数的 fetchmany() 每次取 2 行cur.execute(SELECT id, val FROM sample ORDER BY id)while True:batch cur.fetchmany() # 不传 size就按 arraysize 取if not batch: # 取空了就是读完breakprint(batch)conn.close()除了规范里的arraysize各驱动还会提供服务端流式游标作为扩展。以 PyMySQL 为例pymysql.cursors.SSCursor以及返回字典的SSDictCursor是未缓冲游标按需从服务器拉行客户端内存占用可控。局限也很明确MySQL 协议不回传总行数只能遍历到底才知道有多少行不能向后滚动并且在结果集读完之前这个连接不能执行其他语句——流式读和写要用两条连接。连接池与连接生命周期不要一请求一连接。建立连接要经历握手、认证、会话初始化成本远高于执行一条语句。生产上要用连接池复用场景现成的池mysql-connector-python自带MySQLConnectionPoolSQLAlchemy 的Engine自带连接池如QueuePool由它统一管理asyncioasyncmy / aiomysql各自的create_pool()同样是await获取连接PyMySQL本身不带池需要搭配 SQLAlchemy、DBUtils 之类的工具连接生命周期有两件事要盯。一是空闲连接会被服务端踢掉MySQL 有wait_timeout长时间不用的连接由服务端关闭PyMySQL 提供ping(reconnectTrue)在取用前探活但重连会丢掉未提交的事务所以只能在事务开始之前调用。二是必须设连接超时不设的话网络不通时会长时间阻塞表现为「程序卡住」而不是报错。其余生产事项权限最小化应用账号只授予业务真正需要的权限不要用root凭据外置密码走环境变量或受控配置文件字符集对齐MySQL 的utf8是「最多 3 字节」的历史遗留别名存不了 emoji 和部分生僻字连接和表都用utf8mb4避免SELECT *把 TEXT、BLOB 这类大字段一起拖回来WHERE上常过滤的列建索引并用EXPLAIN看实际执行计划而不是靠猜。常见坑点1. 把驱动和 ORM 当一回事❌ 装完pip install SQLAlchemy就create_engine(mysql://...)—— 引擎底下没有驱动起不来。✅ 先装一个 DBAPI 驱动再在 URL 里同时指明方言和驱动形如mysqlpymysql://...。2. 换驱动只换 import不改占位符❌ 从声明format的驱动换到声明qmark的驱动SQL 里的%s一个字没改 —— 报错或匹配不到数据。✅ 先读目标驱动的paramstyle再逐条替换 SQL 文本里的占位符。3. 用某个驱动的私有异常类兜住所有错误❌ 只写except ...OperationalError:就当成全部错误 —— 唯一键冲突那类错误走的是别的分支会被漏掉。✅ 按 PEP 249 的公共异常树分层捕获DatabaseError之下再分IntegrityError、ProgrammingError、OperationalError等语义各不相同。4. 以为close()等于保存❌ 一批写操作执行完直接conn.close()以为和commit()等价 —— 规范规定未提交就关闭会隐式回滚。✅ 写操作后显式commit()异常路径rollback()close()只负责释放连接。5. 把隔离级别的锅甩给驱动❌ 出现不可重复读就去换驱动或改autocommit—— 隔离级别在数据库会话层换驱动不改它。✅ 先确认当前会话的隔离级别再按业务需要调整驱动只负责把 SQL 发过去。6. 运行中读写Connection.autocommit属性❌conn.autocommit True之后就假设切换立即生效 —— 这是可选扩展且写属性已被 PEP 249 标为弃用。✅ 在建立连接时通过构造参数确定模式或用驱动提供的方法不要靠改属性。7. 用同步 DB-API 的思维写异步驱动❌ 在 asyncmy / aiomysql 里写conn.cursor().execute(sql)就直接取值 —— 拿到的是协程对象不是结果。✅ 连接、游标、执行、取结果一律await连接的获取与释放用async with管。总结环节关键结论驱动Python 不内置 MySQL 驱动同步驱动都遵循 DB-API 2.0异步驱动不在其内契约apilevel/threadsafety/paramstyle是模块级常量paramstyle决定占位符写法迁移核心 API 名字不变连接参数、占位符、私有异常码必须逐个核对事务规范要求自动提交初始关闭未提交就close()会隐式回滚隔离级别属数据库层结果集executemany返回值未定义大结果集用arraysizefetchmany()或驱动级流式游标生产连接池复用、显式超时、探活、最小权限、凭据外置、utf8mb4对齐MySQL 这条线的复杂度几乎都不在 API 名字上而在驱动之间的细微契约差异、事务与隔离级别的语义、连接的复用与生命周期这三处。记牢 PEP 249 的公共契约再对差异保持警惕选型和迁移就都不会乱。