Los métodos de acceso a índices deben manejar las actualizaciones concurrentes
del índice por parte de múltiples procesos.
El sistema central de PostgreSQL obtiene
AccessShareLock en el índice durante un escaneo de índice, y
RowExclusiveLock al actualizar el índice (incluyendo el
VACUUM simple). Dado que estos tipos de bloqueo no entran en conflicto, el método
de acceso es responsable de manejar cualquier bloqueo detallado que pueda necesitar.
Se tomará un bloqueo ACCESS EXCLUSIVE en el índice en su conjunto
solo durante la creación del índice, la destrucción o el REINDEX
(en su lugar se toma SHARE UPDATE EXCLUSIVE con
CONCURRENTLY).
La construcción de un tipo de índice que admita actualizaciones concurrentes suele requerir
un análisis exhaustivo y sutil del comportamiento requerido. Para los tipos de índice b-tree
y hash, puedes leer sobre las decisiones de diseño involucradas en
src/backend/access/nbtree/README y
src/backend/access/hash/README.
Aparte de los propios requisitos de coherencia interna del índice, las actualizaciones concurrentes plantean problemas sobre la coherencia entre la tabla padre (el montón o heap) y el índice. Debido a que PostgreSQL separa los accesos y las actualizaciones del montón de los del índice, existen ventanas en las que el índice podría ser incoherente con el montón. Manejamos este problema con las siguientes reglas:
Se crea una nueva entrada en el montón antes de crear sus entradas en el índice. (Por lo tanto, es probable que un escaneo de índice concurrente no vea la entrada del montón. Esto es aceptable porque el lector del índice no estaría interesado en una fila no confirmada de todos modos. Pero consulta Section 63.5).
Cuando se va a eliminar una entrada del montón (mediante VACUUM), se deben
eliminar primero todas sus entradas del índice.
Un escaneo de índice debe mantener un anclaje (pin)
en la página del índice que contiene el último elemento devuelto por
amgettuple, y ambulkdelete no puede eliminar
entradas de páginas que estén ancladas por otros backends. La necesidad
de esta regla se explica a continuación.
Sin la tercera regla, es posible que un lector de índice vea una
entrada de índice justo antes de que sea eliminada por VACUUM, y
luego llegue a la entrada del montón correspondiente después de que esta fuera eliminada por
VACUUM.
Esto no plantea problemas graves si ese número de elemento
sigue sin usarse cuando el lector llega a él, ya que una ranura de elemento vacía
será ignorada por heap_fetch(). Pero, ¿y si un
tercer backend ya ha reutilizado la ranura de elemento para otra cosa?
Cuando se utiliza una captura instantánea compatible con MVCC, no hay problema porque
el nuevo ocupante de la ranura es seguro que es demasiado nuevo para pasar la
prueba de la captura instantánea. Sin embargo, con una captura instantánea no compatible con MVCC (como
SnapshotAny), sería posible aceptar y devolver
una fila que de hecho no coincide con las claves de escaneo. Podríamos defendernos
contra este escenario exigiendo que las claves de escaneo se vuelvan a comprobar
contra la fila del montón en todos los casos, pero eso es demasiado costoso. En su lugar,
utilizamos un anclaje en una página de índice como un proxy para indicar que el lector
podría estar todavía “en vuelo” desde la entrada del índice a la entrada del montón
coincidente. Hacer que ambulkdelete se bloquee en dicho anclaje garantiza
que VACUUM no pueda eliminar la entrada del montón antes de que el lector
haya terminado con ella. Esta solución cuesta poco en tiempo de ejecución, y añade sobrecarga de bloqueo
solo en los raros casos en que realmente hay un conflicto.
Esta solución requiere que los escaneos de índices sean “síncronos”: tenemos que recuperar cada tupla del montón inmediatamente después de escanear la correspondiente entrada de índice. Esto es costoso por varias razones. Un escaneo “asíncrono” en el que recopilamos muchos TIDs del índice, y solo visitamos las tuplas del montón algún tiempo después, requiere mucho menos sobrecarga de bloqueo del índice y puede permitir un patrón de acceso al montón más eficiente. Según el análisis anterior, debemos usar el enfoque síncrono para las capturas instantáneas no compatibles con MVCC, pero un escaneo asíncrono es viable para una consulta que utiliza una captura instantánea MVCC.
En un escaneo de índice amgetbitmap, el método de acceso no
mantiene un anclaje de índice en ninguna de las tuplas devueltas. Por lo tanto,
solo es seguro usar tales escaneos con capturas instantáneas compatibles con MVCC.
Cuando la bandera ampredlocks no está establecida, cualquier escaneo que use ese
método de acceso al índice dentro de una transacción serializable adquirirá un
bloqueo de predicado no bloqueante en todo el índice. Esto generará un
conflicto de lectura-escritura con la inserción de cualquier tupla en ese índice por parte de una
transacción serializable concurrente. Si se detectan ciertos patrones de conflictos de
lectura-escritura entre un conjunto de transacciones serializables
concurrentes, una de esas transacciones puede ser cancelada para proteger la integridad de los
datos. Cuando la bandera está establecida, indica que el método de acceso al
índice implementa un bloqueo de predicado más detallado, lo que tenderá a
reducir la frecuencia de tales cancelaciones de transacciones.