with的用法

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) 状态。这意味着当前连接可以读写数据库,其他连接只能读,不能写,直到当前连接执行 COMMITROLLBACK

      • 类比:在进入仓库准备大规模整理货物前,在仓库大门挂上"内部整理中,暂停入库,可查询库存"的牌子,防止别人在你整理一半时又扔东西进来搞乱。

    • 分批次插入(例如 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(),则会丢失。

三、代码优化建议(针对省会热点数据)

  1. 异常处理与事务原子性(防翻车,保完整):

    将整个循环和最后的提交操作包裹在 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 之前的状态,一条新数据都不会插入。这保证了操作的原子性
  2. 坚持参数化查询(防黑客):

    始终使用占位符 ?(或其他数据库驱动支持的占位符如 %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 语句中,可能导致数据库被篡改或删除。
  3. 启用 WAL (Write-Ahead Logging) 模式(提高并发读写性能):

    对于需要较高并发读写性能的数据库(尤其是写操作频繁时),可以考虑将日志模式从默认的 DELETE 或 TRUNCATE 改为 WAL。

    复制代码
    # 在连接数据库后,执行 PRAGMA 命令
    conn.execute("PRAGMA journal_mode = WAL;")
    • 作用 :WAL 模式下,写操作不再直接修改原始数据库文件,而是将更改追加到单独的 WAL 文件中。读操作可以直接读取数据库文件,不受写操作的长时间阻塞。这显著提高了读和写操作的并发性,"读不阻塞写,写不阻塞读"。但 WAL 模式会产生额外的 .wal.shm 文件,且在某些特定场景下(如网络文件系统)可能不适用或有性能问题。

四、总结

  • with 语句:提供自动、安全的数据库连接管理,确保连接无论如何都会被关闭,防止资源泄露。

  • 事务 (BEGIN, COMMIT, ROLLBACK) :保证数据操作的原子性 (要么全部成功,要么全部失败回滚),维护数据一致性。BEGIN IMMEDIATE 提供更强的写锁定。

  • 分批次处理与提交策略

    • 分批插入:针对大数据量,通过减小单次操作的数据量来降低内存和数据库负载。

    • 提交时机

      • 循环内提交:牺牲原子性换取部分持久性,适用于允许部分成功的场景,但性能稍差且有最后批次丢失风险。

      • 循环外提交(推荐) :保证整个批量操作的原子性,性能通常更好,配合 try...except...rollback 最为健壮。

  • 批量插入 (executemany):大幅提升多条相同结构数据插入的效率,是性能优化的关键手段。

  • 参数化查询 (?):防止 SQL 注入攻击的金标准,必须坚持使用。

  • WAL 模式:通过改变日志机制,提高数据库的并发读写性能,适用于高并发场景。

实际部署建议 :对于像省会城市这样可能访问频繁、数据量大的数据库文件(如 city_1301.db),建议:

  • 将其存放在性能较好的存储介质上,如 SSD (固态硬盘)

  • 使用监控工具(如 Prometheus + Grafana,或特定于 Python/SQLite 的监控库)来观察数据库的性能指标(如查询耗时、锁等待时间、磁盘 I/O 等),以便及时发现瓶颈并进行优化。

相关推荐
这个DBA有点耶21 分钟前
NULL不是空——数据库里最反直觉的设计,90%新人踩过的坑
数据库·mysql·代码规范
用户83562907805128 分钟前
Python 实现 PDF 文件加密与解密方法
后端·python
用户83562907805132 分钟前
使用 Python 冻结与拆分 Excel 窗格教程
后端·python
这个DBA有点耶2 小时前
AI写的SQL跑崩了生产库,这锅谁背?
数据库·人工智能·程序员
镜舟科技3 小时前
Databricks 再提 LTAP,AI 时代的数据底座为何重回大一统叙事?
数据库·架构·agent
Databend4 小时前
从湖仓升级为 Agent 时代的数据控制面,Snowflake 和 Databricks 有哪些布局
大数据·数据库·agent
ClouGence7 小时前
SQL Server CDC 能放到 Always On 备库读吗?一文讲透原理与实践
数据库·sql server
你好潘先生9 小时前
别再记命令了,用 yeero do 说句人话就能跑脚本,而且不烧 token
服务器·python·命令行
Agent_大师9 小时前
WebSocket 行情重连成功,K线缺口不会自动消失
python
荣码9 小时前
LLM结构化输出:让AI返回JSON而不是废话,我踩了4个坑
java·python