Así que hemos abordado las cuestiones relacionadas con , y hemos hecho una digresión sobre . Y finalmente llegamos a lo más interesante: las versiones de filas.
Encabezado
Como ya hemos mencionado, cada fila puede existir simultáneamente en múltiples versiones en la base de datos. Es necesario distinguir una versión de otra. Con este fin, cada versión tiene dos marcas que definen el 'tiempo' de validez de esta versión (xmin y xmax). En comillas, porque no se utiliza el tiempo como tal, sino un contador especial que aumenta. Y este contador es el número de la transacción.
(Como es habitual, en realidad es más complicado: el número de transacciones no puede aumentar constantemente debido a la limitación del contador. Pero abordaremos estos detalles completados cuando lleguemos a la congelación).
Cuando se crea una fila, el valor xmin se establece en el número de la transacción que ejecutó el comando INSERT, y xmax no se completa.
Cuando se elimina una fila, el valor xmax de la versión actual se marca con el número de la transacción que realizó DELETE.
Cuando una fila se modifica mediante el comando UPDATE, en realidad se llevan a cabo dos operaciones: DELETE e INSERT. En la versión actual de la fila, se establece xmax, igual al número de la transacción que realizó el UPDATE. Luego se crea una nueva versión de la misma fila; el valor xmin de esta es el mismo que el valor xmax de la versión anterior.
Los campos xmin y xmax forman parte del encabezado de la versión de la fila. Además de estos campos, el encabezado contiene otros, como:
- infomask: una serie de bits que determinan las propiedades de esta versión. Hay muchos; analizaremos los principales gradualmente.
- ctid: un enlace a la siguiente versión más nueva de la misma fila. En la versión más nueva y actual, ctid se refiere a esta misma versión. El número tiene el formato (x,y), donde x es el número de página, y es el número secuencial del puntero en el arreglo.
- un mapa de bits de valores indefinidos: indica aquellas columnas de esta versión que contienen un valor indefinido (NULL). NULL no es uno de los valores habituales de los tipos de datos, por lo que esta propiedad debe almacenarse por separado.
Como resultado, el encabezado es bastante grande: al menos 23 bytes para cada versión de la fila, y normalmente más debido al mapa de bits de NULLs. Si la tabla es "estrecha" (es decir, contiene pocas columnas), los costos pueden ocupar más que la información útil.
Inserción
Veamos en detalle cómo se realizan las operaciones de cadenas a bajo nivel y comencemos con la inserción.
Para nuestros experimentos, crearemos una nueva tabla con dos columnas e un índice en una de ellas:
=> CREATE TABLE t(
id serial,
s text
);
=> CREATE INDEX ON t(s);
Insertaremos una fila, comenzando primero la transacción.
=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');
Este es el número de nuestra transacción actual:
=> SELECT txid_current();
txid_current
--------------
3664
(1 row)
Echemos un vistazo al contenido de la página. La función heap_page_items de la extensión pageinspect permite obtener información sobre los punteros y versiones de las filas:
=> SELECT * FROM heap_page_items(get_raw_page('t',0)) gx
-[ RECORD 1 ]-------------------
lp | 1
lp_off | 8160
lp_flags | 1
lp_len | 32
t_xmin | 3664
t_xmax | 0
t_field3 | 0
t_ctid | (0,1)
t_infomask2 | 2
t_infomask | 2050
t_hoff | 24
t_bits |
t_oid |
t_data | x0100000009464f4f
Notemos que la palabra heap (montón) en PostgreSQL se refiere a tablas. Este es otro uso peculiar del término: un montón es una estructura de datos conocida La función muestra los datos "tal como son", en un formato difícil de interpretar. Para entender mejor, dejaremos solo parte de la información y la desglosaremos:
=> SELECT '(0,'||lp||')' AS ctid, CASE lp_flags WHEN 0 THEN 'unused' WHEN 1 THEN 'normal' WHEN 2 THEN 'redirect to '||lp_off WHEN 3 THEN 'dead' END AS state, t_xmin as xmin, t_xmax as xmax, (t_infomask & 256) > 0 AS xmin_commited, (t_infomask & 512) > 0 AS xmin_aborted, (t_infomask & 1024) > 0 AS xmax_commited, (t_infomask & 2048) > 0 AS xmax_aborted, t_ctid FROM heap_page_items(get_raw_page('t',0)) gx
-[ RECORD 1 ]-+-------
ctid | (0,1)
state | normal
xmin | 3664
xmax | 0
xmin_commited | f
xmin_aborted | f
xmax_commited | f
xmax_aborted | t
t_ctid | (0,1)
Esto es lo que hicimos:
Agregamos un cero al número del puntero para llevarlo a la misma forma que t_ctid: (número de la página, número del puntero).
- Describimos el estado del puntero lp_flags. Aquí es "normal" — esto significa que el puntero realmente apunta a la versión de la fila. Veremos otros valores más adelante.
- Describimos el estado del puntero lp_flags. Aquí es "normal" — esto significa que el puntero realmente apunta a la versión de la fila. Veremos otros valores más adelante.
- De todos los bits de información, hasta ahora solo hemos destacado dos pares. Los bits xmin_committed y xmin_aborted indican si la transacción con el número xmin ha sido confirmada (o cancelada). Dos bits análogos se relacionan con la transacción con el número xmax.
¿Qué vemos? Al insertar una fila en una tabla, aparecerá un puntero con el número 1, que apunta a la primera y única versión de la fila.
En la versión de la fila, el campo xmin está lleno con el número de la transacción actual. La transacción aún está activa, por lo que ambos bits xmin_committed y xmin_aborted no están establecidos.
El campo ctid de la versión de la fila se refiere a esta misma fila. Esto significa que no existe una versión más nueva.
El campo xmax está lleno con un número ficticio 0, ya que esta versión de la fila no ha sido eliminada y es válida. Las transacciones no prestarán atención a este número, ya que el bit xmax_aborted está establecido.
Demos otro paso para mejorar la legibilidad, agregando los bits informativos a los números de transacción. Y crearemos una función, ya que es probable que necesitemos la consulta más de una vez:
=> CREATE FUNCTION heap_page(relname text, pageno integer)
RETURNS TABLE(ctid tid, state text, xmin text, xmax text, t_ctid tid)
AS $$
SELECT (pageno,lp)::text::tid AS ctid,
CASE lp_flags
WHEN 0 THEN 'sin usar'
WHEN 1 THEN 'normal'
WHEN 2 THEN 'redirigir a '||lp_off
WHEN 3 THEN 'muerto'
END AS state,
t_xmin || CASE
WHEN (t_infomask & 256) > 0 THEN ' (c)'
WHEN (t_infomask & 512) > 0 THEN ' (a)'
ELSE ''
END AS xmin,
t_xmax || CASE
WHEN (t_infomask & 1024) > 0 THEN ' (c)'
WHEN (t_infomask & 2048) > 0 THEN ' (a)'
ELSE ''
END AS xmax,
t_ctid
FROM heap_page_items(get_raw_page(relname,pageno))
ORDER BY lp;
$$ LANGUAGE SQL;
En esta forma es significativamente más claro qué está sucediendo en el encabezado de la versión de la fila:
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+------+-------+--------
(0,1) | normal | 3664 | 0 (a) | (0,1)
(1 fila)
Se puede obtener información similar, pero sustancialmente menos detallada, directamente de la tabla utilizando las pseudocolumnas xmin y xmax:
=> SELECT xmin, xmax, * FROM t;
xmin | xmax | id | s
------+------+----+-----
3664 | 0 | 1 | FOO
(1 fila)
Confirmación
Al finalizar con éxito la transacción, es necesario recordar su estado — marcarla como confirmada. Para ello, se utiliza una estructura llamada XACT (anteriormente, hasta la versión 10, se le llamaba CLOG (registro de confirmaciones) y este nombre aún puede encontrarse en diferentes lugares).
XACT no es una tabla de catálogo del sistema; son archivos en el directorio PGDATA/pg_xact. Para cada transacción, se reservan dos bits: committed y aborted, exactamente como en el encabezado de la versión de la fila. Esta información se divide en varios archivos exclusivamente por conveniencia, volveremos a este tema cuando abordemos la congelación. Y el trabajo con estos archivos se realiza página por página, al igual que con todos los demás.
Así que, al confirmar una transacción en XACT, se establece el bit committed para dicha transacción. Y eso es todo lo que ocurre durante la confirmación (aunque aún no estamos hablando del journal de pregrabación).
Cuando cualquier otra transacción acceda a la página de la tabla que acabamos de ver, deberá responder a varias preguntas.
- ¿Se completó la transacción xmin? Si no es así, la versión de la fila creada no debería ser visible.
Este tipo de verificación se realiza revisando otra estructura que se encuentra en la memoria compartida de la instancia, llamada ProcArray. En ella se encuentra una lista de todos los procesos activos y, para cada uno, se indica el número de su transacción actual (activa). - Si se completó, ¿cómo fue: con una confirmación o con una anulación? Si fue con una anulación, la versión de la fila tampoco debería ser visible.
Para esto se necesita precisamente XACT. Pero, aunque las últimas páginas de XACT se mantienen en los buffers en la memoria, comprobar XACT cada vez resulta costoso. Por lo tanto, el estado de la transacción, una vez determinado, se guarda en los bits xmin_committed y xmin_aborted de la versión de la fila. Si uno de estos bits está establecido, el estado de la transacción xmin se considera conocido y la siguiente transacción no tendrá que consultar XACT.
¿Por qué estos bits no los establece la propia transacción que realiza la inserción? Cuando se realiza una inserción, la transacción aún no sabe si se completará con éxito. Y en el momento de la confirmación, ya no es claro qué filas en qué páginas fueron modificadas. Puede haber muchas de esas páginas, y recordarlas no es conveniente. Además, algunas páginas pueden ser desalojadas de la caché a disco; volver a leerlas para cambiar los bits significaría ralentizar significativamente la confirmación.
La contraparte del ahorro es que después de los cambios, cualquier transacción (incluso la que realiza una simple lectura - SELECT) puede comenzar a modificar las páginas de datos en la caché.
Así que, confirmemos el cambio.
=> COMPROMETER;
No ha cambiado nada en la página (pero sabemos que el estado de la transacción ya está registrado en XACT):
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+------+-------+--------
(0,1) | normal | 3664 | 0 (a) | (0,1)
(1 fila)
Ahora, la transacción que primero acceda a la página debe determinar el estado de la transacción xmin y registrarlo en los bits de información:
=> SELECCIONAR * DE t;
id | s
----+-----
1 | FOO
(1 fila)
=> SELECT * FROM heap_page('t',0);
ctid | estado | xmin | xmax | t_ctid
-------+--------+----------+-------+--------
(0,1) | normal | 3664 (c) | 0 (a) | (0,1)
(1 fila)
Eliminación
Al eliminar una fila, en el campo xmax de la versión actual se registra el número de la transacción de eliminación actual, y el bit xmax_aborted se restablece.
Observemos que el valor establecido de xmax, correspondiente a la transacción activa, actúa como un bloqueo de fila. Si otra transacción intenta actualizar o eliminar esta fila, tendrá que esperar a que termine la transacción xmax. Más adelante se hablará sobre bloqueos. Por ahora, solo anotemos que el número de bloqueos de filas no está limitado. No ocupan espacio en la memoria y el rendimiento del sistema no se ve afectado por su cantidad. Sin embargo, las 'transacciones largas' tienen otros inconvenientes, pero eso también se abordará más adelante.
Eliminaré la fila.
=> INICIAR;
=> ELIMINAR DE t;
=> SELECCIONAR txid_current();
txid_current
--------------
3665
(1 fila)
Vemos que el número de la transacción se registró en el campo xmax, pero los bits de información no están establecidos:
=> SELECT * FROM heap_page('t',0);
ctid | estado | xmin | xmax | t_ctid
-------+--------+----------+------+--------
(0,1) | normal | 3664 (c) | 3665 | (0,1)
(1 fila)
Cancelar
La cancelación de los cambios funciona de manera similar a la confirmación, solo que en XACT se establece el bit aborted para la transacción. La cancelación se realiza tan rápido como la confirmación. Aunque el comando se llama ROLLBACK, no se produce una reversión de los cambios: todo lo que la transacción pudo modificar en las páginas de datos permanece sin cambios.
=> ROLLBACK;
=> SELECCIONAR * DE heap_page('t',0);
ctid | estado | xmin | xmax | t_ctid
-------+--------+----------+------+--------
(0,1) | normal | 3664 (c) | 3665 | (0,1)
(1 fila)
Al acceder a la página se verificará el estado y en la versión de la fila se establecerá el bit de indicación xmax_aborted. El número xmax permanecerá en la página, pero nadie lo considerará ya.
=> SELECCIONAR * DE t;
id | s
----+-----
1 | FOO
(1 fila)
=> SELECT * FROM heap_page('t',0);
ctid | estado | xmin | xmax | t_ctid
-------+--------+----------+----------+--------
(0,1) | normal | 3664 (c) | 3665 (a) | (0,1)
(1 fila)
Actualización
La actualización funciona como si primero se hubiese eliminado la versión actual de la fila, y luego se hubiese insertado una nueva.
=> INICIAR;
=> ACTUALIZAR t SET s = 'BAR';
=> SELECCIONAR txid_current();
txid_current
--------------
3666
(1 fila)
La consulta devuelve una fila (nueva versión):
=> SELECCIONAR * DE t;
id | s
----+-----
1 | BAR
(1 fila)
Pero en la página vemos ambas versiones:
=> SELECT * FROM heap_page('t',0);
ctid | estado | xmin | xmax | t_ctid
-------+--------+----------+-------+--------
(0,1) | normal | 3664 (c) | 3666 | (0,2)
(0,2) | normal | 3666 | 0 (a) | (0,2)
(2 filas)
La versión eliminada está marcada con el número de la transacción actual en el campo xmax. Este valor se escribe sobre el antiguo, ya que la transacción previa fue cancelada. Y el bit xmax_aborted se restablece, ya que el estado de la transacción actual aún se desconoce.
La primera versión de la fila ahora hace referencia a la segunda (campo t_ctid), como la más nueva.
En la página del índice aparece un segundo puntero y una segunda fila, que se refiere a la segunda versión en la página de la tabla.
Al igual que en la eliminación, el valor xmax en la primera versión de la fila indica que la fila está bloqueada.
Y así, terminaremos la transacción.
=> COMPROMETER;
Índices
Hasta ahora solo hemos hablado de las páginas de la tabla. ¿Qué sucede dentro de los índices?
La información en las páginas de índices depende en gran medida del tipo específico de índice. Y incluso un solo tipo de índice puede tener diferentes tipos de páginas. Por ejemplo, un árbol B tiene una página de metadatos y páginas “normales”.
Sin embargo, por lo general, la página tiene un arreglo de punteros a filas y las mismas filas (así como ocurre en la página de tabla). Además, al final de la página se reserva espacio para datos especiales.
Las filas en los índices también pueden tener estructuras muy diferentes dependiendo del tipo de índice. Por ejemplo, para un árbol B, las filas que pertenecen a las páginas hoja contienen el valor de la clave de índice y una referencia (ctid) a la correspondiente fila de la tabla. En general, el índice puede estar configurado de manera completamente diferente.
El aspecto más importante es que en los índices de cualquier tipo no hay versiones de filas. O se puede considerar que cada fila está representada por exactamente una versión. En otras palabras, en el encabezado de la fila del índice no hay campos xmin y xmax. Se puede considerar que los enlaces del índice apuntan a todas las versiones de la tabla de filas, por lo que para averiguar qué versión verá la transacción, solo se puede mirar en la tabla. (Como suele ser el caso, esta no es toda la verdad. En ciertos casos, el mapa de visibilidad permite optimizar el proceso, pero lo discutiremos más adelante.)
En la página del índice, encontramos punteros a ambas versiones, tanto a la actual como a la anterior:
=> SELECT itemoffset, ctid FROM bt_page_items('t_s_idx',1);
itemoffset | ctid
------------+-------
1 | (0,2)
2 | (0,1)
(2 filas)
Transacciones virtuales
En la práctica, PostgreSQL utiliza una optimización que permite 'ahorrar' números de transacciones.
Si la transacción solo lee datos, entonces no afecta la visibilidad de las versiones de las filas. Por lo tanto, al principio, el proceso servidor asigna un número virtual a la transacción (virtual xid). Este número consiste en un identificador de proceso y un número secuencial.
La asignación de este número no requiere sincronización entre todos los procesos y, por lo tanto, se realiza muy rápidamente. Conoceremos otro motivo para usar números virtuales cuando hablemos sobre la congelación.
Los números virtuales no se tienen en cuenta en las instantáneas de datos.
En diferentes momentos, pueden existir en el sistema transacciones virtuales con números que ya se han utilizado, y esto es normal. Pero tal número no se puede escribir en las páginas de datos, porque, en la siguiente referencia a la página, puede perder todo sentido.
=> BEGIN;
=> SELECT txid_current_if_assigned();
txid_current_if_assigned
--------------------------
(1 fila)
Si la transacción comienza a modificar datos, se le asigna un número de transacción real y único.
=> UPDATE accounts SET amount = amount - 1.00;
=> SELECT txid_current_if_assigned();
txid_current_if_assigned
--------------------------
3667
(1 fila)
=> COMPROMETER;
Transacciones anidadas
Puntos de guardado
En SQL se definen puntos de guardado (savepoint), que permiten revertir parte de las operaciones de una transacción sin interrumpirla completamente. Pero esto no encaja en el esquema anterior, ya que el estado de la transacción es único para todos sus cambios, y físicamente no se revierte ningún dato.
Para implementar tal funcionalidad, la transacción con un punto de guardado se divide en varias transacciones anidadas (subtransacción), cuyo estado se puede gestionar por separado.
Las transacciones anidadas tienen su propio número (mayor que el número de la transacción principal). El estado de las transacciones anidadas se registra de la manera habitual en XACT, sin embargo, el estado final depende del estado de la transacción principal: si esta es cancelada, también se cancelan todas las transacciones anidadas.
La información sobre la anidación de las transacciones se almacena en archivos en el directorio PGDATA/pg_subtrans. La referencia a los archivos ocurre a través de buffers en la memoria compartida de la instancia, organizados de la misma manera que los buffers de XACT.
No confunda transacciones anidadas con transacciones independientes. Las transacciones independientes no dependen entre sí, mientras que las anidadas sí dependen. No existen transacciones independientes en PostgreSQL convencional, y probablemente es para mejor: son necesarias muy raramente, y su existencia en otras bases de datos provoca abusos, lo que afecta a todos.
Limpiamos la tabla, comenzamos la transacción e insertamos una fila:
=> TRUNCATE TABLE t;
=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');
=> SELECT txid_current();
txid_current
--------------
3669
(1 row)
=> SELECT xmin, xmax, * FROM t;
xmin | xmax | id | s
------+------+----+-----
3669 | 0 | 2 | FOO
(1 row)
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+------+-------+--------
(0,1) | normal | 3669 | 0 (a) | (0,1)
(1 row)
Ahora establezcamos un punto de guardado e insertamos otra fila.
=> SAVEPOINT sp;
=> INSERT INTO t(s) VALUES ('XYZ');
=> SELECT txid_current();
txid_current
--------------
3669
(1 row)
Note que la función txid_current() devuelve el número de la transacción principal, no de la anidada.
=> SELECT xmin, xmax, * FROM t;
xmin | xmax | id | s
------+------+----+-----
3669 | 0 | 2 | FOO
3670 | 0 | 3 | XYZ
(2 rows)
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+------+-------+--------
(0,1) | normal | 3669 | 0 (a) | (0,1)
(0,2) | normal | 3670 | 0 (a) | (0,2)
(2 rows)
Volvamos al punto de guardado e insertemos una tercera fila.
=> ROLLBACK TO sp;
=> INSERT INTO t(s) VALUES ('BAR');
=> SELECT xmin, xmax, * FROM t;
xmin | xmax | id | s
------+------+----+-----
3669 | 0 | 2 | FOO
3671 | 0 | 4 | BAR
(2 rows)
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+----------+-------+--------
(0,1) | normal | 3669 | 0 (a) | (0,1)
(0,2) | normal | 3670 (a) | 0 (a) | (0,2)
(0,3) | normal | 3671 | 0 (a) | (0,3)
(3 rows)
En la página seguimos viendo la fila añadida por la transacción anidada cancelada.
Confirmamos los cambios.
=> COMMIT;
=> SELECT xmin, xmax, * FROM t;
xmin | xmax | id | s
------+------+----+-----
3669 | 0 | 2 | FOO
3671 | 0 | 4 | BAR
(2 rows)
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+----------+-------+--------
(0,1) | normal | 3669 (c) | 0 (a) | (0,1)
(0,2) | normal | 3670 (a) | 0 (a) | (0,2)
(0,3) | normal | 3671 (c) | 0 (a) | (0,3)
(3 rows)
Ahora es claro que cada transacción anidada tiene su propio estado.
Observemos que no se pueden usar transacciones anidadas en SQL de forma explícita, es decir, no se puede iniciar una nueva transacción sin haber finalizado la actual. Este mecanismo se activa implícitamente al usar puntos de guardado, además de al manejar excepciones en PL/pgSQL y en otros casos más exóticos.
=> BEGIN;
BEGIN
=> BEGIN;
ADVERTENCIA: ya hay una transacción en progreso
BEGIN
=> COMPROMETER;
COMMIT
=> COMPROMETER;
ADVERTENCIA: no hay transacción en progreso
COMMIT
Errores y atomicidad de las operaciones
¿Qué sucederá si ocurre un error durante la operación? Por ejemplo, así:
=> BEGIN;
=> SELECT * FROM t;
id | s
----+-----
2 | FOO
4 | BAR
(2 rows)
=> UPDATE t SET s = repeat('X', 1/(id-4));
ERROR: división por cero
Se produjo un error. Ahora la transacción se considera interrumpida y no se permite ninguna operación en ella:
=> SELECCIONAR * DE t;
ERROR: la transacción actual está abortada, comandos ignorados hasta el final del bloque de transacción
Y aunque se intente confirmar los cambios, PostgreSQL informará sobre la cancelación:
=> COMPROMETER;
ROLLBACK
¿Por qué no se puede continuar con la ejecución de la transacción después de un fallo? La cuestión es que el error podría haber ocurrido de tal manera que obtuviéramos acceso a parte de los cambios: se habría violado la atomicidad no solo de la transacción, sino también del operador. Como en nuestro ejemplo, donde el operador había actualizado una fila antes del error:
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+----------+-------+--------
(0,1) | normal | 3669 (c) | 3672 | (0,4)
(0,2) | normal | 3670 (a) | 0 (a) | (0,2)
(0,3) | normal | 3671 (c) | 0 (a) | (0,3)
(0,4) | normal | 3672 | 0 (a) | (0,4)
(4 filas)
Cabe mencionar que en psql hay un modo que permite continuar el trabajo de la transacción después de un fallo como si las acciones del operador erróneo se deshicieran.
=> set ON_ERROR_ROLLBACK on
=> BEGIN;
=> SELECT * FROM t;
id | s
----+-----
2 | FOO
4 | BAR
(2 rows)
=> UPDATE t SET s = repeat('X', 1/(id-4));
ERROR: división por cero
=> SELECCIONAR * DE t;
id | s
----+-----
2 | FOO
4 | BAR
(2 rows)
=> COMPROMETER;
No es difícil adivinar que en este modo psql efectivamente coloca un punto de guardado implícito antes de cada comando, y en caso de fallo inicia el retroceso a él. Este modo no se utiliza por defecto, ya que el establecimiento de puntos de guardado (incluso sin retroceder a ellos) conlleva gastos operativos significativos.
Fuente: habr.com
