En esta página

SELECT, WHERE y ORDER BY

14 min lectura TextoCap. 2 — Consultas básicas

SELECT, WHERE y ORDER BY

La instrucción SELECT es el corazón de SQL. Es la que usarás con mayor frecuencia en cualquier aplicación: para mostrar listados, buscar registros, generar reportes o alimentar componentes de tu interfaz. En esta lección dominarás todas las variantes del filtrado y ordenamiento de datos.

Preparar los datos de práctica

Antes de consultar, necesitamos datos. Vamos a crear un esquema simple de tienda para esta lección:

CREATE TABLE categorias (
  id     SERIAL PRIMARY KEY,
  nombre VARCHAR(100) NOT NULL UNIQUE
);

CREATE TABLE productos (
  id           SERIAL        PRIMARY KEY,
  nombre       VARCHAR(200)  NOT NULL,
  descripcion  TEXT,
  precio       NUMERIC(10,2) NOT NULL CHECK (precio >= 0),
  stock        INTEGER       NOT NULL DEFAULT 0 CHECK (stock >= 0),
  categoria_id INTEGER       REFERENCES categorias(id),
  activo       BOOLEAN       NOT NULL DEFAULT true,
  creado_en    TIMESTAMPTZ   NOT NULL DEFAULT NOW()
);

INSERT INTO categorias (nombre) VALUES
  ('Electrónica'), ('Ropa'), ('Libros'), ('Hogar'), ('Deportes');

INSERT INTO productos (nombre, descripcion, precio, stock, categoria_id) VALUES
  ('Laptop Pro 15"',    'Laptop con chip M3',          1299.99,  5,  1),
  ('Auriculares BT',   'Auriculares Bluetooth ANC',    199.99,  15,  1),
  ('Teclado Mecánico', 'Switch Cherry MX Brown',        89.99,  30,  1),
  ('Camiseta Algodón', 'Algodón 100%, varios colores',  19.99, 200,  2),
  ('Jeans Slim',       'Denim premium lavado oscuro',   59.99,  80,  2),
  ('Clean Code',       'Robert C. Martin',              35.99,  50,  3),
  ('The Pragmatic Programmer', 'Hunt & Thomas 20th Ed', 42.99,  40,  3),
  ('Sartén Antiadherente',     'Titanio 28cm',          34.99,  25,  4),
  ('Pelota de Fútbol',         'Tamaño 5, cuero sintético', 24.99, 60, 5),
  ('Zapatillas Running',       'Amortiguación reactiva', 119.99, 35,  5),
  ('Mouse Inalámbrico',        'Ergonómico, 1600 DPI',   49.99,  45,  1),
  ('Sudadera Polar',           'Interior afelpado, unisex', 45.99, 90, 2);

La instrucción SELECT básica

-- Seleccionar todas las columnas (evitar en producción)
SELECT * FROM productos;

-- Seleccionar columnas específicas (la práctica recomendada)
SELECT nombre, precio, stock FROM productos;

-- El orden de las columnas en SELECT define el orden de salida
SELECT stock, nombre, precio FROM productos;

[!tip] Evita SELECT * en código de producción. Cuando añades nuevas columnas a una tabla, SELECT * las incluye automáticamente, lo que puede romper el código que asume un número fijo de columnas. También transfiere datos innecesarios por la red. Especifica siempre las columnas que necesitas.

Aliases con AS

Los aliases renombran columnas o tablas en los resultados, sin modificar la base de datos:

-- Alias de columna
SELECT
  nombre AS producto,
  precio AS precio_venta,
  stock  AS unidades_disponibles
FROM productos;

-- Alias sin AS (también válido, pero menos legible)
SELECT nombre producto, precio precio_venta FROM productos;

-- Alias de tabla (útil en JOINs y subconsultas)
SELECT p.nombre, p.precio
FROM productos p
WHERE p.stock > 0;

-- Expresiones calculadas con alias
SELECT
  nombre,
  precio,
  precio * 1.21 AS precio_con_iva,
  stock * precio AS valor_inventario
FROM productos;

Filtrar con WHERE

La cláusula WHERE filtra las filas que cumplen una condición. Solo las filas donde la condición evalúa a TRUE aparecen en el resultado.

-- Comparaciones básicas: =, <>, !=, <, >, <=, >=
SELECT nombre, precio FROM productos WHERE precio > 100;
SELECT nombre, stock  FROM productos WHERE stock = 0;
SELECT nombre         FROM productos WHERE categoria_id <> 1;

-- NULL requiere IS NULL o IS NOT NULL (nunca =)
SELECT nombre FROM productos WHERE descripcion IS NULL;
SELECT nombre FROM productos WHERE descripcion IS NOT NULL;

AND, OR y NOT

-- AND: ambas condiciones deben ser verdaderas
SELECT nombre, precio, stock
FROM productos
WHERE precio < 50 AND stock > 20;

-- OR: al menos una condición debe ser verdadera
SELECT nombre, precio
FROM productos
WHERE categoria_id = 1 OR categoria_id = 3;

-- NOT: niega la condición
SELECT nombre, activo
FROM productos
WHERE NOT activo;

-- Combinar con paréntesis para controlar precedencia
-- AND tiene mayor precedencia que OR
SELECT nombre, precio, categoria_id
FROM productos
WHERE (categoria_id = 1 OR categoria_id = 5)
  AND precio < 200;

IN y NOT IN

-- IN: equivale a múltiples OR, pero más limpio
SELECT nombre, categoria_id
FROM productos
WHERE categoria_id IN (1, 3, 5);

-- NOT IN: excluir valores
SELECT nombre
FROM productos
WHERE categoria_id NOT IN (2, 4);

-- IN con lista de textos
SELECT nombre
FROM productos
WHERE nombre IN ('Clean Code', 'Mouse Inalámbrico', 'Laptop Pro 15"');

BETWEEN

-- BETWEEN: rango INCLUSIVO (incluye ambos extremos)
SELECT nombre, precio
FROM productos
WHERE precio BETWEEN 20 AND 60;

-- Equivalente a:
SELECT nombre, precio
FROM productos
WHERE precio >= 20 AND precio <= 60;

-- BETWEEN con fechas
SELECT nombre, creado_en
FROM productos
WHERE creado_en BETWEEN '2024-01-01' AND '2024-12-31';

-- NOT BETWEEN
SELECT nombre, precio
FROM productos
WHERE precio NOT BETWEEN 100 AND 1000;

LIKE e ILIKE

-- LIKE: búsqueda de patrones (sensible a mayúsculas)
-- % = cualquier secuencia de caracteres (incluido ninguno)
-- _ = exactamente un carácter

SELECT nombre FROM productos WHERE nombre LIKE 'L%';         -- Empieza con L
SELECT nombre FROM productos WHERE nombre LIKE '%ón%';       -- Contiene 'ón'
SELECT nombre FROM productos WHERE nombre LIKE '__________'; -- Exactamente 10 chars
SELECT nombre FROM productos WHERE nombre LIKE '%ing';       -- Termina en 'ing'

-- ILIKE: búsqueda insensible a mayúsculas/minúsculas (extensión PostgreSQL)
SELECT nombre FROM productos WHERE nombre ILIKE 'laptop%';   -- 'Laptop', 'LAPTOP', etc.
SELECT nombre FROM productos WHERE nombre ILIKE '%pro%';     -- Busca 'Pro', 'PRO', 'pro'

-- Para búsqueda de texto completo, considera usar índices de texto
-- (lo veremos en la lección de índices)

ORDER BY

Sin ORDER BY, el orden de los resultados es indeterminado. Siempre que el orden importe, especifícalo:

-- Orden ascendente (ASC es el predeterminado)
SELECT nombre, precio FROM productos ORDER BY precio ASC;
SELECT nombre, precio FROM productos ORDER BY precio;        -- Mismo resultado

-- Orden descendente
SELECT nombre, precio FROM productos ORDER BY precio DESC;

-- Múltiples columnas de ordenamiento
SELECT nombre, categoria_id, precio
FROM productos
ORDER BY categoria_id ASC, precio DESC;
-- Primero ordena por categoría, luego por precio dentro de cada categoría

-- Ordenar por alias
SELECT nombre, precio * 1.21 AS precio_iva
FROM productos
ORDER BY precio_iva DESC;

-- Ordenar por posición de columna (no recomendado: frágil si cambias columnas)
SELECT nombre, precio FROM productos ORDER BY 2 DESC;

-- NULL al final o al principio
SELECT nombre, descripcion
FROM productos
ORDER BY descripcion NULLS LAST;   -- NULLs al final
ORDER BY descripcion NULLS FIRST;  -- NULLs al principio

LIMIT y OFFSET — Paginación

-- LIMIT: máximo de filas a devolver
SELECT nombre, precio FROM productos
ORDER BY precio DESC
LIMIT 5;   -- Los 5 más caros

-- OFFSET: saltar N filas (para paginación)
-- Página 1: las primeras 4 filas
SELECT nombre, precio FROM productos
ORDER BY id
LIMIT 4 OFFSET 0;

-- Página 2: las siguientes 4 filas
SELECT nombre, precio FROM productos
ORDER BY id
LIMIT 4 OFFSET 4;

-- Página 3
SELECT nombre, precio FROM productos
ORDER BY id
LIMIT 4 OFFSET 8;

-- Fórmula: OFFSET = (número_página - 1) * tamaño_página

[!warning] El problema de rendimiento con OFFSET grande: cuando tienes millones de filas, OFFSET 1000000 hace que PostgreSQL recorra y descarte un millón de filas antes de devolver los resultados. Para paginación eficiente en grandes tablas, usa keyset pagination (también llamada cursor pagination): guarda el último ID devuelto y úsalo en el siguiente WHERE id > ultimo_id ORDER BY id LIMIT n. Esto es siempre O(log n) gracias a los índices.

DISTINCT

-- Eliminar filas duplicadas en el resultado
SELECT DISTINCT categoria_id FROM productos ORDER BY categoria_id;

-- DISTINCT en múltiples columnas (combinación única)
SELECT DISTINCT categoria_id, activo FROM productos ORDER BY categoria_id;

-- COUNT con DISTINCT
SELECT COUNT(DISTINCT categoria_id) AS total_categorias FROM productos;

Expresiones en SELECT

Puedes realizar cálculos y transformaciones directamente en el SELECT:

SELECT
  nombre,
  precio,
  precio * 1.21                           AS precio_con_iva,
  ROUND(precio * 1.21, 2)                 AS precio_iva_redondeado,
  stock * precio                          AS valor_total_inventario,
  UPPER(nombre)                           AS nombre_mayusculas,
  LENGTH(nombre)                          AS largo_nombre,
  CURRENT_DATE                            AS fecha_consulta,
  CASE
    WHEN precio < 30 THEN 'económico'
    WHEN precio < 100 THEN 'rango medio'
    ELSE 'premium'
  END                                     AS categoria_precio
FROM productos
WHERE activo = true
ORDER BY precio DESC;

Ejemplo completo: buscador de productos

Aquí un ejemplo realista de consulta que combina todo lo aprendido:

-- Búsqueda de productos con filtros múltiples y paginación
-- (Como la que haría una API de e-commerce)

SELECT
  p.id,
  p.nombre,
  p.precio,
  p.stock,
  CASE WHEN p.stock > 0 THEN 'disponible' ELSE 'agotado' END AS estado_stock
FROM productos p
WHERE
  p.activo = true
  AND p.categoria_id IN (1, 5)
  AND p.precio BETWEEN 20 AND 200
  AND p.nombre ILIKE '%a%'
ORDER BY
  p.precio ASC,
  p.nombre ASC
LIMIT 10
OFFSET 0;

[!info] El orden de evaluación en SQL NO es el orden de escritura. El motor procesa las cláusulas en este orden lógico: FROMWHEREGROUP BYHAVINGSELECTORDER BYLIMIT/OFFSET. Por eso no puedes usar un alias definido en SELECT dentro de un WHERE (el alias aún no existe cuando se evalúa WHERE). Sin embargo, ORDER BY sí puede usar aliases de SELECT.

Resumen de operadores de filtrado

Operador Uso Ejemplo
= Igualdad exacta precio = 99.99
<>, != Desigualdad estado <> 'activo'
>, <, >=, <= Comparación numérica/fecha precio >= 100
BETWEEN x AND y Rango inclusivo precio BETWEEN 10 AND 50
IN (...) Valor en lista id IN (1, 2, 3)
LIKE Patrón (case-sensitive) nombre LIKE 'A%'
ILIKE Patrón (case-insensitive) nombre ILIKE '%pro%'
IS NULL Valor nulo descripcion IS NULL
IS NOT NULL Valor no nulo email IS NOT NULL
AND, OR, NOT Lógica booleana a = 1 AND b > 2

Dominar estas herramientas te permite responder prácticamente cualquier pregunta sobre los datos almacenados. En la siguiente lección aprenderemos a modificar datos: INSERT, UPDATE, DELETE y las poderosas cláusulas RETURNING y ON CONFLICT.


nextSteps:

  • slug: insert-update-y-delete label: "Siguiente: INSERT, UPDATE y DELETE"