¿Por qué las filas insertadas en un CTE no pueden actualizarse en la misma declaración?


12

En PostgreSQL 9.5, dada una tabla simple creada con:

create table tbl (
    id serial primary key,
    val integer
);

Ejecuto SQL para INSERTAR un valor, luego lo ACTUALIZO en la misma declaración:

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;

El resultado es que se ignora la ACTUALIZACIÓN:

testdb=> select * from tbl;
┌────┬─────┐
 id  val 
├────┼─────┤
  1    1 
└────┴─────┘

¿Por qué es esto? ¿Es esta limitación parte del estándar SQL (es decir, presente en otras bases de datos) o algo específico de PostgreSQL que podría corregirse en el futuro? La documentación de consultas WITH dice que no se admiten múltiples ACTUALIZACIONES, pero no menciona INSERTOS y ACTUALIZACIONES.

Respuestas:


14

Todas las declaraciones en un CTE suceden virtualmente al mismo tiempo. Es decir, se basan en la misma instantánea de la base de datos.

El UPDATEve el mismo estado de la tabla subyacente como el INSERT, lo que significa la fila con val = 1no está, todavía. El manual aclara aquí:

Todas las declaraciones se ejecutan con la misma instantánea (ver Capítulo 13 ), por lo que no pueden "ver" los efectos de los demás en las tablas de destino.

Cada declaración puede ver lo que devuelve otro CTE en la RETURNINGcláusula. Pero las tablas subyacentes les parecen iguales.

Necesitaría dos declaraciones (en una sola transacción) para lo que está tratando de hacer. Para INSERTempezar, el ejemplo dado debería ser solo uno , pero eso puede deberse al ejemplo simplificado.


14

Esta es una decisión de implementación. Se describe en la documentación de Postgres, WITHConsultas (expresiones de tabla comunes) . Hay dos párrafos relacionados con el tema.

Primero, la razón del comportamiento observado:

Las subdeclaraciones WITHse ejecutan simultáneamente entre sí y con la consulta principal . Por lo tanto, cuando se utilizan declaraciones de modificación de datos WITH, el orden en el que las actualizaciones especificadas suceden realmente es impredecible. Todas las declaraciones se ejecutan con la misma instantánea (ver Capítulo 13), por lo que no pueden "ver" los efectos de los demás en las tablas de destino. Esto alivia los efectos de la imprevisibilidad del orden real de las actualizaciones de filas y significa que los RETURNINGdatos son la única forma de comunicar los cambios entre las diferentes WITHsubdeclaraciones y la consulta principal. Un ejemplo de esto es que en ...

Después de publicar una sugerencia junto con pgsql-docs , Marko Tiikkaja explicó (lo que concuerda con la respuesta de Erwin):

Los casos de inserción-actualización e inserción-eliminación no funcionan porque las ACTUALIZACIONES y DELETES no tienen forma de ver las filas INSERTED debido a que se tomó su instantánea antes de que ocurriera el INSERT. No hay nada impredecible en estos dos casos.

Entonces, la razón por la cual su declaración no se actualiza puede explicarse en el primer párrafo anterior (sobre "instantáneas"). Lo que sucede cuando se modifican los CTE es que todos ellos y la consulta principal se ejecutan y "ven" la misma instantánea de los datos (tablas), tal como estaban inmediatamente antes de la ejecución de la instrucción. Los CTE pueden pasar información sobre lo que insertaron / actualizaron / eliminaron entre sí y a la consulta principal utilizando la RETURNINGcláusula, pero no pueden ver los cambios en las tablas directamente. Entonces, veamos qué sucede en su declaración:

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;

Tenemos 2 partes, el CTE ( newval):

-- newval
     INSERT INTO tbl(val) VALUES (1) RETURNING id

y la consulta principal:

-- main 
UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id

El flujo de ejecución es algo como esto:

           initial data: tbl
                id  val 
                 (empty)
               /         \
              /           \
             /             \
    newval:                 \
       tbl (after newval)    \
           id  val           \
            1    1           |
                              |
    newval: returns           |
           id                 |
            1                 |
               \              |
                \             |
                 \            |
                    main query

Como resultado, cuando la consulta principal une la tbl(como se ve en la instantánea) con la newvaltabla, une una tabla vacía con una tabla de 1 fila. Obviamente actualiza 0 filas. Entonces, la declaración nunca llegó a modificar la fila recién insertada y eso es lo que ves.

La solución en su caso es reescribir la declaración para insertar los valores correctos en primer lugar o usar 2 declaraciones. Uno que se inserta y un segundo para actualizar.


Hay otras situaciones similares, como si la declaración tuviera un INSERTy luego un DELETEen las mismas filas. La eliminación fallaría exactamente por las mismas razones.

Algunos otros casos, con actualización-actualización y actualización-eliminación y su comportamiento se explican en el siguiente párrafo, en la misma página de documentos.

Intentar actualizar la misma fila dos veces en una sola declaración no es compatible. Solo se realiza una de las modificaciones, pero no es fácil (y a veces no es posible) predecir de manera confiable cuál. Esto también se aplica a la eliminación de una fila que ya se actualizó en la misma instrucción: solo se realiza la actualización. Por lo tanto, generalmente debe evitar intentar modificar una sola fila dos veces en una sola instrucción. En particular, evite escribir subdeclaraciones WITH que puedan afectar las mismas filas cambiadas por la declaración principal o una subdeclaración hermana. Los efectos de tal declaración no serán predecibles.

Y en la respuesta de Marko Tiikkaja:

Los casos de actualización-actualización y actualización-eliminación no están explícitamente causados ​​por el mismo detalle de implementación subyacente (como los casos de inserción-actualización y inserción-eliminación).
El caso de actualización-actualización no funciona porque internamente se parece al problema de Halloween, y Postgres no tiene forma de saber qué tuplas estaría bien actualizar dos veces y cuáles podrían reintroducir el problema de Halloween.

Entonces, la razón es la misma (cómo se implementan los CTE modificadores y cómo cada CTE ve la misma instantánea), pero los detalles difieren en estos 2 casos, ya que son más complejos y los resultados pueden ser impredecibles en el caso de actualización-actualización.

En la inserción-actualización (como su caso) y una inserción-eliminación similar, los resultados son predecibles. Solo la inserción ocurre ya que la segunda operación (actualizar o eliminar) no tiene forma de ver y afectar las filas recién insertadas.


Sin embargo, la solución sugerida es la misma para todos los casos que intentan modificar las mismas filas más de una vez: no lo haga. Escriba declaraciones que modifiquen cada fila una vez o use declaraciones separadas (2 o más).

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.