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
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}
'
)
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()
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
,语法基本一致:
ALTER
TABLE
students
ADD
COLUMN
address
VARCHAR
(
200
);
ALTER
TABLE
students
DROP
COLUMN
address;
特别注意: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)
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
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)
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
)]
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}
秒"
)
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
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. 结语:用一次实操给你划重点