Таблица с одной строкой:
=> CREATE DATABASE mvcc_snapshots;
CREATE DATABASE
=> \c mvcc_snapshots
You are now connected to database "mvcc_snapshots" as user "postgres".
=> CREATE TABLE t(n integer);
CREATE TABLE
=> BEGIN;
BEGIN
=> INSERT INTO t(n) VALUES (1);
INSERT 0 1
=> SELECT txid_current();
txid_current -------------- 55468 (1 row)
=> COMMIT;
COMMIT
Первая транзакция видит строку:
=> BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN
=> SELECT * FROM t;
n --- 1 (1 row)
=> SELECT txid_current();
txid_current -------------- 55469 (1 row)
=> SELECT txid_current_snapshot();
txid_current_snapshot ----------------------- 55469:55469: (1 row)
Вторая транзакция в другом сеансе удаляет строку:
=> \c mvcc_snapshots
You are now connected to database "mvcc_snapshots" as user "postgres".
=> BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN
=> DELETE FROM t;
DELETE 1
=> SELECT * FROM t;
n --- (0 rows)
=> SELECT txid_current();
txid_current -------------- 55470 (1 row)
=> SELECT txid_current_snapshot();
txid_current_snapshot ----------------------- 55469:55469: (1 row)
Первая транзакция продолжает видеть строку:
=> SELECT xmin, xmax, * FROM t;
xmin | xmax | n -------+-------+--- 55468 | 55470 | 1 (1 row)
Снимки обоих транзакций одинаковы, и по правилам видимости они обе должны были бы видеть строку, так как
Однако вторая транзакция все-таки не видит строку, поскольку она сама ее удалила, а это - исключение в обычных правилах видимости.
=> COMMIT;
COMMIT
=> COMMIT;
COMMIT
Внутри функции, подсчитывающей число строк таблицы, вставим односекундную задержку для удобства тестирования:
=> CREATE FUNCTION test() RETURNS integer AS $$ SELECT pg_sleep(1); SELECT count(*)::integer FROM t; $$ VOLATILE LANGUAGE sql;
CREATE FUNCTION
Теперь начинаем транзакцию с уровнем изоляции Read Committed и в запросе несколько раз вызываем функцию, одновременно подсчитывая количество строк подзапросом.
=> BEGIN ISOLATION LEVEL READ COMMITTED;
BEGIN
=> SELECT (SELECT count(*) FROM t) AS cnt, test() FROM generate_series(1,5);
Параллельно выполняем другую транзакцию, которая увеличивает число строк в таблице. Если основной запрос и вложенный запрос используют разные снимки, мы обнаружим это по разнице в результате.
=> INSERT INTO t VALUES (1);
INSERT 0 1
cnt | test -----+------ 0 | 0 0 | 0 0 | 1 0 | 1 0 | 1 (5 rows)
=> END;
COMMIT
Значение первого столбца не изменяется - запрос использует снимок, созданный в начале выполнения. Однако значение во втором столбце изменяется - запросы внутри volatile-функции используют отдельные снимки.
Повторим эксперимент для уровня изоляции Repeatable Read:
=> TRUNCATE t;
TRUNCATE TABLE
=> BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN
=> SELECT (SELECT count(*) FROM t) AS cnt, test() FROM generate_series(1,5);
=> INSERT INTO t VALUES (1);
INSERT 0 1
cnt | test -----+------ 0 | 0 0 | 0 0 | 0 0 | 0 0 | 0 (5 rows)
=> END;
COMMIT
Теперь еще раз то же самое для функции с категорией изменчивости stable.
=> ALTER FUNCTION test STABLE;
ALTER FUNCTION
Уровень Read Committed:
=> TRUNCATE t;
TRUNCATE TABLE
=> BEGIN ISOLATION LEVEL READ COMMITTED;
BEGIN
=> SELECT (SELECT count(*) FROM t) AS cnt, test() FROM generate_series(1,5);
=> INSERT INTO t VALUES (1);
INSERT 0 1
cnt | test -----+------ 0 | 0 0 | 0 0 | 0 0 | 0 0 | 0 (5 rows)
=> END;
COMMIT
Уровень Repeatable Read:
=> TRUNCATE t;
TRUNCATE TABLE
=> BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN
=> SELECT (SELECT count(*) FROM t) AS cnt, test() FROM generate_series(1,5);
=> INSERT INTO t VALUES (1);
INSERT 0 1
cnt | test -----+------ 0 | 0 0 | 0 0 | 0 0 | 0 0 | 0 (5 rows)
=> END;
COMMIT
Выводы: