CREATE TABLE y tipos de datos en SQL
En esta lección
Hasta ahora has trabajado con tablas que ya existían. En esta lección aprenderás a crearlas tú: a elegir el nombre de cada columna, su tipo de dato y su tamaño. Son decisiones importantes, porque cambiar una tabla llena de datos es mucho más costoso que diseñarla bien desde el principio. Al final crearás la base de datos completa de una biblioteca.
Los ejemplos usan la sintaxis de MySQL, y en notas verás qué cambia en PostgreSQL y SQLite.
Crear una base de datos
Un servidor MySQL puede guardar muchas bases de datos (una por aplicación, normalmente). Primero se crea la base de datos y luego se indica que vas a trabajar en ella:
CREATE DATABASE biblioteca
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE biblioteca;
CREATE DATABASEcrea un contenedor vacío.USEla selecciona: a partir de ahí, todas las tablas se crean dentro debiblioteca.CHARACTER SET utf8mb4indica cómo se guardan los textos (lo vemos más abajo).
Nota: en PostgreSQL no existe
USE: te conectas a la base de datos (enpsql, con\c biblioteca). En SQLite, cada base de datos es un archivo y se crea al abrirlo.
CREATE TABLE
La forma general es:
CREATE TABLE nombre_tabla (
columna1 TIPO [opciones],
columna2 TIPO [opciones],
...
);
Un ejemplo sencillo, la tabla de socios de la biblioteca:
CREATE TABLE socios (
id INT AUTO_INCREMENT PRIMARY KEY,
dni CHAR(9) NOT NULL,
nombre VARCHAR(100) NOT NULL,
email VARCHAR(255),
fecha_alta DATE NOT NULL
);
- Cada línea define una columna: nombre, tipo y, opcionalmente, reglas.
- Las columnas se separan con comas. Ojo: la última no lleva coma.
PRIMARY KEYmarca la clave primaria yNOT NULLobliga a rellenar la columna. Estas reglas se llaman restricciones y tienen su propia lección: claves y restricciones.
Tipos de datos en MySQL
El tipo decide qué se puede guardar en la columna y cuánto ocupa. Estos son los que usarás casi siempre:
| Tipo | Qué guarda | Ejemplo de uso |
|---|---|---|
TINYINT | Entero de -128 a 127 (1 byte) | Nota de 0 a 10 |
SMALLINT | Entero de -32 768 a 32 767 (2 bytes) | Año, número de páginas |
INT | Entero de unos ±2 100 millones (4 bytes) | Ids, cantidades |
BIGINT | Entero enorme (8 bytes) | Ids de tablas gigantes, visitas |
DECIMAL(p, s) | Número exacto con p dígitos, s decimales | Precios, saldos |
FLOAT / DOUBLE | Número aproximado (coma flotante) | Medidas científicas, coordenadas |
CHAR(n) | Texto de longitud fija n | DNI, código postal, ISBN |
VARCHAR(n) | Texto de hasta n caracteres | Nombres, emails, títulos |
TEXT | Texto largo (hasta 64 KB) | Descripción, comentario |
DATE | Fecha AAAA-MM-DD | Fecha de nacimiento |
DATETIME | Fecha y hora | Momento de un préstamo |
TIMESTAMP | Fecha y hora guardada en UTC | Fecha de creación de un registro |
BOOLEAN | Verdadero o falso (en MySQL, TINYINT(1)) | ¿Está disponible? |
ENUM('a', 'b') | Un valor de una lista cerrada | Tipo de socio |
Números enteros
Elige el tipo más pequeño que seguro te vaya a bastar. Un INT llega a unos 2 100 millones: suficiente para el id de casi cualquier tabla. BIGINT es para tablas que pueden superar esa cifra (registros de visitas, mensajes de una red social). En MySQL puedes añadir UNSIGNED para admitir solo positivos y duplicar el máximo.
DECIMAL para el dinero
DECIMAL(6, 2) guarda números con 6 dígitos en total, 2 de ellos decimales: de -9999.99 a 9999.99. Los guarda exactos, cifra a cifra.
FLOAT y DOUBLE guardan los números en binario y no pueden representar exactamente muchos decimales, como 0,1. El error es minúsculo, pero se acumula. Si sumas diez veces 0,1 en una columna DOUBLE, MySQL y PostgreSQL dan:
SUM(columna DOUBLE) → 0.9999999999999999
SUM(columna DECIMAL(10,2)) → 1.00
En una tienda, eso son céntimos que no cuadran en la contabilidad. Por eso el dinero siempre va en DECIMAL. FLOAT y DOUBLE son para medidas donde un error en la decimocuarta cifra no importa (temperaturas, coordenadas GPS).
CHAR frente a VARCHAR
CHAR(9)reserva siempre 9 caracteres. Perfecto cuando todos los valores miden lo mismo: DNI, ISBN-13, código de país (ES).VARCHAR(100)guarda solo lo que escribes (más uno o dos bytes para la longitud). Para nombres, emails y títulos, que varían mucho.
El número entre paréntesis es un máximo de caracteres. Si intentas guardar más, MySQL (en modo estricto, el habitual) da error. Ponlo razonable: 100 para un nombre, 255 para un email. No ganas nada poniendo VARCHAR(5000) “por si acaso”; para textos largos existe TEXT.
Fechas y horas
DATEpara fechas sin hora:'2026-09-30'. Siempre en formato año-mes-día.DATETIMEpara fecha y hora:'2026-09-30 17:45:00'. Se guarda tal cual lo escribes.TIMESTAMPtambién guarda fecha y hora, pero la convierte a UTC al guardarla y de vuelta a tu zona horaria al leerla. Es útil en aplicaciones con usuarios de varios países. Su rango acaba en 2038, así que no sirve para fechas lejanas.
Cuidado: no guardes fechas en un
VARCHAR. Con texto no puedes ordenar bien ('10/02/2026'va antes que'9/01/2026'), ni sumar días, ni comprobar que la fecha exista.
BOOLEAN y ENUM
En MySQL, BOOLEAN es un alias de TINYINT(1): TRUE se guarda como 1 y FALSE como 0. PostgreSQL tiene un tipo booleano de verdad que muestra t y f.
ENUM limita una columna a una lista de valores: tipo ENUM('general', 'estudiante', 'jubilado'). Es cómodo, pero añadir un valor nuevo obliga a modificar la tabla. Si la lista puede crecer, suele ser mejor una tabla aparte. ENUM es propio de MySQL; en PostgreSQL se crea con CREATE TYPE.
Ids automáticos
Casi todas las tablas tienen un id numérico que genera la base de datos. Cada gestor lo escribe a su manera:
| Gestor | Sintaxis |
|---|---|
| MySQL / MariaDB | id INT AUTO_INCREMENT PRIMARY KEY |
| PostgreSQL (moderno) | id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY |
| PostgreSQL (clásico) | id SERIAL PRIMARY KEY |
| SQLite | id INTEGER PRIMARY KEY |
Al insertar, simplemente no das valor al id:
INSERT INTO socios (dni, nombre, email, fecha_alta)
VALUES ('12345678Z', 'Irene Sanz', 'irene@correo.es', '2026-09-01');
-- id = 1, el siguiente será 2...
En PostgreSQL, GENERATED ALWAYS es la opción recomendada: si intentas escribir tú el id, da error, lo que evita choques con el contador. En SQLite, la columna tiene que ser exactamente INTEGER PRIMARY KEY (con INT no funciona igual).
IF NOT EXISTS
Si ejecutas dos veces un CREATE TABLE, la segunda da error: la tabla ya existe. Con IF NOT EXISTS, la base de datos simplemente no hace nada si ya está creada:
CREATE TABLE IF NOT EXISTS socios ( ... );
Es muy útil en scripts de instalación que se pueden lanzar más de una vez. Pero ojo: no comprueba que la tabla existente sea igual a la que describes. Si ya había una socios con otras columnas, la deja como está. Existe también CREATE DATABASE IF NOT EXISTS.
CREATE TABLE … AS SELECT
Crea una tabla nueva a partir del resultado de una consulta, con sus datos:
CREATE TABLE libros_antiguos AS
SELECT id, titulo, autor, anio_publicacion
FROM libros
WHERE anio_publicacion < 1970;
SELECT * FROM libros_antiguos;
| id | titulo | autor | anio_publicacion |
|---|---|---|---|
| 1 | Cien años de soledad | Gabriel García Márquez | 1967 |
| 3 | Nada | Carmen Laforet | 1945 |
Sirve para copias rápidas antes de un cambio arriesgado o para tablas de informes. Copia las columnas y los datos, pero no la clave primaria, los índices ni las claves foráneas. Si solo quieres copiar la estructura vacía, en MySQL tienes CREATE TABLE copia LIKE original;.
Ver qué has creado
En MySQL:
SHOW DATABASES; -- lista las bases de datos
SHOW TABLES; -- lista las tablas de la base de datos actual
DESCRIBE socios; -- columnas, tipos y claves de una tabla
SHOW CREATE TABLE socios; -- el CREATE TABLE completo
DESCRIBE socios muestra algo así:
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| dni | char(9) | NO | | NULL | |
| nombre | varchar(100) | NO | | NULL | |
| email | varchar(255) | YES | | NULL | |
| fecha_alta | date | NO | | NULL | |
+------------+--------------+------+-----+---------+----------------+
Nota: en PostgreSQL (
psql) se usa\dtpara listar tablas y\d sociospara describir una. En SQLite,.tablesy.schema socios, o la consultaPRAGMA table_info(socios);.
Juego de caracteres: utf8mb4
El juego de caracteres decide qué símbolos se pueden guardar. En MySQL, usa siempre utf8mb4: admite todos los caracteres de Unicode, desde la ñ y las tildes hasta los emojis.
El nombre confunde: el antiguo utf8 de MySQL (ahora llamado utf8mb3) no es UTF-8 completo. Solo admite caracteres de hasta 3 bytes, y falla con emojis y algunos símbolos. Desde MySQL 8, utf8mb4 es el valor por defecto, pero indícalo al crear la base de datos para no depender de la configuración del servidor.
La colación (COLLATE) decide cómo se comparan y ordenan los textos: si 'a' es igual a 'A' o a 'á'. Las terminadas en _ci (case insensitive) no distinguen mayúsculas.
Nombres de tablas y columnas
No hay una norma oficial, pero estas convenciones te ahorrarán problemas:
- Minúsculas y
snake_case:fecha_alta, noFechaAlta. En MySQL sobre Linux los nombres de tabla distinguen mayúsculas; en Windows no. Todo en minúsculas evita sorpresas. - Sin tildes, ñ ni espacios:
anio_publicacion, noaño publicación. - Tablas en plural (
libros,socios) y columnas en singular. Lo importante es ser coherente. - Clave primaria
id; claves foráneas<tabla en singular>_id:libro_id,socio_id. - Evita palabras reservadas como
order,group,userodesc. Si una columna se llamaorder, tendrás que escribirla entre comillas invertidas (`order`) en MySQL o comillas dobles en PostgreSQL. Mejorordenopedido.
Ejemplo completo: una biblioteca
Una biblioteca con libros, socios y los préstamos que relacionan a unos con otros:
CREATE DATABASE IF NOT EXISTS biblioteca
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE biblioteca;
CREATE TABLE libros (
id INT AUTO_INCREMENT PRIMARY KEY,
isbn CHAR(13) NOT NULL,
titulo VARCHAR(200) NOT NULL,
autor VARCHAR(100) NOT NULL,
anio_publicacion SMALLINT,
paginas SMALLINT,
precio DECIMAL(6, 2),
disponible BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE socios (
id INT AUTO_INCREMENT PRIMARY KEY,
dni CHAR(9) NOT NULL,
nombre VARCHAR(100) NOT NULL,
email VARCHAR(255),
fecha_alta DATE NOT NULL,
tipo ENUM('general', 'estudiante', 'jubilado') NOT NULL DEFAULT 'general'
);
CREATE TABLE prestamos (
id INT AUTO_INCREMENT PRIMARY KEY,
libro_id INT NOT NULL,
socio_id INT NOT NULL,
fecha_prestamo DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
fecha_devolucion DATE,
FOREIGN KEY (libro_id) REFERENCES libros(id),
FOREIGN KEY (socio_id) REFERENCES socios(id)
);
Por qué cada tipo:
isbn CHAR(13): el ISBN-13 siempre tiene 13 cifras. Es texto y no número porque no se opera con él y podría empezar por cero.anio_publicacion SMALLINT: un año cabe de sobra. (El tipoYEARde MySQL solo llega hasta 1901, y hay libros más antiguos.)precio DECIMAL(6, 2): dinero, exacto.fecha_devolucion DATEsinNOT NULL: valeNULLmientras el libro no se ha devuelto.prestamosse crea la última, porque sus claves foráneas apuntan alibrosysocios, que deben existir antes.
Con unos datos de prueba:
INSERT INTO libros (isbn, titulo, autor, anio_publicacion, paginas, precio) VALUES
('9788420471839', 'Cien años de soledad', 'Gabriel García Márquez', 1967, 496, 22.90),
('9788408172178', 'La sombra del viento', 'Carlos Ruiz Zafón', 2001, 576, 19.95),
('9788433973511', 'Nada', 'Carmen Laforet', 1945, 304, 18.50);
INSERT INTO socios (dni, nombre, email, fecha_alta, tipo) VALUES
('12345678Z', 'Irene Sanz', 'irene@correo.es', '2026-09-01', 'estudiante'),
('87654321X', 'Mario Gil', NULL, '2026-09-05', 'general');
INSERT INTO prestamos (libro_id, socio_id, fecha_prestamo) VALUES
(1, 1, '2026-09-10 17:30:00'),
(3, 2, '2026-09-12 11:05:00');
SELECT p.id, s.nombre AS socio, l.titulo, p.fecha_prestamo, p.fecha_devolucion
FROM prestamos p
JOIN socios s ON s.id = p.socio_id
JOIN libros l ON l.id = p.libro_id;
| id | socio | titulo | fecha_prestamo | fecha_devolucion |
|---|---|---|---|---|
| 1 | Irene Sanz | Cien años de soledad | 2026-09-10 17:30:00 | NULL |
| 2 | Mario Gil | Nada | 2026-09-12 11:05:00 | NULL |
Errores frecuentes
- Coma después de la última columna:
fecha_alta DATE NOT NULL, );da error de sintaxis. - Guardar dinero en
FLOAT: los céntimos acaban descuadrando. UsaDECIMAL. - Guardar fechas o números como texto: pierdes el orden correcto, los cálculos y la validación.
- Teléfonos o códigos postales como
INT: se pierden los ceros iniciales (09001pasaría a ser9001). Si no vas a hacer cuentas con un dato, probablemente es texto.
Resumen
| Necesito guardar… | Tipo en MySQL |
|---|---|
| Un id | INT AUTO_INCREMENT (o BIGINT) |
| Una cantidad entera | INT, SMALLINT o TINYINT |
| Dinero | DECIMAL(10, 2) |
| Una medida aproximada | DOUBLE |
| Un texto de longitud fija | CHAR(n) |
| Un texto corto variable | VARCHAR(n) |
| Un texto largo | TEXT |
| Una fecha | DATE |
| Fecha y hora | DATETIME o TIMESTAMP |
| Sí / no | BOOLEAN |
CREATE DATABASE ... CHARACTER SET utf8mb4yUSEpara empezar.CREATE TABLE IF NOT EXISTSpara scripts repetibles;CREATE TABLE ... AS SELECTpara copias.DESCRIBEySHOW TABLESpara ver lo que has creado.
En la siguiente lección, claves y restricciones, aprenderás a proteger los datos con PRIMARY KEY, UNIQUE, CHECK y claves foráneas.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.