Range types y exclusion constraints

Avanzado
SQL
SQL
Actualizado: 19/04/2026

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 inicio y fin con í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 - Autor del tutorial

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