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

  1. La jerarquía: servidor, base de datos, esquema, tabla
  2. Tablas, filas y columnas
  3. La analogía de la hoja de cálculo (y dónde se rompe)
  4. Tipos de datos numéricos
  5. Por qué el dinero nunca va en coma flotante
  6. Tipos de texto
  7. Tipos de fecha y hora
  8. Booleanos, UUID y JSONB
  9. NULL: la ausencia de valor
  10. Equivalencias entre PostgreSQL, MySQL y SQLite
  11. Cómo inspeccionar tablas existentes
  12. Errores Comunes y Consejos
  13. Ejercicios
  14. Conclusión

  1. 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:

  1. No puedes consultar dos bases de datos a la vez. Si tiendaverde y pruebas_sql viven en el mismo servidor, una consulta no puede combinarlas directamente (harían falta extensiones como dblink o postgres_fdw). En MySQL, en cambio, sí es habitual escribir SELECT ... FROM otra_base.tabla, porque allí "base de datos" y "esquema" son casi sinónimos.
  2. Los esquemas sí se combinan libremente. Dentro de tiendaverde podrías tener un esquema ventas y otro analitica y 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:

SELECT nombre FROM productos;
SELECT nombre FROM public.productos;

El orden de búsqueda lo determina el parámetro search_path:

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

  1. 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:

  1. Todas las filas tienen las mismas columnas. No existe una fila "con un campo extra". Si un dato no aplica, se guarda NULL.
  2. Cada columna tiene un único tipo. No puedes guardar 'mañana' en una columna DATE.
  3. Las filas no tienen orden intrínseco. Una tabla es un conjunto. Si no pides ORDER BY explí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".

  1. 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:

INSERT INTO productos (nombre, precio) VALUES ('Producto raro', 'doce euros');
ERROR:  invalid input syntax for type numeric: "doce euros"

Ese error, que puede parecer una molestia, es exactamente el valor que aporta una base de datos: los datos incorrectos no llegan a entrar.

  1. 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 un INTEGER más una secuencia que lo autoincrementa. Desde PostgreSQL 10 la forma recomendada por el estándar es GENERATED BY DEFAULT AS IDENTITY, que es la que usa el script del curso. Lo verás con detalle en el módulo 5.

  1. 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:

SELECT 0.1::REAL + 0.2::REAL AS suma_flotante,
       0.1::NUMERIC + 0.2::NUMERIC AS suma_exacta;
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. Nunca FLOAT, REAL ni DOUBLE PRECISION. La penalización de velocidad es irrelevante comparada con un céntimo perdido.

Existe además el tipo MONEY en PostgreSQL, pero no se recomienda: depende de la configuración regional del servidor y no admite bien varias divisas. Usa NUMERIC.

  1. 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:

SELECT 'ES'::CHAR(5) = 'ES' AS parecen_iguales,
       LENGTH('ES'::CHAR(5)) AS longitud_almacenada;
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, VARCHAR y TEXT (los TEXT se almacenan fuera de la fila y no admiten valor por defecto). Lo que aquí es indiferente, allí no lo es.

  1. 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:

  • TIMESTAMP guarda literalmente lo que le das. Si un cliente francés y otro español registran "18:30", se guardan igual aunque sean momentos distintos.
  • TIMESTAMPTZ convierte 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), usa DATE. TiendaVerde usa DATE en todas sus fechas porque son fechas de calendario, no instantes.

Dialecto: MySQL tiene DATETIME (sin zona) y TIMESTAMP (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.

  1. Booleanos, UUID y JSONB

BOOLEAN

Guarda TRUE, FALSE o NULL. Ocupa 1 byte. En TiendaVerde lo usan productos.activo y proveedores.activo.

SELECT TRUE AS verdadero, FALSE AS falso, NULL::BOOLEAN AS desconocido;

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

SELECT '{"origen": "Valencia", "certificado": "EU-BIO"}'::JSONB -> 'origen' AS origen;
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.

  1. 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:

SELECT 10 + NULL AS suma,
       'Hola ' || NULL AS concatenacion,
       NULL = NULL AS comparacion;
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 JOIN y a los filtros— es la lección 04-03. Por ahora basta el concepto.

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

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

tiendaverde=# \dt
        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
tiendaverde=# \d productos
                            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_schema es el estándar, pero PostgreSQL tiene además su catálogo propio pg_catalog (pg_tables, pg_class, pg_attribute), más completo y más rápido, aunque no portable. Los metacomandos \d consultan precisamente ese catálogo.

Errores Comunes y Consejos

  • Guardar dinero en FLOAT o REAL. El error más caro de esta lección. Siempre NUMERIC(10,2).
  • Guardar fechas como texto. VARCHAR para una fecha impide ordenar bien, calcular diferencias y validar. Usa DATE o TIMESTAMPTZ.
  • 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 como INTEGER perderás el cero inicial y el +34.
  • Poner VARCHAR(255) por costumbre. El 255 viene de MySQL antiguo. En PostgreSQL usa TEXT salvo 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 NULL con 0 o ''. Son tres cosas distintas y se comportan de forma distinta en filtros y agregaciones.
  • Confiar en el orden de las filas. Sin ORDER BY no 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 tabla es 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.nombre se lee mejor que producto.nombres.

Ejercicios

Ejercicio 1

Para cada dato de TiendaVerde, elige el tipo PostgreSQL más adecuado y justifícalo en una línea:

  1. El precio de venta de un producto.
  2. El código de país de un proveedor en formato ISO (ES, PT, FR).
  3. El comentario de una reseña.
  4. La puntuación de una reseña (1 a 5).
  5. La fecha en que se registró un cliente.
  6. Si un producto está activo o no.
  7. El instante exacto en que se confirmó un pago, con clientes en tres países.
  8. 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, UUID y JSONB.
  • La regla innegociable: el dinero va en NUMERIC, nunca en coma flotante, porque 0.1 + 0.2 <> 0.3 en binario.
  • NULL como ausencia de valor, distinta de 0 y 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 tabla e information_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

Módulo 2: Consultas básicas de SQL

Módulo 3: Trabajando con múltiples tablas

Módulo 4: Filtrado avanzado de datos

Módulo 5: Manipulación de datos

Módulo 6: Funciones avanzadas de SQL

Módulo 7: Subconsultas y consultas anidadas

Módulo 8: Índices y optimización de rendimiento

Módulo 9: Transacciones y concurrencia

Módulo 10: Temas avanzados

Módulo 11: SQL en la práctica

Módulo 12: Proyecto final

© Copyright 2026. Todos los derechos reservados