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. UnDEFAULT 0en 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
| PostgreSQL | id INT GENERATED ALWAYS AS IDENTITY -- antes se usaba SERIAL |
| MySQL | id INT AUTO_INCREMENT PRIMARY KEY |
| SQL Server | id INT IDENTITY(1,1) PRIMARY KEY |
| SQLite | id 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
| PostgreSQL | TEXT o VARCHAR(n), NUMERIC(10,2), BOOLEAN, DATE |
| MySQL | VARCHAR(n) con n obligatorio, DECIMAL(10,2), BOOLEAN que en realidad es TINYINT(1), DATE |
| SQL Server | NVARCHAR(n) o NVARCHAR(MAX), DECIMAL(10,2), BIT porque no hay BOOLEAN, DATE |
| SQLite | TEXT 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️⃣ Escribe primero el
SELECTcon ese mismoWHEREy mira qué filas salen. - 2️⃣ Si son las que querías, cambia el
SELECTpor elUPDATE. - 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?
| PostgreSQL | siempre, no se pueden apagar |
| MySQL | sí con InnoDB; con el viejo MyISAM se aceptan y se IGNORAN en silencio |
| SQL Server | siempre |
| SQLite | apagadas 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
| PostgreSQL | ALTER TABLE t ALTER COLUMN c TYPE NUMERIC(10,2) |
| MySQL | ALTER TABLE t MODIFY c DECIMAL(10,2) |
| SQL Server | ALTER TABLE t ALTER COLUMN c DECIMAL(10,2) |
| SQLite | no 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
| PostgreSQL | CREATE TABLE nueva AS SELECT ... |
| MySQL | CREATE TABLE nueva AS SELECT ... |
| SQL Server | SELECT ... INTO nueva FROM ... -- no tiene CREATE TABLE AS |
| SQLite | CREATE 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
| PostgreSQL | TRUNCATE TABLE t -- y se puede deshacer dentro de una transacción |
| MySQL | TRUNCATE TABLE t -- NO se puede deshacer |
| SQL Server | TRUNCATE TABLE t |
| SQLite | no 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:
IDENTITYen PostgreSQL y SQL Server (con sintaxis distinta),AUTO_INCREMENTen MySQL, y en SQLite bastaINTEGER PRIMARY KEY. - 🛡️
NOT NULL,DEFAULT,UNIQUEyCHECKse 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.
- 💥
UPDATEyDELETEsinWHEREtocan toda la tabla, sin aviso. Escribe elSELECTprimero. - 🧱
ALTER TABLE ADD COLUMNva igual en los cuatro; cambiar el tipo de una columna no se puede en SQLite. - 🧹
DELETEvacía,TRUNCATEvacía rápido y no está en SQLite,DROPdesaparece 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! 🌸