Оно настоящее
Jan. 25th, 2025 01:01 pmИ слегка прифигел. Эта зараза решает задачи по программированию в духе «сделай то-то и то-то одним SQL-оператором»!
Ну ок, не всегда. Иногда выдает что-то с первого взгляда очень похожее на правду, только нерабочее. Но самый прикол в том, что она умеет думать: если подсказать, даже просто намекнуть, она может сообразить, где закралась ошибка. И то, что можно следить за ходом мысли — это прям очень впечатляет.
Оставлю себе на память показательный диалог. На синих плашках мои реплики, на серых — ее размышления и ответы.
В PostgreSQL в двух разных сеансах (обозначенных 1=> и 2=>) выполняются по очереди следующие команды:
1=> CREATE TABLE t(id integer);
1=> BEGIN ISOLATION LEVEL REPEATABLE READ;
2=> BEGIN ISOLATION LEVEL READ COMMITTED;
2=> INSERT INTO t VALUES (1);
2=> COMMIT;
1=> SELECT count(*) FROM t;
Какое число выведет последняя команда?
Хорошо, давайте разбираться. У меня есть два сеанса в PostgreSQL: первый (1=>) и второй (2=>). Они выполняют команды в определённой последовательности. Нужно определить, какое число выведет последняя команда SELECT count(*) FROM t; в сеансе 1.
Сначала посмотрим на последовательность действий:
1. В сеансе 1 создаётся таблица t с колонкой id типа integer.
2. Затем сеанс 1 начинает транзакцию с уровнем изоляции REPEATABLE READ.
3. Сеанс 2 начинает свою транзакцию с уровнем READ COMMITTED.
4. Сеанс 2 вставляет в таблицу t значение (1) и коммитит эту транзакцию.
5. После этого сеанс 1 выполняет SELECT count(*) FROM t; и нужно понять, что он увидит.
Начну с понимания уровней изоляции. Уровень REPEATABLE READ гарантирует, что в течение транзакции будут видны только те данные, которые были закоммичены до начала транзакции, а также изменения, сделанные самой транзакцией. То есть, если другие транзакции закоммитят изменения после начала текущей, то в REPEATABLE READ эти изменения не будут видны.
Сеанс 1 начал транзакцию в REPEATABLE READ. В этот момент создаётся снимок данных (snapshot), который фиксирует состояние базы данных на момент начала транзакции. Далее, сеанс 2 вставляет запись и коммитит её. Но поскольку сеанс 1 работает в REPEATABLE READ, он должен видеть данные на момент своего начала, то есть до того, как сеанс 2 сделал вставку. Однако есть нюанс: таблица t была создана в сеансе 1 до начала транзакции. То есть таблица уже существует, когда сеанс 1 начинает транзакцию.
Но когда сеанс 2 вставляет данные и коммитит, эти данные добавляются в таблицу t. Теперь вопрос: увидит ли сеанс 1 эти изменения после коммита сеанса 2?
Поскольку уровень изоляции REPEATABLE READ создаёт снимок данных в начале транзакции, все изменения, закоммиченные после этого момента, не будут видны в текущей транзакции. Но важно, что таблица t была создана до начала транзакции сеанса 1. Значит, в снимке сеанса 1 таблица t уже существует. Однако на момент начала транзакции сеанса 1 таблица t пустая, потому что сеанс 2 ещё не вставил данные. Вставка сеанса 2 происходит и коммитится после начала транзакции сеанса 1, значит, в снимке данных сеанса 1 этих изменений нет.
Таким образом, когда сеанс 1 выполняет SELECT count(*), он должен видеть только те данные, которые были закоммичены до его транзакции. Поскольку сеанс 2 вставил данные после начала транзакции сеанса 1, сеанс 1 не должен видеть эту вставку. Но тут нужно проверить, влияет ли то, что таблица создана в том же сеансе до транзакции. Но нет, это не влияет на уровень изоляции. Уровень изоляции определяет видимость данных, а не структуру таблиц. Структура таблиц уже зафиксирована, так как CREATE TABLE был выполнен до BEGIN.
Но после создания таблицы и до начала транзакции в сеансе 1, таблица пустая. Начав транзакцию в REPEATABLE READ, сеанс 1 зафиксирует, что таблица t существует и пуста. Сеанс 2 вставляет строку и коммитит. Однако сеанс 1, при выполнении SELECT, должен видеть только данные, которые были на момент начала его транзакции. То есть, в таблице t должно быть 0 строк. Но это только в случае, если таблица действительно была пустой на момент начала транзакции сеанса 1. Однако, если таблица была создана в сеансе 1, то при создании таблицы она, вероятно, была закоммичена, так как CREATE TABLE — это DDL-операция, которая неявно коммитит транзакцию, если не находится внутри блока транзакции. Но в данном случае сеанс 1 выполняет CREATE TABLE, а затем начинает транзакцию. То есть, CREATE TABLE выполняется вне транзакции и сразу коммитится. Потом начинается транзакция в REPEATABLE READ.
Но тогда, когда сеанс 1 начинает транзакцию, таблица t уже существует и пуста. Сеанс 2 вставляет строку и коммитит. Теперь сеанс 1 делает SELECT. Так как уровень изоляции REPEATABLE READ, он не видит изменения, сделанные после начала его транзакции. Поэтому count(*) вернёт 0.
Но есть ли тут какие-то особенности? Например, в PostgreSQL в REPEATABLE READ можно видеть изменения, сделанные в собственной транзакции, но не других. В данном случае, сеанс 1 не делал никаких изменений, только читает. Сеанс 2 сделал изменения и закоммитил. Но для сеанса 1 эти изменения произошли после начала его транзакции, поэтому они не видны.
Таким образом, ответ должен быть 0.
Сеанс 1 начинает транзакцию с уровнем изоляции
REPEATABLE READ
, который фиксирует снимок данных на момент начала транзакции. На этом этапе таблица t уже создана, но пуста. Сеанс 2 вставляет строку и коммитит изменения, но это происходит после начала транзакции сеанса 1.В PostgreSQL уровень
REPEATABLE READ
гарантирует, что транзакция видит только те данные, которые были закоммичены до её начала. Таким образом, сеанс 1 не увидит вставку, выполненную сеансом 2, так как она зафиксирована позже.Ответ: 0.
А все-таки, в какой именно момент в транзакции REPEATABLE READ создается снимок, который используется затем всеми командами этой транзакции?
Хорошо, пользователь задал уточняющий вопрос о том, в какой именно момент транзакции с уровнем изоляции REPEATABLE READ в PostgreSQL создается снимок данных. В предыдущем ответе я упомянул, что снимок фиксируется на момент начала транзакции, но, возможно, стоит углубиться в детали.
Сначала нужно вспомнить, как работают уровни изоляции в PostgreSQL. Уровень REPEATABLE READ гарантирует, что в течение всей транзакции видны только те данные, которые были зафиксированы до ее начала. Это отличается от READ COMMITTED, где каждый запрос видит последние зафиксированные изменения.
Важно точно определить момент создания снимка. Документация PostgreSQL говорит, что снимок для REPEATABLE READ создается при первом выполнении команды, читающей данные, а не сразу при BEGIN. Это ключевой момент. То есть если транзакция начинается с BEGIN, но первое обращение к данным происходит позже, снимок создается именно в момент первого чтения.
В исходном примере транзакция в сеансе 1 начинается с BEGIN ISOLATION LEVEL REPEATABLE READ, а затем выполняется SELECT. Значит, снимок создается при первом SELECT. Но в примере после BEGIN транзакции 1 до выполнения SELECT сеанс 2 успевает вставить данные и закоммитить. Поскольку снимок создается только при первом чтении, которое происходит после вставки и коммита сеанса 2, то SELECT в сеансе 1 должен увидеть данные после коммита сеанса 2. Но это противоречит предыдущему ответу, где я сказал, что результат будет 0. Возникло противоречие.
Нужно проверить это. Возможно, я ошибся в предыдущем ответе. В PostgreSQL для REPEATABLE READ снимок действительно создается при первом операторе SELECT, а не при BEGIN. Поэтому если в примере транзакция 1 начинается, затем транзакция 2 вставляет данные и коммитит, и только потом в транзакции 1 выполняется SELECT, то снимок создается в момент первого SELECT, который уже видит изменения от сеанса 2. Тогда результат должен быть 1, а не 0.
Но тогда возникает путаница с тем, как работают уровни изоляции. Возможно, я перепутал с SERIALIZABLE, где снимок создается при начале транзакции. Нужно свериться с документацией PostgreSQL.
Согласно документации, в REPEATABLE READ снимок создается при первом операторе чтения. Таким образом, в примере, если транзакция 1 начала REPEATABLE READ, но до выполнения SELECT в ней произошла вставка и коммит в другой транзакции, то SELECT в транзакции 1 увидит эти изменения, так как снимок создается в момент первого SELECT. Следовательно, результат должен быть 1. Это меняет предыдущий ответ. Но тогда я допустил ошибку в первом ответе.
Но возможно, есть нюансы. Например, если первая транзакция после BEGIN выполняет какие-то операции, влияющие на создание снимка. В данном примере транзакция 1 создала таблицу до BEGIN, затем начинает транзакцию с REPEATABLE READ, затем сеанс 2 вставляет данные и коммитит, и только потом транзакция 1 делает SELECT. Поскольку таблица уже существовала до начала транзакции 1, и снимок создается при первом SELECT, который происходит после коммита сеанса 2, то SELECT должен увидеть вставленные данные. Но это противоречит предыдущему выводу.
Значит, я ошибся в первом ответе, и правильный ответ — 1. Но нужно это проверить на практике или перечитать документацию.
Проверяю документацию PostgreSQL:
"In REPEATABLE READ mode, a snapshot is taken at the start of the first query or data-modification command (SELECT, INSERT, UPDATE, DELETE, or MERGE) within the transaction, and that same snapshot is used for all subsequent commands in the same transaction."
То есть снимок создается при первом запросе или команде модификации данных. В примере транзакция 1 после BEGIN не выполняла никаких команд до SELECT. Поэтому снимок создается в момент выполнения SELECT. В этот момент транзакция 2 уже закоммитила вставку, значит, SELECT должен увидеть строку с id=1. Следовательно, результат будет 1, а не 0.
Таким образом, предыдущий ответ был неверным. Нужно исправить его и объяснить, что снимок создается при первом запросе, а не при начале транзакции. Поэтому в данном случае SELECT в сеансе 1 увидит изменения, закоммиченные сеансом 2 до первого SELECT в транзакции REPEATABLE READ.
В PostgreSQL при уровне изоляции
REPEATABLE READ
снимок данных создается не в момент выполнения BEGIN
, а при первом операторе, обращающемся к данным (например, SELECT
, INSERT
, UPDATE
, DELETE
). Это ключевой нюанс, который влияет на видимость изменений из других транзакций.Итоговый ответ: 1.
Ну да, мысли путаные, но ведь доперла же! Сама!
Вопрос в том, как приспособить это чудо себе на благо. Кажется логичным переложить на него всякую ю®, а самому заняться чем-то возвышенным. Проблема в том, что пока это уровень стажера, очень трудолюбивого и усидчивого, но немного бестолкового и довольно безответственного. Много чего может, понимает с полуслова, вот только положиться на него нельзя — надо все перепроверять и в каждом слове сомневаться. Но если со стажером имеет смысл возиться, чтобы сделать из него человека™, то тут такой мотивации нет. И получается, что большинство задач все-таки проще сделать самому сразу хорошо, чем исправлять чужие косяки.
Но интере-е-есно, блин.