PostgreSQL hace cumplir las restricciones de unicidad de SQL
utilizando índices únicos, que son índices que no permiten
múltiples entradas con claves idénticas. Un método de acceso que admite esta
característica establece amcanunique en verdadero.
(En la actualidad, solo b-tree lo admite). Las columnas enumeradas en la
cláusula INCLUDE no se consideran al hacer cumplir la
unicidad.
Debido a MVCC, siempre es necesario permitir que existan físicamente entradas duplicadas en un índice: las entradas podrían referirse a versiones sucesivas de una única fila lógica. El comportamiento que realmente queremos hacer cumplir es que ninguna captura instantánea de MVCC pueda incluir dos filas con claves de índice iguales. Esto se desglosa en los siguientes casos que deben comprobarse al insertar una nueva fila en un índice único:
Si una fila válida en conflicto ha sido eliminada por la transacción actual, está bien. (En particular, dado que un UPDATE siempre elimina la versión antigua de la fila antes de insertar la nueva versión, esto permitirá un UPDATE en una fila sin cambiar la clave).
Si se ha insertado una fila en conflicto por una transacción que aún no se ha comprometido, el posible insertador debe esperar a ver si esa transacción se compromete. Si se deshace (rollback), entonces no hay conflicto. Si se compromete sin eliminar la fila en conflicto de nuevo, hay una violación de unicidad. (En la práctica, simplemente esperamos a que la otra transacción termine y luego rehacemos la comprobación de visibilidad en su totalidad).
Del mismo modo, si una fila válida en conflicto ha sido eliminada por una transacción que aún no se ha comprometido, el posible insertador debe esperar a que esa transacción se comprometa o aborte, y luego repetir la prueba.
Además, inmediatamente antes de reportar una violación de unicidad
según las reglas anteriores, el método de acceso debe volver a comprobar la
vigencia de la fila que se está insertando. Si está confirmada muerta, entonces
no se debe reportar ninguna violación. (Este caso no puede ocurrir durante el
escenario ordinario de insertar una fila que acaba de ser creada por
la transacción actual. Sin embargo, puede suceder durante
CREATE UNIQUE INDEX CONCURRENTLY).
Requerimos que el método de acceso al índice aplique estas pruebas por sí mismo, lo que significa que debe acceder al montón para comprobar el estado de confirmación de cualquier fila que se muestre que tiene una clave duplicada según el contenido del índice. Esto es, sin duda, feo y no modular, pero ahorra trabajo redundante: si hiciéramos una búsqueda separada, la búsqueda de índice para una fila en conflicto se repetiría esencialmente al buscar el lugar para insertar la entrada de índice de la nueva fila. Además, no hay una forma obvia de evitar condiciones de carrera a menos que la comprobación de conflicto sea una parte integral de la inserción de la nueva entrada de índice.
Si la restricción de unicidad es diferible, existe una complejidad adicional:
necesitamos poder insertar una entrada de índice para una fila nueva, pero diferir cualquier
error de violación de unicidad hasta el final de la sentencia o incluso más tarde. Para
evitar búsquedas repetidas innecesarias en el índice, el método de acceso al índice
debe realizar una comprobación de unicidad preliminar durante la inserción inicial.
Si esto muestra que definitivamente no hay ninguna tupla viva en conflicto, habremos
terminado. De lo contrario, programamos una nueva comprobación para cuando sea el momento de
hacer cumplir la restricción. Si en el momento de la nueva comprobación, tanto la tupla
insertada como alguna otra tupla con la misma clave están vivas, entonces se debe reportar
el error. (Ten en cuenta que para este propósito, “viva” significa en realidad
“cualquier tupla en la cadena HOT de la entrada del índice está viva”).
Para implementar esto, a la función aminsert se le pasa un
parámetro checkUnique que tiene uno de los siguientes valores:
UNIQUE_CHECK_NO indica que no se debe realizar ninguna comprobación de
unicidad (este no es un índice único).
UNIQUE_CHECK_YES indica que este es un índice único no diferible,
y la comprobación de unicidad debe hacerse inmediatamente, como
se describió anteriormente.
UNIQUE_CHECK_PARTIAL indica que la restricción única
es diferible. PostgreSQL
utilizará este modo para insertar la entrada de índice de cada fila. El método de
acceso debe permitir entradas duplicadas en el índice y reportar cualquier
duplicado potencial devolviendo falso desde aminsert.
Para cada fila para la cual se devuelve falso, se programará una nueva comprobación
diferida.
El método de acceso debe identificar cualquier fila que pueda violar la restricción única, pero no es un error que reporte falsos positivos. Esto permite realizar la comprobación sin esperar a que terminen otras transacciones; los conflictos reportados aquí no se tratan como errores y se volverán a comprobar más tarde, momento en el cual es posible que ya no sean conflictos.
UNIQUE_CHECK_EXISTING indica que se trata de una nueva comprobación
diferida de una fila que fue reportada como una violación potencial de unicidad.
Aunque esto se implementa llamando a aminsert, el método de
acceso no debe insertar una nueva entrada de índice en este
caso. La entrada del índice ya está presente. Más bien, el método de acceso
debe comprobar si hay otra entrada de índice viva. Si es así, y
si la fila de destino también está viva, reporta un error.
Se recomienda que en una llamada a UNIQUE_CHECK_EXISTING,
el método de acceso verifique además que la fila de destino realmente tiene una
entrada existente en el índice, y reporte un error si no es así. Esta
es una buena idea porque los valores de la tupla de índice pasados a
aminsert habrán sido recalculados. Si la definición del
índice involucra funciones que no son realmente inmutables, podríamos estar
comprobando el área incorrecta del índice. Comprobar que la fila de destino
se encuentra en la nueva comprobación verifica que estamos escaneando
para los mismos valores de tupla que se utilizaron en la inserción original.