Ya sabes qué es SQL y cómo se escribe. Toca ahora entender sobre qué actúa: la estructura donde viven los datos. En esta lección desmontaremos la jerarquía completa —servidor, base de datos, esquema, tabla, fila, columna—, veremos por qué la analogía con una hoja de cálculo ayuda al principio pero se rompe enseguida, y recorreremos en detalle el catálogo de tipos de datos de PostgreSQL. Ese último punto es más importante de lo que parece: elegir mal un tipo es un error que se paga durante años, y el caso del dinero guardado en coma flotante es el ejemplo canónico de por qué. Terminaremos aprendiendo a inspeccionar tablas que ya existen, una habilidad que necesitarás cada vez que te enfrentes a una base de datos ajena.
Contenido
- La jerarquía: servidor, base de datos, esquema, tabla
- Tablas, filas y columnas
- La analogía de la hoja de cálculo (y dónde se rompe)
- Tipos de datos numéricos
- Por qué el dinero nunca va en coma flotante
- Tipos de texto
- Tipos de fecha y hora
- Booleanos, UUID y JSONB
- NULL: la ausencia de valor
- Equivalencias entre PostgreSQL, MySQL y SQLite
- Cómo inspeccionar tablas existentes
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- La jerarquía: servidor, base de datos, esquema, tabla
En PostgreSQL los objetos se organizan en cuatro niveles:
graph TD
A["Servidor / Clúster<br/>(un proceso, un puerto: 5432)"] --> B["Base de datos: tiendaverde"]
A --> B2["Base de datos: postgres"]
A --> B3["Base de datos: pruebas_sql"]
B --> C["Esquema: public"]
B --> C2["Esquema: information_schema"]
C --> D["Tabla: productos"]
C --> D2["Tabla: clientes"]
C --> D3["Tabla: pedidos"]
D --> E["Columnas: id, nombre, precio…"]
D --> E2["Filas: cada producto concreto"]
| Nivel | Qué es | Ejemplo en el curso |
|---|---|---|
| Servidor (clúster) | El proceso PostgreSQL en marcha, escuchando en un puerto | El contenedor pg-curso en localhost:5432 |
| Base de datos | Un conjunto aislado de datos. Una conexión solo ve una a la vez | tiendaverde |
| Esquema | Un espacio de nombres dentro de la base de datos | public |
| Tabla | Una colección de filas con la misma estructura | productos |
| Columna | Un campo con nombre y tipo | precio NUMERIC(10,2) |
| Fila | Un registro concreto | El producto "Miel de azahar cruda 500 g" |
Dos consecuencias prácticas de esta jerarquía:
- No puedes consultar dos bases de datos a la vez. Si
tiendaverdeypruebas_sqlviven en el mismo servidor, una consulta no puede combinarlas directamente (harían falta extensiones comodblinkopostgres_fdw). En MySQL, en cambio, sí es habitual escribirSELECT ... FROM otra_base.tabla, porque allí "base de datos" y "esquema" son casi sinónimos. - Los esquemas sí se combinan libremente. Dentro de
tiendaverdepodrías tener un esquemaventasy otroanaliticay consultarlos juntos sin problema.
El esquema public
Toda base de datos PostgreSQL nace con un esquema llamado public. Si creas una tabla sin indicar esquema, va a parar allí, y si la consultas sin prefijo se busca allí. Estas dos sentencias son equivalentes en nuestra configuración:
El orden de búsqueda lo determina el parámetro search_path:
| search_path |
|---|
| "$user", public |
Significa: "busca primero un esquema que se llame como el usuario conectado; si no existe, busca en public". Los esquemas sirven para organizar bases grandes (separar por área funcional, por cliente, por entorno) y para evitar colisiones de nombres. Todo TiendaVerde vive en public, así que no volveremos a preocuparnos por esto.
- Tablas, filas y columnas
Una tabla es una colección de filas que comparten la misma estructura. Cada tabla se define por:
- Un nombre (
productos). - Un conjunto ordenado de columnas, cada una con nombre y tipo de dato.
- Opcionalmente, restricciones que limitan qué valores son válidos (módulo 5).
Una vista simplificada de productos:
| id | nombre | categoria_id | precio | stock | activo |
|---|---|---|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | 1 | 12.50 | 120 | true |
| 2 | Arroz integral ecológico 1 kg | 1 | 3.90 | 200 | true |
| 6 | Crema facial de aloe vera 50 ml | 2 | 18.90 | 60 | true |
Terminología formal frente a la coloquial:
| Término coloquial | Término del modelo relacional | Qué es |
|---|---|---|
| Tabla | Relación | El conjunto de datos |
| Fila / registro | Tupla | Una entidad concreta |
| Columna / campo | Atributo | Una propiedad de esa entidad |
| Tipo de la columna | Dominio | El conjunto de valores válidos |
Tres propiedades fundamentales que conviene grabar desde el principio:
- Todas las filas tienen las mismas columnas. No existe una fila "con un campo extra". Si un dato no aplica, se guarda
NULL. - Cada columna tiene un único tipo. No puedes guardar
'mañana'en una columnaDATE. - Las filas no tienen orden intrínseco. Una tabla es un conjunto. Si no pides
ORDER BYexplícitamente, el motor puede devolvértelas en cualquier orden, y ese orden puede cambiar mañana. Es un error clásico confiar en el "orden natural".
- La analogía de la hoja de cálculo (y dónde se rompe)
Al principio ayuda pensar en una tabla como una hoja de Excel: la primera fila son los encabezados (columnas) y las siguientes, los datos (filas). La analogía funciona... hasta cierto punto.
| Aspecto | Hoja de cálculo | Tabla de base de datos |
|---|---|---|
| Tipos | Cada celda puede contener lo que sea | Toda la columna comparte un único tipo, verificado por el motor |
| Orden de las filas | Es visible y significativo | No existe orden implícito; se pide con ORDER BY |
| Relaciones | Se simulan con BUSCARV y referencias frágiles |
Son de primera clase: claves foráneas con integridad garantizada |
| Tamaño | Miles o cientos de miles de filas | Millones o miles de millones |
| Concurrencia | Un usuario a la vez (o conflictos de versión) | Cientos de usuarios simultáneos con transacciones |
| Integridad | Nada impide teclear "doce euros" en una columna de importes | Restricciones que rechazan el dato inválido |
| Fórmulas | Guardadas en las celdas | El dato se guarda; el cálculo se hace al consultar |
| Deshacer | Ctrl+Z | ROLLBACK de una transacción (módulo 9) |
La diferencia conceptual más profunda es la tipificación estricta. En Excel, una columna de precios puede tener 12,50, 12.50, "12,50 €" y doce con cincuenta, y nadie te avisa hasta que la suma da mal. En PostgreSQL, si la columna es NUMERIC(10,2), el motor rechaza cualquier cosa que no sea un número:
Ese error, que puede parecer una molestia, es exactamente el valor que aporta una base de datos: los datos incorrectos no llegan a entrar.
- Tipos de datos numéricos
| Tipo | Tamaño | Rango / Precisión | Cuándo usarlo |
|---|---|---|---|
SMALLINT |
2 bytes | -32 768 a 32 767 | Contadores muy pequeños, edades |
INTEGER (INT) |
4 bytes | ±2 147 483 647 | Por defecto para enteros: ids, stock, cantidades |
BIGINT |
8 bytes | ±9,2 × 10¹⁸ | Ids de tablas enormes, contadores masivos |
NUMERIC(p,s) / DECIMAL(p,s) |
Variable | Exacto, hasta 131 072 dígitos | Dinero y cualquier cálculo exacto |
REAL |
4 bytes | ~6 dígitos decimales | Magnitudes científicas aproximadas |
DOUBLE PRECISION |
8 bytes | ~15 dígitos decimales | Cálculos científicos, coordenadas |
SERIAL / BIGSERIAL |
4/8 bytes | Entero autoincremental | Claves primarias (ver nota) |
Sobre NUMERIC(p,s):
p(precisión) es el número total de dígitos.s(escala) es cuántos de esos dígitos van tras la coma.NUMERIC(10,2)admite hasta 99 999 999,99 → ocho dígitos enteros y dos decimales.
En TiendaVerde usamos NUMERIC(10,2) para precio, coste, gastos_envio, precio_unitario, importe y salario, y NUMERIC(4,2) para descuento (una fracción entre 0 y 1, con dos decimales).
Nota sobre
SERIAL: no es un tipo real, sino un atajo que crea unINTEGERmás una secuencia que lo autoincrementa. Desde PostgreSQL 10 la forma recomendada por el estándar esGENERATED BY DEFAULT AS IDENTITY, que es la que usa el script del curso. Lo verás con detalle en el módulo 5.
- Por qué el dinero nunca va en coma flotante
REAL y DOUBLE PRECISION almacenan los números en coma flotante binaria (estándar IEEE 754). El problema es que muchos decimales que en base 10 son exactos, en base 2 son periódicos: 0,1 en binario es infinito, igual que 1/3 lo es en decimal. El ordenador guarda una aproximación, y esos errores minúsculos se acumulan.
Compruébalo:
| suma_flotante | suma_exacta |
|---|---|
| 0.30000001 | 0.3 |
Y el caso que te arruinaría un cierre contable:
SELECT (0.1::DOUBLE PRECISION + 0.2::DOUBLE PRECISION) = 0.3 AS son_iguales_flotante,
(0.1::NUMERIC + 0.2::NUMERIC) = 0.3 AS son_iguales_exacto;| son_iguales_flotante | son_iguales_exacto |
|---|---|
| false | true |
Llevado a TiendaVerde: si guardáramos precio como REAL y sumáramos las 47 líneas de pedido de la base de datos, el total podría salir 143,20999999998 en lugar de 143,21. Multiplícalo por miles de pedidos al mes y tendrás un descuadre contable imposible de justificar.
| Tipo | Naturaleza | Velocidad | Exactitud | Uso correcto |
|---|---|---|---|---|
REAL / DOUBLE PRECISION |
Aproximada (binaria) | Muy rápida | No exacta | Física, estadística, coordenadas, medias aproximadas |
NUMERIC(p,s) |
Exacta (decimal) | Más lenta | Exacta | Dinero, porcentajes contables, cantidades facturables |
Regla sin excepciones: dinero →
NUMERIC. NuncaFLOAT,REALniDOUBLE PRECISION. La penalización de velocidad es irrelevante comparada con un céntimo perdido.Existe además el tipo
MONEYen PostgreSQL, pero no se recomienda: depende de la configuración regional del servidor y no admite bien varias divisas. UsaNUMERIC.
- Tipos de texto
| Tipo | Descripción | Cuándo usarlo |
|---|---|---|
VARCHAR(n) |
Texto de longitud variable con máximo n caracteres |
Cuando el límite es una regla de negocio real |
TEXT |
Texto de longitud ilimitada | La opción por defecto en PostgreSQL |
CHAR(n) |
Longitud fija; rellena con espacios hasta n |
Casi nunca. Solo para códigos de longitud fija estricta |
Una particularidad de PostgreSQL que sorprende a quien viene de otros motores: TEXT y VARCHAR tienen exactamente el mismo rendimiento. Internamente son el mismo tipo; VARCHAR(n) solo añade una comprobación de longitud. No hay ninguna ventaja de velocidad en poner un límite.
Entonces, ¿cuándo poner VARCHAR(n)? Cuando el límite signifique algo:
pais VARCHAR(60): nombre de país, un límite razonable.email VARCHAR(120): hay un máximo práctico conocido.descripcion TEXT: no sabemos cuánto escribirá nadie.comentario TEXT: una reseña puede ser larga.
Evita CHAR(n). Rellena con espacios a la derecha y provoca comparaciones sorprendentes:
| parecen_iguales | longitud_almacenada |
|---|---|
| true | 2 |
El valor se guarda como 'ES ' pero al compararlo se ignoran los espacios finales, lo que genera confusión constante al exportar o concatenar.
Nota de dialecto: en MySQL sí hay diferencias de rendimiento y almacenamiento entre
CHAR,VARCHARyTEXT(losTEXTse almacenan fuera de la fila y no admiten valor por defecto). Lo que aquí es indiferente, allí no lo es.
- Tipos de fecha y hora
| Tipo | Qué guarda | Ejemplo | Uso en TiendaVerde |
|---|---|---|---|
DATE |
Solo la fecha | 2026-02-14 |
fecha_pedido, fecha_registro, fecha_alta, fecha_contratacion |
TIME |
Solo la hora | 18:30:00 |
Horarios de apertura |
TIMESTAMP |
Fecha y hora, sin zona horaria | 2026-02-14 18:30:00 |
Cuando la zona es irrelevante |
TIMESTAMPTZ |
Fecha y hora con zona horaria | 2026-02-14 18:30:00+01 |
Marcas de auditoría, eventos reales |
INTERVAL |
Una duración | 3 days, 2 hours 30 minutes |
Plazos de entrega |
La distinción entre TIMESTAMP y TIMESTAMPTZ es la que más problemas causa en producción:
TIMESTAMPguarda literalmente lo que le das. Si un cliente francés y otro español registran "18:30", se guardan igual aunque sean momentos distintos.TIMESTAMPTZconvierte a UTC al guardar y a la zona del cliente al leer. Representa un instante real del tiempo.
SELECT NOW() AS ahora_con_zona,
NOW()::TIMESTAMP AS ahora_sin_zona,
CURRENT_DATE AS hoy,
AGE(DATE '2026-02-14', DATE '2025-11-14') AS diferencia;| ahora_con_zona | ahora_sin_zona | hoy | diferencia |
|---|---|---|---|
| 2026-02-25 10:14:07.412+01 | 2026-02-25 10:14:07.412 | 2026-02-25 | 3 mons |
Regla práctica: si el momento tiene relevancia real (cuándo ocurrió algo), usa
TIMESTAMPTZ. Si es una fecha de calendario sin hora (fecha de un pedido, fecha de nacimiento), usaDATE. TiendaVerde usaDATEen todas sus fechas porque son fechas de calendario, no instantes.Dialecto: MySQL tiene
DATETIME(sin zona) yTIMESTAMP(con conversión a UTC, pero limitado hasta 2038). SQLite no tiene tipo de fecha: guarda texto ISO, números o julian days, y las funciones de fecha operan sobre esas representaciones.
- Booleanos, UUID y JSONB
BOOLEAN
Guarda TRUE, FALSE o NULL. Ocupa 1 byte. En TiendaVerde lo usan productos.activo y proveedores.activo.
Ese NULL tiene sentido de negocio: "activo = TRUE" es un producto en venta, "FALSE" uno descatalogado, y NULL significaría "todavía no lo hemos decidido". Por eso los booleanos en SQL tienen tres estados, no dos.
UUID
Un identificador universal de 128 bits, del estilo a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11. Ocupa 16 bytes.
| Ventaja | Inconveniente |
|---|---|
| Se puede generar en el cliente sin consultar al servidor | Ocupa 4 veces más que un INTEGER |
| No revela cuántos registros tienes | Ilegible para humanos |
| Único entre sistemas distintos (útil en microservicios) | Peor localidad de índice: inserciones más lentas |
TiendaVerde usa INTEGER para sus id porque es una base pequeña, didáctica y donde poder escribir WHERE id = 7 es una ventaja de aprendizaje.
JSONB
PostgreSQL permite guardar documentos JSON en una columna. JSON guarda el texto tal cual; JSONB lo guarda en formato binario indexado, y es el que se usa en la práctica. Sirve para datos semiestructurados cuyo esquema varía (atributos específicos de cada producto, respuestas de una pasarela de pago, configuraciones).
| origen |
|---|
| "Valencia" |
Lo mencionamos aquí para que sepas que existe; su uso completo se trata en la lección 10-06. Y una advertencia: JSONB no es una excusa para no diseñar bien tus tablas. Lo que es estructurado va en columnas.
- NULL: la ausencia de valor
NULL no es cero, ni cadena vacía, ni FALSE. Significa "aquí no hay valor" o "no se sabe".
En TiendaVerde hay tres nulos con significado de negocio bien definido:
| Columna | Qué significa su NULL |
|---|---|
pedidos.empleado_id |
El pedido llegó por la web; ningún comercial lo gestionó |
clientes.referido_por_id |
El cliente llegó por su cuenta, nadie lo refirió |
empleados.jefe_id |
Es la dirección general: no tiene superior |
Lo esencial ahora es entender que NULL se propaga por cualquier operación:
| suma | concatenacion | comparacion |
|---|---|---|
| (null) | (null) | (null) |
Cualquier cálculo que toque un NULL devuelve NULL, porque operar con algo desconocido produce algo desconocido. Y como ya viste en la lección anterior, para comprobar si algo es nulo se usa IS NULL, nunca = NULL.
Distingue estos tres casos, que no son lo mismo:
| Valor | Significado |
|---|---|
NULL |
No hay dato / se desconoce |
0 |
Hay dato, y vale cero |
'' (cadena vacía) |
Hay dato, y es un texto sin caracteres |
El tratamiento completo de los nulos —cómo afectan a las agregaciones, a los
JOINy a los filtros— es la lección 04-03. Por ahora basta el concepto.
- Equivalencias entre PostgreSQL, MySQL y SQLite
Si tienes que portar un esquema o leer código ajeno, esta tabla te ahorrará tiempo:
| Concepto | PostgreSQL 16 | MySQL 8 | SQLite 3 |
|---|---|---|---|
| Entero | INTEGER, BIGINT |
INT, BIGINT |
INTEGER |
| Autoincremental | GENERATED AS IDENTITY / SERIAL |
AUTO_INCREMENT |
INTEGER PRIMARY KEY AUTOINCREMENT |
| Decimal exacto | NUMERIC(p,s) |
DECIMAL(p,s) |
NUMERIC (afinidad, sin garantía) |
| Coma flotante | REAL, DOUBLE PRECISION |
FLOAT, DOUBLE |
REAL |
| Texto corto | VARCHAR(n) |
VARCHAR(n) |
TEXT |
| Texto largo | TEXT |
TEXT, LONGTEXT |
TEXT |
| Booleano | BOOLEAN (real) |
TINYINT(1) (alias) |
Sin tipo: 0 / 1 |
| Fecha | DATE |
DATE |
TEXT con formato ISO |
| Fecha y hora | TIMESTAMP, TIMESTAMPTZ |
DATETIME, TIMESTAMP |
TEXT / INTEGER |
| JSON | JSONB (binario, indexable) |
JSON |
TEXT + funciones JSON1 |
| UUID | UUID (nativo) |
CHAR(36) o BINARY(16) |
TEXT |
| Sistema de tipos | Estricto | Estricto (con modo estricto activo) | Dinámico: casi todo se acepta |
La diferencia más peligrosa está en la última fila. SQLite usa afinidad de tipos: si declaras una columna INTEGER y le insertas el texto 'hola', lo guarda sin protestar. Es cómodo para prototipar y desastroso para garantizar integridad. Es la razón principal por la que este curso usa PostgreSQL.
- Cómo inspeccionar tablas existentes
Cuando llegas a un proyecto nuevo, lo primero es entender su esquema. Hay dos caminos.
11.1. Metacomandos de psql (rápido)
List of relations Schema | Name | Type | Owner --------+---------------+-------+----------- public | categorias | table | curso_sql public | clientes | table | curso_sql public | devoluciones | table | curso_sql public | empleados | table | curso_sql public | lineas_pedido | table | curso_sql public | pedidos | table | curso_sql public | productos | table | curso_sql public | proveedores | table | curso_sql public | resenas | table | curso_sql
Table "public.productos"
Column | Type | Nullable | Default
--------------+-----------------------+----------+------------------------------
id | integer | not null | generated by default as identity
nombre | character varying(150)| not null |
categoria_id | integer | |
proveedor_id | integer | |
precio | numeric(10,2) | not null |
coste | numeric(10,2) | |
stock | integer | not null | 0
activo | boolean | not null | true
fecha_alta | date | not null | CURRENT_DATE
Indexes:
"productos_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"productos_categoria_id_fkey" FOREIGN KEY (categoria_id) REFERENCES categorias(id)
"productos_proveedor_id_fkey" FOREIGN KEY (proveedor_id) REFERENCES proveedores(id)
Referenced by:
TABLE "lineas_pedido" CONSTRAINT ... FOREIGN KEY (producto_id) REFERENCES productos(id)Un solo comando te da columnas, tipos, nulabilidad, valores por defecto, índices, claves foráneas y qué otras tablas apuntan a esta. Usa \d+ productos para ver además el tamaño y los comentarios.
| Metacomando | Qué muestra |
|---|---|
\dt |
Tablas del esquema actual |
\d nombre_tabla |
Estructura completa de una tabla |
\d+ nombre_tabla |
Lo anterior más tamaño, estadísticas y comentarios |
\dn |
Esquemas |
\di |
Índices |
\dv |
Vistas |
\l+ |
Bases de datos con su tamaño |
11.2. information_schema (portable y consultable)
El estándar SQL define un esquema de metadatos llamado information_schema, disponible en PostgreSQL, MySQL y SQL Server. Su ventaja sobre \d es que es SQL normal: puedes filtrarlo, ordenarlo y usarlo desde cualquier lenguaje.
SELECT column_name,
data_type,
character_maximum_length,
numeric_precision,
numeric_scale,
is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'productos'
ORDER BY ordinal_position;| column_name | data_type | character_maximum_length | numeric_precision | numeric_scale | is_nullable |
|---|---|---|---|---|---|
| id | integer | (null) | 32 | 0 | NO |
| nombre | character varying | 150 | (null) | (null) | NO |
| categoria_id | integer | (null) | 32 | 0 | YES |
| proveedor_id | integer | (null) | 32 | 0 | YES |
| precio | numeric | (null) | 10 | 2 | NO |
| coste | numeric | (null) | 10 | 2 | YES |
| stock | integer | (null) | 32 | 0 | NO |
| activo | boolean | (null) | (null) | (null) | NO |
| fecha_alta | date | (null) | (null) | (null) | NO |
Vistas útiles de information_schema:
| Vista | Contiene |
|---|---|
information_schema.tables |
Todas las tablas y vistas |
information_schema.columns |
Todas las columnas con sus tipos |
information_schema.table_constraints |
Restricciones (PK, FK, UNIQUE, CHECK) |
information_schema.key_column_usage |
Qué columnas participan en cada clave |
Y una consulta que usarás a menudo para hacerte un mapa rápido de una base desconocida:
SELECT table_name, COUNT(*) AS num_columnas
FROM information_schema.columns
WHERE table_schema = 'public'
GROUP BY table_name
ORDER BY table_name;| table_name | num_columnas |
|---|---|
| categorias | 3 |
| clientes | 8 |
| devoluciones | 5 |
| empleados | 8 |
| lineas_pedido | 6 |
| pedidos | 7 |
| productos | 9 |
| proveedores | 5 |
| resenas | 6 |
information_schemaes el estándar, pero PostgreSQL tiene además su catálogo propiopg_catalog(pg_tables,pg_class,pg_attribute), más completo y más rápido, aunque no portable. Los metacomandos\dconsultan precisamente ese catálogo.
Errores Comunes y Consejos
- Guardar dinero en
FLOAToREAL. El error más caro de esta lección. SiempreNUMERIC(10,2). - Guardar fechas como texto.
VARCHARpara una fecha impide ordenar bien, calcular diferencias y validar. UsaDATEoTIMESTAMPTZ. - Guardar números que no son cantidades como números. Un código postal (
03001), un teléfono o un NIF son texto: si los guardas comoINTEGERperderás el cero inicial y el+34. - Poner
VARCHAR(255)por costumbre. El 255 viene de MySQL antiguo. En PostgreSQL usaTEXTsalvo que exista un límite de negocio real. - Usar
CHAR(n). El relleno con espacios provoca errores sutiles. Prácticamente nunca es la elección correcta. - Confundir
NULLcon0o''. Son tres cosas distintas y se comportan de forma distinta en filtros y agregaciones. - Confiar en el orden de las filas. Sin
ORDER BYno hay orden garantizado, por mucho que hoy salgan ordenadas. - Consejo: elige el tipo pensando en cinco años vista. Cambiar el tipo de una columna con millones de filas en producción es una operación delicada (módulo 5).
- Consejo:
\d tablaes tu primer comando en cualquier base ajena. Antes de escribir una consulta, mira la estructura. - Consejo: pon nombres de tabla en plural y de columna en singular.
productos.nombrese lee mejor queproducto.nombres.
Ejercicios
Ejercicio 1
Para cada dato de TiendaVerde, elige el tipo PostgreSQL más adecuado y justifícalo en una línea:
- El precio de venta de un producto.
- El código de país de un proveedor en formato ISO (
ES,PT,FR). - El comentario de una reseña.
- La puntuación de una reseña (1 a 5).
- La fecha en que se registró un cliente.
- Si un producto está activo o no.
- El instante exacto en que se confirmó un pago, con clientes en tres países.
- El descuento aplicado a una línea de pedido (fracción de 0 a 1, dos decimales).
Ejercicio 2
Ejecuta estas expresiones y explica qué demuestra cada una:
SELECT 1.0 / 3.0 AS a;
SELECT (1.0 / 3.0)::REAL AS b;
SELECT 100000000.0::REAL + 1 AS c;
SELECT 'abc'::CHAR(6) || '|' AS d;
SELECT NULL + 5 AS e;Ejercicio 3
Escribe una consulta sobre information_schema que muestre todas las columnas de tipo numeric de la base de datos tiendaverde, indicando en qué tabla están y con qué precisión y escala. Ordénalas por tabla y por posición dentro de la tabla.
Soluciones
Solución 1
| Dato | Tipo | Justificación |
|---|---|---|
| 1. Precio de venta | NUMERIC(10,2) |
Es dinero: exige aritmética decimal exacta |
| 2. Código de país ISO | CHAR(2) o VARCHAR(2) |
Longitud fija conocida. Es la única situación donde CHAR se defiende; VARCHAR(2) evita el relleno con espacios. (TiendaVerde guarda el nombre completo del país, así que usa VARCHAR(60)) |
| 3. Comentario de reseña | TEXT |
Longitud impredecible; sin límite de negocio |
| 4. Puntuación 1-5 | SMALLINT (o INTEGER) |
Entero muy pequeño. El rango 1-5 se garantiza con una restricción CHECK (módulo 5), no con el tipo |
| 5. Fecha de registro | DATE |
Fecha de calendario, sin hora relevante |
| 6. Producto activo | BOOLEAN |
Dos estados más el desconocido |
| 7. Instante del pago | TIMESTAMPTZ |
Es un instante real y hay varias zonas horarias implicadas |
| 8. Descuento | NUMERIC(4,2) |
Valor exacto entre 0,00 y 1,00; interviene en cálculos de importe |
Solución 2
| Expresión | Resultado | Qué demuestra |
|---|---|---|
1.0 / 3.0 |
0.33333333333333333333 |
Los literales decimales son numeric: PostgreSQL conserva muchos dígitos exactos |
(1.0/3.0)::REAL |
0.33333334 |
REAL solo guarda unos 6-7 dígitos significativos: hay pérdida de información |
100000000.0::REAL + 1 |
100000000 |
El +1 desaparece: REAL no tiene precisión suficiente para distinguir 100 000 000 de 100 000 001. Es el argumento definitivo contra usar coma flotante para dinero |
'abc'::CHAR(6) || '|' |
abc| |
Aunque CHAR(6) rellena a 6 caracteres, la concatenación elimina los espacios finales. Comportamiento inconsistente que justifica evitar CHAR |
NULL + 5 |
(null) |
NULL se propaga: cualquier operación aritmética con un nulo da nulo |
Solución 3
SELECT table_name,
column_name,
numeric_precision,
numeric_scale,
is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
AND data_type = 'numeric'
ORDER BY table_name, ordinal_position;Resultado esperado sobre TiendaVerde:
| table_name | column_name | numeric_precision | numeric_scale | is_nullable |
|---|---|---|---|---|
| devoluciones | importe | 10 | 2 | YES |
| empleados | salario | 10 | 2 | YES |
| lineas_pedido | precio_unitario | 10 | 2 | NO |
| lineas_pedido | descuento | 4 | 2 | NO |
| pedidos | gastos_envio | 10 | 2 | NO |
| productos | precio | 10 | 2 | NO |
| productos | coste | 10 | 2 | YES |
Observa que data_type devuelve 'numeric' en minúsculas, y que la precisión y la escala vienen en columnas separadas: information_schema normaliza los tipos al vocabulario del estándar SQL, no al nombre que escribiste al crear la tabla.
Conclusión
En esta lección has recorrido la estructura sobre la que actúa SQL:
- La jerarquía servidor → base de datos → esquema (
public) → tabla → columnas y filas, y por qué no se pueden consultar dos bases de datos a la vez en PostgreSQL. - Una tabla es un conjunto de filas con la misma estructura, sin orden intrínseco, con un tipo por columna verificado por el motor: ahí está la gran diferencia con una hoja de cálculo.
- El catálogo de tipos de PostgreSQL: numéricos (
INTEGER,BIGINT,NUMERIC(p,s),REAL), texto (TEXT,VARCHAR(n),CHAR(n)), fecha y hora (DATE,TIMESTAMP,TIMESTAMPTZ,INTERVAL),BOOLEAN,UUIDyJSONB. - La regla innegociable: el dinero va en
NUMERIC, nunca en coma flotante, porque0.1 + 0.2 <> 0.3en binario. NULLcomo ausencia de valor, distinta de0y de'', que se propaga en toda operación.- Las equivalencias de tipos entre PostgreSQL, MySQL y SQLite, y el peligro del sistema de tipos dinámico de SQLite.
- Cómo inspeccionar una base ajena con
\dt,\d tablaeinformation_schema.columns.
En la siguiente lección, El modelo relacional: claves primarias y foráneas, veremos cómo las tablas dejan de estar aisladas y se relacionan entre sí: qué es una clave primaria y por qué usamos id subrogados, cómo las claves foráneas garantizan que no existan pedidos de clientes inexistentes, qué pasa al borrar un registro del que dependen otros, cómo se representan las cardinalidades 1:1, 1:N y N:M —con lineas_pedido como ejemplo real de tabla puente— y cómo la normalización explica por qué el esquema de TiendaVerde está partido en nueve tablas y no en una sola.
Curso de SQL
Módulo 1: Introducción a SQL
- ¿Qué es SQL?
- Configurando tu entorno SQL
- Sintaxis básica de SQL
- Entendiendo bases de datos y tablas
- El modelo relacional: claves primarias y foráneas
- La base de datos del curso: TiendaVerde
Módulo 2: Consultas básicas de SQL
- Instrucción SELECT
- Alias, expresiones y columnas calculadas
- Filtrando datos con WHERE
- DISTINCT y eliminación de duplicados
- Ordenando datos con ORDER BY
- Limitando resultados con LIMIT
Módulo 3: Trabajando con múltiples tablas
- Operaciones JOIN
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN y CROSS JOIN
- Uniones de conjuntos: UNION, INTERSECT y EXCEPT
Módulo 4: Filtrado avanzado de datos
- Usando LIKE para coincidencia de patrones
- Operadores IN y BETWEEN
- Valores NULL y IS NULL
- Funciones de agregación: COUNT, SUM, AVG, MIN y MAX
- Agregando datos con GROUP BY
- Cláusula HAVING
Módulo 5: Manipulación de datos
- Creando tablas y restricciones con CREATE TABLE
- Instrucción INSERT
- Instrucción UPDATE
- Instrucción DELETE
- Instrucción UPSERT (MERGE)
- Modificando el esquema: ALTER TABLE y migraciones seguras
Módulo 6: Funciones avanzadas de SQL
- Funciones de cadena
- Funciones numéricas
- Funciones de fecha y hora
- Conversión de tipos y manejo de NULL: CAST y COALESCE
- Expresiones condicionales
Módulo 7: Subconsultas y consultas anidadas
- Introducción a subconsultas
- Subconsultas correlacionadas
- EXISTS y NOT EXISTS
- Usando subconsultas en cláusulas SELECT, FROM y WHERE
- Subconsultas o JOIN: cuál elegir
Módulo 8: Índices y optimización de rendimiento
- Entendiendo los índices
- Creación y gestión de índices
- Tipos de índice y cuándo no indexar
- Técnicas de optimización de consultas
- Análisis del rendimiento de consultas
Módulo 9: Transacciones y concurrencia
- Introducción a las transacciones
- Propiedades ACID
- Instrucciones de control de transacciones
- Niveles de aislamiento y anomalías de concurrencia
- Manejo de concurrencia: bloqueos e interbloqueos
Módulo 10: Temas avanzados
- Vistas
- Expresiones de tabla comunes (CTE)
- Funciones de ventana
- Procedimientos almacenados
- Triggers
- JSON y datos semiestructurados
Módulo 11: SQL en la práctica
- Casos de uso en el mundo real
- Mejores prácticas
- Seguridad: inyección SQL, permisos y roles
- SQL para análisis de datos
- SQL en desarrollo web
