Python操作MySQL与SQLite:从零到壹的底层数据库实操指南

Python MySQL SQLite
于 2026-09-28 06:52:59 修改
· 本内容遵循CC 4.0 BY-SA版权协议

1. 为什么我把MySQL和Sqlite放在一起讲

接触Python有一段时间的朋友,迟早会撞上"数据库"这道坎。爬虫爬到一定量级需要存数据,Web项目需要用户表和会话记录,量化策略需要跑历史行情——这些场景绕不开数据库。很多入门教程一上来就丢给你一堆ORM(对象关系映射)框架,比如SQLAlchemy、Django ORM,把SQL语句藏在底层,看似方便,但一旦遇到性能瓶颈或者复杂查询,你就抓瞎了。反过来,直接从最基础的数据库操作入手,用Python原生的方式连接MySQL和Sqlite,把SQL语言本身搞明白,后面再用什么框架都只是套壳的事情。

这篇博文的核心思路就一句话:用最笨、最直接的方式,把Python操作MySQL和Sqlite的底层逻辑打通。我会从零开始,带你完成环境搭建、建库建表、增删改查、事务处理、常见坑位排查这整个流程。整个过程不需要安装任何重量级框架,最多用到 pymysql 和Python自带的 sqlite3 标准库。适合三种人:刚学完Python基础语法、想了解数据库怎么用的初学者;会用ORM但没搞懂SQL本质的"半吊子";以及需要快速在本地搭建测试数据库、不想折腾重型数据库服务的开发人员。

顺便说一句,标题里的"从零到壹"不是虚的。我把每一步需要执行什么命令、每个参数是什么意思、每个模块为什么存在都拆开讲清楚。MySQL和Sqlite两套系统同时对比着学,你会发现很多概念是相通的——学会一个,另一个基本就是换皮。

2. 数据库选型:什么时候用MySQL,什么时候用Sqlite

2.1 两个数据库的核心差异

先说明一个容易混淆的点:MySQL和Sqlite虽然都叫SQL数据库,但它们的工作方式完全不同。MySQL是典型的客户端-服务器架构,数据库引擎作为一个独立的服务进程跑在机器上,你的Python程序通过网络协议(默认3306端口)和它通信。Sqlite则是一个嵌入式数据库,它不是一个独立服务,而是一个库,直接在你的进程内读写文件——你的Python程序就是数据库引擎本体。

这个本质差异决定了几乎所有使用场景:

MySQL Sqlite

用生活化的类比来说,把MySQL想象成一家银行——它有柜台、有金库、有安保系统,你只需要拿着银行卡(账号密码)去窗口办业务,银行内部怎么运转你不用管。Sqlite则是你家里的保险箱——保险箱就摆在你卧室,钥匙在你手里,打开就能存取东西,但你想让远在外地的朋友也来存取物品,这就不太现实了。

2.2 初学者怎么选

我给初学者的建议分三步走:第一步,先在Sqlite上练手,把所有SQL语法、增删改查逻辑跑通,因为它零配置、无网络依赖、报错信息直白,最适合当练习场。第二步,当你的项目需要被多个程序访问、需要长期稳定运行、需要账号权限管理时,迁移到MySQL。第三步,如果你要做Web开发,直接用MySQL起步也没问题——毕竟生产环境十有八九是它。

从学习路径来看,先学Sqlite再学MySQL还有一个隐藏好处:你会更清楚地知道哪些是SQL标准语法(两个数据库通用),哪些是特定数据库的方言。比如MySQL有 AUTO_INCREMENT 自增关键字,Sqlite也有同样的 AUTOINCREMENT ,但细节用法不一样;MySQL有 REPLACE INTO 这种方言,Sqlite就没有。把这些差异搞清楚,以后换数据库心里才有底。

3. 环境准备:从零搭建MySQL和Sqlite开发环境

3.1 MySQL的安装与初始化

很多人卡在第一步——MySQL装不上。我倒不是要说教,而是想分享一个最省心的路径。Windows用户直接去MySQL官网下载MySQL Community Server安装包,选MSI Installer版本,安装时勾选"Developer Default"会附带装好MySQL Workbench图形工具。这里有个关键选择:安装过程中会要求设置root密码,一定要记住,后面Python连接全靠它。Linux用户则简单一些,Ubuntu/Debian系执行 sudo apt install mysql-server ,CentOS/RHEL系用 sudo yum install mysql-server ,装完执行 sudo systemctl start mysqld 启动服务。

装完之后做两件事:第一,用 mysql -u root -p 进入命令行客户端,执行 SELECT VERSION(); 确认安装版本;第二,创建一个专门给Python程序用的账号,而不是直接用root。在MySQL命令行里执行:

CREATE USER 'pyuser' @ 'localhost' IDENTIFIED BY 'your_password' ;
GRANT ALL PRIVILEGES ON * . * TO 'pyuser' @ 'localhost' ;
FLUSH PRIVILEGES;

生产环境肯定不该用 ALL PRIVILEGES ON *.* 这么宽松的权限,但本地学习阶段这样最省事,省得老是因为权限不足报错。

3.2 Python端依赖安装

Python操作MySQL需要单独的驱动库,主流选择是 pymysql ——它纯Python实现,不需要编译,支持Python 3全版本。一条命令装完:

pip install pymysql

如果你用的是 mysql-connector-python 也可以,那是MySQL官方出的驱动,但实测下来 pymysql 在易用性和错误提示方面更友好一些。至于Sqlite,完全不用装任何东西——Python标准库里的 sqlite3 模块直接就能用。不需要额外下载DB Browser for SQLite之类的工具,当然如果你喜欢图形化查看数据,后面我会提一嘴。

3.3 验证环境是否就绪

写个最简单的脚本验证环境。创建一个 test_db.py 文件,内容如下:

PYTHON
import pymysql
import sqlite3
# 测试MySQL连接
try :
conn = pymysql.connect(
host= 'localhost' ,
user= 'pyuser' ,
password= 'your_password' ,
database= 'mysql' , # 先连到系统自带的数据库
charset= 'utf8mb4'
print ( 'MySQL连接成功,版本信息:' )
cursor = conn.cursor()
cursor.execute( 'SELECT VERSION()' )
row = cursor.fetchone()
print ( f'MySQL版本: {row[ 0 ]} ' )
cursor.close()
conn.close()
except Exception as e:
print ( f'MySQL连接失败: {e} ' )
# 测试Sqlite连接
try :
sqlite_conn = sqlite3.connect( 'test.db' )
cursor = sqlite_conn.cursor()
cursor.execute( 'SELECT SQLITE_VERSION()' )
row = cursor.fetchone()
print ( f'Sqlite版本: {row[ 0 ]} ' )
cursor.close()
sqlite_conn.close()
print ( 'Sqlite连接成功,数据库文件已生成' )
except Exception as e:
print ( f'Sqlite连接失败: {e} ' )

这里有个细节值得注意: sqlite3.connect('test.db') 这个操作会在当前目录下生成一个 test.db 文件,里面是空的。Sqlite的连接就是"文件是否存在+我能不能打开"这么简单。如果传的是 :memory: 字符串,则会创建内存数据库,进程结束数据就没了——这个后面会细说。

4. 建库建表:第一个数据库和第一张表

4.1 MySQL端:先建库再建表

连接上MySQL之后,第一步通常是创建自己的业务数据库。MySQL默认有几个系统数据库( information_schema 、 mysql 、 performance_schema 等),不建议直接在那里建业务表。新建一个独立的库:

CREATE DATABASE IF NOT EXISTS py_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE py_demo;

字符集这块是头号大坑。如果漏了 CHARACTER SET utf8mb4 ,默认是 latin1 ,到时候存中文就是乱码。很多老教程还在用 utf8 ,但在MySQL 5.7以上版本,展示全量字符的 utf8mb4 才是唯一正确的选择——它兼容emoji表情,也兼容各种生僻字。

建表语句直接看,我用一个经典的学生信息表做例子:

CREATE TABLE IF NOT EXISTS students (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR ( 50 ) NOT NULL ,
age INT ,
gender ENUM( '男' , '女' ) DEFAULT '男' ,
email VARCHAR ( 100 ) UNIQUE ,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP

解释几个关键设计: id 字段用 INT AUTO_INCREMENT PRIMARY KEY ,让数据库自动生成自增主键,避免业务层去管"下一条id是多少"这种脏活; name 字段给 NOT NULL 约束,防止空数据混进表里; email 加 UNIQUE 唯一约束,重复邮箱直接写入报错; created_at 用 TIMESTAMP DEFAULT CURRENT_TIMESTAMP ,插入记录时自动打时间戳,不需要应用层传时间。这些约束和默认值的设计思路贯穿整个数据库开发——尽量让数据库替你完成数据校验,应用层少操心。

4.2 Sqlite端:文件即数据库

Sqlite建表和MySQL非常相似,关键差异在于数据类型。Sqlite对数据类型不敏感,叫"动态类型"——你可以在任何类型的列里存任何类型的值,它不会强校验。但为了可维护性和跨库兼容性,还是建议按标准类型声明。

PYTHON
import sqlite3
# 连接并创建表
conn = sqlite3.connect( 'py_demo.db' )
cursor = conn.cursor()
cursor.execute( '''
CREATE TABLE IF NOT EXISTS students (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER,
gender TEXT DEFAULT '男',
email TEXT UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
''' )
conn.commit() # Sqlite必须手动提交事务
cursor.close()
conn.close()

对比一下就能看出来,MySQL里的 INT AUTO_INCREMENT 在Sqlite里是 INTEGER PRIMARY KEY AUTOINCREMENT ;MySQL的 VARCHAR(50) 在Sqlite里用 TEXT 就可以; ENUM('男','女') 这种枚举类型Sqlite直接不支持,用 TEXT DEFAULT 凑合。记住一个口诀:先会MySQL的标准写法,再改造成Sqlite的简化写法,两个库通吃。

4.3 表的修改与删除

实际开发中建表通常不是一次到位的,经常需要加字段或者改约束。两个数据库都支持 ALTER TABLE ,语法基本一致:

-- MySQL/Sqlite通用
ALTER TABLE students ADD COLUMN address VARCHAR ( 200 );
ALTER TABLE students DROP COLUMN address; -- MySQL可以,Sqlite 3.35以上版本才支持

特别注意:Sqlite在3.35.0版本之前不支持 DROP COLUMN 。如果遇到老版本的嵌入式环境,删字段你得自己走"建新表-复制数据-删旧表-重命名"的老路,麻烦得很。所以在Sqlite里建表之前多想想清楚,字段能一次设计全就一次到位,省得后面做"外科手术"。

5. 核心操作实战:增删改查全流程

5.1 插入数据:单条与批量

搞懂了建表,接下来是CRUD(增删改查)操作。这是数据库用到最多的地方,也是Python与数据库交互最频繁的环节。

MySQL端的插入有两种姿势。第一种是应用层写死SQL:

PYTHON
import pymysql
conn = pymysql.connect(
host= 'localhost' ,
user= 'pyuser' ,
password= 'your_password' ,
database= 'py_demo' ,
charset= 'utf8mb4'
cursor = conn.cursor()
# 单条插入
sql = "INSERT INTO students (name, age, gender, email) VALUES (%s, %s, %s, %s)"
cursor.execute(sql, ( '张三' , 18 , '男' , 'zhangsan@example.com' ))
print ( f"影响行数: {cursor.rowcount} ,自增ID: {cursor.lastrowid} " )
# 多条批量插入
data = [
( '李四' , 19 , '男' , 'lisi@example.com' ),
( '王五' , 20 , '女' , 'wangwu@example.com' ),
( '赵六' , 18 , '女' , 'zhaoliu@example.com' ),
cursor.executemany(sql, data)
conn.commit() # 记住提交事务
print ( f"批量插入影响行数: {cursor.rowcount} " )
cursor.close()
conn.close()

重点看两个地方:第一,SQL语句里的占位符是 %s ,这个占位符是 pymysql 的规范,不是MySQL服务端的规范——服务端用的是 ? ,但 pymysql 帮我们做了转换。第二, conn.commit() 必须手动调用,否则数据不会真正写入数据库。 pymysql 默认是手动提交事务的,这一点踩坑率极高——很多人发现自己Insert了数据但查不到,十有八九是漏了这一行。

Sqlite端的插入几乎一个模子:

PYTHON
import sqlite3
conn = sqlite3.connect( 'py_demo.db' )
cursor = conn.cursor()
# 单条插入
sql = "INSERT INTO students (name, age, gender, email) VALUES (?, ?, ?, ?)"
cursor.execute(sql, ( '张三' , 18 , '男' , 'zhangsan@example.com' ))
print ( f"自增ID: {cursor.lastrowid} " )
# 批量插入
data = [
( '李四' , 19 , '男' , 'lisi@example.com' ),
( '王五' , 20 , '女' , 'wangwu@example.com' ),
cursor.executemany(sql, data)
conn.commit()
cursor.close()
conn.close()

注意到差异了吗?Sqlite的占位符是 ? ,这是SQLite原生语法。另外Sqlite连接对象有几个参数很值得用起来: sqlite3.connect('py_demo.db', timeout=10) 设置超时时间避免大事务锁死; sqlite3.connect('py_demo.db', isolation_level=None) 开启自动提交模式,省去每次手动commit的麻烦。

5.2 查询数据:基本查询与条件筛选

查询是所有操作里花样最多的。只说最核心的几种。

MySQL端查询:

PYTHON
import pymysql
conn = pymysql.connect(
host= 'localhost' , user= 'pyuser' ,
password= 'your_password' , database= 'py_demo' ,
charset= 'utf8mb4'
cursor = conn.cursor()
# 查询所有数据
cursor.execute( "SELECT * FROM students" )
rows = cursor.fetchall()
for row in rows:
print (row) # 每行是一个元组,顺序对应SELECT的列
# 条件查询 + 排序 + 限制条数
cursor.execute(
"SELECT name, age FROM students WHERE age >= %s ORDER BY age DESC LIMIT %s" ,
( 18 , 5 )
rows = cursor.fetchall()
for name, age in rows:
print ( f"姓名: {name} , 年龄: {age} " )
conn.close()

Sqlite端几乎一模一样,就是占位符换成 ? :

PYTHON
import sqlite3
conn = sqlite3.connect( 'py_demo.db' )
conn.row_factory = sqlite3.Row # 设置为行工厂模式
cursor = conn.cursor()
cursor.execute( "SELECT name, age FROM students WHERE age >= ? ORDER BY age DESC LIMIT ?" , ( 18 , 5 ))
rows = cursor.fetchall()
for row in rows:
# 使用行工厂后,可以通过列名访问
print ( f"姓名: {row[ 'name' ]} , 年龄: {row[ 'age' ]} " )
conn.close()

这里强烈建议你拿到Sqlite连接后马上设置 conn.row_factory = sqlite3.Row 。这个操作把返回结果从"只能按下标访问的元组"变成"既支持下标又支持列名访问的类字典对象"。代码可读性一下子高很多,不然过两个月回来看, row[0] 是什么鬼自己都忘了。

关于查询还有一个贯穿所有数据库的原则:只取你需要的字段。 SELECT * 在测试时很方便,但到了生产环境就是性能毒药——表字段多了以后,每次查询都把无关的大字段(比如长文本、二进制)扛回来,白白浪费内存和带宽。

5.3 更新和删除:记得加WHERE条件

更新和删除的SQL本身很简单,真正的坑在于粗心——没写 WHERE 条件,导致全表更新或全表清空。这条用血泪教训记住了:执行 UPDATE 和 DELETE 之前,先把SQL语句在脑海里过一遍:我到底想改哪几行?条件够不够精确?

PYTHON
# MySQL
sql = "UPDATE students SET age = %s WHERE name = %s"
cursor.execute(sql, ( 19 , '张三' ))
print ( f"更新行数: {cursor.rowcount} " )
sql = "DELETE FROM students WHERE name = %s"
cursor.execute(sql, ( '张三' ,))
print ( f"删除行数: {cursor.rowcount} " )
conn.commit()

Sqlite端只是把 %s 换成 ? ,其余逻辑完全一致。有一个细节: cursor.rowcount 这个属性,在MySQL里返回的是影响的行数,在Sqlite里Update操作如果更新的值没有变化(比如把年龄19改成19),它返回的行数可能是0。这个差异在做幂等操作时要注意。

5.4 参数化查询:为什么不能拼接字符串

这一点是安全底线,我单独拿出来说。永远不要用Python的字符串拼接来组装SQL:

PYTHON
# 极度危险,千万别这样写
name = "张三'; DROP TABLE students;--"
sql = "SELECT * FROM students WHERE name = '" + name + "'"
cursor.execute(sql)

这就是著名的SQL注入。用户输入的内容被当成了SQL代码执行,轻则查询错乱,重则整表删除。参数化查询的原理是把SQL代码和数据分开传送——先解析SQL语句结构,再绑定数据值,数据永远只是数据,不可能被当成指令执行。所以只要用 %s 或 ? 占位符传参,就天然免疫SQL注入。

我在教学过程中遇到不少初学者问:既然参数化这么好,为什么还有人用拼接?答案是"图省事"和"不了解"。侥幸心理在数据库安全面前一文不值,数据无价,老老实实参数化。

6. 进阶操作:事务、异常处理与连接管理

6.1 事务的概念与实操

事务是一条大河里的水位线——要么全部成功,要么全部回滚。经典的例子是转账:A扣100,B加100,两步必须同时成功;如果A扣了100但B加钱失败了,整个操作必须撤销,否则钱就凭空消失了。

MySQL端的事务操作:

PYTHON
import pymysql
conn = pymysql.connect(host= 'localhost' , user= 'pyuser' ,
password= 'your_password' , database= 'py_demo' ,
charset= 'utf8mb4' )
cursor = conn.cursor()
try :
# 开启事务
conn.begin()
cursor.execute( "UPDATE accounts SET balance = balance - 100 WHERE user_id = 1" )
cursor.execute( "UPDATE accounts SET balance = balance + 100 WHERE user_id = 2" )
# 提交事务
conn.commit()
print ( "转账成功" )
except Exception as e:
# 出错回滚
conn.rollback()
print ( f"转账失败,已回滚: {e} " )
finally :
cursor.close()
conn.close()

Sqlite的事务机制也一样:

PYTHON
import sqlite3
conn = sqlite3.connect( 'accounts.db' )
cursor = conn.cursor()
try :
conn.execute( "BEGIN" )
cursor.execute( "UPDATE accounts SET balance = balance - 100 WHERE user_id = 1" )
cursor.execute( "UPDATE accounts SET balance = balance + 100 WHERE user_id = 2" )
conn.commit()
except Exception as e:
conn.rollback()
print ( f"转账失败,已回滚: {e} " )
finally :
cursor.close()
conn.close()

关于事务有几点心得:第一,默认情况下 pymysql 是手动提交模式,任何写操作不commit就是不发出去;而 sqlite3 模块默认是自动开启事务的——每次执行写操作前自动 BEGIN ,但不自动 COMMIT ,所以你不commit的话,程序退出时数据就丢了。第二,不要在一个事务里放太多操作,事务持有锁的时间越长,并发冲突的概率越大。保持事务短小精悍,这是数据库领域的黄金法则。

6.2 连接管理:资源回收不可忽视

数据库连接是稀缺资源,用完不还就是一种资源泄漏。在Python里,最优雅的姿势是用 with 上下文管理器。MySQL的 pymysql.connect() 返回的连接对象恰好支持上下文管理器:

PYTHON
import pymysql
conn = pymysql.connect(host= 'localhost' , user= 'pyuser' ,
password= 'your_password' , database= 'py_demo' ,
charset= 'utf8mb4' )
with conn:
cursor = conn.cursor()
cursor.execute( "SELECT * FROM students" )
rows = cursor.fetchall()
for row in rows:
print (row)
# 退出with块时,连接自动关闭但cursor还在

Sqlite同样支持:

PYTHON
import sqlite3
with sqlite3.connect( 'py_demo.db' ) as conn:
cursor = conn.cursor()
cursor.execute( "SELECT * FROM students" )
rows = cursor.fetchall()
for row in rows:
print (row)

注意一个细节:Sqlite的 with conn 块退出时只会提交事务,不会自动关闭连接。真正确保关闭还得手动 conn.close() 。相比之下MySQL的上下文管理器做的比较彻底,退出即关闭。我常用的习惯是:连接对象手动管理,保证 close() 在 finally 里执行;游标对象用完即关。新手最容易忽略的坑就是这——连接没关,程序跑着跑着,MySQL报"Too many connections"错误,查了半天才发现问题是连接泄漏了。

6.3 异常处理与日志

数据库操作是最容易出错的环节,网络断了、语法错了、约束冲突了、超时了,什么奇怪的问题都可能碰到。合理处理异常是工程化代码的分水岭。拿MySQL举例:

PYTHON
import pymysql
import logging
logging.basicConfig(level=logging.INFO)
try :
conn = pymysql.connect(host= 'localhost' , user= 'pyuser' ,
password= 'your_password' , database= 'py_demo' ,
charset= 'utf8mb4' )
cursor = conn.cursor()
cursor.execute( "INSERT INTO students (name, age) VALUES (%s, %s)" , ( '小明' , 20 ))
conn.commit()
except pymysql.err.IntegrityError as e:
logging.warning( f"数据完整性冲突: {e} " )
conn.rollback()
except pymysql.err.OperationalError as e:
logging.error( f"数据库操作错误(连接/权限/超时): {e} " )
conn.rollback()
except Exception as e:
logging.error( f"未知错误: {e} " )
conn.rollback()
finally :
if 'cursor' in locals ():
cursor.close()
if 'conn' in locals ():
conn.close()

这里的关键是把异常分层处理: IntegrityError 代表唯一约束、外键约束等数据冲突,通常是业务层面可以预见的; OperationalError 代表数据库服务本身的问题,比如连不上、socket超时、权限不足;兜底的 Exception 捕捉所有遗漏情况。分层的意义在于:不同错误对应不同的处理策略——数据冲突可以让用户换个值重试,连接问题可能需要开启重连机制,未知错误要记录详细日志。

7. MySQL与Sqlite的核心差异踩坑实录

7.1 数据类型差异对照表

实操中,两种数据库的数据类型映射经常让人头大。我整理了一份最常用的对照表,照着用基本不会出错:

MySQL类型 Sqlite类型

在实际项目中,如果你需要把MySQL的数据导入Sqlite,重点检查三样东西:日期时间的格式化方式、大文本字段的存储内容、以及其他数据库方言特有的语法( ON DUPLICATE KEY UPDATE 等)。把这三样先处理好,迁移基本就顺了。

7.2 存储与性能差异的实操验证

用数据说话。写一个简单的压测脚本,向两个数据库分别插入一万条记录,看看消耗时间:

PYTHON
import pymysql
import sqlite3
import time
# 准备数据
data = [( f'user_ {i} ' , 20 + i % 30 ) for i in range ( 10000 )]
# MySQL插入测试
start = time.time()
conn = pymysql.connect(host= 'localhost' , user= 'pyuser' ,
password= 'your_password' , database= 'py_demo' ,
charset= 'utf8mb4' )
cursor = conn.cursor()
cursor.execute( "DROP TABLE IF EXISTS speed_test" )
cursor.execute( "CREATE TABLE speed_test (name VARCHAR(50), age INT)" )
cursor.executemany( "INSERT INTO speed_test VALUES (%s, %s)" , data)
conn.commit()
conn.close()
print ( f"MySQL插入10000条耗时: {time.time() - start: .3 f} 秒" )
# Sqlite插入测试
start = time.time()
conn = sqlite3.connect( 'speed_test.db' )
cursor = conn.cursor()
cursor.execute( "DROP TABLE IF EXISTS speed_test" )
cursor.execute( "CREATE TABLE speed_test (name TEXT, age INTEGER)" )
cursor.executemany( "INSERT INTO speed_test VALUES (?, ?)" , data)
conn.commit()
conn.close()
print ( f"Sqlite插入10000条耗时: {time.time() - start: .3 f} 秒" )

实测结果通常让人惊讶:Sqlite的本地插入速度往往比MySQL还快,因为省去了网络通信和服务器进程的调度开销。但这不代表Sqlite就能吊打MySQL——一旦并发查询量上来,MySQL的成熟服务架构优势就体现出来了。所以结论依然清晰:数据量小、单机使用,Sqlite又快又省事;生产环境多用户并发,MySQL才是正解。

7.3 隔离级别与锁的行为差异

数据库的"隔离级别"决定了并发事务之间的可见性。MySQL有完整的事务隔离机制:读未提交、读已提交、可重复读、串行化。默认是 REPEATABLE READ (可重复读),高并发下还有 MVCC (多版本并发控制)机制,读写不互相阻塞。

Sqlite则简单很多,它只有两种模式:读写。写入操作会锁住整个数据库文件,期间其他写入事务一律阻塞。这也是Sqlite并发写入弱的核心原因。在实际使用中,如果你的Sqlite程序偶尔报"database is locked"错误,解决方法就三个:加大连接超时时间 timeout 参数;把写操作合并,减少写入频率;实在不行升级到MySQL。

8. 连接池:让数据库访问更高效

8.1 为什么需要连接池

很多入门教程不会讲连接池,但生产环境离了它活不下去。每次创建连接都要走一遍TCP握手、认证、环境初始化,开销很大。如果你的程序要频繁执行数据库操作,每次现连现断,性能损耗相当可观。连接池的思路是:预先创建N个连接放在池子里,程序需要时就借一个,用完就还回去,省去重复建连的开销。

Python里连接池的轮子有不少。以 dbutils 库为例,一条命令安装:

pip install dbutils

8.2 MySQL连接池实操

PYTHON
from dbutils.pooled_db import PooledDB
import pymysql
# 创建连接池,最多保持10个连接
pool = PooledDB(
creator=pymysql,
maxconnections= 10 ,
mincached= 2 ,
maxcached= 5 ,
blocking= True ,
host= 'localhost' ,
user= 'pyuser' ,
password= 'your_password' ,
database= 'py_demo' ,
charset= 'utf8mb4'
# 从池中获取连接
conn = pool.connection()
cursor = conn.cursor()
cursor.execute( "SELECT COUNT(*) FROM students" )
count = cursor.fetchone()[ 0 ]
print ( f"学生总数: {count} " )
cursor.close()
conn.close() # 这里不是真正关闭,而是把连接归还给池

PooledDB 几个参数要理解到位: maxconnections=10 限制池中最大连接数,扛不住高并发时这里要调大; mincached=2 让池子启动时先建2个空闲连接,预热减少首次访问延迟; blocking=True 表示池子满了之后,新的连接请求排队等待而不是直接报错。最后 conn.close() 归还连接而不是销毁连接——这是连接池使用中最大的认知差。

Sqlite其实不太需要连接池,因为它是单文件嵌入式数据库,打开和关闭文件的开销比网络连接小太多。真遇到并发不够,优先考虑调整 timeout 参数和合并写操作。

9. 常见问题速查:这些坑我替你踩过了

9.1 MySQL相关的高频报错

最让人摸不着头脑的就是"MySQL server has gone away"。查询执行过程中连接断了,大概率是SQL语句返回数据量超过了 max_allowed_packet 上限。在MySQL命令行里执行 SHOW VARIABLES LIKE '%max_allowed_packet%'; 看看当前值,默认是64MB,如果不够就改配置文件:

[mysqld]
max_allowed_packet=128M

改完重启MySQL服务,问题基本就解决了。

9.2 Sqlite相关的典型报错

Sqlite最伤的是"database is locked"。我遇到过印象最深的一次:程序在后台线程写数据,主线程又同时读,结果频繁报lock。排查后发现是写事务没有及时提交,锁一直在手里攥着不放。所以用Sqlite的准则是:写操作要短平快,事务尽早提交,写完之后马上腾出锁。

9.3 Python端容易忽略的细节

连接字符集没设置。MySQL连接时漏了 charset='utf8mb4' ,中文写入数据库变成"????",查出来也是问号。所有带中文的MySQL连接,字符集参数必须显式指定。

fetchall() 与内存问题。大批量查询时直接 fetchall() 会把所有结果一次性拉到内存,数据量巨大时直接内存爆炸。改用游标逐行遍历:

PYTHON
cursor.execute( "SELECT * FROM big_table" )
while True :
row = cursor.fetchone()
if row is None :
break
# 处理单行数据

时间字段的处理。MySQL返回的 DATETIME 在 pymysql 里是 datetime.datetime 对象,边写入时直接放Python的 datetime 对象就行,不用手动转字符串。但Sqlite没有原生时间类型,约定俗成存ISO格式字符串 '2025-06-01 12:30:00' ,取出后用 datetime.fromisoformat() 解析。两边的处理方式不一样,跨库迁移时尤其要小心。

10. 结语:用一次实操给你划重点