Capítulo 21 de 23 8 secciones 19 min

Compartir

Vistas, disparadores y lo que SQLite no tiene

Consultas con nombre, código que corre solo, y los procedimientos almacenados que en uno de los cuatro motores no existen.

Una vista es una consulta con nombre. No guarda datos, siempre está al día, y la uso para que una definición signifique lo mismo en todos los reportes, que es más importante de lo que suena. Un disparador es código que la base ejecuta sola cuando algo cambia, con OLD y NEW a mano. Y la diferencia más grande del libro: SQLite no tiene procedimientos almacenados, así que esa lógica se va a tu programa.

Hasta aquí tus consultas viven en tu archivo, en tu laptop. Este capítulo va de lo contrario: cosas que se guardan dentro de la base y que están ahí para todo el mundo, corra la consulta quien la corra 🏠

Son tres, y en SQLite solo hay dos, que es la sorpresa del capítulo.

Y una pregunta que vale más de lo que parece: ¿en tu equipo todos calculan "cliente activo" igual? Si cada quien lo escribe a su manera, salen tres números para la misma pregunta, y de eso va la primera mitad de este capítulo 🏷️

Una vista es una consulta con nombre

En el capítulo 7 escribimos el resumen por ciudad. Es de esas consultas que pides una vez y después la quiere todo el mundo, y cada persona la reescribe un poquito distinta y salen tres números distintos para la misma pregunta 🫠

CREATE VIEW ventas_por_ciudad AS
SELECT c.ciudad,
       COUNT(DISTINCT c.id) AS clientes,
       COUNT(p.id)          AS pedidos,
       ROUND(SUM(p.monto), 2) AS soles
FROM clientes c
LEFT JOIN pedidos p ON p.id_cliente = c.id
GROUP BY c.ciudad;
SELECT * FROM ventas_por_ciudad ORDER BY soles DESC;
ciudad    clientes  pedidos  soles
--------  --------  -------  ---------
Chiclayo  25        212      115979.43
Piura     24        167      107165.17
Arequipa  22        168      101432.51
Cusco     17        112      64966.23
Trujillo  17        113      64150.66
Lima      15        104      63748.53

A partir de ahora ventas_por_ciudad se usa como si fuera una tabla, y nadie más tiene que acordarse del LEFT JOIN ni del COUNT(DISTINCT).

SELECT ciudad, soles FROM ventas_por_ciudad WHERE soles > 100000 ORDER BY soles DESC;
ciudad    soles
--------  ---------
Chiclayo  115979.43
Piura     107165.17
Arequipa  101432.51

Una vista no guarda datos. Es un apodo: cada vez que la consultas, la base pega tu consulta encima de la de dentro y ejecuta todo junto. Por eso siempre está al día, y por eso no es más rápida que escribir la consulta a mano.

Para qué la uso yo, que es la parte que casi nunca te cuentan:

  • 📐 Para que "cliente activo" signifique lo mismo en los cinco reportes. La definición vive en un sitio.
  • 🧼 Para esconder la suciedad. Una vista que ya venga con el UPPER(TRIM(...)) del capítulo 5 y nadie tenga que acordarse.
  • 🔐 Para dar acceso a una parte. Le das permiso a la vista y no a la tabla, y esa persona ve las columnas que le tocan y ninguna más.
CREATE VIEW clientes_limpios AS
SELECT id, UPPER(TRIM(nombre)) AS nombre, ciudad, segmento, fecha_alta
FROM clientes;
SELECT id, nombre FROM clientes_limpios WHERE id IN (1, 12, 26) ORDER BY id;
id  nombre
--  ---------------------
1   MINIMARKET EL SOL 001
12  MARKET CENTRAL 012
26  MINIMARKET EL SOL 026

Los dieciocho nombres sucios del capítulo 5, arreglados de una vez y para todos, sin tocar la tabla original. Esa es mi vista favorita de este libro 🧽

Lo que no se puede hacer con una vista es escribir a través de ella.

INSERT INTO clientes_limpios (id, nombre, ciudad, segmento, fecha_alta)
VALUES (200, 'NUEVA', 'Lima', 'Bodega', '2026-06-24');
OperationalError: cannot modify clientes_limpios because it is a view

¿Se puede escribir a través de una vista?

PostgreSQLsí si la vista es simple; si no, con un trigger INSTEAD OF
MySQLsí si la vista es simple, o sea sin GROUP BY, DISTINCT ni funciones raras
SQL Serversí si es simple, o con un trigger INSTEAD OF
SQLitenunca directamente: siempre hace falta un trigger INSTEAD OF

Que una vista "simple" sea escribible y una complicada no, y que dónde está la raya lo decida cada motor, es de las cosas que hacen que yo trate las vistas como solo lectura y listo.

SELECT name FROM sqlite_master WHERE type = 'view' ORDER BY name;
name
-----------------
clientes_limpios
ventas_por_ciudad
DROP VIEW clientes_limpios;
SELECT COUNT(*) AS vistas FROM sqlite_master WHERE type = 'view';
vistas
------
1

La vista que recorta filas: quién puede ver qué

Hasta acá la vista sirvió para no repetir una consulta. Tiene otro uso que es de seguridad y que vale más: dar acceso a una parte de la tabla en vez de a la tabla.

El caso típico. La jefa de la zona norte tiene que ver sus pedidos y no los de las otras zonas. La respuesta perezosa es darle acceso a pedidos y pedirle que filtre. La respuesta correcta es que no pueda ver lo demás:

CREATE VIEW pedidos_lima AS
SELECT p.*
FROM pedidos p
JOIN clientes c ON c.id = p.id_cliente
WHERE c.ciudad = 'Lima';

SELECT (SELECT COUNT(*) FROM pedidos)      AS todos,
       (SELECT COUNT(*) FROM pedidos_lima) AS solo_lima;
todos  solo_lima
-----  ---------
900    104

Novecientos contra ciento cuatro. A quien use la vista le existen ciento cuatro pedidos, y los otros no es que estén prohibidos: es que no aparecen 🔒

Y ahí está la diferencia con pedir que filtre, que no es de comodidad: un filtro se olvida, y una consulta con un WHERE mal escrito devuelve de más sin avisar. Lo que no se puede ver no se puede olvidar de filtrar.

Cómo se hace esto en serio, fuera de SQLite

La vista de arriba tiene la ciudad escrita a mano, así que harían falta seis vistas para seis zonas. Las bases grandes lo resuelven con una regla sola que se aplica sobre la tabla y se evalúa según quién pregunta. Se llama seguridad a nivel de fila, o Row Level Security, y en PostgreSQL se ve así:

-- Esto es de PostgreSQL y no corre en SQLite, que no lo trae.
ALTER TABLE pedidos ENABLE ROW LEVEL SECURITY;

CREATE POLICY zona_propia ON pedidos
  USING (id_cliente IN (SELECT id FROM clientes
                        WHERE ciudad = current_setting('app.zona')));

Una regla, todas las zonas, y aplicada en la base y no en cada consulta. La gracia es esa: si la regla vive en la base, da igual por dónde entren, por la aplicación, por un reporte o por alguien con acceso directo. Si vive en el código de la aplicación, cada camino nuevo hay que acordarse de protegerlo.

Es la misma lección del capítulo 13 vista desde el otro lado. Allá el problema era una consulta que hacía de más; acá es una consulta que devuelve de más. Las dos se arreglan igual, quitando la posibilidad en vez de confiando en que nadie se equivoque 🔒

Comparación entre lo que vive en tu archivo de consultas, donde cada quien tiene su versión, y lo que vive dentro de la base: vistas, disparadores y procedimientos almacenados, que en SQLite no existen, y que significan lo mismo para todo el mundo.
La pregunta no es cuál es más potente, es dónde tiene que vivir la definición. Si tres personas calculan cliente activo cada una en su archivo, vas a tener tres números para la misma pregunta.

La vista que sí guarda datos

Como una vista se recalcula entera cada vez, si por dentro tiene un GROUP BY sobre diez millones de filas, cada persona que abra el dashboard paga ese GROUP BY. Para eso existen las vistas materializadas, que sí guardan el resultado.

CREATE MATERIALIZED VIEW ventas_cache AS SELECT 1;
OperationalError: near "MATERIALIZED": syntax error

Vistas materializadas

PostgreSQLCREATE MATERIALIZED VIEW y REFRESH MATERIALIZED VIEW cuando quieras
MySQLno las tiene: se hace con una tabla normal y un proceso que la rellena
SQL Servervistas indexadas: CREATE VIEW ... WITH SCHEMABINDING y un índice único encima
SQLiteno las tiene

La de PostgreSQL se refresca cuando tú digas, así que el dato puede estar viejo y hay que decidir cada cuánto. La de SQL Server se mantiene sola en cada escritura, que es más cómodo y más caro. En MySQL y SQLite lo haces a mano con INSERT INTO ... SELECT del capítulo 10.

Los disparadores

Un disparador es código que la base ejecuta sola cuando pasa algo. Nadie lo llama: se dispara.

El caso que de verdad vale la pena es la auditoría: quiero saber quién cambió un precio y cuándo, sin depender de que el programa que lo cambió se acuerde de anotarlo.

CREATE TABLE precios (
    id_producto INTEGER PRIMARY KEY,
    precio      REAL NOT NULL
);
CREATE TABLE precios_historial (
    id           INTEGER PRIMARY KEY,
    id_producto  INTEGER NOT NULL,
    precio_viejo REAL,
    precio_nuevo REAL,
    cuando       TEXT NOT NULL
);
CREATE TRIGGER tr_precios_cambio
AFTER UPDATE OF precio ON precios
FOR EACH ROW
BEGIN
    INSERT INTO precios_historial (id_producto, precio_viejo, precio_nuevo, cuando)
    VALUES (OLD.id_producto, OLD.precio, NEW.precio, '2026-06-24');
END;
INSERT INTO precios (id_producto, precio) SELECT id, precio FROM productos WHERE id <= 3;
SELECT * FROM precios ORDER BY id_producto;
id_producto  precio
-----------  ------
1            7.08
2            15.92
3            59.12

Tres productos cargados y ni una fila en el historial, porque el disparador solo mira los UPDATE. Ahora sube los precios.

UPDATE precios SET precio = precio * 1.10 WHERE id_producto <= 2;
SELECT id_producto, ROUND(precio_viejo, 2) AS antes, ROUND(precio_nuevo, 2) AS despues, cuando
FROM precios_historial ORDER BY id;
id_producto  antes  despues  cuando
-----------  -----  -------  ----------
1            7.08   7.79     2026-06-24
2            15.92  17.51    2026-06-24

Yo no escribí ningún INSERT en el historial y ahí están las dos filas 🪄

Las piezas del disparador, que son las mismas en los cuatro motores aunque se escriban distinto:

  • ⏱️ AFTER o BEFORE: si corre después o antes del cambio.
  • 🎬 El evento: INSERT, UPDATE o DELETE. Y se puede afinar a una columna, como aquí con UPDATE OF precio.
  • 🔁 FOR EACH ROW: se ejecuta una vez por fila cambiada, no una vez por sentencia.
  • 👯 OLD y NEW: la fila como estaba y como queda. En un INSERT solo hay NEW; en un DELETE, solo OLD.

Y la otra cosa que hacen bien los disparadores: impedir algo.

CREATE TRIGGER tr_precio_no_negativo
BEFORE INSERT ON precios
FOR EACH ROW
WHEN NEW.precio < 0
BEGIN
    SELECT RAISE(ABORT, 'el precio no puede ser negativo');
END;
INSERT INTO precios (id_producto, precio) VALUES (99, -5);
IntegrityError: el precio no puede ser negativo

El mensaje es el mío, escrito por mí. Ese WHEN es la condición para que el disparador se moleste siquiera, y con precio positivo ni se entera.

INSERT INTO precios (id_producto, precio) VALUES (99, 5);
SELECT COUNT(*) AS filas FROM precios;
filas
-----
4

Aunque, siendo honesta, para este caso concreto un CHECK (precio >= 0) del capítulo 10 es más simple, más rápido y se lee mejor. Antes de escribir un disparador, pregúntate si un CHECK, un DEFAULT o una clave foránea hacen el trabajo, porque casi siempre sí 💛

Escribir un disparador

PostgreSQLel cuerpo va en una FUNCTION aparte y el TRIGGER la llama: son dos objetos, siempre
MySQLel cuerpo va dentro, y hace falta cambiar el DELIMITER para poder escribirlo
SQL Servertrabaja con las tablas INSERTED y DELETED, no con OLD y NEW fila a fila
SQLiteel cuerpo va dentro, entre BEGIN y END, con OLD y NEW

Lo de SQL Server cambia la cabeza y no solo la sintaxis: sus disparadores se ejecutan una vez por SENTENCIA y te dan dos tablas con todas las filas afectadas, así que el código que escribas ahí es un UPDATE de conjunto y no un "para cada fila".

Abortar desde un disparador con tu propio mensaje

PostgreSQLRAISE EXCEPTION 'el precio no puede ser negativo'
MySQLSIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'el precio no puede ser negativo'
SQL ServerTHROW 50000, 'el precio no puede ser negativo', 1
SQLiteSELECT RAISE(ABORT, 'el precio no puede ser negativo')

Cuatro formas distintas de decir la misma frase. La de MySQL es la que nadie se aprende: ese 45000 es el código de "error definido por el usuario" y hay que ponerlo tal cual.

SELECT name, tbl_name FROM sqlite_master WHERE type = 'trigger' ORDER BY name;
name                   tbl_name
---------------------  --------
tr_precio_no_negativo  precios
tr_precios_cambio      precios
DROP TRIGGER tr_precio_no_negativo;
SELECT COUNT(*) AS disparadores FROM sqlite_master WHERE type = 'trigger';
disparadores
------------
1

Y el aviso que va con los disparadores, que es serio: un disparador es código que corre sin que nadie lo llame y que no se ve en ninguna consulta. Cuando alguien pase tres días buscando por qué una tabla cambia sola, va a ser esto. Ponles nombres que se entiendan, apúntalos en algún sitio y úsalos poco.

Y los procedimientos, que en SQLite no existen

CREATE PROCEDURE resumen_canal()
BEGIN
    SELECT canal, COUNT(*) FROM pedidos GROUP BY canal;
END;
OperationalError: near "PROCEDURE": syntax error
CREATE FUNCTION doble(x INT) RETURNS INT RETURN x * 2;
OperationalError: near "FUNCTION": syntax error

Ninguna de las dos. SQLite no tiene procedimientos almacenados ni funciones definidas por el usuario en SQL, y no es un olvido: es la decisión de diseño de caber en un archivo y no traer un lenguaje de programación dentro.

En SQLite, esa lógica va en tu programa: en Python, en tu app, donde sea. Y la verdad es que hoy mucha gente lo hace así incluso teniendo motores que sí los soportan, porque el código en un archivo .py se versiona en git, se revisa en un pull request y se prueba; el código dentro de la base, no 🤷‍♀️

Procedimientos almacenados

PostgreSQLCREATE PROCEDURE y CREATE FUNCTION, en PL/pgSQL y hasta en Python
MySQLCREATE PROCEDURE, cambiando antes el DELIMITER
SQL ServerCREATE PROCEDURE en T-SQL, y es donde más se usan de los cuatro
SQLiteNO EXISTEN

Esta es la diferencia más grande de todo el libro: no es que se escriba distinto, es que en uno de los cuatro no está. Si vienes de SQL Server, donde media la lógica de negocio suele vivir en procedimientos, el cambio de cabeza es enorme.

Ejercicios

Siete sobre tu copia. Intenta antes de abrir 💛

1. Tu vista de clientes activos

Una vista con los clientes que han comprado alguna vez, su número de pedidos y sus soles.

CREATE VIEW clientes_activos AS
SELECT c.id, TRIM(c.nombre) AS nombre, c.ciudad, c.segmento,
       COUNT(p.id) AS pedidos,
       ROUND(SUM(p.monto), 2) AS soles
FROM clientes c
JOIN pedidos p ON p.id_cliente = c.id
GROUP BY c.id, c.nombre, c.ciudad, c.segmento;
SELECT COUNT(*) AS activos FROM clientes_activos;
activos
-------
119

119, que son los 120 menos Comercial Rojas 120 del capítulo 7. Y ahora "cliente activo" quiere decir lo mismo para todo el que consulte esta base, que es exactamente para lo que sirve una vista 🌟

2. Consultar la vista como si fuera tabla

Los cinco activos que más gastaron, en Chiclayo.

SELECT nombre, pedidos, soles
FROM clientes_activos
WHERE ciudad = 'Chiclayo'
ORDER BY soles DESC
LIMIT 5;
nombre                  pedidos  soles
----------------------  -------  -------
Market Central 087      12       8148.56
MARKET CENTRAL 078      14       7977.9
Autoservicio Norte 008  14       7494.38
Bodega San Martin 048   14       7328.96
Market Central 064      11       6941.42

Mira lo corta que quedó la consulta. Todo el JOIN y el GROUP BY están escondidos en la vista, y quien pregunta solo tiene que saber filtrar y ordenar.

3. Una vista sobre otra vista

Los activos que están por encima de S/8.000, apoyándote en la vista anterior.

CREATE VIEW clientes_top AS
SELECT * FROM clientes_activos WHERE soles > 8000;
SELECT nombre, ciudad, soles FROM clientes_top ORDER BY soles DESC;
nombre                      ciudad    soles
--------------------------  --------  --------
Distribuidora Paz 058       Lima      10131.72
Almacenes Vega 067          Piura     9516.06
Market Central 087          Chiclayo  8148.56
Restaurante Miraflores 093  Trujillo  8119.95

Sí, se pueden encadenar. Y también es fácil pasarse: cinco vistas una encima de otra son imposibles de depurar cuando algo sale mal, porque el EXPLAIN te muestra la consulta expandida entera y no se parece a nada de lo que escribiste 😵‍💫

4. Un disparador que lleva la cuenta

Una tabla de pedidos nuevos que mantenga sola un contador por canal.

CREATE TABLE pedidos_nuevos (id INTEGER PRIMARY KEY, canal TEXT NOT NULL, monto REAL NOT NULL);
CREATE TABLE contador_canal (canal TEXT PRIMARY KEY, pedidos INTEGER NOT NULL);
CREATE TRIGGER tr_contar_pedido
AFTER INSERT ON pedidos_nuevos
FOR EACH ROW
BEGIN
    INSERT INTO contador_canal (canal, pedidos) VALUES (NEW.canal, 1)
    ON CONFLICT (canal) DO UPDATE SET pedidos = contador_canal.pedidos + 1;
END;
INSERT INTO pedidos_nuevos (canal, monto) VALUES ('Web', 100), ('Web', 200), ('Tienda', 50);
SELECT * FROM contador_canal ORDER BY canal;
canal   pedidos
------  -------
Tienda  1
Web     2

Un disparador con el upsert del capítulo 11 adentro. Web en 2 y Tienda en 1, y yo solo escribí tres INSERT en la otra tabla.

Así se llevan los contadores de verdad en producción, y también así nacen los misterios: dentro de un año, quien vea contador_canal no va a saber de dónde salen esos números 🕵️‍♀️

5. Un disparador que borra en cascada

Al borrar un pedido nuevo, que su contador baje solo.

CREATE TRIGGER tr_descontar_pedido
AFTER DELETE ON pedidos_nuevos
FOR EACH ROW
BEGIN
    UPDATE contador_canal SET pedidos = pedidos - 1 WHERE canal = OLD.canal;
END;
DELETE FROM pedidos_nuevos WHERE canal = 'Web' AND monto = 100;
SELECT * FROM contador_canal ORDER BY canal;
canal   pedidos
------  -------
Tienda  1
Web     1

Aquí NEW no existe porque no hay fila nueva: en un DELETE solo tienes OLD. Y en un INSERT es al revés.

6. El disparador que protege

Impide que nadie deje un monto en cero en pedidos nuevos.

CREATE TRIGGER tr_monto_positivo
BEFORE INSERT ON pedidos_nuevos
FOR EACH ROW
WHEN NEW.monto <= 0
BEGIN
    SELECT RAISE(ABORT, 'un pedido no puede valer cero');
END;
INSERT INTO pedidos_nuevos (canal, monto) VALUES ('Web', 0);
IntegrityError: un pedido no puede valer cero

Y fíjate en algo importante: como es BEFORE, la fila no llegó a entrar, así que el contador del ejercicio 4 tampoco se movió. El orden de los disparadores importa.

7. Escríbelo para los cuatro

Sin ejecutar: la misma vista y el mismo disparador de auditoría, en los cuatro motores.

-- La VISTA se escribe igual en los cuatro. Esta es de las buenas.
CREATE VIEW ventas_por_ciudad AS
SELECT c.ciudad, COUNT(p.id) AS pedidos, SUM(p.monto) AS soles
FROM clientes c LEFT JOIN pedidos p ON p.id_cliente = c.id
GROUP BY c.ciudad;

-- El DISPARADOR, no. PostgreSQL pide una función aparte:
CREATE FUNCTION auditar_precio() RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO precios_historial (id_producto, precio_viejo, precio_nuevo, cuando)
    VALUES (OLD.id_producto, OLD.precio, NEW.precio, CURRENT_DATE);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER tr_precios_cambio AFTER UPDATE OF precio ON precios
FOR EACH ROW EXECUTE FUNCTION auditar_precio();

-- MySQL: el cuerpo va dentro, pero hay que cambiar el DELIMITER
DELIMITER //
CREATE TRIGGER tr_precios_cambio AFTER UPDATE ON precios
FOR EACH ROW
BEGIN
    INSERT INTO precios_historial (id_producto, precio_viejo, precio_nuevo, cuando)
    VALUES (OLD.id_producto, OLD.precio, NEW.precio, CURRENT_DATE);
END//
DELIMITER ;

-- SQL Server: una vez por sentencia, con las tablas INSERTED y DELETED
CREATE TRIGGER tr_precios_cambio ON precios AFTER UPDATE AS
BEGIN
    INSERT INTO precios_historial (id_producto, precio_viejo, precio_nuevo, cuando)
    SELECT d.id_producto, d.precio, i.precio, CAST(GETDATE() AS DATE)
    FROM deleted d JOIN inserted i ON i.id_producto = d.id_producto;
END;

Lo de MySQL merece una nota: ese DELIMITER // existe porque el cliente de MySQL corta las sentencias en el punto y coma, y el cuerpo del disparador tiene punto y comas dentro. Así que hay que decirle "por un rato, el final de sentencia es //". Es puro cliente, no es SQL 🫠

Y mira el de SQL Server: no hay OLD ni NEW, hay un JOIN entre deleted e inserted. Es otra forma de pensarlo, y es la que hay que aprender si trabajas ahí.

El registro que dice que no pasó nada

Antes de cerrar, una trampa de disparadores que da especialmente mal cuerpo, porque el que la sufre es justo quien confía en la auditoría 👻

La trampa

Pones un disparador para llevar el registro de todo lo que cambia en la tabla, que es justo para lo que sirven. Cargas el archivo del mes con INSERT OR REPLACE y vas a mirar el registro.

CREATE TRIGGER t_upd AFTER UPDATE ON clientes
BEGIN INSERT INTO log VALUES ('update'); END;

CREATE TRIGGER t_del AFTER DELETE ON clientes
BEGIN INSERT INTO log VALUES ('delete'); END;

INSERT OR REPLACE INTO clientes VALUES (1, 'Otro nombre', ...);

SELECT * FROM log;
-- (vacío)
Qué está mal

El nombre del cliente cambió y el registro está vacío 👻

REPLACE por dentro borra la fila vieja y mete una nueva, así que uno esperaría que disparara el de DELETE, o el de UPDATE, o los dos. No dispara ninguno: SQLite solo lanza los disparadores de borrado en un REPLACE si tienes encendido PRAGMA recursive_triggers, que también está apagado por defecto.

Y esto es peor que un disparador que no existe, porque un registro vacío se lee como "no cambió nada". Tienes una auditoría que dice que el dato está intacto mientras el dato cambió.

Con datos que alguien va a auditar, prefiero no usar REPLACE nunca y escribir el UPDATE o el ON CONFLICT DO UPDATE a mano. Es una línea más y hace lo que parece que hace, que en una tabla que alguien va a revisar vale mucho más que la línea que te ahorras.

Comprueba que lo tienes

Creas una vista que resume las ventas por ciudad. Al día siguiente entran ventas nuevas. ¿Qué ves al consultar la vista?

  • Los datos nuevos, porque la vista guarda la consulta y no el resultado
  • Los datos de ayer, hasta que la actualices
  • Un error, porque los datos cambiaron
  • Depende de si le pusiste un índice

Lo que te llevas

  • 🏷️ Una vista es una consulta con nombre. No guarda datos, siempre está al día y no acelera nada.
  • 📐 Sirve para que una definición de negocio signifique lo mismo para todos, para esconder la limpieza y para dar acceso a una parte.
  • 💾 Las materializadas sí guardan el resultado: están en PostgreSQL y (a su manera) en SQL Server. En MySQL y SQLite se hacen a mano con una tabla.
  • 🪄 Un disparador corre solo cuando pasa algo, con OLD y NEW a mano. Sirve para auditar y para impedir.
  • 🛑 Antes de escribir uno, mira si un CHECK, un DEFAULT o una clave foránea hacen el trabajo. Casi siempre sí.
  • 👻 Un disparador es código invisible desde la consulta. Nómbralos bien y úsalos poco.
  • 🚫 SQLite no tiene procedimientos almacenados. Esa lógica va en tu programa, que además se versiona y se prueba.

Y si de todo el capítulo te llevas una sola frase, que sea esta:

Una definición que vive en la base significa lo mismo para todo el mundo. Una que vive en tu archivo, no.

Y poner de acuerdo a un equipo en qué significa cada número es la mitad del trabajo de verdad. Es justo lo que hacemos en el Full Day IA, sobre los datos de cada quien 💜

En el capítulo 18 juntamos todo: un análisis completo de la tienda de punta a punta, con las preguntas que de verdad te van a hacer y las consultas que las contestan.

Que tengas lindo día! 🌸

Practica este capítulo 📓

Todo el código de arriba en un cuaderno que corre de principio a fin, y los ejercicios con una celda vacía para que los hagas tú. Se abre en Google Colab de un clic y no hay que instalar nada. Donde veas %%revisa, escribe tu respuesta y el cuaderno te dice si te salió.

¿Prefieres trabajar en tu máquina? Bájate el cuaderno de práctica o el de soluciones. Todos están también en github.com/soymissyera/MissYeraEjercicios.

¿Le sirve a alguien que conoces?

Pásale el libro. Es gratis, está entero y no pide registro 🐣

Instagram y TikTok no dejan compartir enlaces desde la web: esos dos copian la URL para que la pegues en tu historia.

¿Tienes alguna duda o consulta?