PostgreSQL
SELECT y consultas básicas
Fundamentos de SELECT: columnas, filtros, expresiones, ordenamiento, límites y evaluación lógica para construir consultas correctas y legibles.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
PostgreSQL
Fundamentos de SELECT: columnas, filtros, expresiones, ordenamiento, límites y evaluación lógica para construir consultas correctas y legibles.
Un SELECT no recupera “objetos”; construye una nueva relación a partir de fuentes, filtros, expresiones y un orden explícito. Sin ORDER BY, PostgreSQL no promete en qué secuencia llegarán las filas.
Una consulta de lectura define cuatro cosas principales:
qué columnas o expresiones producir
→ SELECT
de dónde obtener filas
→ FROM / JOIN
qué filas conservar
→ WHERE
cómo ordenar y limitar el resultado
→ ORDER BY / LIMITEjemplo:
SELECT
id,
name,
price,
price * 1.19 AS price_with_tax
FROM products
WHERE business_id = $1
AND deleted_at IS NULL
ORDER BY name ASC, id ASC
LIMIT 20;El resultado tiene columnas propias y puede utilizarse como input de otra consulta. No conserva una identidad de objeto ni un orden físico implícito.
Las consultas básicas parecen sencillas, pero errores pequeños producen contratos inestables:
LIMIT sin orden devuelve subconjuntos no deterministas.SELECT * acopla la aplicación al esquema completo.Una consulta correcta debe definir semántica, no solo devolver “algo parecido” en los datos de desarrollo.
FROM
→ WHERE
→ GROUP BY
→ HAVING
→ SELECT
→ DISTINCT
→ ORDER BY
→ LIMIT / OFFSETEste orden explica por qué un alias creado en SELECT no suele estar disponible en WHERE:
SELECT price * quantity AS subtotal
FROM order_items
WHERE subtotal > 100000; -- no disponible aquíPuede repetirse la expresión o crear una subquery:
SELECT *
FROM (
SELECT id, price * quantity AS subtotal
FROM order_items
) AS items
WHERE subtotal > 100000;SELECT id, sku, name
FROM products;Seleccionar columnas explícitas:
SELECT * sigue siendo útil en exploración, debugging controlado y algunas subqueries EXISTS, donde la lista de selección no se materializa de la misma manera.
SELECT
quantity,
unit_price,
quantity * unit_price AS subtotal,
round(quantity * unit_price * 0.19, 2) AS tax
FROM order_items;Las expresiones se evalúan con los tipos y operadores de PostgreSQL. Revisa:
El alias forma parte del nombre de salida, no cambia el esquema almacenado.
WHERE conserva solo condiciones TRUE.
SELECT id, status
FROM orders
WHERE status IN ('pending', 'confirmed')
AND total_amount >= $1;Parámetros externos deben enviarse mediante el driver. No concatenes strings.
= <> < <= > >=
IS NULL
IS DISTINCT FROM
IN
BETWEEN
LIKE!= suele aceptarse como equivalente a <>, pero <> es la forma SQL estándar.
Para NULL:
WHERE delivered_at IS NULLNo:
WHERE delivered_at = NULLPatrón recomendado:
WHERE created_at >= $1
AND created_at < $2Para el día 24 de julio:
inicio: 2026-07-24 00:00 en zona definida
fin: 2026-07-25 00:00 en la misma zonaEl intervalo [inicio, fin):
23:59:59.999999.La aplicación debe convertir correctamente la fecha local a instantes si la columna es timestamptz.
price BETWEEN 100 AND 500Incluye ambos límites. Es apropiado para rangos cerrados; para timestamps consecutivos suele preferirse inicio inclusivo y final exclusivo.
WHERE status IN ('pending', 'confirmed')Con parámetros arrays puede utilizarse:
WHERE status = ANY($1::text[])Una lista vacía necesita semántica definida. IN () no es sintaxis válida; el backend puede omitir el filtro o devolver conjunto vacío según el contrato.
SELECT
id,
CASE
WHEN stock = 0 THEN 'out_of_stock'
WHEN stock < minimum_stock THEN 'low_stock'
ELSE 'available'
END AS availability
FROM inventory;Las ramas se evalúan en orden lógico. CASE devuelve un único tipo compatible; casts inesperados pueden aparecer si las ramas tienen tipos distintos.
No conviertas un state machine complejo en una expresión gigante de presentación.
COALESCE(discount_amount, 0)Es válido si NULL significa “sin descuento”.
revenue / NULLIF(order_count, 0)Evita división por cero devolviendo NULL.
Ambas funciones cambian la semántica del resultado; no deben ocultar datos incompletos.
SELECT DISTINCT customer_id
FROM orders;Elimina duplicados del resultado, normalmente mediante sort o hash.
Es correcto cuando la pregunta requiere valores únicos. Es sospechoso cuando aparece después de un join inesperadamente multiplicado.
Antes de añadirlo, pregunta:
¿Los duplicados representan varias filas reales?
¿La condición del join es incompleta?
¿La consulta debería agregar?Extensión PostgreSQL:
SELECT DISTINCT ON (customer_id)
customer_id,
id,
created_at
FROM orders
ORDER BY customer_id, created_at DESC, id DESC;Devuelve la primera fila de cada customer según ORDER BY.
Sin orden coherente, la fila elegida no es determinista. La expresión de DISTINCT ON debe alinearse con el inicio del ORDER BY.
Alternativas:
row_number() window function.WHERE sku LIKE 'ABC-%'% representa cualquier secuencia y _ un carácter.
Variante case-insensitive específica de PostgreSQL:
WHERE name ILIKE '%' || $1 || '%'WHERE name ~* $1Regex y búsquedas con wildcard inicial pueden ser costosas. Para producción:
pg_trgm y GIN/GiST.No construyas el patrón concatenando SQL; concatena valores dentro de la expresión parametrizada.
Si el usuario busca literalmente % o _, deben escaparse con un carácter definido:
WHERE name LIKE $1 ESCAPE '\'El backend transforma el input según ese contrato. No confundas escaping de LIKE con parametrización SQL; resuelven problemas diferentes.
ORDER BY created_at DESC, id DESCUn orden total incluye un desempate único. Esto es esencial para paginación.
Puede ordenar por:
ORDER BY 1 es menos claro y frágil.NULL:
ORDER BY delivered_at DESC NULLS LASTORDER BY created_at DESC, id DESC
LIMIT 20 OFFSET 1000;OFFSET obliga a PostgreSQL a localizar y descartar filas anteriores. En páginas profundas:
Es aceptable para:
SELECT id, created_at, total_amount
FROM orders
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 20;El cursor contiene la última pareja vista.
Ventajas:
(created_at DESC, id DESC).Costes:
Un parámetro no puede reemplazar un identifier:
ORDER BY $1 -- ordena por un valor constante, no por una columna elegidaUsa whitelist en aplicación:
const sortColumns = {
createdAt: 'created_at',
total: 'total_amount',
} as const;La dirección también debe limitarse a ASC o DESC conocidos.
LIMIT $1Puede parametrizarse como valor. Impón máximo:
1 <= limit <= 100Sin máximo, una consulta válida puede agotar memoria, red y pool.
PostgreSQL puede utilizar planes custom o generic para prepared statements. Una misma consulta con parámetros muy selectivos o poco selectivos puede beneficiarse de planes diferentes.
No intentes resolverlo desde el principio. Diagnostica con EXPLAIN, estadísticas y comportamiento del driver cuando exista evidencia de parameter-sensitive planning.
SELECT
p.id,
p.sku,
p.name,
p.price,
i.quantity
FROM products AS p
JOIN inventory AS i
ON i.product_id = p.id
AND i.branch_id = $1
WHERE p.business_id = $2
AND p.deleted_at IS NULL
AND ($3::text IS NULL OR p.name ILIKE '%' || $3 || '%')
AND ($4::numeric IS NULL OR p.price >= $4)
ORDER BY p.name, p.id
LIMIT $5;Flujo:
Trade-off: patrones como ($3 IS NULL OR ...) pueden dificultar planes e índices. Para endpoints con muchas combinaciones, SQL dinámico seguro construido desde fragmentos whitelisted puede producir consultas más sargables.
Devuelve cero filas; no es error.
Devuelve cero filas. Decide si la API lo acepta o valida mínimo uno.
Sin desempate único, el orden entre empates puede cambiar.
El orden textual depende de collation. No asumas que coincide con orden de negocio o búsqueda case-insensitive.
Valores numéricos especiales tienen reglas de comparación particulares. Si el dominio no los permite, añade constraints.
Una consulta ve un snapshot según aislamiento. Dos páginas ejecutadas en transacciones separadas pueden observar datos diferentes.
Aumenta acoplamiento y puede exponer columnas nuevas.
No define qué filas son “las primeras”.
Paginación inestable.
Pierde valores con mayor precisión y complica zonas.
Puede esconder un join incorrecto.
ILIKE '%text%' puede escanear gran parte de la tabla.
Un solo SQL con muchos OR puede producir planes pobres; evalúa query building seguro.
% y _.EXPLAIN (ANALYZE, BUFFERS) con datos representativos.[inicio, fin).ORDER BY created_at puede ser insuficiente?ORDER BY $1 no selecciona una columna dinámica?Joins y cardinalidad del resultado explica cómo combinar relaciones, predecir multiplicación de filas y elegir correctamente INNER, LEFT, semi y anti-joins.