5.11. Herança #

5.11.1. Advertências

O PostgreSQL implementa a herança de tabelas, que pode ser uma ferramenta útil para projetistas de banco de dados. (O SQL:1999 e posteriores definem um recurso de herança de tipo, que difere em muitos aspectos dos recursos descritos aqui.)

Vamos começar com um exemplo: suponha que estamos tentando construir um padrão de dados para cidades. Cada estado tem muitas cidades, mas apenas uma é a capital. Queremos conseguir buscar rapidamente a capital de qualquer estado em particular. Isso pode ser feito criando duas tabelas, uma para as capitais dos estados e outra para as cidades que não são capitais. Entretanto, o que acontece quando queremos buscar dados sobre uma cidade, independentemente de ser uma capital ou não? O recurso de herança pode ajudar a resolver esse problema. Definimos a tabela capitais de forma que ela herde de cidades:

CREATE TABLE cidades (
    nome        text,
    populacao   float,
    altitude    int     -- (em pés)
);

CREATE TABLE capitais (
    estado       char(2)
) INHERITS (cidades);

Neste caso, a tabela capitais herda todas as colunas de sua tabela-mãe cidades. As capitais dos estados possuem uma coluna extra, estado, mostrando a qual estado pertencem.

No PostgreSQL, uma tabela pode herdar de zero ou mais outras tabelas, e uma consulta pode referenciar todas as linhas da tabela, ou todas as linhas da tabela mais todas as linhas de suas tabelas descendentes. O último comportamento é o padrão. Por exemplo, a consulta a seguir encontra os nomes de todas as cidades, incluindo as capitais dos estados, localizadas a uma altitude acima de 500 pés:

SELECT nome, altitude
    FROM cidades
    WHERE altitude > 500;

Usando os dados de exemplo do tutorial do PostgreSQL (veja a Seção 2.1), esta consulta retorna:

   nome    | altitude
-----------+----------
 Las Vegas |     2174
 Mariposa  |     1953
 Madison   |      845
(3 linhas)

Por outro lado, a consulta a seguir encontra todas as cidades que não são capitais de estado e estão situadas a uma altitude acima de 500 pés:

SELECT nome, altitude
    FROM ONLY cidades
    WHERE altitude > 500;

   nome    | altitude
-----------+----------
 Las Vegas |     2174
 Mariposa  |     1953
(2 linhas)

Aqui, a palavra-chave ONLY indica que a consulta deve ser aplicada apenas à tabela cidades, e não a quaisquer tabelas abaixo de cidades na hierarquia de herança. Muitos comandos que já discutimos — SELECT, UPDATE e DELETE — aceitam a palavra-chave ONLY.

O nome da tabela também pode ser escrito com * à direita para especificar, explicitamente, que as tabelas descendentes devem ser incluídas:

SELECT nome, altitude
    FROM cidades*
    WHERE altitude > 500;

Não é necessário escrever *, porque este comportamento é sempre o padrão. Entretanto, esta sintaxe ainda tem suporte para compatibilidade com versões mais antigas em que o padrão pode ser alterado.

Em alguns casos se deseja saber de qual tabela uma determinada linha se originou. Existe uma coluna do sistema chamada tableoid, em cada tabela, que pode informar a tabela de origem da linha:

SELECT c.tableoid, c.nome, c.altitude
FROM cidades c
WHERE c.altitude > 500;

que retorna:

 tableoid |   nome    | altitude
----------+-----------+----------
    16647 | Las Vegas |     2174
    16647 | Mariposa  |     1953
    16652 | Madison   |      845
(3 linhas)

(Se você tentar reproduzir este exemplo, com certeza obterá números diferentes para os OIDs.) Fazendo a junção da tabela cidades com a tabela pg_class podem ser obtidos os nomes das tabelas:

SELECT p.relname, c.nome, c.altitude
FROM cidades c, pg_class p
WHERE c.altitude > 500 AND c.tableoid = p.oid;

que retorna:

 relname  |   nome    | altitude
----------+-----------+----------
 cidades  | Las Vegas |     2174
 cidades  | Mariposa  |     1953
 capitais | Madison   |      845
(3 linhas)

Outra maneira de obter o mesmo resultado é usando o tipo alias para OID regclass, que mostra textualmente o OID da tabela ao se fazer a conversão de tipo de dados:

SELECT c.tableoid::regclass, c.nome, c.altitude
FROM cidades c
WHERE c.altitude > 500;

que também retorna:

 tableoid |   nome    | altitude
----------+-----------+----------
 cidades  | Las Vegas |     2174
 cidades  | Mariposa  |     1953
 capitais | Madison   |      845
(3 linhas)

A herança não propaga automaticamente os dados dos comandos INSERT e COPY para outras tabelas na hierarquia de herança. No nosso exemplo, o comando INSERT abaixo irá falhar:

INSERT INTO cidades (nome, populacao, altitude, estado)
VALUES ('Albany', NULL, NULL, 'NY');
ERRO:  não existe a coluna "populacao" na relação "cidades"
LINHA 1: INSERT INTO cidades (nome, populacao, altitude, estado)
                                    ^

Podemos esperar que os dados sejam de alguma forma roteados para a tabela capitais, mas isso não acontece: o comando INSERT sempre insere exatamente na tabela especificada. Em alguns casos é possível redirecionar a inserção usando uma regra (veja o Capítulo 39). No entanto, isso não ajuda no caso acima, porque a tabela cidades não contém a coluna estado, portanto o comando será rejeitado antes que a regra possa ser aplicada.

Todas as restrições de verificação e de não nulo em uma tabela-mãe são herdadas automaticamente por suas tabelas-filhas, a menos que seja especificado explicitamente de outra forma com cláusulas NO INHERIT. Outros tipos de restrições (restrições de unicidade, de chave primária e de chave estrangeira) não são herdadas.

Uma tabela pode herdar de mais de uma tabela, recebendo nesse caso a união das colunas definidas nas tabelas-mães. Quaisquer colunas declaradas na definição da tabela-filha são adicionadas a ela. Se o mesmo nome de coluna aparecer em várias tabelas-mães, ou em uma tabela-mãe e na definição da tabela-filha, estas colunas são mescladas para haver apenas uma destas colunas na tabela-filha. Para serem mescladas, as colunas devem ter os mesmos tipos de dados, senão será gerado um erro. As restrições de verificação herdáveis e as restrições de não nulo são mescladas de maneira semelhante. Assim, por exemplo, uma coluna mesclada será marcada como não nula se qualquer uma das definições de coluna de onde veio estiver marcada como não nula. As restrições de verificação são mescladas se tiverem o mesmo nome e a mesclagem falhará se suas condições forem diferentes.

A herança de tabela é normalmente estabelecida quando a tabela-filha é criada usando a cláusula INHERITS do comando CREATE TABLE. Como alternativa, uma tabela já definida de forma compatível pode ter um novo relacionamento mãe adicionado, usando a forma alternativa INHERIT do comando ALTER TABLE. Para fazer isso, a nova tabela-filha já deve ter colunas com os mesmos nomes e tipos de dados das colunas da tabela-mãe. Também deve incluir restrições de verificação com os mesmos nomes e expressões de verificação da tabela-mãe. Da mesma forma, uma ligação de herança pode ser removida de uma filha usando a forma alternativa NO INHERIT de ALTER TABLE. Adicionar e remover dinamicamente ligações de herança como esta pode ser útil quando o relacionamento de herança está sendo usado para particionamento de tabela (veja a Seção 5.12).

Uma forma conveniente para criar uma tabela compatível que mais tarde se tornará uma tabela-filha é usando a cláusula LIKE no comando CREATE TABLE. Isso cria uma tabela com as mesmas colunas da tabela de origem. Se houver alguma restrição CHECK definida na tabela de origem, deve ser especificada a opção INCLUDING CONSTRAINTS da cláusula LIKE, porque a nova filha deve ter restrições que correspondam às da mãe para ser considerada compatível.

Uma tabela-mãe não pode ser excluída enquanto qualquer uma de suas filhas ainda existir. As colunas ou restrições de verificação das tabelas-filhas também não podem ser excluídas ou alteradas se forem herdadas de qualquer tabela-mãe. Se for desejado excluir uma tabela juntamente com todas as suas tabelas descendentes, uma maneira fácil é excluir a tabela-mãe usando a opção CASCADE (veja Seção 5.15).

O comando ALTER TABLE propaga quaisquer alterações nas definições de dados da coluna e verifica as restrições na hierarquia de herança. Novamente, excluir colunas que dependem de outras tabelas só é possível usando a opção CASCADE. O comando ALTER TABLE segue as mesmas regras para mesclagem e rejeição de colunas duplicadas aplicadas ao executar CREATE TABLE.

As consultas herdadas executam verificações de permissão de acesso apenas na tabela-mãe. Assim, por exemplo, conceder permissão UPDATE na tabela cidades implica na permissão para atualizar as linhas da tabela capitais também, quando são acessadas por meio de cidades. Isso preserva a aparência de que os dados estão (também) na tabela-mãe. Mas a tabela capitais não pode ser atualizada diretamente sem uma concessão adicional. De maneira semelhante, as políticas de segurança de linha da tabela-mãe (veja a Seção 5.9) são aplicadas a linhas provenientes de tabelas-filhas durante uma consulta herdada. As políticas de uma tabela-filha, se houver, são aplicadas somente quando a tabela é explicitamente referenciada na consulta; e, nesse caso, quaisquer políticas vinculadas à(s) mãe(s) serão ignoradas.

As tabelas estrangeiras (veja a Seção 5.13) também podem fazer parte de hierarquias de herança, como tabelas mãe ou filha, da mesma forma que as tabelas regulares. Se uma tabela estrangeira fizer parte de uma hierarquia de herança, quaisquer operações sem suporte pela tabela estrangeira também não terão suporte em toda a hierarquia.

5.11.1. Advertências #

Note que nem todos os comandos SQL funcionam em hierarquias de herança. Comandos usados para consulta de dados, modificação de dados ou modificação de esquema (por exemplo, SELECT, UPDATE, DELETE, a maioria das formas alternativas de ALTER TABLE, mas não INSERT ou ALTER TABLE ... RENAME), incluem normalmente as tabelas-filhas e aceitam a notação ONLY para excluí-las. A maioria dos comandos que realizam manutenção e otimização de banco de dados (por exemplo, REINDEX) operam apenas em tabelas físicas individuais e não têm suporte para recursão em hierarquias de herança. Entretanto, os comandos VACUUM e ANALYZE incluem por padrão as tabelas-filhas e têm suporte para a notação ONLY para permitir que as tabelas-filhas não sejam incluídas. O comportamento individual de cada comando está documentado na sua página de referência (veja SQL Commands).

Uma limitação séria do recurso de herança, é que os índices (incluindo restrições de unicidade) e restrições de chave estrangeira só se aplicam a uma única tabela, não sendo aplicados às suas filhas na hierarquia de herança. Isto é verdade tanto no lado que faz a referência, quanto no lado que é referenciado, em uma restrição de chave estrangeira. Assim, nos termos do exemplo acima:

  • Se declararmos cidades.nome como sendo UNIQUE ou PRIMARY KEY, isto não vai impedir que a tabela capitais tenha linhas com nomes duplicados com relação à tabela cidades. E estas linhas duplicadas são mostradas, por padrão, nas consultas de cidades. Na verdade, por padrão, capitais não teria nenhuma restrição de unicidade, portanto poderia conter várias linhas com o mesmo nome de capital. É possível adicionar uma restrição de unicidade a capitais, mas isto não vai impedir a existência de linhas duplicadas com relação a cidades.

  • Da mesma forma, se declararmos que cidades.nome REFERENCES alguma outra tabela, esta restrição não vai se propagar automaticamente para capitais. Neste caso, isto pode ser contornado adicionando manualmente a mesma restrição REFERENCES à tabela capitais.

  • Especificar que uma coluna de outra tabela REFERENCES cidades(nome), permitirá que a outra tabela tenha os nomes das cidades, mas não os nomes das capitais. Não existe uma boa solução para este caso.

Algumas funcionalidades não implementadas para hierarquias de herança são implementadas para particionamento declarativo. É necessário um cuidado considerável ao decidir se o particionamento com herança legada é útil para sua aplicação.