🎓 Guía Educativa · Python & PyQt5

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.

📅 2026 👨‍💻 Nivel: Intermedio ⏱️ ~2 horas de estudio

📑 Contenido

01
Introducción a las Bases de Datos Relacionales
Tablas, filas, columnas, claves primarias y foráneas — los cimientos del modelo relacional.
fundamentosteoría
02
Relaciones y Cardinalidad
Diagramas de pata de gallo (crow's foot) para 1:1, 1:N y N:M con ejemplos reales.
diagramasdiseño
03
Normalización (1FN, 2FN, 3FN, BCNF)
Paso a paso con tablas visuales y un simulador interactivo para ver la descomposición.
normalizaciónavanzado
04
PyQt5 y Bases de Datos
Módulos QtSql, QSqlDatabase, QSqlTableModel — el puente entre Python y SQL.
PyQt5código
05
CRUD con SQLite
Conexión, inserción, consulta, actualización y borrado en una base de datos local.
SQLitepráctica
06
CRUD con MariaDB
La misma aplicación pero conectada a un servidor MariaDB — configuración y diferencias.
MariaDBservidor
07
Miniproyecto: Libreta de Contactos
Aplicación CRUD completa con interfaz PyQt5 + simulador interactivo en el navegador.
proyectoCRUD
08
SQLite vs MariaDB
Comparativa directa: cuándo usar cada uno, ventajas y limitaciones.
comparativa
09
Buenas Prácticas
Consejos profesionales para escribir SQL y diseñar bases de datos sólidas.
consejosproducción
Capítulo 01

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).

id_empleado (PK) nombre email depto_id (FK) 🔑 1 Ana García ana@mail.com 🔗 3 🔑 2 Luis Martínez luis@mail.com 🔗 1 🔑 3 Carla Ruiz carla@mail.com 🔗 2 ▲ Una fila = un empleado. Cada columna guarda un dato atómico.

🔑 Claves primarias y foráneas

Aquí está la magia del modelo relacional:

🔑 Clave primaria (PK): Una columna (o combinación) que identifica de forma única cada fila. Nunca se repite. Como el DNI de cada registro.
🔗 Clave foránea (FK): Una columna que apunta a la clave primaria de otra tabla. Crea el "puente" entre dos tablas. Así es como se relacionan los datos sin repetirlos.

🌐 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.

EMPLEADOS id (PK) nombre depto_id (FK) N 1 DEPARTAMENTOS id (PK) nombre ubicación

⬆️ Un departamento tiene muchos empleados (1:N). La FK depto_id en EMPLEADOS apunta a id en DEPARTAMENTOS.

Capítulo 02

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:

Símbolo Significado ● — Exactamente 1 ● — Muchos (0 o más) Otros símbolos | — exactamente 1 ○ — 0 o 1 (opcional)

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.

PERSONA 1 1 1 PASAPORTE 1
💡 Regla: La FK puede estar en cualquiera de las dos tablas, pero suele ir en la "débil" (la que no puede existir sin la otra).

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 1 1 N PEDIDOS N
✅ Implementación: La FK va en la tabla del lado "muchos" (PEDIDOS tendría 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.

ESTUDIANTE N N MATRÍCULA (tabla puente) CURSO N
⚠️ Importante: Una relación N:M siempre necesita una tabla intermedia (tabla puente). No se puede modelar directamente con una FK. La tabla puente contiene las FKs de ambas tablas principales.

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)

Capítulo 03

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.

🎯 Objetivo: Que no haya información duplicada, que los cambios se hagan en un solo lugar y que no se puedan producir inconsistencias.

🧪 Simulador de Normalización

0 1FN 2FN 3FN BCNF

📖 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.

📌 En la práctica: La mayoría de los diseños llegan hasta 3FN. BCNF se aplica en casos muy específicos. Ir más allá (4FN, 5FN) es raro en aplicaciones reales.
Capítulo 04

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
💡 Concepto clave: QtSql usa el patrón Model-View. El modelo (QSqlTableModel) se encarga de los datos y la vista (QTableView) de mostrarlos. ¡Cambias el motor de BD y la UI no se entera!
Capítulo 05

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()
🔒 Buenas prácticas: Siempre usa 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!

Capítulo 06

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

⚠️ Antes de empezar: Necesitas (1) MariaDB instalado, (2) el servidor corriendo, (3) el driver Qt para MariaDB (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()
💡 Diferencia clave: En SQLite el modo 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).
Capítulo 07

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())
📝 Nota: Para MariaDB, cambia 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
IDNombreEmailTeléfonoAcción
📭 No hay contactos. ¡Añade el primero!
Motor: SQLite (simulado) Registros: 0
💻 ¿Cómo se ve en la app real? La demo de arriba usa JavaScript y localStorage. La aplicación PyQt5 real tiene el mismo aspecto pero usando QTableView + QSqlTableModel — los botones y la tabla se ven prácticamente igual.
Capítulo 08

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ísticaSQLiteMariaDB
TipoEmbebido (archivo)Cliente-Servidor
InstalaciónNinguna (incluido en Python)Servidor aparte
ConcurrenciaUna escritura a la vezMúltiples escrituras simultáneas
Velocidad (simple)Muy rápida (sin red)Rápida (latencia de red)
UsuariosNoSí (GRANT, REVOKE)
AlmacenamientoUn archivo .dbDatadir del servidor
Ideal paraDesktop, móvil, tests, IoTWeb, SaaS, enterprise
Driver QtQSQLITE (nativo)QMARIADB (plugin)
💡 Mi recomendación: Desarrolla con SQLite (es más rápido para iterar) y despliega con MariaDB. QtSql hace que el cambio sea trivial — solo cambias el nombre del driver y la cadena de conexión.
Capítulo 09

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.

Fundamentos de Bases de Datos Relacionales — Guía educativa interactiva

Construida con HTML, CSS, JavaScript y mucho ☕ · 2026

Libre para compartir y estudiar · Hecho con ❤️ para aprendices curiosos