8.16. Tipos de dados compostos #

8.16.1. Declaração de tipos de dados compostos
8.16.2. Construção de valores compostos
8.16.3. Acesso a tipos de dados compostos
8.16.4. Modificação de tipos de dados compostos
8.16.5. Uso de tipos de dados compostos em consultas
8.16.6. Sintaxe de entrada e saída dos tipos de dados compostos

O tipo de dados composto representa a estrutura de uma linha ou registro; é essencialmente apenas uma lista de nomes de campos e seus tipos de dados. O PostgreSQL permite que tipos de dados compostos sejam usados da mesma forma que os tipos de dados simples podem ser usados. Por exemplo, uma coluna de uma tabela pode ser declarada como sendo de um tipo de dados composto.

8.16.1. Declaração de tipos de dados compostos #

Aqui estão dois exemplos simples de definição de tipos de dados compostos:

CREATE TYPE complexo AS (
    r       double precision,
    i       double precision
);

CREATE TYPE item_inventário AS (
    nome            text,
    id_fornecedor   integer,
    preco           numeric
);

A sintaxe é comparável a do comando CREATE TABLE, exceto por somente poderem ser especificados os nomes e tipos de dados dos campos; no momento não pode ser incluída nenhuma restrição (como NOT NULL). Note que a palavra-chave AS é essencial; sem ela, o sistema vai pensar que se trata de um tipo diferente de comando CREATE TYPE, e vai mostrar erros de sintaxe estranhos.

Após definir os tipos de dados, eles podem ser utilizados para criar tabelas:

CREATE TABLE estoque (
    item       item_inventário,
    quantidade integer
);

INSERT INTO estoque VALUES (ROW('dado de pano', 42, 1.99), 1000);

ou funções:

CREATE FUNCTION preco_quantidade(item_inventário, integer) RETURNS numeric
AS 'SELECT $1.preco * $2' LANGUAGE SQL;

SELECT preco_quantidade(item, 10) FROM estoque;

Sempre que uma tabela é criada, um tipo de dados composto também é criado automaticamente, com o mesmo nome da tabela, para representar o tipo de dados da linha da tabela. Por exemplo, se tivesse sido escrito:

CREATE TABLE item_inventário (
    nome            text,
    id_fornecedor   integer REFERENCES fornecedores,
    preco           numeric CHECK (preco > 0)
);

então o mesmo tipo de dados composto item_inventário mostrado acima passaria a existir como um subproduto, e poderia ser usado como acima. Note, no entanto, uma restrição importante da implementação corrente: como nenhuma restrição está associada ao tipo de dados composto, as restrições mostradas na definição da tabela não se aplicam a valores do tipo de dados composto fora da tabela. (Para contornar isso, deve ser criado um domínio sobre o tipo de dados composto e aplicadas as restrições desejadas, como restrições CHECK do domínio.)

8.16.2. Construção de valores compostos #

Para escrever um valor composto como uma constante literal, os valores dos campos devem ser colocados entre parênteses e separados por vírgulas. Podem ser colocadas aspas em torno de qualquer valor de campo, e isso deve ser feito se o campo contiver vírgulas ou parênteses. (Estão mostrados mais detalhes abaixo.) Assim, o formato geral de uma constante composta é o seguinte:

'( val1 , val2 , ... )'

Um exemplo é

'("dado de pano",42,1.99)'

que seria um valor válido para o tipo de dados item_inventário definido acima. Para tornar um campo nulo, não deve ser escrito nenhum caractere em sua posição na lista. Por exemplo, esta constante especifica um terceiro campo nulo:

'("dado de pano",42,)'

Se for desejada uma cadeia de caracteres vazia em vez de nulo, devem ser escritas duas aspas juntas, uma ao lado da outra:

'("",42,)'

Nesta constante, o primeiro campo é uma cadeia de caracteres vazia, não nula, e o terceiro campo é nulo.

(Essas constantes são, na verdade, apenas um caso especial das constantes de tipo genérico discutidas na Seção 4.1.2.7. A constante é inicialmente tratada como uma cadeia de caracteres e passada para a rotina de conversão de entrada do tipo de dados composto. Pode ser necessária uma especificação de tipo de dados explícita para informar para qual tipo de dados converter a constante.)

Também pode ser usada a sintaxe da expressão ROW para construir valores compostos. Geralmente isso é consideravelmente mais simples de usar do que a sintaxe de literal cadeia de caracteres, porque não é necessário se preocupar com várias camadas de aspas. Esse método já foi usado acima:

ROW('dado de pano', 42, 1.99)
ROW('', 42, NULL)

Na realidade, a palavra-chave ROW é opcional, desde que a expressão tenha mais de um campo, portanto os exemplos acima podem ser simplificados para:

('dado de pano', 42, 1.99)
('', 42, NULL)

A sintaxe da expressão ROW é discutida com mais detalhes na Seção 4.2.13.

8.16.3. Acesso a tipos de dados compostos #

Para acessar um campo de uma coluna composta, se escreve um ponto e o nome do campo, parecido com selecionar um campo de um nome de tabela. Na verdade, é tão parecido com o que é feito para selecionar a partir de um nome de tabela, que é muitas vezes necessário usar parênteses para não confundir o analisador. Por exemplo, pode-se tentar selecionar alguns subcampos da nossa tabela de exemplo estoque com algo como:

SELECT item.nome
    FROM estoque
    WHERE item.preco > 9.99;
ERRO:  faltando entrada para tabela "item" na cláusula FROM
LINHA 1: SELECT item.nome
                ^

Isso não funciona, porque o nome item é considerado um nome de tabela, e não um nome de coluna da tabela estoque, segundo as regras de sintaxe da linguagem SQL. Deve ser escrito assim:

SELECT (item).nome
    FROM estoque
    WHERE (item).preco > 9.99;

ou desta forma, se for necessário usar o nome da tabela também (por exemplo, em uma consulta com várias tabelas):

SELECT (estoque.item).nome
    FROM estoque
    WHERE (estoque.item).preco > 9.99;

Agora o objeto entre parênteses é interpretado corretamente como sendo uma referência à coluna item, e então o subcampo pode ser selecionado a partir dele.

Problemas de sintaxe semelhantes se aplicam sempre que se seleciona um campo de um valor composto. Por exemplo, para selecionar apenas um campo do resultado de uma função que retorna um valor composto, deve ser escrito algo como:

SELECT (minha_função(...)).campo FROM ...

Sem os parênteses extras, isso gera um erro de sintaxe.

O nome especial de campo * significa todos os campos, conforme explicado na Seção 8.16.5.

8.16.4. Modificação de tipos de dados compostos #

Aqui estão alguns exemplos da sintaxe adequada para inserir e atualizar colunas compostas. Primeiramente, inserir ou atualizar uma coluna inteira:

INSERT INTO minha_tabela (coluna_complexa) VALUES((1.1,2.2));

UPDATE minha_tabela SET coluna_complexa = ROW(1.1,2.2) WHERE ...;

O primeiro exemplo omite ROW, o segundo usa; pode ser feito das duas maneiras.

Pode ser atualizado um subcampo de uma coluna composta individualmente:

UPDATE minha_tabela SET coluna_complexa.r = (coluna_complexa).r + 1 WHERE ...;

Note que aqui não precisamos (e de fato não podemos) colocar parênteses ao redor do nome da coluna que aparece logo após SET, mas precisamos de parênteses ao referenciar a mesma coluna na expressão à direita do sinal de igual.

Podem ser especificados subcampos como destinos para o comando INSERT também:

INSERT INTO minha_tabela (coluna_complexa.r, coluna_complexa.i) VALUES(1.1, 2.2);

Se não tivesse sido fornecido valores para todos os subcampos da coluna, os subcampos restantes teriam sido preenchidos com valores nulos.

8.16.5. Uso de tipos de dados compostos em consultas #

Existem várias regras de sintaxe e comportamentos especiais associados a tipos de dados compostos em consultas. Essas regras fornecem atalhos úteis, mas podem ser confusas se não for conhecida a lógica por trás delas.

No PostgreSQL, uma referência a um nome de tabela (ou alias) em uma consulta é de fato uma referência ao valor composto da linha corrente da tabela. Por exemplo, se tivéssemos a tabela item_inventário conforme mostrado acima, poderia ser escrito:

SELECT c FROM item_inventário c;

Essa consulta produz uma única coluna de valor composto, portanto podemos obter uma saída como:

            c
--------------------------
 ("dado de pano",42,1.99)
(1 linha)

Note, no entanto, que os nomes simples correspondem aos nomes das colunas antes dos nomes das tabelas, portanto este exemplo funciona apenas por não haver uma coluna chamada c nas tabelas da consulta.

A sintaxe comum de nome de coluna qualificada nome_da_tabela.nome_da_coluna pode ser compreendida como a aplicação da seleção de campo ao valor composto da linha corrente da tabela. (Por razões de eficiência, não é na verdade implementado desta maneira.)

Quando se escreve

SELECT c.* FROM item_inventário c;

então, segundo o padrão SQL, devemos obter o conteúdo da tabela expandido em colunas separadas:

     nome     | id_fornecedor | preco
--------------+---------------+-------
 dado de pano |            42 |  1.99
(1 linha)

como se a consulta fosse

SELECT c.nome, c.id_fornecedor, c.preco FROM item_inventário c;

O PostgreSQL irá aplicar este comportamento de expansão a qualquer expressão de valor composto, embora como mostrado acima, é necessário escrever parênteses em torno do valor que é aplicado .* sempre que não for um nome de tabela simples. Por exemplo, se minha_função() for uma função que retorna um tipo de dados composto com colunas a, b e c, então essas duas consultas produzem o mesmo resultado:

SELECT (minha_função(x)).* FROM alguma_tabela;
SELECT (minha_função(x)).a, (minha_função(x)).b, (minha_função(x)).c FROM alguma_tabela;

Dica

O PostgreSQL lida com a expansão de colunas transformando a primeira forma na segunda. Então, nesse exemplo, minha_função() seria chamada três vezes por linha com qualquer sintaxe. Se for uma função cara, isso pode ser evitado, o que pode ser feito usando uma consulta como:

SELECT m.* FROM alguma_tabela, LATERAL minha_função(x) AS m;

Colocar a função como LATERAL no FROM impede que ela seja chamada mais de uma vez por linha. m.* ainda é expandida em m.a, m.b, m.c, mas agora essas variáveis são apenas referências à saída do FROM. (A palavra-chave LATERAL é opcional nesse caso, mas é mostrada para ficar claro que a função está obtendo x de alguma_tabela.)

A sintaxe valor_composto.* resulta numa expansão de coluna desse tipo de dados quando aparece no nível superior de SELECT lista de saída, em RETURNING lista, em INSERT/UPDATE/DELETE/MERGE, em VALUES cláusula, ou em construtor de linha. Em todos os outros contextos (incluindo quando aninhado dentro de uma dessas construções), anexar .* a um valor composto não altera o valor, porque significa todas as colunas, portanto o mesmo valor composto é produzido novamente. Por exemplo, se a função alguma_função() aceita um argumento de valor composto, essas consultas são as mesmas:

SELECT alguma_função(c.*) FROM item_inventário c;
SELECT alguma_função(c) FROM item_inventário c;

Nos dois casos, a linha corrente de item_inventário é passada para a função como um único argumento de valor composto. Muito embora .* não faça nada nesses casos, usá-lo é um bom estilo, porque deixa claro que se pretende um valor composto. Em particular, o analisador considerará c em c.* para se referir a um nome de tabela ou alias, não a um nome de coluna, para que não haja ambiguidade; enquanto que sem .*, não fica claro se c significa um nome de tabela ou um nome de coluna, e de fato a interpretação do nome da coluna será preferida se houver uma coluna chamada c.

Outro exemplo demonstrando esses conceitos, é todas essas consultas significarem a mesma coisa:

SELECT * FROM item_inventário c ORDER BY c;
SELECT * FROM item_inventário c ORDER BY c.*;
SELECT * FROM item_inventário c ORDER BY ROW(c.*);

Todas essas cláusulas ORDER BY especificam o valor composto da linha, resultando na classificação das linhas conforme as regras descritas na Seção 9.25.6. No entanto, se item_inventário contivesse uma coluna chamada c, o primeiro caso seria diferente dos demais, porque significaria classificar apenas por esta coluna. Dados os nomes das colunas mostrados anteriormente, essas consultas também são equivalentes as acima:

SELECT * FROM item_inventário c ORDER BY ROW(c.nome, c.id_fornecedor, c.preco);
SELECT * FROM item_inventário c ORDER BY (c.nome, c.id_fornecedor, c.preco);

(O último caso usa um construtor de linha com a palavra-chave ROW omitida.)

Outro comportamento sintático especial associado a valores compostos, é poder ser usada a notação de função para extrair um campo de um valor composto. A maneira simples de explicar isso, é as notações campo(tabela) e tabela.campo serem intercambiáveis. Por exemplo, essas consultas são equivalentes:

SELECT c.nome FROM item_inventário c WHERE c.preco > 1000;
SELECT nome(c) FROM item_inventário c WHERE preco(c) > 1000;

Além disso, se tivermos uma função que aceita um único argumento de tipo de dados composto, podemos chamá-la com qualquer uma das notações. Essas consultas são todas equivalentes:

SELECT alguma_função(c) FROM item_inventário c;
SELECT alguma_função(c.*) FROM item_inventário c;
SELECT c.alguma_função FROM item_inventário c;

Essa equivalência entre notação de função e notação de campo, torna possível usar funções em tipos de dados compostos para implementar campos computados. Uma aplicação usando a última consulta acima não precisaria estar diretamente ciente de que alguma_função não é uma coluna real da tabela.

Dica

Devido a este comportamento, não se recomenda dar a uma função que recebe um único argumento de tipo de dados composto o mesmo nome de qualquer um dos campos desse tipo de dados composto. Se houver ambiguidade, a interpretação do nome do campo será escolhida se for usada a sintaxe de nome de campo, enquanto a função será escolhida se for usada a sintaxe de chamada de função. No entanto, as versões do PostgreSQL anteriores a 11 sempre escolhiam a interpretação do nome do campo, a menos que a sintaxe da chamada exigisse que fosse uma chamada de função. Uma maneira de forçar a interpretação da função em versões mais antigas era qualificar o nome da função pelo esquema, ou seja, escrever esquema.função(valor_composto).

8.16.6. Sintaxe de entrada e saída dos tipos de dados compostos #

A representação textual externa de um valor composto consiste em itens interpretados segundo as regras de conversão de E/S para os tipos de dados de campo individuais, além de um adorno que indica a estrutura composta. O adorno consiste em parênteses (( e )) ao redor de todo o valor, mais vírgulas (,) entre os itens adjacentes. O espaço em branco fora dos parênteses é ignorado, mas dentro dos parênteses é considerado parte do valor do campo e pode ou não ser significativo dependendo das regras de conversão de entrada para o tipo de dados do campo. Por exemplo, em

'(  42)'

o espaço em branco será ignorado se o tipo de dados do campo for inteiro, mas não se for de texto.

Conforme foi mostrado anteriormente, ao escrever um valor composto podem ser escritas aspas em torno de qualquer valor de campo individual. Isso deve ser feito se o valor do campo confundir o analisador de valor composto. Em particular, os campos que contêm parênteses, vírgulas, aspas ou contrabarras devem estar entre aspas. Para colocar aspas ou contrabarra em um valor de campo composto entre aspas, este caractere deve ser precedido por uma contrabarra. (Além disso, um par de aspas dentro de um valor de campo entre aspas é usado para representar um caractere aspas, de forma análoga às regras para apóstrofos em literais cadeias de caracteres no SQL.) Como alternativa, as aspas podem ser evitadas e usado o escape de contrabarra para proteger todos os caracteres de dados que, de outra forma, seriam considerados sendo sintaxe de tipo de dados composto.

Um valor de campo inteiramente vazio (sem caracteres entre as vírgulas ou parênteses) representa um NULL. Para escrever um valor que seja uma cadeia de caracteres vazia em vez de NULL, deve ser escrito "".

A rotina de saída de tipo de dados composto coloca aspas nos valores de campo se forem cadeias de caracteres vazias, ou contiverem parênteses, vírgulas, aspas, contrabarras ou espaços em branco. (Fazer isso para espaços em branco não é essencial, mas ajuda na legibilidade.) Aspas e contrabarras incorporadas a valores de campo são duplicadas.

Nota

Lembre-se de que o que se escreve em um comando SQL é interpretado primeiro como literal cadeia de caracteres e, depois, como tipo de dados composto. Isso dobra o número de contrabarras necessárias (assumindo que esteja sendo usada a sintaxe de escape na cadeia de caracteres). Por exemplo, para inserir um campo text contendo aspas e contrabarra em um valor composto, é necessário escrever:

INSERT ... VALUES ('("\"\\")');

O processador de literal cadeia de caracteres remove um nível de contrabarras, de modo que o que chega ao analisador de valor composto se parece com ("\"\\"). Por sua vez, a cadeia de caracteres alimentada para a rotina de entrada do tipo de dados text se torna "\. (Se estivéssemos trabalhando com um tipo de dados cuja rotina de entrada também tratasse as contrabarras de forma especial, o tipo de dados bytea por exemplo, poderíamos precisar de até oito contrabarras no comando para obter uma contrabarra no campo composto armazenado.) Pode ser usada a delimitação por cifrão (veja a Seção 4.1.2.4) para evitar a necessidade de dobrar as contrabarras.

Dica

Geralmente é mais fácil trabalhar com a sintaxe do construtor ROW do que com a sintaxe de literal composto ao escrever valores compostos em comandos SQL. Na sintaxe ROW, valores de campo individuais são escritos da mesma forma como são escritos quando não são membros de um valor composto.