8.17. Tipos de rangos (Range Types) #

8.17.1. Tipos de rangos y multirangos integrados
8.17.2. Ejemplos
8.17.3. Límites inclusivos y exclusivos
8.17.4. Rangos infinitos (no acotados)
8.17.5. Entrada/Salida de rangos
8.17.6. Construcción de rangos y multirangos
8.17.7. Tipos de rangos discretos
8.17.8. Definición de nuevos tipos de rangos
8.17.9. Indexación
8.17.10. Restricciones en rangos

Los tipos de rangos son tipos de datos que representan un rango de valores de algún tipo de elemento (llamado el subtipo del rango). Por ejemplo, los rangos de timestamp se pueden utilizar para representar los rangos de tiempo en los que una sala de reuniones está reservada. En este caso, el tipo de datos es tsrange (abreviatura de timestamp range), y timestamp es el subtipo. El subtipo debe tener un orden total para que esté bien definido si los valores de los elementos están dentro, antes o después de un rango de valores.

Los tipos de rangos son útiles porque representan muchos valores de elementos en un solo valor de rango, y porque los conceptos como el solapamiento de rangos se pueden expresar claramente. El uso de rangos de fecha y hora con fines de programación es el ejemplo más claro; pero los rangos de precios, los rangos de medición de un instrumento, etc., también pueden ser útiles.

Cada tipo de rango tiene un tipo de multirango correspondiente. Un multirango es una lista ordenada de rangos no contiguos, no vacíos y no nulos. La mayoría de los operadores de rango también funcionan en multirangos, y tienen algunas funciones propias.

8.17.1. Tipos de rangos y multirangos integrados #

PostgreSQL viene con los siguientes tipos de rangos integrados:

  • int4range — Rango de integer, int4multirange — multirango correspondiente

  • int8range — Rango de bigint, int8multirange — multirango correspondiente

  • numrange — Rango de numeric, nummultirange — multirango correspondiente

  • tsrange — Rango de timestamp sin zona horaria, tsmultirange — multirango correspondiente

  • tstzrange — Rango de timestamp con zona horaria, tstzmultirange — multirango correspondiente

  • daterange — Rango de date, datemultirange — multirango correspondiente

Además, puedes definir tus propios tipos de rangos; consulta CREATE TYPE para obtener más información.

8.17.2. Ejemplos #

CREATE TABLE reservation (room int, during tsrange);
INSERT INTO reservation VALUES
    (1108, '[2010-01-01 14:30, 2010-01-01 15:30)');

-- Containment (Contención)
SELECT int4range(10, 20) @> 3;

-- Overlaps (Solapamiento)
SELECT numrange(11.1, 22.2) && numrange(20.0, 30.0);

-- Extract the upper bound (Extraer el límite superior)
SELECT upper(int8range(15, 25));

-- Compute the intersection (Calcular la intersección)
SELECT int4range(10, 20) * int4range(15, 25);

-- Is the range empty? (¿Está vacío el rango?)
SELECT isempty(numrange(1, 5));

Consulta Table 9.58 y Table 9.60 para obtener listas completas de operadores y funciones en tipos de rangos.

8.17.3. Límites inclusivos y exclusivos #

Cada rango no vacío tiene dos límites, el límite inferior y el límite superior. Todos los puntos entre estos valores se incluyen en el rango. Un límite inclusivo significa que el punto límite en sí mismo también se incluye en el rango, mientras que un límite exclusivo significa que el punto límite no se incluye en el rango.

En la forma de texto de un rango, un límite inferior inclusivo se representa por [, mientras que un límite inferior exclusivo se representa por (. Del mismo modo, un límite superior inclusivo se representa por ], mientras que un límite superior exclusivo se representa por ). (Consulta Section 8.17.5 para obtener más detalles).

Las funciones lower_inc y upper_inc comprueban la inclusividad de los límites inferior y superior de un valor de rango, respectivamente.

8.17.4. Rangos infinitos (no acotados) #

El límite inferior de un rango se puede omitir, lo que significa que todos los valores menores que el límite superior se incluyen en el rango, por ejemplo, (,3]. Del mismo modo, si el límite superior del rango se omite, entonces todos los valores mayores que el límite inferior se incluyen en el rango. Si se omiten tanto el límite inferior como el superior, se considera que todos los valores del tipo de elemento están en el rango. Especificar un límite faltante como inclusivo se convierte automáticamente a exclusivo, por ejemplo, [,] se convierte en (,). Puedes pensar en estos valores faltantes como +/-infinito, pero son valores especiales del tipo de rango y se considera que están más allá de los valores de +/-infinito de cualquier tipo de elemento de rango.

Los tipos de elementos que tienen la noción de infinito pueden usarlos como valores de límite explícitos. Por ejemplo, con rangos de marcas de tiempo (timestamp), [today,infinity) excluye el valor especial de timestamp infinity, mientras que [today,infinity] lo incluye, al igual que lo hacen [today,) y [today,].

Las funciones lower_inf y upper_inf comprueban los límites inferior y superior infinitos de un rango, respectivamente.

8.17.5. Entrada/Salida de rangos #

La entrada para un valor de rango debe seguir uno de los siguientes patrones:

(límite-inferior,límite-superior)
(límite-inferior,límite-superior]
[límite-inferior,límite-superior)
[límite-inferior,límite-superior]
empty

Los paréntesis o corchetes indican si los límites inferior y superior son exclusivos o inclusivos, como se describió anteriormente. Ten en cuenta que el patrón final es empty, el cual representa un rango vacío (un rango que no contiene puntos).

El límite-inferior puede ser una cadena que sea una entrada válida para el subtipo, o vacío para indicar que no hay límite inferior. Del mismo modo, el límite-superior puede ser una cadena que sea una entrada válida para el subtipo, o vacío para indicar que no hay límite superior.

Cada valor de límite se puede entrecomillar usando caracteres " (comilla doble). Esto es necesario si el valor del límite contiene paréntesis, corchetes, comas, comillas dobles o barras diagonales inversas, ya que de lo contrario estos caracteres se tomarían como parte de la sintaxis del rango. Para poner una comilla doble o una barra diagonal inversa en un valor de límite entrecomillado, colócale una barra diagonal inversa antes. (Además, un par de comillas dobles dentro de un valor de límite entrecomillado con comillas dobles se toma para representar un carácter de comilla doble, de manera análoga a las reglas para comillas simples en cadenas literales SQL). Alternativamente, puedes evitar las comillas y usar el escape de barra diagonal inversa para proteger todos los caracteres de datos que de lo contrario se tomarían como sintaxis de rango. Además, para escribir un valor de límite que sea una cadena vacía, escribe "", ya que no escribir nada significa un límite infinito.

Se permite espacio en blanco antes y después del valor del rango, pero cualquier espacio en blanco entre los paréntesis o corchetes se toma como parte del valor del límite inferior o superior. (Dependiendo del tipo de elemento, podría ser significativo o no).

Note

Estas reglas son muy similares a las de la escritura de valores de campo en literales de tipo compuesto. Consulta Section 8.16.6 para obtener comentarios adicionales.

Ejemplos:

-- incluye 3, no incluye 7, e incluye todos los puntos intermedios
SELECT '[3,7)'::int4range;

-- no incluye 3 ni 7, pero incluye todos los puntos intermedios
SELECT '(3,7)'::int4range;

-- incluye solo el punto único 4
SELECT '[4,4]'::int4range;

-- no incluye ningún punto (y se normalizará a 'empty')
SELECT '[4,4)'::int4range;

La entrada para un multirango consiste en llaves ({ y }) que contienen cero o más rangos válidos, separados por comas. Se permite espacio en blanco alrededor de las llaves y las comas. Esto pretende recordar la sintaxis de los arrays, aunque los multirangos son mucho más simples: tienen solo una dimensión y no es necesario entrecomillar su contenido. (Sin embargo, los límites de sus rangos pueden entrecomillarse como se explicó anteriormente).

Ejemplos:

SELECT '{}'::int4multirange;
SELECT '{[3,7)}'::int4multirange;
SELECT '{[3,7), [8,9)}'::int4multirange;

8.17.6. Construcción de rangos y multirangos #

Cada tipo de rango tiene una función constructora con el mismo nombre que el tipo de rango. El uso de la función constructora es frecuentemente más conveniente que escribir una constante literal de rango, ya que evita la necesidad de entrecomillar adicionalmente los valores de los límites. La función constructora acepta dos o tres argumentos. La forma de dos argumentos construye un rango en forma estándar (límite inferior inclusivo, límite superior exclusivo), mientras que la forma de tres argumentos construye un rango con límites de la forma especificada por el tercer argumento. El tercer argumento debe ser una de las cadenas (), (], [) o []. Por ejemplo:

-- La forma completa es: límite inferior, límite superior y argumento de texto que indica
-- la inclusividad/exclusividad de los límites.
SELECT numrange(1.0, 14.0, '(]');

-- Si se omite el tercer argumento, se asume '[)'.
SELECT numrange(1.0, 14.0);

-- Aunque aquí se especifica '(]', al mostrarse el valor se convertirá a la forma
-- canónica, ya que int8range es un tipo de rango discreto (ver más abajo).
SELECT int8range(1, 14, '(]');

-- El uso de NULL para cualquiera de los límites hace que el rango no esté acotado por ese lado.
SELECT numrange(NULL, 2.2);

Cada tipo de rango también tiene un constructor de multirango con el mismo nombre que el tipo de multirango. La función constructora toma cero o más argumentos que son todos rangos del tipo apropiado. Por ejemplo:

SELECT nummultirange();
SELECT nummultirange(numrange(1.0, 14.0));
SELECT nummultirange(numrange(1.0, 14.0), numrange(20.0, 25.0));

8.17.7. Tipos de rangos discretos #

Un rango discreto es aquel cuyo tipo de elemento tiene un paso (step) bien definido, como integer o date. En estos tipos se puede decir que dos elementos son adyacentes cuando no hay valores válidos entre ellos. Esto contrasta con los rangos continuos, donde siempre (o casi siempre) es posible identificar otros valores de elementos entre dos valores dados. Por ejemplo, un rango sobre el tipo numeric es continuo, al igual que un rango sobre timestamp. (Aunque timestamp tiene una precisión limitada, por lo que teóricamente podría tratarse como discreto, es mejor considerarlo continuo ya que el tamaño del paso normalmente no es de interés).

Otra forma de pensar en un tipo de rango discreto es que hay una idea clara de un valor siguiente o anterior para cada valor de elemento. Sabiendo eso, es posible convertir entre representaciones inclusivas y exclusivas de los límites de un rango, eligiendo el valor del elemento siguiente o anterior en lugar del dado originalmente. Por ejemplo, en un tipo de rango entero, [4,8] y (3,9) denotan el mismo conjunto de valores; pero esto no sería así para un rango sobre numeric.

Un tipo de rango discreto debe tener una función de canonización que sea consciente del tamaño del paso deseado para el tipo de elemento. La función de canonización se encarga de convertir valores equivalentes del tipo de rango para que tengan representaciones idénticas, en particular límites consistentemente inclusivos o exclusivos. Si no se especifica una función de canonización, los rangos con formato diferente siempre se tratarán como desiguales, aunque en realidad representen el mismo conjunto de valores.

Los tipos de rangos integrados int4range, int8range y daterange utilizan una forma canónica que incluye el límite inferior y excluye el límite superior; es decir, [). Sin embargo, los tipos de rango definidos por el usuario pueden utilizar otras convenciones.

8.17.8. Definición de nuevos tipos de rangos #

Los usuarios pueden definir sus propios tipos de rangos. La razón más común para hacer esto es usar rangos sobre subtipos que no se proporcionan entre los tipos de rangos integrados. Por ejemplo, para definir un nuevo tipo de rango de subtipo float8:

CREATE TYPE floatrange AS RANGE (
    subtype = float8,
    subtype_diff = float8mi
);

SELECT '[1.234, 5.678]'::floatrange;

Debido a que float8 no tiene un paso significativo, no definimos una función de canonización en este ejemplo.

Cuando defines tu propio rango, obtienes automáticamente un tipo de multirango correspondiente.

Definir tu propio tipo de rango también te permite especificar una clase de operador B-tree de subtipo o una colación diferente para usar, con el fin de cambiar el orden de clasificación que determina qué valores caen en un rango dado.

Si se considera que el subtipo tiene valores discretos en lugar de continuos, el comando CREATE TYPE debe especificar una función canonical. La función de canonización toma un valor de rango de entrada y debe devolver un valor de rango equivalente que puede tener límites y formatos diferentes. La salida canónica para dos rangos que representan el mismo conjunto de valores, por ejemplo, los rangos de enteros [1, 7] y [1, 8), debe ser idéntica. No importa qué representación elijas para ser la canónica, siempre que dos valores equivalentes con diferentes formatos se mapeen siempre al mismo valor con el mismo formato. Además de ajustar el formato de los límites inclusivos/exclusivos, una función de canonización podría redondear los valores límite, en caso de que el tamaño de paso deseado sea mayor de lo que el subtipo es capaz de almacenar. Por ejemplo, un tipo de rango sobre timestamp podría definirse para tener un tamaño de paso de una hora, en cuyo caso la función de canonización necesitaría redondear los límites que no fuesen un múltiplo de una hora, o tal vez lanzar un error en su lugar.

Además, cualquier tipo de rango que se pretenda utilizar con índices GiST o SP-GiST debe definir una función de diferencia de subtipo, o subtype_diff. (El índice seguirá funcionando sin subtype_diff, pero es probable que sea considerablemente menos eficiente que si se proporciona una función de diferencia). La función de diferencia de subtipo toma dos valores de entrada del subtipo y devuelve su diferencia (es decir, X menos Y) representada como un valor float8. En nuestro ejemplo anterior, se puede utilizar la función float8mi que subyace al operador menos regular de float8; pero para cualquier otro subtipo, sería necesaria alguna conversión de tipo. También puede ser necesario pensar de forma creativa sobre cómo representar las diferencias como números. En la mayor medida posible, la función subtype_diff debe concordar con el orden de clasificación implícito por la clase de operador y colación seleccionadas; es decir, su resultado debe ser positivo siempre que su primer argumento sea mayor que el segundo según el orden de clasificación.

Un ejemplo menos simplificado de una función subtype_diff es:

CREATE FUNCTION time_subtype_diff(x time, y time) RETURNS float8 AS
'SELECT EXTRACT(EPOCH FROM (x - y))' LANGUAGE sql STRICT IMMUTABLE;

CREATE TYPE timerange AS RANGE (
    subtype = time,
    subtype_diff = time_subtype_diff
);

SELECT '[11:10, 23:00]'::timerange;

Consulta CREATE TYPE para obtener más información sobre la creación de tipos de rangos.

8.17.9. Indexación #

Se pueden crear índices GiST y SP-GiST para columnas de tablas de tipos de rangos. También se pueden crear índices GiST para columnas de tablas de tipos de multirangos. Por ejemplo, para crear un índice GiST:

CREATE INDEX reservation_idx ON reservation USING GIST (during);

Un índice GiST o SP-GiST en rangos puede acelerar las consultas que involucran estos operadores de rango: =, &&, <@, @>, <<, >>, -|-, &< y &>. Un índice GiST en multirangos puede acelerar las consultas que involucran el mismo conjunto de operadores de multirango. Un índice GiST en rangos y un índice GiST en multirangos también pueden acelerar las consultas que involucran estos operadores cruzados de rango a multirango y de multirango a rango correspondientemente: &&, <@, @>, <<, >>, -|-, &< y &>. Consulta Table 9.58 para obtener más información.

Además, se pueden crear índices B-tree y hash para columnas de tablas de tipos de rangos. Para estos tipos de índices, básicamente la única operación de rango útil es la igualdad. Hay un orden de clasificación B-tree definido para los valores de rango, con los operadores correspondientes < y >, pero el ordenamiento es bastante arbitrario y no suele ser útil en el mundo real. El soporte B-tree y hash para tipos de rangos está destinado principalmente a permitir la clasificación y el hash interno en las consultas, en lugar de la creación de índices reales.

8.17.10. Restricciones en rangos #

Aunque UNIQUE es una restricción natural para los valores escalares, generalmente es inadecuada para los tipos de rangos. En su lugar, una restricción de exclusión suele ser más apropiada (ver CREATE TABLE ... CONSTRAINT ... EXCLUDE). Las restricciones de exclusión permiten la especificación de restricciones como sin solapamiento (non-overlapping) en un tipo de rango. Por ejemplo:

CREATE TABLE reservation (
    during tsrange,
    EXCLUDE USING GIST (during WITH &&)
);

Esa restricción evitará que existan valores solapados en la tabla al mismo tiempo:

INSERT INTO reservation VALUES
    ('[2010-01-01 11:30, 2010-01-01 15:00)');
INSERT 0 1

INSERT INTO reservation VALUES
    ('[2010-01-01 14:45, 2010-01-01 15:45)');
ERROR:  conflicting key value violates exclusion constraint "reservation_during_excl"
DETAIL:  Key (during)=(["2010-01-01 14:45:00","2010-01-01 15:45:00")) conflicts
with existing key (during)=(["2010-01-01 11:30:00","2010-01-01 15:00:00"])).

Puedes utilizar la extensión btree_gist para definir restricciones de exclusión en tipos de datos escalares simples, que luego se pueden combinar con exclusiones de rango para obtener la máxima flexibilidad. Por ejemplo, después de instalar btree_gist, la siguiente restricción rechazará los rangos solapados solo si los números de sala de reuniones son iguales:

CREATE EXTENSION btree_gist;
CREATE TABLE room_reservation (
    room text,
    during tsrange,
    EXCLUDE USING GIST (room WITH =, during WITH &&)
);

INSERT INTO room_reservation VALUES
    ('123A', '[2010-01-01 14:00, 2010-01-01 15:00)');
INSERT 0 1

INSERT INTO room_reservation VALUES
    ('123A', '[2010-01-01 14:30, 2010-01-01 15:30)');
ERROR:  conflicting key value violates exclusion constraint "room_reservation_room_during_excl"
DETAIL:  Key (room, during)=(123A, ["2010-01-01 14:30:00","2010-01-01 15:30:00")) conflicts
with existing key (room, during)=(123A, ["2010-01-01 14:00:00","2010-01-01 15:00:00"])).

INSERT INTO room_reservation VALUES
    ('123B', '[2010-01-01 14:30, 2010-01-01 15:30)');
INSERT 0 1