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.
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.
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.
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.
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.
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).
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;
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));
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.
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.
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.
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