Reglas sobre INSERT, UPDATE y DELETE

Diferencias con las reglas de las vistas.

Las reglas que se definien para ON INSERT, UPDATE y DELETE son totalmente diferentes de las que se han descrito en la sección anterior para las vistas. Primero, su comando CREATE RULE permite más:

Segundo, no modifican el árbol de traducción en el sitio. En lugar de ello, crean cero o varios árboles de traducción nuevos y pueden desechar el original.

Cómo trabajan estas reglas

Mantenga en mente la sintaxis

    CREATE RULE rule_name AS ON event
        TO object [WHERE rule_qualification]
        DO [INSTEAD] [action | (actions) | NOTHING];
En lo que sigue, "las reglas de update" muestran reglas que están definidas ON INSERT, UPDATE o DELETE.

Update toma las reglas aplicadas por el sistema de reglas cuando la relación resultado y el tipo de comando de un árbol de traducción son iguales al objeto y el acontecimiento dado en el comando CREATE RULE. Para reglas de update, el sistema de reglas crea una lista de árboles de traducción. Inicialmente la lista de árboles de traducción está vacía. Puede haber cero (palabra clave NOTHING), una o múltiples acciones. Para simplificar, veremos una regla con una acción. Esta regla puede tener una cualificación o no y puede ser INSTEAD o no.

¿Qué es una cualificación de una regla? Es una restricción que se dice cuándo las acciones de una regla se deberían realizar y cuándo no. Esta cualficación sólo se puede referir a las pseudo-relaciones NEW y/o OLD, que básicamente son la relación dada como objeto (pero con unas características especiales).

De este modo tenemos cuatro casos que producen los siguientes árboles de traducción para una regla de una acción:

Finalmente, si la regla no es INSTEAD, el árbol de traducción original sin cambiar se añade a la lista. Puesto que sólo las reglas INSTEAD cualificadas se añaden al árbol de traducción original, terminamos con un máximo total de dos árboles de traducción para una regla con una acción.

Los árboles de traducción generados a partir de las acciones de las reglas se colocan en el sistema de reescritura de nuevo, y puede ser que otras reglas aplicadas resulten en más o menos árboles de traducción. De este modo, los árboles de traducción de las acciones de las reglas deberían tener bien otro tipo de comando, bien otra relación resultado. De otro modo, este proceso recursivo terminaría en un bucle. Hay un límite de recursiones compiladas actualmente de 10 iteraciones. Si tras 10 iteraciones aún sigue habiendo reglas de update para aplicar, el sistema de reglas asumirá que se ha producido un bucle entre muchas definiciones de reglas y aborta la transacción.

Los árboles de traducción encontrados en las acciones del catálogo de sistema pg_rewrite son sólo plantillas. Una vez que ellos pueden hacer referencia a las entradas de tabla de rango para NEW u OLD, algunas sustituciones habrán sido hechas antes de ser utilizadas. Para cualquier referencia a NEW, la lista objetivo de la query original se revisa busando una entrada correspondiente. si se encuentra, esas entradas de la expresión se sitúan en la referencia. De otro modo, NEW se mantiene igual que OLD. Cualquier referencia a OLD se reemplaza por una referencia a la entrada de la tabla de rango que es la relación resultado.

Una primera regla paso a paso.

Queremos tracear los cambios en la columna sl_avail de la relación shoelace_data. Para ello, crearemos una tabla de log, y una regla que escriba las entradas cada vez que se realice una UPDATE sobre shoelace_data.

    CREATE TABLE shoelace_log (
        sl_name    char(10),      -- shoelace changed
        sl_avail   integer,       -- new available value
        log_who    name,          -- who did it
        log_when   datetime       -- when
    );

    CREATE RULE log_shoelace AS ON UPDATE TO shoelace_data
        WHERE NEW.sl_avail != OLD.sl_avail
        DO INSERT INTO shoelace_log VALUES (
                                        NEW.sl_name,
                                        NEW.sl_avail,
                                        getpgusername(),
                                        'now'::text
                                    );
Un detalle interesante es la caracterización de 'now' en la reglas de la acción INSERT para teclear texto. Sin ello, el traductor vería en el momento del CREATE RULE, que el tipo objetivo en shoelace_log es un dato de tipo fecha, e intenta hacer una constante de él... con éxito. De ese modo, se almacenaría un valor constante en la acción de la regla y todas las entradas del log tendrían la hora de la instrucción CREATE RULE. No es eso exactamente lo que queremos. La caracterización lleva al traductor a construir un "fecha-hora" que será evaluada en el momento de la ejecución (datetime('now'::text)).

Ahora Al hace

    al_bundy=> UPDATE shoelace_data SET sl_avail = 6                       
    al_bundy->     WHERE sl_name = 'sl7';
y nosotros miramos en la tabla de log.
    al_bundy=> SELECT * FROM shoelace_log;
    sl_name   |sl_avail|log_who|log_when                        
    ----------+--------+-------+--------------------------------
    sl7       |       6|Al     |Tue Oct 20 16:14:45 1998 MET DST
    (1 row)
Que es justo lo que nosotros esperábamos. Veamos qué ha ocurrido en la sombra. El traductor creó un árbol de traducción (esta vez la parte del árbol de traducción original está resaltado porque la base de las operación es es la acción de la regla para las reglas de update)
    UPDATE shoelace_data SET sl_avail = 6
      FROM shoelace_data shoelace_data
     WHERE bpchareq(shoelace_data.sl_name, 'sl7');
Hay una regla para 'log_shoelace' que es ON UPDATE con la expresión de cualificación de la regla:
    int4ne(NEW.sl_avail, OLD.sl_avail)
y una acción
    INSERT INTO shoelace_log SELECT 
           *NEW*.sl_name, *NEW*.sl_avail,
           getpgusername(), datetime('now'::text)
      FROM shoelace_data *NEW*, shoelace_data *OLD*,
           shoelace_log shoelace_log;
No detallaremos la salida de la vista del sistema pg_rules. Especialmente manipula la siutación de que aquí sólo se haga referencia a NEW y OLD en la INSERT, y las salidas del formato de VALUES de INSERT. De hecho, no hay diferencia entre una INSERT ... VALUES y una INSERT ... SELECT al nivel del árbol de traducción. Ambos tienen tablas de rango, listas objetivo, pueden tener cualificación, etc. El optimizador decide más tarde si crear un plan de ejecución de tio resultado, barrido secuencial, barrido de índice, join o cualquier otro para ese árbol de traducción. Si no hay referencias en entradas de la tabla de rango previas al árbol de traducción, éste se convierte en un plan de ejecución (la versión INSERT ... VALUES). La acción de las reglas anterior puede ciertamente resultar en ambas variantes.

La regla es una regla no-INSTEAD cualificada, de modo que el sistema de reglas deberá devolver dos árboles de traducción. La acción de la regla modificada y el árbol de traducción original. En el primer paso, la tabla de rango de la query original está incorporada al árbol de traducción de la acción de las reglas. Esto da como resultado

    INSERT INTO shoelace_log SELECT 
           *NEW*.sl_name, *NEW*.sl_avai,
           getpgusername(), datetime('now'::text)
      FROM shoelace_data shoelace_data, 
           shoelace_data *NEW*,
           shoelace_data *OLD*, 
	   shoelace_log shoelace_log;
En el segundo paso, se añade la cualificación de la regla, de modo que el resultado se restringe a las filas en las que sl_avail cambie.
    INSERT INTO shoelace_log SELECT 
           *NEW*.sl_name, *NEW*.sl_avai,
           getpgusername(), datetime('now'::text)
      FROM shoelace_data shoelace_data, shoelace_data *NEW*,
           shoelace_data *OLD*, shoelace_log shoelace_log
     WHERE int4ne(*NEW*.sl_avail, *OLD*.sl_avail);
En el tercer paso, se añade la cualificación de los árboles de traducción originales, restringiendo el juego de resultados más aún, a sólo las filas tocadas por el árbol de traducción original.
    INSERT INTO shoelace_log SELECT 
           *NEW*.sl_name, *NEW*.sl_avai,
           getpgusername(), datetime('now'::text)
      FROM shoelace_data shoelace_data, shoelace_data *NEW*,
           shoelace_data *OLD*, shoelace_log shoelace_log
     WHERE int4ne(*NEW*.sl_avail, *OLD*.sl_avail)
       AND bpchareq(shoelace_data.sl_name, 'sl7');
En el paso cuatro se sustituyen las referencias NEW por las entradas de la lista objetivo del árbol de traducción original o con las referencias a variables correspondientes de la relación resultado.
    INSERT INTO shoelace_log SELECT 
           shoelace_data.sl_name, 
	   6,
           getpgusername(), datetime('now'::text)
      FROM shoelace_data shoelace_data, shoelace_data *NEW*,
           shoelace_data *OLD*, shoelace_log shoelace_log
     WHERE int4ne(6, *OLD*.sl_avail)
       AND bpchareq(shoelace_data.sl_name, 'sl7');
El paso 5 reemplaza las referencias OLD por referencias en la relación resultado.
    INSERT INTO shoelace_log SELECT 
           shoelace_data.sl_name, 6,
           getpgusername(), datetime('now'::text)
      FROM shoelace_data shoelace_data, shoelace_data *NEW*,
           shoelace_data *OLD*, shoelace_log shoelace_log
     WHERE int4ne(6, shoelace_data.sl_avail)
       AND bpchareq(shoelace_data.sl_name, 'sl7');
Y esto es. De modo que la máxima reducción de la salida del sistema de reglas es una lista de dos árboles de traducción que son lo mismo que las instrucciones:
    INSERT INTO shoelace_log SELECT
           shoelace_data.sl_name, 6,
           getpgusername(), 'now'
      FROM shoelace_data
     WHERE 6 != shoelace_data.sl_avail
       AND shoelace_data.sl_name = 'sl7';

    UPDATE shoelace_data SET sl_avail = 6
     WHERE sl_name = 'sl7';
Estas con ejecutadas en este orden y eso es exactamente lo que la regla define. Las sustituciones y las cualificaciones añadidas aseguran que si la query original fuese una
    UPDATE shoelace_data SET sl_color = 'green'
     WHERE sl_name = 'sl7';
no se habría escrito ninguna entrada en la tabla de log, ya que esta vez el árbol de traducción original no contiene una entrada de la lista objetivo para sl_avail, de modo que NEW.sl_avail será reemplazada por shoelace_data.sl_avail resultando en la query adicional
    INSERT INTO shoelace_log SELECT
           shoelace_data.sl_name, 
	   shoelace_data.sl_avail,
           getpgusername(), 'now'
      FROM shoelace_data
     WHERE shoelace_data.sl_avail != 
           shoelace_data.sl_avail
       AND shoelace_data.sl_name = 'sl7';
cuya cualificación nunca será cierta. Una vez que no hay diferencias a nivel de árbol de traducción entre una INSERT ... SELECT, y una INSERT ... VALUES, trabajará también si la query original modificaba multiples columnas. De modo que si Al hubiese pedido el comando
    UPDATE shoelace_data SET sl_avail = 0
     WHERE sl_color = 'black';
serán actualizadas cuatro filas (sl1, sl2, sl3 y sl4). Pero sl3 ya tiene sl_avail = 0. Esta vez, la cualificación de los árboles de traducción originales es diferente y como resultado tenemos el árbol de traducción adicional
    INSERT INTO shoelace_log SELECT
           shoelace_data.sl_name, 0,
           getpgusername(), 'now'
      FROM shoelace_data
     WHERE 0 != shoelace_data.sl_avail
       AND shoelace_data.sl_color = 'black';
Este árbol de traducción seguramente insertará tres nuevas entradas de la tabla de log. Y eso es absolútamente correcto.

Es importante recordar que el árbol de traducción original se ejecuta el último. El "agente de tráfico" de Postgres incrementa el contador de comandos entre la ejecución de los dos árboles de traducción, de modo que el segundo puede ver cambios realizados por el primero. Si la UPDATE hubiera sido ejecutada primero, todas las filas estarían ya a 0, de modo que la INSERT del logging no habría encontrado ninguna fila para las que shoelace_data.sl_avail != 0: no habría dejado ningún rastro.

Cooperación con las vistas

Una forma sencilla de proteger las relaciones vista de la mencionada posibilidad de que alguien pueda INSERT, UPDATE y DELETE datos invisibles es permitir a sus árboles de traducción recorrerlas de nuevo. Creamos las reglas

    CREATE RULE shoe_ins_protect AS ON INSERT TO shoe
        DO INSTEAD NOTHING;
    CREATE RULE shoe_upd_protect AS ON UPDATE TO shoe
        DO INSTEAD NOTHING;
    CREATE RULE shoe_del_protect AS ON DELETE TO shoe
        DO INSTEAD NOTHING;
Si Al ahora intenta hacer cualquiera de estas operaciones en la relación vista shoe, el sistema de reglas aplicará las reglas. Una vez que las reglas no tiene acciones y son INSTEAD, la lista resultante de árboles de traducción estará vacía, y la query no devolverá nada, debido a que no hay nada para ser optimizado o ejecutado tras la actuación del sistema de reglas.

Note: Este hecho debería irritar a las aplicaciones cliente, ya que no ocurre absolútamente nada en la base de datos, y por ello, el servidor no devuelve nada para la query. Ni siquiera un PGRES_EMPTY_QUERY o similar será utilizable en libpq. En psql, no ocurre nada. Esto debería cambiar en el futuro.

Una forma más sofisticada de utilizar el sistema de reglas es crear reglas que reescriban el árbol de traducción en uno que haga la operación correcta en las tablas reales. Para hacer esto en la vista shoelace, crearemos las siguientes reglas:

    CREATE RULE shoelace_ins AS ON INSERT TO shoelace
        DO INSTEAD
        INSERT INTO shoelace_data VALUES (
               NEW.sl_name,
               NEW.sl_avail,
               NEW.sl_color,
               NEW.sl_len,
               NEW.sl_unit);

    CREATE RULE shoelace_upd AS ON UPDATE TO shoelace
        DO INSTEAD
        UPDATE shoelace_data SET
               sl_name = NEW.sl_name,
               sl_avail = NEW.sl_avail,
               sl_color = NEW.sl_color,
               sl_len = NEW.sl_len,
               sl_unit = NEW.sl_unit
         WHERE sl_name = OLD.sl_name;

    CREATE RULE shoelace_del AS ON DELETE TO shoelace
        DO INSTEAD
        DELETE FROM shoelace_data
         WHERE sl_name = OLD.sl_name;
Ahora llega un paquete de cordones de zapatos a la tienda de Al, y el tiene una gran lista de artículos. Al no es particularmente bueno haciendo cálculos, y no lo queremos actualizando manualmente la vista shoelace. En su lugar, creamos dos tablas pequeñas, una donde él pueda insertar los datos de la lista de artículos, y otra con un truco especial. Los comandos CREATE completos son:
    CREATE TABLE shoelace_arrive (
        arr_name    char(10),
        arr_quant   integer
    );

    CREATE TABLE shoelace_ok (
        ok_name     char(10),
        ok_quant    integer
    );

    CREATE RULE shoelace_ok_ins AS ON INSERT TO shoelace_ok
        DO INSTEAD
        UPDATE shoelace SET
               sl_avail = sl_avail + NEW.ok_quant
         WHERE sl_name = NEW.ok_name;
ahora Al puede sentarse y hacer algo como:
    al_bundy=> SELECT * FROM shoelace_arrive;
    arr_name  |arr_quant
    ----------+---------
    sl3       |       10
    sl6       |       20
    sl8       |       20
    (3 rows)
Que es exactametne lo que había en la lista de artículos. Daremos una rápida mirada en los datos actuales.
    al_bundy=> SELECT * FROM shoelace ORDER BY sl_name;
    sl_name   |sl_avail|sl_color  |sl_len|sl_unit |sl_len_cm
    ----------+--------+----------+------+--------+---------
    sl1       |       5|black     |    80|cm      |       80
    sl2       |       6|black     |   100|cm      |      100
    sl7       |       6|brown     |    60|cm      |       60
    sl3       |       0|black     |    35|inch    |     88.9
    sl4       |       8|black     |    40|inch    |    101.6
    sl8       |       1|brown     |    40|inch    |    101.6
    sl5       |       4|brown     |     1|m       |      100
    sl6       |       0|brown     |   0.9|m       |       90
    (8 rows)
trasladamos los cordones recien llegados:
    al_bundy=> INSERT INTO shoelace_ok SELECT * FROM shoelace_arrive;
y comprobamos los resultados:
    al_bundy=> SELECT * FROM shoelace ORDER BY sl_name;
    sl_name   |sl_avail|sl_color  |sl_len|sl_unit |sl_len_cm
    ----------+--------+----------+------+--------+---------
    sl1       |       5|black     |    80|cm      |       80
    sl2       |       6|black     |   100|cm      |      100
    sl7       |       6|brown     |    60|cm      |       60
    sl4       |       8|black     |    40|inch    |    101.6
    sl3       |      10|black     |    35|inch    |     88.9
    sl8       |      21|brown     |    40|inch    |    101.6
    sl5       |       4|brown     |     1|m       |      100
    sl6       |      20|brown     |   0.9|m       |       90
    (8 rows)

    al_bundy=> SELECT * FROM shoelace_log;
    sl_name   |sl_avail|log_who|log_when                        
    ----------+--------+-------+--------------------------------
    sl7       |       6|Al     |Tue Oct 20 19:14:45 1998 MET DST
    sl3       |      10|Al     |Tue Oct 20 19:25:16 1998 MET DST
    sl6       |      20|Al     |Tue Oct 20 19:25:16 1998 MET DST
    sl8       |      21|Al     |Tue Oct 20 19:25:16 1998 MET DST
    (4 rows)
Esta es una larga vía desde la primera INSERT ... SELECT a estos resultados. Y su descripción será la última en este documento (pero no el último ejemplo :-). Primero estaba la salida de los traductores:
    INSERT INTO shoelace_ok SELECT
           shoelace_arrive.arr_name, shoelace_arrive.arr_quant
      FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok;
Ahora se aplica la primera regla 'shoelace_ok_in' y se vuelve:
    UPDATE shoelace SET
           sl_avail = int4pl(shoelace.sl_avail, shoelace_arrive.arr_quant)
      FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok,
           shoelace_ok *OLD*, shoelace_ok *NEW*,
           shoelace shoelace
     WHERE bpchareq(shoelace.sl_name, showlace_arrive.arr_name);
y lanza otra vez la INSERT original sobre shoelace_ok. Esta query reescrita se pasa al sistema de reglas de nuevo, y la aplicación de la segunda regla 'shoelace_upd' produce
    UPDATE shoelace_data SET
           sl_name = shoelace.sl_name,
           sl_avail = int4pl(shoelace.sl_avail, shoelace_arrive.arr_quant),
           sl_color = shoelace.sl_color,
           sl_len = shoelace.sl_len,
           sl_unit = shoelace.sl_unit
      FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok,
           shoelace_ok *OLD*, shoelace_ok *NEW*,
           shoelace shoelace, shoelace *OLD*,
           shoelace *NEW*, shoelace_data showlace_data
     WHERE bpchareq(shoelace.sl_name, showlace_arrive.arr_name)
       AND bpchareq(shoelace_data.sl_name, shoelace.sl_name);
Otra vez es una regla INSTEAD, y el árbol de traducción anterior se deshecha. Nótese que esta query aún utiliza la vista shoelace. Pero el sistema de reglas no ha terminado con esta vuelta, de modo que continúa y aplica la regla '_RETshoelace', produciendo
    UPDATE shoelace_data SET
           sl_name = s.sl_name,
           sl_avail = int4pl(s.sl_avail, shoelace_arrive.arr_quant),
           sl_color = s.sl_color,
           sl_len = s.sl_len,
           sl_unit = s.sl_unit
      FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok,
           shoelace_ok *OLD*, shoelace_ok *NEW*,
           shoelace shoelace, shoelace *OLD*,
           shoelace *NEW*, shoelace_data showlace_data,
           shoelace *OLD*, shoelace *NEW*,
           shoelace_data s, unit u
     WHERE bpchareq(s.sl_name, showlace_arrive.arr_name)
       AND bpchareq(shoelace_data.sl_name, s.sl_name);
De nuevo se ha aplicado una regla de update y por ello vuelve a girar la rueda, y llegamos a la ronda de reescritura número 3. Esta vez, se aplica la regla 'log_shoelace', que produce el árbol de traducción extra
    INSERT INTO shoelace_log SELECT
           s.sl_name,
           int4pl(s.sl_avail, shoelace_arrive.arr_quant),
           getpgusername(),
           datetime('now'::text)
      FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok,
           shoelace_ok *OLD*, shoelace_ok *NEW*,
           shoelace shoelace, shoelace *OLD*,
           shoelace *NEW*, shoelace_data showlace_data,
           shoelace *OLD*, shoelace *NEW*,
           shoelace_data s, unit u,
           shoelace_data *OLD*, shoelace_data *NEW*
           shoelace_log shoelace_log
     WHERE bpchareq(s.sl_name,  showlace_arrive.arr_name)
       AND bpchareq(shoelace_data.sl_name, s.sl_name);
       AND int4ne(int4pl(s.sl_avail, shoelace_arrive.arr_quant),
                                                    s.sl_avail);
Tras de lo cual, el sistema de reglas se desconecta y devuelve los árboles de traducción generados. De esta forma, terminamos con dos árboles de traducción finales que son iguales a las instrucciones de SQL
    INSERT INTO shoelace_log SELECT
           s.sl_name,
           s.sl_avail + shoelace_arrive.arr_quant,
           getpgusername(),
           'now'
      FROM shoelace_arrive shoelace_arrive, shoelace_data shoelace_data,
           shoelace_data s
     WHERE s.sl_name = shoelace_arrive.arr_name
       AND shoelace_data.sl_name = s.sl_name
       AND s.sl_avail + shoelace_arrive.arr_quant != s.sl_avail;
           
    UPDATE shoelace_data SET
           sl_avail = shoelace_data.sl_avail + shoelace_arrive.arr_quant
     FROM shoelace_arrive shoelace_arrive,
          shoelace_data shoelace_data,
          shoelace_data s
    WHERE s.sl_name = shoelace_arrive.sl_name
      AND shoelace_data.sl_name = s.sl_name;
El resultado es que los datos vienen de una relación, se insertan en otra, cambian por actualizaciones una tercera, cambian por actualizaciones una cuarta, más registran esa actualización final en una quinta: todo eso se reduce a dos queries.

Hay un pequeño detalle un tanto desagradable. Mirando en las dos queries, descrubrimos que la relación shoelace_data aparece dos veces en la tabla de rango, lo que se debería reducir a una sóla. El optimizador no manipula esto, y por ello el plan de ejecución para la salida del sistema de reglas de la INSERT será

Nested Loop
  ->  Merge Join
        ->  Seq Scan
              ->  Sort
                    ->  Seq Scan on s
        ->  Seq Scan
              ->  Sort
                    ->  Seq Scan on shoelace_arrive
  ->  Seq Scan on shoelace_data
mientras que omitiendo la entrada extra a la tabla de rango debería ser
Merge Join
  ->  Seq Scan
        ->  Sort
              ->  Seq Scan on s
  ->  Seq Scan
        ->  Sort
              ->  Seq Scan on shoelace_arrive
que produce exactamente las mismas entradas en la relación de log. Es decir, el sistema de reglás ha probocado un barrido extra de la relación shoelace_data absolútamente innecesario. Y el mismo barrido obsoleto se produce de nuevo en la UPDATE. Pero era un trabajo realmente duro hacer que todo sea posible.

Una demostración final del sistema de reglas de Postgres y de su poder. Hay una astuta rubia que vende cordones de zapatos. Y lo que Al nunca hubiese imaginado, ella no es sólo astuta, también es elegante, un poco demasiado elegante. Por ello, ella se empeña de tiempo en tiempo en que Al pida cordones que son absolútamente invendibles. Esta vez ha pedido 1000 pares de cordones magenta, y aunque ahora no es posible adquirir otro color, como él se comprometió a comprar algo, prepara su base de datos para cordones rosa.

    al_bundy=> INSERT INTO shoelace VALUES 
    al_bundy->     ('sl9', 0, 'pink', 35.0, 'inch', 0.0);
    al_bundy=> INSERT INTO shoelace VALUES 
    al_bundy->     ('sl10', 1000, 'magenta', 40.0, 'inch', 0.0);
Ahora quiere revisar los cordones que no casan con ningún par de zapatos. El podría realizar una complicada query cada vez, o bien le podemos preparar una vista al efecto:
    CREATE VIEW shoelace_obsolete AS
        SELECT * FROM shoelace WHERE NOT EXISTS
            (SELECT shoename FROM shoe WHERE slcolor = sl_color);
cuya salida es
    al_bundy=> SELECT * FROM shoelace_obsolete;
    sl_name   |sl_avail|sl_color  |sl_len|sl_unit |sl_len_cm
    ----------+--------+----------+------+--------+---------
    sl9       |       0|pink      |    35|inch    |     88.9
    sl10      |    1000|magenta   |    40|inch    |    101.6
Sobre los 1000 cordones magenta, deberíamos avisar a Al antes de que podamos hacerlo de nuevo, pero ese es otro problema. La entrada rosa, la borramos. Para hacerlo un poco más dificil para Postgres, no la borramos directamente. En su lugar, crearemos una nueva vista
    CREATE VIEW shoelace_candelete AS
        SELECT * FROM shoelace_obsolete WHERE sl_avail = 0;
Y lo haremos de esta forma:
    DELETE FROM shoelace WHERE EXISTS
        (SELECT * FROM shoelace_candelete
                 WHERE sl_name = shoelace.sl_name);
Voila:
    al_bundy=> SELECT * FROM shoelace;
    sl_name   |sl_avail|sl_color  |sl_len|sl_unit |sl_len_cm
    ----------+--------+----------+------+--------+---------
    sl1       |       5|black     |    80|cm      |       80
    sl2       |       6|black     |   100|cm      |      100
    sl7       |       6|brown     |    60|cm      |       60
    sl4       |       8|black     |    40|inch    |    101.6
    sl3       |      10|black     |    35|inch    |     88.9
    sl8       |      21|brown     |    40|inch    |    101.6
    sl10      |    1000|magenta   |    40|inch    |    101.6
    sl5       |       4|brown     |     1|m       |      100
    sl6       |      20|brown     |   0.9|m       |       90
    (9 rows)
Una DELETE en una vista, con una subselect como cualificación, que en total utiliza 4 vistas anidadas/cruzadas, donde una de ellas mismas tiene una subselect de cualificación conteniendo una vista y donde se utilizan columnas calculadas queda reescrita en un único árbol de traducción que borra los datos requeridos de una tabla real.

Pienso que hay muy pocas ocasiones en el mundo real en las que se una construcción similar sea necesaria. Pero me tranquiliza un poco que esto funcione.

La verdad es: Haciendo esto encontré otro bug mientras escribía este documento. Pero tras fijarlo comprobé un poco avergonzado que trabajaba correctamente.