Solo para recordarle, ya discutimos cuestiones relacionadas con el
aislamiento , hicimos una digresión sobre
la estructura de datos de bajo nivel , y luego exploramos las
versiones de fila y observamos cómo se obtienen las
instantáneas de datos de las versiones de fila.
Ahora procederemos a dos problemas estrechamente relacionados:
vacío en la página y
actualizaciones CALIENTES . Ambas técnicas pueden referirse a optimizaciones; son importantes, pero prácticamente no están cubiertos en la documentación.
Aspiración en la página durante actualizaciones periódicas
Al acceder a una página para una actualización o lectura, si PostgreSQL entiende que la página se está quedando sin espacio, puede hacer un vacío rápido en la página. Esto sucede en cualquiera de los casos:
- Una actualización anterior en esta página no encontró suficiente espacio para asignar una nueva versión de fila en la misma página. Tal situación se recuerda en el encabezado de la página, y la próxima vez que la página se vacíe.
- La página tiene más del
fillfactor
de fillfactor
por ciento lleno. En este caso, el vacío se realiza de inmediato sin posponer hasta el siguiente.
fillfactor
es un parámetro de almacenamiento que se puede definir para una tabla (y para un índice). PostgresSQL inserta una nueva fila en una página solo si la página tiene menos del
fillfactor
de
fillfactor
por ciento lleno. El espacio restante está reservado para las nuevas tuplas que se crean como resultado de las actualizaciones. El valor predeterminado para las tablas es 100, es decir, no se reserva espacio (y el valor predeterminado para los índices es 90).
El vacío en la página elimina las tuplas que no son visibles en ninguna instantánea (aquellas que están más allá del horizonte de transacciones de la base de datos, que se discutió la
última vez ), pero lo hace estrictamente dentro de una página de tabla. Los punteros a las tuplas aspiradas no se liberan, ya que se puede hacer referencia a ellos desde índices, y hay un índice en otra página. El vacío en la página nunca llega más allá de una página de tabla, sino que funciona muy rápidamente.
Por las mismas razones, el mapa de espacio libre no se actualiza; esto también reserva el espacio extra para actualizaciones en lugar de para inserciones. El mapa de visibilidad tampoco se actualiza.
El hecho de que una página se pueda aspirar durante las lecturas significa que una consulta SELECT puede implicar el cambio de páginas. Este es un caso más como este, además de un cambio diferido de bits de pista, discutido anteriormente.
Consideremos un ejemplo de cómo funciona. Creemos 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 la columna
s
almacena solo caracteres latinos, cada versión de fila ocupará 2004 bytes más 24 bytes de un encabezado. Establecemos el parámetro de almacenamiento del
fillfactor
al 75%, que reserva suficiente espacio para tres filas.
Para examinar convenientemente el contenido de la página de la tabla, recreemos una función ya familiar agregando dos campos más a la salida:
=> 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;
También creemos una función para mirar en 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;
Veamos cómo funciona el vacío en la página. Para hacer esto, insertamos una fila y la cambiamos 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 tuplas en la página ahora:
=> 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, acabamos de superar el umbral del
fillfactor
. Esto queda claro por la diferencia entre el
pagesize
y
upper
valores
upper
: excede el umbral igual al 75% del tamaño de la página, lo que hace 6144 bytes.
=> SELECT lower, upper, pagesize FROM page_header(get_raw_page('hot',0));
lower | upper | pagesize -------+-------+---------- 40 | 64 | 8192 (1 row)
Entonces, cuando se accede a la página la próxima vez, debe ocurrir un vacío en la página. Vamos a ver 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)
Todas las tuplas muertas (0,1), (0,2) y (0,3) son aspiradas; después de eso se agrega una nueva tupla (0.5) en el espacio liberado.
Las tuplas que sobrevivieron a la aspiración se mueven físicamente hacia direcciones altas de la página para que todo el espacio libre esté representado por un área continua. Los valores de los punteros se modifican en consecuencia. Gracias a esto, no surgen problemas con la fragmentación del espacio libre en una página.
Los punteros a las tuplas aspiradas no se pueden liberar ya que se hace referencia a ellos desde la página de índice. Veamos la primera página del índice
hot_s
(porque la página cero está ocupada por 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)
También vemos la misma imagen en el 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 notar que los punteros a las filas de la tabla siguen aquí en orden inverso, pero esto no hace ninguna diferencia ya que todas las tuplas tienen el mismo valor: id = 1. Pero en el índice anterior, los punteros están ordenados por los valores de
s
, y esto es esencial.
Con acceso al índice, PostgreSQL puede obtener (0,1), (0,2) o (0,3) como identificadores de tupla. Luego intentará obtener la versión de fila adecuada de la página de la tabla, pero debido al estado "muerto" del puntero, PostgreSQL descubrirá que tal versión ya no existe y la ignorará. (En realidad, una vez que descubrió que la versión de una fila de la tabla no está disponible, PostgreSQL cambiará el estado del puntero en la página de índice para no acceder más a la página de la tabla).
Es esencial que el vacío en la página funcione solo dentro de una página de tabla y no aspire las páginas de índice.
Actualizaciones CALIENTES
¿Por qué no es bueno almacenar referencias a todas las versiones de fila en el índice?
Primero, para cualquier cambio de la fila, todos los índices creados para la tabla deben actualizarse: una vez que se ha creado una nueva versión, se debe hacer referencia a ella. Y debemos hacer esto en cualquier caso, incluso si se cambian los campos que no están indexados. Obviamente, esto no es muy eficiente.
En segundo lugar, los índices acumulan referencias a tuplas históricas, que luego deben aspirarse junto con las tuplas mismas (discutiremos un poco más adelante cómo se hace).
Además, B-tree en PostgreSQL tiene los detalles de implementación. Si una página de índice no tiene espacio suficiente para insertar una nueva fila, la página se divide en dos y todos los datos se distribuyen entre ellas. Esto se llama división de una página. Sin embargo, cuando se eliminan las filas, las dos páginas de índice no se fusionan en una sola. Debido a esto, el tamaño del índice puede no reducirse incluso si se elimina una parte significativa de los datos.
Naturalmente, cuantos más índices se crean en una tabla, se encuentran más complejidades.
Sin embargo, si se cambia un valor en una columna que no está indexada en absoluto, no tiene sentido crear una fila de árbol B adicional que contenga el mismo valor de la clave. Así es exactamente cómo funciona la optimización llamada
actualización HOT (
actualización de Tupla solo de montón).
Durante esta actualización, la página de índice contiene solo una fila, que hace referencia a la primera versión de la fila en la página de la tabla. Y ya está dentro de la página de la tabla, que se organiza una cadena de tuplas:
- Las filas actualizadas que están en la cadena están etiquetadas con el bit Heap Hot Updated.
- Las filas a las que no se hace referencia desde el índice se etiquetan con el bit Heup Only Tuple.
- Como de costumbre, las versiones de fila están vinculadas a través del campo
ctid
.
Si durante la exploración del índice, PostgreSQL accede a una página de tabla y encuentra una tupla etiquetada como Heap Hot Updates, entiende que no debe detenerse, sino que debe seguir la cadena HOT, considerando cada tupla en ella. Ciertamente, para todas las tuplas obtenidas de esta manera, la visibilidad se verifica antes de devolverlas al cliente.
Para observar cómo funciona una actualización HOT, eliminemos un índice y borremos la tabla.
=> DROP INDEX hot_s; => TRUNCATE TABLE hot;
Ahora rehacemos la inserción y actualización de una fila.
=> INSERT INTO hot VALUES (1, 'A'); => UPDATE hot SET s = 'B';
Y 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)
Hay una cadena de cambios en la página:
- El indicador Heap Hot Updated indica que se debe seguir la cadena
ctid
. - El indicador Tupla de solo almacenamiento dinámico indica que esta tupla no está referenciada desde los índices.
La cadena crecerá (dentro de la página) con más cambios:
=> 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)
Pero solo hay una referencia a la cabeza de la cadena en el índice:
=> SELECT * FROM index_page('hot_id',1);
itemoffset | ctid ------------+------- 1 | (0,1) (1 row)
Para enfatizar, las actualizaciones HOT funcionan en el caso en que los campos para actualizar no están indexados en absoluto. De lo contrario, algún índice contendría una referencia directamente a una nueva versión de fila, y esto es incompatible con el concepto de esta optimización.
La optimización funciona solo dentro de una página y, por lo tanto, un recorrido adicional por la cadena no necesita acceso a otras páginas y no afecta el rendimiento.
Vacío en la página durante las actualizaciones CALIENTES
Aspirar durante las actualizaciones CALIENTES es un caso especial, pero importante, de vacío en la página.
Como antes, ya hemos excedido el umbral del
fillfactor
, por lo que la próxima actualización debe causar un vacío en la página. Pero esta vez hay una cadena de actualizaciones en la página. La cabeza de esta cadena HOT siempre debe permanecer donde está, ya que el índice hace referencia a ella, mientras que el resto de los punteros se pueden liberar: se sabe que no tienen referencias desde el exterior.
Para no tocar el puntero de la cabeza, se usa direccionamiento indirecto: el puntero al que hace referencia el índice - (0,1) en este caso - adquiere el estado de "redireccionamiento", que redirige a la tupla apropiada.
=> 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:
- Las tuplas (0,1), (0,2) y (0,3) se eliminaron por aspiración.
- El puntero de la cabeza (0,1) permanece, pero adquirió el estado de "redirección".
- La nueva versión de la fila sobrescribió (0,2) ya que no había referencias a esa tupla con seguridad, y el puntero se soltó (estado "no utilizado").
Hagamos una actualización varias 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 próxima actualización hace que la aspiradora en la página vuelva a aparecer:
=> 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 de las tuplas se aspiran y el puntero a la cabeza de la cadena se mueve en consecuencia.
Conclusión: si las columnas que no están indexadas se actualizan con frecuencia, puede tener sentido reducir el parámetro
fillfactor
para reservar espacio en la página para las actualizaciones. Sin embargo, debemos tener en cuenta que cuanto menor sea el
fillfactor
, más espacio libre queda en una página, por lo que aumenta el tamaño físico de la tabla.
Rotura de una cadena CALIENTE
Si la página carece de espacio libre para asignar una nueva tupla, la cadena se romperá. Y tendremos que hacer una referencia separada del índice a la versión de fila ubicada en una página diferente.
Para reproducir esta situación, comencemos una transacción concurrente y construyamos la instantánea de datos en ella.
| => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SELECT count(*) FROM hot;
| count | ------- | 1 | (1 row)
La instantánea no permitirá aspirar las tuplas de la página. Ahora hagamos una 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)
En la próxima actualización, la página no tendrá suficiente espacio, pero el vacío en la página no podrá aspirar 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 tupla (0,5), hay una referencia a (1,1), que se encuentra en 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 documentación carece prácticamente de información sobre el vacío en la página y las actualizaciones HOT, y debe buscar respuestas en el código fuente. Te aconsejo que comiences con README.HOT .
Sigue leyendo .