Fundamentos de Bases de Datos Relacionales
De la teoría relacional a una aplicación CRUD real con SQLite y MariaDB — todo explicado paso a paso con código, diagramas y práctica interactiva.
📑 Contenido
Introducción a las Bases de Datos Relacionales
Antes de escribir una sola línea de código, hay que entender el terreno que pisamos. Una base de datos relacional no es más que un conjunto de tablas que se relacionan entre sí — como un gran archivador con fichas que se cruzan por códigos comunes.
🧱 ¿Qué es una tabla?
La unidad fundamental. Piensa en una hoja de cálculo: cada fila es un registro (una persona, un producto, un pedido) y cada columna es un atributo (nombre, precio, fecha).
🔑 Claves primarias y foráneas
Aquí está la magia del modelo relacional:
🌐 El modelo relacional de un vistazo
Una base de datos no es una tabla sola, sino un conjunto de tablas conectadas por claves foráneas. Este esqueleto se llama esquema relacional.
⬆️ Un departamento tiene muchos empleados (1:N). La FK depto_id en EMPLEADOS apunta a id en DEPARTAMENTOS.
Relaciones y Cardinalidad
La cardinalidad define cuántos elementos de una entidad se asocian con cuántos de otra. Es el lenguaje que usamos para dibujar bases de datos antes de escribirlas.
📐 Notación de pata de gallo (Crow's Foot)
Es el estándar visual en el modelado de datos. Cada extremo de la línea puede tener:
Relación 1:1 (Uno a Uno)
Un registro de la tabla A se relaciona con exactamente uno de la tabla B, y viceversa. Como una persona y su pasaporte.
Relación 1:N (Uno a Muchos)
La más común. Un registro de A se relaciona con varios de B, pero cada B pertenece a un solo A. Ejemplo: un cliente tiene muchos pedidos.
cliente_id).Relación N:M (Muchos a Muchos)
Un registro de A puede relacionarse con muchos de B, y viceversa. Ejemplo: estudiantes y cursos — un estudiante se matricula en muchos cursos, y cada curso tiene muchos estudiantes.
1:1 — Usuario ↔ Perfil, País ↔ Capital, Empleado ↔ Casillero
1:N — Categoria ↔ Producto, Autor ↔ Libros, País ↔ Ciudades
N:M — Actores ↔ Películas (tabla: Reparto), Proveedores ↔ Productos (tabla: Suministro), Usuarios ↔ Grupos (tabla: Membresía)
Normalización (1FN, 2FN, 3FN, BCNF)
La normalización es el proceso de organizar los datos para eliminar redundancias y evitar anomalías. Piénsalo como "ordenar el armario": cada cosa en su sitio, una sola vez.
🧪 Simulador de Normalización
📖 Explicación detallada
Regla: Cada celda contiene un valor atómico (un solo valor, no una lista). Y todas las filas deben tener el mismo número de columnas.
Problema: Una tabla con teléfonos: "612345678, 698765432" tiene valores múltiples en una celda.
Solución: Crear una fila por cada teléfono, o mover los teléfonos a otra tabla.
Regla: Estar en 1FN y todos los atributos no clave deben depender de toda la clave primaria (no solo de una parte).
Problema: En tablas con clave primaria compuesta (varias columnas), puede haber columnas que solo dependan de una parte de la clave.
Solución: Separar en dos tablas: una para la dependencia parcial y otra para el resto.
Regla: Estar en 2FN y ningún atributo no clave debe depender transitivamente de la clave primaria.
Problema: Si A → B y B → C, entonces C depende transitivamente de A. Ejemplo: si tenemos empleado_id → depto_id → depto_ciudad, la ciudad depende del empleado a través del departamento.
Solución: Separar la tabla para que cada columna dependa directamente de la clave.
Regla: Estar en 3FN y todo determinante (columna de la que depende otra) debe ser clave candidata.
Problema: Puede haber dependencias donde un atributo determina otro sin ser clave completa. Es un refinamiento de 3FN para casos donde hay solapamiento de claves candidatas.
Solución: Descomponer eliminando la dependencia problemática.
PyQt5 y Bases de Datos
PyQt5 viene con un módulo llamado QtSql que abstrae las diferencias entre motores de base de datos. Puedes escribir el mismo código para SQLite, MariaDB, PostgreSQL, etc., y solo cambiar la cadena de conexión.
🧩 Componentes principales de QtSql
📦 QSqlDatabase
- Representa la conexión a la base de datos
- Configuras driver, host, usuario, contraseña
- Puedes tener múltiples conexiones
📦 QSqlQuery
- Ejecuta sentencias SQL directamente
- Ideal para consultas ad-hoc
- Puedes preparar consultas con parámetros
📦 QSqlTableModel
- Modelo de datos para una tabla SQL
- Se conecta directamente a un QTableView
- CRUD automático con la UI
📦 QSqlRelationalTableModel
- Extiende QSqlTableModel con claves foráneas
- Muestra valores de la tabla referenciada
- Ideal para relaciones 1:N
🔌 Estructura básica de conexión
Este es el esqueleto que usaremos tanto para SQLite como para MariaDB:
from PyQt5.QtSql import QSqlDatabase, QSqlQuery
from PyQt5.QtWidgets import QApplication, QMessageBox
def conectar_bd(tipo="QSQLITE"):
db = QSqlDatabase.addDatabase(tipo)
if tipo == "QSQLITE":
db.setDatabaseName("contactos.db")
elif tipo == "QMARIADB":
db.setHostName("localhost")
db.setDatabaseName("contactos_db")
db.setUserName("root")
db.setPassword("tu_password")
db.setPort(3306)
if not db.open():
QMessageBox.critical(None, "Error",
db.lastError().text())
return db
CRUD con SQLite
SQLite es el motor de base de datos más usado del mundo. Está en tu teléfono, en tu navegador y en miles de millones de dispositivos. Es un archivo local — no necesita servidor.
📁 Conectar y crear tabla
import sys
from PyQt5.QtSql import QSqlDatabase, QSqlQuery
from PyQt5.QtWidgets import QApplication, QMainWindow, QTableView, QVBoxLayout, QWidget
from PyQt5.QtCore import Qt
app = QApplication(sys.argv)
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName("contactos.db")
db.open()
# Crear tabla si no existe
q = QSqlQuery()
q.exec("""CREATE TABLE IF NOT EXISTS contactos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL,
email TEXT,
telefono TEXT,
creado DATE DEFAULT CURRENT_DATE
)""")
Con solo estas líneas ya tienes una base de datos funcional. El archivo contactos.db se crea automáticamente en el mismo directorio.
➕ Operaciones CRUD
# Leer todos los contactos
q = QSqlQuery("SELECT * FROM contactos")
while q.next():
print(f"{q.value(0)}: {q.value(1)} — {q.value(2)}")
# Insertar un contacto
q = QSqlQuery()
q.prepare("INSERT INTO contactos (nombre, email, telefono) VALUES (?, ?, ?)")
q.addBindValue("Ana García")
q.addBindValue("ana@mail.com")
q.addBindValue("612345678")
q.exec()
# Actualizar un contacto
q = QSqlQuery()
q.prepare("UPDATE contactos SET email = ? WHERE id = ?")
q.addBindValue("ana.garcia@mail.com")
q.addBindValue(1)
q.exec()
# Eliminar un contacto
q = QSqlQuery()
q.prepare("DELETE FROM contactos WHERE id = ?")
q.addBindValue(2)
q.exec()
prepare() + addBindValue() en lugar de concatenar strings. Esto evita inyección SQL automáticamente.
📋 QSqlTableModel — La forma más fácil
# Modelo conectado a la tabla entera
modelo = QSqlTableModel()
modelo.setTable("contactos")
modelo.setEditStrategy(QSqlTableModel.OnManualSubmit)
modelo.select() # carga los datos
# Mostrar en una tabla
vista = QTableView()
vista.setModel(modelo)
vista.show()
# Los cambios se guardan con:
modelo.submitAll()
Con QSqlTableModel, el usuario puede editar las celdas directamente en la tabla. ¡No necesitas escribir consultas SQL para el CRUD básico!
CRUD con MariaDB
MariaDB es un motor cliente-servidor. Necesitas tener el servidor instalado y ejecutándose. El cambio desde SQLite es mínimo en código, pero enorme en capacidades.
⚙️ Requisitos previos
libqt6-sql-plugin-mariadb en Linux, o el driver QMARIADB incluido con PyQt5). Crea la base de datos con CREATE DATABASE contactos_db;
🔌 Conexión a MariaDB
db = QSqlDatabase.addDatabase("QMARIADB")
db.setHostName("localhost") # o la IP del servidor
db.setPort(3306) # puerto por defecto de MariaDB
db.setDatabaseName("contactos_db")
db.setUserName("root")
db.setPassword("password_seguro")
if not db.open():
print("Error:", db.lastError().text())
else:
print("✅ Conectado a MariaDB")
🔄 Las mismas operaciones CRUD
Una vez conectado, las operaciones SQL son idénticas a SQLite. La magia de QtSql es que abstrae las diferencias:
q = QSqlQuery()
q.prepare("""INSERT INTO contactos (nombre, email, telefono)
VALUES (?, ?, ?)""")
q.addBindValue("María López")
q.addBindValue("maria@mail.com")
q.addBindValue("698765432")
q.exec()
print("Insertado con ID:", q.lastInsertId())
q = QSqlQuery("SELECT id, nombre, email FROM contactos")
while q.next():
print(f"{q.value('id')}: {q.value('nombre')} <{q.value('email')}>")
# ¡Exactamente el mismo código que con SQLite!
modelo = QSqlTableModel()
modelo.setTable("contactos")
modelo.setEditStrategy(QSqlTableModel.OnFieldChange)
modelo.select()
modelo.setHeaderData(1, Qt.Horizontal, "Nombre")
modelo.setHeaderData(2, Qt.Horizontal, "Email")
vista = QTableView()
vista.setModel(modelo)
vista.show()
OnFieldChange guarda automáticamente al terminar de editar una celda. En MariaDB también funciona, pero ten en cuenta que es una operación de red (un poco más lenta que en local).
Miniproyecto: Libreta de Contactos
Vamos a construir una aplicación completa de libreta de contactos con PyQt5 que funcione tanto con SQLite como con MariaDB. El usuario podrá elegir el motor al iniciar.
📦 Código completo del proyecto
# libreta_contactos.py — App CRUD con PyQt5
# Funciona con SQLITE (local) o MARIADB (servidor)
import sys
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QPushButton, QTableView, QLineEdit,
QLabel, QMessageBox, QComboBox
)
from PyQt5.QtSql import QSqlDatabase, QSqlTableModel, QSqlQuery
from PyQt5.QtCore import Qt
class LibretaContactos(QMainWindow):
def __init__(self, motor="sqlite"):
super().__init__()
self.setWindowTitle("📒 Libreta de Contactos")
self.resize(700, 500)
self.conectar_bd(motor)
self.init_ui()
def conectar_bd(self, motor):
db = QSqlDatabase.addDatabase(
"QSQLITE" if motor == "sqlite" else "QMARIADB"
)
if motor == "sqlite":
db.setDatabaseName("contactos.db")
else:
db.setHostName("localhost")
db.setDatabaseName("contactos_db")
db.setUserName("root")
db.setPassword("password")
db.setPort(3306)
if not db.open():
QMessageBox.critical(self, "Error BD",
db.lastError().text())
sys.exit(1)
# Crear tabla si es necesario
q = QSqlQuery()
q.exec("""CREATE TABLE IF NOT EXISTS contactos (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
nombre TEXT NOT NULL,
email TEXT,
telefono TEXT,
creado DATE DEFAULT CURRENT_DATE
)""")
def init_ui(self):
central = QWidget()
self.setCentralWidget(central)
layout = QVBoxLayout(central)
# Barra de búsqueda
top = QHBoxLayout()
self.busqueda = QLineEdit()
self.busqueda.setPlaceholderText("🔍 Buscar contacto...")
self.busqueda.textChanged.connect(self.filtrar)
top.addWidget(QLabel("Buscar:"))
top.addWidget(self.busqueda)
layout.addLayout(top)
# Tabla de datos
self.modelo = QSqlTableModel()
self.modelo.setTable("contactos")
self.modelo.setEditStrategy(QSqlTableModel.OnManualSubmit)
self.modelo.select()
self.modelo.setHeaderData(1, Qt.Horizontal, "Nombre")
self.modelo.setHeaderData(2, Qt.Horizontal, "Email")
self.modelo.setHeaderData(3, Qt.Horizontal, "Teléfono")
self.tabla = QTableView()
self.tabla.setModel(self.modelo)
self.tabla.setSelectionBehavior(QTableView.SelectRows)
self.tabla.setAlternatingRowColors(True)
layout.addWidget(self.tabla)
# Botones
btn_layout = QHBoxLayout()
btn_add = QPushButton("➕ Añadir")
btn_add.clicked.connect(self.anadir)
btn_save = QPushButton("💾 Guardar cambios")
btn_save.clicked.connect(self.guardar)
btn_del = QPushButton("🗑️ Eliminar")
btn_del.clicked.connect(self.eliminar)
btn_layout.addWidget(btn_add)
btn_layout.addWidget(btn_save)
btn_layout.addWidget(btn_del)
layout.addLayout(btn_layout)
def anadir(self):
# Añade fila vacía para editar
row = self.modelo.rowCount()
self.modelo.insertRow(row)
def guardar(self):
if self.modelo.submitAll():
QMessageBox.information(self, "OK", "Cambios guardados ✅")
else:
QMessageBox.warning(self, "Error",
self.modelo.lastError().text())
def eliminar(self):
sel = self.tabla.selectionModel().selectedRows()
for i in reversed(sel):
self.modelo.removeRow(i.row())
self.guardar()
def filtrar(self, texto):
if texto:
self.modelo.setFilter(
f"nombre LIKE '%{texto}%' OR email LIKE '%{texto}%'")
else:
self.modelo.setFilter("")
self.modelo.select()
if __name__ == "__main__":
app = QApplication(sys.argv)
# Cambia a "mariadb" para usar servidor
ventana = LibretaContactos(motor="sqlite")
ventana.show()
sys.exit(app.exec())
motor="mariadb" al instanciar la clase, y asegúrate de que la tabla existe (AUTO_INCREMENT en lugar de AUTOINCREMENT).🎮 Demo interactiva de la Libreta de Contactos
Prueba el CRUD directamente aquí. Los contactos se guardan en el navegador (localStorage) y la interfaz simula el comportamiento de la app real.
📒 Libreta de Contactos — Demo
Simulación en navegador| ID | Nombre | Teléfono | Acción |
|---|
SQLite vs MariaDB
No hay un "mejor" motor — hay el motor adecuado para cada situación. Aquí tienes una comparativa directa para que puedas elegir con criterio.
📁 SQLite
- Archivo local — sin servidor
- Configuración cero
- Ideal para apps de escritorio, móviles
- Desarrollo y prototipado rápido
- Un solo usuario escritor a la vez
- Sin autenticación ni usuarios
- Límite práctico: ~140 TB (suficiente)
- Backup = copiar el archivo
- No soporta concurrencia pesada
🐬 MariaDB
- Arquitectura cliente-servidor
- Requiere instalación y configuración
- Ideal para apps web, multiusuario
- Producción y escalado horizontal
- Múltiples conexiones simultáneas
- Usuarios, roles y permisos granulares
- Prácticamente ilimitado
- Backup con mysqldump / réplicas
- Optimizado para concurrencia
📊 Tabla comparativa rápida
| Característica | SQLite | MariaDB |
|---|---|---|
| Tipo | Embebido (archivo) | Cliente-Servidor |
| Instalación | Ninguna (incluido en Python) | Servidor aparte |
| Concurrencia | Una escritura a la vez | Múltiples escrituras simultáneas |
| Velocidad (simple) | Muy rápida (sin red) | Rápida (latencia de red) |
| Usuarios | No | Sí (GRANT, REVOKE) |
| Almacenamiento | Un archivo .db | Datadir del servidor |
| Ideal para | Desktop, móvil, tests, IoT | Web, SaaS, enterprise |
| Driver Qt | QSQLITE (nativo) | QMARIADB (plugin) |
Buenas Prácticas
Pequeños hábitos que marcan la diferencia entre un diseño que funciona y uno que duele mantener.
Siempre clave primaria
Toda tabla debe tener una PK, aunque creas que no la necesita. id INTEGER PRIMARY KEY AUTO_INCREMENT es tu amigo.
FK con índice
Las columnas que son clave foránea deberían tener un índice. Las búsquedas por FK se vuelven mucho más rápidas.
Prepared Statements
Nunca concatenes strings para construir SQL. Usa prepare() + bindValue(). Seguridad y rendimiento.
Normaliza hasta 3FN
Es el punto dulce entre integridad y rendimiento. Desnormaliza solo si tienes un problema de rendimiento medido.
SQLite para desarrollo
Usa SQLite en local. Es instantáneo, no necesita configuración y tus tests vuelan. Cambia a MariaDB en producción.
Nombres consistentes
snake_case para tablas y columnas. Singular para tablas (usuario, no usuarios). Siempre en minúsculas.
Transacciones
Si haces varias operaciones relacionadas, envuélvelas en una transacción. O todas se guardan o ninguna.
EXPLAIN tus queries
Antes de optimizar, mide. Usa EXPLAIN QUERY PLAN (SQLite) o EXPLAIN (MariaDB) para ver cómo se ejecuta tu consulta.
Tipos de datos precisos
No uses TEXT para todo. INTEGER, DATE, DECIMAL existen por algo: ocupan menos y validan por ti.
📖 Glosario
- SQL
- Structured Query Language — lenguaje estándar para consultar y manipular bases de datos relacionales.
- DDL / DML
- Data Definition Language (CREATE, ALTER, DROP) vs Data Manipulation Language (SELECT, INSERT, UPDATE, DELETE).
- Clave primaria (PK)
- Columna o combinación de columnas que identifica unívocamente cada fila de una tabla.
- Clave foránea (FK)
- Columna que referencia la clave primaria de otra tabla, estableciendo una relación entre ellas.
- Cardinalidad
- Especifica cuántas instancias de una entidad se relacionan con cuántas de otra (1:1, 1:N, N:M).
- Normalización
- Proceso de organizar datos para reducir redundancia y dependencias no deseadas.
- ACID
- Atomicity, Consistency, Isolation, Durability — propiedades que garantizan transacciones fiables.
- QSqlTableModel
- Clase de Qt que proporciona un modelo de datos editable directamente vinculado a una tabla SQL.
- CRUD
- Create, Read, Update, Delete — las cuatro operaciones básicas de persistencia de datos.