Python SQLite 操作详解
本文档详细解释了使用 Python 操作 SQLite 数据库时涉及的关键概念和代码实践,包括 with
语句、事务处理、批量插入以及相关的优化建议。
一、with
语句的作用(自动关门的保险库)
with sqlite3.connect('city_1301.db') as conn:
# 代码块
# 在这里执行数据库操作,例如创建 cursor、执行 SQL 等
pass
-
作用 :
with
语句就像一个"自动关门的保险库"。-
开门 :当程序执行进入
with
代码块时,它会自动执行sqlite3.connect('city_1301.db')
,建立数据库连接,并将连接对象赋值给conn
变量(类似打开保险库的门并拿到钥匙)。 -
关门 :无论
with
代码块内部的代码是正常执行完毕,还是中途发生了错误(异常),当程序流程离开with
代码块时,它都会自动确保数据库连接conn
被关闭(conn.close()
会被隐式调用)。
-
-
为什么用它 :核心目的是资源管理自动化 ,防止程序员忘记手动调用
conn.close()
来关闭数据库连接。忘记关闭连接可能会导致:-
资源泄露:连接一直占用系统资源。
-
数据丢失或损坏:如果程序异常退出而连接未正常关闭,可能导致缓冲区的数据未完全写入磁盘。
-
达到连接数上限:如果频繁创建连接而不关闭,可能耗尽数据库允许的最大连接数。
-
二、代码逐行解析(省会垃圾数据批量入库)
# 假设 conn 是已经通过 with 语句建立的数据库连接
# data 是包含大量待插入数据的列表,例如 [(val1_row1, val2_row1, ...), (val1_row2, ...), ...]
cursor = conn.cursor() # 1. 获取游标:拿一个"笔"准备写数据到数据库这个"本子"上。
cursor.execute("BEGIN IMMEDIATE") # 2. 开启立即事务:大喊:"现在我要开始连续写一堆数据了,在我喊停之前,其他人(其他连接)最好别写,可以读,但别干扰我!"
try: # (结合优化建议中的异常处理)
for i in range(0, len(data), 1000): # 3. 分批处理循环:数据太多(比如60万条),一次处理不完,每次处理1000条。
batch = data[i:i+1000] # 4. 获取批次数据:从总数据 data 中取出当前这一小批(1000条)数据,装进一个临时的"小箱子" batch。
# 5. 批量插入:使用 executemany 将 batch 这个"小箱子"里的所有数据一次性"传送"到数据库表中。
# "INSERT INTO bins VALUES (?, ?, ?)" 是 SQL 模板,? 是占位符,具体的值会从 batch 里的元组中按顺序填充。
cursor.executemany("INSERT INTO bins (column1, column2, column3) VALUES (?, ?, ?)", batch)
# 6. 阶段性提交:每成功插入1000条数据后,就执行一次 conn.commit()。
# 这就像是把刚刚装满的那个"小箱子"里的货,正式确认推进仓库存档。
conn.commit()
except Exception as e: # (结合优化建议中的异常处理)
conn.rollback() # 如果上面 try 块中任何一步出错,就执行回滚
print(f"数据库操作出错,事务已回滚: {e}")
# 注意:实际代码中,如果最后一批不足1000条,循环结束后也需要确保提交或有合适的处理。
# 更好的做法是将 commit 放在循环结束后,配合异常处理,实现原子性。见优化部分。
-
关键点解析:
-
BEGIN IMMEDIATE
:-
作用 :开启一个立即事务 (IMMEDIATE Transaction) 。一旦执行成功,数据库会被锁定为写锁定 (RESERVED lock) 状态。这意味着当前连接可以读写数据库,其他连接只能读,不能写,直到当前连接执行
COMMIT
或ROLLBACK
。 -
类比:在进入仓库准备大规模整理货物前,在仓库大门挂上"内部整理中,暂停入库,可查询库存"的牌子,防止别人在你整理一半时又扔东西进来搞乱。
-
-
分批次插入(例如 1000 条/次):
-
原因 :如果
data
列表非常大(如几十万、几百万条),一次性构建巨大的 SQL 语句或将所有数据加载到内存中传递给executemany
可能导致程序内存溢出或数据库处理超时、卡顿。分批可以显著降低单次操作的内存消耗和数据库负载。 -
类比:搬家时,用小推车分多次搬运包裹,而不是试图用一辆超载的大卡车一次性把所有东西硬塞进去,那样容易把车压垮或者堵在路上。
-
-
executemany
:-
作用 :这是 SQLite(以及其他许多数据库接口)提供的批量执行 同一条 SQL 语句的优化方法。它接收一个 SQL 模板和一个包含多组参数的序列(如列表的列表或列表的元组
batch
)。相比于在 Python 循环中反复调用execute
执行单条INSERT
语句,executemany
的效率通常高得多(可能快 10 倍甚至更多),因为它减少了 Python 与数据库驱动之间的通信开销。 -
示例中的
?
:这是参数化查询 的占位符。使用占位符是防止 SQL 注入攻击 的标准做法。数据库驱动会安全地将batch
中的值替换到?
的位置,而不是简单地将值拼接到 SQL 字符串中。
-
-
循环内提交 (
conn.commit()
):-
优势 :如果在插入大量数据的过程中程序意外崩溃(比如处理到第 3500 条时崩溃),那么前 3000 条(前 3 批)因为已经被
commit
,所以它们的数据会永久保存在数据库中 ,不会丢失。只丢失了当前正在处理但尚未提交的那一批(第 3001-4000 条)。这提供了部分持久性。 -
风险/缺点:
-
非原子性:整个"插入 60 万条数据"的操作不再是一个原子操作。它被分成了多个小的原子提交。如果业务要求这 60 万条数据必须要么全部成功插入,要么一条都不插入,那么这种方式就不合适。
-
性能开销 :每次
commit
都是一次相对昂贵的操作,因为它需要确保数据写入磁盘并释放锁。频繁提交会比在所有批次都插入完成后进行一次总提交要慢。 -
最后批次丢失风险 :如果总数据量不是批次大小的整数倍(例如 60500 条,批次 1000),最后一批 500 条在循环结束后如果没有显式调用
conn.commit()
,则会丢失。
-
-
-
三、代码优化建议(针对省会热点数据)
-
异常处理与事务原子性(防翻车,保完整):
将整个循环和最后的提交操作包裹在 try...except...finally 或 try...except 块中,并在 except 中执行 conn.rollback()。将 commit 移到循环外部,确保整个批量插入操作的原子性。
cursor = conn.cursor() try: cursor.execute("BEGIN IMMEDIATE") # 或者默认的 BEGIN DEFERRED 也可以 for i in range(0, len(data), 1000): batch = data[i:i+1000] cursor.executemany("INSERT INTO bins (column1, column2, column3) VALUES (?, ?, ?)", batch) conn.commit() # 所有批次成功插入后,在循环外进行一次总提交 print("所有数据批量插入成功并已提交。") except Exception as e: conn.rollback() # 如果循环中任何地方出错,回滚整个事务 print(f"批量插入过程中发生错误,事务已回滚: {e}") finally: # 可以在这里关闭 cursor,但如果使用 with conn:,连接会自动关闭 # if cursor: # cursor.close() pass
- 作用 :如果插入过程中任何一批数据失败(例如数据格式错误、违反约束等),整个事务会被回滚,数据库状态恢复到
BEGIN
之前的状态,一条新数据都不会插入。这保证了操作的原子性。
- 作用 :如果插入过程中任何一批数据失败(例如数据格式错误、违反约束等),整个事务会被回滚,数据库状态恢复到
-
坚持参数化查询(防黑客):
始终使用占位符 ?(或其他数据库驱动支持的占位符如 %s)配合 execute 或 executemany 的第二个参数来传递数据,绝对不要手动拼接 SQL 字符串。
# 正确写法 (已在上面示例中使用) cursor.executemany("INSERT INTO bins VALUES (?, ?, ?)", batch) # 错误且危险的写法 (容易被 SQL 注入) # for row in batch: # sql = f"INSERT INTO bins VALUES ('{row[0]}', '{row[1]}', '{row[2]}')" # 非常危险! # cursor.execute(sql)
- 避免 SQL 注入 :防止用户输入或外部数据中包含恶意的 SQL 代码片段(例如
'; DROP TABLE bins; --
),如果直接拼接到 SQL 语句中,可能导致数据库被篡改或删除。
- 避免 SQL 注入 :防止用户输入或外部数据中包含恶意的 SQL 代码片段(例如
-
启用 WAL (Write-Ahead Logging) 模式(提高并发读写性能):
对于需要较高并发读写性能的数据库(尤其是写操作频繁时),可以考虑将日志模式从默认的 DELETE 或 TRUNCATE 改为 WAL。
# 在连接数据库后,执行 PRAGMA 命令 conn.execute("PRAGMA journal_mode = WAL;")
- 作用 :WAL 模式下,写操作不再直接修改原始数据库文件,而是将更改追加到单独的 WAL 文件中。读操作可以直接读取数据库文件,不受写操作的长时间阻塞。这显著提高了读和写操作的并发性,"读不阻塞写,写不阻塞读"。但 WAL 模式会产生额外的
.wal
和.shm
文件,且在某些特定场景下(如网络文件系统)可能不适用或有性能问题。
- 作用 :WAL 模式下,写操作不再直接修改原始数据库文件,而是将更改追加到单独的 WAL 文件中。读操作可以直接读取数据库文件,不受写操作的长时间阻塞。这显著提高了读和写操作的并发性,"读不阻塞写,写不阻塞读"。但 WAL 模式会产生额外的
四、总结
-
with
语句:提供自动、安全的数据库连接管理,确保连接无论如何都会被关闭,防止资源泄露。 -
事务 (
BEGIN
,COMMIT
,ROLLBACK
) :保证数据操作的原子性 (要么全部成功,要么全部失败回滚),维护数据一致性。BEGIN IMMEDIATE
提供更强的写锁定。 -
分批次处理与提交策略:
-
分批插入:针对大数据量,通过减小单次操作的数据量来降低内存和数据库负载。
-
提交时机:
-
循环内提交:牺牲原子性换取部分持久性,适用于允许部分成功的场景,但性能稍差且有最后批次丢失风险。
-
循环外提交(推荐) :保证整个批量操作的原子性,性能通常更好,配合
try...except...rollback
最为健壮。
-
-
-
批量插入 (
executemany
):大幅提升多条相同结构数据插入的效率,是性能优化的关键手段。 -
参数化查询 (
?
):防止 SQL 注入攻击的金标准,必须坚持使用。 -
WAL 模式:通过改变日志机制,提高数据库的并发读写性能,适用于高并发场景。
实际部署建议 :对于像省会城市这样可能访问频繁、数据量大的数据库文件(如 city_1301.db
),建议:
-
将其存放在性能较好的存储介质上,如 SSD (固态硬盘)。
-
使用监控工具(如 Prometheus + Grafana,或特定于 Python/SQLite 的监控库)来观察数据库的性能指标(如查询耗时、锁等待时间、磁盘 I/O 等),以便及时发现瓶颈并进行优化。