Home / Bases de Datos / Fundamentos de DBMS y SQL: guía práctica con PostgreSQL y SQLite
Fundamentos de DBMS y SQL: guía práctica con PostgreSQL y SQLite

Fundamentos de DBMS y SQL: guía práctica con PostgreSQL y SQLite

25 October 2026 · 22 min de lectura ··

Por qué los datos necesitan algo más que archivos sueltos

Casi cualquier aplicación que uses a diario guarda información en algún lugar: los pedidos de una tienda en línea, los mensajes de una app de chat, el historial clínico de una mascota en una veterinaria. Al principio, un desarrollador podría pensar que basta con escribir esa información en archivos de texto o en una hoja de cálculo. Funciona mientras los datos son pocos y los usa una sola persona. En cuanto aparecen miles de registros, varios usuarios escribiendo al mismo tiempo y la necesidad de que nada se pierda tras un corte de luz, los archivos sueltos se convierten en un problema.

Para resolver exactamente eso existe el sistema gestor de bases de datos, conocido por sus siglas en inglés como DBMS (Database Management System) o SGBD en español. Es el software que se interpone entre tus datos y quien quiere usarlos, y que se encarga de guardarlos de forma ordenada, protegerlos, permitir que muchas personas trabajen con ellos a la vez y recuperarlos con rapidez.

En esta guía vas a entender qué es un DBMS, qué tipos existen, cómo procesa internamente una consulta y cómo se usa SQL para comunicarte con él. No se queda en la teoría: construirás paso a paso la base de datos de una clínica veterinaria, practicarás cada grupo de comandos SQL y verás errores reales con su solución. Los ejemplos están pensados para PostgreSQL 18, la versión estable actual, y se indican las diferencias cuando trabajes con SQLite.

Qué es un DBMS y qué problemas resuelve

Un DBMS es un programa especializado en crear, almacenar, consultar, modificar y proteger colecciones de datos relacionados. La palabra clave es “relacionados”: no se trata de guardar información aleatoria, sino de datos que tienen sentido juntos, como los dueños de mascotas, sus animales y las visitas que hacen a la clínica.

Frente a un conjunto de archivos manejados a mano, un gestor de bases de datos aporta varias garantías:

  • Consistencia: impide que se guarden datos que violan reglas definidas, como un correo duplicado o una consulta asociada a una mascota inexistente.
  • Concurrencia: permite que decenas o miles de usuarios lean y escriban al mismo tiempo sin pisarse entre ellos.
  • Durabilidad: una vez que confirma una operación, la mantiene aunque el servidor se reinicie de golpe.
  • Seguridad: controla quién puede ver o cambiar cada tabla, columna o fila.
  • Eficiencia: usa índices, cachés y planes de ejecución para encontrar datos entre millones de registros en milisegundos.
  • Independencia: las aplicaciones piden datos sin preocuparse de cómo están guardados físicamente en disco.

Algunos de los gestores más usados hoy son PostgreSQL, MySQL, Microsoft SQL Server, Oracle Database y SQLite en el mundo relacional, y MongoDB o Redis en el mundo no relacional. Cada uno tiene su enfoque, pero todos comparten la misma idea central: separar la lógica de tu aplicación de la gestión de los datos.

Modelos de bases de datos: relacionales y no relacionales

Los DBMS se clasifican según la forma en que organizan la información. Comprender la diferencia entre modelos relacionales y no relacionales te ayudará a elegir la herramienta adecuada para cada proyecto.

Las bases de datos relacionales, también llamadas RDBMS, organizan los datos en tablas formadas por filas y columnas. Cada tabla representa una entidad, como dueños o mascotas, y las relaciones entre entidades se expresan mediante claves. Todas se consultan con SQL. En este grupo están PostgreSQL, MySQL, SQL Server, Oracle y SQLite.

Una tabla de dueños, vista de forma conceptual, se vería así:

 id |    nombre     |       email        |  telefono
----+---------------+--------------------+------------
  1 | Lucía Paredes | [email protected]   | 5551234567
  2 | Tomás Ibarra  | [email protected]   | 5559876543

Las bases de datos no relacionales, agrupadas bajo el término NoSQL, no imponen un esquema de tablas rígido. Existen varias familias: almacenes clave-valor como Redis, bases documentales como MongoDB, bases de columnas anchas como Cassandra y bases de grafos como Neo4j. Un documento en MongoDB para el mismo dueño podría incluir directamente sus mascotas:

{
    "_id": 1,
    "nombre": "Lucía Paredes",
    "email": "[email protected]",
    "mascotas": [
        { "nombre": "Canela", "especie": "perro" },
        { "nombre": "Miso", "especie": "gato" }
    ]
}

Ninguno de los dos modelos es mejor en términos absolutos. Más adelante verás una comparación con criterios concretos para decidir cuál usar.

Actores y componentes de un sistema gestor

Un DBMS no vive aislado. Alrededor de él interactúan distintos perfiles y piezas de software, y conocerlos ayuda a entender responsabilidades y permisos.

Los actores principales son:

  • Administradores de bases de datos (DBA): instalan el gestor, configuran la seguridad, planifican copias de seguridad y vigilan el rendimiento.
  • Desarrolladores: diseñan el esquema, escriben las consultas y conectan la base con las aplicaciones.
  • Analistas de datos: consultan la información para generar reportes e indicadores.
  • Usuarios finales: interactúan con los datos de forma indirecta a través de una aplicación web o móvil.

Entre los componentes internos destacan:

  • El procesador de consultas, que interpreta el SQL recibido, lo valida y genera un plan de ejecución.
  • El optimizador, que elige la forma más barata de resolver cada consulta, por ejemplo usando un índice en lugar de recorrer toda la tabla.
  • El gestor de almacenamiento o de archivos, que decide cómo se escriben y leen los datos en disco.
  • El gestor de transacciones, que coordina las operaciones concurrentes y garantiza que se apliquen completas o no se apliquen.
  • El gestor de búfer, que mantiene en memoria las páginas de datos más usadas para evitar lecturas lentas de disco.
  • El catálogo o diccionario de datos, donde el propio gestor guarda la descripción de tablas, columnas, índices y permisos.

Cómo procesa un DBMS una consulta SQL

Cuando una aplicación envía una consulta, el gestor la recorre por varias etapas antes de devolver un resultado. Ver este flujo ayuda a entender por qué algunas consultas son rápidas y otras no.

  1. Análisis sintáctico: el texto SQL se convierte en un árbol de sintaxis. Si falta una coma o una palabra está mal escrita, la consulta se rechaza aquí.
  2. Análisis semántico: el gestor comprueba en el catálogo que las tablas y columnas existen y que tienes permisos sobre ellas.
  3. Reescritura: se expanden vistas y se simplifican expresiones.
  4. Optimización: el planificador evalúa varias estrategias y elige la de menor costo estimado según estadísticas de las tablas.
  5. Ejecución: el ejecutor recorre el plan, lee las páginas de datos necesarias y construye el resultado.

En PostgreSQL puedes ver el plan elegido con EXPLAIN:

EXPLAIN SELECT nombre FROM mascotas WHERE especie = 'gato';
                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on mascotas  (cost=0.00..1.06 rows=1 width=118)
   Filter: ((especie)::text = 'gato'::text)

Seq Scan indica que el gestor recorrerá toda la tabla. En tablas pequeñas es la opción correcta; en tablas grandes, un índice puede cambiar el plan por completo, como verás en la sección de rendimiento.

SQL: el lenguaje estándar de las bases relacionales

SQL nació en IBM en la década de 1970 y se convirtió en estándar ANSI e ISO en la década siguiente. Desde entonces ha recibido revisiones periódicas; la más reciente es SQL:2023, que incorporó, entre otras cosas, mejoras para trabajar con JSON y consultas sobre grafos. Aunque cada gestor añade extensiones propias, el núcleo del lenguaje de consulta estructurado es común a todos, por lo que lo que aprendas en PostgreSQL te servirá en MySQL, SQL Server o SQLite.

SQL es declarativo: describes qué datos quieres, no cómo obtenerlos. El optimizador decide el cómo. Sus instrucciones se agrupan en sublenguajes:

GrupoPropósitoInstrucciones principales
DDLDefinir la estructuraCREATE, ALTER, DROP, TRUNCATE
DMLManipular datosINSERT, UPDATE, DELETE, MERGE
DQLConsultar datosSELECT
TCLControlar transaccionesBEGIN, COMMIT, ROLLBACK, SAVEPOINT
DCLGestionar permisosGRANT, REVOKE

Muchas fuentes incluyen SELECT dentro de DML; otras lo separan como DQL. La clasificación importa menos que entender qué hace cada instrucción.

Preparar un entorno de práctica con PostgreSQL y SQLite

Para seguir los ejemplos necesitas un gestor instalado. La forma más rápida y limpia de tener PostgreSQL es con Docker, usando la imagen oficial de la versión 18:

docker run --name pg-veterinaria \
  -e POSTGRES_USER=admin \
  -e POSTGRES_PASSWORD=cambia_esta_clave \
  -e POSTGRES_DB=veterinaria \
  -p 5432:5432 \
  -d postgres:18

Conéctate con el cliente psql incluido en el contenedor:

docker exec -it pg-veterinaria psql -U admin -d veterinaria
psql (18.6 (Debian 18.6-1.pgdg13+1))
Type "help" for help.

veterinaria=#

Si prefieres algo todavía más ligero, SQLite guarda toda la base en un único archivo y no necesita servidor. Su versión estable actual es la 3.54.1:

# Debian / Ubuntu
sudo apt install sqlite3

# macOS con Homebrew
brew install sqlite

sqlite3 veterinaria.db

La estructura del proyecto de práctica puede ser tan simple como esta:

practica-sql/
├── 01_esquema.sql
├── 02_datos.sql
├── 03_consultas.sql
├── 04_transacciones.sql
└── veterinaria.db

Guardar cada bloque de instrucciones en archivos te permite reconstruir la base en cualquier momento con psql -f 01_esquema.sql o sqlite3 veterinaria.db < 01_esquema.sql.

Definir la estructura con DDL

El primer paso es describir las entidades del dominio. La clínica necesita registrar dueños, sus mascotas y las consultas médicas. En PostgreSQL, el esquema se define así:

CREATE TABLE duenos (
  id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  nombre     VARCHAR(100) NOT NULL,
  email      VARCHAR(150) NOT NULL UNIQUE,
  telefono   VARCHAR(20),
  creado_en  TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE mascotas (
  id                BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  dueno_id          BIGINT NOT NULL REFERENCES duenos (id) ON DELETE CASCADE,
  nombre            VARCHAR(60) NOT NULL,
  especie           VARCHAR(30) NOT NULL
                    CHECK (especie IN ('perro', 'gato', 'ave', 'reptil', 'otro')),
  fecha_nacimiento  DATE
);

CREATE TABLE consultas (
  id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  mascota_id  BIGINT NOT NULL REFERENCES mascotas (id),
  fecha       TIMESTAMPTZ NOT NULL DEFAULT now(),
  motivo      TEXT NOT NULL,
  costo       NUMERIC(10, 2) NOT NULL CHECK (costo >= 0)
);

Cada línea aporta una regla. GENERATED ALWAYS AS IDENTITY es la forma estándar de crear claves autoincrementales y sustituye al antiguo tipo SERIAL, que sigue funcionando pero ya no es la opción recomendada. REFERENCES crea una clave foránea que vincula cada mascota con su dueño, y ON DELETE CASCADE indica que, si se elimina un dueño, sus mascotas se eliminan con él.

En SQLite la sintaxis cambia ligeramente, porque no admite IDENTITY ni TIMESTAMPTZ. Además, las claves foráneas vienen desactivadas por defecto:

PRAGMA foreign_keys = ON;

CREATE TABLE duenos (
  id         INTEGER PRIMARY KEY,
  nombre     TEXT NOT NULL,
  email      TEXT NOT NULL UNIQUE,
  telefono   TEXT,
  creado_en  TEXT NOT NULL DEFAULT (datetime('now'))
);

Con el tiempo, el esquema evoluciona. ALTER TABLE permite modificarlo sin perder los datos existentes:

ALTER TABLE mascotas ADD COLUMN peso_kg NUMERIC(5, 2);
ALTER TABLE duenos ALTER COLUMN telefono TYPE VARCHAR(25);
ALTER TABLE mascotas RENAME COLUMN fecha_nacimiento TO nacimiento;

Para eliminar estructuras hay dos instrucciones que conviene no confundir. DROP TABLE borra la tabla completa, con su definición; TRUNCATE vacía todas sus filas pero conserva la estructura:

TRUNCATE TABLE consultas RESTART IDENTITY;
DROP TABLE IF EXISTS consultas;

Manipular y consultar datos con DML

Con la estructura lista, toca llenarla. INSERT admite varias filas en una sola instrucción, lo que es más eficiente que insertarlas una a una:

INSERT INTO duenos (nombre, email, telefono) VALUES
  ('Lucía Paredes', '[email protected]', '5551234567'),
  ('Tomás Ibarra',  '[email protected]', '5559876543'),
  ('Renata Solís',  '[email protected]', NULL);

INSERT INTO mascotas (dueno_id, nombre, especie, nacimiento) VALUES
  (1, 'Canela', 'perro', '2021-03-14'),
  (1, 'Miso',   'gato',  '2023-07-02'),
  (2, 'Kiwi',   'ave',   '2024-01-20'),
  (3, 'Bruno',  'perro', '2019-11-05');

INSERT INTO consultas (mascota_id, motivo, costo) VALUES
  (1, 'Vacuna anual', 450.00),
  (2, 'Revisión dental', 800.00),
  (1, 'Desparasitación', 300.00),
  (4, 'Control de peso', 350.00);

La consulta más básica recupera filas filtradas y ordenadas:

SELECT nombre, especie, nacimiento
FROM mascotas
WHERE especie = 'perro'
ORDER BY nacimiento;
 nombre | especie | nacimiento
--------+---------+------------
 Bruno  | perro   | 2019-11-05
 Canela | perro   | 2021-03-14
(2 rows)

El verdadero poder de SQL aparece al combinar tablas. Un JOIN une filas relacionadas a través de sus claves:

SELECT d.nombre AS dueno, m.nombre AS mascota, c.motivo, c.costo
FROM consultas c
JOIN mascotas m ON m.id = c.mascota_id
JOIN duenos d   ON d.id = m.dueno_id
ORDER BY d.nombre;
     dueno     | mascota |     motivo      | costo
---------------+---------+-----------------+--------
 Lucía Paredes | Canela  | Vacuna anual    | 450.00
 Lucía Paredes | Miso    | Revisión dental | 800.00
 Lucía Paredes | Canela  | Desparasitación | 300.00
 Renata Solís  | Bruno   | Control de peso | 350.00
(4 rows)

Tomás no aparece porque Kiwi no tiene consultas. Si quieres listar a todos los dueños, incluso sin visitas, usa LEFT JOIN. Las funciones de agregación resumen datos, y HAVING filtra sobre esos resúmenes:

SELECT d.nombre, COUNT(c.id) AS visitas, COALESCE(SUM(c.costo), 0) AS total
FROM duenos d
LEFT JOIN mascotas m  ON m.dueno_id = d.id
LEFT JOIN consultas c ON c.mascota_id = m.id
GROUP BY d.id, d.nombre
HAVING COALESCE(SUM(c.costo), 0) < 1000
ORDER BY total DESC;
    nombre    | visitas | total
--------------+---------+--------
 Renata Solís |       1 | 350.00
 Tomás Ibarra |       0 |      0
(2 rows)

Para modificar datos existentes se usan UPDATE y DELETE. Ambos deben llevar casi siempre un WHERE; sin él afectan a todas las filas de la tabla:

UPDATE duenos
SET telefono = '5550001122'
WHERE email = '[email protected]';

DELETE FROM consultas
WHERE fecha < now() - INTERVAL '5 years';

PostgreSQL permite además devolver las filas afectadas con RETURNING, muy útil para confirmar qué cambió:

UPDATE consultas SET costo = costo * 1.10
WHERE motivo = 'Vacuna anual'
RETURNING id, costo;

Controlar transacciones con TCL

Una transacción agrupa varias operaciones en una unidad indivisible: o se aplican todas o no se aplica ninguna. Este comportamiento descansa en las propiedades ACID de las transacciones, que todo gestor relacional serio garantiza:

  • Atomicidad: la transacción se ejecuta completa o se revierte por completo.
  • Consistencia: al terminar, la base cumple todas sus reglas y restricciones.
  • Aislamiento: las transacciones concurrentes no ven estados intermedios de las demás.
  • Durabilidad: lo confirmado sobrevive a fallos del sistema.

Imagina que la clínica controla un inventario de vacunas. Registrar una consulta de vacunación implica dos cambios: crear la consulta y descontar una dosis. Si el segundo falla, el primero no debe quedar guardado:

CREATE TABLE inventario (
  producto  VARCHAR(80) PRIMARY KEY,
  unidades  INTEGER NOT NULL CHECK (unidades >= 0)
);

INSERT INTO inventario VALUES ('Vacuna antirrábica', 12);

BEGIN;

INSERT INTO consultas (mascota_id, motivo, costo)
VALUES (4, 'Vacuna antirrábica', 450.00);

UPDATE inventario
SET unidades = unidades - 1
WHERE producto = 'Vacuna antirrábica';

COMMIT;

Si algo sale mal antes del COMMIT, ROLLBACK deshace todo lo hecho dentro de la transacción. Los puntos de guardado permiten revertir solo una parte:

BEGIN;

UPDATE inventario SET unidades = unidades - 2 WHERE producto = 'Vacuna antirrábica';
SAVEPOINT tras_descuento;

DELETE FROM consultas WHERE mascota_id = 4;
-- Nos damos cuenta del error y volvemos al punto seguro
ROLLBACK TO SAVEPOINT tras_descuento;

COMMIT;

Por defecto, PostgreSQL trabaja en modo autocommit: cada instrucción suelta es su propia transacción. Solo cuando escribes BEGIN agrupas varias. El nivel de aislamiento predeterminado es READ COMMITTED; para operaciones críticas, como cierres contables, puedes subirlo a SERIALIZABLE:

BEGIN ISOLATION LEVEL SERIALIZABLE;
-- operaciones sensibles a la concurrencia
COMMIT;

Gestionar permisos con DCL

El control de acceso es la primera línea de defensa de cualquier base de datos. El principio de mínimo privilegio dice que cada usuario o aplicación debe tener solo los permisos que necesita. En la clínica, el personal de recepción registra dueños y mascotas, pero no debería borrar consultas ni ver costos:

CREATE ROLE recepcion LOGIN PASSWORD 'otra_clave_segura';

GRANT CONNECT ON DATABASE veterinaria TO recepcion;
GRANT USAGE ON SCHEMA public TO recepcion;
GRANT SELECT, INSERT, UPDATE ON duenos, mascotas TO recepcion;
GRANT SELECT (id, mascota_id, fecha, motivo) ON consultas TO recepcion;

El último GRANT concede permisos a nivel de columna: recepción puede ver las consultas, pero no la columna costo. Si más adelante cambian las responsabilidades, REVOKE retira permisos:

REVOKE UPDATE ON duenos FROM recepcion;

Una práctica habitual es crear roles por función, como lectura o escritura, otorgarles permisos y luego asignar esos roles a usuarios concretos. Así se gestiona el acceso de equipos grandes sin repetir decenas de instrucciones.

Normalización: diseñar tablas sin redundancia

Un buen diseño evita repetir información. La normalización de bases de datos es el proceso de organizar las tablas para que cada dato se guarde en un único lugar, lo que reduce inconsistencias y ahorra espacio.

Observa esta tabla mal diseñada, donde cada consulta repite los datos del dueño y la mascota:

 consulta | dueno         | email_dueno      | mascota | especie | motivo
----------+---------------+------------------+---------+---------+-----------------
        1 | Lucía Paredes | [email protected] | Canela  | perro   | Vacuna anual
        2 | Lucía Paredes | [email protected] | Miso    | gato    | Revisión dental
        3 | Lucía Paredes | [email protected] | Canela  | perro   | Desparasitación

Si Lucía cambia de correo, hay que actualizarlo en tres filas; si se olvida una, la base queda inconsistente. Las formas normales dan reglas progresivas para evitarlo:

  • Primera forma normal (1FN): cada columna guarda un único valor atómico y no hay grupos repetidos. Una columna con “Canela, Miso” la incumple.
  • Segunda forma normal (2FN): cumple 1FN y todas las columnas dependen de la clave primaria completa, no solo de una parte cuando la clave es compuesta.
  • Tercera forma normal (3FN): cumple 2FN y ninguna columna depende de otra columna que no sea clave. El correo depende del dueño, no de la consulta, así que debe vivir en la tabla de dueños.

El esquema de tres tablas que creaste antes ya está en tercera forma normal. Normalizar no es una regla absoluta: en sistemas analíticos o con lecturas muy intensas a veces se desnormaliza a propósito, duplicando datos para evitar uniones costosas. La clave es hacerlo de forma consciente y documentada.

Restricciones para proteger la integridad

Las restricciones son reglas que el propio gestor hace cumplir, sin importar qué aplicación escriba los datos. Delegar estas reglas en la base es mucho más fiable que confiar en que todo el código cliente las respete.

Las más importantes son:

  • PRIMARY KEY: identifica cada fila de forma única y no admite nulos.
  • FOREIGN KEY: asegura que una referencia apunte a una fila existente en otra tabla.
  • UNIQUE: impide valores repetidos en una columna o combinación de columnas.
  • NOT NULL: obliga a que la columna tenga siempre un valor.
  • CHECK: valida una condición arbitraria, como que el costo no sea negativo.
  • DEFAULT: asigna un valor cuando no se especifica ninguno.

También puedes añadir restricciones a tablas existentes y darles nombres claros, lo que facilita leer los mensajes de error:

ALTER TABLE mascotas
  ADD CONSTRAINT mascotas_peso_positivo CHECK (peso_kg > 0);

ALTER TABLE duenos
  ADD CONSTRAINT duenos_telefono_unico UNIQUE (telefono);

Índices y rendimiento de consultas

Un índice funciona como el índice alfabético de un libro: en lugar de leer todas las páginas, el gestor salta directamente a la ubicación de los datos buscados. Las claves primarias y las restricciones UNIQUE crean índices automáticamente, pero las claves foráneas no, y es un descuido frecuente.

Crea índices sobre las columnas que usas a menudo en filtros y uniones:

CREATE INDEX idx_mascotas_dueno ON mascotas (dueno_id);
CREATE INDEX idx_consultas_mascota_fecha ON consultas (mascota_id, fecha DESC);

Con una tabla de consultas grande, EXPLAIN ANALYZE muestra la diferencia, porque ejecuta la consulta y reporta tiempos reales:

EXPLAIN ANALYZE
SELECT fecha, motivo FROM consultas
WHERE mascota_id = 4
ORDER BY fecha DESC
LIMIT 10;
 Limit  (cost=0.43..12.61 rows=10 width=40) (actual time=0.031..0.044 rows=10 loops=1)
   ->  Index Scan using idx_consultas_mascota_fecha on consultas
         (cost=0.43..487.20 rows=400 width=40) (actual time=0.030..0.041 rows=10 loops=1)
         Index Cond: (mascota_id = 4)
 Planning Time: 0.142 ms
 Execution Time: 0.061 ms

Los índices no son gratuitos. Cada uno ocupa espacio y ralentiza ligeramente las inserciones y actualizaciones, porque también hay que mantenerlo al día. Indexa según tus consultas reales, no por si acaso, y revisa periódicamente cuáles no se usan. Otras recomendaciones de rendimiento útiles son pedir solo las columnas necesarias en lugar de SELECT *, paginar resultados con LIMIT y mantener las estadísticas actualizadas con ANALYZE para que el optimizador tome buenas decisiones.

Errores comunes al empezar con SQL

Hay tropiezos que casi todo el mundo comete en sus primeras semanas con bases de datos. Conocerlos te ahorrará sustos.

Olvidar el WHERE en UPDATE o DELETE. Un DELETE FROM consultas; sin condición borra la tabla entera. Un buen hábito es escribir primero el SELECT con el mismo WHERE, comprobar qué filas devuelve y solo después cambiarlo por UPDATE o DELETE. Trabajar dentro de una transacción te da además la opción de revertir.

Comparar con NULL usando el signo igual. En SQL, NULL significa valor desconocido, y cualquier comparación con = devuelve desconocido, no verdadero. La forma correcta es IS NULL o IS NOT NULL:

-- Incorrecto: no devuelve ninguna fila
SELECT nombre FROM duenos WHERE telefono = NULL;

-- Correcto
SELECT nombre FROM duenos WHERE telefono IS NULL;

Construir consultas concatenando texto del usuario. Es la puerta de entrada a la inyección SQL, una de las vulnerabilidades más explotadas. Usa siempre consultas parametrizadas. Este ejemplo en Python con psycopg 3 deja que el controlador escape los valores:

import psycopg

email = input("Correo del dueño: ")

with psycopg.connect("dbname=veterinaria user=admin") as conn:
    fila = conn.execute(
        "SELECT id, nombre FROM duenos WHERE email = %s",
        (email,),
    ).fetchone()
    print(fila)

Guardar dinero en columnas de punto flotante. Los tipos REAL o DOUBLE PRECISION introducen errores de redondeo. Para importes usa NUMERIC con precisión fija, como en la columna costo.

No crear índices en las claves foráneas. Las uniones y los borrados en cascada se vuelven lentos a medida que las tablas crecen.

Usar la cuenta de superusuario desde la aplicación. Si la aplicación se ve comprometida, el atacante tendrá control total. Crea un rol específico con permisos limitados.

Solución de problemas: mensajes de error frecuentes

Los mensajes de error de PostgreSQL son bastante descriptivos si sabes leerlos. Estos son los que aparecerán con más frecuencia mientras practicas.

Clave duplicada al insertar un correo existente:

ERROR:  duplicate key value violates unique constraint "duenos_email_key"
DETAIL:  Key (email)=([email protected]) already exists.

La restricción UNIQUE está haciendo su trabajo. Si el objetivo es insertar o actualizar según exista el registro, usa INSERT ... ON CONFLICT:

INSERT INTO duenos (nombre, email, telefono)
VALUES ('Lucía Paredes', '[email protected]', '5552223344')
ON CONFLICT (email) DO UPDATE SET telefono = EXCLUDED.telefono;

Referencia a una fila que no existe:

ERROR:  insert or update on table "mascotas" violates foreign key constraint "mascotas_dueno_id_fkey"
DETAIL:  Key (dueno_id)=(99) is not present in table "duenos".

Inserta primero el dueño o corrige el identificador. En SQLite, recuerda que este error solo aparece si activaste PRAGMA foreign_keys = ON; de lo contrario, la fila huérfana se guarda en silencio.

Valor que no cumple una restricción de validación:

ERROR:  new row for relation "mascotas" violates check constraint "mascotas_especie_check"
DETAIL:  Failing row contains (5, 2, Nemo, pez, null, null).

La especie “pez” no está en la lista permitida. Amplía la restricción con ALTER TABLE si el negocio lo requiere, o corrige el dato.

Columnas ambiguas en uniones:

ERROR:  column reference "nombre" is ambiguous
LINE 1: SELECT nombre FROM duenos d JOIN mascotas m ON m.dueno_id = d.id;

Ambas tablas tienen una columna nombre. Califícala con el alias: d.nombre o m.nombre.

Columnas fuera de la agrupación:

ERROR:  column "m.nombre" must appear in the GROUP BY clause or be used in an aggregate function

Cuando usas GROUP BY, cada columna del SELECT debe estar agrupada o envuelta en una función de agregación como COUNT, SUM o MAX.

En SQLite, los mensajes son más breves pero equivalentes:

Runtime error: UNIQUE constraint failed: duenos.email (19)

SQL frente a NoSQL: cuándo elegir cada enfoque

Elegir entre un gestor relacional y uno no relacional es una de las decisiones de arquitectura más comunes. En lugar de seguir modas, conviene evaluar la forma de tus datos y tus patrones de acceso.

Un gestor relacional como PostgreSQL o MySQL encaja mejor cuando:

  • Los datos tienen relaciones claras y estables entre entidades.
  • Necesitas transacciones fiables que afecten a varias tablas, como pagos o inventarios.
  • Harás consultas ad hoc y reportes con agregaciones y uniones complejas.
  • La integridad de los datos es prioritaria.

Una base NoSQL encaja mejor cuando:

  • La estructura de los datos varía mucho entre registros o evoluciona con rapidez.
  • El patrón de acceso es simple y conocido, como leer un documento completo por su identificador.
  • Necesitas escalar horizontalmente a volúmenes enormes repartidos en muchos servidores.
  • Requieres latencias de microsegundos para cachés o sesiones, terreno típico de Redis.

La frontera, además, se ha difuminado. PostgreSQL maneja documentos JSON con el tipo JSONB e índices especializados, y MongoDB, cuya versión 9.0 alcanzó disponibilidad general en septiembre de 2026, ofrece transacciones multidocumento. Muchas arquitecturas modernas combinan ambos mundos: una base relacional como fuente de verdad y Redis como caché.

ALTER TABLE mascotas ADD COLUMN datos_extra JSONB;

UPDATE mascotas
SET datos_extra = '{"microchip": "985112003456789", "alergias": ["penicilina"]}'
WHERE nombre = 'Canela';

SELECT nombre FROM mascotas
WHERE datos_extra -> 'alergias' ? 'penicilina';

En cuanto a versiones, el panorama relacional actual incluye PostgreSQL 18 como línea estable con la versión 19 en su fase final de lanzamiento, MySQL con sus ramas de soporte extendido 8.4 y 9.7 y la nueva numeración por calendario de sus versiones Innovation, que comenzó con la 26.7, y SQLite en la 3.54.1.

Casos de uso reales

Los conceptos de esta guía aparecen en sistemas que usas todos los días. Una tienda en línea modela clientes, productos, pedidos y líneas de pedido en tablas relacionadas, y usa transacciones para que cobrar y descontar inventario ocurra como una sola operación.

Los bancos y fintech dependen de las garantías ACID para que una transferencia nunca reste dinero de una cuenta sin sumarlo en la otra, y de niveles de aislamiento estrictos para evitar condiciones de carrera.

Las aplicaciones móviles utilizan SQLite como base local en el propio dispositivo: es el gestor más desplegado del mundo y viene incluido en Android, iOS y la mayoría de navegadores.

Los equipos de análisis consultan bases relacionales o almacenes de datos con SQL para construir paneles de indicadores, y los sistemas de recomendación combinan bases relacionales con almacenes clave-valor que sirven resultados precalculados a gran velocidad.

En el ámbito de la salud, como la clínica de este tutorial, la normalización y las restricciones evitan historiales duplicados o asignados al paciente equivocado, y los permisos por columna protegen datos sensibles.

Conclusiones

Un DBMS es la pieza que convierte un montón de datos en información fiable: los organiza, los protege, permite el trabajo simultáneo de muchos usuarios y garantiza que nada se pierda. SQL es el lenguaje con el que te comunicas con los gestores relacionales, y su carácter declarativo te permite centrarte en qué necesitas mientras el optimizador decide cómo obtenerlo.

A lo largo de la guía construiste la base de una clínica veterinaria y practicaste los cinco grupos de instrucciones: DDL para definir tablas, DML y consultas para trabajar con los datos, TCL para agrupar operaciones en transacciones ACID y DCL para aplicar el principio de mínimo privilegio. También viste cómo la normalización y las restricciones protegen la integridad, cómo los índices aceleran las consultas y cómo interpretar los errores más habituales.

El siguiente paso es practicar con datos propios. Diseña el esquema de un proyecto que conozcas, normalízalo hasta la tercera forma normal, define sus restricciones y revisa los planes de ejecución de tus consultas más frecuentes. Con estas bases podrás moverte con soltura entre PostgreSQL, MySQL o SQLite, y decidir con criterio cuándo un enfoque NoSQL tiene más sentido.