在 PostgreSQL 中,一个操作默认并不是原子的,因此在较弱的事务隔离级别下(如 PG 默认的读已提交),操作可能出现意外的结果。

简单的数据查询一般可以认为是原子的,例如:

SELECT amount, pg_sleep(2) -- sleep two seconds after each row is read
FROM accounts WHERE client = 'bob';

如果在查询过程中使用 UPDATE 语句修改 accounts.amount 的值,SELECT 语句返回的结果如同数据库一直处于其执行开始时的状态,不会受到 UPDATE 的影响。

但是当语句存在嵌套时情况会发生变化。如果查询中包含一个声明为 VOLATILE 的函数,而这个函数执行了另一个查询,例如:

CREATE FUNCTION get_amount(id integer) RETURNS numeric
	AS $$
	SELECT amount FROM accounts a WHERE a.id = get_amount.id;
	$$ VOLATILE LANGUAGE sql;
SELECT get_amount(id), pg_sleep(2)
	FROM accounts WHERE client = 'bob';

与此同时另一个事务开始修改 accounts.amount 的值:

BEGIN;
UPDATE accounts SET amount = amount + 100 WHERE id = 2;
UPDATE accounts SET amount = amount - 100 WHERE id = 3;
COMMIT;

那么这个嵌套查询将会实时地看到最新的数据,并且导致主查询的结果出现异常——结果既包含了更新前 id=2 的值,也包含了更新后的 id=3 的值。

PostgreSQL 默认就使用读已提交的隔离级别和 VOLATILE 的稳定性级别。

另一个例子是两个 UPDATE 试图同时修改同一行数据。 假设目前数据库的状态如下:

SELECT * FROM accounts WHERE client = 'bob';
 id | client | amount
−−−−+−−−−−−−−+−−−−−−−−
  2 | bob 	 | 200.00
  3 | bob 	 | 800.00
(2 rows)

第一个 UPDATE 语句试图为所有余额大于 1000 的账户增加 1% 的利息:

UPDATE accounts SET amount = amount * 1.01
WHERE client IN (
  SELECT client
  FROM accounts
  GROUP BY client
  HAVING sum(amount) >= 1000
);

当第一个 UPDATE 还在执行其嵌套的 SELECT 时,第二个 UPDATE 语句尝试减少账户余额:

BEGIN;
UPDATE accounts SET amount = amount - 100 WHERE id = 3;

由于第二个 UPDATE 语句仍未提交,因此嵌套的 SELECT 语句将不会看到第二个 UPDATE 语句的结果,并将 id=3 的余额视为 1000。 此后第二个 UPDATE 语句提交,id=3 的余额变为 700。 第一个 UPDATE 语句将会依次增加 id=2id=3 的余额 1%。

SELECT * FROM accounts WHERE client = 'bob';
 id | client | amount
−−−−+−−−−−−−−+−−−−−−−−
  2 | bob 	 | 202.00
  3 | bob 	 | 707.00
(2 rows)

尽管 UPDATE accounts SET amount = amount * 1.01 的过程仍然是原子的,但是执行结果却显示出不一致的状态——bob 的账户余额在不满足大于等于 1000 的情况下增加了 1% 的利息。

在可重复读下则不会出现这种异常现象,而是会在执行 UPDATE accounts SET amount = amount * 1.01 的过程中因为无法满足可重复读条件被数据库中止操作并返回错误。