Introdução
Para que esta questão seja útil para futuros leitores, usarei o modelo de dados genérico para ilustrar o problema que enfrento.
Nosso modelo de dados consiste em 3 entidades, que serão rotuladas como A
, B
e C
. Para manter as coisas simples, todos os seus atributos serão do int
tipo.
A entidade A
possui os seguintes atributos: D
, E
e X
;
A entidade B
possui os seguintes atributos: D
, E
e Y
;
A entidade C
possui os seguintes atributos: D
e Z
;
Como todas as entidades compartilham atributos comuns D
, decidi aplicar o design de tipo/subtipo .
Importante: As entidades são mutuamente exclusivas! Isso significa que a entidade é A ou B ou C.
Problema:
Entidades A
e B
possuem ainda outro atributo comum E
, mas este atributo não está presente na entidade C
.
Pergunta:
Gostaria de usar a característica descrita acima para otimizar ainda mais meu design, se possível.
Para ser honesto, não tenho ideia de como fazer isso, nem por onde começar a tentar, daí este post.
De acordo com Martin Fowler, existem 3 abordagens para o problema de herança de tabelas:
Você pode começar com eles como ponto de partida para pesquisar os prós e os contras de cada abordagem. A essência disso é que todas as abordagens têm grandes desvantagens e nenhuma tem qualquer vantagem esmagadora. Mais conhecido como incompatibilidade de impedância relacional de objeto , esse problema ainda não encontrou uma solução.
Pessoalmente, acho que o tipo de problema que um design relacional ruim pode causar é muito mais sério do que o tipo de problema decorrente de um design de tipo ruim . O design incorreto do banco de dados leva a consultas lentas, anomalias de atualização, explosão de tamanho de dados, bloqueios e aplicativos que não respondem e dezenas a centenas de Gigabytes de dados afundados no formato errado . O design de tipo ruim dificulta a manutenção e atualização do código , não o tempo de execução. Portanto, em meu livro, o design relacional correto supera qualquer tipo de pureza OO repetidamente.
Na medida em que esta pergunta é uma continuação de Minha implementação do padrão de design de tipo/subtipo (para subclasses mutuamente exclusivas) está correta? , que é uma continuação de Não sei como transformar entidade variável em tabela relacional , eu perguntaria: o que exatamente você está tentando otimizar? Armazenar? O modelo de objeto? Complexidade da consulta? Desempenho da consulta? Existem compensações ao otimizar um aspecto em relação a outro, pois não é possível otimizar todos os aspectos ao mesmo tempo.
Eu concordo completamente com os pontos de Remus em relação a:
Dito isso, a escolha que você enfrenta é entre as seguintes, organizadas em ordem de menos normalização para mais normalização:
E
para a tabela do tipo baseE
para uma nova tabela de subclasse intermediária no mesmo nível queC
, issoA
eB
serão diretamente subclasses de ( resposta de @MDCCL )Vejamos cada opção:
Mover propriedade
E
para tabela do tipo basePROs
E
mas nãoX
,Y
ouZ
.E
mas nãoX
,Y
ouZ
(especialmente consultas agregadas) devido a nenhum JOIN.(D, E)
(e, em caso afirmativo, potencialmente um índice filtrado em(D, E)
que EntityType <>C
, se tal condição for permitida)CONs
E
comoNOT NULL
CHECK CONSTRAINT
na tabela de tipo base para garantir queE IS NULL
quando EntityType =C
(embora isso não seja um grande problema)E
deve serNULL
, e deve até mesmo ser totalmente ignorado, quando EntityType =C
.E
é um tipo de comprimento fixo e uma grande parte das linhas são para EntityType deC
(ou seja, não usandoE
, portanto, éNULL
) e não usando aSPARSE
opção na coluna ou compactação de dados no índice agrupadoE
pois a presença deE
na tabela do tipo base aumentará o tamanho de cada linha que, por sua vez, diminuirá o número de linhas que podem caber em uma página de dados. Mas isso é altamente dependente do tipo de dados exato deE
, o FILLFACTOR, quantas linhas existem na tabela de tipo base, etc.Mantenha a propriedade
E
em cada tabela de subtipoPROs
E
na tabela do tipo base não deve ser usada porque "ela realmente não está lá")NOT NULL
se esta fosse uma propriedade obrigatória da entidadeCHECK CONSTRAINT
na tabela de tipo base para garantir queE IS NULL
quando EntityType =C
(embora isso não seja um ganho enorme)CONs
E
de , devido ao JOIN, dependendo de quantas linhas deA
+B
você tem em oposição a quantas linhasC
existem.A
eB
(e nãoC
) como sendo do mesmo “tipo”. Claro, você pode abstrair isso por meio de uma View que faz umUNION ALL
entre umaSELECT
das tabelas JOINed paraA
e outraSELECT
das tabelas JOINed paraB
. Isso reduzirá a complexidade das consultas SELECT, mas não será tão útil para as consultasINSERT
and .UPDATE
(D, E)
ativado realmente ajudaria uma ou mais consultas usadas com frequência, pois elas não podem ser indexadas juntas.Normalize
E
para a tabela intermediária entre a classe base eA
&B
(Observe que eu gosto da resposta de @MDCCL como uma alternativa viável, dependendo das circunstâncias. O que se segue não é uma crítica estrita a essa abordagem, mas um meio de adicionar alguma perspectiva - a minha, é claro - avaliando no mesmo contexto das duas opções que já havia proposto. Isso tornará mais fácil esclarecer o que vejo como a diferença relativa entre a normalização total e a abordagem atual de normalização parcial.)
PROs
A
eB
, mas nãoC
(ou seja, não há necessidade de duas consultas unidas viaUNION ALL
)CONs
Bar
tabela duplica o ID e há uma nova coluna,BarTypeCode
) [insignificante, mas algo para estar ciente]JOIN
é necessário um adicional para chegar a umA
ouB
INSERT
(DELETE
pode ser manipulado implicitamente por meio da marcação de chaves estrangeiras comoON CASCADE DELETE
), pois a transação será mantida aberta um pouco mais na tabela de classe base (ou sejaFoo
) [insignificante, mas algo para estar ciente]nenhum conhecimento direto do tipo real --
A
ouB
-- dentro da tabela da classe base,Foo
; ele só conhece o tipoBr
que pode serA
ouB
:Ou seja, se você precisar fazer consultas sobre as informações básicas gerais, mas precisar categorizar pelo tipo de entidade ou filtrar um ou mais tipos de entidade, a tabela de classe base não possui informações suficientes; nesse caso, você precisa
LEFT JOIN
aBar
mesa. Isso também reduzirá a eficácia da indexação daFooTypeCode
coluna.nenhuma abordagem consistente para interagir com
A
&B
vsC
:Ou seja, se cada entidade se relacionar diretamente com a tabela de classe base, de modo que haja apenas um JOIN para obter a entidade completa, todos poderão criar familiaridade de maneira mais rápida e fácil em termos de trabalho com o modelo de dados. Haverá uma abordagem comum para consultas/procedimentos armazenados que os torna mais rápidos de desenvolver e menos propensos a ter erros. Uma abordagem consistente também torna mais rápido e fácil adicionar novos subtipos no futuro.
potencialmente menos adaptável a regras de negócios que mudam com o tempo:
Ou seja, as coisas sempre mudam e é bastante fácil mover
E
-se para a tabela de classe base se ela se tornar comum a todos os subtipos. Também é bastante fácil mover uma propriedade comum para os subtipos se mudanças na natureza das entidades fizerem com que essa mudança valha a pena. É fácil dividir um subtipo em dois subtipos (basta criar outroSubTypeID
valor) ou combinar dois ou mais subtipos em um. Por outro lado, e seE
mais tarde se tornasse uma propriedade comum de todos os subtipos? Então a camada intermediária daBar
mesa não teria sentido e a complexidade adicional não valeria a pena. Claro, é impossível saber se tal mudança aconteceria em 5 ou até 10 anos, então aBar
tabela não é necessariamente, nem mesmo altamente provável de ser, uma má ideia (é por isso que eu disse " potencialmente menos adaptável"). Esses são apenas pontos a serem considerados; é uma aposta em qualquer direção.agrupamento potencialmente inapropriado:
Ou seja, apenas porque a
E
propriedade é compartilhada entre os tipos de entidadeA
eB
não significa queA
devaB
ser agrupada. Só porque as coisas "parecem" as mesmas (isto é, as mesmas propriedades) não significa que sejam as mesmas.Resumo
Assim como decidir se/quando desnormalizar, a melhor forma de abordar essa situação específica depende de considerar os seguintes aspectos do uso do modelo de dados e garantir que os benefícios superem os custos:
E
E
e com que frequência elas serão executadasE
e com que frequência elas serão executadasAcho que costumo manter por padrão
E
as tabelas de subtipos separadas porque é, no mínimo, "mais limpa". Eu consideraria mudarE
para a tabela de tipo base IF: a maioria das linhas não era para EntityType deC
; e o número de linhas era pelo menos na casa dos milhões; e , na maioria das vezes, executei consultas que precisavamE
e/ou as consultas que se beneficiariam de um índice(D, E)
ou executam com muita frequência e/ou exigem recursos de sistema suficientes, de modo que ter o índice reduz a utilização geral de recursos ou, pelo menos, evita surtos no consumo de recursos que ultrapassam os níveis aceitáveis ou duram o suficiente para causar bloqueio excessivo e/ou aumento de deadlocks.ATUALIZAR
OP comentou sobre esta resposta que:
Essa alteração é particularmente importante porque é exatamente o que previ que poderia acontecer na subseção "CONs" da seção "Normalizar
E
para a tabela intermediária entre a classe base eA
&B
" acima (6º marcador). A questão específica é quão fácil/difícil é refatorar o modelo de dados quando essas mudanças acontecem (e sempre acontecem). Alguns argumentarão que qualquer modelo de dados pode ser refatorado/alterado, então comece com o ideal. Mas embora seja verdade em um nível técnico que qualquer coisa pode ser refatorada, a realidade da situação é uma questão de escala.Os recursos não são infinitos, não apenas CPU/Disco/RAM, mas também recursos de desenvolvimento: tempo e dinheiro. As empresas estão constantemente estabelecendo prioridades em projetos porque esses recursos são muito limitados. E muitas vezes (pelo menos na minha experiência), os projetos para ganhar eficiência (tanto o desempenho do sistema quanto o desenvolvimento mais rápido / menos bugs) são priorizados abaixo dos projetos que aumentam a funcionalidade. Embora seja frustrante para nós, pessoal técnico, porque entendemos quais são os benefícios de longo prazo dos projetos de refatoração, é apenas a natureza do negócio que o pessoal menos técnico e de negócios tenha mais facilidade em ver a relação direta entre novas funcionalidades e novos receita. O que isso significa é: "voltaremos para consertar isso mais tarde" == "
Com isso em mente, se o tamanho dos dados for pequeno o suficiente para que as alterações possam ser feitas muito consulta e/ou você tiver uma janela de manutenção longa o suficiente para não apenas fazer as alterações, mas também reverter se algo der errado errado, então normalizar
E
para uma tabela intermediária entre a tabela de classe base e as tabelasA
deB
subclasse pode funcionar (embora isso ainda o deixe sem conhecimento direto do tipo específico (A
ouB
) na tabela da classe base). MAS, se você tiver centenas de milhões de linhas nessas tabelas e uma quantidade incrível de código referenciando as tabelas (código que deve ser testado quando as alterações são feitas), geralmente vale a pena ser mais pragmático do que idealista. E este é o ambiente com o qual tive que lidar por anos: 987 milhões de linhas e 615 GB na tabela de classe base, espalhados por 18 servidores. E tanto código atingiu essas tabelas (tabelas de classe base e subclasse) que houve muita resistência -- principalmente da gerência, mas às vezes do resto da equipe -- para fazer qualquer alteração devido à quantidade de desenvolvimento e Recursos de controle de qualidade que precisariam ser alocados.Portanto, mais uma vez, a "melhor" abordagem só pode ser determinada situação por situação: você precisa conhecer seu sistema (ou seja, quantos dados e como as tabelas e o código se relacionam), como realizar a refatoração e as pessoas com quem você trabalha (sua equipe e possivelmente a gerência - você pode obter a adesão deles para esse projeto?). Há algumas mudanças que mencionei e planejei por 1 a 2 anos e fiz vários sprints / lançamentos para obter talvez 85% deles implementados. Mas se você tiver apenas < 1 milhão de linhas e não muito código vinculado a essas tabelas, provavelmente poderá começar do lado mais ideal / "puro" das coisas.
Apenas lembre-se, seja qual for o caminho que você escolher, preste atenção em como esse modelo funciona nos próximos 2 anos, pelo menos (se possível). Preste atenção no que funcionou e no que causou dor, mesmo que pareça a melhor ideia no momento (o que significa que você também precisa se permitir aceitar estragar tudo - todos nós aceitamos - para que você possa avaliar honestamente os pontos problemáticos ). E preste atenção ao motivo pelo qual certas decisões funcionaram ou não, para que você possa tomar decisões com maior probabilidade de serem "melhores" da próxima vez :-).
De acordo com minha interpretação de suas especificações, você deseja encontrar um método para implementar duas estruturas supertipo-subtipo diferentes (mas conectadas ) .
A fim de expor uma abordagem para realizar a tarefa supracitada, adicionarei ao cenário em questão os dois tipos clássicos de entidade hipotética
Foo
chamados eBar
, que detalharei a seguir.Regras do negócio
Aqui estão algumas declarações que me ajudarão a criar um modelo lógico:
A Foo is either one Bar or one C
A Foo is categorized by one FooType
A Bar is either one A or one C
A Bar is classified by one BarType
modelo lógico
E então, o modelo lógico IDEF1X [1] resultante é mostrado na Figura 1 (e você também pode baixá-lo do Dropbox como um PDF ):
A adição de Foo e Bar
I did not add
Foo
andBar
to make the model look better, but to make it more expressive. I deem they are important due to the following:As
A
andB
share the attribute namedE
, this feature suggests that they are subentity types of a distinct (but related) sort of concept, event, person, measurement, etc., which I represented by means of theBar
superentity type that, in turn, is a subentity type ofFoo
, which holds theD
attribute at the top of the hierarchy.Since
C
only shares one attribute with the rest of the entity types under discussion, i.e.,D
, this aspect insinuates that it is a subentity type of another kind of concept, event, person, measurement, etc., so I depicted this circumstance by virtue of theFoo
super entity type.However, these are just assumptions, and since a relational database is meant to reflect the semantics of a certain business context accurately, you have to identify and classify all the things of interest in your specific domain so that you can, precisely, capture more meaning.
Important factors at the design phase
It is quite useful to be aware of the fact that, putting all the terminology aside, an exclusive supertype-subtype cluster is an ordinary relationship. Let us describe the situation in the following way:
Thus, there is a correspondance (or cardinality) of one-to-one (1:1) in these cases.
As you know from your preceding posts, the discriminator attribute (column, when implemented) plays a paramount role when creating an association of this nature, because it indicates the correct subtype instance with which the supertype is connected. The migration of the PRIMARY KEY from (i) the supertype to (ii) the subtypes is also of prime significance.
Concrete DDL structure
And then I wrote a DDL structure that is based on the logical model presented above:
With this structure you avoid the storage of NULL marks in your base tables (or relations), which would introduce ambiguity to your data base.
Integrity, consistency and other considerations
Once you are implementing your database, you must ensure that (a) each exclusive supertype row is always complemented by its corresponding subtype counterpart and, in turn, guarantee that (b) such subtype row is compatible with the value contained in the supertype discriminator column. Therefore, it is quite convenient to employ ACID
TRANSACTIONS
in order to make sure that these conditions are met in your database.You should not give up the logical soundness, self-expressivity and accuracy of your database, these are aspects that decidedly make your database more solid.
The two previously posted answers already include pertinent points that are certainly worth taking into account when designing, creating and managing your database and its application program(s).
Retrieving data by way of VIEW definitions
You can set up some views that combine columns of the different supertype-subtype groups, so that you can retrieve the data at hand without, e.g., writing the necessary JOIN clauses every time. In this way, you can SELECT directly FROM the VIEW (a derived relation or table) of interest with ease.
As you can see, “Ted” Codd was, undoubtedly, a genius. The tools he bequeathed are quite strong and elegant, and, of course, are well integrated with each other.
Related resources
If you want to analyze some extensive database which involves supertype-subtype relationships, you would find of value the extraordinary answers proposed by @PerformanceDBA to the following Stack Overflow questions:
Historical / auditable database.
[O]ne table or many for many different but interacting events?, which comprises nested subtypes.
Note
1. Definição de Integração para Modelagem de Informações ( IDEF1X ) é uma técnica de modelagem de dados altamente recomendável que foi estabelecida como padrão em dezembro de 1993 pelo Instituto Nacional de Padrões e Tecnologia dos Estados Unidos ( NIST ). É solidamente baseado em (a) o material teórico inicial de autoria do Dr. EF Codd; em (b) a visão Entidade-Relacionamento de dados, desenvolvida pelo Dr. PP Chen ; e também sobre (c) a Logical Database Design Technique, criada por Robert G. Brown. Vale a pena notar que o IDEF1X foi formalizado por meio da lógica de primeira ordem.