Capítulo 10 de 15 11 secciones 20 min

Crear tablas y escribir en ellas

CREATE, INSERT, UPDATE, DELETE y los cuatro autoincrementos, con las restricciones que la base sí cumple.

Este es el capítulo donde más se separan los cuatro motores: el autoincremento se llama IDENTITY en PostgreSQL y SQL Server, AUTO_INCREMENT en MySQL, y en SQLite basta con INTEGER PRIMARY KEY. Lo que sí se escribe igual en los cuatro es la estructura: CREATE TABLE, NOT NULL, DEFAULT, UNIQUE, CHECK y REFERENCES. Y ojo con SQLite, que trae las claves foráneas apagadas de fábrica.

Nueve capítulos leyendo. Hoy escribimos 🛠️

Y sí, da un poquito de miedo, porque hasta ahora lo peor que podía pasar era un número mal y ahora lo peor que puede pasar es borrar algo. Por eso este capítulo tiene más avisos que los otros, y por eso el primero es este: todo lo que sigue corre sobre una copia de tienda.db, no sobre la original. Antes de escribir en una base que le importa a alguien, haz una copia. Siempre. Es un Ctrl+C y te salva el trabajo.

Vamos a crear una tabla de promociones, llenarla, cambiarla y borrarla, y en el camino van a saltar siete errores de verdad.

Crear una tabla

CREATE TABLE promociones (
    id          INTEGER PRIMARY KEY,
    nombre      TEXT    NOT NULL,
    descuento   REAL    NOT NULL DEFAULT 0,
    canal       TEXT,
    activa      INTEGER NOT NULL DEFAULT 1,
    creada      TEXT    NOT NULL DEFAULT (DATE('now'))
);
PRAGMA table_info(promociones);
cid  name       type     notnull  dflt_value   pk
---  ---------  -------  -------  -----------  --
0    id         INTEGER  0                     1
1    nombre     TEXT     1                     0
2    descuento  REAL     1        0            0
3    canal      TEXT     0                     0
4    activa     INTEGER  1        1            0
5    creada     TEXT     1        DATE('now')  0

Cada línea de esas es una decisión, y todas se toman una sola vez pero se pagan durante años. Vamos por partes.

  • 🔑 PRIMARY KEY: la columna que identifica la fila. No se repite, no puede ser nula, y en SQLite además se llena sola.
  • 🚫 NOT NULL: aquí no se admite "no se sabe". Después del capítulo 4 sabes bien por qué esto vale oro.
  • 🎁 DEFAULT: qué poner si no dices nada. Un DEFAULT 0 en un descuento es mucho mejor que un nulo, porque un cero se puede sumar.
  • 📛 El orden de las columnas: primero la clave, después lo obligatorio, después lo opcional. Nadie te obliga y se lee muchísimo mejor.

Fíjate en canal, que es la única sin NOT NULL: es a propósito, porque una promo puede valer para todos los canales y ahí el nulo significa algo. Un nulo con significado está bien; un nulo porque nadie lo pensó, no.

La columna que se numera sola

PostgreSQLid INT GENERATED ALWAYS AS IDENTITY -- antes se usaba SERIAL
MySQLid INT AUTO_INCREMENT PRIMARY KEY
SQL Serverid INT IDENTITY(1,1) PRIMARY KEY
SQLiteid INTEGER PRIMARY KEY -- ya se numera sola, sin decir nada

Cuatro palabras distintas para lo mismo, y esta es de las que más se busca. En SQLite, INTEGER PRIMARY KEY (con INTEGER completo, no INT) ya se autoincrementa; la palabra AUTOINCREMENT existe y hace otra cosa que vemos al final del capítulo.

Los tipos, al crear la tabla

PostgreSQLTEXT o VARCHAR(n), NUMERIC(10,2), BOOLEAN, DATE
MySQLVARCHAR(n) con n obligatorio, DECIMAL(10,2), BOOLEAN que en realidad es TINYINT(1), DATE
SQL ServerNVARCHAR(n) o NVARCHAR(MAX), DECIMAL(10,2), BIT porque no hay BOOLEAN, DATE
SQLiteTEXT sin longitud, REAL o NUMERIC, INTEGER 0 y 1 porque no hay BOOLEAN, y tampoco hay DATE

Lo de BOOLEAN es de las cosas que más despistan: solo PostgreSQL tiene uno de verdad. En los otros tres es un número disfrazado, y por eso en tienda.db la columna es_principal es un 0 o un 1 y no un true.

Meter filas

INSERT INTO promociones (nombre, descuento, canal)
VALUES ('Bienvenida bodega', 0.10, 'WhatsApp');
SELECT id, nombre, descuento, canal, activa FROM promociones;
id  nombre             descuento  canal     activa
--  -----------------  ---------  --------  ------
1   Bienvenida bodega  0.1        WhatsApp  1

No puse el id y salió 1: eso es la clave autoincremental trabajando. Tampoco puse activa y salió 1, que es el DEFAULT.

Se pueden meter varias de una vez, y así es muchísimo más rápido que una por una.

INSERT INTO promociones (nombre, descuento, canal) VALUES
    ('Fin de mes', 0.15, 'Web'),
    ('Mayorista 2026', 0.20, NULL),
    ('Recuperacion', 0.25, 'Tienda');
SELECT id, nombre, descuento, canal FROM promociones ORDER BY id;
id  nombre             descuento  canal
--  -----------------  ---------  --------
1   Bienvenida bodega  0.1        WhatsApp
2   Fin de mes         0.15       Web
3   Mayorista 2026     0.2
4   Recuperacion       0.25       Tienda

Ese NULL escrito a mano en Mayorista 2026 es el nulo con significado del que hablábamos: la promo vale para cualquier canal.

INSERT INTO promociones (nombre)
VALUES ('Sin nada mas');
SELECT id, nombre, descuento, canal, activa FROM promociones WHERE nombre = 'Sin nada mas';
id  nombre        descuento  canal  activa
--  ------------  ---------  -----  ------
5   Sin nada mas  0.0               1

Con el nombre solo alcanza, porque todo lo demás tiene DEFAULT o admite nulos. Siempre escribe la lista de columnas entre paréntesis, aunque el motor te deje omitirla: el día que alguien añada una columna en el medio, un INSERT sin lista mete los datos corridos y no da error.

Ahora rompamos algo a propósito.

INSERT INTO promociones (nombre, descuento) VALUES (NULL, 0.5);
IntegrityError: NOT NULL constraint failed: promociones.nombre

NOT NULL constraint failed. Y esa es exactamente la idea 🙌 Una restricción es una promesa que la base te obliga a cumplir, y prefieres mil veces un error al meter el dato que descubrir tres meses después que tienes cuatrocientas filas sin nombre.

Cambiar filas, y el WHERE que hay que escribir primero

UPDATE promociones SET descuento = 0.30 WHERE nombre = 'Recuperacion';
SELECT id, nombre, descuento FROM promociones ORDER BY id;
id  nombre             descuento
--  -----------------  ---------
1   Bienvenida bodega  0.1
2   Fin de mes         0.15
3   Mayorista 2026     0.2
4   Recuperacion       0.3
5   Sin nada mas       0.0

Una fila cambiada. Ahora mira esto, y mira bien porque es el error que más caro sale en toda la vida de alguien que trabaja con datos.

UPDATE promociones SET activa = 0;
SELECT COUNT(*) AS apagadas FROM promociones WHERE activa = 0;
apagadas
--------
5

Cinco. Todas. Se me olvidó el WHERE y apagué la tabla entera, sin error, sin confirmación, sin "¿estás segura?" 🫠

Con cinco filas se arregla; con la tabla de precios de una empresa un viernes a las siete, no. La costumbre que te recomiendo, y que yo uso siempre:

  1. 1️⃣ Escribe primero el SELECT con ese mismo WHERE y mira qué filas salen.
  2. 2️⃣ Si son las que querías, cambia el SELECT por el UPDATE.
  3. 3️⃣ Y si la base tiene transacciones, envuélvelo en una. Eso es el capítulo 11.

DELETE tiene exactamente el mismo peligro y exactamente el mismo remedio.

DELETE FROM promociones WHERE nombre = 'Sin nada mas';
SELECT COUNT(*) AS quedan FROM promociones;
quedan
------
4

El peligro que no es tuyo: la inyección SQL

Todo lo de arriba lo escribes tú, a mano, y sabes qué hace. El problema empieza cuando la consulta se arma con algo que escribió otra persona:

  • Lo que alguien tecleó en un buscador
  • En un formulario
  • En una app

Imagina que tu programa arma la consulta pegando texto:

-- MAL. Nunca. Ni una vez.
consulta = "SELECT * FROM clientes WHERE nombre = '" + lo_que_escribio + "'"

Si alguien escribe x' OR '1'='1, la consulta que llega a la base es WHERE nombre = 'x' OR '1'='1', que es verdadera para todas las filas y devuelve la tabla entera. Con un poco más de imaginación se borran tablas. Eso es una inyección SQL, y lleva veinticinco años siendo de las formas más comunes de reventar un sistema 💀

La solución es una sola y es igual en los cuatro motores: consultas parametrizadas. Se escribe un hueco en el SQL y el valor viaja aparte, así que el motor nunca lo interpreta como código.

# Python con SQLite, y el hueco es la interrogación
cursor.execute("SELECT * FROM clientes WHERE nombre = ?", (lo_que_escribio,))

El símbolo del hueco cambia: ? en SQLite y en MySQL, $1 o %s en PostgreSQL según la librería, y @nombre en SQL Server. Lo que no cambia es la regla: los valores nunca se pegan al texto de la consulta.

Y si escribes SQL solo para analizar, esto igual te toca: el día que armes una consulta con un nombre de columna que viene de una lista desplegable, ya estás en este terreno 🕵️‍♀️

Las otras tres promesas: UNIQUE, CHECK y las claves foráneas

CREATE TABLE canales (
    codigo TEXT PRIMARY KEY,
    nombre TEXT NOT NULL UNIQUE
);
INSERT INTO canales VALUES ('WEB', 'Web'), ('WSP', 'WhatsApp');
INSERT INTO canales VALUES ('WEB2', 'Web');
IntegrityError: UNIQUE constraint failed: canales.nombre

UNIQUE es "este valor no se repite". Es lo que impide tener el mismo canal dos veces con códigos distintos, que es como nacen los reportes donde Web aparece partido en dos filas.

CREATE TABLE cupones (
    id INTEGER PRIMARY KEY,
    codigo TEXT NOT NULL,
    descuento REAL NOT NULL CHECK (descuento > 0 AND descuento <= 0.5)
);
INSERT INTO cupones (codigo, descuento) VALUES ('POLLITO10', 0.10);
INSERT INTO cupones (codigo, descuento) VALUES ('TODOGRATIS', 0.99);
IntegrityError: CHECK constraint failed: descuento > 0 AND descuento <= 0.5

CHECK es una regla de negocio metida en la base: aquí, que ningún cupón pase del 50%. Se escribe igual en los cuatro motores y se usa poquísimo, y es una pena, porque una regla en la base la cumple todo el mundo y una regla en el código la cumple solo el programa que se acordó.

Y ahora la más importante de las tres, que es la que conecta tablas.

CREATE TABLE visitas (
    id INTEGER PRIMARY KEY,
    id_cliente INTEGER NOT NULL REFERENCES clientes(id),
    fecha TEXT NOT NULL
);
INSERT INTO visitas (id_cliente, fecha) VALUES (999999, '2026-06-24');
SELECT id, id_cliente, fecha FROM visitas;
id  id_cliente  fecha
--  ----------  ----------
1   999999      2026-06-24

Y entró 😳

El cliente 999999 no existe. Le puse a la columna un REFERENCES clientes(id), que es justamente la promesa de que ahí solo van clientes que existen, y SQLite la aceptó igual.

PRAGMA foreign_keys;
foreign_keys
------------
0

Cero. SQLite trae las claves foráneas APAGADAS por defecto, y hay que encenderlas en cada conexión. Es la peor sorpresa del motor y viene de que cuando se añadieron, encenderlas de fábrica habría roto las bases que ya existían.

PRAGMA foreign_keys = ON;
INSERT INTO visitas (id_cliente, fecha) VALUES (888888, '2026-06-25');
IntegrityError: FOREIGN KEY constraint failed

Ahora sí. Y acuérdate de esto cuando te preguntes de dónde salen los 24 pedidos sin cliente que llevamos arrastrando desde el capítulo 4: de una base donde la promesa estaba escrita y no estaba encendida 🕳️

Las claves foráneas, ¿se cumplen?

PostgreSQLsiempre, no se pueden apagar
MySQLsí con InnoDB; con el viejo MyISAM se aceptan y se IGNORAN en silencio
SQL Serversiempre
SQLiteapagadas de fábrica: PRAGMA foreign_keys = ON en cada conexión

Los dos de los extremos son los peligrosos y por motivos parecidos: la restricción está escrita, se ve en el CREATE TABLE, y no hace nada. Si heredas una base y ves huérfanos, esto es lo primero que hay que mirar.

Cambiar una tabla que ya existe

ALTER TABLE promociones ADD COLUMN observacion TEXT;
SELECT id, nombre, observacion FROM promociones ORDER BY id LIMIT 2;
id  nombre             observacion
--  -----------------  -----------
1   Bienvenida bodega
2   Fin de mes

Añadir una columna es lo único que se hace igual y sin drama en los cuatro motores. Las filas que ya estaban se quedan con NULL, que es la razón por la que una columna nueva casi nunca puede ser NOT NULL sin darle un DEFAULT.

ALTER TABLE promociones RENAME COLUMN observacion TO nota;
ALTER TABLE promociones DROP COLUMN nota;
SELECT COUNT(*) AS columnas FROM pragma_table_info('promociones');
columnas
--------
6

Volvimos a las seis columnas del principio. Renombrar y borrar columnas llegó tarde a SQLite: RENAME COLUMN en la 3.25 y DROP COLUMN en la 3.35, o sea 2021. En una SQLite anterior, ninguna de las dos existe.

Y hay una cosa que SQLite directamente no sabe hacer.

ALTER TABLE promociones ALTER COLUMN descuento TYPE NUMERIC(4,2);
OperationalError: near "ALTER": syntax error

Cambiarle el tipo a una columna

PostgreSQLALTER TABLE t ALTER COLUMN c TYPE NUMERIC(10,2)
MySQLALTER TABLE t MODIFY c DECIMAL(10,2)
SQL ServerALTER TABLE t ALTER COLUMN c DECIMAL(10,2)
SQLiteno se puede: hay que crear la tabla nueva, copiar los datos y renombrar

Tres sintaxis parecidas y una imposible. El baile de SQLite es CREATE TABLE nueva, INSERT INTO nueva SELECT de la vieja, DROP de la vieja y ALTER TABLE nueva RENAME TO vieja. Se hace mucho y por eso conviene pensar los tipos antes.

Crear una tabla a partir de una consulta

CREATE TABLE clientes_lima AS
SELECT id, nombre, ciudad, segmento
FROM clientes
WHERE ciudad = 'Lima';
SELECT COUNT(*) AS copiados FROM clientes_lima;
copiados
--------
15

Los 15 clientes de Lima, copiados a una tabla nueva en tres líneas. Esto lo uso muchísimo para trabajar tranquila: te haces tu copia, la rompes todo lo que quieras y la borras.

El primo de esto es meter el resultado de una consulta en una tabla que ya existe.

CREATE TABLE resumen_canal (
    canal   TEXT PRIMARY KEY,
    pedidos INTEGER NOT NULL,
    soles   REAL    NOT NULL
);
INSERT INTO resumen_canal (canal, pedidos, soles)
SELECT canal, COUNT(*), ROUND(SUM(monto), 2)
FROM pedidos
GROUP BY canal;
SELECT * FROM resumen_canal ORDER BY soles DESC;
canal        pedidos  soles
-----------  -------  ---------
Web          234      140446.17
WhatsApp     231      137968.56
Marketplace  229      135754.05
Tienda       206      118485.07

INSERT INTO ... SELECT, sin VALUES. Así se arman las tablas de resumen que alimentan un dashboard: se calculan una vez de noche y de día se leen, en vez de rehacer el GROUP BY cada vez que alguien abre el reporte 📊

Crear una tabla con el resultado de un SELECT

PostgreSQLCREATE TABLE nueva AS SELECT ...
MySQLCREATE TABLE nueva AS SELECT ...
SQL ServerSELECT ... INTO nueva FROM ... -- no tiene CREATE TABLE AS
SQLiteCREATE TABLE nueva AS SELECT ...

Tres iguales y SQL Server con lo suyo, que además pone el nombre de la tabla nueva en medio de la consulta y se lee raro la primera vez.

Limpiar y borrar

UPDATE clientes_lima
SET nombre = UPPER(TRIM(nombre));
SELECT id, nombre FROM clientes_lima ORDER BY id LIMIT 3;
id  nombre
--  --------------------------
4   MAYORISTA PERU 004
5   BODEGA LA ESQUINA 005
9   RESTAURANTE MIRAFLORES 009

Ese es el UPDATE que arregla de verdad la suciedad del capítulo 5. Hasta ahora limpiábamos en cada consulta; aquí se limpia una vez y ya. Es mejor y es más peligroso, las dos cosas: si te equivocas de fórmula, el dato original ya no está.

DELETE FROM clientes_lima WHERE segmento = 'Bodega';
SELECT COUNT(*) AS quedan FROM clientes_lima;
quedan
------
12
DELETE FROM clientes_lima;
SELECT COUNT(*) AS quedan FROM clientes_lima;
quedan
------
0

Sin WHERE, DELETE vacía la tabla pero la deja ahí, con sus columnas y sus restricciones. En los otros tres motores hay una forma más rápida de hacer eso mismo.

TRUNCATE TABLE resumen_canal;
OperationalError: near "TRUNCATE": syntax error

Vaciar una tabla entera

PostgreSQLTRUNCATE TABLE t -- y se puede deshacer dentro de una transacción
MySQLTRUNCATE TABLE t -- NO se puede deshacer
SQL ServerTRUNCATE TABLE t
SQLiteno existe: DELETE FROM t

TRUNCATE es más rápido que DELETE porque no va fila por fila, y por eso mismo en MySQL no hay vuelta atrás. Dos motores con la misma palabra y consecuencias distintas es de lo peor que te puede pasar un lunes.

DROP TABLE clientes_lima;
SELECT COUNT(*) AS existe FROM sqlite_master WHERE name = 'clientes_lima';
existe
------
0

DELETE vacía, DROP desaparece. Los tres verbos en una línea para que no se mezclen nunca:

  • 🧹 DELETE FROM t WHERE ...: quita las filas que digas.
  • 🚿 TRUNCATE TABLE t: quita todas, rapidísimo, y no está en SQLite.
  • 💣 DROP TABLE t: quita la tabla.

El AUTOINCREMENT de SQLite, que no es lo que parece

Te dije arriba que INTEGER PRIMARY KEY ya se numera solo y que la palabra AUTOINCREMENT hace otra cosa. Esta es la otra cosa.

CREATE TABLE sin_auto (id INTEGER PRIMARY KEY, cosa TEXT);
INSERT INTO sin_auto (cosa) VALUES ('a'), ('b'), ('c');
DELETE FROM sin_auto WHERE cosa = 'c';
INSERT INTO sin_auto (cosa) VALUES ('d');
SELECT id, cosa FROM sin_auto ORDER BY id;
id  cosa
--  ----
1   a
2   b
3   d
CREATE TABLE conteo (id INTEGER PRIMARY KEY AUTOINCREMENT, cosa TEXT);
INSERT INTO conteo (cosa) VALUES ('a'), ('b'), ('c');
DELETE FROM conteo WHERE cosa = 'c';
INSERT INTO conteo (cosa) VALUES ('d');
SELECT id, cosa FROM conteo ORDER BY id;
id  cosa
--  ----
1   a
2   b
4   d

Sin AUTOINCREMENT, la d se quedó con el 3 que había dejado libre la c. Con AUTOINCREMENT, se fue al 4 y el 3 no vuelve nunca.

¿Y eso importa? Muchísimo, si ese id salió alguna vez de la base: en una factura, en un enlace, en un correo. Reutilizar un identificador significa que dos cosas distintas tuvieron el mismo número en momentos distintos, y desenredar eso es una tarde perdida 🫠

Los otros tres motores nunca reutilizan por defecto, así que si vienes de ellos, esto de SQLite te va a sorprender.

Ejercicios

Siete. Estos escriben, así que van sobre tu copia 💛

1. Tu propia tabla

Crea una tabla de visitas comerciales con id automático, cliente obligatorio, fecha obligatoria, un comentario opcional y un resultado que por defecto sea "pendiente".

CREATE TABLE visitas_comerciales (
    id          INTEGER PRIMARY KEY,
    id_cliente  INTEGER NOT NULL REFERENCES clientes(id),
    fecha       TEXT    NOT NULL,
    comentario  TEXT,
    resultado   TEXT    NOT NULL DEFAULT 'pendiente'
);

No lleva salida porque crear una tabla no devuelve nada: si no dice nada, fue bien. La comprobación es PRAGMA table_info(visitas_comerciales).

Y acuérdate de encender las claves foráneas, o ese REFERENCES es un adorno.

2. El resumen por ciudad, guardado

Arma una tabla con clientes, pedidos y soles por ciudad, calculada con lo del capítulo 7.

CREATE TABLE resumen_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 resumen_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

Ojo con una cosa que CREATE TABLE AS no hace: la tabla nueva no hereda claves ni restricciones, solo los datos y los nombres de columna. Si esa tabla va a vivir, créala a mano y llénala con INSERT INTO ... SELECT.

3. Limpiar los nombres de una vez

Sobre una copia de clientes, arregla los dieciocho nombres sucios del capítulo 5 y comprueba que quedaron cero.

CREATE TABLE clientes_limpios AS SELECT * FROM clientes;
UPDATE clientes_limpios SET nombre = TRIM(nombre) WHERE nombre <> TRIM(nombre);
SELECT COUNT(*) AS todavia_sucios FROM clientes_limpios WHERE nombre <> TRIM(nombre);
todavia_sucios
--------------
0

Ese WHERE nombre <> TRIM(nombre) del UPDATE no hace falta para que funcione, y lo pongo igual por costumbre: así el UPDATE toca 18 filas y no 120, y si algo sale mal el destrozo es más chico.

4. El CHECK que te habría salvado

Crea una tabla de pedidos nuevos donde el monto no pueda ser negativo ni nulo, y compruébalo intentando meter uno de -50.

CREATE TABLE pedidos_nuevos (
    id     INTEGER PRIMARY KEY,
    fecha  TEXT NOT NULL,
    monto  REAL NOT NULL CHECK (monto >= 0)
);
INSERT INTO pedidos_nuevos (fecha, monto) VALUES ('2026-06-24', -50);
IntegrityError: CHECK constraint failed: monto >= 0

Un CHECK de tres palabras que impide para siempre una categoría entera de errores. Si pedidos lo hubiera tenido con NOT NULL, no tendríamos los 27 montos vacíos del capítulo 4.

5. Añadir una columna con valor por defecto

Añádele a tu tabla de promociones una columna de prioridad que valga 3 para todas las que ya existen.

ALTER TABLE promociones ADD COLUMN prioridad INTEGER NOT NULL DEFAULT 3;
SELECT id, nombre, prioridad FROM promociones ORDER BY id;
id  nombre             prioridad
--  -----------------  ---------
1   Bienvenida bodega  3
2   Fin de mes         3
3   Mayorista 2026     3
4   Recuperacion       3

Con DEFAULT, una columna nueva sí puede ser NOT NULL: el motor rellena las filas viejas con el valor por defecto. Sin él, ese mismo ALTER da error en los cuatro motores.

6. El UPDATE con la red puesta

Sube un 5% el descuento de las promos de Web, mirando antes a quién le va a tocar.

SELECT id, nombre, descuento FROM promociones WHERE canal = 'Web';
id  nombre      descuento
--  ----------  ---------
2   Fin de mes  0.15
UPDATE promociones SET descuento = descuento + 0.05 WHERE canal = 'Web';
SELECT id, nombre, descuento FROM promociones WHERE canal = 'Web';
id  nombre      descuento
--  ----------  ---------
2   Fin de mes  0.2

Primero mirar, después cambiar. Es un segundo más y es la diferencia entre un martes normal y un martes de esos.

Y fíjate en el descuento = descuento + 0.05: en un UPDATE, el lado derecho ve el valor viejo. Puedes calcular a partir de lo que ya había, y eso vale para cualquier columna de la misma fila.

7. Escríbelo para los cuatro

Sin ejecutar: la misma tabla de promociones, en los cuatro motores.

-- PostgreSQL
CREATE TABLE promociones (
    id        INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    nombre    VARCHAR(80)  NOT NULL,
    descuento NUMERIC(4,2) NOT NULL DEFAULT 0,
    activa    BOOLEAN      NOT NULL DEFAULT TRUE,
    creada    DATE         NOT NULL DEFAULT CURRENT_DATE
);

-- MySQL
CREATE TABLE promociones (
    id        INT AUTO_INCREMENT PRIMARY KEY,
    nombre    VARCHAR(80)  NOT NULL,
    descuento DECIMAL(4,2) NOT NULL DEFAULT 0,
    activa    BOOLEAN      NOT NULL DEFAULT TRUE,
    creada    DATE         NOT NULL DEFAULT (CURRENT_DATE)
);

-- SQL Server
CREATE TABLE promociones (
    id        INT IDENTITY(1,1) PRIMARY KEY,
    nombre    NVARCHAR(80) NOT NULL,
    descuento DECIMAL(4,2) NOT NULL DEFAULT 0,
    activa    BIT          NOT NULL DEFAULT 1,
    creada    DATE         NOT NULL DEFAULT CAST(GETDATE() AS DATE)
);

-- SQLite
CREATE TABLE promociones (
    id        INTEGER PRIMARY KEY,
    nombre    TEXT    NOT NULL,
    descuento REAL    NOT NULL DEFAULT 0,
    activa    INTEGER NOT NULL DEFAULT 1,
    creada    TEXT    NOT NULL DEFAULT (DATE('now'))
);

Cuatro veces la misma tabla y no hay ni una línea idéntica en las cuatro. Este es el capítulo donde más se separan, y por eso es también donde más se copia mal de internet: buscas "crear tabla SQL", te sale la de MySQL, la pegas en Postgres y no arranca 🫠

Lo que sí es igual en los cuatro: las palabras CREATE TABLE, NOT NULL, DEFAULT, PRIMARY KEY, UNIQUE, CHECK y REFERENCES. La estructura se comparte; los tipos y el autoincremento, no.

Lo que te llevas

  • 💾 Antes de escribir en una base que le importa a alguien, copia. Todo este capítulo corrió sobre una copia.
  • 🔑 El autoincremento se llama distinto en los cuatro: IDENTITY en PostgreSQL y SQL Server (con sintaxis distinta), AUTO_INCREMENT en MySQL, y en SQLite basta INTEGER PRIMARY KEY.
  • 🛡️ NOT NULL, DEFAULT, UNIQUE y CHECK se escriben igual en los cuatro y son las promesas que la base sí cumple.
  • 🕳️ Las claves foráneas vienen APAGADAS en SQLite. Está escrita la promesa y no se cumple, y así nacen los huérfanos.
  • 💥 UPDATE y DELETE sin WHERE tocan toda la tabla, sin aviso. Escribe el SELECT primero.
  • 🧱 ALTER TABLE ADD COLUMN va igual en los cuatro; cambiar el tipo de una columna no se puede en SQLite.
  • 🧹 DELETE vacía, TRUNCATE vacía rápido y no está en SQLite, DROP desaparece la tabla.

En el capítulo 11 vienen las transacciones, que es la red de seguridad de todo lo que acabamos de hacer, y el UPSERT, que es "insértalo, y si ya está, actualízalo". Ahí los cuatro motores se separan otra vez y feo.

Que tengas lindo día! 🌸

¿Tienes alguna duda o consulta?