Comenzamos con problemas relacionados con el
aislamiento , hicimos una digresión sobre
la estructura de datos de bajo nivel , luego discutimos las
versiones de fila y observamos cómo se obtienen las
instantáneas de datos de las versiones de fila.
La última vez hablamos sobre actualizaciones CALIENTES y aspiradoras en la página, y hoy procederemos a un conocido
vacío vulgar . Realmente, ya se ha escrito tanto al respecto que casi no puedo agregar nada nuevo, pero la belleza de una imagen completa requiere sacrificio. Así que ten paciencia.
Vacío
¿Qué hace el vacío?
El vacío en la página funciona rápido, pero libera solo una parte del espacio. Funciona dentro de una página de tabla y no toca índices.
El vacío básico "normal" se realiza utilizando el comando VACUUM, y lo llamaremos simplemente "vacío" (dejando "autovacuum" para una discusión por separado).
Entonces, el vacío procesa toda la tabla. Aspira no solo tuplas muertas, sino también referencias a ellas desde todos los índices.
Aspirar es concurrente con otras actividades en el sistema. La tabla y los índices se pueden usar de manera regular tanto para lecturas como para actualizaciones (sin embargo, la ejecución concurrente de comandos como CREATE INDEX, ALTER TABLE y algunos otros es imposible).
Solo se examinan las páginas de la tabla donde se realizaron algunas actividades. Para detectarlos, se utiliza el
mapa de visibilidad (para recordarle, el mapa rastrea aquellas páginas que contienen tuplas bastante antiguas, que con seguridad son visibles en todas las instantáneas de datos). Solo se procesan aquellas páginas que el mapa de visibilidad no rastrea, y el mapa en sí se actualiza.
El
mapa de espacio libre también se actualiza en el proceso para reflejar el espacio libre adicional en las páginas.
Como de costumbre, creemos 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';
Usamos el parámetro
autovacuum_enabled para desactivar el proceso de autovacuum. Lo discutiremos la próxima vez, y ahora es crítico para nuestros experimentos que controlemos manualmente la aspiradora.
La tabla ahora tiene tres tuplas, cada una de las cuales se hace referencia desde el í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 pasar la aspiradora, las tuplas muertas se aspiran, y solo queda una, viva, tupla. Y solo queda una referencia en el índice:
=> 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 adquirieron el estado "no utilizado" en lugar de "muerto", que adquirirían con el vacío en la página.
Sobre el horizonte de transacciones una vez más
¿Cómo descubre PostgreSQL qué tuplas pueden considerarse muertas? Ya hablamos sobre el concepto de horizonte de transacciones cuando discutimos
las instantáneas de datos , pero no hará daño reiterar un asunto tan importante.
Comencemos el experimento anterior nuevamente.
=> TRUNCATE vac; => INSERT INTO vac(s) VALUES ('A'); => UPDATE vac SET s = 'B';
Pero antes de actualizar la fila una vez más, deje que comience una transacción más (pero no finalice). En este ejemplo, usará el nivel Confirmar lectura, pero debe obtener un número de transacción verdadero (no virtual). Por ejemplo, la transacción puede cambiar e incluso bloquear ciertas filas en cualquier tabla, no es obligatorio
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 referencias en el índice ahora. ¿Qué pasará después de pasar la aspiradora?
=> 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 tuplas en la tabla: VACUUM decidió que la tupla (0,2) aún no se puede aspirar. La razón ciertamente está en el horizonte de transacciones de la base de datos, que en este ejemplo está determinada por la transacción no completada:
| => SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
| backend_xmin | -------------- | 4006 | (1 row)
Podemos pedirle a VACUUM que informe lo que está sucediendo:
=> 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
Tenga en cuenta que:
2 nonremovable row versions
: en la tabla se encuentran dos tuplas que no se pueden eliminar.1 dead row versions cannot be removed yet
, una de ellas está muerta.oldest xmin
muestra el horizonte actual.
Reiteremos la conclusión: si una base de datos tiene transacciones de larga duración (no completadas o que se realizan demasiado tiempo), esto puede implicar una hinchazón de la tabla, independientemente de la frecuencia con la que ocurra la aspiración. Por lo tanto, las cargas de trabajo de tipo OLTP y OLAP coexisten pobremente en una base de datos PostgreSQL: los informes que se ejecutan durante horas no permitirán que las tablas actualizadas se aspiren debidamente. La creación de una réplica separada para fines de informes puede ser una posible solución a esto.
Después de completar una transacción abierta, el horizonte se mueve y la situación se arregla:
| => 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 solo queda la última versión en vivo de la fila en la página:
=> 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)
El índice también tiene solo una fila:
=> SELECT * FROM index_page('vac_s',1);
itemoffset | ctid ------------+------- 1 | (0,3) (1 row)
¿Qué pasa adentro?
La aspiración debe procesar la tabla y los índices al mismo tiempo y hacerlo para no bloquear los otros procesos. ¿Cómo puede hacerlo?
Todo comienza con la fase de
almacenamiento dinámico de escaneo (el mapa de visibilidad tomado en cuenta, como ya se mencionó). En las páginas leídas, se detectan tuplas muertas y sus
tid
se escriben en una matriz especializada. La matriz se almacena en la memoria local del proceso de vacío, donde se le asignan bytes de memoria
maintenance_work_mem . El valor predeterminado de este parámetro es 64 MB. Tenga en cuenta que la cantidad total de memoria se asigna de una vez, en lugar de cuando sea necesario. Sin embargo, si la tabla no es grande, se asigna una cantidad menor de memoria.
Luego llegamos al final de la tabla o la memoria asignada para la matriz ha terminado. En cualquier caso, comienza la fase de
índices de aspiración . Con este fin,
cada índice creado en la tabla
se escanea completamente en busca de las filas que hacen referencia a las tuplas recordadas. Las filas encontradas se eliminan por aspiración de las páginas de índice.
Aquí nos enfrentamos a lo siguiente: los índices aún no tienen referencias a tuplas muertas, mientras que la tabla todavía las tiene. Y esto no es contrario a nada: al ejecutar una consulta, no topamos con tuplas muertas (con acceso al índice) ni las rechazamos en la verificación de visibilidad (al escanear la tabla).
Después de eso, comienza la fase de
montón de aspiración . La tabla se escanea nuevamente para leer las páginas apropiadas, aspirarlas de las tuplas recordadas y liberar los punteros. Podemos hacer esto ya que ya no hay referencias de los índices.
Si la tabla no se leyó por completo durante el primer ciclo, la matriz se borra y todo se repite desde donde llegamos.
En resumen:
- La tabla siempre se escanea dos veces.
- Si aspirar elimina tantas tuplas que no caben en la memoria del tamaño maintenance_work_mem , todos los índices se escanearán tantas veces como sea necesario.
Para tablas grandes, esto puede requerir mucho tiempo y agregar una carga de trabajo significativa al sistema. Por supuesto, las consultas no se bloquearán, pero la entrada / salida adicional es definitivamente indeseable.
Para acelerar el proceso, tiene sentido llamar a VACUUM con más frecuencia (para que no se aspiren demasiadas tuplas cada vez) o asignar más memoria.
Para observar entre paréntesis, a partir de la versión 11, PostgreSQL
puede omitir los escaneos de índice a menos que surja una necesidad imperiosa. Esto debe facilitar la vida de los propietarios de tablas grandes donde solo se agregan filas (pero no se modifican).
Monitoreo
¿Cómo podemos descubrir que VACUUM no puede hacer su trabajo en un ciclo?
Ya hemos visto la primera forma: llamar al comando VACUUM con la opción VERBOSE. En este caso, la información sobre las fases del proceso se enviará a la consola.
En segundo lugar, a partir de la versión 9.6, está disponible la vista
pg_stat_progress_vacuum
, que también proporciona toda la información necesaria.
(La tercera forma también está disponible: enviar la información al registro de mensajes, pero esto solo funciona para el vacío automático, que se discutirá la próxima vez).
Insertemos bastantes filas en la tabla, para que el proceso de vacío dure bastante tiempo, y actualicémoslas todas, para que VACUUM pueda hacer cosas.
=> TRUNCATE vac; => INSERT INTO vac(s) SELECT 'A' FROM generate_series(1,500000); => UPDATE vac SET s = 'B';
Reduzcamos el tamaño de memoria asignado para la matriz de identificadores:
=> ALTER SYSTEM SET maintenance_work_mem = '1MB'; => SELECT pg_reload_conf();
Comencemos VACUUM y mientras esté funcionando,
pg_stat_progress_vacuum
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í podemos ver, en particular:
- El nombre de la fase actual: discutimos tres fases principales, pero hay más de ellas en general.
- El número total de páginas de la tabla (
heap_blks_total
). - El número de páginas escaneadas (
heap_blks_scanned
). - El número de páginas ya aspiradas (
heap_blks_vacuumed
). - El número de ciclos de vacío de índice (
index_vacuum_count
).
El progreso general está determinado por la proporción de
heap_blks_vacuumed
a
heap_blks_total
, pero debemos tener en cuenta que este valor cambia en grandes incrementos en lugar de suavemente debido al escaneo de los índices. Sin embargo, se debe prestar la atención principal al número de ciclos de vacío: el número mayor que 1 significa que la memoria asignada no fue suficiente para completar el vacío en un ciclo.
El resultado del comando VACUUM VERBOSE, ya completado en ese momento, mostrará la imagen 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
Podemos ver aquí que se realizaron tres ciclos sobre los índices, y en cada ciclo, se aspiraron 174480 punteros a tuplas muertas. ¿Por qué exactamente este número? Un
tid
ocupa 6 bytes, y 1024 * 1024/6 = 174762, que es el número que vemos en
pg_stat_progress_vacuum.max_dead_tuples
. En realidad, se puede usar un poco menos: esto asegura que cuando se lea la siguiente página, todos los punteros a las tuplas muertas queden seguros en la memoria.
Análisis
El análisis o, en otras palabras, la recopilación de estadísticas para el planificador de consultas, no tiene nada que ver con la aspiradora. Sin embargo, podemos realizar el análisis no solo usando el comando ANALIZAR, sino combinar la aspiración y el análisis en ANÁLISIS DE VACÍO. Aquí el vacío se realiza primero y luego el análisis, por lo que esto no proporciona ganancias.
Pero como veremos más adelante, el análisis de vacío automático y automático se realizan en un solo proceso y se controlan de manera similar.
VACIO LLENO
Como se señaló anteriormente, el vacío libera más espacio que el vacío en la página, pero aún así no resuelve completamente el problema.
Si por alguna razón el tamaño de una tabla o un índice ha aumentado mucho, VACUUM liberará espacio dentro de las páginas existentes: allí se producirán "agujeros", que luego se usarán para insertar nuevas tuplas. 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 el mismo espacio que antes del vacío. Y esto no es bueno porque:
- La exploración completa de la tabla (o índice) se ralentiza.
- Es posible que se requiera una memoria caché de búfer más grande (ya que son las páginas las que están almacenadas allí y la densidad de información útil disminuye).
- En el árbol de índice puede ocurrir un nivel adicional, que ralentizará el acceso al índice.
- Los archivos ocupan espacio adicional en el disco y en las copias de seguridad.
(La única excepción son las páginas completamente aspiradas, ubicadas al final del archivo. Estas páginas se recortan del archivo y se devuelven al sistema operativo).
Si la parte de información útil en los archivos cae por debajo de un límite razonable, el administrador puede hacer VACÍO COMPLETO de la tabla. En este caso, la tabla y todos sus índices se reconstruyen desde cero y los datos se empaquetan de una manera principalmente compacta (por supuesto, se tiene en cuenta el parámetro
fillfactor
). Durante la reconstrucción, PostgreSQL primero reconstruye la tabla y luego cada uno de sus índices uno por uno. Para cada objeto, se crean archivos nuevos y los archivos antiguos se eliminan al final de la reconstrucción. Debemos tener en cuenta que se necesitará espacio adicional en el disco en el proceso.
Para ilustrar esto, inserte nuevamente un cierto número de filas en la tabla:
=> TRUNCATE vac; => INSERT INTO vac(s) SELECT 'A' FROM generate_series(1,500000);
¿Cómo podemos estimar la densidad de información? Para hacer esto, es conveniente usar una extensión especializada:
=> 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: qué datos ocupan la cantidad de espacio en los archivos. La información principal de nuestro interés ahora es el campo
tuple_percent
: el porcentaje de datos útiles. Es inferior a 100 debido a la inevitable sobrecarga de información dentro de una página, pero sigue siendo bastante alta.
Para el índice, se
avg_leaf_density
información diferente, 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 estos son los tamaños de la tabla y los índices:
=> 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 eliminemos el 90% de todas las filas. Hacemos una elección aleatoria de filas para eliminar, por lo que es muy probable que al menos una fila permanezca en cada página:
=> DELETE FROM vac WHERE random() < 0.9;
DELETE 450189
¿Qué tamaño tendrán los objetos después de VACÍO?
=> 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)
Podemos ver que el tamaño no cambió: VACUUM de ninguna manera puede reducir el tamaño de los archivos. Y esto es aunque la densidad de información disminuyó aproximadamente 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 verifiquemos lo que obtenemos después de VACUUM FULL. Ahora la tabla y los índices usan los siguientes archivos:
=> 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)
Los archivos se reemplazan por nuevos ahora. Los tamaños de la tabla y los índices disminuyeron significativamente, mientras que la densidad de información aumentó 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 es incluso mayor que la original. Es más ventajoso reconstruir un índice (árbol B) a partir de los datos disponibles que insertar los datos en un índice existente fila por fila.
Las funciones de la extensión
pgstattuple que utilizamos leen toda la tabla. Pero esto es inconveniente si la tabla es grande, por lo que la extensión tiene la función
pgstattuple_approx
, que omite las páginas marcadas en el mapa de visibilidad y muestra cifras aproximadas.
Una forma más, pero aún menos precisa, es utilizar el catálogo del sistema para estimar aproximadamente la relación del tamaño de los datos con el tamaño del archivo. Puede encontrar ejemplos de tales consultas
en wiki .
VACUUM FULL no está diseñado para un uso regular, ya que bloquea cualquier trabajo con la tabla (consulta incluida) durante toda la duración del proceso. Está claro que para un sistema muy usado, esto puede parecer inaceptable. Los bloqueos se analizarán por separado, y ahora solo mencionaremos la extensión
pg_repack , que bloquea la tabla solo por un corto período de tiempo al final del trabajo.
Comandos similares
Hay algunos comandos que también reconstruyen completamente tablas e índices y, por lo tanto, se parecen a VACUUM FULL. Todos ellos bloquean completamente cualquier trabajo con la tabla, todos eliminan los archivos de datos antiguos y crean nuevos.
El comando CLUSTER es similar a VACUUM FULL, pero también ordena físicamente las tuplas de acuerdo con uno de los índices disponibles. Esto permite al planificador utilizar el acceso al índice de manera más eficiente en algunos casos. Pero debemos tener en cuenta que el agrupamiento no se mantiene: el orden físico de las tuplas se romperá con los cambios posteriores de la tabla.
El comando REINDEX reconstruye un índice separado en la tabla. VACUUM FULL y CLUSTER realmente usan este comando para reconstruir índices.
La lógica del comando TRUNCATE es similar a la de DELETE: elimina todas las filas de la tabla. Pero DELETE, como ya se mencionó, solo marca las tuplas como eliminadas, y esto requiere una mayor aspiración. Y TRUNCATE solo crea un archivo nuevo y limpio. Como regla, esto funciona más rápido, pero debemos tener en cuenta que TRUNCATE bloqueará cualquier trabajo con la tabla hasta el final de la transacción.
Sigue leyendo .