Manual teórico de SQL en MySQL / MariaDB
Explicaciones, sintaxis y ejemplos ejecutados de verdad: cada resultado que ves aquí lo produjo el servidor de base de datos al ejecutar el código mostrado. Cada tema avanza por niveles: Básico Intermedio Avanzado
0. Descargas e instalación
Para practicar todo lo del manual necesitas dos programas gratuitos: XAMPP, que trae el servidor de base de datos (MariaDB, compatible con MySQL), el servidor web Apache, PHP y phpMyAdmin; y MySQL Workbench 8.0, para diagramar las bases de datos (modelo entidad-relación) y trabajar con ellas de forma visual.
0.1. Qué descargar y dónde
XAMPP 8.2
Apache + MariaDB + PHP + phpMyAdmin en un solo instalador. Versión usada en este manual: 8.2.12 (Windows y Linux) y 8.2.4 (macOS).
Página oficial de descargasWindows (.exe, 149 MB) Linux (.run, 151 MB) macOS (.dmg, 150 MB)
MySQL Workbench 8.0
Para diagramar se usa la versión 8.0 (la última es la 8.0.47). Oracle cambió la numeración y la página principal ya ofrece versiones nuevas (26.x), por eso la 8.0 se descarga desde el archivo de versiones: en Product Version elige 8.0.47 y en Operating System tu sistema.
Workbench 8.0 (archivo oficial) Visual C++ Redistributable (requisito en Windows)Descarga solo MySQL Workbench, no el "MySQL Installer" completo: ese instala además un servidor MySQL que ocupa el mismo puerto que XAMPP (0.10).
0.2. Instalar XAMPP
- Ejecuta
xampp-windows-x64-8.2.12-0-VS16-installer.exe. Si aparece un aviso del antivirus o del Control de cuentas de usuario (UAC), acepta y continúa. - En los componentes deja marcados al menos Apache, MySQL, PHP y phpMyAdmin.
- Carpeta de instalación: deja
C:\xampp. No la instales enC:\Program Files: Windows protege esa carpeta y XAMPP no puede escribir en ella. - Al terminar abre el XAMPP Control Panel y pulsa Start en Apache y en MySQL. Ambos deben ponerse en verde con su número de puerto (80 y 3306).
- Comprueba en el navegador:
http://localhost/(página de XAMPP) yhttp://localhost/phpmyadmin/. - Si Windows pregunta por el Firewall, permite el acceso en redes privadas.
Tus proyectos web van en C:\xampp\htdocs; la consola de MySQL está en C:\xampp\mysql\bin (sección 1.2).
- Dale permiso de ejecución al instalador y ejecútalo como administrador:
- Se instala en
/opt/lampp. Para iniciar, detener o ver el estado de los servicios:
- Si al iniciar aparece
netstat: command not found, instala las herramientas de red:sudo apt install net-tools(Ubuntu/Debian). - Si tu sistema ya tiene Apache o MySQL instalados, ocupan los mismos puertos: detenlos con
sudo systemctl stop apache2ysudo systemctl stop mysql(0.6). - Proyectos web:
/opt/lampp/htdocs. Consola de MySQL:/opt/lampp/bin/mysql -u root -p.
- Abre
xampp-osx-8.2.4-0-installer.dmgy ejecuta el instalador. Funciona en Mac con procesador Intel y Apple (M1, M2…). - Si macOS dice que la aplicación "no se puede abrir porque es de un desarrollador no identificado", haz clic derecho sobre ella → Abrir → Abrir (o ve a Configuración del Sistema → Privacidad y seguridad y pulsa Abrir igualmente).
- Se instala en
/Applications/XAMPP. Abre la aplicación XAMPP (manager-osx) → pestaña Manage Servers → inicia Apache Web Server y MySQL Database. - Comprueba en el navegador
http://localhost/yhttp://localhost/phpmyadmin/.
Proyectos web: /Applications/XAMPP/htdocs. Consola de MySQL (en la Terminal): /Applications/XAMPP/bin/mysql -u root -p.
0.3. Instalar MySQL Workbench 8.0
En el archivo de Workbench elige Product Version: 8.0.47 y luego tu sistema:
- Instala primero el Visual C++ Redistributable (enlace en 0.1). Sin él, Workbench no inicia o el instalador lo pide.
- En Operating System elige Microsoft Windows y descarga Windows (x86, 64-bit), MSI Installer
(
mysql-workbench-community-8.0.47-winx64.msi). - Ejecuta el
.msi→ Next → tipo de instalación Complete → Install.
- En Operating System elige Ubuntu Linux y en OS Version la versión de tu Ubuntu (por ejemplo 24.04 o 22.04). Descarga el paquete
.deb. - Instálalo con
apt, que descarga también las dependencias:
Para Red Hat / Oracle Linux elige ese sistema y descarga el paquete .rpm (sudo dnf install ./mysql-workbench-community-8.0.47*.rpm).
- En Operating System elige macOS y descarga el
.dmgde tu procesador: ARM, 64-bit para Mac con chip Apple (M1, M2…) o x86, 64-bit para Mac con Intel. (Para saberlo: menú Apple → Acerca de esta Mac.) - Abre el
.dmgy arrastra MySQLWorkbench a la carpeta Aplicaciones. - La primera vez, si macOS lo bloquea: clic derecho → Abrir.
0.4. Conectar Workbench con XAMPP
- Inicia MySQL en el panel de XAMPP.
- En Workbench, junto a MySQL Connections, pulsa + y completa: Connection Name
XAMPP local, Hostname127.0.0.1, Port3306, Usernameroot. - Pulsa Test Connection. En XAMPP recién instalado
rootno tiene contraseña: deja el campo vacío.
0.5. Diagramar: modelo EER, Forward y Reverse Engineer
Crear un diagrama nuevo
File → New Model → doble clic en Add Diagram. Con el botón de tabla agregas tablas y columnas (marca PK, NN, AI); con los botones de relación (1:1, 1:N, N:M) unes las tablas y Workbench crea las llaves foráneas.
Forward Engineer: del diagrama a la base
Database → Forward Engineer → elige la conexión XAMPP local → Workbench genera el script
CREATE TABLE y lo ejecuta en el servidor.
Reverse Engineer: de la base al diagrama
Database → Reverse Engineer → conexión → elige la base → Workbench dibuja el diagrama de las tablas que ya existen, con sus relaciones.
El diagrama se guarda como archivo .mwb. Para entregarlo como imagen: File → Export → Export as PNG (o PDF).
El script de Workbench 8.0 falla en MariaDB: la palabra VISIBLE
Workbench 8.0 genera el SQL pensando en MySQL 8. En los índices escribe la palabra VISIBLE, que existe solo en MySQL 8,
y XAMPP trae MariaDB, que no la reconoce. Así se ve el script que genera el Forward Engineer (o la exportación del modelo):
Ojo: el error no menciona VISIBLE; señala la línea siguiente (CONSTRAINT), por eso parece que el problema es la llave foránea.
Al quitar VISIBLE, el mismo script crea las tablas sin problema. A veces aparece también el cotejamiento de MySQL 8, que MariaDB tampoco tiene:
Solución recomendada: bajar la versión destino del modelo
Con el modelo abierto: Model → Model Options → pestaña MySQL → desmarca Use defaults from global settings
→ Target MySQL Version = 5.7 → OK. Vuelve a hacer el Forward Engineer o la exportación: ya no aparece VISIBLE.
Sirve cualquier versión anterior a 8.0, pero usa 5.7: es la versión real que precede a la 8.0 (MySQL nunca publicó una 6.0 estable).
Si Workbench se cierra al cambiar la versión
Cambiar la versión en la configuración general (Edit → Preferences → Modeling → MySQL → Default Target MySQL Version) con un modelo abierto a veces hace que Workbench 8.0 se cierre solo. Para evitarlo:
- Cámbiala por modelo (solución 1) en lugar de en Preferences, y guarda el modelo (Ctrl+S) antes de hacerlo.
- O cierra todos los modelos, cámbiala en Preferences sin nada abierto, pulsa OK y reinicia Workbench.
Alternativa sin cambiar la versión: corregir el script
En el paso Review the SQL Script del Forward Engineer pulsa Save to Text File (o abre el .sql exportado) con el Bloc de notas →
Edición → Reemplazar: VISIBLE (con el espacio de adelante) por nada, y utf8mb4_0900_ai_ci por utf8mb4_general_ci
→ Reemplazar todo. Luego ejecuta el script en phpMyAdmin o en la consola (SOURCE C:/ruta/script.sql).
Si exportas con Server → Data Export y falla con "Unknown table 'COLUMN_STATISTICS' in information_schema", Workbench está usando su propio
mysqldump de MySQL 8. En Edit → Preferences → Administration → Path to mysqldump Tool indica el de XAMPP:
C:\xampp\mysql\bin\mysqldump.exe. Para respaldos, la forma más segura sigue siendo mysqldump desde la consola (módulo 11).
0.6. Qué son los puertos y quién los ocupa
Un puerto es un número que identifica a un programa dentro del equipo: así el navegador sabe que debe hablar con Apache y Workbench con MySQL. Solo un programa puede escuchar en cada puerto. Si otro programa ya lo usa, XAMPP no puede iniciar ese servicio.
| Servicio | Puerto | Quién suele ocuparlo |
|---|---|---|
| Apache (http) | 80 | IIS / World Wide Web Publishing Service de Windows, otro Apache, programas como VMware o Skype (versiones antiguas). En Linux, el Apache del sistema (apache2). |
| Apache (https) | 443 | Los mismos que el 80, y algunos programas de VPN o de máquinas virtuales. |
| MySQL / MariaDB | 3306 | Otro servidor MySQL instalado (por ejemplo el servicio MySQL80 que agrega el "MySQL Installer"), otra copia de XAMPP o un MySQL de Docker. |
Cuando el puerto está ocupado, el panel de XAMPP muestra en rojo un aviso como "Problem detected! Port 80 in use by …" o, para MySQL, "Error: MySQL shutdown unexpectedly". Para saber qué programa ocupa un puerto (en CMD):
- La última columna de
netstates el PID (número del proceso);tasklistdice qué programa es. Aquí esmysqld.exe: si es el de XAMPP, MySQL ya está corriendo; si es de otra instalación (por ejemploC:\Program Files\MySQL\…), es el que bloquea. - El botón Netstat del panel de XAMPP muestra la misma información en una ventana.
- Si el PID es 4 (System) en el puerto 80, casi siempre es IIS: detén el servicio World Wide Web Publishing Service en
services.mscy cámbialo a inicio Manual. - En Linux:
sudo ss -ltnp | grep -E ':80|:3306'. En macOS:sudo lsof -i :3306.
Dos soluciones: detener el programa que ocupa el puerto (en Windows: services.msc → servicio → Detener y tipo de inicio Manual),
o cambiar el puerto de XAMPP (0.7 y 0.8) si necesitas los dos programas.
0.7. Cambiar el puerto de Apache
- Detén Apache. En el panel: Apache → Config → Apache (httpd.conf). Cambia estas dos líneas (por ejemplo, a 8080):
- Si el ocupado es el 443: Config → Apache (httpd-ssl.conf) →
Listen 443→Listen 4433yServerName www.example.com:443→:4433. - Avísale al panel el puerto nuevo: botón Config (arriba a la derecha) → Service and Port Settings → pestaña Apache → Main Port 8080 (y SSL Port 4433) → Save.
- Inicia Apache. Desde ahora las direcciones llevan el puerto:
http://localhost:8080/,http://localhost:8080/phpmyadmin/.
Si Apache no inicia después de editar: probablemente quedó un error de escritura en el archivo. Compruébalo antes de iniciar:
Si en lugar de Syntax OK muestra un error, indica el archivo y la línea. Para volver atrás, deja Listen 80 y ServerName localhost:80 como estaban.
0.8. Cambiar el puerto de MySQL
- Detén MySQL. En el panel: MySQL → Config → my.ini. Cambia el puerto en las dos secciones (por ejemplo, a 3307):
- phpMyAdmin: en
C:\xampp\phpMyAdmin\config.inc.php, debajo de la línea$cfg['Servers'][$i]['host'] = '127.0.0.1';agrega:$cfg['Servers'][$i]['port'] = '3307'; - En el panel: Config → Service and Port Settings → MySQL → Main Port 3307 → Save. Inicia MySQL.
- Todo lo que se conecta a MySQL debe usar el puerto nuevo:
| Dónde | Cómo |
|---|---|
| Consola | mysql -u root -p -P 3307 (-P mayúscula = puerto) |
| Workbench | Editar la conexión → Port 3307 |
| PHP (PDO) | new PDO('mysql:host=127.0.0.1;port=3307;dbname=mi_base', 'root', '') |
| Esta plataforma | En el archivo .env: DB_HOST=127.0.0.1;port=3307 |
Para volver al puerto normal, repite los pasos con 3306 (y borra la línea de port agregada en phpMyAdmin).
0.9. Reparar MySQL cuando se detiene inesperadamente
El problema más común de XAMPP: MySQL se pone verde un momento y se detiene con "Error: MySQL shutdown unexpectedly", aunque el puerto esté libre.
Casi siempre pasa después de apagar el equipo o cerrar XAMPP sin detener MySQL: algunos archivos internos del servidor quedan dañados.
XAMPP guarda una copia limpia de esos archivos en C:\xampp\mysql\backup.
C:\xampp\mysql\data\mysql_error.log).
Si el error habla del puerto ("Can't start server: Bind on TCP/IP port") el problema es el puerto (0.6), no los archivos.
- Haz una copia de seguridad de la carpeta
C:\xampp\mysql\datacompleta (cópiala y pégala comodata_respaldo). Si algo sale mal, puedes volver a dejarla como estaba. - Abre
C:\xampp\mysql\backupy selecciona todo excepto el archivoibdata1. - Cópialo y pégalo dentro de
C:\xampp\mysql\data; cuando Windows pregunte, elige Reemplazar los archivos. - Inicia MySQL en el panel y revisa en phpMyAdmin que tus bases y tablas estén.
Lo que se copia desde backup
Los archivos del propio servidor: aria_log.00000001, aria_log_control, ib_logfile0, ib_logfile1,
ib_buffer_pool, ibtmp1, my.ini, multi-master.info y las carpetas mysql, performance_schema,
phpmyadmin y test. Tus bases de datos no están en backup, así que sus carpetas no se tocan.
Por qué no se copia ibdata1
ibdata1 guarda información interna de todas tus tablas InnoDB. El de backup está vacío:
si lo copias, MySQL inicia pero tus tablas aparecen con el error "Table doesn't exist in engine". Por eso se conserva el tuyo.
- Usuarios y permisos vuelven a los de fábrica: la carpeta
mysqlcontiene los usuarios. Después de reparar,rootqueda sin contraseña y se pierden los usuarios y roles creados (módulo 10). En esta plataforma hay que volver a importardatabase/03_seguridad_y_sandbox.sqlpara recrear el usuariosandbox_runner. - Los últimos cambios hechos justo antes del fallo pueden perderse. Por eso conviene tener copias de seguridad recientes (módulo 11).
- Si después de reparar falta alguna tabla, restaura la copia
data_respaldoo tu último respaldo.sql. - Para que no vuelva a pasar: antes de apagar el equipo o cerrar XAMPP, pulsa Stop en MySQL y espera a que se ponga gris.
- En Linux y macOS no existe esa carpeta
backup: revisa el registro (/opt/lampp/var/mysql/*.erro/Applications/XAMPP/xamppfiles/var/mysql/*.err) y restaura desde un respaldo.
0.10. Otros problemas frecuentes
| Síntoma | Causa probable | Solución |
|---|---|---|
| Apache o MySQL no inician y no hay otro programa en el puerto | El panel no tiene permisos para escribir en la carpeta. | Abre el panel con clic derecho → Ejecutar como administrador. Verifica que XAMPP esté en C:\xampp. |
El antivirus elimina o bloquea httpd.exe o mysqld.exe | Falso positivo. | Agrega C:\xampp a las exclusiones del antivirus. |
http://localhost/ muestra otra página o no carga | Otro servidor web en el puerto 80, o Apache en otro puerto. | Revisa 0.6; si cambiaste el puerto, escribe http://localhost:8080/. |
| El script de Workbench falla con "error in your SQL syntax … near CONSTRAINT" | La palabra VISIBLE de MySQL 8 en los índices. | Bajar la versión destino del modelo a 5.7 o quitar VISIBLE del script (0.5). |
| Workbench no inicia en Windows | Falta el Visual C++ Redistributable. | Instálalo (enlace en 0.1) y vuelve a abrir Workbench. |
| El puerto 3306 ya está ocupado justo después de instalar MySQL | Se instaló el "MySQL Installer" con un servidor MySQL (servicio MySQL80). | services.msc → MySQL80 → Detener y tipo de inicio Manual, o cambia el puerto (0.8). |
| phpMyAdmin dice "mysqli::real_connect(): (HY000/2002)" | MySQL está detenido o en otro puerto. | Inicia MySQL; si cambiaste el puerto, configúralo en phpMyAdmin (0.8). |
1. Primeros pasos: la consola de MySQL
C:
mpp\mysql\bin. Las salidas que se muestran son reales, tomadas de un XAMPP con MariaDB 10.4.
1.1. Dos programas distintos: mysql y mysqldump
| Programa | Para qué sirve | Dónde se escribe |
|---|---|---|
mysql.exe | Cliente de consola: conectarse al servidor, ejecutar consultas y cargar (restaurar) archivos .sql. | En CMD. Al entrar, el indicador cambia a MariaDB [(none)]> |
mysqldump.exe | Exportar (respaldar) una base de datos a un archivo .sql. | Siempre en CMD, nunca dentro de MariaDB> |
Error muy común: escribir mysqldump ... dentro de la consola MariaDB [(none)]>. Allí no existe y
produce un error de sintaxis. Primero sal con exit.
1.2. Iniciar sesión en la base de datos desde la consola
Hay dos formas. Ambas requieren que MySQL esté iniciado (botón Start de MySQL en el panel de XAMPP).
Opción A: desde CMD, entrando a la carpeta de MySQL
- Abre CMD: tecla Windows, escribe
cmdy presiona Enter. - Ve a la carpeta donde están los programas de MySQL y conéctate:
-u root: el usuario con el que entras (en XAMPP el administrador se llamaroot).-p: pide la contraseña de forma oculta. En un XAMPP recién instaladorootno tiene contraseña: presiona Enter sin escribir nada (o conéctate directamente conmysql -u root).- No escribas la contraseña pegada en el comando (
-pMiClave): queda visible en pantalla y en el historial de la consola.
Si la conexión es correcta verás algo así:
(none) indica que todavía no has elegido una base de datos.
Opción B: con el botón «Shell» del panel de XAMPP
En el panel de control de XAMPP, el botón Shell abre una consola que ya tiene configurada la ruta de MySQL
(agrega C:\xampp\mysql\bin a la variable PATH). Por eso puedes conectarte sin cambiar de carpeta:
Errores frecuentes al conectarse
| Mensaje | Causa y solución |
|---|---|
"mysql" no se reconoce como un comando interno o externo | No estás en C:\xampp\mysql\bin (usa cd C:\xampp\mysql\bin) o no abriste la consola con el botón Shell. |
ERROR 2002 (HY000): Can't connect to MySQL server on 'localhost' | El servicio MySQL está detenido: inícialo en el panel de XAMPP. |
ERROR 1045 (28000): Access denied for user 'root'@'localhost' | Contraseña incorrecta (o root sí tiene contraseña y no usaste -p). |
1.3. Comandos básicos dentro de la consola
- Toda sentencia termina en
;. Si presionas Enter sin él, aparece->esperando el resto: escribe;y Enter. - Si te equivocas a mitad de una sentencia, escribe
\cpara cancelarla. - Terminar una consulta con
\Gen lugar de;muestra cada fila en vertical (útil con tablas anchas). - En
DESCRIBE,PRI= clave primaria yMUL= columna con índice que admite repetidos (como una clave foránea).
2. DDL: definir la estructura (CREATE, ALTER, DROP)
cd C:\xampp\mysql\bin y mysql -u root -p). Las salidas son reales, tomadas de un XAMPP con MariaDB 10.4.
2.1. Las familias de sentencias SQL
| Familia | Para qué sirve | Sentencias |
|---|---|---|
| DDL (Data Definition Language) | Crear, modificar y eliminar la estructura: bases, tablas, columnas, índices, vistas. | CREATE, ALTER, DROP, RENAME, TRUNCATE |
| DML (Data Manipulation Language) | Trabajar con los datos guardados en las tablas (módulos 3 y 4). | INSERT, UPDATE, DELETE, SELECT |
| TCL (Transaction Control) | Confirmar o deshacer grupos de cambios (módulo 5). | START TRANSACTION, COMMIT, ROLLBACK, SAVEPOINT |
| DCL (Data Control) | Crear usuarios y roles, y dar o quitar permisos (módulo 10). | GRANT, REVOKE |
2.2. Bases de datos: crear, ver, usar y eliminar
CHARACTER SET utf8mb4permite guardar tildes, eñes y emojis. Inclúyelo siempre al crear una base.IF NOT EXISTSevita el error si ya existe (solo deja una advertencia). Es útil en scripts que se ejecutan varias veces.- Para eliminar una base y todo su contenido:
DROP DATABASE colegio;(no pide confirmación ni se puede deshacer).
2.3. Crear tablas: CREATE TABLE
Tipos de datos más usados
| Tipo | Guarda | Ejemplo |
|---|---|---|
INT / TINYINT / BIGINT | Enteros (TINYINT: -128 a 127; INT: ±2.147 millones; BIGINT: mucho más) | edad TINYINT |
DECIMAL(p, d) | Números exactos con p dígitos en total y d decimales. Úsalo para dinero. | precio DECIMAL(10,2) → hasta 99.999.999,99 |
VARCHAR(n) | Texto de hasta n caracteres | nombre VARCHAR(60) |
CHAR(n) | Texto de longitud fija | codigo_pais CHAR(2) |
TEXT | Textos largos (hasta 65.535 bytes) | observaciones TEXT |
DATE / DATETIME / TIMESTAMP | Fecha / fecha y hora / fecha y hora con zona horaria del servidor | fecha_nacimiento DATE |
BOOLEAN | Verdadero o falso (se guarda como TINYINT(1): 1 o 0) | activo BOOLEAN |
ENUM('a','b') | Solo uno de los valores de la lista | jornada ENUM('mañana','tarde','noche') |
Restricciones (reglas que protegen los datos)
| Restricción | Qué garantiza |
|---|---|
PRIMARY KEY | Identifica cada fila: única y nunca vacía. Una por tabla. |
AUTO_INCREMENT | MySQL asigna el número automáticamente (1, 2, 3…). Normalmente en la clave primaria. |
NOT NULL | La columna es obligatoria. |
DEFAULT valor | Valor que toma la columna si no se indica al insertar. |
UNIQUE | No se puede repetir el valor en otra fila (documento, correo). |
CHECK (condición) | El valor debe cumplir la condición (MariaDB 10.2+ y MySQL 8.0.16+ la hacen cumplir). |
FOREIGN KEY ... REFERENCES | El valor debe existir en otra tabla (relación). Ver 2.6. |
CONSTRAINT nombre le da un nombre a la regla (útil para luego borrarla o entender un error).
UNIQUE (aprendiz_id, curso_id) es una restricción de tabla: la combinación no se repite (un aprendiz no se inscribe dos veces al mismo curso).
Las llaves foráneas y sus opciones ON DELETE se explican en la sección 2.6.
Revisar lo creado
SHOW CREATE TABLE muestra la sentencia completa tal como la guardó el servidor: observa que BOOLEAN se convirtió en
tinyint(1) y TRUE en 1. El \G final muestra el resultado en vertical.
Errores y comprobaciones al crear
El CHECK rechazó 15 créditos porque la regla exige entre 1 y 10. Ojo: con IF NOT EXISTS la tabla existente no se modifica; si necesitas otra estructura, usa ALTER TABLE.
2.4. Modificar tablas: ALTER TABLE
| Quiero… | Sentencia |
|---|---|
| Agregar una columna (al final) | ALTER TABLE t ADD COLUMN col TIPO; |
| Agregarla en una posición | ... ADD COLUMN col TIPO AFTER otra; o ... FIRST; |
| Cambiar tipo o restricciones (mismo nombre) | ALTER TABLE t MODIFY COLUMN col NUEVO_TIPO; |
| Cambiar el nombre (y el tipo) | ALTER TABLE t CHANGE COLUMN viejo nuevo TIPO; |
| Cambiar solo el valor por defecto | ALTER TABLE t ALTER COLUMN col SET DEFAULT valor; |
| Eliminar una columna | ALTER TABLE t DROP COLUMN col; |
| Agregar una restricción | ALTER TABLE t ADD CONSTRAINT nombre UNIQUE (col); |
| Eliminar una restricción única / un índice | ALTER TABLE t DROP INDEX nombre; |
| Eliminar una llave foránea | ALTER TABLE t DROP FOREIGN KEY nombre; |
| Renombrar la tabla | ALTER TABLE t RENAME TO nuevo; o RENAME TABLE t TO nuevo; |
- MODIFY cambia el tipo conservando el nombre; CHANGE pide el nombre viejo, el nuevo y el tipo completo (aunque no cambie).
- Con MODIFY y CHANGE hay que repetir todas las restricciones de la columna (
NOT NULL,DEFAULT…): las que no escribas se pierden.
RENAME COLUMN existe en MySQL 8 y MariaDB 10.5 o superior, pero no en el MariaDB 10.4 de XAMPP: allí se usa CHANGE COLUMN.
El nombre completo se perdió para siempre. Antes de cambiar tipos o tamaños en una tabla con datos, haz una copia de seguridad (sección 11.1) y revisa los datos más largos con SELECT MAX(CHAR_LENGTH(nombre)) FROM aprendices;.
2.5. Índices: búsquedas más rápidas
Un índice es como el índice de un libro: permite encontrar filas sin recorrer toda la tabla. Las claves primarias y las columnas UNIQUE ya tienen uno.
Non_unique = 0 indica índice único. Crea índices en columnas que se usan mucho en WHERE, JOIN u ORDER BY; no en todas: cada índice hace más lentos los INSERT y UPDATE.
Para comprobar si una consulta realmente usa un índice se usa EXPLAIN (4.17).
2.6. Llaves foráneas (FOREIGN KEY): relacionar tablas
Una llave foránea es una regla que obliga a que el valor de una columna exista en otra tabla. La tabla que guarda la referencia
es la hija (ciudades.departamento_id) y la tabla referenciada es la padre (departamentos.id).
Así la base de datos impide, por sí misma, datos "huérfanos" o inconsistentes.
Sintaxis completa
- Se puede escribir dentro del
CREATE TABLE(como restricción de tabla) o agregarse después conALTER TABLE ... ADD CONSTRAINT. CONSTRAINT nombrees opcional pero muy recomendado: sin nombre, MySQL inventa uno (sedes_ibfk_1) y es más difícil identificarla en los errores o borrarla.- Convención útil:
fk_hija_padre, por ejemplofk_ciudad_depto.
Crear tablas relacionadas: el padre primero
El error 1005 con errno 150 es genérico ("la llave foránea está mal formada"). La causa real siempre aparece en SHOW WARNINGS:
aquí, la tabla padre departamentos todavía no existe. Se crea primero el padre y luego la hija:
Sin nombre y con índice automático
- Como no se le dio nombre, MySQL la llamó
sedes_ibfk_1(InnoDB foreign key número 1). - MySQL creó solo un índice (
KEY `ciudad_id`) en la columna hija: la llave foránea lo necesita para verificar rápido.
Requisitos: cuándo NO se puede crear
| Requisito | Causa que muestra SHOW WARNINGS si no se cumple |
|---|---|
| La tabla padre debe existir | Referenced table ... not found in the data dictionary |
Mismo tipo de dato en ambas columnas: INT con INT (tampoco se mezcla INT con INT UNSIGNED ni con BIGINT). En textos, también el mismo juego de caracteres. | Field type or character set for column 'ciudad_id' does not mach referenced column 'id' |
La columna padre debe tener índice (normalmente es la PRIMARY KEY o una UNIQUE) | There is no index in the referenced table where the referenced columns appear as the first columns |
Con ON DELETE SET NULL, la columna hija debe permitir NULL | You have defined a SET NULL condition but column 'ciudad_id' is defined as NOT NULL |
| Ambas tablas con motor InnoDB (el predeterminado) | Ninguna: ver la advertencia de abajo. |
Los tres errores son idénticos: solo SHOW WARNINGS; dice cuál requisito falló (columna derecha de la tabla de arriba).
No dio error, aceptó la ciudad 999 (que no existe) y en la estructura no hay ningún CONSTRAINT: solo quedó un índice. Usa siempre InnoDB, que es el motor predeterminado (no escribas ENGINE=MyISAM).
Agregar una llave foránea a una tabla que ya tiene datos
No se puede crear porque ya hay un dato que la viola: un cliente con la ciudad 7, que no existe (el nombre raro #sql-... es la copia temporal que MySQL usa durante el ALTER). Primero se buscan los huérfanos con un LEFT JOIN:
Un NULL en la columna hija sí está permitido: significa "sin ciudad" y no viola la llave foránea (Pedro no causó el error).
Opciones ON DELETE y ON UPDATE
Definen qué pasa con las filas hijas cuando se borra (ON DELETE) o se cambia el id (ON UPDATE) de su fila padre:
| Opción | Efecto en las filas hijas | Cuándo usarla |
|---|---|---|
RESTRICT (por defecto) / NO ACTION | Se impide borrar o cambiar el padre mientras tenga hijos (error 1451). En MySQL/MariaDB ambas se comportan igual. | La opción más segura: obliga a decidir qué hacer con los hijos. |
CASCADE | Los hijos se borran (ON DELETE) o se actualizan (ON UPDATE) junto con el padre. | Hijos que no tienen sentido sin el padre: líneas de un pedido, inscripciones de un aprendiz. |
SET NULL | Los hijos se conservan con la columna en NULL. | Relación opcional: el cliente sigue existiendo aunque se elimine su ciudad. |
Luis quedó sin ciudad (SET NULL) y Ana pasó automáticamente a la ciudad 10 (CASCADE). Con RESTRICT, en cambio:
El efecto sobre los datos al usar INSERT, UPDATE y DELETE se profundiza en las secciones 3.2, 3.3 y 3.4.
Ver las llaves foráneas de una base
Además de SHOW CREATE TABLE tabla\G, el catálogo information_schema las lista todas:
La segunda consulta muestra las reglas de cada llave: las que no indicaron opciones quedaron en RESTRICT.
Modificar o eliminar una llave foránea
No existe un "ALTER CONSTRAINT": para cambiar una llave foránea (por ejemplo, sus opciones) se elimina y se vuelve a crear:
DROP FOREIGN KEYelimina la regla, pero el índice queda. Si ya no se necesita:ALTER TABLE ciudades DROP INDEX fk_ciudad_depto;- Para borrarla hay que conocer su nombre exacto (por eso conviene nombrarlas): búscalo con
SHOW CREATE TABLEo eninformation_schema.
Casos especiales
En la autorreferencia, el primer registro (la jefa principal) se inserta con jefe_id en NULL; por eso la columna debe permitir NULL.
Orden correcto al crear y al borrar tablas relacionadas
| Crear | Primero los padres, luego las hijas: departamentos → ciudades → sedes. |
| Borrar | Al revés, primero las hijas: sedes → ciudades → departamentos. Si no, DROP TABLE da el error 1451 (sección 2.7). |
2.7. Renombrar y eliminar: RENAME y DROP
DROP TABLEelimina la tabla con su estructura y todos sus datos.- No se puede eliminar una tabla de la que dependen otras por llave foránea (
cursoses usada pormatriculas): primero se elimina la tabla dependiente o su llave foránea. DROP DATABASE colegio;elimina la base entera:Query OK, 4 rows affectedindica cuántos objetos borró.
2.8. Vistas: consultas guardadas con nombre
Una vista no guarda datos: cada vez que se consulta, ejecuta su SELECT. Sirve para simplificar consultas largas o mostrar solo algunas columnas.
2.9. Otras formas de crear tablas y columnas
Columnas calculadas (generadas)
Una columna generada calcula su valor a partir de otras columnas de la misma fila, y se mantiene sola al cambiar los datos:
- El total del Té se recalculó solo al cambiar la cantidad (2 → 10). El valor 99 que se intentó escribir en
totalfue ignorado. STOREDguarda el resultado en disco (se puede indexar);VIRTUALlo calcula cada vez que se consulta (no ocupa espacio).
Copiar la estructura o crear desde una consulta
CREATE TABLE ... AS SELECT copia las columnas y los datos, pero no copia la clave primaria, los índices ni las llaves foráneas (y la columna calculada pasó a ser una columna normal). Útil para respaldos rápidos de una tabla o tablas de reporte.
Tablas temporales
Una tabla TEMPORARY solo existe para tu conexión (otros usuarios no la ven) y se borra sola al cerrar la consola. Observa que no aparece en SHOW TABLES, aunque se puede consultar. Sirve para guardar resultados intermedios.
Reiniciar o adelantar el contador AUTO_INCREMENT
No se puede bajar el contador por debajo del id más alto que ya existe: MySQL lo ajusta solo al siguiente número libre. Para volver a 1 hay que vaciar la tabla con TRUNCATE (3.4).
2.10. Las sentencias DDL no se pueden deshacer
CREATE, ALTER, DROP, RENAME y TRUNCATE hacen un COMMIT automático: el ROLLBACK no los revierte
(y además confirma lo que hubiera pendiente en la transacción). La única protección real ante un DROP o un ALTER equivocado es una copia de seguridad.
2.11. Chuleta
| Crear base | CREATE DATABASE IF NOT EXISTS b CHARACTER SET utf8mb4; |
| Crear tabla | CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, col VARCHAR(50) NOT NULL); |
| Ver estructura | DESCRIBE t; · SHOW CREATE TABLE t\G |
| Agregar / cambiar / quitar columna | ALTER TABLE t ADD COLUMN … · MODIFY COLUMN … · CHANGE COLUMN viejo nuevo … · DROP COLUMN … |
| Índice | CREATE INDEX idx ON t (col); · DROP INDEX idx ON t; |
| Llave foránea | ALTER TABLE hija ADD CONSTRAINT fk_hija_padre FOREIGN KEY (col) REFERENCES padre(id) ON DELETE RESTRICT; · ALTER TABLE hija DROP FOREIGN KEY fk_hija_padre; |
| ¿Por qué falló? (errno 150) | SHOW WARNINGS; |
| Renombrar tabla | RENAME TABLE t TO nuevo; |
| Eliminar | DROP TABLE IF EXISTS t; · DROP DATABASE b; |
3. DML: manipular los datos
El DML (Data Manipulation Language) agrupa las sentencias que trabajan con los datos de las tablas ya creadas:
INSERT (agregar), UPDATE (modificar) y DELETE (eliminar). A diferencia del DDL,
sus cambios sí se pueden deshacer dentro de una transacción.
3.1. Insertar datos en una tabla
Sintaxis y reglas básicas
- Los valores van en el mismo orden que las columnas de la lista, y debe haber la misma cantidad.
- Textos y fechas entre comillas simples:
'Cafetera','2026-01-15'. - Números sin comillas, con punto decimal y sin separador de miles:
1450000.50(no1.450.000,50). NULLsin comillas significa "sin valor";'NULL'con comillas sería el texto N-U-L-L.- Fechas:
'AAAA-MM-DD'; fecha y hora:'AAAA-MM-DD HH:MM:SS'. Toda sentencia termina en;.
Tabla de ejemplo
Fíjate en las reglas: codigo no se repite (UNIQUE), nombre es obligatorio (NOT NULL) y varias columnas tienen un valor por defecto (DEFAULT).
Una fila y varias filas
Query OK, 1 row affected confirma que se guardó. En la inserción múltiple cada fila va entre paréntesis, separadas por comas: es más rápida que varios INSERT.
Records = filas procesadas, Duplicates = repetidas, Warnings = advertencias (si no es 0, revísalas con SHOW WARNINGS;).
Columnas que se pueden omitir: AUTO_INCREMENT, DEFAULT y NULL
- Las columnas que no aparecen reciben su valor por defecto:
id→ siguiente número,categoria→ 'General',stock→ 0,creado_en→ fecha y hora actuales. DEFAULTpide explícitamente el valor por defecto yNULLdeja la columna vacía (solo si la columna lo permite).LAST_INSERT_ID()devuelve el id automático de la última fila que tú insertaste en esta conexión.
Otras formas de escribir el INSERT
Sin lista de columnas funciona, pero no se recomienda: si mañana se agrega una columna a la tabla, la sentencia deja de funcionar.
Comillas, tildes, fechas y funciones
Se pueden usar funciones como valor: NOW(), CURDATE(), UPPER('texto'), ROUND(12.345, 2).
Verifica siempre con SELECT ... ORDER BY: sin ORDER BY el orden de las filas no está garantizado.
Si las tildes se ven mal, conéctate con mysql -u root -p --default-character-set=utf8mb4.
Errores al insertar
| Error | Qué significa | Solución |
|---|---|---|
1062 Duplicate entry | Ese valor ya existe en una columna PRIMARY KEY o UNIQUE. | Usa otro valor o mira "si el registro ya existe" más abajo. |
1136 Column count doesn't match | Distinta cantidad de columnas y de valores. | Cuenta columnas y valores. |
1064 SQL syntax | Falta una coma, comilla o paréntesis (aquí, el paréntesis final). | Revisa cerca del texto que indica near '...'. |
1054 Unknown column | Nombre de columna mal escrito. | Revisa con DESCRIBE productos; |
1364 / 1406 / 1366 | Campo obligatorio omitido / texto demasiado largo / valor no numérico. | Ver el modo estricto, a continuación. |
¡Cuidado! Modo estricto y advertencias
XAMPP viene configurado sin modo estricto: algunos datos inválidos no producen error sino una simple advertencia, y se guardan "corregidos" a la fuerza:
Quedó un producto sin nombre y otro con el nombre cortado y precio 0. Por eso: si la respuesta dice warning, ejecuta SHOW WARNINGS;. Con el modo estricto activado, esas sentencias se rechazan y no se guarda nada:
SET SESSION aplica solo a tu conexión. Para dejarlo permanente se edita la línea sql_mode= de C:\xampp\mysql\bin\my.ini y se reinicia MySQL. Modo actual: SELECT @@sql_mode;
Si el registro ya existe: IGNORE, ON DUPLICATE KEY UPDATE y REPLACE
Los ejemplos de esta parte y la siguiente parten de una tabla productos nueva con tres filas: P001 (id 1, stock 40), P002 (id 2) y P005 (id 3, categoría Papelería, stock 50).
| Sentencia | Si el código ya existe… | Respuesta |
|---|---|---|
INSERT | Error, no guarda nada. | ERROR 1062 |
INSERT IGNORE | Lo omite (solo deja una advertencia). | 0 rows affected, 1 warning |
INSERT ... ON DUPLICATE KEY UPDATE | Actualiza la fila existente. | 1 row = insertó · 2 rows = actualizó · 0 rows = no cambió nada |
REPLACE | Borra la fila vieja e inserta una nueva. | 2 rows affected (1 borrada + 1 insertada) |
P001 ya existía: se sumaron 5 a su stock (40 → 45). P011 no existía: se insertó. VALUES(stock) es el valor que se intentaba insertar.
REPLACE es peligroso: como borra la fila y crea otra, cambió el id (3 → 10) y la categoría y el stock volvieron a sus valores por defecto porque no se indicaron. Para modificar un registro existente, lo correcto es UPDATE (3.3) o ON DUPLICATE KEY UPDATE.
Los huecos en AUTO_INCREMENT son normales
El id 4 se "gastó" en el INSERT que falló. Lo mismo pasa con INSERT IGNORE, ON DUPLICATE KEY UPDATE, ROLLBACK o al borrar filas (por eso Audífonos quedó con el id 8). Un id automático no se reutiliza y no sirve para contar filas: para eso usa SELECT COUNT(*).
Varias inserciones como un bloque: transacciones
Cada INSERT se guarda de inmediato (autocommit). Con una transacción, un grupo de cambios se confirma (COMMIT) o se deshace (ROLLBACK) completo:
ROW_COUNT() dice cuántas filas afectó la sentencia anterior. Si cierras la consola sin COMMIT, la transacción se deshace sola.
Muchas inserciones desde un archivo .sql
Escribe los INSERT en un archivo de texto (Bloc de notas, guardado en UTF-8 con extensión .sql), por ejemplo C:\datos\productos.sql:
Con < se detiene en el primer error e indica la línea (ERROR 1062 (23000) at line 2: ...). Escribir en un archivo evita perder un INSERT largo por un error de tecleo.
Cargar datos desde un archivo CSV (Excel)
Un CSV es texto con un registro por línea y los campos separados por comas. Por ejemplo C:\datos\productos.csv:
| Cláusula | Significado |
|---|---|
--local-infile=1 | Opción al abrir la consola que permite leer archivos del equipo. Sin ella: ERROR 1148 (42000): The used command is not allowed with this MariaDB version. |
FIELDS TERMINATED BY ',' | Separador de campos. El Excel en español suele guardar con punto y coma: usa ';'. |
OPTIONALLY ENCLOSED BY '"' | Textos entre comillas dobles: así la coma de "Silla, ergonómica" no se confunde con un separador. |
LINES TERMINATED BY '\r\n' | Fin de línea de Windows (Excel, Bloc de notas). Para archivos de Linux: '\n'. |
IGNORE 1 LINES | Salta la fila de títulos. |
(codigo, nombre, ...) | Columna de la tabla para cada campo del archivo, en orden. |
Trucos de la consola
- ↑ / ↓ recuperan sentencias anteriores para corregirlas.
\ccancela la sentencia que estás escribiendo.- Si el indicador cambia a
'>o">, dejaste una comilla sin cerrar: ciérrala y escribe\c. - Clic derecho en la ventana de CMD pega el texto copiado.
3.2. Insertar datos en tablas relacionadas
Cuando una tabla tiene una llave foránea (FOREIGN KEY), cada valor de esa columna debe existir en la tabla relacionada.
La tabla referenciada se llama padre (por ejemplo categorias) y la que guarda la referencia se llama hija (articulos).
Regla 1: primero el padre, después el hijo
Si el padre no existe, el hijo se rechaza:
El mensaje dice qué tabla hija (articulos), qué restricción (fk_art_cat) y a qué tabla padre apunta (categorias).
Solución: insertar primero la categoría 3, o usar un id que exista.
AUTO_INCREMENT. Si un artículo falla y luego se inserta otro, este no recibe el id que esperabas, y cualquier fila hija que use
ese id fijo fallará o quedará relacionada con el registro equivocado. Por eso es mejor obtener el id real, como se muestra a continuación.
Regla 2: usar el id real, no suponerlo
Maestro-detalle: LAST_INSERT_ID() y una transacción
Un pedido (maestro) y sus líneas (detalle) deben guardarse juntos: o todo, o nada.
@pedidoes una variable de sesión: guarda el id del pedido para usarlo en las líneas de detalle sin escribirlo a mano.- Si alguna línea fallara, se ejecuta
ROLLBACK;en lugar deCOMMIT;y no queda un pedido sin líneas.
Copiar datos entre tablas: INSERT ... SELECT
En lugar de VALUES va un SELECT (con JOIN si hace falta). Las columnas del SELECT deben coincidir en cantidad y orden con la lista del INSERT.
Desactivar la verificación de llaves foráneas (solo para cargas especiales)
Con FOREIGN_KEY_CHECKS = 0 se pudo guardar un artículo apuntando a una categoría que no existe (un registro huérfano), y
volver a activarla no lo detecta. Los respaldos de mysqldump lo usan para cargar tablas en cualquier orden, pero en el trabajo diario no se debe usar.
Resumen
- Inserta primero las tablas padre y luego las hijas.
- No supongas los id: búscalos o guárdalos con
LAST_INSERT_ID(). - Agrupa maestro y detalle en una transacción.
- El error
1452siempre significa "el valor no existe en la tabla padre".
3.3. Modificar datos: UPDATE
WHERE, el UPDATE modifica todas las filas de la tabla.
Antes de un UPDATE, ejecuta un SELECT con el mismo WHERE para ver exactamente qué filas vas a cambiar.
Los ejemplos usan la tabla productos de la sección 3.1.
Revisar, actualizar y comprobar
- El nuevo valor puede calcularse a partir del actual:
precio = precio * 1.10sube el precio un 10 %,stock = stock + 5suma 5. - Rows matched = filas que cumplieron el WHERE. Changed = filas que realmente cambiaron.
Leer la respuesta: matched vs. changed
Rows matched: 0 no es un error: simplemente no había filas con ese código. Revisa el WHERE si esperabas cambios.
Varias columnas, varias condiciones y NULL
Para dejar una columna vacía se usa SET col = NULL; para buscar vacíos, WHERE col IS NULL (nunca = NULL).
Valores distintos según una condición: CASE
Este UPDATE no tiene WHERE a propósito: recorre las 10 filas (matched: 10) y solo cambian las 5 que no tenían ya el valor correcto (Changed: 5).
Limitar cuántas filas: ORDER BY + LIMIT
Suma 1 al stock de los 2 productos más caros. Sin ORDER BY, "los 2 primeros" serían arbitrarios.
Errores al actualizar
Las mismas reglas del INSERT se aplican al UPDATE: UNIQUE, NOT NULL, tipos de datos, modo estricto (si ves warning, usa SHOW WARNINGS;) y llaves foráneas (ver más abajo).
Protección: modo de actualizaciones seguras
Con sql_safe_updates = 1, MySQL solo permite UPDATE y DELETE cuyo WHERE use una columna con índice (como id o codigo) o que tengan LIMIT.
Evita el clásico "olvidé el WHERE". Se puede abrir la consola así desde el inicio: mysql -u root -p --safe-updates.
Probar antes de confirmar: transacciones
Dentro de una transacción, un UPDATE equivocado se deshace con ROLLBACK. Sin transacción (autocommit), el cambio es inmediato y la única salida es un respaldo.
UPDATE con tablas relacionadas
Con las tablas de la sección 3.2 (categorias y articulos, con ON UPDATE CASCADE):
- ON UPDATE CASCADE: al cambiar el id en el padre, los hijos se actualizan solos (Cafetera y Licuadora pasaron a la categoría 20).
- Sin
ON UPDATE CASCADE(o conRESTRICT), cambiar el id de un padre con hijos da el error1451.
UPDATE con una subconsulta sobre la misma tabla
Sube el 10 % a los productos más baratos que el promedio. En el MariaDB de XAMPP (10.3 o superior) funciona, pero en
MySQL la misma sentencia da ERROR 1093: You can't specify target table 'productos' for update in FROM clause.
La forma que funciona en ambos es envolver la subconsulta en una tabla derivada:
WHERE precio < (SELECT promedio FROM (SELECT AVG(precio) AS promedio FROM productos) AS t).
Resumen
- Primero un
SELECTcon el mismo WHERE; luego el UPDATE. - Lee
Rows matchedyChanged. - Para cambios grandes, usa
START TRANSACTIONy confirma conCOMMITsolo después de revisar. - Activa
sql_safe_updatessi trabajas sobre datos importantes.
3.4. Eliminar datos: DELETE y TRUNCATE
WHERE, DELETE borra todas las filas.
Primero ejecuta un SELECT con el mismo WHERE y revisa qué se va a borrar.
Revisar y eliminar
El SELECT previo mostró 8 filas y el DELETE confirmó 8 rows affected: coinciden, así que se borró exactamente lo esperado. 0 rows affected no es un error: ninguna fila cumplía la condición.
Borrar solo algunas: ORDER BY + LIMIT
Deshacer un DELETE: transacciones
Protección: modo de actualizaciones seguras
DELETE vs. TRUNCATE: el contador AUTO_INCREMENT
DELETE conserva el contador: el siguiente id siguió en 12. TRUNCATE vacía la tabla y reinicia el contador en 1.
Observa también que TRUNCATE responde 0 rows affected aunque haya borrado filas: no las borra una por una, sino que recrea la tabla vacía.
TRUNCATE no se puede deshacer
TRUNCATE es técnicamente una sentencia DDL (sección 2.10): hace un COMMIT automático y el ROLLBACK no recupera los datos.
DELETE con tablas relacionadas
Con las tablas de la sección 3.2. Lo que ocurre al borrar un padre depende de la opción ON DELETE de la llave foránea:
| Opción | Al borrar el padre… |
|---|---|
RESTRICT / NO ACTION (por defecto) | Se impide el borrado si tiene hijos (error 1451). |
CASCADE | Se borran también sus hijos automáticamente. |
SET NULL | Los hijos se conservan con la columna en NULL (la columna debe permitir NULL). |
Con RESTRICT hay que borrar (o reasignar) primero los hijos. Con CASCADE se borraron las 2 líneas del pedido aunque la respuesta dijo
1 row affected: el borrado en cascada no se cuenta en el mensaje. Úsalo con cuidado.
En el DELETE con JOIN, el alias escrito después de DELETE (DELETE a) indica de qué tabla se borran las filas.
DELETE, TRUNCATE y DROP: comparación
| DELETE | TRUNCATE | DROP TABLE | |
|---|---|---|---|
| Familia | DML | DDL | DDL |
| Qué elimina | Las filas que cumplen el WHERE (o todas) | Todas las filas | La tabla completa: estructura y datos |
| Admite WHERE | Sí | No | No |
| Contador AUTO_INCREMENT | Se conserva | Vuelve a 1 | Desaparece con la tabla |
| Se deshace con ROLLBACK | Sí, dentro de una transacción | No | No |
| Con llaves foráneas | Aplica RESTRICT / CASCADE / SET NULL | Error 1701 si es tabla padre | Error 1451 si es tabla padre |
| Velocidad en tablas grandes | Lento (fila por fila) | Muy rápido | Muy rápido |
Buena práctica: borrado lógico
En muchos sistemas no se borran los registros, sino que se marcan como inactivos para conservar el historial y poder recuperarlos:
DELETE con una subconsulta sobre la misma tabla
Igual que en UPDATE (3.3): la primera forma funciona en MariaDB 10.3+, pero en MySQL da el error 1093; la segunda funciona en ambos.
Resumen
- Antes de borrar:
SELECTcon el mismo WHERE y, si es importante, un respaldo (sección 11.1). - Usa
DELETEdentro de una transacción cuando quieras poder deshacerlo. - Usa
TRUNCATEsolo para vaciar una tabla por completo, sabiendo que no tiene vuelta atrás. - Conoce el
ON DELETEde tus llaves foráneas antes de borrar un registro padre.
4. Consultas SQL: SELECT y JOIN
Una consulta le pide datos a la base de datos sin modificarlos. Siempre empieza con SELECT
y su resultado es una tabla temporal (filas y columnas) que se muestra y luego desaparece.
En este módulo aprenderás a filtrar, calcular, resumir y combinar información de una o varias tablas.
duenos: personas dueñas de mascotas. Andrés no tiene mascotas.
| id | nombre | ciudad | telefono |
|---|---|---|---|
| 1 | Marta Rojas | Bogotá | 3001112233 |
| 2 | Jorge Silva | Medellín | NULL |
| 3 | Lucía Peña | Bogotá | 3104445566 |
| 4 | Andrés Mejía | Cali | 3157778899 |
mascotas.dueno_id apunta a duenos.id. Toby no tiene dueño (NULL) y Luna no tiene raza.
| id | nombre | especie | raza | edad | peso | dueno_id |
|---|---|---|---|---|---|---|
| 1 | Firulais | Perro | Labrador | 5 | 28.50 | 1 |
| 2 | Michi | Gato | Siamés | 3 | 4.20 | 1 |
| 3 | Rocky | Perro | Bulldog | 7 | 22.00 | 2 |
| 4 | Luna | Gato | NULL | 1 | 3.10 | 3 |
| 5 | Coco | Ave | Loro | 12 | 0.45 | 3 |
| 6 | Toby | Perro | Beagle | 2 | 11.30 | NULL |
| 7 | Nala | Gato | Persa | 4 | 5.00 | 2 |
citas.mascota_id apunta a mascotas.id. Toby y Nala nunca han tenido cita.
| id | mascota_id | fecha | motivo | costo |
|---|---|---|---|---|
| 1 | 1 | 2024-01-10 | Vacunación | 45000.00 |
| 2 | 2 | 2024-01-15 | Control | 30000.00 |
| 3 | 1 | 2024-02-20 | Cirugía | 350000.00 |
| 4 | 3 | 2024-02-22 | Vacunación | 45000.00 |
| 5 | 4 | 2024-03-05 | Control | 30000.00 |
| 6 | 3 | 2024-03-18 | Desparasitación | 25000.00 |
| 7 | 5 | 2024-04-02 | Control | 30000.00 |
| 8 | 1 | 2024-04-25 | Control | 30000.00 |
4.1. Anatomía de una consulta y orden de ejecución
Las cláusulas se escriben en ese orden, pero MySQL las ejecuta en este otro:
- FROM / JOIN
- WHERE
- GROUP BY
- HAVING
- SELECT
- ORDER BY
- LIMIT
Esto explica dos reglas que confunden al principio: WHERE no puede usar funciones de agregado (todavía no existen
los grupos) y ORDER BY sí puede usar los alias definidos en el SELECT (ya se calcularon).
Solo SELECT y FROM son obligatorias; las demás se usan cuando se necesitan.
4.2. SELECT: elegir columnas, alias y cálculos
Elige las columnas separadas por comas, en el orden en que quieres verlas. SELECT * trae todas, pero en los ejercicios debes mostrar exactamente las columnas pedidas.
| nombre | especie | edad |
|---|---|---|
| Firulais | Perro | 5 |
| Michi | Gato | 3 |
| Rocky | Perro | 7 |
| Luna | Gato | 1 |
| Coco | Ave | 12 |
| Toby | Perro | 2 |
| Nala | Gato | 4 |
Con AS renombras una columna en el resultado. También puedes calcular con + - * /.
| mascota | peso_gramos |
|---|---|
| Firulais | 28500.00 |
| Michi | 4200.00 |
| Rocky | 22000.00 |
| Luna | 3100.00 |
| Coco | 450.00 |
| Toby | 11300.00 |
| Nala | 5000.00 |
4.3. ORDER BY, LIMIT y OFFSET
ASC (por defecto) ordena de menor a mayor; DESC de mayor a menor.
| nombre | edad |
|---|---|
| Coco | 12 |
| Rocky | 7 |
| Firulais | 5 |
| Nala | 4 |
| Michi | 3 |
| Toby | 2 |
| Luna | 1 |
Con varias columnas, la segunda desempata a la primera: primero por especie y, dentro de cada especie, por nombre.
| especie | nombre |
|---|---|
| Ave | Coco |
| Gato | Luna |
| Gato | Michi |
| Gato | Nala |
| Perro | Firulais |
| Perro | Rocky |
| Perro | Toby |
LIMIT n devuelve solo las primeras n filas. Úsalo SIEMPRE después de ORDER BY para que "los primeros" tengan sentido (aquí: las 3 mascotas más pesadas).
| nombre | peso |
|---|---|
| Firulais | 28.50 |
| Rocky | 22.00 |
| Toby | 11.30 |
OFFSET n salta las primeras n filas: es la base de la paginación (página 2 de 3 en 3). También se escribe LIMIT 3, 3 (desde, cuántas).
| nombre | peso |
|---|---|
| Nala | 5.00 |
| Michi | 4.20 |
| Luna | 3.10 |
Al ordenar de forma ascendente, los NULL salen primero (Luna no tiene raza). Con DESC salen al final.
| nombre | raza |
|---|---|
| Luna | NULL |
| Toby | Beagle |
| Rocky | Bulldog |
| Firulais | Labrador |
| Coco | Loro |
| Nala | Persa |
| Michi | Siamés |
4.4. WHERE: filtrar filas
| Operador | Significado | Ejemplo |
|---|---|---|
= <> < > <= >= | Comparaciones (<> o != = distinto) | edad >= 5 |
AND OR NOT | Combinar condiciones. AND se evalúa antes que OR: usa paréntesis | a = 1 AND (b = 2 OR c = 3) |
BETWEEN a AND b | Rango incluyendo ambos extremos | precio BETWEEN 10 AND 20 |
IN (...) / NOT IN | Pertenece (o no) a una lista | ciudad IN ('Cali','Pasto') |
LIKE | Patrón de texto: % = cualquier cantidad de caracteres, _ = exactamente uno | nombre LIKE 'A%' |
IS NULL / IS NOT NULL | Valor ausente | email IS NULL |
| nombre | edad |
|---|---|
| Firulais | 5 |
| Rocky | 7 |
| Coco | 12 |
Perros que sean muy jóvenes o muy viejos. Sin los paréntesis, la condición cambiaría de significado.
| nombre | especie | edad |
|---|---|---|
| Rocky | Perro | 7 |
| Toby | Perro | 2 |
Las fechas se escriben como texto 'AAAA-MM-DD'.
| id | fecha | costo |
|---|---|---|
| 3 | 2024-02-20 | 350000.00 |
| 4 | 2024-02-22 | 45000.00 |
| 5 | 2024-03-05 | 30000.00 |
| 6 | 2024-03-18 | 25000.00 |
| nombre | especie |
|---|---|
| Michi | Gato |
| Luna | Gato |
| Coco | Ave |
| Nala | Gato |
| nombre | especie |
|---|---|
| Firulais | Perro |
| Rocky | Perro |
| Toby | Perro |
'%o%': contiene una "o" en cualquier posición.
| nombre |
|---|
| Rocky |
| Coco |
| Toby |
'_o%': la segunda letra es "o" (el _ ocupa exactamente un carácter).
| nombre |
|---|
| Rocky |
| Coco |
| Toby |
NULL significa "sin valor". Se pregunta con IS NULL…
| nombre | especie |
|---|---|
| Luna | Gato |
…y NUNCA con = NULL: cualquier comparación con NULL da "desconocido", por eso esta consulta no devuelve nada.
| nombre |
|---|
| (sin filas) |
4.5. DISTINCT: quitar repetidos
Hay 7 mascotas, pero solo 3 especies distintas.
| especie |
|---|
| Ave |
| Gato |
| Perro |
4.6. Funciones de fila (se aplican a cada fila)
| Tipo | Funciones más usadas |
|---|---|
| Texto | UPPER(t), LOWER(t), CONCAT(a, b, ...), CHAR_LENGTH(t), SUBSTRING(t, inicio, largo), TRIM(t), REPLACE(t, buscar, poner) |
| Números | ROUND(n, decimales), FLOOR(n) (hacia abajo), CEIL(n) (hacia arriba), ABS(n), MOD(a, b) |
| Fechas | YEAR(f), MONTH(f), DAY(f), DATEDIFF(f1, f2) (días), TIMESTAMPDIFF(unidad, inicio, fin), CURDATE(), NOW(), DATE_ADD(f, INTERVAL n DAY) |
| Condicionales y NULL | Ver la sección 4.7 |
| mayus | letras | ficha |
|---|---|---|
| COCO | 4 | Coco (Ave) |
| FIRULAIS | 8 | Firulais (Perro) |
| LUNA | 4 | Luna (Gato) |
| MICHI | 5 | Michi (Gato) |
| NALA | 4 | Nala (Gato) |
| ROCKY | 5 | Rocky (Perro) |
| TOBY | 4 | Toby (Perro) |
ROUND redondea al más cercano; FLOOR siempre hacia abajo.
| nombre | peso | redondeado | un_decimal | piso |
|---|---|---|---|---|
| Firulais | 28.50 | 29 | 28.5 | 28 |
| Michi | 4.20 | 4 | 4.2 | 4 |
| Rocky | 22.00 | 22 | 22.0 | 22 |
| Luna | 3.10 | 3 | 3.1 | 3 |
| Coco | 0.45 | 0 | 0.5 | 0 |
| Toby | 11.30 | 11 | 11.3 | 11 |
| Nala | 5.00 | 5 | 5.0 | 5 |
DATEDIFF(fin, inicio) devuelve la diferencia en días (ojo con el orden de los argumentos).
| id | fecha | anio | mes | dias_hasta_fin_de_anio |
|---|---|---|---|---|
| 1 | 2024-01-10 | 2024 | 1 | 356 |
| 2 | 2024-01-15 | 2024 | 1 | 351 |
| 3 | 2024-02-20 | 2024 | 2 | 315 |
| 4 | 2024-02-22 | 2024 | 2 | 313 |
| 5 | 2024-03-05 | 2024 | 3 | 301 |
| 6 | 2024-03-18 | 2024 | 3 | 288 |
| 7 | 2024-04-02 | 2024 | 4 | 273 |
| 8 | 2024-04-25 | 2024 | 4 | 250 |
TIMESTAMPDIFF(MONTH | YEAR | DAY, inicio, fin) cuenta unidades completas: ideal para calcular edades o antigüedad.
| id | fecha | meses_completos |
|---|---|---|
| 1 | 2024-01-10 | 11 |
| 3 | 2024-02-20 | 10 |
| 8 | 2024-04-25 | 8 |
4.7. Manejo de NULL y funciones condicionales
NULL significa "valor desconocido o ausente". Casi cualquier operación con NULL da NULL, así que hay funciones específicas para tratarlo y para elegir valores según una condición:
| Función | Devuelve | Ejemplo → resultado |
|---|---|---|
COALESCE(a, b, c, …) | El primer valor que no sea NULL (estándar SQL, acepta muchos valores) | COALESCE(NULL, NULL, 'c') → c |
IFNULL(a, b) | b si a es NULL; si no, a (solo 2 valores) | IFNULL(NULL, 'x') → x |
NVL(a, b) / NVL2(a, b, c) | Igual que IFNULL / "si no es NULL b, si es NULL c". Solo MariaDB (vienen de Oracle; no existen en MySQL) | NVL2(NULL, 'a', 'b') → b |
NULLIF(a, b) | NULL si a = b; si no, a. Convierte un valor "especial" en NULL | NULLIF(5, 5) → NULL |
IF(cond, si, no) | Un CASE corto de dos opciones (propio de MySQL/MariaDB) | IF(1 > 2, 'si', 'no') → no |
CASE WHEN cond THEN … END | Evalúa condiciones en orden (forma "buscada") | ver ejemplo |
CASE col WHEN valor THEN … END | Compara una columna con valores fijos (forma "simple") | ver ejemplo |
GREATEST(…) / LEAST(…) | El mayor / menor de varios valores de la misma fila (MAX y MIN comparan entre filas) | LEAST(3, 7) → 3 |
CONCAT_WS(sep, …) | Une textos con un separador saltando los NULL | CONCAT_WS('-', 'a', NULL, 'b') → a-b |
ISNULL(a) | 1 si es NULL, 0 si no | ISNULL(NULL) → 1 |
a <=> b | Igualdad "segura": trata NULL como un valor comparable | NULL <=> NULL → 1 |
COALESCE reemplaza los NULL por un valor legible.
| nombre | contacto |
|---|---|
| Marta Rojas | 3001112233 |
| Jorge Silva | Sin teléfono |
| Lucía Peña | 3104445566 |
| Andrés Mejía | 3157778899 |
IFNULL hace lo mismo con dos valores. Úsalo cuando solo hay un posible reemplazo.
| nombre | raza |
|---|---|
| Firulais | Labrador |
| Michi | Siamés |
| Rocky | Bulldog |
| Luna | Mestizo |
| Coco | Loro |
| Toby | Beagle |
| Nala | Persa |
CASE "buscado": evalúa las condiciones en orden y usa la primera que se cumpla. Por eso la condición más exigente va primero.
| nombre | peso | tamano |
|---|---|---|
| Firulais | 28.50 | Grande |
| Rocky | 22.00 | Grande |
| Toby | 11.30 | Mediano |
| Nala | 5.00 | Mediano |
| Michi | 4.20 | Pequeño |
| Luna | 3.10 | Pequeño |
| Coco | 0.45 | Pequeño |
CASE "simple": compara una sola columna con valores fijos. Es más corto cuando todas las condiciones son igualdades.
| nombre | especie | familia |
|---|---|---|
| Firulais | Perro | Canino |
| Michi | Gato | Felino |
| Rocky | Perro | Canino |
| Luna | Gato | Felino |
| Coco | Ave | Otro |
| Toby | Perro | Canino |
| Nala | Gato | Felino |
IF sirve cuando solo hay dos opciones.
| nombre | edad | etapa |
|---|---|---|
| Firulais | 5 | Adulto |
| Michi | 3 | Adulto |
| Rocky | 7 | Senior |
| Luna | 1 | Adulto |
| Coco | 12 | Senior |
| Toby | 2 | Adulto |
| Nala | 4 | Adulto |
NULLIF(motivo, 'Control') vuelve NULL las citas de control, y COUNT(columna) no cuenta los NULL: así se cuentan solo las que no son control.
| citas | no_son_control |
|---|---|
| 8 | 4 |
Uso clásico de NULLIF: evitar dividir por cero. Para Luna (edad 1) el divisor sería 0; NULLIF(edad - 1, 0) lo convierte en NULL y el resultado es NULL en lugar de un error (en modo estricto) o una advertencia.
| nombre | edad | kg_por_anio_extra |
|---|---|---|
| Firulais | 5 | 7.13 |
| Michi | 3 | 2.10 |
| Rocky | 7 | 3.67 |
| Luna | 1 | NULL |
| Coco | 12 | 0.04 |
| Toby | 2 | 11.30 |
| Nala | 4 | 1.67 |
GREATEST fija un valor mínimo (nunca menos de 40.000) y LEAST un tope (nunca más de 100.000), fila por fila.
| id | costo | cobro_minimo | cobro_con_tope |
|---|---|---|---|
| 1 | 45000.00 | 45000.00 | 45000.00 |
| 2 | 30000.00 | 40000.00 | 30000.00 |
| 3 | 350000.00 | 350000.00 | 100000.00 |
| 4 | 45000.00 | 45000.00 | 45000.00 |
| 5 | 30000.00 | 40000.00 | 30000.00 |
| 6 | 25000.00 | 40000.00 | 25000.00 |
| 7 | 30000.00 | 40000.00 | 30000.00 |
| 8 | 30000.00 | 40000.00 | 30000.00 |
Jorge no tiene teléfono: CONCAT con un NULL devuelve todo NULL, mientras que CONCAT_WS simplemente lo salta.
| nombre | con_concat | con_concat_ws |
|---|---|---|
| Marta Rojas | Marta Rojas - 3001112233 | Marta Rojas - 3001112233 |
| Jorge Silva | NULL | Jorge Silva |
| Lucía Peña | Lucía Peña - 3104445566 | Lucía Peña - 3104445566 |
| Andrés Mejía | Andrés Mejía - 3157778899 | Andrés Mejía - 3157778899 |
Las trampas de NULL en una sola consulta
NULL = NULL no es verdadero sino NULL (desconocido); <=> e IS NULL sí responden 1. Un solo NULL vuelve NULL a GREATEST y hace que NOT IN (…, NULL) nunca sea verdadero: un NOT IN con un NULL en la lista no devuelve ninguna fila.
| igual | igual_seguro | es_null | greatest_con_null | not_in_con_null | es_desconocido |
|---|---|---|---|---|---|
| NULL | 1 | 1 | NULL | NULL | 1 |
4.8. Funciones de agregado (resumen muchas filas en una)
| Función | Qué calcula | Con NULL |
|---|---|---|
COUNT(*) | Número de filas | Cuenta todas las filas |
COUNT(columna) | Número de valores | Ignora los NULL |
COUNT(DISTINCT columna) | Número de valores distintos | Ignora los NULL |
SUM, AVG | Suma y promedio | Ignoran los NULL |
MIN, MAX | Menor y mayor valor (también con textos y fechas) | Ignoran los NULL |
Hay 7 mascotas, pero solo 6 tienen raza registrada (Luna tiene NULL).
| total | con_raza | especies |
|---|---|---|
| 7 | 6 | 3 |
Usa ROUND(AVG(...), 2) cuando el enunciado pida 2 decimales.
| facturado | promedio | minimo | maximo |
|---|---|---|---|
| 585000.00 | 73125.00 | 25000.00 | 350000.00 |
Primero WHERE filtra las filas y luego se agrega solo lo que quedó.
| controles | total |
|---|---|
| 4 | 120000.00 |
4.9. GROUP BY y HAVING
GROUP BY parte las filas en grupos (uno por cada valor distinto) y aplica las funciones de agregado
a cada grupo. Regla de oro: toda columna del SELECT que no esté dentro de una función de agregado
debe aparecer en el GROUP BY.
Una fila por especie, con su cantidad y su peso promedio.
| especie | cantidad | peso_promedio |
|---|---|---|
| Ave | 1 | 0.45 |
| Gato | 3 | 4.10 |
| Perro | 3 | 20.60 |
También se puede agrupar por un cálculo, por ejemplo el mes de una fecha.
| mes | citas | total |
|---|---|---|
| 1 | 2 | 75000.00 |
| 2 | 2 | 395000.00 |
| 3 | 2 | 55000.00 |
| 4 | 2 | 60000.00 |
| WHERE | HAVING | |
|---|---|---|
| ¿Qué filtra? | Filas individuales | Grupos ya formados |
| ¿Cuándo actúa? | Antes de GROUP BY | Después de GROUP BY |
| ¿Puede usar COUNT, SUM, AVG…? | No | Sí |
Solo las especies con 2 o más mascotas (el Ave queda fuera).
| especie | cantidad |
|---|---|
| Gato | 3 |
| Perro | 3 |
Ambos a la vez: WHERE descarta las citas costosas y HAVING descarta los motivos que aparecen una sola vez.
| motivo | veces |
|---|---|
| Control | 4 |
| Vacunación | 2 |
4.10. JOIN: combinar dos tablas relacionadas
Las tablas se relacionan con claves foráneas: mascotas.dueno_id guarda el id de un dueño.
Un JOIN junta cada fila de una tabla con las filas de la otra que cumplan la condición ON.
Usa alias cortos (mascotas m) y antepón el alias a cada columna (m.nombre) para evitar ambigüedades.
| Tipo | ¿Qué filas conserva? | Si no hay pareja… |
|---|---|---|
[INNER] JOIN | Solo las que tienen pareja en ambas tablas | La fila desaparece |
LEFT JOIN | TODAS las de la tabla izquierda (la del FROM) | Las columnas de la derecha quedan NULL |
RIGHT JOIN | TODAS las de la tabla derecha (la del JOIN) | Las columnas de la izquierda quedan NULL |
INNER JOIN
Toby no aparece porque no tiene dueño (no hay pareja).
| mascota | dueno |
|---|---|
| Coco | Lucía Peña |
| Firulais | Marta Rojas |
| Luna | Lucía Peña |
| Michi | Marta Rojas |
| Nala | Jorge Silva |
| Rocky | Jorge Silva |
Después de unir, WHERE puede filtrar por columnas de cualquiera de las tablas.
| nombre | fecha | motivo |
|---|---|---|
| Michi | 2024-01-15 | Control |
| Luna | 2024-03-05 | Control |
| Coco | 2024-04-02 | Control |
| Firulais | 2024-04-25 | Control |
LEFT JOIN
Todas las mascotas aparecen; las que no tienen citas muestran NULL en la fecha.
| nombre | fecha |
|---|---|
| Coco | 2024-04-02 |
| Firulais | 2024-01-10 |
| Firulais | 2024-02-20 |
| Firulais | 2024-04-25 |
| Luna | 2024-03-05 |
| Michi | 2024-01-15 |
| Nala | NULL |
| Rocky | 2024-02-22 |
| Rocky | 2024-03-18 |
| Toby | NULL |
Patrón "sin relación": LEFT JOIN + WHERE derecha.id IS NULL encuentra las mascotas que nunca han tenido cita.
| nombre |
|---|
| Nala |
| Toby |
RIGHT JOIN
Conserva todos los dueños (la tabla de la derecha), incluso Andrés, que no tiene mascotas. Un RIGHT JOIN equivale a un LEFT JOIN con las tablas invertidas.
| dueno | mascota |
|---|---|
| Andrés Mejía | NULL |
| Jorge Silva | Nala |
| Jorge Silva | Rocky |
| Lucía Peña | Coco |
| Lucía Peña | Luna |
| Marta Rojas | Firulais |
| Marta Rojas | Michi |
4.11. Varias tablas, agregados y auto-JOIN
Se encadenan los JOIN: dueños → mascotas → citas. Cada JOIN lleva su propia condición ON.
| dueno | mascota | fecha | costo |
|---|---|---|---|
| Marta Rojas | Firulais | 2024-01-10 | 45000.00 |
| Marta Rojas | Michi | 2024-01-15 | 30000.00 |
| Marta Rojas | Firulais | 2024-02-20 | 350000.00 |
| Jorge Silva | Rocky | 2024-02-22 | 45000.00 |
| Lucía Peña | Luna | 2024-03-05 | 30000.00 |
| Jorge Silva | Rocky | 2024-03-18 | 25000.00 |
| Lucía Peña | Coco | 2024-04-02 | 30000.00 |
| Marta Rojas | Firulais | 2024-04-25 | 30000.00 |
JOIN + GROUP BY: cuánto ha gastado cada dueño. Agrupa por d.id, d.nombre por si hay dos personas con el mismo nombre.
| dueno | citas | gastado |
|---|---|---|
| Marta Rojas | 4 | 455000.00 |
| Jorge Silva | 2 | 70000.00 |
| Lucía Peña | 2 | 60000.00 |
| dueno | gastado |
|---|---|
| Marta Rojas | 455000.00 |
Auto-JOIN: con dos alias de la misma tabla se comparan filas entre sí (pares de mascotas del mismo dueño). a.id < b.id evita repetir pares e impide emparejar una mascota consigo misma.
| mascota_1 | mascota_2 | dueno_id |
|---|---|---|
| Firulais | Michi | 1 |
| Luna | Coco | 3 |
| Rocky | Nala | 2 |
4.12. Trampas frecuentes (con prueba)
✗ COUNT(*) con LEFT JOIN
Andrés aparece con 1 mascota, ¡pero no tiene ninguna! COUNT(*) cuenta la fila de NULLs.
| nombre | mascotas |
|---|---|
| Andrés Mejía | 1 |
| Jorge Silva | 2 |
| Lucía Peña | 2 |
| Marta Rojas | 2 |
✓ COUNT(columna_de_la_derecha)
COUNT(m.id) ignora los NULL: ahora Andrés tiene 0.
| nombre | mascotas |
|---|---|
| Andrés Mejía | 0 |
| Jorge Silva | 2 |
| Lucía Peña | 2 |
| Marta Rojas | 2 |
✗ Filtrar la tabla derecha en WHERE
El WHERE elimina las filas con NULL: el LEFT JOIN se convierte en INNER JOIN y se pierden mascotas.
| nombre | motivo |
|---|---|
| Coco | Control |
| Firulais | Control |
| Luna | Control |
| Michi | Control |
✓ Filtrar la tabla derecha en el ON
Con la condición en el ON se conservan todas las mascotas.
| nombre | motivo |
|---|---|
| Coco | Control |
| Firulais | Control |
| Luna | Control |
| Michi | Control |
| Nala | NULL |
| Rocky | NULL |
| Toby | NULL |
✗ JOIN sin condición ON
Sin ON se combina cada mascota con cada dueño (7 × 4 = 28 filas): un producto cartesiano casi siempre incorrecto.
| filas |
|---|
| 28 |
- Usar
= NULLen vez deIS NULL: nunca encuentra nada. - Poner una función de agregado en WHERE (
WHERE COUNT(*) > 2): es un error; va en HAVING. - Mostrar una columna que no está en el GROUP BY ni dentro de un agregado: el valor es arbitrario o da error.
- Olvidar el desempate en ORDER BY cuando el enunciado lo pide ("en empate, por nombre").
- Un NULL dentro de un
NOT INhace que la condición nunca se cumpla (ver 4.7). - Comillas: los textos y fechas van entre comillas simples (
'Cali'), los números sin comillas.
4.13. GROUP_CONCAT y WITH ROLLUP
GROUP_CONCAT junta en un solo texto los valores de cada grupo (con su propio ORDER BY y separador).
| dueno | mascotas |
|---|---|
| Jorge Silva | Nala, Rocky |
| Lucía Peña | Coco, Luna |
| Marta Rojas | Firulais, Michi |
WITH ROLLUP agrega una fila final de total general (la que tiene NULL en la columna agrupada).
| especie | cantidad |
|---|---|
| Ave | 1 |
| Gato | 3 |
| Perro | 3 |
| NULL | 7 |
4.14. CROSS JOIN, FULL OUTER JOIN y UNION
CROSS JOIN combina todas las filas con todas, a propósito (aquí, el dueño de Cali con cada mascota). Útil para generar combinaciones.
| dueno | mascota |
|---|---|
| Andrés Mejía | Coco |
| Andrés Mejía | Firulais |
| Andrés Mejía | Luna |
| Andrés Mejía | Michi |
| Andrés Mejía | Nala |
| Andrés Mejía | Rocky |
| Andrés Mejía | Toby |
UNION ALL apila resultados de varias consultas (mismo número de columnas). UNION sin ALL además quita filas repetidas.
| nombre | tipo |
|---|---|
| Marta Rojas | dueño |
| Lucía Peña | dueño |
| Coco | mascota |
MySQL/MariaDB no tiene FULL OUTER JOIN: se simula uniendo un LEFT y un RIGHT JOIN. Aparecen tanto Andrés (sin mascotas) como Toby (sin dueño).
| dueno | mascota |
|---|---|
| NULL | Toby |
| Andrés Mejía | NULL |
| Jorge Silva | Nala |
| Jorge Silva | Rocky |
| Lucía Peña | Coco |
| Lucía Peña | Luna |
| Marta Rojas | Firulais |
| Marta Rojas | Michi |
4.15. Subconsultas y EXISTS
Una subconsulta es un SELECT dentro de otro. Puede ir en el WHERE, en el FROM o en el SELECT.
Subconsulta que devuelve un valor: mascotas más pesadas que el promedio.
| nombre | peso |
|---|---|
| Firulais | 28.50 |
| Rocky | 22.00 |
| Toby | 11.30 |
Subconsulta que devuelve una lista para usar con IN: dueños que tienen gatos.
| nombre |
|---|
| Jorge Silva |
| Lucía Peña |
| Marta Rojas |
NOT EXISTS: mascotas para las que no existe ninguna cita (mismo resultado que el patrón LEFT JOIN … IS NULL de 4.10).
| nombre |
|---|
| Nala |
| Toby |
Subconsulta en el FROM (tabla derivada): se le da un alias y se consulta como si fuera una tabla.
| dueno | gastado |
|---|---|
| Marta Rojas | 455000.00 |
| Jorge Silva | 70000.00 |
| Lucía Peña | 60000.00 |
4.16. CTE (WITH) y funciones de ventana
Una CTE (WITH nombre AS (…)) da nombre a una subconsulta y la deja al inicio: la misma consulta de 4.15, más fácil de leer.
| dueno | gastado |
|---|---|
| Marta Rojas | 455000.00 |
| Jorge Silva | 70000.00 |
| Lucía Peña | 60000.00 |
Una CTE recursiva genera filas que no existen en ninguna tabla (los meses 1 a 6). Con LEFT JOIN aparecen también los meses sin citas (5 y 6).
| mes | citas |
|---|---|
| 1 | 2 |
| 2 | 2 |
| 3 | 2 |
| 4 | 2 |
| 5 | 0 |
| 6 | 0 |
Las funciones de ventana (OVER) calculan sobre un grupo de filas sin juntarlas en una: aquí, el puesto de cada mascota por peso dentro de su especie.
| nombre | especie | peso | puesto |
|---|---|---|---|
| Coco | Ave | 0.45 | 1 |
| Nala | Gato | 5.00 | 1 |
| Michi | Gato | 4.20 | 2 |
| Luna | Gato | 3.10 | 3 |
| Firulais | Perro | 28.50 | 1 |
| Rocky | Perro | 22.00 | 2 |
| Toby | Perro | 11.30 | 3 |
SUM(…) OVER (ORDER BY fecha) calcula un acumulado fila por fila.
| id | fecha | costo | acumulado |
|---|---|---|---|
| 1 | 2024-01-10 | 45000.00 | 45000.00 |
| 2 | 2024-01-15 | 30000.00 | 75000.00 |
| 3 | 2024-02-20 | 350000.00 | 425000.00 |
| 4 | 2024-02-22 | 45000.00 | 470000.00 |
| 5 | 2024-03-05 | 30000.00 | 500000.00 |
| 6 | 2024-03-18 | 25000.00 | 525000.00 |
| 7 | 2024-04-02 | 30000.00 | 555000.00 |
| 8 | 2024-04-25 | 30000.00 | 585000.00 |
4.17. EXPLAIN: ¿usa mi consulta un índice?
EXPLAIN antes de una consulta no la ejecuta: muestra el plan que seguirá el servidor para resolverla,
es decir, qué tablas lee, en qué orden, si usa un índice (2.5) y cuántas filas calcula que tendrá que revisar.
Es la forma de comprobar si un índice sirve. Ejemplo con una tabla pedidos de 50.000 filas:
type = ALL y key = NULL: recorre toda la tabla (unas 49.071 filas estimadas) para encontrar los 25 pedidos del cliente. Después de crear un índice:
Ahora usa el índice (key) y lee solo 25 filas en lugar de casi 50.000.
| Columna | Qué mirar |
|---|---|
type | Cómo busca, de peor a mejor: ALL (toda la tabla) → index (todo el índice) → range (un rango del índice) → ref (filas con un valor) → eq_ref / const (una sola fila). |
possible_keys / key | Índices que podría usar / el que realmente usa. key = NULL = no usa ninguno. |
rows | Filas que calcula revisar (es una estimación). Cuanto menor, mejor. |
Extra | Using index = todo sale del índice (muy bueno). Using temporary o Using filesort = necesita una tabla temporal u ordenar aparte (costoso en tablas grandes). |
Trampa frecuente: envolver la columna en una función impide usar el índice. Con un índice en fecha:
- Las dos consultas devuelven lo mismo, pero solo la segunda aprovecha el índice: compara la columna sola contra un rango.
- Lo mismo pasa con
LIKE '%texto'(comodín al inicio): no puede usar el índice.LIKE 'texto%'sí. - En un
JOIN,EXPLAINmuestra una fila por tabla. La columna de la llave foránea debería tener índice (type = refen la tabla hija); si apareceALLen una tabla grande, falta un índice. - En tablas pequeñas (unas cientas de filas) el servidor puede preferir leer toda la tabla aunque exista un índice: es normal.
4.18. Cómo responder en la plataforma
- Escribe una sola consulta que empiece con
SELECT. No se permiten subconsultas (un SELECT dentro de otro) niUNION. - Muestra exactamente las columnas pedidas y en ese orden. Los nombres o alias de las columnas no importan.
- Si el enunciado pide un orden, debes escribir
ORDER BYy el orden de las filas se evalúa; si no lo pide, el orden no importa. Aunque a veces las filas salgan ordenadas sin él (por ejemplo, después de unGROUP BY), la base de datos no lo garantiza, así que sinORDER BYla respuesta no se acepta. - Si el enunciado pide redondear, usa
ROUND(valor, 2). - Tu consulta se prueba con los datos que ves y con otros datos ocultos: si escribes valores fijos (por ejemplo,
WHERE id IN (2, 5)en vez de la condición real), no aprobará. - Algunos retos exigen una palabra clave (por ejemplo
LEFT JOIN,HAVINGoCOALESCE): aparece indicada en el reto. - Si fallas, verás cuántas filas y columnas devolvió tu consulta y una vista previa de tu resultado para comparar con lo pedido.
4.19. Chuleta: cómo armar una consulta paso a paso
- ¿De qué tablas salen los datos? →
FROMy, si son varias,JOIN ... ONpor sus claves. - ¿Deben aparecer también los que no tienen relación? →
LEFT/RIGHT JOIN. ¿Solo los que no la tienen? →LEFT JOIN ... WHERE derecha.id IS NULL. - ¿Qué filas sirven? →
WHERE. - ¿Pide "por cada…", "cuántos por…", "total por…"? →
GROUP BY+ función de agregado. - ¿Pide "solo los que tengan más de…" sobre un total? →
HAVING. - ¿Hay valores vacíos que mostrar de otra forma? →
COALESCE/IFNULL. - ¿Qué columnas y en qué orden? →
SELECT. - ¿Pide un orden o "los N primeros"? →
ORDER BY(+ desempate) yLIMIT.
5. Transacciones: todo o nada
Una transacción es un grupo de sentencias que la base de datos trata como una sola unidad: o se guardan todas,
o no se guarda ninguna. Sus sentencias forman la familia TCL (Transaction Control Language):
START TRANSACTION, COMMIT, ROLLBACK y SAVEPOINT.
CREATE TABLE cuentas (id INT PRIMARY KEY, titular VARCHAR(30), saldo DECIMAL(12,2));
5.1. ¿Qué es una transacción?
Piensa en una transferencia bancaria de 100.000 de Ana a Luis. Son dos sentencias: restar a Ana y sumar a Luis. Si el servidor se apaga justo después de la primera, el dinero desaparece: a Ana se le restó y a Luis nunca se le sumó. Una transacción evita ese estado intermedio: hasta que no se confirma, ningún cambio es definitivo y, si algo falla, todos se deshacen.
START TRANSACTION
Inicia la transacción. Los cambios siguientes quedan "pendientes".
COMMIT
Confirma: todos los cambios pendientes se guardan de forma definitiva.
ROLLBACK
Deshace: todos los cambios pendientes se descartan, como si no hubieran ocurrido.
5.2. START TRANSACTION, COMMIT y ROLLBACK
La transferencia completa, confirmada con COMMIT:
Un error grave (un UPDATE sin WHERE) deshecho con ROLLBACK:
- Mientras la transacción está abierta, tú ves tus cambios pendientes; los demás usuarios todavía no (5.11).
- Si cierras la consola o se pierde la conexión sin
COMMIT, la transacción se deshace sola. BEGIN;es un sinónimo deSTART TRANSACTION;en la consola (dentro de un procedimiento usaSTART TRANSACTION, porque allíBEGINabre un bloque).- En el módulo de DML verás este mismo recurso aplicado: revisar un
UPDATE(3.3) o unDELETE(3.4) antes de confirmarlo, y guardar un pedido con sus líneas (3.2).
5.3. autocommit: cada sentencia es su propia transacción
Por defecto autocommit está activado (1): cada sentencia que no está dentro de un START TRANSACTION
se confirma sola e inmediatamente. Por eso un UPDATE escrito "suelto" ya no se puede deshacer.
Si se desactiva en la sesión, todas las sentencias quedan pendientes hasta un COMMIT o un ROLLBACK:
La forma recomendada es dejar autocommit = 1 y abrir una transacción con START TRANSACTION solo cuando se necesita:
así queda claro dónde empieza y dónde termina.
5.4. Propiedades ACID
Una base de datos transaccional garantiza cuatro propiedades, conocidas por sus iniciales en inglés:
| Propiedad | Significa | En la transferencia |
|---|---|---|
| Atomicidad | Todo o nada: la transacción no se divide. | No puede quedar el retiro de Ana sin el depósito a Luis. |
| Consistencia | Se pasa de un estado válido a otro válido: se respetan las reglas (llaves foráneas, CHECK, UNIQUE). | La suma de los saldos es la misma antes y después. |
| Islamiento (Isolation) | Las transacciones simultáneas no se estorban: cada una ve los datos como si estuviera sola (5.8 a 5.11). | Otro usuario no ve el dinero "en el aire" a mitad de la transferencia. |
| Durabilidad | Lo confirmado con COMMIT sobrevive a un apagón o a un reinicio. | Después del COMMIT, la transferencia no se pierde aunque el servidor se apague. |
5.5. Puntos de guardado: SAVEPOINT
Un SAVEPOINT marca un punto dentro de la transacción. ROLLBACK TO nombre deshace solo lo que se hizo
después de ese punto; lo anterior sigue pendiente y se puede confirmar.
- Puede haber varios savepoints con nombres distintos;
RELEASE SAVEPOINT nombreelimina uno sin deshacer nada. - Un
ROLLBACKsinTOsigue deshaciendo toda la transacción.
5.6. Lo que una transacción no puede deshacer
- Sentencias DDL (
CREATE,ALTER,DROP,RENAME,TRUNCATE): hacen un COMMIT automático antes y después de ejecutarse. Además de no poder deshacerse, confirman lo que estuviera pendiente. Ver el ejemplo en 2.10. - Tablas que no son InnoDB. Solo el motor InnoDB (el predeterminado) maneja transacciones. En una tabla MyISAM, el
ROLLBACKno la toca y avisa con una advertencia:
Para saber el motor de cada tabla: SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'banco';.
Tampoco se deshacen los valores AUTO_INCREMENT ya usados: el siguiente id salta el número "gastado" (3.1).
5.7. Transacciones en procedimientos, funciones y triggers
| Objeto | ¿Puede usar START TRANSACTION, COMMIT y ROLLBACK? | Dónde se explica |
|---|---|---|
| Procedimiento | Sí. Es el lugar habitual para una operación de varios pasos; se combina con un HANDLER que hace ROLLBACK si algo falla. | 6.9 |
| Función | No. Una función se usa dentro de otras sentencias y no puede confirmar ni deshacer. | 7.9 |
| Trigger | No. Forma parte de la transacción de la sentencia que lo disparó: si el trigger falla, se deshace también esa sentencia. | 8.12 |
5.8. Dos usuarios a la vez: bloqueos de fila
Cuando una transacción modifica una fila, InnoDB la bloquea hasta el COMMIT o el ROLLBACK.
Si otra sesión quiere modificar la misma fila, espera. Las demás filas siguen libres: el bloqueo es por fila, no por tabla.
En el ejemplo, la sesión A empieza primero y la B un segundo después (dos consolas abiertas a la vez):
B tardó 2.940 s: estuvo esperando a que A confirmara. Ningún cambio se perdió (350.000 − 10.000 + 20.000 = 360.000).
Si la espera supera innodb_lock_wait_timeout (50 segundos por defecto), la sentencia de B se cancela:
Por eso una transacción debe ser corta: mientras está abierta, bloquea a los demás. Nunca dejes una transacción abierta mientras esperas a un usuario.
5.9. Leer para modificar: SELECT … FOR UPDATE
Un SELECT normal no bloquea. Si dos sesiones leen el mismo saldo, calculan el nuevo valor en la aplicación y lo guardan,
la segunda escritura borra la primera: se produce una actualización perdida. SELECT ... FOR UPDATE lee la fila y la
bloquea como si ya se fuera a modificar; la otra sesión espera y luego lee el valor actualizado:
B no leyó el saldo viejo (360.000) sino el ya descontado por A (300.000). Se usa siempre que se lee un valor para decidir con él
(¿hay saldo suficiente?, ¿hay stock?) y luego modificarlo. LOCK IN SHARE MODE hace un bloqueo más débil: permite que otros lean, pero no que modifiquen.
5.10. Interbloqueos (deadlock)
Un interbloqueo ocurre cuando dos transacciones se esperan mutuamente: A tiene la fila 1 y quiere la 2; B tiene la 2 y quiere la 1. Ninguna puede seguir. InnoDB lo detecta de inmediato, cancela una de las dos (deshace toda su transacción) y deja continuar a la otra:
- A recibió el error 1213 y toda su transacción se deshizo, incluido su primer
UPDATE. La aplicación debe volver a intentarla. - Para evitarlos: accede a las filas siempre en el mismo orden (por ejemplo, de menor a mayor id) y mantén las transacciones cortas.
5.11. Niveles de aislamiento
El nivel de aislamiento decide qué cambios de otras transacciones puede ver la tuya mientras está abierta. Más aislamiento = menos sorpresas, pero más esperas. Cada nivel evita distintos problemas:
- Lectura sucia: leer un cambio que otra transacción todavía no confirma (y que quizá deshaga).
- Lectura no repetible: leer la misma fila dos veces en una transacción y obtener valores distintos porque otra la modificó y confirmó.
- Fila fantasma: repetir una consulta y que aparezcan filas nuevas que otra transacción insertó.
| Nivel | Lectura sucia | Lectura no repetible | Fila fantasma |
|---|---|---|---|
READ UNCOMMITTED | Puede ocurrir | Puede ocurrir | Puede ocurrir |
READ COMMITTED | Se evita | Puede ocurrir | Puede ocurrir |
REPEATABLE READ predeterminado | Se evita | Se evita | InnoDB la evita en lecturas normales |
SERIALIZABLE | Se evita | Se evita | Se evita |
En los tres ejemplos, la sesión A lee un saldo, espera dos segundos y lo vuelve a leer; mientras tanto, la sesión B lo modifica.
REPEATABLE READ: A ve siempre lo mismoA trabaja con una "foto" de los datos tomada en su primera lectura.
READ COMMITTED: A ve lo confirmadoCada lectura ve lo último que se confirmó.
READ UNCOMMITTED: A ve lo no confirmadoA tomó una decisión con un valor que nunca se guardó.
Lo habitual es dejar el predeterminado (REPEATABLE READ) y usar SELECT ... FOR UPDATE (5.9) cuando se lee para modificar.
5.12. Buenas prácticas
- Transacciones cortas: abre, haz los cambios y confirma. Nunca esperes a que un usuario responda con la transacción abierta.
- Siempre termina con
COMMIToROLLBACK; revisa los resultados (ROW_COUNT(), unSELECT) antes de confirmar. - Usa
SELECT ... FOR UPDATEcuando leas un valor para decidir con él y luego modificarlo. - Accede a las filas en el mismo orden en todas las operaciones para evitar interbloqueos, y prepara la aplicación para reintentar ante un error 1213 o 1205.
- No mezcles DDL dentro de una transacción: confirma lo pendiente sin avisar (5.6).
- Usa tablas InnoDB; MyISAM no admite transacciones ni llaves foráneas.
5.13. Chuleta
| Quiero… | Sentencia |
|---|---|
| Iniciar una transacción | START TRANSACTION; |
| Guardar todos los cambios | COMMIT; |
| Deshacer todos los cambios | ROLLBACK; |
| Marcar un punto / volver a él | SAVEPOINT p1; · ROLLBACK TO p1; |
| Ver / cambiar autocommit | SELECT @@autocommit; · SET autocommit = 0; |
| Leer y bloquear para modificar | SELECT ... FOR UPDATE; |
| Ver / cambiar el nivel de aislamiento | SELECT @@tx_isolation; · SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; |
| Cambiar el tiempo máximo de espera | SET SESSION innodb_lock_wait_timeout = 10; |
6. Procedimientos almacenados
6.1. ¿Qué es un procedimiento y cuándo usarlo?
Un procedimiento almacenado es un programa con nombre guardado dentro de la base de datos.
Agrupa varias instrucciones SQL (consultas, inserciones, condiciones, ciclos, transacciones) y se ejecuta
explícitamente con CALL nombre(argumentos).
Menos idas y vueltas
La aplicación envía un solo CALL en vez de muchas sentencias: todo el trabajo ocurre dentro del servidor.
Seguridad
Se puede dar permiso de EXECUTE sobre el procedimiento sin dar permisos directos sobre las tablas.
Reutilización
La regla de negocio vive en un solo lugar y todas las aplicaciones la usan igual.
6.2. Sintaxis general
Un procedimiento puede no tener parámetros: CREATE PROCEDURE sp_x() ... y se llama con CALL sp_x();. Usa prefijos como p_ (parámetros) y v_ (variables) para no confundirlos con columnas.
¿Qué es DELIMITER y cuándo se usa?
En la consola de MariaDB/MySQL, en la pestaña SQL de phpMyAdmin o en Workbench, el punto y coma (;)
indica dónde termina una sentencia: al verlo, el programa envía al servidor todo lo escrito hasta ahí.
El problema es que dentro de BEGIN ... END hay varias instrucciones, y cada una termina en ;.
El programa corta el CREATE PROCEDURE en el primer ; y lo envía incompleto:
DELIMITER cambia temporalmente el símbolo que marca el final de la sentencia. Así los ;
del cuerpo dejan de cortarla, y el CREATE PROCEDURE completo se envía al llegar al nuevo símbolo:
- Es una orden del programa cliente, no de SQL: el servidor nunca recibe la palabra
DELIMITER. Por eso no aparece en la definición que se guarda. - El símbolo puede ser cualquiera que no aparezca dentro del cuerpo; los más usados son
//y$$. Deja un espacio entreDELIMITERy el símbolo. - Siempre restáuralo con
DELIMITER ;al terminar. Si lo olvidas, las siguientes sentencias no se ejecutan hasta que escribas//. - Solo hace falta cuando el cuerpo tiene varios
;(un bloqueBEGIN ... ENDcon varias instrucciones). Un cuerpo de una sola instrucción sinBEGIN ... ENDno lo necesita:CREATE PROCEDURE sp_contar() SELECT COUNT(*) FROM productos; - Vale igual para funciones (7), triggers (8) y eventos (9): cada vez que el cuerpo lleve
BEGIN ... ENDcon varias instrucciones, se escriben entreDELIMITER //yDELIMITER ;. - En phpMyAdmin, la pestaña SQL tiene un campo Delimitador debajo del editor: escribe ahí
//y termina la sentencia con//(o usaDELIMITERen el texto, que phpMyAdmin también entiende). - En esta plataforma no hace falta: cada respuesta se envía como una sola sentencia. Si escribes
DELIMITER(como lo harías en la consola), la plataforma lo reconoce y lo quita antes de evaluar.
6.3. Parámetros de entrada (IN)
Un parámetro IN (el modo por defecto) recibe un valor desde el CALL. Si el procedimiento lo cambia, quien llama no ve el cambio.
| id | nombre | ciudad |
|---|---|---|
| 1 | Ana | Cali |
| 2 | Pedro | Bogotá |
| 3 | Rosa | Cali |
6.4. Procedimientos que devuelven resultados
Un SELECT sin INTO dentro del procedimiento devuelve sus filas a quien lo llama, como si fuera una consulta:
| nombre | precio |
|---|---|
| Cuaderno | 12000 |
| Morral | 85000 |
6.5. Parámetros OUT e INOUT
| Modo | Dirección | Comportamiento | Cómo se llama |
|---|---|---|---|
| IN | Entrada | Recibe un valor. Si el procedimiento lo cambia, quien llama no ve el cambio. Es el modo por defecto. | CALL sp(105); |
| OUT | Salida | Empieza en NULL dentro del procedimiento; el valor que se le asigne queda en la variable de quien llama. | CALL sp(@total); |
| INOUT | Entrada y salida | Recibe un valor inicial, lo transforma y lo devuelve en la misma variable. | SET @x = 500; |
Para asignar el resultado de una consulta a una variable o parámetro se usa SELECT ... INTO variable.
Un SELECT sin INTO solo muestra un resultado (6.4): no guarda nada en el parámetro.
Parámetro OUT con SELECT ... INTO
| @total_cali | @total_bogota |
|---|---|
| 2 | 1 |
Parámetro INOUT
| @precio |
|---|
| 119000.00 |
6.6. Variables y control de flujo
- Variables locales:
DECLARE v_total INT DEFAULT 0;y se asignan conSET v_total = v_total + 1;. Solo existen dentro del procedimiento. - Variables de sesión (con
@):SET @x = 5;viven mientras dure la conexión; se usan para recibir parámetros OUT. - Condicionales:
IF cond THEN ... ELSEIF cond THEN ... ELSE ... END IF;oCASE ... END CASE; - Ciclos:
WHILE cond DO ... END WHILE;(puede no ejecutarse nunca),REPEAT ... UNTIL cond END REPEAT;(al menos una vez) yetiqueta: LOOP ... LEAVE etiqueta; END LOOP;(ITERATE etiquetasalta a la siguiente vuelta).
Ciclo WHILE
| factor | resultado |
|---|---|
| 1 | 7 |
| 2 | 14 |
| 3 | 21 |
| 4 | 28 |
| 5 | 35 |
6.7. Administrar procedimientos
6.8. Validaciones con SIGNAL
Igual que en los triggers, SIGNAL SQLSTATE '45000' detiene el procedimiento con un mensaje propio. Lo que se ejecutó antes del SIGNAL no se deshace solo (para eso, ver transacciones en 6.9):
| producto | stock |
|---|---|
| Café | 6 |
El primer retiro (4 unidades) se hizo; los otros dos se rechazaron con su mensaje y el stock quedó en 6.
6.9. Transacciones y manejo de errores (HANDLER)
Una transacción agrupa varias operaciones que deben ocurrir todas o ninguna (explicadas en el módulo 5).
Dentro de un procedimiento se combinan con un HANDLER, que define qué hacer si ocurre un error:
EXIT sale del bloque y CONTINUE sigue con la siguiente instrucción. Lo habitual es un
DECLARE EXIT HANDLER FOR SQLEXCEPTION que hace ROLLBACK, para que un error a mitad del proceso no deje cambios a medias.
UPDATE ... WHERE saldo >= monto que no encuentra filas
no es un error: simplemente afecta 0 filas y ningún HANDLER se activa. Por eso el ejemplo verifica
ROW_COUNT() (filas afectadas por la última instrucción) antes de acreditar el dinero.
La primera transferencia se aprueba; la segunda se rechaza por fondos insuficientes y los saldos no se alteran.
| @estado1 | @estado2 |
|---|---|
| APROBADA | RECHAZADA: fondos insuficientes |
| id | titular | saldo |
|---|---|---|
| 1 | Ana | 300000.00 |
| 2 | Luis | 300000.00 |
6.10. Cursores: recorrer un resultado fila por fila
Se usan cuando cada fila requiere un tratamiento individual. Siempre siguen estos pasos:
DECLARE cur CURSOR FOR SELECT ...;(después de las variables)DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_fin = 1;(después del cursor)OPEN cur;→ ciclo conFETCH cur INTO ...;→CLOSE cur;
Si una sola sentencia SQL puede hacer el trabajo (por ejemplo, INSERT INTO bonos SELECT id, salario * 0.10 FROM empleados WHERE activo = 1), prefiérela: es más rápida que un cursor.
| empleado_id | monto |
|---|---|
| 1 | 300000.00 |
| 3 | 400000.00 |
6.11. SQL dinámico y seguridad
Cuando una parte de la consulta (por ejemplo, el nombre de una tabla) solo se conoce al ejecutar, se arma como texto y se ejecuta con PREPARE / EXECUTE:
| tabla | filas |
|---|---|
| clientes | 3 |
| tabla | filas |
|---|---|
| ventas | 1 |
- Riesgo de inyección SQL: concatenar textos que vienen de un usuario permite que alguien "inyecte" SQL. Para valores, usa marcadores
?conEXECUTE sentencia USING @valor; para nombres de tablas, valida contra una lista de nombres permitidos. - SQL SECURITY: por defecto un procedimiento se ejecuta con los permisos de quien lo creó (
SQL SECURITY DEFINER). ConSQL SECURITY INVOKERusa los permisos de quien lo llama. Se escribe antes deBEGIN.
6.12. Errores comunes
- Usar
SELECT COUNT(*) FROM t;esperando que llene un parámetro OUT: faltaINTO p_total. - Declarar variables después de otras instrucciones: todos los
DECLAREvan al inicio del bloque, en el orden variables → cursores → handlers. - Llamar un procedimiento con OUT pasando un valor literal (
CALL sp(5)): el parámetro de salida necesita una variable (@resultado). - Nombrar un parámetro igual que una columna (
id): genera ambigüedad. Usa prefijos comop_yv_. - Un ciclo
WHILEsinSET i = i + 1nunca termina.
6.13. Cómo responder en la plataforma
- Escribe una sola sentencia
CREATE PROCEDUREcon el nombre y los parámetros exactos del enunciado. No hace faltaDELIMITER(qué es y cuándo se usa en la consola: sección 6.2). - No incluyas el
CALL: el sistema lo ejecuta por ti con valores de prueba y revisa el resultado.
7. Funciones almacenadas
7.1. ¿Qué es una función almacenada?
Una función almacenada recibe parámetros (solo de entrada) y devuelve exactamente un valor
(un número, un texto o una fecha). Su gran ventaja es que se usa dentro de cualquier consulta, igual que
UPPER() o ROUND(): en el SELECT, en WHERE, en ORDER BY o en GROUP BY.
7.2. Sintaxis general
Si el cálculo cabe en una línea, se puede omitir BEGIN ... END: CREATE FUNCTION fn_doble(n INT) RETURNS INT DETERMINISTIC RETURN n * 2;
7.3. Primera función: un cálculo (descuento + IVA)
| precio_final | sin_descuento |
|---|---|
| 107100.00 | 59500.00 |
7.4. Función vs. procedimiento
| Característica | Función | Procedimiento |
|---|---|---|
| Resultado | Siempre un único valor con RETURNS y RETURN. | Ninguno directo; puede devolver varios valores con parámetros OUT/INOUT o mostrar consultas. |
| Parámetros | Solo de entrada (no se escribe IN). | IN, OUT e INOUT. |
| Cómo se usa | Dentro de expresiones: SELECT fn_iva(precio) FROM productos; | Con CALL sp_procesar(10); |
| Modificar datos | Puede (MODIFIES SQL DATA), pero no la misma tabla que usa la consulta que la invoca, y no es buena práctica. | Sin restricciones. |
| Transacciones | No permite START TRANSACTION, COMMIT ni ROLLBACK. | Sí. |
7.5. IF dentro de la función y uso en consultas
Una función puede tener varios RETURN (uno por cada camino del IF) y se puede usar incluso en GROUP BY:
| aprendiz | nota | desempeno |
|---|---|---|
| Ana | 4.6 | Superior |
| Juan | 3.9 | Aprobado |
| Eva | 3.0 | Aprobado |
| Luis | 2.8 | Reprobado |
| desempeno | cantidad |
|---|---|
| Aprobado | 2 |
| Reprobado | 1 |
| Superior | 1 |
7.6. Características: determinismo y acceso a datos
DETERMINISTIC / NOT DETERMINISTIC
DETERMINISTIC: con los mismos parámetros siempre devuelve lo mismo (cálculos puros).
NOT DETERMINISTIC (por defecto): puede variar, por ejemplo si usa NOW(), RAND() o lee tablas que cambian.
NO SQL / READS SQL DATA / MODIFIES SQL DATA
Indican si la función no usa tablas, si solo las lee (SELECT ... INTO) o si las modifica.
DETERMINISTIC, NO SQL o
READS SQL DATA (error 1418), porque necesita saber si es seguro repetirlas en otro servidor.
Declararlas correctamente es una buena práctica siempre.
7.7. Administrar funciones
7.8. Funciones que leen otras tablas (READS SQL DATA)
COALESCE(SUM(total), 0) evita devolver NULL cuando el cliente no tiene pedidos (Eva).
| nombre | categoria |
|---|---|
| Ana | PLATINO |
| Luis | ORO |
| Eva | ESTÁNDAR |
7.9. Límites de las funciones
Una función no puede llamarse a sí misma. MySQL acepta crearla, pero falla al usarla:
- Para cálculos repetitivos usa un ciclo (
WHILE) dentro de la función, o una CTE recursiva en la consulta (sección 4.16). - Una función no puede devolver un conjunto de filas: un
SELECTsinINTOdentro de ella da error 1415. Si necesitas devolver filas, usa un procedimiento (6.4) o una vista (2.8). - No admite
START TRANSACTION,COMMIT,ROLLBACKni SQL dinámico (PREPARE).
7.10. Rendimiento
- Una función usada en el
SELECTse ejecuta una vez por fila. Si además consulta otras tablas (7.8), en una tabla con miles de filas se ejecutan miles de consultas: a veces un JOIN con GROUP BY es mucho más rápido. - Una función aplicada a una columna en el
WHERE(WHERE fn_categoria(id) = 'ORO') impide usar los índices de esa columna: MySQL debe calcularla para todas las filas.
7.11. Errores comunes
- Olvidar
RETURNS tipo(antes deBEGIN) o elRETURN valor;dentro del cuerpo. - Confundir
RETURNS(declara el tipo, va en la cabecera) conRETURN(devuelve el valor, va en el cuerpo). - Escribir
INantes de los parámetros: en las funciones de MySQL (y de esta versión de MariaDB) es un error de sintaxis. - Un
SELECTsinINTOdentro de la función produce error: una función no puede devolver un conjunto de resultados. - Tipo de retorno muy pequeño (
DECIMAL(5,2)para valores grandes) provoca truncamiento o error.
7.12. Cómo responder en la plataforma
- Escribe una sola sentencia
CREATE FUNCTIONcon el nombre, parámetros y tipo de retorno exactos. No hace faltaDELIMITER(qué es y cuándo se usa en la consola: sección 6.2). - Incluye
DETERMINISTIC(o la característica que pida el reto). El sistema invoca tu función con valores de prueba y compara el resultado.
8. Disparadores (Triggers)
8.1. ¿Qué es un disparador y para qué sirve?
Un disparador (trigger) es un bloque de código SQL asociado a una tabla que el motor ejecuta
automáticamente cuando ocurre un INSERT, UPDATE o DELETE sobre ella.
Nadie lo llama: reacciona a los cambios. Se ejecuta una vez por cada fila afectada (FOR EACH ROW).
Validar
Hacer cumplir reglas que un CHECK o una clave foránea no pueden expresar (cupos, horarios, estados válidos).
Auditar
Guardar qué cambió, cuándo y cuáles eran los valores antes y después.
Sincronizar
Mantener totales o inventarios actualizados en otras tablas (descontar stock al vender).
Bloquear
Cancelar operaciones no permitidas con SIGNAL SQLSTATE '45000' (nivel avanzado).
8.2. Sintaxis general
Si el cuerpo tiene una sola instrucción, BEGIN ... END es opcional. Convención útil para el nombre:
tg_tabla_bi (before insert), _au (after update), _bd (before delete)…
Para escribir un trigger con BEGIN ... END en la consola hay que usar DELIMITER, igual que con los procedimientos (6.2).
8.3. Momento y evento: la matriz de activación
| Momento | Evento | Datos disponibles | ¿Puede cambiar la fila? | Uso recomendado |
|---|---|---|---|---|
| BEFORE | INSERT | NEW | SÍ con SET NEW.col = valor | Validar, corregir o completar datos antes de guardarlos. |
| AFTER | INSERT | NEW (ya con su id autoincremental) | NO | Auditoría, actualizar otras tablas (stock, contadores). |
| BEFORE | UPDATE | OLD y NEW | SÍ con SET NEW.col = valor | Validar transiciones (no pasar de "Entregado" a "Pendiente"). |
| AFTER | UPDATE | OLD y NEW | NO | Historial de cambios. |
| BEFORE | DELETE | OLD | NO | Impedir el borrado (SIGNAL) o respaldar la fila. |
| AFTER | DELETE | OLD | NO | Papelera / bitácora, ajustar totales. |
8.4. Pseudorregistros OLD y NEW
NEW: la fila como va a quedar. Existe enINSERTyUPDATE. Solo enBEFOREpuedes modificarla conSET NEW.columna = valor;.OLD: la fila como estaba. Existe enUPDATEyDELETE. Es de solo lectura.- Usar
NEWen un trigger deDELETE(oOLDen uno deINSERT) produce error al crearlo: "There is no NEW row in on DELETE trigger".
8.5. Primer trigger: BEFORE INSERT para corregir datos
El precio negativo se convierte en 0 y el nombre se guarda en mayúsculas, sin que quien inserta tenga que hacerlo.
| id | nombre | precio |
|---|---|---|
| 1 | TECLADO | 85000.00 |
| 2 | MOUSE | 0.00 |
8.6. AFTER INSERT, AFTER UPDATE y BEFORE DELETE
AFTER INSERT: descontar inventario
Cada venta descuenta su cantidad del stock. El trigger se ejecuta una vez por fila insertada (3 veces aquí).
| producto | stock |
|---|---|
| Café | 42 |
| Té | 20 |
AFTER UPDATE: historial comparando OLD y NEW
Se actualizan dos empleados, pero solo a Ana le cambió el salario: por eso hay un único registro en el historial.
| id | empleado_id | anterior | nuevo |
|---|---|---|---|
| 1 | 1 | 3000000.00 | 3300000.00 |
BEFORE DELETE: respaldar la fila que se borra
| id | nombre |
|---|---|
| 1 | Carla |
| id | nombre |
|---|---|
| 2 | Mario |
8.7. Variables locales e IF dentro del trigger
Dentro de BEGIN ... END se pueden declarar variables (DECLARE, siempre al inicio) y usar
IF ... ELSEIF ... ELSE ... END IF o CASE, igual que en un procedimiento.
| id | cantidad | precio | total |
|---|---|---|---|
| 1 | 2 | 30000.00 | 60000.00 |
| 2 | 5 | 40000.00 | 180000.00 |
La venta de 60.000 se guardó sin cambios; la de 200.000 superó el límite y quedó en 180.000 (10 % de descuento).
8.8. Administrar disparadores
8.9. Bloquear una operación con SIGNAL
SIGNAL SQLSTATE '45000' lanza un error definido por ti: la sentencia que activó el trigger se cancela
y el mensaje llega a la aplicación. Observa que Tomás nunca se guardó.
| id | nombre | edad |
|---|---|---|
| 1 | Sara | 20 |
8.10. Varios triggers en el mismo evento: FOLLOWS y PRECEDES
Una tabla puede tener varios triggers para el mismo momento y evento (MySQL 5.7+ y MariaDB 10.2+). Por defecto se ejecutan en el
orden en que se crearon; con FOLLOWS otro (después de) o PRECEDES otro (antes de) se fija el orden:
| paso |
|---|
| 0. preparar |
| 1. validar |
| 2. auditar |
tg_preparar se creó de último, pero se ejecutó primero porque se declaró PRECEDES tg_validar.
8.11. Triggers y llaves foráneas en cascada
Un detalle importante: las acciones automáticas de una llave foránea (ON DELETE CASCADE, ON UPDATE CASCADE)
no activan los triggers de la tabla hija.
| lineas_que_quedan |
|---|
| 0 |
| linea_id |
|---|
| 10 |
Se borraron las dos líneas (10 directamente y 11 en cascada al borrar el pedido), pero la bitácora solo registró la 10.
Si necesitas auditar también los borrados en cascada, haz el borrado de las filas hijas explícitamente (o desde un procedimiento) en lugar de depender de CASCADE.
8.12. Restricciones y errores comunes
- Modificar la misma tabla del trigger (por ejemplo, un
UPDATE productosdentro de un trigger deproductos) produce el error 1442 "Can't update table ... because it is already used by statement which invoked this trigger". Para cambiar la fila actual usaSET NEW.columnaen un triggerBEFORE. - No se permiten
START TRANSACTION,COMMITniROLLBACKdentro de un trigger: forma parte de la transacción de la sentencia que lo activó. Si el trigger falla, la sentencia completa se cancela. - Un trigger no puede llamarse a sí mismo en cadena (recursión) ni devolver resultados con un
SELECTsinINTO. - Rendimiento: se ejecuta por cada fila. Un
UPDATEque afecta 100.000 filas ejecuta el trigger 100.000 veces; mantén su lógica sencilla. SET NEW.colen un triggerAFTERda error: la fila ya se guardó.- Olvidar
END IF;o el;al final de cada instrucción dentro deBEGIN ... END.
8.13. Cómo responder en la plataforma
- Escribe una sola sentencia
CREATE TRIGGERcon el nombre exacto que pide el reto. No hace faltaDELIMITER(si lo pones, se ignora). - Las tablas ya existen: no las crees. El sistema ejecuta tu trigger, realiza una operación de prueba y revisa el efecto en los datos.
- En los retos de corrección, reescribe la sentencia completa ya corregida.
9. Eventos programados (Event Scheduler)
9.1. ¿Qué es un evento?
Un evento es una tarea SQL que el propio servidor ejecuta automáticamente según un horario: una sola vez en una fecha concreta o de forma repetida (cada hora, cada día…). Es como una alarma o un "cron" dentro de la base de datos: sirve para limpiar datos viejos, generar resúmenes nocturnos, cerrar periodos, etc.
| Trigger | Procedimiento | Evento | |
|---|---|---|---|
| ¿Qué lo ejecuta? | Un INSERT/UPDATE/DELETE en su tabla | Alguien que hace CALL | El reloj del servidor |
9.2. Activar el programador de eventos
event_scheduler=ON en la sección
[mysqld] del archivo my.ini. Si está en OFF, los eventos se crean pero nunca se ejecutan.
9.3. Sintaxis general
Una sola vez: AT
ON SCHEDULE AT '2026-12-31 23:59:00'ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 3 HOUR
Repetido: EVERY
ON SCHEDULE EVERY 1 DAYON SCHEDULE EVERY 15 MINUTE
Unidades válidas: SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR (en singular). EVERY siempre necesita cantidad y unidad: EVERY 1 DAY, nunca EVERY DAY.
9.4. Primer evento: una sola vez
Se programa para "ahora mismo"; unos instantes después, el servidor ya lo ejecutó y el valor cambió de 0 a 1.
| id | activo |
|---|---|
| 1 | 0 |
9.5. STARTS, ENDS y ON COMPLETION
ON COMPLETION NOT PRESERVE(valor por defecto): cuando un eventoATse ejecuta, o un eventoEVERYllega a suENDS, MySQL lo elimina automáticamente.ON COMPLETION PRESERVE: el evento se conserva (queda deshabilitado) para poder revisarlo o reactivarlo sin reescribirlo.
9.6. Eventos que se repiten: limpieza diaria
Con STARTS CURRENT_TIMESTAMP la primera ejecución es inmediata; luego se repetirá cada día. Las sesiones de hace 10 y 30 días se eliminaron.
| id | usuario |
|---|---|
| 1 | ana |
| 2 | luis |
| 3 | eva |
9.7. Administrar eventos
9.8. Eventos que llaman procedimientos
La práctica recomendada es dejar la lógica en un procedimiento y que el evento solo lo llame: así se puede probar con CALL y cambiar sin tocar el horario.
| id | estado | vence |
|---|---|---|
| 1 | activa | 2026-10-12 |
| 2 | activa | 2026-10-19 |
La reserva que venció hace 2 días pasó a "vencida"; la que vence en 5 días sigue activa.
9.9. Consultar el estado de los eventos
El catálogo information_schema.EVENTS muestra el tipo, la frecuencia, la fecha programada y si están activos:
| EVENT_NAME | EVENT_TYPE | INTERVAL_VALUE | INTERVAL_FIELD | EXECUTE_AT | STATUS | ON_COMPLETION |
|---|---|---|---|---|---|---|
| ev_cierre | ONE TIME | NULL | NULL | 2030-12-31 23:59:00 | DISABLED | PRESERVE |
| ev_limpieza | RECURRING | 1 | HOUR | NULL | ENABLED | NOT PRESERVE |
RECURRING = se repite; ONE TIME = una sola vez (AT). La columna LAST_EXECUTED indica cuándo se ejecutó por última vez.
9.10. Errores silenciosos y buenas prácticas
Cuando un evento falla no aparece ningún mensaje, porque no hay un usuario conectado que lo vea. El error solo queda en el
registro de errores del servidor, en XAMPP: C:\xampp\mysql\data\mysql_error.log. Así se ve allí el evento ev_limpieza del ejemplo anterior, que borra de una tabla que no existe:
- Prueba la lógica antes como consulta normal o con
CALL(por eso conviene usar procedimientos, 9.8). - Lleva una bitácora: que el procedimiento registre en una tabla cada ejecución (fecha, filas afectadas con
ROW_COUNT(), y unHANDLERque guarde los errores). - Hora del servidor: los horarios usan la zona horaria de MySQL (
SELECT NOW();), que puede ser distinta a la de tu equipo o a la de la aplicación. - Ejecuciones que se solapan: si una ejecución tarda más que el intervalo (por ejemplo,
EVERY 1 MINUTEcon una tarea de 2 minutos), puede comenzar otra antes de que termine la anterior. - Para crear eventos el usuario necesita el privilegio
EVENTsobre la base de datos.
9.11. Errores comunes
EVERY DAYoEVERY 1 DAYS: la sintaxis correcta esEVERY 1 DAY.- Olvidar la palabra
DOantes del cuerpo. - Programador apagado (
event_scheduler = OFF): el evento existe pero nunca se ejecuta. - Una fecha en el pasado con
AT(sinPRESERVE): MySQL descarta el evento en el momento de crearlo, sin ejecutarlo, y solo muestra una advertencia.
9.12. Cómo responder en la plataforma
- Escribe una sola sentencia
CREATE EVENTcon el nombre y el horario que pide el reto. No hace faltaDELIMITER(qué es y cuándo se usa en la consola: sección 6.2). - Para comprobarlo, el sistema revisa la programación de tu evento, ejecuta una vez su cuerpo (lo que va después de
DO), como lo haría el programador de eventos, y revisa el efecto en los datos. No hace falta tener encendidoevent_scheduler.
10. Usuarios, roles y permisos (DCL)
El DCL (Data Control Language) decide quién puede entrar al servidor y qué puede hacer en cada base, tabla o columna.
Sus sentencias principales son GRANT (dar permisos) y REVOKE (quitarlos); junto a ellas se usan
CREATE USER y CREATE ROLE.
root (sección 1.2). Las salidas son reales, tomadas de un XAMPP con MariaDB 10.4.
10.1. ¿Por qué no trabajar siempre con root?
root puede hacer cualquier cosa en cualquier base: borrar bases completas, ver todos los datos o crear usuarios.
Si una aplicación se conecta como root y alguien encuentra una falla en ella, tiene el servidor completo.
Lo correcto es aplicar el principio de mínimo privilegio: cada usuario o aplicación recibe solo los permisos que necesita.
Un reporte
Solo necesita SELECT sobre las tablas que consulta.
Una aplicación web
SELECT, INSERT, UPDATE y DELETE en su base, nunca permisos para borrar tablas.
Los respaldos
Permisos para leer y bloquear tablas, sin poder modificar datos (ver 11.4).
10.2. Crear, modificar y eliminar usuarios
Un usuario se identifica por su nombre y por el equipo desde donde se conecta: 'nombre'@'equipo'.
'localhost'= solo desde el mismo equipo del servidor;'%'= desde cualquier equipo;'192.168.1.%'= desde la red local.'ana'@'localhost'y'ana'@'%'son dos usuarios distintos, cada uno con sus permisos.- Un usuario recién creado puede conectarse pero no puede hacer nada: no tiene permisos hasta que se le den con
GRANT(10.3). - Para conectarse con él, desde CMD:
mysql -u ana -p(pide la contraseña).
| Quiero… | Sentencia |
|---|---|
| Crear un usuario | CREATE USER 'ana'@'localhost' IDENTIFIED BY 'Clave_Ana_123'; |
| Cambiar su contraseña | ALTER USER 'ana'@'localhost' IDENTIFIED BY 'NuevaClave_456'; |
| Bloquear / desbloquear la cuenta | ALTER USER 'ana'@'localhost' ACCOUNT LOCK; · ACCOUNT UNLOCK |
| Ver los usuarios del servidor | SELECT user, host FROM mysql.user; |
| Saber con qué usuario estoy conectado | SELECT CURRENT_USER(); |
| Eliminar un usuario | DROP USER 'ana'@'localhost'; |
root viene sin contraseña. Está bien para practicar en tu equipo,
pero en un servidor real hay que asignarle una: ALTER USER 'root'@'localhost' IDENTIFIED BY 'una_clave_fuerte';
(después, las aplicaciones que se conecten como root deben configurarse con esa clave).
10.3. Dar y quitar permisos: GRANT y REVOKE
- Estructura:
GRANT permisos ON dónde TO quién;yREVOKE permisos ON dónde FROM quién;. SHOW GRANTSmuestra los permisos actuales.USAGEsignifica "puede conectarse, pero sin permisos": siempre aparece. La contraseña se guarda cifrada, nunca en texto.- Los cambios con
GRANTyREVOKEse aplican de inmediato; no hace faltaFLUSH PRIVILEGES(solo se necesita si se modifican a mano las tablas demysql). - Si el usuario intenta algo sin permiso recibe un error y la operación no se hace. Por ejemplo,
anapuede leer y modificar mascotas (10.5), pero no borrarlas:
10.4. Niveles de permiso: servidor, base, tabla, columna y rutina
La parte ON del GRANT decide a qué se aplica el permiso. Elige siempre el nivel más pequeño que sirva:
| Nivel | Se escribe | Ejemplo |
|---|---|---|
| Todo el servidor evitar | ON *.* | GRANT ALL PRIVILEGES ON *.* TO 'admin'@'localhost'; |
| Una base de datos | ON base.* | GRANT SELECT, INSERT, UPDATE, DELETE ON veterinaria.* TO 'app'@'localhost'; |
| Una tabla | ON base.tabla | GRANT SELECT ON veterinaria.mascotas TO 'lector'@'localhost'; |
| Algunas columnas | SELECT (col1, col2) ON base.tabla | GRANT SELECT (id, nombre, especie) ON veterinaria.mascotas TO 'luis'@'localhost'; |
| Un procedimiento o función | ON PROCEDURE base.nombre | GRANT EXECUTE ON PROCEDURE veterinaria.sp_registrar_mascota TO 'web'@'localhost'; |
Con el permiso por columnas, luis puede ver el nombre de las mascotas, pero no el costo de la consulta:
| Quiero que el usuario pueda… | Permisos |
|---|---|
| Solo leer una base | GRANT SELECT ON base.* TO 'u'@'localhost'; |
| Leer y modificar datos (una aplicación) | GRANT SELECT, INSERT, UPDATE, DELETE ON base.* TO 'u'@'localhost'; |
| Ejecutar procedimientos y funciones | GRANT EXECUTE ON base.* TO 'u'@'localhost'; |
| Crear y modificar tablas (DDL) | GRANT CREATE, ALTER, DROP, INDEX ON base.* TO 'u'@'localhost'; |
| Hacer respaldos (11.4) | GRANT SELECT, SHOW VIEW, TRIGGER, EVENT, LOCK TABLES ON base.* TO 'respaldo'@'localhost'; |
| Todo sobre una base (no sobre el servidor) | GRANT ALL PRIVILEGES ON base.* TO 'u'@'localhost'; |
| Dar permisos a otros evitar | ... WITH GRANT OPTION |
10.5. Roles: permisos agrupados
Cuando varias personas hacen el mismo trabajo (por ejemplo, todo el personal de recepción), dar los mismos permisos uno por uno es repetitivo y fácil de equivocar. Un rol es un conjunto de permisos con nombre: se le dan los permisos al rol y luego se asigna el rol a cada usuario. Si cambian las tareas, se modifica el rol una sola vez.
Importante: tener un rol asignado no basta; el rol debe estar activo en la sesión. ana lo tiene como rol por defecto,
así que se activa solo al conectarse. luis tiene el rol, pero no por defecto: hasta que no ejecuta SET ROLE solo cuenta con sus permisos propios.
ana (rol por defecto)El rol no incluye DELETE, así que ana no puede borrar.
luis (activa el rol a mano)Sin el rol activo, luis solo tiene su permiso por columnas (10.4).
Al cambiar los permisos del rol, el cambio llega a todos sus usuarios. Por ejemplo, para que recepción ya no pueda modificar:
| Quiero… | Sentencia |
|---|---|
| Crear un rol | CREATE ROLE recepcion; |
| Dar permisos al rol | GRANT SELECT ON base.tabla TO recepcion; |
| Asignar el rol a un usuario | GRANT recepcion TO 'ana'@'localhost'; |
| Que el rol se active al conectarse | SET DEFAULT ROLE recepcion FOR 'ana'@'localhost'; |
| Activar un rol en la sesión actual | SET ROLE recepcion; (y SET ROLE NONE; para desactivarlo) |
| Ver el rol activo | SELECT CURRENT_ROLE(); |
| Quitar el rol a un usuario | REVOKE recepcion FROM 'ana'@'localhost'; |
| Eliminar el rol | DROP ROLE recepcion; |
10.6. Dar acceso solo a vistas o procedimientos
A veces un usuario no debe ver la tabla completa ni modificarla libremente. En lugar de darle permisos sobre la tabla:
- Se le da
SELECTsobre una vista (2.8) que muestra solo las columnas o filas permitidas. - Se le da
EXECUTEsobre un procedimiento (módulo 6) que hace la operación con sus propias reglas. ConSQL SECURITY DEFINER(el valor por defecto) el procedimiento usa los permisos de quien lo creó, así que el usuario puede registrar datos sin tener permiso directo sobre la tabla.
Conectado como web:
web ve las mascotas sin el costo de la consulta y puede registrar una mascota solo a través del procedimiento, que fija el costo.
No puede leer la tabla ni insertar directamente en ella.
10.7. Buenas prácticas
- Un usuario por aplicación o persona, nunca una cuenta compartida: así se sabe quién hizo qué y se puede quitar el acceso a uno solo.
- Mínimo privilegio: empieza sin permisos y agrega solo los necesarios, en el nivel más pequeño (tabla o columna antes que base; base antes que servidor).
- Las aplicaciones nunca se conectan como
root. Esta misma plataforma ejecuta el código de los aprendices con un usuario que solo puede trabajar en bases temporales de prueba. - Usa roles cuando varias personas comparten las mismas tareas, y revisa los permisos con
SHOW GRANTSde vez en cuando. - Prefiere
'usuario'@'localhost'o una red concreta antes que'%', y contraseñas largas que no se escriban en archivos ni en el código. - Cuando alguien deja de necesitar el acceso, quítalo de inmediato (
REVOKEoDROP USER).
10.8. Chuleta
| Tarea | Sentencia |
|---|---|
| Crear usuario | CREATE USER 'u'@'localhost' IDENTIFIED BY 'Clave_123'; |
| Dar permisos | GRANT SELECT, INSERT ON base.tabla TO 'u'@'localhost'; |
| Quitar permisos | REVOKE INSERT ON base.tabla FROM 'u'@'localhost'; |
| Ver permisos | SHOW GRANTS FOR 'u'@'localhost'; |
| Crear y asignar un rol | CREATE ROLE r; · GRANT ... TO r; · GRANT r TO 'u'@'localhost'; · SET DEFAULT ROLE r FOR 'u'@'localhost'; |
| Cambiar contraseña | ALTER USER 'u'@'localhost' IDENTIFIED BY 'Nueva_456'; |
| Eliminar usuario / rol | DROP USER 'u'@'localhost'; · DROP ROLE r; |
11. Copias de seguridad y restauración
C:\xampp\mysql\bin.
Las salidas que se muestran aquí son reales, tomadas de un XAMPP con MariaDB 10.4.
11.1. Sacar una copia de seguridad con mysqldump
Se ejecuta en CMD, fuera de la consola de MySQL, desde C:\xampp\mysql\bin (o desde el Shell de XAMPP):
El símbolo > le dice a CMD que guarde en un archivo lo que el programa mostraría en pantalla.
Si el archivo ya existe, se reemplaza. La carpeta de destino debe existir (créala antes con mkdir C:\respaldos).
| ¿Qué necesitas? | Comando | El archivo contiene |
|---|---|---|
| Estructura y datos (copia completa) | mysqldump -u root -p veterinaria > C:\respaldos\veterinaria_completo.sql |
DROP TABLE + CREATE TABLE + INSERT |
| Solo estructura | mysqldump -u root -p --no-data veterinaria > C:\respaldos\veterinaria_estructura.sql |
Solo DROP TABLE + CREATE TABLE: tablas vacías, ideal para crear la base en otro equipo. |
| Solo datos | mysqldump -u root -p --no-create-info veterinaria > C:\respaldos\veterinaria_datos.sql |
Solo INSERT: para llenar tablas que ya existen. |
| Solo algunas tablas | mysqldump -u root -p veterinaria mascotas duenos > C:\respaldos\tablas.sql |
Solo las tablas nombradas después de la base. |
| Incluir la creación de la base | mysqldump -u root -p --databases veterinaria > C:\respaldos\veterinaria_con_bd.sql |
Agrega CREATE DATABASE y USE al inicio. |
| Incluir procedimientos, funciones y eventos | mysqldump -u root -p --routines --events veterinaria > C:\respaldos\veterinaria_total.sql |
Además de tablas y triggers (que se incluyen por defecto), los procedimientos, funciones y eventos. |
| Todas las bases del servidor | mysqldump -u root -p --all-databases > C:\respaldos\servidor_completo.sql |
Todas las bases, cada una con su CREATE DATABASE. |
Opciones útiles que se pueden combinar:
--routines --events: importante en este curso. Sin ellas, los procedimientos, funciones y eventos que creaste no quedan en la copia.--single-transaction: copia consistente de tablas InnoDB sin bloquearlas mientras otros usuarios trabajan.--skip-extended-insert: unINSERTpor fila (archivo más largo, pero más fácil de leer y comparar).--default-character-set=utf8mb4: conserva bien tildes y eñes.--result-file=C:\respaldos\copia.sql: alternativa a>.
Ejemplo de una copia completa recomendada para el curso:
Si el comando no muestra nada después de la contraseña, terminó bien: mysqldump solo imprime mensajes cuando hay errores.
11.2. ¿Qué contiene el archivo .sql que se genera?
Un respaldo es un archivo de texto con las sentencias SQL necesarias para reconstruir la base. Este es el archivo real que genera mysqldump para la base veterinaria, con la explicación de cada parte:
--) con la versión de la herramienta, el servidor y la base respaldada. No se ejecutan./*!40101 ... */ son comentarios condicionales: MySQL/MariaDB los ejecuta solo si su versión es igual o mayor (40101 = versión 4.1.1); otros programas los ignoran.
Guardan la configuración actual en variables @OLD_... y luego: fijan la codificación utf8mb4, desactivan las claves foráneas (para poder crear las tablas en cualquier orden) y respetan los id en 0.
DROP TABLE IF EXISTS borra la tabla si ya existe en el destino (¡cuidado al restaurar sobre una base con datos!) y CREATE TABLE la crea igual a la original.
AUTO_INCREMENT=3 indica que el próximo id será 3. Esta parte no aparece en una copia hecha con --no-create-info.
--no-data.
Dump completed confirma que la copia terminó completa: si falta, el archivo quedó cortado y no sirve.
Diferencias según la opción usada (misma base de ejemplo):
| Copia | CREATE TABLE | INSERT | CREATE DATABASE / USE | Líneas |
|---|---|---|---|---|
| Completa (sin opciones) | Sí | Sí | No | 80 |
--no-data | Sí | No | No | 60 |
--no-create-info | No | Sí | No | 45 |
--databases veterinaria | Sí | Sí | Sí | 88 |
Lo que agrega --databases al inicio del archivo:
11.3. Cargar (restaurar) una base de datos desde un archivo .sql
Se usa el cliente mysql y el símbolo <, que le entrega el contenido del archivo como si lo escribieras. Hay dos casos:
Caso 1: el archivo NO trae CREATE DATABASE (copia normal, --no-data o --no-create-info)
Primero crea la base vacía y luego carga el archivo indicando en qué base:
-e "..." ejecuta una sola sentencia sin entrar a la consola. Igual que mysqldump, si no muestra nada, terminó bien.
Caso 2: el archivo SÍ trae CREATE DATABASE (copia hecha con --databases o --all-databases)
No se indica base de destino: el archivo la crea y la selecciona con USE. Ojo: siempre se restaura con el nombre original.
Alternativa: cargar desde dentro de la consola con SOURCE
Con SOURCE la ruta va con barras / (o dobles \\) y se ve el resultado de cada sentencia, lo que ayuda a encontrar en qué línea falla.
Comprobar que la restauración quedó bien
Errores frecuentes al restaurar
| Mensaje | Causa y solución |
|---|---|
ERROR 1049 (42000): Unknown database 'veterinaria_copia' | La base de destino no existe: créala primero (caso 1). |
El sistema no puede encontrar el archivo especificado. | Ruta o nombre del archivo incorrectos. Si la ruta tiene espacios, ponla entre comillas: < "C:\mis respaldos\copia.sql". |
ERROR 1050 (42S01): Table 'x' already exists | El archivo no trae DROP TABLE y la tabla ya existe: restaura en una base vacía. |
ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY' | Cargaste una copia de solo datos sobre tablas que ya tenían esas filas. |
ERROR 1452 ... a foreign key constraint fails | Copia de solo datos cargada en tablas cuyas filas relacionadas no existen. |
Tildes y eñes salen como á o ? | Codificación distinta: usa --default-character-set=utf8mb4 tanto al respaldar como al restaurar. |
< no funciona para cargar archivos y
> guarda el respaldo con otra codificación (UTF-16) que luego da errores al restaurar.
Si estás en PowerShell, escribe cmd y presiona Enter para pasar a CMD.
11.4. Automatizar los respaldos
Un archivo .bat guarda los comandos para no escribirlos cada vez. Crea con el Bloc de notas C:\respaldos\respaldo_diario.bat:
- Cada ejecución crea un archivo con la fecha (
veterinaria_2026-10-13.sql) y borra los de más de 30 días. - El ejemplo usa
rootsin contraseña (XAMPP recién instalado). Si root tiene contraseña, no la escribas en el .bat: crea un usuario solo para respaldos (10.4). - Programarlo: en Windows, abre el Programador de tareas → Crear tarea básica → frecuencia Diariamente → acción Iniciar un programa → selecciona el
.bat. - Revisa de vez en cuando que el archivo termine en
-- Dump completedy prueba restaurarlo (11.3).
11.5. Buenas prácticas de respaldo
- Prueba tus copias: una copia que nunca restauraste no está comprobada. Restáurala en una base distinta (
_copia) y revisa tablas y conteos. - Nombra los archivos con la fecha (
tienda_2026-10-12.sql) y conserva varias versiones, no solo la última. - Guarda las copias fuera del equipo (memoria USB, nube): si el disco falla, la copia en el mismo disco se pierde con él.
- Antes de un cambio grande (borrar datos, modificar tablas), haz una copia.
- Revisa que el archivo termine en
-- Dump completed.
11.6. Chuleta
| Tarea | Comando (en CMD, dentro de C:\xampp\mysql\bin) |
|---|---|
| Entrar a la consola | mysql -u root -p |
| Respaldo completo | mysqldump -u root -p --routines --events base > C:\respaldos\base.sql |
| Solo estructura | mysqldump -u root -p --no-data base > C:\respaldos\base_estructura.sql |
| Solo datos | mysqldump -u root -p --no-create-info base > C:\respaldos\base_datos.sql |
| Crear la base de destino | mysql -u root -p -e "CREATE DATABASE base_copia" |
| Restaurar | mysql -u root -p base_copia < C:\respaldos\base.sql |
| Restaurar desde la consola | USE base_copia; y luego SOURCE C:/respaldos/base.sql |