MVCC-5. Limpieza en página y CALIENTE

Permítame recordarle que examinamos cuestiones relacionadas con el aislamiento , hicimos una digresión sobre la organización de los datos a un nivel bajo y luego hablamos en detalle sobre las versiones de fila y cómo se obtienen instantáneas de las versiones.

Hoy nos ocuparemos de dos problemas bastante relacionados: la limpieza dentro de la página y las actualizaciones HOT . Ambos mecanismos pueden clasificarse como optimizaciones; son importantes, pero casi no están cubiertos en la documentación del usuario.

Limpieza en la página con actualizaciones periódicas


Al acceder a una página, tanto durante la actualización como durante la lectura, se puede realizar una limpieza rápida dentro de la página si PostgreSQL entiende que la página se está quedando sin espacio. Esto ocurre en dos casos.

  1. Una actualización realizada anteriormente en esta página (ACTUALIZACIÓN) no encontró suficiente espacio para colocar una nueva versión de la fila en la misma página. Esta situación se recuerda en el título de la página y la próxima vez que se borra la página.
  2. La página se llena más que en fillfactor. En este caso, la limpieza se realiza de inmediato, sin retrasar la próxima vez.

Fillfactor es un parámetro de almacenamiento que se puede definir para la tabla (y para el índice). PostgreSQL inserta una nueva fila (INSERT) en una página solo si esta página tiene menos del porcentaje de relleno o está llena. El espacio restante está reservado para nuevas versiones de cadenas que resultan de actualizaciones (ACTUALIZACIÓN). El valor predeterminado para las tablas es 100, es decir, el espacio no está reservado (y el valor para los índices es 90).

La limpieza dentro de la página elimina versiones de filas que no son visibles en ninguna imagen (ubicada más allá del "horizonte de eventos" de la base de datos, hablamos de esto la última vez ), pero funciona estrictamente dentro de la misma página tabular. Los punteros a las versiones depuradas de cadenas no se liberan, ya que se puede hacer referencia a ellos desde índices, y el índice es otra página. La limpieza en la página nunca va más allá de una página tabular, pero es muy rápida.

Por las mismas razones, el mapa de espacio libre no se actualiza; También ahorra espacio para actualizaciones, no para inserciones. El mapa de visibilidad tampoco se actualiza.

El hecho de que una página se pueda borrar al leer significa que una solicitud de lectura (SELECCIONAR) puede hacer que las páginas cambien. Este es otro de esos casos, además del cambio diferido previamente de los bits de sugerencia.

Veamos cómo funciona esto, usando un ejemplo. Cree una tabla e índices en ambas columnas.

=> CREATE TABLE hot(id integer, s char(2000)) WITH (fillfactor = 75); => CREATE INDEX hot_id ON hot(id); => CREATE INDEX hot_s ON hot(s); 

Si solo se almacenan letras latinas en la columna s, entonces cada versión de la fila ocupará 2004 bytes más 24 bytes del encabezado. Establecemos el parámetro de almacenamiento del factor de relleno al 75%; habrá suficiente espacio para tres líneas.

Por conveniencia, recreamos una función ya familiar, complementando la salida con dos campos:

 => CREATE FUNCTION heap_page(relname text, pageno integer) RETURNS TABLE(ctid tid, state text, xmin text, xmax text, hhu text, hot text, t_ctid tid) AS $$ SELECT (pageno,lp)::text::tid 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 || 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, CASE WHEN (t_infomask2 & 16384) > 0 THEN 't' END AS hhu, CASE WHEN (t_infomask2 & 32768) > 0 THEN 't' END AS hot, t_ctid FROM heap_page_items(get_raw_page(relname,pageno)) ORDER BY lp; $$ LANGUAGE SQL; 

Y creemos una función para mirar dentro de la página de índice:

 => CREATE FUNCTION index_page(relname text, pageno integer) RETURNS TABLE(itemoffset smallint, ctid tid) AS $$ SELECT itemoffset, ctid FROM bt_page_items(relname,pageno); $$ LANGUAGE SQL; 

Comprobaremos cómo funciona la limpieza dentro de la página. Para hacer esto, inserte una línea y cámbiela varias veces:

 => INSERT INTO hot VALUES (1, 'A'); => UPDATE hot SET s = 'B'; => UPDATE hot SET s = 'C'; => UPDATE hot SET s = 'D'; 

Hay cuatro versiones de la línea en la página:

 => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+----------+-----+-----+-------- (0,1) | normal | 3979 (c) | 3980 (c) | | | (0,2) (0,2) | normal | 3980 (c) | 3981 (c) | | | (0,3) (0,3) | normal | 3981 (c) | 3982 | | | (0,4) (0,4) | normal | 3982 | 0 (a) | | | (0,4) (4 rows) 

Como se esperaba, simplemente excedimos el umbral del factor de relleno. Esto se indica por la diferencia entre el tamaño de página y los valores superiores: supera el umbral del 75% del tamaño de la página, que es 6144 bytes.

 => SELECT lower, upper, pagesize FROM page_header(get_raw_page('hot',0)); 
  lower | upper | pagesize -------+-------+---------- 40 | 64 | 8192 (1 row) 

Por lo tanto, la próxima vez que acceda a la página, debe realizarse una limpieza en la página. Mira esto.

 => UPDATE hot SET s = 'E'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+-------+-----+-----+-------- (0,1) | dead | | | | | (0,2) | dead | | | | | (0,3) | dead | | | | | (0,4) | normal | 3982 (c) | 3983 | | | (0,5) (0,5) | normal | 3983 | 0 (a) | | | (0,5) (5 rows) 

Se borran todas las versiones irrelevantes de las líneas (0,1), (0,2) y (0,3); después de eso, se agrega una nueva versión de la línea (0.5) al espacio desocupado.

Las versiones de las líneas restantes después de la limpieza se desplazan físicamente al lado de las direcciones de las páginas principales para que todo el espacio libre esté representado por un fragmento continuo. Los valores de los punteros cambian en consecuencia. Gracias a esto, no hay problemas con la fragmentación del espacio libre en la página.

Los punteros a versiones eliminadas de cadenas no se pueden liberar porque se hace referencia desde una página de índice. Veamos la primera página del índice hot_s (porque el cero está ocupado con metainformación):

 => SELECT * FROM index_page('hot_s',1); 
  itemoffset | ctid ------------+------- 1 | (0,1) 2 | (0,2) 3 | (0,3) 4 | (0,4) 5 | (0,5) (5 rows) 

Veremos la misma imagen en otro índice:

 => SELECT * FROM index_page('hot_id',1); 
  itemoffset | ctid ------------+------- 1 | (0,5) 2 | (0,4) 3 | (0,3) 4 | (0,2) 5 | (0,1) (5 rows) 

Puede observar que los punteros a las filas de la tabla van "hacia atrás", pero no importa, porque en todas las versiones de las filas el mismo valor es id = 1. Pero en el índice anterior los punteros están ordenados por valores de s, y esto sustancialmente.

Con acceso al índice, PostgreSQL puede obtener (0,1), (0,2) o (0,3) como el identificador de versión de fila. Luego intentará obtener la fila correspondiente de la página de la tabla, pero gracias al estado muerto del puntero, encontrará que tal versión ya no existe y la ignorará. (De hecho, la primera vez que detecta la falta de una versión de una fila de la tabla, PostgreSQL también cambiará el estado del puntero en la página de índice para que no vuelva a acceder a la página de la tabla).

Es importante que la limpieza dentro de la página solo funcione dentro de una página tabular y no borre las páginas de índice.

Actualizaciones CALIENTES


¿Por qué es malo mantener enlaces a todas las versiones de una cadena en el índice?

En primer lugar, con cualquier cambio de fila, debe actualizar todos los índices creados para la tabla: como ha aparecido una nueva versión, debe tener enlaces a ella. Y debe hacerlo en cualquier caso, incluso si cambian los campos que no están incluidos en el índice. Obviamente, esto no es muy efectivo.

En segundo lugar, los índices acumulan enlaces a versiones históricas de la cadena, que luego deben borrarse junto con las versiones mismas (lo veremos un poco más adelante).

Además, hay una característica de la implementación del árbol B en PostgreSQL. Si no hay suficiente espacio en la página de índice para insertar una nueva fila, la página se divide en dos y todos los datos se redistribuyen entre ellos. Esto se llama una página dividida. Sin embargo, al eliminar filas, dos páginas de índice ya no se "pegan" en una. Debido a esto, el tamaño del índice puede no disminuir incluso si se elimina una parte sustancial de los datos.

Naturalmente, cuantos más índices se creen en la tabla, mayores dificultades tendrá que enfrentar.

Sin embargo, si el valor de una columna que no pertenece a ningún índice cambia, entonces no tiene sentido crear un registro adicional en el árbol B que contenga el mismo valor clave. Así es como funciona la optimización, llamada actualización HOT: la actualización de tupla de solo almacenamiento dinámico.

Con esta actualización, solo hay una entrada en la página de índice que se refiere a la primera versión de la fila en la página de la tabla. Y ya dentro de esta página tabular se organiza una cadena de versiones:

  • las cadenas que se cambian e incluyen en la cadena se marcan con el bit Heap Hot Updated;
  • las filas a las que no se hace referencia desde el índice se marcan con el bit Heap Only Tuple (es decir, "solo la versión tabular de la fila");
  • Se admite la vinculación regular de versiones de cadena a través del campo ctid.

Si, al escanear un índice, PostgreSQL entra en una página de tabla y descubre una versión marcada como Heap Hot Updates, comprende que no necesita detenerse y avanza a lo largo de toda la cadena de actualización. Por supuesto, para todas las versiones de cadenas obtenidas de esta manera, la visibilidad se verifica antes de que se devuelvan al cliente.

Para ver el funcionamiento de una actualización HOT, elimine un índice y borre la tabla.

 => DROP INDEX hot_s; => TRUNCATE TABLE hot; 

Repita la inserción y actualice la fila.

 => INSERT INTO hot VALUES (1, 'A'); => UPDATE hot SET s = 'B'; 

Esto es lo que vemos en la página de la tabla:

 => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+-------+-----+-----+-------- (0,1) | normal | 3986 (c) | 3987 | t | | (0,2) (0,2) | normal | 3987 | 0 (a) | | t | (0,2) (2 rows) 

En la página hay una cadena de cambios:

  • el indicador Heap Hot Updated indica que debe seguir la cadena ctid,
  • el indicador Heup Only Tuple indica que no hay enlaces de índice a esta versión de la fila.

Con más cambios, la cadena crecerá (dentro de la página):

 => UPDATE hot SET s = 'C'; => UPDATE hot SET s = 'D'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+----------+-----+-----+-------- (0,1) | normal | 3986 (c) | 3987 (c) | t | | (0,2) (0,2) | normal | 3987 (c) | 3988 (c) | t | t | (0,3) (0,3) | normal | 3988 (c) | 3989 | t | t | (0,4) (0,4) | normal | 3989 | 0 (a) | | t | (0,4) (4 rows) 

Además, en el índice hay una sola referencia a la "cabeza" de la cadena:

 => SELECT * FROM index_page('hot_id',1); 
  itemoffset | ctid ------------+------- 1 | (0,1) (1 row) 

Hacemos hincapié en que las actualizaciones HOT funcionan si los campos actualizados no están incluidos en ningún índice. De lo contrario, en algún índice habría un enlace directamente a la nueva versión de la cadena, lo que contradice la idea de esta optimización.

La optimización solo funciona dentro de los límites de una página; por lo tanto, un bypass adicional de la cadena no requiere acceso a otras páginas y no afecta el rendimiento.

Limpieza en la página con actualizaciones CALIENTES


Un caso especial pero importante de limpieza entre páginas es la limpieza durante las actualizaciones HOT.

Como la última vez, ya excedimos el umbral del factor de relleno, por lo que la próxima actualización debería conducir a una limpieza en la página. Pero esta vez en la página hay una cadena de actualizaciones. La "cabeza" de esta cadena HOT siempre debe permanecer en su lugar, ya que el índice se refiere a ella, y el resto de los punteros se pueden liberar: se sabe que no se hace referencia desde el exterior.

Para no tocar el "encabezado", se utiliza el direccionamiento doble: el puntero al que se refiere el índice, en este caso (0,1), recibe el estado de "redireccionar", redirigiendo a la versión deseada de la cadena.

 => UPDATE hot SET s = 'E'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+-------+-----+-----+-------- (0,1) | redirect to 4 | | | | | (0,2) | normal | 3990 | 0 (a) | | t | (0,2) (0,3) | unused | | | | | (0,4) | normal | 3989 (c) | 3990 | t | t | (0,2) (4 rows) 

Tenga en cuenta que:

  • Se borraron las versiones (0,1), (0,2) y (0,3).
  • El puntero de la cabeza (0,1) permaneció, pero recibió el estado de redireccionamiento,
  • se escribe una nueva versión de la línea (0.2), ya que se garantizó que esta versión no tenía enlaces de índices y se liberó el puntero (sin usar).

Realice la actualización unas cuantas veces más:

 => UPDATE hot SET s = 'F'; => UPDATE hot SET s = 'G'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+----------+-----+-----+-------- (0,1) | redirect to 4 | | | | | (0,2) | normal | 3990 (c) | 3991 (c) | t | t | (0,3) (0,3) | normal | 3991 (c) | 3992 | t | t | (0,5) (0,4) | normal | 3989 (c) | 3990 (c) | t | t | (0,2) (0,5) | normal | 3992 | 0 (a) | | t | (0,5) (5 rows) 

La siguiente actualización nuevamente causa la limpieza dentro de la página:

 => UPDATE hot SET s = 'H'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+-------+-----+-----+-------- (0,1) | redirect to 5 | | | | | (0,2) | normal | 3993 | 0 (a) | | t | (0,2) (0,3) | unused | | | | | (0,4) | unused | | | | | (0,5) | normal | 3992 (c) | 3993 | t | t | (0,2) (5 rows) 

Nuevamente, algunas versiones se borran y el puntero a la "cabeza" se desplaza en consecuencia.

Conclusión: con actualizaciones frecuentes de columnas fuera de los índices, puede tener sentido reducir el parámetro fillfactor para reservar espacio en la página para las actualizaciones. Por supuesto, debemos tener en cuenta que cuanto más bajo es el factor de relleno, más espacio no asignado queda en la página y, en consecuencia, aumenta el tamaño físico de la tabla.

Rotura de cadena CALIENTE


Si no hay suficiente espacio libre en la página para publicar una nueva versión de una línea, la cadena se romperá. La versión de la línea publicada en otra página tendrá que hacer un enlace separado del índice.

Para obtener esta situación, comenzamos una transacción paralela y construimos una instantánea de datos en ella.

 | => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SELECT count(*) FROM hot; 
 | count | ------- | 1 | (1 row) 

Una instantánea no borrará la versión de las líneas en la página. Ahora realizamos la actualización en la primera sesión:

 => UPDATE hot SET s = 'I'; => UPDATE hot SET s = 'J'; => UPDATE hot SET s = 'K'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+----------+-----+-----+-------- (0,1) | redirect to 2 | | | | | (0,2) | normal | 3993 (c) | 3994 (c) | t | t | (0,3) (0,3) | normal | 3994 (c) | 3995 (c) | t | t | (0,4) (0,4) | normal | 3995 (c) | 3996 | t | t | (0,5) (0,5) | normal | 3996 | 0 (a) | | t | (0,5) (5 rows) 

La próxima vez que se actualice la página, no habrá suficiente espacio en la página, pero la limpieza en la página no podrá liberar nada:

 => UPDATE hot SET s = 'L'; 

 | => COMMIT; --     

 => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+----------+-----+-----+-------- (0,1) | redirect to 2 | | | | | (0,2) | normal | 3993 (c) | 3994 (c) | t | t | (0,3) (0,3) | normal | 3994 (c) | 3995 (c) | t | t | (0,4) (0,4) | normal | 3995 (c) | 3996 (c) | t | t | (0,5) (0,5) | normal | 3996 (c) | 3997 | | t | (1,1) (5 rows) 

En la versión (0.5) vemos un enlace a (1.1) que conduce a la página 1.

 => SELECT * FROM heap_page('hot',1); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+------+-------+-----+-----+-------- (1,1) | normal | 3997 | 0 (a) | | | (1,1) (1 row) 

Ahora hay dos filas en el índice, cada una de las cuales apunta al comienzo de su cadena HOT:

 => SELECT * FROM index_page('hot_id',1); 
  itemoffset | ctid ------------+------- 1 | (1,1) 2 | (0,1) (2 rows) 

Desafortunadamente, la información sobre la limpieza in-page y las actualizaciones HOT está prácticamente ausente en la documentación, y la verdad debe buscarse en el código fuente. Recomiendo comenzar con README.HOT .

Continuará

Source: https://habr.com/ru/post/449704/


All Articles