Despu茅s de haber discutido los problemas de
aislamiento y haber hecho una digresi贸n sobre la
estructura de datos de bajo nivel , la 煤ltima vez exploramos las
versiones de fila y observamos c贸mo las diferentes operaciones cambiaron los campos de encabezado de tupla.
Ahora veremos c贸mo se obtienen instant谩neas de datos consistentes de las tuplas.
驴Qu茅 es una instant谩nea de datos?
Las p谩ginas de datos pueden contener f铆sicamente varias versiones de la misma fila. Pero cada transacci贸n debe ver solo una versi贸n (o ninguna) de cada fila, de modo que todas ellas constituyan una imagen coherente de los datos (en el sentido de ACID) a partir de un cierto punto en el tiempo.
El aislamiento en PosgreSQL se basa en instant谩neas: cada transacci贸n funciona con su propia instant谩nea de datos, que "contiene" datos que se confirmaron antes del momento en que se cre贸 la instant谩nea y no "contiene" datos que a煤n no se confirmaron en ese momento. Ya hemos
visto que, aunque el aislamiento resultante parece m谩s estricto de lo requerido por el est谩ndar, todav铆a tiene anomal铆as.
En el nivel de aislamiento de lectura confirmada, se crea una instant谩nea al comienzo de cada declaraci贸n de transacci贸n. Esta instant谩nea est谩 activa mientras se realiza la declaraci贸n. En la figura, el momento en que se cre贸 la instant谩nea (que, como recordamos, est谩 determinada por el ID de la transacci贸n) se muestra en azul.

En los niveles de Lectura repetible y Serializable, la instant谩nea se crea una vez, al comienzo de la primera declaraci贸n de transacci贸n. Dicha instant谩nea permanece activa hasta el final de la transacci贸n.

Visibilidad de tuplas en una instant谩nea
Reglas de visibilidad
Una instant谩nea ciertamente no es una copia f铆sica de todas las tuplas necesarias. Una instant谩nea est谩 realmente especificada por varios n煤meros, y la visibilidad de las tuplas en una instant谩nea est谩 determinada por las reglas.
Si una tupla ser谩 visible o no en una instant谩nea depende de dos campos en el encabezado, a saber,
xmin
y
xmax
, es decir, los ID de las transacciones que crearon y eliminaron la tupla. Los intervalos como este no se superponen y, por lo tanto, no m谩s de una versi贸n representa una fila en cada instant谩nea.
Las reglas de visibilidad exactas son bastante complicadas y tienen en cuenta muchos casos diferentes y extremos.
Puede asegurarse f谩cilmente de eso mirando src / backend / utils / time / tqual.c (en la versi贸n 12, la verificaci贸n se movi贸 a src / backend / access / heap / heapam_visibility.c).
Para simplificar, podemos decir que una tupla es visible cuando en la instant谩nea, los cambios realizados por la transacci贸n
xmin
son visibles, mientras que los realizados por la transacci贸n
xmax
no lo son (en otras palabras, ya est谩 claro que la tupla fue creada, pero a煤n no est谩 claro si se elimin贸).
Con respecto a una transacci贸n, sus cambios son visibles en la instant谩nea, ya sea si esa misma transacci贸n cre贸 la instant谩nea (s铆 ve sus propios cambios a煤n no confirmados) o si la transacci贸n se confirm贸 antes de que se creara la instant谩nea.
Podemos representar gr谩ficamente las transacciones por segmentos (desde la hora de inicio hasta la hora de confirmaci贸n):

Aqu铆:
- Los cambios de la transacci贸n 2 ser谩n visibles ya que se complet贸 antes de que se creara la instant谩nea.
- Los cambios de la transacci贸n 1 no ser谩n visibles ya que estaba activa en el momento en que se cre贸 la instant谩nea.
- Los cambios de la transacci贸n 3 no ser谩n visibles desde que comenz贸 despu茅s de que se cre贸 la instant谩nea (independientemente de si se complet贸 o no).
Desafortunadamente, el sistema desconoce el tiempo de confirmaci贸n de las transacciones. Solo se conoce su hora de inicio (que est谩 determinada por el ID de la transacci贸n y marcada con una l铆nea discontinua en las figuras anteriores), pero el evento de finalizaci贸n no se escribe en ninguna parte.
Todo lo que podemos hacer es averiguar el estado
actual de las transacciones en la creaci贸n de la instant谩nea. Esta informaci贸n est谩 disponible en la memoria compartida del servidor, en la estructura ProcArray, que contiene la lista de todas las sesiones activas y sus transacciones.
Sin embargo, no podremos determinar si existe una transacci贸n activa en el momento en que se cre贸 la instant谩nea. Por lo tanto, una instant谩nea tiene que almacenar una lista de todas las transacciones activas actuales.
De lo anterior se deduce que en PostgreSQL, no es posible crear una instant谩nea que muestre datos consistentes a partir de cierto tiempo hacia atr谩s,
incluso si todas las tuplas necesarias est谩n disponibles en las p谩ginas de la tabla. A menudo surge una pregunta de por qu茅 PostgreSQL carece de consultas retrospectivas (o temporales; o flashback, como las llama Oracle), y esta es una de las razones.
Algo gracioso es que esta funcionalidad estuvo disponible por primera vez, pero luego se elimin贸 del DBMS. Puedes leer sobre esto en el art铆culo de Joseph M. Hellerstein .
Entonces, la instant谩nea est谩 determinada por varios par谩metros:
- En el momento en que se cre贸 la instant谩nea, m谩s exactamente, el ID de la pr贸xima transacci贸n, a煤n no disponible en el sistema (
snapshot.xmax
). - La lista de transacciones activas (en progreso) en el momento en que se cre贸 la
snapshot.xip
( snapshot.xip
).
Por conveniencia y optimizaci贸n, el ID de la primera transacci贸n activa tambi茅n se almacena (
snapshot.xmin
). Este valor tiene un sentido importante, que se discutir谩 a continuaci贸n.
Sin embargo, la instant谩nea tambi茅n almacena algunos par谩metros m谩s, que no son importantes para nosotros.

Ejemplo
Para comprender c贸mo la instant谩nea determina la visibilidad, reproduzcamos el ejemplo anterior con tres transacciones. La tabla tendr谩 tres filas, donde:
- El primero fue agregado por una transacci贸n que comenz贸 antes de la creaci贸n de la instant谩nea pero que se complet贸 despu茅s.
- El segundo fue agregado por una transacci贸n que comenz贸 y se complet贸 antes de la creaci贸n de la instant谩nea.
- El tercero se agreg贸 despu茅s de la creaci贸n de la instant谩nea.
=> TRUNCATE TABLE accounts;
La primera transacci贸n (a煤n no completada):
=> BEGIN; => INSERT INTO accounts VALUES (1, '1001', 'alice', 1000.00); => SELECT txid_current();
=> SELECT txid_current(); txid_current -------------- 3695 (1 row)
La segunda transacci贸n (completada antes de que se creara la instant谩nea):
| => BEGIN; | => INSERT INTO accounts VALUES (2, '2001', 'bob', 100.00); | => SELECT txid_current();
| txid_current | -------------- | 3696 | (1 row)
| => COMMIT;
Crear una instant谩nea en una transacci贸n en otra sesi贸n.
|| => BEGIN ISOLATION LEVEL REPEATABLE READ; || => SELECT xmin, xmax, * FROM accounts;
|| xmin | xmax | id | number | client | amount || ------+------+----+--------+--------+-------- || 3696 | 0 | 2 | 2001 | bob | 100.00 || (1 row)
Confirmar la primera transacci贸n despu茅s de que se cre贸 la instant谩nea:
=> COMMIT;
Y la tercera transacci贸n (apareci贸 despu茅s de que se cre贸 la instant谩nea):
| => BEGIN; | => INSERT INTO accounts VALUES (3, '2002', 'bob', 900.00); | => SELECT txid_current();
| txid_current | -------------- | 3697 | (1 row)
| => COMMIT;
Evidentemente, solo una fila sigue siendo visible en nuestra instant谩nea:
|| => SELECT xmin, xmax, * FROM accounts;
|| xmin | xmax | id | number | client | amount || ------+------+----+--------+--------+-------- || 3696 | 0 | 2 | 2001 | bob | 100.00 || (1 row)
La pregunta es c贸mo Postgres entiende esto.
Todo est谩 determinado por la instant谩nea. Ve谩moslo:
|| => SELECT txid_current_snapshot();
|| txid_current_snapshot || ----------------------- || 3695:3697:3695 || (1 row)
Aqu铆 se enumeran
snapshot.xmin
,
snapshot.xmax
y
snapshot.xip
, delimitados por dos puntos (
snapshot.xip
es un n煤mero en este caso, pero en general es una lista).
De acuerdo con las reglas anteriores, en la instant谩nea, esos cambios deben ser visibles que fueron realizados por transacciones con ID
xid
modo que
snapshot.xmin <= xid < snapshot.xmax
excepto aquellos que est谩n en la lista
snapshot.xip
. Veamos todas las filas de la tabla (en la nueva instant谩nea):
=> SELECT xmin, xmax, * FROM accounts ORDER BY id;
xmin | xmax | id | number | client | amount ------+------+----+--------+--------+--------- 3695 | 0 | 1 | 1001 | alice | 1000.00 3696 | 0 | 2 | 2001 | bob | 100.00 3697 | 0 | 3 | 2002 | bob | 900.00 (3 rows)
La primera fila no es visible: fue creada por una transacci贸n que est谩 en la lista de transacciones activas (
xip
).
La segunda fila es visible: fue creada por una transacci贸n que est谩 en el rango de instant谩neas.
La tercera fila no es visible: fue creada por una transacci贸n que est谩 fuera del rango de la instant谩nea.
|| => COMMIT;
Cambios propios de la transacci贸n.
Determinar la visibilidad de los propios cambios de la transacci贸n complica un poco la situaci贸n. En este caso, puede ser necesario ver solo una parte de dichos cambios. Por ejemplo: en cualquier nivel de aislamiento, un cursor abierto en un momento determinado no debe ver los cambios realizados m谩s tarde.
Para este fin, un encabezado de tupla tiene un campo especial (representado en las pseudocolumnas
cmin
y
cmax
), que muestra el n煤mero de orden dentro de la transacci贸n.
cmin
es el n煤mero para la inserci贸n, y
cmax
- para la eliminaci贸n, pero para ahorrar espacio en el encabezado de la tupla, este es en realidad un campo en lugar de dos diferentes. Se supone que una transacci贸n inserta y elimina con poca frecuencia la misma fila.
Pero si esto sucede, se inserta un ID de comando combinado especial (
combocid
) en el mismo campo, y el proceso de fondo recuerda los
cmin
y
cmax
reales para este
combocid
. Pero esto es completamente ex贸tico.
Aqu铆 hay un ejemplo simple. Comencemos una transacci贸n y agreguemos una fila a la tabla:
=> BEGIN; => SELECT txid_current();
txid_current -------------- 3698 (1 row)
INSERT INTO accounts(id, number, client, amount) VALUES (4, 3001, 'charlie', 100.00);
Vamos a mostrar el contenido de la tabla, junto con el campo
cmin
(pero solo para las filas agregadas por la transacci贸n; para otros no tiene sentido):
=> SELECT xmin, CASE WHEN xmin = 3698 THEN cmin END cmin, * FROM accounts;
xmin | cmin | id | number | client | amount ------+------+----+--------+---------+--------- 3695 | | 1 | 1001 | alice | 1000.00 3696 | | 2 | 2001 | bob | 100.00 3697 | | 3 | 2002 | bob | 900.00 3698 | 0 | 4 | 3001 | charlie | 100.00 (4 rows)
Ahora abrimos un cursor para una consulta que devuelve el n煤mero de filas en la tabla.
=> DECLARE c CURSOR FOR SELECT count(*) FROM accounts;
Y despu茅s de eso agregamos otra fila:
=> INSERT INTO accounts(id, number, client, amount) VALUES (5, 3002, 'charlie', 200.00);
La consulta devuelve 4: la fila agregada despu茅s de abrir el cursor no entra en la instant谩nea de datos:
=> FETCH c;
count ------- 4 (1 row)
Por qu茅 Porque la instant谩nea solo tiene en cuenta las tuplas con
cmin < 1
.
=> SELECT xmin, CASE WHEN xmin = 3698 THEN cmin END cmin, * FROM accounts;
xmin | cmin | id | number | client | amount ------+------+----+--------+---------+--------- 3695 | | 1 | 1001 | alice | 1000.00 3696 | | 2 | 2001 | bob | 100.00 3697 | | 3 | 2002 | bob | 900.00 3698 | 0 | 4 | 3001 | charlie | 100.00 3698 | 1 | 5 | 3002 | charlie | 200.00 (5 rows)
=> ROLLBACK;
Horizonte de eventos
El ID de la primera transacci贸n activa (
snapshot.xmin
) tiene un sentido importante: determina el "horizonte de eventos" de la transacci贸n. Es decir, m谩s all谩 de su horizonte, la transacci贸n siempre ve solo versiones de fila actualizadas.
Realmente, una versi贸n de fila desactualizada (inactiva) debe ser visible solo cuando la actualizada fue creada por una transacci贸n a煤n no completada y, por lo tanto, a煤n no es visible. Pero todas las transacciones "m谩s all谩 del horizonte" se completan con seguridad.

Puede ver el horizonte de transacciones en el cat谩logo del sistema:
=> BEGIN; => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
backend_xmin -------------- 3699 (1 row)
Tambi茅n podemos definir el horizonte a nivel de la base de datos. Para hacer esto, necesitamos tomar todas las instant谩neas activas y encontrar las
xmin
m谩s
xmin
entre ellas. Y definir谩 el horizonte, m谩s all谩 del cual las tuplas muertas en la base de datos nunca ser谩n visibles para ninguna transacci贸n.
Tales tuplas se pueden aspirar , y esta es exactamente la raz贸n por la cual el concepto de horizonte es tan importante desde un punto de vista pr谩ctico.
Si una determinada transacci贸n retiene una instant谩nea durante mucho tiempo, tambi茅n mantendr谩 el horizonte de la base de datos. Adem谩s, solo la existencia de una transacci贸n incompleta mantendr谩 el horizonte incluso si la transacci贸n en s铆 no contiene la instant谩nea.
Y esto significa que las tuplas muertas en el DB no se pueden aspirar. Adem谩s, es posible que una transacci贸n de "larga duraci贸n" no se cruce con los datos con otras transacciones, pero esto realmente no importa ya que todos comparten un horizonte de base de datos.
Si ahora hacemos que un segmento represente instant谩neas (desde
snapshot.xmin
a
snapshot.xmax
) en lugar de transacciones, podemos visualizar la situaci贸n de la siguiente manera:

En esta figura, la instant谩nea m谩s baja corresponde a una transacci贸n incompleta, y en las otras instant谩neas,
snapshot.xmin
no puede ser mayor que el ID de la transacci贸n.
En nuestro ejemplo, la transacci贸n se inici贸 con el nivel de aislamiento de lectura confirmada. Aunque no tiene ninguna instant谩nea de datos activa, sigue manteniendo el horizonte:
| => BEGIN; | => UPDATE accounts SET amount = amount + 1.00; | => COMMIT;
=> SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
backend_xmin -------------- 3699 (1 row)
Y solo despu茅s de la finalizaci贸n de la transacci贸n, el horizonte avanza, lo que permite aspirar las tuplas muertas:
=> COMMIT; => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
backend_xmin -------------- 3700 (1 row)
En el caso de que la situaci贸n descrita realmente cause problemas y no haya forma de solucionarlo a nivel de aplicaci贸n, hay dos par谩metros disponibles a partir de la versi贸n 9.6:
old_snapshot_threshold
determina la vida 煤til m谩xima de la instant谩nea. Cuando transcurra este tiempo, el servidor ser谩 elegible para aspirar tuplas muertas, y si una transacci贸n de "larga duraci贸n" a煤n las necesita, obtendr谩 un error "instant谩nea demasiado antigua".idle_in_transaction_session_timeout
determina la vida 煤til m谩xima de una transacci贸n inactiva. Cuando transcurre este tiempo, la transacci贸n se cancela.
Exportaci贸n de instant谩neas
A veces surgen situaciones en las que se debe garantizar que varias transacciones concurrentes vean los mismos datos. Un ejemplo es una utilidad
pg_dump
, que puede funcionar en modo paralelo: todos los procesos de trabajo deben ver la base de datos en el mismo estado para que la copia de seguridad sea coherente.
Por supuesto, no podemos confiar en la creencia de que las transacciones ver谩n los mismos datos solo porque se iniciaron "simult谩neamente". Para este fin, la exportaci贸n e importaci贸n de una instant谩nea est谩n disponibles.
La funci贸n
pg_export_snapshot
devuelve el ID de la instant谩nea, que se puede pasar a otra transacci贸n (usando herramientas fuera del DBMS).
=> BEGIN ISOLATION LEVEL REPEATABLE READ; => SELECT count(*) FROM accounts;
count ------- 3 (1 row)
=> SELECT pg_export_snapshot();
pg_export_snapshot --------------------- 00000004-00000E7B-1 (1 row)
La otra transacci贸n puede importar la instant谩nea utilizando el comando SET TRANSACTION SNAPSHOT antes de realizar su primera consulta. El nivel de aislamiento de lectura repetible o serializable tambi茅n debe especificarse antes, ya que en el nivel de confirmaci贸n de lectura, las declaraciones utilizar谩n sus propias instant谩neas.
| => DELETE FROM accounts; | => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SET TRANSACTION SNAPSHOT '00000004-00000E7B-1';
La segunda transacci贸n ahora funcionar谩 con la instant谩nea de la primera y, por lo tanto, ver谩 tres filas (en lugar de cero):
| => SELECT count(*) FROM accounts;
| count | ------- | 3 | (1 row)
La duraci贸n de una instant谩nea exportada es la misma que la duraci贸n de la transacci贸n de exportaci贸n.
| => COMMIT; => COMMIT;
Sigue leyendo .