Comenzamos con problemas relacionados con el
aislamiento , hicimos una digresión sobre la
organización de datos en un nivel bajo , luego hablamos en detalle
sobre las versiones de fila y cómo se obtienen
instantáneas de las versiones.
La última vez hablamos sobre las actualizaciones CALIENTES y la limpieza en la página, y hoy repasaremos la conocida rutina de limpieza,
aspiradora vulgar . Sí, ya se ha escrito tanto sobre ella que es poco probable que diga algo nuevo, pero la integridad de la imagen requiere sacrificio. Se paciente.
Limpieza normal (aspiradora)
¿Qué hace la limpieza?
La limpieza dentro de la página es rápida, pero libera solo una fracción del espacio. Funciona dentro de la misma página tabular y no afecta a los índices.
La limpieza principal "normal" se realiza mediante el comando VACUUM y la llamaremos simplemente limpieza (y hablaremos sobre la limpieza automática por separado).
Entonces, la limpieza procesa la tabla por completo. Limpia no solo versiones innecesarias de cadenas, sino también referencias a ellas de todos los índices.
El procesamiento ocurre en paralelo con otras actividades en el sistema. En este caso, la tabla y los índices se pueden usar de la manera habitual tanto para leer como para cambiar (sin embargo, la ejecución simultánea de comandos como CREATE INDEX, ALTER TABLE y algunos otros será imposible).
Solo se ven en la tabla las páginas en las que se produjo alguna actividad. Para esto, se utiliza un mapa de visibilidad (le recuerdo que las páginas que contienen solo versiones bastante antiguas de filas que están garantizadas para ser visibles en todas las instantáneas de datos están marcadas en él). Solo se procesan las páginas que no están marcadas en el mapa, mientras que el mapa en sí se actualiza.
En el proceso, el mapa de espacio libre se actualiza para reflejar el espacio libre que aparece en las páginas.
Como de costumbre, crea una tabla:
=> CREATE TABLE vac( id serial, s char(100) ) WITH (autovacuum_enabled = off); => CREATE INDEX vac_s ON vac(s); => INSERT INTO vac(s) VALUES ('A'); => UPDATE vac SET s = 'B'; => UPDATE vac SET s = 'C';
Usando el parámetro
autovacuum_enabled, desactivamos la limpieza automática. Hablaremos de eso la próxima vez, pero por ahora, para los experimentos, es importante que gestionemos la limpieza manualmente.
Hay tres versiones de la fila en la tabla ahora, y cada una está vinculada desde un índice:
=> SELECT * FROM heap_page('vac',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+----------+-----+-----+-------- (0,1) | normal | 4000 (c) | 4001 (c) | | | (0,2) (0,2) | normal | 4001 (c) | 4002 | | | (0,3) (0,3) | normal | 4002 | 0 (a) | | | (0,3) (3 rows)
=> SELECT * FROM index_page('vac_s',1);
itemoffset | ctid ------------+------- 1 | (0,1) 2 | (0,2) 3 | (0,3) (3 rows)
Después de la limpieza, las versiones "muertas" desaparecen y solo hay una, relevante. Y al índice también le queda un enlace:
=> VACUUM vac; => SELECT * FROM heap_page('vac',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+-------+-----+-----+-------- (0,1) | unused | | | | | (0,2) | unused | | | | | (0,3) | normal | 4002 (c) | 0 (a) | | | (0,3) (3 rows)
=> SELECT * FROM index_page('vac_s',1);
itemoffset | ctid ------------+------- 1 | (0,3) (1 row)
Tenga en cuenta que los dos primeros punteros recibieron el estado no utilizado, y no muerto, como lo sería con la limpieza dentro de la página.
Y nuevamente sobre el horizonte de transacciones
¿Cómo determina PostgreSQL qué versiones de fila pueden considerarse "muertas"? Ya consideramos el concepto de un horizonte de transacciones cuando hablamos
de las instantáneas de
datos , pero este es un tema tan importante que no es pecado repetirlo.
Comencemos la experiencia previa nuevamente.
=> TRUNCATE vac; => INSERT INTO vac(s) VALUES ('A'); => UPDATE vac SET s = 'B';
Pero antes de actualizar la línea nuevamente, deje que comience otra transacción (pero no termine). En nuestro ejemplo, funcionará en el nivel de lectura confirmada, pero debería obtener un número de transacción real (no virtual). Por ejemplo, puede cambiar o incluso bloquear algunas filas en cualquier tabla, no necesariamente en vac:
| => BEGIN; | => SELECT s FROM t FOR UPDATE;
| s | ----- | FOO | BAR | (2 rows)
=> UPDATE vac SET s = 'C';
Hay tres filas en la tabla y tres enlaces en el índice. ¿Qué pasa después de la limpieza?
=> VACUUM vac; => SELECT * FROM heap_page('vac',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+----------+-----+-----+-------- (0,1) | unused | | | | | (0,2) | normal | 4005 (c) | 4007 (c) | | | (0,3) (0,3) | normal | 4007 (c) | 0 (a) | | | (0,3) (3 rows)
=> SELECT * FROM index_page('vac_s',1);
itemoffset | ctid ------------+------- 1 | (0,2) 2 | (0,3) (2 rows)
Quedan dos versiones de la fila en la tabla: la limpieza decidió que la versión (0.2) aún no se podía eliminar. La razón, por supuesto, está en el horizonte de transacciones de la base de datos, que en nuestro ejemplo está determinado por una transacción incompleta:
| => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
| backend_xmin | -------------- | 4006 | (1 row)
Puede solicitar limpieza para hablar sobre lo que sucede:
=> VACUUM VERBOSE vac;
INFO: vacuuming "public.vac" INFO: index "vac_s" now contains 2 row versions in 2 pages DETAIL: 0 index row versions were removed. 0 index pages have been deleted, 0 are currently reusable. CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s. INFO: "vac": found 0 removable, 2 nonremovable row versions in 1 out of 1 pages DETAIL: 1 dead row versions cannot be removed yet, oldest xmin: 4006 There were 1 unused item pointers. Skipped 0 pages due to buffer pins, 0 frozen pages. 0 pages are entirely empty. CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s. VACUUM
Por favor tenga en cuenta:
- 2 versiones de fila no extraíbles: se encontraron 2 versiones en la tabla que no se pueden eliminar,
- Todavía no se pueden eliminar las versiones de 1 fila muerta, incluido 1 "muerto",
- xmin más antiguo muestra el horizonte actual.
Repetimos la conclusión una vez más: la presencia de transacciones de larga duración en la base de datos (no completadas o de muy larga ejecución) puede conducir a la expansión de tablas (abarrotadas), independientemente de la frecuencia con que se realice la limpieza. Por lo tanto, en PostgreSQL, las cargas de trabajo OLTP y OLAP están mal combinadas en una base de datos: los informes que se ejecutan durante horas no permitirán que las tablas actualizadas con frecuencia se borren a tiempo. Una posible solución sería crear una réplica de "informes" separada.
Después de completar una transacción abierta, el horizonte cambia y la situación se corrige:
| => COMMIT;
=> VACUUM VERBOSE vac;
INFO: vacuuming "public.vac" INFO: scanned index "vac_s" to remove 1 row versions DETAIL: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s INFO: "vac": removed 1 row versions in 1 pages DETAIL: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s INFO: index "vac_s" now contains 1 row versions in 2 pages DETAIL: 1 index row versions were removed. 0 index pages have been deleted, 0 are currently reusable. CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s. INFO: "vac": found 1 removable, 1 nonremovable row versions in 1 out of 1 pages DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 4008 There were 1 unused item pointers. Skipped 0 pages due to buffer pins, 0 frozen pages. 0 pages are entirely empty. CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s. VACUUM
Ahora la página solo tiene la última versión actual de la línea:
=> SELECT * FROM heap_page('vac',0);
ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+-------+-----+-----+-------- (0,1) | unused | | | | | (0,2) | unused | | | | | (0,3) | normal | 4007 (c) | 0 (a) | | | (0,3) (3 rows)
También hay solo una entrada en el índice:
=> SELECT * FROM index_page('vac_s',1);
itemoffset | ctid ------------+------- 1 | (0,3) (1 row)
Que esta pasando adentro
La limpieza debe procesar tanto la tabla como los índices al mismo tiempo, y hacerlo de una manera que no bloquee la operación de otros procesos. ¿Cómo lo hace ella?
Todo comienza con
un escaneo de tabla (teniendo en cuenta el mapa de visibilidad, como ya se señaló). En las páginas leídas, se determinan versiones innecesarias de cadenas y sus identificadores (tid) se escriben en una matriz especial. La matriz se encuentra en la memoria local del proceso de limpieza; se le asigna un fragmento de
mantenimiento_trabajo_mem . El valor predeterminado para este parámetro es 64 MB. Tenga en cuenta que esta memoria se asigna inmediatamente en su totalidad, y no según sea necesario. Es cierto que si la tabla es pequeña, el fragmento se asigna menos.
A continuación, una de dos cosas: o llegaremos al final de la tabla, o la memoria asignada para la matriz finalizará. En cualquiera de los dos casos, comienza la
fase de limpieza del índice . Para hacer esto,
cada uno de los índices creados en la tabla se
escanea completamente en busca de registros que hagan referencia a versiones almacenadas de filas. Los registros encontrados se borran de las páginas de índice.
En este punto obtenemos la siguiente imagen: en los índices ya no hay enlaces a versiones innecesarias de filas, pero todavía existen en la tabla. Esto no contradice nada: al ejecutar una consulta, no llegamos a versiones muertas de filas (con acceso al índice), o las marcamos al verificar la visibilidad (al escanear una tabla).
Después de eso, comienza la
fase de limpieza de la mesa . La tabla se escanea nuevamente para leer las páginas necesarias, eliminar las versiones almacenadas de las líneas de ellas y liberar los punteros. Podemos hacer esto porque ya no hay enlaces de índices.
Si la tabla no se leyó completamente en la primera pasada, la matriz se borra y todo se repite desde el lugar donde lo dejamos.
De esta manera:
- la tabla siempre se escanea dos veces;
- Si durante la limpieza se eliminan tantas versiones de fila que no pueden caber en la memoria maintenance_work_mem , todos los índices se explorarán por completo tantas veces como sea necesario.
En tablas grandes, esto puede llevar una cantidad considerable de tiempo y crear una carga significativa en el sistema. Por supuesto, las solicitudes no serán bloqueadas, pero la E / S "extra" también es desagradable.
Para acelerar el proceso, tiene sentido pedir una limpieza más a menudo (para que no se borre un número muy grande de versiones de fila cada vez) o asignar más memoria.
Noto entre paréntesis que, a partir de la versión 11, PostgreSQL
puede omitir los escaneos de índice si esto no es absolutamente necesario. Esto debería facilitar la vida de los propietarios de tablas grandes en las que solo se agregan filas (pero no se modifican).
Monitoreo
¿Cómo entender que la limpieza no hace frente al trabajo en una sola pasada?
Ya hemos visto el primer método: puede llamar al comando VACUUM con VERBOSE. Luego, la información sobre las fases del trabajo se mostrará en la consola.
En segundo lugar, a partir de la versión 9.6, hay una vista pg_stat_progress_vacuum, que también contiene toda la información necesaria.
(Hay una tercera forma: mostrar información en el registro de mensajes, pero esto solo funciona para la limpieza automática, que se discutirá la próxima vez).
Insertaremos más filas en la tabla para que la limpieza tarde un tiempo notable, y las actualizaremos para que haya algo que ver con la limpieza.
=> TRUNCATE vac; => INSERT INTO vac(s) SELECT 'A' FROM generate_series(1,500000); => UPDATE vac SET s = 'B';
Reduzca el tamaño de la memoria asignada para la matriz de identificadores:
=> ALTER SYSTEM SET maintenance_work_mem = '1MB'; => SELECT pg_reload_conf();
Comenzamos a limpiar y, mientras funciona, pasaremos a la vista pg_stat_progress_vacuum varias veces:
=> VACUUM VERBOSE vac;
| => SELECT * FROM pg_stat_progress_vacuum \gx
| -[ RECORD 1 ]------+------------------ | pid | 6715 | datid | 41493 | datname | test | relid | 57383 | phase | vacuuming indexes | heap_blks_total | 16667 | heap_blks_scanned | 2908 | heap_blks_vacuumed | 0 | index_vacuum_count | 0 | max_dead_tuples | 174762 | num_dead_tuples | 174480
| => SELECT * FROM pg_stat_progress_vacuum \gx
| -[ RECORD 1 ]------+------------------ | pid | 6715 | datid | 41493 | datname | test | relid | 57383 | phase | vacuuming indexes | heap_blks_total | 16667 | heap_blks_scanned | 5816 | heap_blks_vacuumed | 2907 | index_vacuum_count | 1 | max_dead_tuples | 174762 | num_dead_tuples | 174480
Aquí vemos en particular:
- nombre de la fase actual (fase): hablamos de tres fases principales, pero en general hay más de ellas;
- número total de páginas de la tabla (heap_blks_total);
- el número de páginas rastreadas (heap_blks_scanned);
- el número de páginas ya borradas (heap_blks_vacuumed);
- El número de pases por índice (index_vacuum_count).
El progreso general está determinado por la proporción de heap_blks_vacuumed a heap_blks_total, pero tenga en cuenta que este valor no cambia sin problemas, sino "bruscamente" debido a los escaneos de índice. Sin embargo, se debe prestar especial atención al número de ciclos de limpieza: un valor mayor que 1 significa que la memoria asignada no fue suficiente para completar la limpieza de una sola vez.
La salida del comando VACUUM VERBOSE, completada en este momento, mostrará el panorama general:
INFO: vacuuming "public.vac"
INFO: scanned index "vac_s" to remove 174480 row versions DETAIL: CPU: user: 0.50 s, system: 0.07 s, elapsed: 1.36 s INFO: "vac": removed 174480 row versions in 2908 pages DETAIL: CPU: user: 0.02 s, system: 0.02 s, elapsed: 0.13 s
INFO: scanned index "vac_s" to remove 174480 row versions DETAIL: CPU: user: 0.26 s, system: 0.07 s, elapsed: 0.81 s INFO: "vac": removed 174480 row versions in 2908 pages DETAIL: CPU: user: 0.01 s, system: 0.02 s, elapsed: 0.10 s
INFO: scanned index "vac_s" to remove 151040 row versions DETAIL: CPU: user: 0.13 s, system: 0.04 s, elapsed: 0.47 s INFO: "vac": removed 151040 row versions in 2518 pages DETAIL: CPU: user: 0.01 s, system: 0.02 s, elapsed: 0.08 s
INFO: index "vac_s" now contains 500000 row versions in 17821 pages DETAIL: 500000 index row versions were removed. 8778 index pages have been deleted, 0 are currently reusable. CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s. INFO: "vac": found 500000 removable, 500000 nonremovable row versions in 16667 out of 16667 pages DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 4011 There were 0 unused item pointers. 0 pages are entirely empty. CPU: user: 1.10 s, system: 0.37 s, elapsed: 3.71 s. VACUUM
Aquí puede ver que en total hubo tres pases a través de los índices, cada uno de los cuales despejó 174,480 punteros a versiones muertas de cadenas. ¿De dónde viene este número? Un enlace (tid) toma 6 bytes, y 1024 * 1024/6 = 174762 es el número que vemos en pg_stat_progress_vacuum.max_dead_tuples. En realidad, se puede usar un poco menos: se garantiza que, al leer la página siguiente, todos los punteros a las versiones "muertas" quepan exactamente en la memoria.
Análisis
El análisis o, en otras palabras, la recopilación de información estadística para el planificador de consultas, no está formalmente relacionado con la limpieza. Sin embargo, podemos realizar el análisis no solo con el equipo ANALYZE, sino también combinar la limpieza con el análisis: ANÁLISIS DE VACÍO. En este caso, primero se realiza la limpieza y luego el análisis: no se producen ahorros.
Pero, como veremos más adelante, la limpieza automática y el análisis automático se realizan en un solo proceso y se gestionan de manera similar.
Limpieza completa (aspiradora llena)
Como hemos visto, la limpieza convencional libera más espacio que la limpieza entre páginas, pero incluso esto no siempre resuelve el problema por completo.
Si por alguna razón una tabla o índice ha crecido significativamente en tamaño, la limpieza regular liberará espacio dentro de las páginas existentes: tendrán agujeros que luego se usarán para insertar nuevas versiones de filas. Pero el número de páginas no cambiará y, por lo tanto, desde el punto de vista del sistema operativo, los archivos ocuparán exactamente la misma cantidad de espacio que ocuparon antes de la limpieza. Y esto es malo porque:
- el escaneo completo de una tabla (o índice) se ralentiza;
- puede ser necesario un caché de búfer más grande (porque las páginas están almacenadas y la densidad de información útil disminuye);
- puede aparecer un nivel "extra" en el árbol de índice, lo que ralentizará el acceso al índice;
- los archivos ocupan espacio en disco adicional y copias de seguridad.
(La única excepción son las páginas completamente limpiadas al final del archivo, tales páginas "muerden" el archivo y regresan al sistema operativo).
Si la parte de información útil en los archivos cae por debajo de un límite razonable, el administrador puede realizar una limpieza completa de la tabla. Al mismo tiempo, la tabla y todos sus índices se reconstruyen completamente desde cero, y los datos se empaquetan de la forma más compacta posible (por supuesto, teniendo en cuenta el parámetro factor de relleno). Al reconstruir, PostgreSQL reconstruye secuencialmente la tabla primero y luego cada uno de sus índices. Se crean nuevos archivos para cada objeto y, al final de la reconstrucción, se eliminan los archivos antiguos. Tenga en cuenta que en el proceso de trabajo en el disco requerirá espacio adicional.
Para ilustrar, inserte varias filas en la tabla nuevamente:
=> TRUNCATE vac; => INSERT INTO vac(s) SELECT 'A' FROM generate_series(1,500000);
¿Cómo evaluar la densidad de la información? Para hacer esto, es conveniente usar la extensión especial:
=> CREATE EXTENSION pgstattuple; => SELECT * FROM pgstattuple('vac') \gx
-[ RECORD 1 ]------+--------- table_len | 68272128 tuple_count | 500000 tuple_len | 64500000 tuple_percent | 94.47 dead_tuple_count | 0 dead_tuple_len | 0 dead_tuple_percent | 0 free_space | 38776 free_percent | 0.06
La función lee toda la tabla y muestra estadísticas sobre cuánto espacio ocupan los datos en los archivos. La información principal que nos interesa ahora es el campo tuple_percent: el porcentaje ocupado por datos útiles. Es menos de 100 debido a la inevitable sobrecarga de información de servicio dentro de la página, pero no obstante es bastante alta.
Para el índice, se muestra otra información, pero el campo avg_leaf_density tiene el mismo significado: el porcentaje de información útil (en páginas de hoja).
=> SELECT * FROM pgstatindex('vac_s') \gx
-[ RECORD 1 ]------+--------- version | 3 tree_level | 3 index_size | 72802304 root_block_no | 2722 internal_pages | 241 leaf_pages | 8645 empty_pages | 0 deleted_pages | 0 avg_leaf_density | 83.77 leaf_fragmentation | 64.25
Y aquí está el tamaño de la tabla y el índice:
=> SELECT pg_size_pretty(pg_table_size('vac')) table_size, pg_size_pretty(pg_indexes_size('vac')) index_size;
table_size | index_size ------------+------------ 65 MB | 69 MB (1 row)
Ahora elimine el 90% de todas las líneas. Seleccionamos las filas para eliminar al azar, de modo que en cada página con una alta probabilidad permanezca al menos una fila:
=> DELETE FROM vac WHERE random() < 0.9;
DELETE 450189
¿Qué tamaño tendrán los objetos después de la limpieza normal?
=> VACUUM vac; => SELECT pg_size_pretty(pg_table_size('vac')) table_size, pg_size_pretty(pg_indexes_size('vac')) index_size;
table_size | index_size ------------+------------ 65 MB | 69 MB (1 row)
Vemos que el tamaño no ha cambiado: la limpieza regular no puede reducir el tamaño de los archivos de ninguna manera. Aunque la densidad de información obviamente ha disminuido unas 10 veces:
=> SELECT vac.tuple_percent, vac_s.avg_leaf_density FROM pgstattuple('vac') vac, pgstatindex('vac_s') vac_s;
tuple_percent | avg_leaf_density ---------------+------------------ 9.41 | 9.73 (1 row)
Ahora verifique qué sucede después de una limpieza completa. Aquí están los archivos utilizados por la tabla y los índices ahora:
=> SELECT pg_relation_filepath('vac'), pg_relation_filepath('vac_s');
pg_relation_filepath | pg_relation_filepath ----------------------+---------------------- base/41493/57392 | base/41493/57393 (1 row)
=> VACUUM FULL vac; => SELECT pg_relation_filepath('vac'), pg_relation_filepath('vac_s');
pg_relation_filepath | pg_relation_filepath ----------------------+---------------------- base/41493/57404 | base/41493/57407 (1 row)
Ahora los archivos se reemplazan por otros nuevos. El tamaño de la tabla y el índice ha disminuido significativamente, y la densidad de información ha aumentado en consecuencia:
=> SELECT pg_size_pretty(pg_table_size('vac')) table_size, pg_size_pretty(pg_indexes_size('vac')) index_size;
table_size | index_size ------------+------------ 6648 kB | 6480 kB (1 row)
=> SELECT vac.tuple_percent, vac_s.avg_leaf_density FROM pgstattuple('vac') vac, pgstatindex('vac_s') vac_s;
tuple_percent | avg_leaf_density ---------------+------------------ 94.39 | 91.08 (1 row)
Tenga en cuenta que la densidad de información en el índice incluso ha aumentado en comparación con el original. Volver a crear un índice (árbol B) a partir de los datos disponibles es más rentable que insertar datos en un índice existente línea por línea.
Las
funciones de extensión
pgstattuple que utilizamos leen toda la tabla. Si la tabla es grande, esto es inconveniente y, por lo tanto, hay una función pgstattuple_approx allí, que omite las páginas marcadas en el mapa de visibilidad y muestra números aproximados.
Una forma aún más rápida, pero incluso menos precisa, es estimar la proporción del volumen de datos con respecto al tamaño del archivo en el directorio del sistema. Las opciones para tales consultas se pueden encontrar
en la wiki .
Una limpieza completa no requiere un uso regular, ya que bloquea completamente todo el trabajo con la tabla (incluida la consulta) durante toda la duración de su trabajo. Está claro que en un sistema utilizado activamente esto puede ser inaceptable. Los bloqueos se considerarán por separado, pero por ahora nos limitaremos a mencionar la extensión
pg_repack , que bloquea la tabla solo por un corto tiempo al final del trabajo.
Equipos similares
Hay varios comandos que también reconstruyen completamente tablas e índices, y esto es similar a una limpieza completa. Todos ellos bloquean completamente el trabajo con la tabla, todos eliminan los archivos de datos antiguos y crean nuevos.
El comando CLUSTER es similar en todo a VACUUM FULL, pero además organiza físicamente la versión de las cadenas de acuerdo con uno de los índices disponibles. Esto le da al planificador la capacidad de utilizar el acceso al índice de manera más efectiva en algunos casos. Sin embargo, debe entenderse que la agrupación no es compatible: con los cambios posteriores en la tabla, se violará el orden físico de las versiones de fila.
El comando REINDEX reconstruye un índice único en una tabla. De hecho, VACUUM FULL y CLUSTER usan este comando para reconstruir índices.
El comando TRUNCATE funciona lógicamente igual que DELETE: elimina todas las filas de la tabla. Pero DELETE, como ya se discutió, solo marca la versión de las filas como eliminada, lo que requiere una limpieza adicional. TRUNCATE solo crea un archivo nuevo y limpio. Como regla, esto funciona más rápido, pero tenga en cuenta que TRUNCATE bloqueará completamente el trabajo con la tabla durante todo el tiempo hasta el final de la transacción.
Continuará