Un tipo compuesto representa la estructura de una fila o registro; esencialmente es solo una lista de nombres de campos y sus tipos de datos. PostgreSQL permite utilizar tipos compuestos de muchas de las mismas formas en que se pueden usar los tipos simples. Por ejemplo, una columna de una tabla puede declararse como un tipo compuesto.
Aquí tienes dos ejemplos sencillos para definir tipos compuestos:
CREATE TYPE complex AS (
r double precision,
i double precision
);
CREATE TYPE inventory_item AS (
name text,
supplier_id integer,
price numeric
);
La sintaxis es similar a CREATE TABLE, excepto que solo
se pueden especificar los nombres y tipos de campos; actualmente no se pueden
incluir restricciones (como NOT NULL). Ten en cuenta que la
palabra clave AS es esencial; sin ella, el sistema pensará que
te refieres a una clase diferente de comando CREATE TYPE y
obtendrás errores de sintaxis extraños.
Una vez definidos los tipos, podemos usarlos para crear tablas:
CREATE TABLE on_hand (
item inventory_item,
count integer
);
INSERT INTO on_hand VALUES (ROW('fuzzy dice', 42, 1.99), 1000);
o funciones:
CREATE FUNCTION price_extension(inventory_item, integer) RETURNS numeric AS 'SELECT $1.price * $2' LANGUAGE SQL; SELECT price_extension(item, 10) FROM on_hand;
Siempre que creas una tabla, automáticamente se crea también un tipo compuesto, con el mismo nombre que la tabla, para representar el tipo de fila de la tabla. Por ejemplo, si hubiéramos escrito:
CREATE TABLE inventory_item (
name text,
supplier_id integer REFERENCES suppliers,
price numeric CHECK (price > 0)
);
entonces se crearía como subproducto el mismo tipo compuesto inventory_item
que se muestra arriba, y podría usarse de la misma manera. Sin embargo, ten en cuenta una
restricción importante de la implementación actual: dado que no se asocian restricciones
a un tipo compuesto, las restricciones mostradas en la definición de la tabla
no se aplican a los valores del tipo compuesto fuera de la tabla.
(Para solucionar esto, crea un dominio sobre el
tipo compuesto y aplica las restricciones deseadas como restricciones CHECK
del dominio).
Para escribir un valor compuesto como una constante literal, encierra los valores de los campos entre paréntesis y sepáralos con comas. Puedes poner comillas dobles alrededor de cualquier valor de campo, y debes hacerlo si contiene comas o paréntesis. (Aparecen más detalles más abajo). Por lo tanto, el formato general de una constante compuesta es el siguiente:
'(val1,val2, ... )'
Un ejemplo es:
'("fuzzy dice",42,1.99)'
el cual sería un valor válido del tipo inventory_item definido
anteriormente. Para hacer que un campo sea NULL, no escribas ningún carácter en
su posición en la lista. Por ejemplo, esta constante especifica un tercer campo NULL:
'("fuzzy dice",42,)'
Si deseas una cadena vacía en lugar de NULL, escribe comillas dobles:
'("",42,)'
Aquí, el primer campo es una cadena vacía que no es NULL, y el tercero es NULL.
(Estas constantes son en realidad solo un caso especial de las constantes de tipo genérico analizadas en la Section 4.1.2.7. La constante se trata inicialmente como una cadena y se pasa a la rutina de conversión de entrada de tipos compuestos. Podría ser necesaria una especificación de tipo explícita para indicar a qué tipo se debe convertir la constante).
La sintaxis de la expresión ROW también se puede usar para construir
valores compuestos. En la mayoría de los casos, esto es considerablemente más sencillo
de usar que la sintaxis de cadena literal, ya que no tienes que preocuparte por múltiples
niveles de comillas. Ya usamos este método anteriormente:
ROW('fuzzy dice', 42, 1.99)
ROW('', 42, NULL)
La palabra clave ROW es opcional siempre que tengas más de un campo en la expresión, por lo que estos ejemplos se pueden simplificar a:
('fuzzy dice', 42, 1.99)
('', 42, NULL)
La sintaxis de la expresión ROW se analiza con más detalle en la
Section 4.2.13.
Para acceder a un campo de una columna compuesta, se escribe un punto y el nombre del
campo, de manera muy similar a la selección de un campo de un nombre de tabla. De hecho,
es tan similar a seleccionar desde un nombre de tabla que a menudo tienes que usar paréntesis
para evitar confundir al analizador sintáctico. Por ejemplo, podrías intentar seleccionar
algunos subcampos de nuestra tabla de ejemplo on_hand con algo como:
SELECT item.name FROM on_hand WHERE item.price > 9.99;
Esto no funcionará ya que el nombre item se toma como un nombre de
tabla, no como un nombre de columna de on_hand, según las reglas de
sintaxis de SQL. Debes escribirlo así:
SELECT (item).name FROM on_hand WHERE (item).price > 9.99;
o si necesitas usar también el nombre de la tabla (por ejemplo, en una consulta multitable), así:
SELECT (on_hand.item).name FROM on_hand WHERE (on_hand.item).price > 9.99;
Ahora el objeto entre paréntesis se interpreta correctamente como una referencia a la columna
item, y luego se puede seleccionar el subcampo a partir de él.
Problemas sintácticos similares se aplican siempre que seleccionas un campo de un valor compuesto. Por ejemplo, para seleccionar solo un campo del resultado de una función que devuelve un valor compuesto, tendrías que escribir algo como:
SELECT (my_func(...)).field FROM ...
Sin los paréntesis adicionales, esto generará un error de sintaxis.
El nombre de campo especial * significa “todos los campos”,
como se explica detalladamente en la Section 8.16.5.
Aquí tienes algunos ejemplos de la sintaxis adecuada para insertar y actualizar columnas compuestas. Primero, insertar o actualizar una columna completa:
INSERT INTO mytab (complex_col) VALUES((1.1,2.2)); UPDATE mytab SET complex_col = ROW(1.1,2.2) WHERE ...;
El primer ejemplo omite ROW, el segundo lo usa; podríamos haberlo
hecho de cualquiera de las dos formas.
Podemos actualizar un subcampo individual de una columna compuesta:
UPDATE mytab SET complex_col.r = (complex_col).r + 1 WHERE ...;
Observa aquí que no necesitamos (y de hecho no podemos) poner paréntesis alrededor del
nombre de la columna que aparece justo después de SET, pero sí
necesitamos paréntesis cuando hacemos referencia a la misma columna en la expresión
a la derecha del signo igual.
And también podemos especificar subcampos como objetivos para INSERT:
INSERT INTO mytab (complex_col.r, complex_col.i) VALUES(1.1, 2.2);
Si no hubiéramos suministrado valores para todos los subcampos de la columna, los subcampos restantes se habrían llenado con valores nulos.
Existen varias reglas de sintaxis especiales y comportamientos asociados con los tipos compuestos en las consultas. Estas reglas proporcionan atajos útiles, pero pueden resultar confusas si no conoces la lógica detrás de ellas.
En PostgreSQL, una referencia a un nombre de tabla (o alias)
en una consulta es efectivamente una referencia al valor compuesto de la fila actual de la
tabla. Por ejemplo, si tuviéramos una tabla inventory_item como
se muestra arriba, podríamos escribir:
SELECT c FROM inventory_item c;
Esta consulta produce una única columna con valor compuesto, por lo que podríamos obtener una salida como:
c
------------------------
("fuzzy dice",42,1.99)
(1 row)
Ten en cuenta, sin embargo, que los nombres simples se comparan con los nombres de columna
antes que con los nombres de tabla, por lo que este ejemplo funciona únicamente porque
no hay ninguna columna llamada c en las tablas de la consulta.
La sintaxis ordinaria de nombre de columna calificado
table_name.column_name
puede entenderse como la aplicación de la selección de campo
al valor compuesto de la fila actual de la tabla.
(Por razones de eficiencia, en realidad no se implementa de esa manera).
Cuando escribimos
SELECT c.* FROM inventory_item c;
entonces, según el estándar SQL, deberíamos obtener el contenido de la tabla expandido en columnas separadas:
name | supplier_id | price
------------+-------------+-------
fuzzy dice | 42 | 1.99
(1 row)
como si la consulta fuera
SELECT c.name, c.supplier_id, c.price FROM inventory_item c;
PostgreSQL aplicará este comportamiento de expansión a cualquier
expresión de valor compuesto, aunque como se muestra arriba,
necesitas escribir paréntesis alrededor del valor al que se le aplica .*
siempre que no sea un nombre de tabla simple. Por ejemplo, si myfunc()
es una función que devuelve un tipo compuesto con columnas a,
b y c, entonces estas dos consultas
tienen el mismo resultado:
SELECT (myfunc(x)).* FROM some_table; SELECT (myfunc(x)).a, (myfunc(x)).b, (myfunc(x)).c FROM some_table;
PostgreSQL maneja la expansión de columnas transformando
realmente la primera forma en la segunda. Por lo tanto, en este ejemplo,
myfunc() se invocaría tres veces por fila con cualquiera de
las dos sintaxis. Si es una función costosa, es posible que desees evitar eso, lo
cual puedes hacer con una consulta como:
SELECT m.* FROM some_table, LATERAL myfunc(x) AS m;
Colocar la función en un elemento LATERAL FROM evita
que se invoque más de una vez por fila. m.* todavía se expande
en m.a, m.b, m.c, pero ahora esas variables son solo referencias
a la salida del elemento FROM.
(La palabra clave LATERAL is opcional aquí, pero la mostramos para
aclarar que la función obtiene x de
some_table).
La sintaxis composite_value.* da como
resultado una expansión de columnas de este tipo cuando aparece en el nivel superior de una
lista de salida de SELECT,
una lista RETURNING en
INSERT/UPDATE/DELETE/MERGE,
una cláusula VALUES o
un constructor de filas.
En todos los demás contextos (incluido cuando está anidado dentro de uno de esos constructores),
adjuntar .* a un valor compuesto no cambia el valor, ya que significa
“todas las columnas” y, por lo tanto, se produce el mismo valor compuesto nuevamente.
Por ejemplo, si somefunc() acepta un argumento con valor compuesto, estas
consultas son iguales:
SELECT somefunc(c.*) FROM inventory_item c; SELECT somefunc(c) FROM inventory_item c;
En ambos casos, la fila actual de inventory_item se pasa a la
función como un único argumento con valor compuesto. Aunque .* no
hace nada en tales casos, usarlo es un buen estilo, ya que deja claro que se pretende
usar un valor compuesto. En particular, el analizador considerará que c
en c.* se refiere a un nombre de tabla o alias, no a un nombre de columna,
de modo que no haya ambigüedad; mientras que sin .*, no está claro si
c significa un nombre de tabla o un nombre de columna y, de hecho, se preferirá
la interpretación de nombre de columna si existe una columna llamada c.
Otro ejemplo que demuestra estos conceptos es que todas estas consultas significan lo mismo:
SELECT * FROM inventory_item c ORDER BY c; SELECT * FROM inventory_item c ORDER BY c.*; SELECT * FROM inventory_item c ORDER BY ROW(c.*);
Todas estas cláusulas ORDER BY especifican el valor compuesto de la
fila, lo que resulta en ordenar las filas de acuerdo con las reglas descritas en la
Section 9.25.6. Sin embargo, si
inventory_item contuviera una columna llamada
c, el primer caso sería diferente de los demás, ya que significaría
ordenar solo por esa columna. Dados los nombres de columnas mostrados anteriormente,
estas consultas también son equivalentes a las de arriba:
SELECT * FROM inventory_item c ORDER BY ROW(c.name, c.supplier_id, c.price); SELECT * FROM inventory_item c ORDER BY (c.name, c.supplier_id, c.price);
(El último caso utiliza un constructor de filas con la palabra clave ROW
omitida).
Otro comportamiento sintáctico especial asociado con los valores compuestos es que podemos usar
la notación funcional para extraer un campo de un valor compuesto.
La forma sencilla de explicar esto es que las notaciones
y
field(table) son
intercambiables. Por ejemplo, estas consultas son equivalentes:
table.field
SELECT c.name FROM inventory_item c WHERE c.price > 1000; SELECT name(c) FROM inventory_item c WHERE price(c) > 1000;
Además, si tenemos una función que acepta un único argumento de un tipo compuesto, podemos llamarla con cualquiera de las dos notaciones. Estas consultas son todas equivalentes:
SELECT somefunc(c) FROM inventory_item c; SELECT somefunc(c.*) FROM inventory_item c; SELECT c.somefunc FROM inventory_item c;
Esta equivalencia entre la notación funcional y la notación de campos hace posible utilizar
funciones en tipos compuestos para implementar “campos calculados”.
Una aplicación que utilice la última consulta anterior no necesitaría ser consciente
directamente de que somefunc no es una columna real de la tabla.
Debido a este comportamiento, no es aconsejable dar a una función que toma un único argumento
de tipo compuesto el mismo nombre que cualquiera de los campos de ese tipo compuesto. Si hay
ambigüedad, se elegirá la interpretación del nombre del campo si se utiliza la sintaxis de nombre
de campo, mientras que se elegirá la función si se utiliza la sintaxis de llamada a función.
Sin embargo, las versiones de PostgreSQL anteriores a la 11 siempre
elegían la interpretación de nombre de campo, a menos que la sintaxis de la llamada requiriera
que fuera una llamada a función. Una forma de forzar la interpretación de la función en versiones
anteriores es calificar el nombre de la función con el esquema, es decir, escribir
.
schema.func(compositevalue)
La representación de texto externa de un valor compuesto consta de elementos que se interpretan
de acuerdo con las reglas de conversión de E/S para los tipos de campo individuales, más la
decoración que indica la estructura compuesta.
La decoración consta de paréntesis (( y )) alrededor de todo
el valor, más comas (,) entre elementos adyacentes. El espacio en blanco fuera
de los paréntesis se ignora, pero dentro de los paréntesis se considera parte del valor del campo
y puede o no ser significativo según las reglas de conversión de entrada para el tipo de datos
del campo. Por ejemplo, en:
'( 42)'
el espacio en blanco se ignorará si el tipo de campo es entero, pero no si es texto.
Como se mostró anteriormente, al escribir un valor compuesto puedes escribir comillas dobles alrededor de cualquier valor de campo individual. Debes hacerlo si el valor del campo pudiera de otro modo confundir al analizador de valores compuestos. En particular, los campos que contienen paréntesis, comas, comillas dobles o barras invertidas deben estar entre comillas dobles. Para colocar una comilla doble o una barra invertida en un valor de campo compuesto entre comillas, antecede el carácter con una barra invertida. (Además, un par de comillas dobles dentro de un valor de campo entre 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 de SQL). Alternativamente, puedes evitar las comillas y usar el escape de barra invertida para proteger todos los caracteres de datos que de otro modo se tomarían como sintaxis compuesta.
Un valor de campo completamente vacío (sin caracteres entre las comas o paréntesis) representa
un NULL. Para escribir un valor que sea una cadena vacía en lugar de NULL, escribe
"".
La rutina de salida compuesta colocará comillas dobles alrededor de los valores de los campos si son cadenas vacías o contienen paréntesis, comas, comillas dobles, barras invertidas o espacios en blanco. (Hacerlo para los espacios en blanco no es esencial, pero ayuda a la legibilidad). Las comillas dobles y las barras invertidas incrustadas en los valores de los campos se duplicarán.
Recuerda que lo que escribes en un comando SQL se interpretará primero como una cadena literal
y luego como un compuesto. Esto duplica el número de barras invertidas que necesitas (suponiendo
que se use la sintaxis de cadena de escape). Por ejemplo, para insertar un campo text
que contiene una comilla doble y una barra invertida en un valor compuesto, tendrías que escribir:
INSERT ... VALUES ('("\"\\")');
El procesador de cadenas literales elimina un nivel de barras invertidas, de modo que lo que llega
al analizador de valores compuestos se ve como ("\"\\"). A su vez, la cadena
alimentada a la rutina de entrada del tipo de datos text se convierte en
"\. (Si estuviéramos trabajando con un tipo de datos cuya rutina de entrada
también tratara las barras invertidas de manera especial, bytea for example, podríamos
necesitar hasta ocho barras invertidas en el comando para obtener una barra invertida en el campo
compuesto almacenado). Se pueden usar las comillas de dólar (consulta la
Section 4.1.2.4) para evitar la necesidad de duplicar las barras
invertidas.
La sintaxis del constructor ROW suele ser más fácil de usar que la sintaxis
de literales compuestos al escribir valores compuestos en comandos SQL.
En ROW, los valores de los campos individuales se escriben de la misma manera
en que se escribirían si no fueran miembros de un compuesto.