Saltar al contenido

003 — Tu primera base de datos: crear, insertar y leer

🗂️ parte 🎚️ nivel ⏱️ duración 📗 clase

Programa · Parte 00 · ← Anterior · Siguiente →

Parte 00 — Primeros pasos: del archivo a la base de datos · Fundamentos · 2 horas estimadas · motores sqlite, duckdb, postgresql, mysql, mongodb · laboratorio labs/01-sql-foundations · 3 fuentes.

Conceptos centrales: CREATE TABLE · INSERT · SELECT · definición frente a manipulación · NULL

En este caso se comparan 5 motores: 5 lo resuelven (5 con el resultado comprobado por máquina) y 0 no, con el motivo escrito.

De qué trata esta clase

La primera base de datos real, creada, poblada y consultada en la misma sesión. Introduce la separación entre definir estructuras y manipular contenido, y la primera aparición del nulo. A partir de aquí todo el programa se puede ejecutar, no solo leer.

flowchart LR
    C["🗄️ Clase 003"]
    C --> K1["CREATE TABLE"]
    C --> K2["INSERT"]
    C --> K3["SELECT"]
    C --> K4["definición frente a manipulación"]
    C --> K5["NULL"]
    classDef raiz fill:#0b3d2e,stroke:#3fb950,color:#fff
    class C raiz

Antes de empezar

Esta clase supone que ya trabajaste lo siguiente. Si algo de la última columna no te suena, vuelve a esa clase antes de seguir: aquí se usa sin volver a explicarlo.

# Clase previa Lo que se da por sabido
001 Qué es un dato, un registro y una tabla dato · información · registro · campo · tabla
002 Del archivo y la hoja de cálculo a la base de datos integridad declarada · concurrencia · consulta declarativa · durabilidad

Vocabulario de la clase

Los términos que siguen se usan más adelante con este significado exacto. La definición completa, con sus términos relacionados, está en el glosario del programa.

Término Qué significa Procedencia
CREATE TABLE La orden que declara una tabla: columnas, tipos y restricciones. Es el contrato; a partir de ahí el motor rechaza todo lo que no lo cumpla, venga de donde venga. se introduce aquí
INSERT La orden que añade filas. Falla —y debe fallar— si la fila viola una restricción declarada: es el momento en que la integridad declarada demuestra que sirve para algo. se introduce aquí
SELECT La orden de lectura. Nunca modifica datos; describe el conjunto que se quiere y deja al motor la estrategia para producirlo. se introduce aquí
definición frente a manipulación SQL se separa en DDL, que define y cambia estructuras (CREATE, ALTER, DROP), y DML, que consulta y cambia contenido (SELECT, INSERT, UPDATE, DELETE). La distinción importa porque no todos los motores dan al DDL las mismas garantías transaccionales. se introduce aquí
NULL Marca de ausencia de valor: no es cero, ni cadena vacía, ni «desconocido» codificado a mano. Introduce una lógica de tres valores que cambia el resultado de comparaciones, agregados y NOT IN. se introduce aquí

Propósito

Crear una base de datos, una tabla y guardar datos dentro, con la menor cantidad de ceremonia posible. Al terminar esta clase habrás ejecutado las tres órdenes que sostienen todo lo demás —CREATE TABLE, INSERT y SELECT— y sabrás qué hace cada una.

Resultados de aprendizaje

Al terminar podrás:

  1. Crear una tabla declarando sus campos y sus tipos.
  2. Insertar filas y leerlas.
  3. Explicar la diferencia entre definir el esquema y modificar los datos.
  4. Reconocer el error de sintaxis más común y corregirlo sin buscarlo.
  5. Nombrar la fuente de cada afirmación anterior.

Fundamentos

Tres órdenes y dos familias

Todo lo que se hace con SQL cae en dos familias, y conviene separarlas desde el primer día:

Familia Qué hace Órdenes
Definición (DDL) Describe la forma de los datos CREATE, ALTER, DROP
Manipulación (DML) Trabaja con los datos INSERT, SELECT, UPDATE, DELETE

La distinción importa porque las dos familias se usan en momentos distintos: la definición, pocas veces y con cuidado; la manipulación, todo el rato. Y porque en la mayoría de los motores un cambio de definición no se puede deshacer con la misma facilidad que un cambio de datos.

CREATE TABLE: declarar la forma

CREATE TABLE estudiantes (
    id     INTEGER PRIMARY KEY,
    nombre TEXT NOT NULL,
    correo TEXT
);

Se lee de arriba abajo: una tabla llamada estudiantes, con tres campos. id es un número entero y es la clave primaria —el campo que distingue una fila de otra, que se estudiará en su propia clase—. nombre es texto y no puede quedar vacío. correo es texto y sí puede.

Eso es todo lo que hace CREATE TABLE: escribir en el catálogo del motor cómo tienen que ser las filas de esa tabla. No guarda ningún dato.

INSERT: añadir filas

INSERT INTO estudiantes (id, nombre, correo)
VALUES (1, 'Ada Lovelace', 'ada@example.org');

Se nombran los campos que se van a rellenar y se dan los valores en el mismo orden. Nombrar los campos parece redundante y no lo es: el día que la tabla gane una columna, el INSERT que no los nombraba deja de funcionar o, peor, empieza a poner cada valor en el sitio equivocado.

El texto va entre comillas simples. Los números, no. Es el error de sintaxis más frecuente de las primeras horas, y da un mensaje distinto en cada motor.

SELECT: leer

SELECT nombre, correo FROM estudiantes;

«De la tabla estudiantes, dame los campos nombre y correo de todas las filas.» El * sirve para pedir todos los campos, y conviene acostumbrarse a no usarlo fuera de la exploración: una consulta con SELECT * cambia de resultado cuando alguien añade una columna, y quien la escribió ya no está para explicarlo.

Dónde ocurre todo esto

Para esta clase no hace falta instalar nada: SQLite viene incluido con Python, y una base de datos es un archivo —o ni siquiera eso, si se pide en memoria—. El laboratorio del repositorio funciona así, y por eso se puede ejecutar en cualquier máquina.

flowchart LR
    A["CREATE TABLE<br/>declara la forma"] --> B["INSERT<br/>añade filas"]
    B --> C["SELECT<br/>lee filas"]
    C -->|"la forma no cambia"| B

Ejemplo trabajado

Una academia quiere registrar sus tres primeros estudiantes.

CREATE TABLE estudiantes (
    id     INTEGER PRIMARY KEY,
    nombre TEXT NOT NULL,
    correo TEXT
);

INSERT INTO estudiantes (id, nombre, correo) VALUES (1, 'Ada Lovelace', 'ada@example.org');
INSERT INTO estudiantes (id, nombre, correo) VALUES (2, 'Linus Torvalds', 'linus@example.org');
INSERT INTO estudiantes (id, nombre, correo) VALUES (3, 'Grace Hopper', NULL);

SELECT id, nombre FROM estudiantes ORDER BY nombre;

Resultado:

id nombre
1 Ada Lovelace
3 Grace Hopper
2 Linus Torvalds

Tres cosas que merece la pena mirar.

El orden del resultado no es el de inserción: es el que pidió ORDER BY. Sin esa cláusula, ningún motor está obligado a devolver las filas en un orden concreto, aunque en una tabla pequeña casi siempre lo parezca. Es una de las confusiones más persistentes y tiene su propia clase más adelante.

NULL —sin comillas— no es la palabra «NULL»: es la marca de ausencia de valor. Grace no tiene correo, y eso es distinto de tener un correo vacío.

Y si se intenta insertar un cuarto estudiante con id 1, el motor lo rechaza: la clave primaria no admite repetidos. Esa negativa es exactamente lo que se compró al elegir una base de datos.

Errores frecuentes

  1. Olvidar las comillas en el texto o ponerlas en los números. VALUES (1, Ada) falla; VALUES ('1', 'Ada') a veces funciona y guarda el número como texto, que es peor.
  2. Usar comillas dobles para el texto. En SQL estándar, las comillas dobles son para los nombres de tabla y columna; el texto va en comillas simples. Algunos motores lo perdonan y otros no.
  3. INSERT sin nombrar los campos. Funciona hasta que la tabla cambia.
  4. Confundir NULL con 'NULL'. El primero es ausencia de valor; el segundo es un texto de cuatro letras.
  5. Suponer que el orden de salida es el de entrada. Sin ORDER BY no hay orden garantizado.
  6. Ejecutar DROP TABLE para «volver a empezar» en la base equivocada. La definición no se deshace con Ctrl+Z.

Ejemplo de transferencia

Estas tres órdenes son las mismas —con diferencias mínimas de sintaxis— en PostgreSQL, MySQL, SQL Server, Oracle y DuckDB. Lo que se aprende aquí no es SQLite: es el subconjunto de SQL que la norma ISO/IEC 9075 define y que todos implementan. Cambiar de motor no obliga a reaprender esto.

Reto de transferencia

  1. Crea una tabla para algo que lleves de verdad: libros, gastos, plantas, partidas. Declara al menos cuatro campos y decide cuáles no pueden quedar vacíos.
  2. Inserta cinco filas, y que una de ellas tenga un campo sin valor.
  3. Escribe tres consultas distintas sobre esos datos.
  4. Intenta insertar una fila que viole una de tus reglas, y guarda el mensaje de error: es la prueba de que la regla existe.

Preguntas de evaluación

  1. ¿Qué diferencia hay entre la familia de definición y la de manipulación?
  2. ¿Por qué conviene nombrar los campos en un INSERT?
  3. ¿Qué significa NOT NULL y qué ocurre exactamente al violarlo?
  4. Explica por qué SELECT * es cómodo para explorar y mala idea en el código de una aplicación.

🌐 El mismo problema en cada motor

Caso: Crear una tabla, guardar tres filas y leerlas

Las tres órdenes que sostienen todo lo demás, en su forma mínima: crear la tabla, insertar filas y leerlas. Tres estudiantes, uno de ellos sin correo —porque la ausencia de dato es un caso normal y hay que saber escribirla— y la lista devuelta ordenada por nombre, que es una decisión que hay que pedir explícitamente.

Que el resultado salga en orden alfabético y no en el de inserción es lo único sorprendente del caso, y es deliberado: una tabla no tiene orden, y creer lo contrario es la primera confusión que hay que quitarse de encima.

Salida esperada, idéntica en todos los motores que lo resuelven:

id nombre
1 Ada
3 Grace
2 Linus

El contrato vive en motores.yaml y lo comprueba python scripts/verificar_equivalencia.py --clase 003: 5 de las 5 implementaciones se ejecutan de verdad y su resultado se compara con esa tabla; el resto se declara como material revisado, no ejecutado.

Motor ¿Resuelve el caso? Nivel de prueba Código Fuente
SQLite núcleo código doc oficial
DuckDB núcleo código doc oficial
PostgreSQL servicio código doc oficial
MySQL servicio código doc oficial
MongoDB servicio código doc oficial

Los que resuelven el caso

SQLite · implementaciones/sqlite/consulta.sql

verificado — se ejecuta en CI sin servicios

-- motor: sqlite
-- doc: https://sqlite.org/lang_insert.html
-- nota: el resultado NO sale en orden de insercion, sale en el que pidio el
--       ORDER BY. Sin esa clausula, ningun motor esta obligado a devolver nada
--       en un orden concreto.

-- === preparacion ===
CREATE TABLE estudiantes (
    id     INTEGER PRIMARY KEY,
    nombre TEXT NOT NULL,
    correo TEXT
);

INSERT INTO estudiantes (id, nombre, correo) VALUES (1, 'Ada', 'ada@example.org');
INSERT INTO estudiantes (id, nombre, correo) VALUES (2, 'Linus', 'linus@example.org');
-- Grace no tiene correo. NULL sin comillas: ausencia de valor, no la palabra.
INSERT INTO estudiantes (id, nombre, correo) VALUES (3, 'Grace', NULL);

-- === consulta ===
SELECT id, nombre FROM estudiantes ORDER BY nombre;

DuckDB · implementaciones/duckdb/consulta.sql

verificado — se ejecuta en CI sin servicios

-- motor: duckdb
-- doc: https://duckdb.org/docs/stable/sql/statements/insert
-- nota: insertar filas de una en una es lo que peor hace un motor columnar.
--       Funciona, y va contra su diseno: aqui se hace asi para que la sentencia
--       sea identica a la de los demas.

-- === preparacion ===
CREATE TABLE estudiantes (
    id     INTEGER PRIMARY KEY,
    nombre VARCHAR NOT NULL,
    correo VARCHAR
);

INSERT INTO estudiantes (id, nombre, correo) VALUES (1, 'Ada', 'ada@example.org');
INSERT INTO estudiantes (id, nombre, correo) VALUES (2, 'Linus', 'linus@example.org');
-- Grace no tiene correo. NULL sin comillas: ausencia de valor, no la palabra.
INSERT INTO estudiantes (id, nombre, correo) VALUES (3, 'Grace', NULL);

-- === consulta ===
SELECT id, nombre FROM estudiantes ORDER BY nombre;

PostgreSQL · implementaciones/postgresql/consulta.sql

verificado — se ejecuta contra el motor real levantado con docker compose

-- motor: postgresql
-- doc: https://www.postgresql.org/docs/current/sql-insert.html
-- nota: en cuanto hay mas de un cliente escribiendo, el identificador no lo
--       pone la aplicacion: lo pone el motor.
--         id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY
--       Aqui se escribe a mano para que las tres filas sean comparables con las
--       de los demas motores.

-- === preparacion ===
DROP TABLE IF EXISTS estudiantes;

CREATE TABLE estudiantes (
    id     integer PRIMARY KEY,
    nombre text NOT NULL,
    correo text
);

INSERT INTO estudiantes (id, nombre, correo) VALUES (1, 'Ada', 'ada@example.org');
INSERT INTO estudiantes (id, nombre, correo) VALUES (2, 'Linus', 'linus@example.org');
-- Grace no tiene correo. NULL sin comillas: ausencia de valor, no la palabra.
INSERT INTO estudiantes (id, nombre, correo) VALUES (3, 'Grace', NULL);

-- === consulta ===
SELECT id, nombre FROM estudiantes ORDER BY nombre;

MySQL · implementaciones/mysql/consulta.sql

verificado — se ejecuta contra el motor real levantado con docker compose

-- motor: mysql
-- doc: https://dev.mysql.com/doc/refman/8.4/en/insert.html
-- nota: casi identico. La diferencia que no se ve aqui y muerde despues: la
--       comparacion de texto ignora mayusculas por omision, asi que un UNIQUE
--       sobre un correo trata 'Ada@x.org' y 'ada@x.org' como el mismo valor.

-- === preparacion ===
DROP TABLE IF EXISTS estudiantes;

CREATE TABLE estudiantes (
    id     INT PRIMARY KEY,
    nombre VARCHAR(50) NOT NULL,
    correo VARCHAR(50)
);

INSERT INTO estudiantes (id, nombre, correo) VALUES (1, 'Ada', 'ada@example.org');
INSERT INTO estudiantes (id, nombre, correo) VALUES (2, 'Linus', 'linus@example.org');
-- Grace no tiene correo. NULL sin comillas: ausencia de valor, no la palabra.
INSERT INTO estudiantes (id, nombre, correo) VALUES (3, 'Grace', NULL);

-- === consulta ===
SELECT id, nombre FROM estudiantes ORDER BY nombre;

MongoDB · implementaciones/mongodb/consulta.js

verificado — se ejecuta contra el motor real levantado con docker compose

// motor: mongodb
// doc: https://www.mongodb.com/docs/manual/reference/method/db.collection.insertMany/
// nota: no hay CREATE. Insertar un documento crea la coleccion, y cada
//       documento puede tener campos distintos. Grace no lleva el campo correo:
//       en una tabla habria una celda vacia, aqui no hay celda.

// === preparacion ===
db.estudiantes.drop();
db.estudiantes.insertMany([
  { _id: 1, nombre: "Ada", correo: "ada@example.org" },
  { _id: 2, nombre: "Linus", correo: "linus@example.org" },
  { _id: 3, nombre: "Grace" },
]);

// === consulta ===
db.estudiantes
  .find({}, { _id: 1, nombre: 1 })
  .sort({ nombre: 1 })
  .forEach((d) => print(d._id + "|" + d.nombre));

Laboratorio

python scripts/validate_repository.py
python labs/01-sql-foundations/run_lab.py

Guarda como evidencia la salida completa, la versión del motor y la semilla o los parámetros usados. Una captura sin comando no es evidencia: no se puede repetir.

Evaluación

Criterio Peso Qué se comprueba
Comprensión conceptual 25 % Explica el mecanismo, no solo el resultado
Ejecución reproducible 25 % Otra persona obtiene lo mismo con las instrucciones dadas
Interpretación basada en evidencia 25 % Cada conclusión se apoya en una salida o una medición
Límites y riesgos declarados 25 % Dice qué no demuestra el ejercicio y qué faltaría en producción

La clase se da por superada cuando la respuesta explica el mecanismo, muestra la salida que la respalda y declara al menos un límite del ejercicio.

Fuentes de esta clase

Todo lo afirmado arriba procede de estas obras. Los identificadores viven en catalog/sources.json y el estado de los enlaces se comprueba con python scripts/check_external_links.py.


Programa · Parte 00 · ← Anterior · Siguiente →