Cualquier sistema de reservas, calendarios u horarios se enfrenta a la misma necesidad: garantizar que dos rangos no se solapen para el mismo recurso. Resolverlo a nivel de aplicación con SELECT previo y luego INSERT abre la puerta a race conditions. PostgreSQL ofrece range types y exclusion constraints que delegan esta lógica al motor con garantías ACID.
Tipos de rango disponibles
PostgreSQL incluye varios tipos de rango predefinidos sobre los tipos numéricos y de fecha:
| Tipo | Base | Uso típico |
|------|------|------------|
| int4range | INT | IDs, cantidades enteras |
| int8range | BIGINT | IDs grandes |
| numrange | NUMERIC | Precios, escalas |
| tsrange | TIMESTAMP sin tz | Eventos sin zona |
| tstzrange | TIMESTAMPTZ | Reservas reales con zona |
| daterange | DATE | Periodos por días |
Y permite definir tipos de rango personalizados sobre cualquier tipo ordenable con CREATE TYPE ... AS RANGE.
Sintaxis y bordes
Un literal de rango se escribe entre comillas con bordes inclusivos [ ] o exclusivos ( ):
SELECT
'[1, 10]'::int4range AS cerrado,
'(1, 10)'::int4range AS abierto,
'[1, 10)'::int4range AS semi_abierto,
'empty'::int4range AS vacio;
La convención
[inicio, fin)(inicio inclusivo, fin exclusivo) es la más usada para rangos temporales: facilita la concatenación de periodos sin solapes ni huecos.
PostgreSQL normaliza automáticamente los rangos discretos. '[1, 10]'::int4range se almacena internamente como '[1, 11)', porque int4range es discreto y conoce la "siguiente" función.
Caso práctico: reservas de salas sin solapes
Imagina un sistema de reservas de salas de reuniones. La tabla básica:
CREATE TABLE reservas (
id BIGSERIAL PRIMARY KEY,
sala_id INT NOT NULL,
periodo TSTZRANGE NOT NULL,
organizador TEXT NOT NULL,
creado_en TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Para garantizar que dos reservas no se solapen para la misma sala, se añade un exclusion constraint con índice GiST:
ALTER TABLE reservas
ADD CONSTRAINT no_solapes_por_sala
EXCLUDE USING gist (
sala_id WITH =,
periodo WITH &&
);
La sintaxis se lee así: "para dos filas distintas, el constraint se viola si sala_id es igual y los periodo se solapan (&&)". PostgreSQL crea un índice GiST que permite la verificación rápida en tiempo de inserción.
-- Esto funciona
INSERT INTO reservas (sala_id, periodo, organizador) VALUES
(1, '[2026-04-15 10:00, 2026-04-15 11:00)', 'alice'),
(1, '[2026-04-15 11:00, 2026-04-15 12:00)', 'bob');
-- Esto falla con conflicting key value
INSERT INTO reservas (sala_id, periodo, organizador) VALUES
(1, '[2026-04-15 10:30, 2026-04-15 11:30)', 'carol');
-- ERROR: conflicting key value violates exclusion constraint "no_solapes_por_sala"
Esta restricción es transaccional y a prueba de race conditions. Dos sesiones simultáneas que intenten reservar el mismo slot solo verán una triunfar; la otra recibe el error y debe reintentar.
Operadores de rango
Los range types soportan un conjunto rico de operadores:
| Operador | Significado | Ejemplo |
|----------|-------------|---------|
| @> | contiene | range @> valor o range @> range |
| <@ | contenido por | valor <@ range |
| && | solapa | r1 && r2 |
| = | igualdad | r1 = r2 |
| + | unión | r1 + r2 (devuelve rango si son contiguos) |
| * | intersección | r1 * r2 |
| - | diferencia | r1 - r2 |
| << | estrictamente antes | r1 << r2 |
| >> | estrictamente después | r1 >> r2 |
| -\|- | adyacentes | r1 -\|- r2 |
SELECT
'[1, 10]'::int4range && '[5, 15]'::int4range AS solapan, -- true
'[1, 10]'::int4range @> 5 AS contiene_valor, -- true
'[1, 10]'::int4range * '[5, 15]'::int4range AS interseccion, -- [5,10]
'[1, 5]'::int4range -|- '[6, 10]'::int4range AS adyacentes; -- true
Funciones de extracción
Para obtener los bordes y propiedades de un rango:
SELECT
lower(periodo) AS inicio,
upper(periodo) AS fin,
lower_inc(periodo) AS inicio_inclusivo,
upper_inc(periodo) AS fin_inclusivo,
isempty(periodo) AS vacio,
upper(periodo) - lower(periodo) AS duracion
FROM reservas
WHERE id = 1;
lower_inc y upper_inc devuelven el borde de inclusión (true si es [ o ], false si es ( o )). isempty distingue rangos vacíos del resto.
Daterange para periodos por días
Para rangos de fechas el tipo recomendado es daterange:
CREATE TABLE precios_promocion (
id SERIAL PRIMARY KEY,
producto_id INT NOT NULL,
precio NUMERIC(10,2) NOT NULL,
vigencia DATERANGE NOT NULL,
EXCLUDE USING gist (
producto_id WITH =,
vigencia WITH &&
)
);
INSERT INTO precios_promocion (producto_id, precio, vigencia) VALUES
(1, 100.00, '[2026-01-01, 2026-02-01)'),
(1, 90.00, '[2026-02-01, 2026-03-01)'),
(1, 80.00, '[2026-03-01, 2026-04-01)');
La consulta del precio vigente para una fecha es trivial:
SELECT precio
FROM precios_promocion
WHERE producto_id = 1
AND vigencia @> DATE '2026-02-15';
Multirange types: añadidos en PostgreSQL 14
Los multirange permiten representar uniones disjuntas de rangos en una sola columna:
SELECT '{[1,10], [20,30]}'::int4multirange;
-- Operaciones similares
SELECT '{[1,10]}'::int4multirange + '{[20,30]}'::int4multirange;
-- {[1,11), [20,31)}
Útiles para modelar disponibilidades complejas: por ejemplo, los tramos horarios libres de un médico durante la semana.
Cuándo conviene usar rangos
Los range types brillan en:
- Sistemas de reservas y bookings con garantía de no solape.
- Históricos temporales: precio vigente en cada periodo, asignaciones que cambian.
- Validez de configuraciones: feature flags activos durante un rango.
- Slots y calendarios: turnos, horarios, agendas.
Y son menos adecuados cuando:
- Necesitas interrogar puntos individuales muy frecuentemente: una columna
inicioyfincon índice clásico puede ser más eficiente. - La regla de no-solape no aplica: los exclusion constraints añaden coste de escritura.
flowchart LR
A[INSERT con periodo solapante] --> B{Existe otra fila<br/>misma sala<br/>periodo solapante?}
B -->|Si| C[ERROR exclusion constraint]
B -->|No| D[Insertar correctamente]
E[GiST index] --> B
Los rangos y exclusion constraints son una de las funcionalidades más diferenciadoras de PostgreSQL. Un esquema bien diseñado con ellos resuelve en una línea lo que en otros motores requiere triggers complicados o lógica de aplicación con bloqueos manuales.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, SQL es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de SQL
Explora más contenido relacionado con SQL y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Definir columnas de tipo tstzrange, int4range y daterange con bordes adecuados. Aplicar operadores de contención @>, solapamiento && y operaciones de conjunto. Crear EXCLUDE constraints con índice GiST para impedir solapes. Resolver casos de bookings, calendarios y slots con rangos. Construir y desempaquetar rangos con funciones lower, upper, isempty.
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje