Esta sección proporciona una visión general de TOAST (The Oversized-Attribute Storage Technique).
PostgreSQL utiliza un tamaño de página fijo (comúnmente 8 kB), y no permite que las tuplas abarquen varias páginas. Por lo tanto, no es posible almacenar directamente valores de campo muy grandes. Para superar esta limitación, los valores de campo grandes se comprimen y/o se dividen en múltiples filas físicas. Esto sucede de forma transparente para el usuario, con un impacto mínimo en la mayor parte del código del backend. La técnica se conoce afectuosamente como TOAST (o “lo mejor desde la invención del pan de molde”). La infraestructura de TOAST también se utiliza para mejorar el manejo de valores de datos grandes en memoria.
Solo ciertos tipos de datos admiten TOAST; no es necesario imponer esta sobrecarga a los tipos de datos
que no pueden producir valores de campo grandes. Para admitir TOAST, un tipo de datos debe tener una
representación de longitud variable (varlena), en la cual, ordinariamente, la primera palabra de cuatro
bytes de cualquier valor almacenado contiene la longitud total del valor en bytes (incluyéndose a sí misma).
TOAST no restringe el resto de la representación del tipo de datos. Las representaciones especiales llamadas
colectivamente valores TOASTed funcionan modificando o reinterpretando esta palabra de longitud inicial.
Por lo tanto, las funciones a nivel de C que admiten un tipo de datos compatible con TOAST deben tener cuidado
con la forma en que manejan los valores de entrada potencialmente TOASTed: una entrada podría no consistir realmente en una palabra
de longitud de cuatro bytes y el contenido hasta después de haber sido detoasted. (Esto se hace normalmente
invocando PG_DETOAST_DATUM antes de hacer cualquier cosa con un valor de entrada, pero en algunos casos
son posibles enfoques más eficientes. Consulta Section 36.13.1 para obtener más detalles).
TOAST usurpa dos bits de la palabra de longitud de varlena (los bits de orden superior en máquinas big-endian, los bits de orden inferior en máquinas little-endian), limitando así el tamaño lógico de cualquier valor de un tipo de datos compatible con TOAST a 1 GB (230 - 1 bytes). Cuando ambos bits son cero, el valor es un valor ordinario no TOASTed del tipo de datos, y los bits restantes de la palabra de longitud proporcionan el tamaño total del dato (incluida la palabra de longitud) en bytes. Cuando el bit de mayor o menor orden está establecido, el valor tiene solo una cabecera de un solo byte en lugar de la cabecera normal de cuatro bytes, y los bits restantes de ese byte proporcionan el tamaño total del dato (incluido el byte de longitud) en bytes. Esta alternativa admite el almacenamiento eficiente en espacio de valores de menos de 127 bytes, al tiempo que permite que el tipo de datos crezca a 1 GB si es necesario. Los valores con cabeceras de un solo byte no están alineados en ningún límite en particular, mientras que los valores con cabeceras de cuatro bytes están alineados en al menos un límite de cuatro bytes; esta omisión del relleno de alineación proporciona un ahorro de espacio adicional que es significativo en comparación con los valores cortos. Como caso especial, si los bits restantes de una cabecera de un solo byte son todos cero (lo que sería imposible para una longitud autoinclusiva), el valor es un puntero a datos fuera de línea, con varias alternativas posibles que se describen a continuación. El tipo y el tamaño de dicho puntero TOAST se determinan mediante un código almacenado en el segundo byte del dato. Por último, cuando el bit de mayor o menor orden está limpio pero el bit adyacente está establecido, el contenido del dato ha sido comprimido y debe ser descomprimido antes de su uso. En este caso, los bits restantes de la palabra de longitud de cuatro bytes indican el tamaño total del dato comprimido, no los datos originales. Ten en cuenta que la compresión también es posible para datos fuera de línea, pero la cabecera de varlena no indica si ha ocurrido; en su lugar, el contenido del puntero TOAST lo indica.
La técnica de compresión utilizada para los datos comprimidos en línea o fuera de línea se puede seleccionar para cada columna mediante
la configuración de la opción de columna COMPRESSION en CREATE TABLE o ALTER TABLE.
El valor predeterminado para las columnas sin una configuración explícita es consultar el parámetro
default_toast_compression en el momento en que se insertan los datos.
Como se ha mencionado, hay múltiples tipos de datos de puntero de TOAST. El tipo más antiguo y común es un
puntero a datos fuera de línea almacenados en una tabla TOAST que es independiente de,
pero asociada a, la tabla que contiene el dato de puntero TOAST en sí. Estos datos de puntero en disco
son creados por el código de gestión de TOAST (en access/common/toast_internals.c) cuando un
tuplo que se va a almacenar en disco es demasiado grande para almacenarse tal cual. Aparecen más detalles en Section 66.2.1.
Alternativamente, un dato de puntero TOAST puede contener un puntero a datos fuera de línea que aparecen en otro lugar de
la memoria. Estos datos son necesariamente de corta duración y nunca aparecerán en el disco, pero son muy útiles para evitar la copia y
el procesamiento redundante de valores de datos grandes. Aparecen más detalles en Section 66.2.2.
Si alguna de las columnas de una tabla es compatible con TOAST, la tabla tendrá una tabla TOAST
asociada, cuyo OID se almacena en la entrada pg_class.reltoastrelid de la tabla.
Los valores TOASTed en disco se mantienen en la tabla TOAST, como se describe con más detalle a continuación.
Los valores fuera de línea se dividen (después de la compresión, si se utiliza) en fragmentos (chunks) de como máximo
TOAST_MAX_CHUNK_SIZE bytes (por defecto, este valor se elige de modo que quepan cuatro filas de fragmentos en una página,
lo que lo hace de unos 2000 bytes). Cada fragmento se almacena como una fila independiente en la tabla TOAST perteneciente
a la tabla propietaria. Cada tabla TOAST tiene las columnas chunk_id (un OID que identifica
el valor TOASTed en particular), chunk_seq (un número de secuencia para el fragmento dentro de su valor), y
chunk_data (los datos reales del fragmento). Un índice único en chunk_id y
chunk_seq proporciona una recuperación rápida de los valores. Un dato de puntero que representa un valor TOASTed
fuera de línea en disco necesita, por lo tanto, almacenar el OID de la tabla TOAST en la que buscar y el OID del valor
específico (su chunk_id). Para mayor comodidad, los datos de puntero también almacenan el tamaño del dato lógico
(longitud original de los datos sin comprimir), el tamaño físico almacenado (diferente si se aplicó compresión) y el método de compresión utilizado,
si lo hay. Teniendo en cuenta los bytes de cabecera de varlena, el tamaño total de un dato de puntero TOAST en disco es, por tanto,
de 18 bytes, independientemente del tamaño real del valor representado.
El código de gestión de TOAST se activa únicamente cuando el valor de una fila que se va a almacenar en una tabla es
más ancho que TOAST_TUPLE_THRESHOLD bytes (normalmente 2 kB). El código de TOAST comprimirá y/o moverá
los valores de campo fuera de línea hasta que el valor de la fila sea más corto que TOAST_TUPLE_TARGET bytes (también normalmente
2 kB, ajustable) o no se puedan obtener más ganancias. Durante una operación UPDATE, los valores de los campos que no han cambiado normalmente
se conservan tal cual; por lo tanto, una actualización de una fila con valores fuera de línea no incurre en costos de TOAST
si ninguno de los valores fuera de línea cambia.
El código de gestión de TOAST reconoce cuatro estrategias diferentes para almacenar columnas compatibles con TOAST en el disco:
PLAIN evita tanto la compresión como el almacenamiento fuera de línea. Esta es la única estrategia posible
para las columnas de tipos de datos no compatibles con TOAST.
EXTENDED permite tanto la compresión como el almacenamiento fuera de línea. Este es el valor predeterminado
para la mayoría de los tipos de datos compatibles con TOAST. Se intentará primero la compresión, y luego el
almacenamiento fuera de línea si la fila sigue siendo demasiado grande.
EXTERNAL permite el almacenamiento fuera de línea pero no la compresión. El uso de EXTERNAL
hará que las operaciones de subcadena en columnas anchas de tipo text y bytea sean más rápidas (a costa de
un mayor espacio de almacenamiento) porque estas operaciones están optimizadas para obtener solo las partes requeridas del valor
fuera de línea cuando no está comprimido.
MAIN permite la compresión pero no el almacenamiento fuera de línea. (En realidad, el almacenamiento fuera de línea
se seguirá realizando para tales columnas, pero solo como último recurso cuando no haya otra forma de hacer que la fila sea lo
suficientemente pequeña como para caber en una página).
Cada tipo de datos compatible con TOAST especifica una estrategia predeterminada para las columnas de ese tipo de datos,
pero la estrategia para una columna de tabla determinada se puede alterar con ALTER TABLE ... SET STORAGE.
TOAST_TUPLE_TARGET se puede ajustar para cada tabla utilizando ALTER TABLE ... SET (toast_tuple_target = N).
Este esquema tiene una serie de ventajas en comparación con un enfoque más directo, como permitir que los valores de fila abarquen páginas. Asumiendo que las consultas se restringen normalmente mediante comparaciones con valores clave relativamente pequeños, la mayor parte del trabajo del ejecutor se realizará utilizando la entrada de fila principal. Los valores grandes de los atributos TOASTed solo se extraerán (si se seleccionan en absoluto) en el momento en que el conjunto de resultados se envía al cliente. Por lo tanto, la tabla principal es mucho más pequeña y caben más filas suyas en la caché de búferes compartidos de lo que ocurriría sin ningún almacenamiento fuera de línea. Los conjuntos de ordenación también se reducen, y las ordenaciones se realizarán con más frecuencia completamente en memoria. Una pequeña prueba demostró que una tabla que contenía páginas HTML típicas y sus URL se almacenaba en aproximadamente la mitad del tamaño de los datos brutos, incluida la tabla TOAST, y que la tabla principal contenía solo aproximadamente el 10% de todos los datos (las URL y algunas páginas HTML pequeñas). No hubo diferencias en el tiempo de ejecución en comparación con una tabla de comparación no TOASTed, en la que todas las páginas HTML se recortaron a 7 kB para que cupieran.
Los punteros de TOAST pueden apuntar a datos que no están en el disco, sino en otra parte de la memoria del proceso del servidor actual. Obviamente, estos punteros no pueden ser de larga duración, pero no por ello dejan de ser útiles. Actualmente existen dos subcasos: punteros a datos indirectos y punteros a datos expandidos.
Los punteros indirectos de TOAST apuntan simplemente a un valor varlena no indirecto almacenado en algún lugar de la memoria. Este caso se creó originalmente como una prueba de concepto, pero actualmente se utiliza durante la decodificación lógica para evitar tener que crear potencialmente tuplas físicas que superen 1 GB (lo que podría ocurrir al extraer todos los valores de campo fuera de línea en la tupla). El caso es de uso limitado, ya que el creador del dato del puntero es totalmente responsable de que los datos referenciados sobrevivan durante todo el tiempo que el puntero pueda existir, y no hay infraestructura que ayude con esto.
Los punteros de TOAST expandidos son útiles para tipos de datos complejos cuya representación en disco no es especialmente adecuada
para fines computacionales. Como ejemplo, la representación estándar varlena de un array de PostgreSQL incluye información
de dimensionalidad, un mapa de bits de nulos si hay elementos nulos, y luego los valores de todos los elementos en orden. Cuando el tipo de elemento en
sí es de longitud variable, la única forma de encontrar el elemento N-ésimo es escanear todos los elementos precedentes.
Esta representación es adecuada para el almacenamiento en disco debido a su compacidad, pero para los cálculos con el array es mucho mejor tener una
representación “expandida” o “descompuesta” en la que se hayan identificado todas las ubicaciones de inicio de los elementos.
El mecanismo de puntero de TOAST admite esta necesidad permitiendo que un Datum de paso por referencia apunte a un valor varlena estándar
(la representación en disco) o a un puntero TOAST que apunta a una representación expandida en algún lugar de la memoria. Los detalles
de esta representación expandida dependen del tipo de datos, aunque debe tener una cabecera estándar y cumplir con los demás requisitos de la API que se indican
en src/include/utils/expandeddatum.h. Las funciones a nivel de C que trabajan con el tipo de datos pueden elegir manejar cualquiera
de las dos representaciones. Las funciones que no conocen la representación expandida, sino que simplemente aplican PG_DETOAST_DATUM
a sus entradas, recibirán automáticamente la representación varlena tradicional; de este modo, el soporte para una representación expandida se puede
introducir de forma incremental, función por función.
Los punteros de TOAST a valores expandidos se desglosan además en punteros de lectura y escritura (read-write) y de solo lectura (read-only). La representación a la que se apunta es la misma en ambos casos, pero una función que recibe un puntero de lectura y escritura tiene permiso para modificar el valor referenciado in-situ, mientras que una que recibe un puntero de solo lectura no debe hacerlo; debe crear primero una copia si desea realizar una versión modificada del valor. Esta distinción y algunas convenciones asociadas permiten evitar la copia innecesaria de valores expandidos durante la ejecución de las consultas.
Para todos los tipos de punteros TOAST en memoria, el código de gestión de TOAST garantiza que ningún dato de puntero de este tipo pueda almacenarse accidentalmente en el disco. Los punteros TOAST en memoria se expanden automáticamente a valores varlena normales en línea antes del almacenamiento, y luego se convierten opcionalmente en punteros TOAST en disco, si la tupla contenedora fuera de lo contrario demasiado grande.