Como você pode representar uma herança em um banco de dados?

236

Estou pensando em como representar uma estrutura complexa em um banco de dados SQL Server.

Considere um aplicativo que precisa armazenar detalhes de uma família de objetos, que compartilham alguns atributos, mas que muitos outros não são comuns. Por exemplo, um pacote de seguro comercial pode incluir cobertura de responsabilidade, automóvel, propriedade e indenização no mesmo registro de apólice.

É trivial implementá-lo em C #, etc, pois você pode criar uma Política com uma coleção de Seções, em que Seção é herdada conforme necessário para os vários tipos de capa. No entanto, os bancos de dados relacionais não parecem permitir isso facilmente.

Percebo que existem duas opções principais:

  1. Crie uma tabela Política e, em seguida, uma tabela Seções, com todos os campos necessários para todas as variações possíveis, a maioria das quais seria nula.

  2. Crie uma tabela de políticas e várias tabelas de seção, uma para cada tipo de capa.

Ambas as alternativas parecem insatisfatórias, especialmente porque é necessário escrever consultas em todas as seções, o que envolveria inúmeras junções ou verificações nulas.

Qual é a melhor prática para esse cenário?

Steve Jones
fonte

Respostas:

430

@Bill Karwin descreve três modelos de herança em seu livro SQL Antipatterns , ao propor soluções para o antipattern SQL Entity-Attribute-Value . Esta é uma breve visão geral:

Herança de tabela única (também conhecida como tabela por herança de hierarquia):

Usar uma única tabela como na sua primeira opção é provavelmente o design mais simples. Como você mencionou, muitos atributos específicos ao subtipo deverão receber um NULLvalor nas linhas em que esses atributos não se aplicam. Com este modelo, você teria uma tabela de políticas, com a seguinte aparência:

+------+---------------------+----------+----------------+------------------+
| id   | date_issued         | type     | vehicle_reg_no | property_address |
+------+---------------------+----------+----------------+------------------+
|    1 | 2010-08-20 12:00:00 | MOTOR    | 01-A-04004     | NULL             |
|    2 | 2010-08-20 13:00:00 | MOTOR    | 02-B-01010     | NULL             |
|    3 | 2010-08-20 14:00:00 | PROPERTY | NULL           | Oxford Street    |
|    4 | 2010-08-20 15:00:00 | MOTOR    | 03-C-02020     | NULL             |
+------+---------------------+----------+----------------+------------------+

\------ COMMON FIELDS -------/          \----- SUBTYPE SPECIFIC FIELDS -----/

Manter o design simples é uma vantagem, mas os principais problemas com essa abordagem são os seguintes:

  • Quando se trata de adicionar novos subtipos, você teria que alterar a tabela para acomodar os atributos que descrevem esses novos objetos. Isso pode se tornar rapidamente problemático quando você tem muitos subtipos ou se planeja adicionar subtipos regularmente.

  • O banco de dados não poderá impor quais atributos se aplicam e quais não se aplicam, pois não há metadados para definir quais atributos pertencem a quais subtipos.

  • Você também não pode impor NOT NULLatributos de um subtipo que devem ser obrigatórios. Você precisaria lidar com isso em seu aplicativo, o que geralmente não é o ideal.

Herança concreta da tabela:

Outra abordagem para lidar com a herança é criar uma nova tabela para cada subtipo, repetindo todos os atributos comuns em cada tabela. Por exemplo:

--// Table: policies_motor
+------+---------------------+----------------+
| id   | date_issued         | vehicle_reg_no |
+------+---------------------+----------------+
|    1 | 2010-08-20 12:00:00 | 01-A-04004     |
|    2 | 2010-08-20 13:00:00 | 02-B-01010     |
|    3 | 2010-08-20 15:00:00 | 03-C-02020     |
+------+---------------------+----------------+
                          
--// Table: policies_property    
+------+---------------------+------------------+
| id   | date_issued         | property_address |
+------+---------------------+------------------+
|    1 | 2010-08-20 14:00:00 | Oxford Street    |   
+------+---------------------+------------------+

Esse design basicamente resolverá os problemas identificados para o método de tabela única:

  • Agora, os atributos obrigatórios podem ser aplicados NOT NULL.

  • Adicionar um novo subtipo requer adicionar uma nova tabela em vez de adicionar colunas a uma existente.

  • Também não há risco de que um atributo inadequado seja definido para um subtipo específico, como o vehicle_reg_nocampo para uma política de propriedade.

  • Não há necessidade do typeatributo, como no método de tabela única. O tipo agora é definido pelos metadados: o nome da tabela.

No entanto, este modelo também possui algumas desvantagens:

  • Os atributos comuns são misturados com os atributos específicos do subtipo e não há uma maneira fácil de identificá-los. O banco de dados também não saberá.

  • Ao definir as tabelas, você teria que repetir os atributos comuns para cada tabela de subtipo. Definitivamente não é SECO .

  • A pesquisa de todas as políticas, independentemente do subtipo, torna-se difícil e exigiria vários UNIONs.

É assim que você precisaria consultar todas as políticas, independentemente do tipo:

SELECT     date_issued, other_common_fields, 'MOTOR' AS type
FROM       policies_motor
UNION ALL
SELECT     date_issued, other_common_fields, 'PROPERTY' AS type
FROM       policies_property;

Observe como a adição de novos subtipos exigiria que a consulta acima fosse modificada com um adicional UNION ALLpara cada subtipo. Isso pode facilmente levar a erros no seu aplicativo se esta operação for esquecida.

Herança de tabela de classe (também conhecida como tabela por herança de tipo):

Esta é a solução que @David menciona na outra resposta . Você cria uma única tabela para sua classe base, que inclui todos os atributos comuns. Em seguida, você criaria tabelas específicas para cada subtipo, cuja chave primária também serve como uma chave estrangeira para a tabela base. Exemplo:

CREATE TABLE policies (
   policy_id          int,
   date_issued        datetime,

   -- // other common attributes ...
);

CREATE TABLE policy_motor (
    policy_id         int,
    vehicle_reg_no    varchar(20),

   -- // other attributes specific to motor insurance ...

   FOREIGN KEY (policy_id) REFERENCES policies (policy_id)
);

CREATE TABLE policy_property (
    policy_id         int,
    property_address  varchar(20),

   -- // other attributes specific to property insurance ...

   FOREIGN KEY (policy_id) REFERENCES policies (policy_id)
);

Esta solução resolve os problemas identificados nos outros dois projetos:

  • Atributos obrigatórios podem ser aplicados com NOT NULL.

  • Adicionar um novo subtipo requer adicionar uma nova tabela em vez de adicionar colunas a uma existente.

  • Não há risco de que um atributo inadequado seja definido para um subtipo específico.

  • Não há necessidade do typeatributo.

  • Agora, os atributos comuns não são mais misturados aos atributos específicos do subtipo.

  • Podemos ficar secos, finalmente. Não há necessidade de repetir os atributos comuns para cada tabela de subtipo ao criar as tabelas.

  • O gerenciamento de um incremento automático idpara as políticas se torna mais fácil, porque isso pode ser tratado pela tabela base, em vez de cada tabela de subtipo gerá-las independentemente.

  • A pesquisa de todas as políticas, independentemente do subtipo, agora é muito fácil: não UNIONé necessário - apenas a SELECT * FROM policies.

Considero a abordagem da tabela de classes a mais adequada na maioria das situações.


Os nomes desses três modelos vêm do livro de Martin Fowler, Patterns of Enterprise Application Architecture .

Daniel Vassallo
fonte
97
Também estou usando esse design, mas você não menciona os inconvenientes. Especificamente: 1) você diz que não precisa do tipo; true, mas você não pode identificar o tipo real de uma linha, a menos que observe todas as tabelas de subtipos para encontrar uma correspondência. 2) É difícil manter a tabela principal e as tabelas de subtipos sincronizadas (pode-se, por exemplo, remover a linha na tabela de subtipos e não na tabela principal). 3) Você pode ter mais de um subtipo para cada linha principal. Eu uso gatilhos para contornar 1, mas 2 e 3 são problemas muito difíceis. Na verdade 3 não é um problema se você modelar a composição, mas é para herança estrita.
19
+1 no comentário do @ Tibo, esse é um problema grave. Na verdade, a herança de tabela de classes produz um esquema não normalizado. Onde, como a herança da tabela concreta, não, e eu não concordo com o argumento de que a herança da tabela concreta impede o DRY. O SQL dificulta o DRY, porque não possui recursos de metaprogramação. A solução é usar um Database Toolkit (ou escrever o seu próprio) para fazer o trabalho pesado, em vez de escrever SQL diretamente (lembre-se, na verdade, é apenas uma linguagem de interface do banco de dados). Afinal, você também não escreve seu aplicativo corporativo em montagem.
Jo Então,
18
@Tibo, sobre o ponto 3, você pode usar a abordagem explicada aqui: sqlteam.com/article/… , verifique a seção Modelando restrições uma a uma .
Andrew
4
@DanielVassallo Em primeiro lugar, obrigado pela resposta impressionante, duvido que uma pessoa tenha uma política. Como saber se o seu policy_motor ou policy_property? Uma maneira é pesquisar policyId em todas as sub-tabelas, mas acho que esse é o caminho errado, não é? Qual deve ser a abordagem correta?
precisa saber é o seguinte
11
Eu realmente gosto da sua terceira opção. No entanto, estou confuso como o SELECT funcionará. Se você selecionar as políticas * FROM, receberá de volta os IDs das políticas, mas ainda não saberá a qual tabela de subtipos a política pertence. Você ainda não precisará fazer um JOIN com todos os subtipos para obter todos os detalhes da política?
Adam
14

A terceira opção é criar uma tabela "Política" e, em seguida, uma tabela "SectionsMain" que armazena todos os campos comuns aos tipos de seção. Em seguida, crie outras tabelas para cada tipo de seção que contenha apenas os campos que não são comuns.

Decidir qual é o melhor depende principalmente de quantos campos você possui e como deseja gravar seu SQL. Todos eles trabalhariam. Se você tiver apenas alguns campos, provavelmente eu usaria o número 1. Com "lotes" de campos, eu me inclinaria para o # 2 ou # 3.

David
fonte
+1: 3ª opção é o mais próximo do modelo de herança, e mais normalizado IMO
RedFilter
Sua opção 3 é realmente exatamente o que eu quis dizer com opção 2. Existem muitos campos e algumas seções também teriam entidades filhas.
Steve Jones
9

Com as informações fornecidas, eu modelaria o banco de dados para ter o seguinte:

POLÍTICAS

  • POLICY_ID (chave primária)

PASSIVOS

  • LIABILITY_ID (chave primária)
  • POLICY_ID (chave estrangeira)

PROPRIEDADES

  • PROPERTY_ID (chave primária)
  • POLICY_ID (chave estrangeira)

... e assim por diante, porque eu esperaria que houvesse atributos diferentes associados a cada seção da política. Caso contrário, poderia haver uma única SECTIONStabela e, além da policy_id, haveria uma section_type_code...

De qualquer forma, isso permitiria o suporte de seções opcionais por política ...

Não entendo o que você acha insatisfatório sobre essa abordagem - é assim que você armazena dados enquanto mantém a integridade referencial e não duplica os dados. O termo é "normalizado" ...

Como o SQL é baseado em SET, é um pouco estranho aos conceitos de programação processual / OO e exige que o código faça a transição de um domínio para outro. ORMs são frequentemente considerados, mas não funcionam bem em sistemas complexos e de alto volume.

Pôneis OMG
fonte
Sim, eu entendi a normalização ;-) Para uma estrutura tão complexa, com algumas seções simples e outras com sua própria subestrutura complexa, parece improvável que um ORM funcione, embora seja bom.
26710 Steve
6

Além disso, na solução Daniel Vassallo, se você usa o SQL Server 2016 ou superior, existe outra solução que eu usei em alguns casos sem perda considerável de desempenho.

Você pode criar apenas uma tabela com apenas o campo comum e adicionar uma única coluna com a sequência JSON que contém todos os campos específicos do subtipo.

Testei esse design para gerenciar herança e estou muito feliz com a flexibilidade que posso usar no aplicativo relativo.

vencedor
fonte
1
Essa é uma ideia interessante. Ainda não usei o JSON no SQL Server, mas use-o muito em outros lugares. Obrigado pela atenção.
Steve Jones
5

A outra maneira de fazer isso é usar o INHERITScomponente Por exemplo:

CREATE TABLE person (
    id int ,
    name varchar(20),
    CONSTRAINT pessoa_pkey PRIMARY KEY (id)
);

CREATE TABLE natural_person (
    social_security_number varchar(11),
    CONSTRAINT pessoaf_pkey PRIMARY KEY (id)
) INHERITS (person);


CREATE TABLE juridical_person (
    tin_number varchar(14),
    CONSTRAINT pessoaj_pkey PRIMARY KEY (id)
) INHERITS (person);

Assim, é possível definir uma herança entre tabelas.

Marco Paulo Ollivier
fonte
Outros bancos de dados são compatíveis INHERITSalém do PostgreSQL ? MySQL por exemplo?
Giannis christofakis
1
@giannischristofakis: O MySQL é apenas um banco de dados relacional, enquanto o Postgres é um banco de dados objeto-relacional. Portanto, o MySQL não suporta isso. Na verdade, acho que o Postgres é o único DBMS atual que suporta esse tipo de herança.
a_horse_with_no_name
2
@ marco-paulo-ollivier, a pergunta do OP é sobre o SQL Server, então não entendo por que você fornece uma solução que funciona apenas com o Postgres. Obviamente, não abordando o problema.
mapto
@ mapto esta questão se tornou uma espécie de "como se faz uma herança de estilo OO em um banco de dados"; que era originalmente sobre servidor sql é agora irrelevante
Caius Jard
0

Eu me inclino para o método nº 1 (uma tabela unificada de seções), a fim de recuperar com eficiência diretivas inteiras com todas as suas seções (o que eu assumo que seu sistema estará fazendo muito).

Além disso, não sei qual versão do SQL Server você está usando, mas em 2008+ Sparse Columns ajuda a otimizar o desempenho em situações em que muitos dos valores em uma coluna serão NULL.

Por fim, você terá que decidir o quão "semelhantes" são as seções de política. A menos que eles diferem substancialmente, acho que uma solução mais normalizada pode ser mais problemática do que vale a pena ... mas apenas você pode fazer essa ligação. :)

Dan J
fonte
Haverá informações demais para apresentar toda a Política de uma só vez, portanto nunca seria necessário recuperar o registro inteiro. Eu acho que é 2005, embora eu tenha usado o escasso de 2008 em outros projetos.
Steve Jones
De onde vem o termo "tabela de seção unificada"? O Google quase não mostra resultados e já existem termos bastante confusos aqui.
Stephan-v
-1

Como alternativa, considere o uso de bancos de dados de documentos (como o MongoDB) que oferecem suporte nativo a estruturas de dados e aninhamento avançados.

Grigori Melnik
fonte