三种模式:自动提交(每条语句一个事务)、隐式(语句替你开、你负责提交)、显式(你全包)。 实测 400 行:每条自动提交 4657 毫秒,包在一个事务里 26 毫秒——差 178 倍。 差的不是计算,是「等磁盘」:400 次提交 = 400 次刷盘确认,一个事务只等一次。
15.2 讲的是「你自己写 BEGIN」——那是最容易理解的一种。
这一节讲另外两种:数据库自动开的、和驱动替你开的。
教材 15.3 分三小节:隐式事务模式、自动提交事务、嵌套事务。
嵌套那节的核心是 @@TRANCOUNT 这个计数器,沙箱没有,用 Python 复刻。
这一节有个反直觉的结论要提前说:
「事务能提速」是错觉——它省的是刷盘次数,不是让数据库算得更快。
实测那个 178 倍就是这么来的。
一、自动提交:每条语句自己一个事务
SQL Server 的默认模式。你写一条 INSERT,它自己包一个事务、自己提交。
所以不写 BEGIN 的时候,每条语句都是原子的。
实测:自动提交模式下 insert 之后不做任何提交,另一个连接立刻就能看到 1 行。
因为那条语句自己已经提交了。
这也解释了一个常见疑惑:「我没写事务,为什么单条 UPDATE 要么全改要么不改?
」因为每条语句本来就在一个事务里。
Python 的 sqlite3 里对应的是 isolation_level=None。
默认值是空字符串,那是「隐式」模式——两者只差一个参数,行为完全不同,
这也是很多人第一次用 sqlite3 时「数据没进去」的原因。
二、隐式事务:语句替你开,你负责提交
默认参数下,INSERT 之后事务已经开了,但没提交:
| 谁在看 | 看到几行 |
|---|---|
| 自己(同一个连接) | 1 |
| 另一个连接 | 0 |
提交之后,另一个连接才看到 1 行。 这就是隐式事务:开事务不用你管,收尾得你来。
SQL Server 上对应 SET IMPLICIT_TRANSACTIONS ON。
打开之后,INSERT/UPDATE/DELETE/SELECT 都会自动开新事务,直到你 COMMIT 或 ROLLBACK。
这个开关最危险的地方是「忘了提交」:
程序看着在跑、日志也在打,数据一条没进去,别人改同一张表还会卡死——
因为在等一个永远不会来的提交。 排查时看 sys.dm_tran_active_transactions 里挂着的老事务。
三、三种模式差在哪:谁负责收尾
| 模式 | 开事务的 | 提交的 | 忘了会怎样 |
|---|---|---|---|
| 自动提交 | 数据库 | 数据库 | 不会忘 |
| 隐式 | 驱动/数据库 | 你 | 锁一直挂着,日志一直涨 |
| 显式 | 你 | 你 | 同上,而且更难看懂 |
这三行的共同点是最后一句:只要「提交的人是你」,就有忘掉的风险。
所以工程上的做法是「把事务包起来」——用框架的 with transaction: 或者 try/finally,
让「提交或回滚」变成一定会执行的那条路径。
一个可靠的写法长这样:
try:
conn.execute(u"BEGIN")
do_work(conn)
conn.execute(u"COMMIT")
except Exception:
conn.execute(u"ROLLBACK") # 出错一定撤,绝不留下半截事务
raise
四、一次提交 vs 逐条提交:那 178 倍是怎么来的
插 400 行(文件库,synchronous = FULL) |
最快一次 |
|---|---|
| 每条自动提交 | 4657.1 ms |
| 包在一个事务里 | 26.1 ms |
差 178 倍。 这个数不是「事务有魔法」,而是「等磁盘」的次数不同:
synchronous = FULL 时每次提交都要等数据真正落到盘上——
400 次提交就是 400 次等盘,包成一个事务只等一次。
算一下账就明白了: 4657 ÷ 400 ≈ 每次提交 11.6 毫秒,
这 11.6 毫秒几乎全是磁盘的响应时间,不是数据库在算什么。
26 毫秒是「一次性写完 400 行 + 一次刷盘」的价钱。
所以要记住的结论是:事务省的是刷盘次数,不是计算量。
这也解释了为什么「批量插入要包事务」是铁律——跟数据库无关,是磁盘的物理特性决定的。
但别过头:事务不是越大越好。 事务开着的时候锁着、日志在涨、
回滚代价随规模线性上升(回滚一个 100 万行的事务能跑几分钟)。
常见做法是「几千行一批」,分批提交。
五、嵌套事务:@@TRANCOUNT 只是个计数器
教材讲嵌套,核心就一句:内层的 COMMIT 只是把计数减一,不真提交。
BEGIN TRAN -- @@TRANCOUNT = 1
BEGIN TRAN -- @@TRANCOUNT = 2
COMMIT -- @@TRANCOUNT = 1,数据还没落盘
COMMIT -- @@TRANCOUNT = 0,这时才真提交
GO
用 Python 复刻这个计数(SQLite 不允许嵌套 BEGIN,所以内层用保存点代替):
| 时刻 | 复刻的计数 |
|---|---|
BEGIN 之后 |
2 |
| 内层「提交」之后 | 1 |
外层 COMMIT 之后 |
0 |
只有最外层那次 COMMIT 真正生效。 所以有一个反直觉的结论:
「内层提交了、外层回滚了」的结果是——全没了。
这也是嵌套事务最坑的地方: 内层代码看到自己的 COMMIT 成功了,
以为数据已经落地,其实还在等外层。
所以存储过程里最好不要自己开事务——让调用方决定边界,
否则「过程里提交了、外面回滚了」这种事根本说不清。
六、动手 Demo:自动提交、隐式提交、178 倍,以及那个计数器
这一段先用两个连接对比「自动提交」和「隐式提交」的可见性差别,
再真测一次「400 行逐条提交 vs 包成一个事务」,最后复刻 @@TRANCOUNT 的计数。
注意 ④ 那段用的是文件库 + synchronous = FULL——换成内存库,这个 178 倍会直接消失,
因为内存库根本没有「等磁盘」这回事。
import sqlite3
# ① 自动提交:isolation_level=None
c = sqlite3.connect(u"tx3.db", isolation_level=None)
c.execute(u"INSERT INTO t (v) VALUES (1)")
# 另一个连接立刻能看到 1 行(这条语句自己提交了)
# ② 隐式:默认 isolation_level="",驱动替你 BEGIN
c2 = sqlite3.connect(u"tx3.db")
c2.execute(u"INSERT INTO t (v) VALUES (2)")
print(u"自己", count(c2), u"别人", count(other)) # 1 / 0
c2.commit()
print(u"提交后别人", count(other)) # 1
# ④ 逐条提交 vs 一个事务
def one_by_one():
cc = sqlite3.connect(u"tx3.db", isolation_level=None)
cc.execute(u"PRAGMA synchronous = FULL")
for i in range(400):
cc.execute(u"INSERT INTO big (v) VALUES (?)", (i * 1.0,))
def in_one_tx():
cc = sqlite3.connect(u"tx3.db", isolation_level=None)
cc.execute(u"PRAGMA synchronous = FULL")
cc.execute(u"BEGIN")
for i in range(400):
cc.execute(u"INSERT INTO big (v) VALUES (?)", (i * 1.0,))
cc.execute(u"COMMIT")
print(u"逐条 %.1f ms,一个事务 %.1f ms" % (clock(one_by_one), clock(in_one_tx)))
实跑输出:
--------------------------------------------------------------------
隐式事务、自动提交、嵌套
--------------------------------------------------------------------
① 自动提交:每条语句自己一个事务
--------------------------------------------------------------------
自动提交模式下,insert 之后没有 commit:
另一个连接看到 1 行
已经能看到了。 因为 isolation_level=None 就是「每条语句自动提交」——
这也正是 SQL Server 的默认模式:你不管事务,每条语句自己是一个事务。
② 隐式事务:语句替你开,你得自己提交
--------------------------------------------------------------------
隐式模式下,insert 之后还没 commit:
自己看到 1 行,另一个连接看到 0 行
提交之后,另一个连接看到 1 行
这就是「隐式」的意思:你不用写 BEGIN,驱动替你开了,但 COMMIT 还得你自己来。
SQL Server 对应的是 SET IMPLICIT_TRANSACTIONS ON——打开之后每条语句都开新事务,
直到你 COMMIT 或 ROLLBACK。忘了提交,锁就一直挂着。
③ 两种模式的差在哪:谁负责收尾
--------------------------------------------------------------------
| 模式 | 开事务的 | 提交的 | 忘了会怎样 |
|---|---|---|---|
| 自动提交 | 数据库 | 数据库 | 不会忘 |
| 隐式 | 驱动/数据库 | 你 | 锁一直挂着,日志一直涨 |
| 显式 | 你 | 你 | 同上,而且更难看懂 |
「忘了提交」是新手最典型的线上事故。 程序看着在跑、数据一条没进去,
别人改同一张表就卡死——因为在等一个永远不会来的提交。
④ 一次提交 vs 逐条提交:差多少
--------------------------------------------------------------------
| 插 400 行(文件库,synchronous = FULL) | 最快一次 |
|---|---:|
| 每条自动提交 | 4656.6 ms |
| 包在一个事务里 | 26.6 ms |
差 175 倍。 快的那个不是「事务有魔法」,而是它只等了一次磁盘确认——
synchronous = FULL 时,每次提交都要等数据真正落到盘上;400 次提交就是 400 次等磁盘,
包成一个事务就只等一次。 这就是「事务越大越快」的真实原因:省的是刷盘次数,不是计算。
反过来说也要当心: 事务开太大,锁住的时间长、回滚代价大、日志暴涨——不是越大越好。
⑤ 嵌套事务:SQL Server 的 @@TRANCOUNT 是什么
--------------------------------------------------------------------
教材讲嵌套事务,核心就一句:内层的 COMMIT 只是把计数减一,不真提交。
1> BEGIN TRAN -- @@TRANCOUNT = 1
1> BEGIN TRAN -- @@TRANCOUNT = 2
1> COMMIT -- @@TRANCOUNT = 1,数据还没落盘
1> COMMIT -- @@TRANCOUNT = 0,这时才真提交
用 Python 复刻这个计数(BEGIN 只能有一次,所以用保存点代替内层):
BEGIN 之后 @@TRANCOUNT 复刻值 = 2
内层提交之后 = 1(数据还没落盘)
外层提交之后 = 0(这时才真落盘)
只有最外层那次 COMMIT 真正生效。 内层的 COMMIT 只是计数减一——
所以「内层提交了,外层回滚了」的结果是:全没了。 这一点跟直觉相反,很值得记。
①② 两个数(1 / 0)是这一节的分水岭:
同一个数据库、同一条 INSERT,只因为 isolation_level 不同,另一个连接的可见性就不一样。
这不是 SQLite 的怪癖——它就是「自动提交」和「隐式事务」的定义差别。
④ 那 4657 毫秒听着夸张,但拆开算一点也不夸张:
4657 ÷ 400 ≈ 11.6 毫秒一次,这就是一块普通磁盘「确认写入了」的响应时间。
400 次就是 400 次,跑不掉。
26 毫秒那边是把 400 行攒在内存里、最后一次性刷盘。
所以快的本质是「攒」,不是「事务」——事务只是提供了「攒」的边界。
⑤ 那个计数(2 → 1 → 0)看着简单,但结论很反直觉:
内层提交是假的。 如果外层最后回滚,内层「提交」的那些改动一条都留不下。
30 秒实验
- 把 ④ 的
synchronous = FULL改成OFF,重跑。 两个数的差距会大幅缩小—— 因为不等磁盘了,逐条提交也不贵了。这就反证了「时间花在等盘上」。 - 把 400 行改成 4000 行,重测。 逐条那个大概会涨到十倍以上,一个事务那个涨得很少—— 前者是线性的「次数」,后者是线性的「数据量」,两者不同。
- 想一想:一个程序每天要导入 50 万行 CSV,你会怎么切事务? 每 5000 行提交一次——既不把事务开得太大(回滚和锁的代价),也不至于 50 万次刷盘。 具体数字要实测,别照抄。
常见误区
- 「事务能让数据库算得更快」。 不能。 它省的是「等磁盘」的次数。实测 178 倍全在刷盘上,不在计算上。
- 「隐式事务等于不用管事务」。
恰恰相反:它把「提交」的责任全推给了你,忘了提交就是锁死加数据不落盘。
宁可显式写
BEGIN/COMMIT,也别开IMPLICIT_TRANSACTIONS。 - 「内层
COMMIT之后数据就安全了」。 没有。 它只是把@@TRANCOUNT减一,外层回滚时内层一起没。
自测三问(点开对答案)
1. 自动提交和隐式事务差在哪? —— **自动提交是「每条语句数据库自己开、自己提交」**(SQL Server 默认); **隐式事务是「语句自动开,但提交要你来」**。**区别就在最后那一下谁负责。** 2. 400 行逐条提交 4657 毫秒、一个事务 26 毫秒,这 178 倍差在哪? —— **差在刷盘次数。** 每提交一次等一次磁盘确认(约 11.6 毫秒), **400 次就是 400 次;包成一个事务只等一次。** 3. 内层 `COMMIT` 之后、外层 `ROLLBACK`,数据会怎样? —— **全没了。** 内层 `COMMIT` 只是把 `@@TRANCOUNT` 减一, **只有最外层那次 `COMMIT` 才真正落盘。**下一篇 15.4 讲隔离级别: 脏读、不可重复读、幻读各是什么样,以及为什么 SQLite 的快照隔离连幻读都挡住了——代价又是什么。
- ☐ (本篇完,下一篇 15.4 事务隔离级别)