Implementando o subtipo de um subtipo no padrão de design de tipo / subtipo com subclasses mutuamente exclusivas

20

Introdução

Para que essa pergunta seja útil para futuros leitores, usarei o modelo de dados genéricos para ilustrar o problema que enfrentar.

Nosso modelo de dados consiste em 3 entidades, que devem ser rotuladas como A, Be C. Para manter as coisas simples, todos os seus atributos serão do inttipo.

Entidade Atem os seguintes atributos: D, Ee X;

Entidade Btem os seguintes atributos: D, Ee Y;

A entidade Cpossui os seguintes atributos: De 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 Ae Bainda tem outro atributo comum E, mas esse atributo não está presente na entidade C.

Questão:

Gostaria de usar a característica descrita acima para otimizar ainda mais meu design, se possível.

Para ser honesto, não tenho idéia de como fazer isso, nem por onde começar a tentar, daí este post.

AlwaysLearningNewStuff
fonte

Respostas:

6

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? Armazenamento? O modelo de objeto? Questão de complexidade? Query performance? Existem trade-offs na otimização de um aspecto versus outro, pois você não pode otimizar todos os aspectos ao mesmo tempo.

Concordo plenamente com os pontos de Remus em relação a:

  • Existem prós e contras em cada abordagem (ou seja, o sempre presente fator "depende") e
  • A primeira prioridade é a eficiência do modelo de dados (um modelo de dados ineficiente não pode ser corrigido por um código de aplicativo limpo e / ou eficiente)

Dito isto, a escolha que você enfrenta é entre os seguintes, organizados em ordem de menor normalização para mais normalização:

  • promovendo propriedade Epara a tabela de tipo base
  • mantendo-o em várias tabelas de subtipo
  • normalizar totalmente Epara uma nova tabela de subclasses intermediária no mesmo nível que C, Ae Bserá diretamente subclasses de ( resposta da @ MDCCL )

Vejamos cada opção:

Mover propriedade Epara a tabela de tipo base

PROs

  • Complexidade consulta reduzida para consultas que necessidade E, mas não X, You Z.
  • Potencialmente mais eficiente para consultas que precisam E, mas não X, You Z(consultas especialmente agregados) devido a não JOIN.
  • Potencial para criar um índice (D, E)(e, em caso afirmativo, potencialmente, um Índice Filtrado em (D, E)que EntityType <> C, se tal condição for permitida)

CONs

  • Não pode marcar EcomoNOT NULL
  • Precisa de mais CHECK CONSTRAINTna tabela do tipo base para garantir que, E IS NULLquando EntityType = C(embora esse não seja um grande problema)
  • É necessário educar os usuários do modelo de dados sobre o porquê Edeve ser NULLe deve ser totalmente ignorado quando EntityType = C.
  • Um pouco menos eficiente quando Eé um tipo de comprimento fixo, e uma grande parte das linhas é para EntityType de C(ou seja, não está sendo usado, Eportanto NULL) e não está usando a SPARSEopção na coluna ou Compactação de Dados no Índice Clusterizado
  • Potencialmente menos eficiente para consultas que não precisam, Euma vez que a presença Ena tabela de tipo base aumentará o tamanho de cada linha, o que diminui o número de linhas que podem caber em uma página de dados. Mas isso é altamente dependente do tipo de dados exato de E, FILLFACTOR, quantas linhas existem na tabela de tipo base etc.

Manter propriedade Eem cada tabela de subtipo

PROs

  • Modelo de dados mais limpo (ou seja, não precisa se preocupar em educar os outros sobre o motivo pelo qual a coluna Ena tabela de tipo base não deve ser usada porque "realmente não existe")
  • Provavelmente se parece mais com o modelo de objeto
  • Pode marcar a coluna como NOT NULLse esta fosse uma propriedade necessária da entidade
  • Não há necessidade de extra CHECK CONSTRAINTna tabela de tipo base para garantir que E IS NULLquando EntityType = C(embora isso não seja um grande ganho)

CONs

  • Requer JOIN para subtipo Tabela (s) para obter essa propriedade
  • Potencialmente um pouco menos eficiente quando necessário E, devido a JOIN, dependendo de quantas linhas de A+ Bvocê possui, em oposição a quantas linhas Cexistem.
  • Um pouco mais difícil / complexo para operações que lidam apenas com entidades Ae B(e não C ) como sendo do mesmo "tipo". Obviamente, você pode abstrair isso por meio de uma Visualização que faz um UNION ALLentre uma SELECTdas tabelas JOINed de Ae outra SELECTdas tabelas JOINed de B. Isso vai reduzir a complexidade de consultas SELECT, mas não tão útil para INSERTe UPDATEconsultas.
  • Dependendo das consultas específicas e da frequência com que são executadas, isso pode ser uma ineficiência potencial nos casos em que ter um índice (D, E)realmente ajudaria uma ou mais consultas usadas com frequência, pois elas não podem ser indexadas juntas.

Normalize Epara tabela intermediária entre classe base e A&B

(Observe que eu gosto da resposta do @ MDCCL como uma alternativa viável, dependendo das circunstâncias. O que se segue não se destina a ser uma crítica estrita a essa abordagem, mas como um meio de adicionar alguma perspectiva - a minha, é claro - avaliando no mesmo contexto das duas opções que eu já havia proposto, o que tornará mais fácil esclarecer o que considero a diferença relativa entre normalização total e a abordagem atual da normalização parcial.)

PROs

  • o modelo de dados é totalmente normalizado (não pode haver nada de errado com isso, pois é para isso que os RDBMSs são projetados)
  • complexidade de consulta reduzida para consultas que precisam Ae B, mas não C(ou seja, não há necessidade de duas consultas unidas via UNION ALL)

CONs

  • um pouco mais de espaço ocupado (a Bartabela duplica o ID e há uma nova coluna BarTypeCode) [insignificante, mas algo para estar ciente]
  • ligeiro aumento na complexidade consulta como um adicional JOINé necessário para obter a qualquer AouB
  • aumento da área de superfície para bloqueio, principalmente ativada INSERT( DELETEpode ser manipulada implicitamente através da marcação de Chaves estrangeiras como ON CASCADE DELETE), pois a transação será mantida aberta um pouco mais na tabela da classe base (ie Foo) [insignificante, mas algo para estar ciente]
  • nenhum conhecimento direto do tipo real - Aou B- na tabela da classe base Foo; só conhece o tipo Brque pode ser Aou B:

    Ou seja, se você precisar fazer consultas sobre as informações gerais de base, mas precisar categorizar pelo tipo de entidade ou filtrar um ou mais tipos de entidade, a tabela da classe base não terá informações suficientes; nesse caso, você precisará LEFT JOINa Barmesa Isso também reduzirá a eficácia da indexação da FooTypeCodecoluna.

  • nenhuma abordagem consistente para interagir com o A& Bvs C:

    Ou seja, se cada entidade se relacionar diretamente com a tabela da classe base, de modo que haja apenas um JOIN para obter a entidade completa, todos poderão criar uma familiaridade 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 para desenvolver e menos propensos a ter bugs. Uma abordagem consistente também torna mais rápido e fácil adicionar novos subtipos no futuro.

  • potencialmente menos adaptável às regras de negócios que mudam com o tempo:

    Ou seja, as coisas sempre mudam e é bastante fácil passar Epara a Tabela da classe base se isso se tornar comum a todos os subtipos. Também é fácil mover uma propriedade comum para os subtipos se as mudanças na natureza das entidades fizerem uma mudança que valha a pena. É fácil o suficiente dividir um subtipo em dois subtipos (basta criar outro SubTypeIDvalor) ou combinar dois ou mais subtipos em um. Por outro lado, e se Emais tarde se tornar uma propriedade comum de todos os subtipos? Então a camada intermediária da Bartabela não teria sentido e a complexidade adicionada não valeria a pena. Obviamente, é impossível saber se tal mudança aconteceria em 5 ou 10 anos, portanto a Bartabela não é necessariamente, nem mesmo com grande probabilidade de ser uma má ideia (foi por isso que disse " potencialmente menos adaptável"). Estes são apenas pontos a considerar; é uma aposta em qualquer direção.

  • agrupamento potencialmente inadequado:

    Ou seja, apenas porque a Epropriedade é compartilhada entre tipos de entidade Ae Bnão significa isso Ae B deve ser agrupada. Só porque as coisas "parecem" as mesmas (ou seja, mesmas propriedades) não significa que são as mesmas.

Sumário

Assim como decidir se / quando desnormalizar, a melhor maneira 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:

  • quantas linhas você terá para cada EntityType (procure pelo menos 5 anos no futuro, assumindo um crescimento acima da média)
  • quantos GB cada uma dessas tabelas (tipo base e subtipos) terá em 5 anos?
  • que tipo de dados específico é propriedade E
  • é apenas uma propriedade ou existem algumas ou mesmo várias propriedades
  • de quais consultas você precisará Ee com que frequência elas serão executadas
  • de que consultas você precisará, que não precisam Ee com que frequência elas serão executadas

Eu acho que tenho como padrão manter Eas tabelas de subtipo separadas porque é, no mínimo, "mais limpo". Eu consideraria mudar Epara a tabela do tipo base SE: a maioria das linhas não era para EntityType of C; e o número de linhas era pelo menos na casa dos milhões; e eu, na maioria das vezes, as consultas executadas que precisavam Ee / ou as que se beneficiariam de um índice (D, E)são executadas com muita frequência e / ou exigem recursos suficientes do sistema, de modo que ter o índice reduz a utilização geral de recursos ou, pelo menos, impede surtos no consumo de recursos que ultrapassam níveis aceitáveis ​​ou duram o suficiente para causar bloqueio excessivo e / ou aumento de impasses.


ATUALIZAR

O OP comentou sobre esta resposta que:

Meus empregadores mudaram a lógica do negócio, removendo E completamente!

Esta mudança é particularmente importante porque é exatamente o que eu predicado pode acontecer no "contras" subseção da "Normalizar Epara Tabela intermediário entre base-classe e A& B" acima (6º ponto). A questão específica é quão fácil / difícil é refatorar o modelo de dados quando essas alterações ocorrem (e sempre acontecem). Alguns argumentam que qualquer modelo de dados pode ser refatorado / alterado, então comece com o ideal. Mas, embora seja verdade, em nível técnico, que qualquer coisa possa 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 definindo prioridades nos projetos porque esses recursos são muito limitados. E muitas vezes (pelo menos na minha experiência), projetos para obter eficiência (mesmo o desempenho do sistema, além de um 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 a longo prazo dos projetos de refatoração, é da natureza dos negócios que os profissionais menos técnicos têm mais facilidade em ver a relação direta entre novas funcionalidades e novas receita. O que isso se resume é: "voltaremos para corrigir 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 e / ou você tenha uma janela de manutenção que seja longa o suficiente para não apenas fazer as alterações, mas também reverter se algo ocorrer errado, a normalização Epara uma Tabela intermediária entre a tabela da classe base e as tabelas A& Bsubclasse poderia funcionar (embora isso ainda não o deixe com conhecimento direto do tipo específico ( AouB) 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 precisa ser testado quando as alterações são feitas), geralmente vale a pena ser mais pragmático do que idealista. E esse é 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 do gerenciamento, mas às vezes do resto da equipe - a fazer alterações devido à quantidade de desenvolvimento e desenvolvimento. Recursos de controle de qualidade que precisariam ser alocados.

Portanto, mais uma vez, a abordagem "melhor" só pode ser determinada situação por situação: você precisa conhecer seu sistema (ou seja, quantos dados e como todas as tabelas e códigos se relacionam), como realizar a refatoração e as pessoas com quem você trabalha (sua equipe e possivelmente a gerência - você pode obter o apoio deles para esse projeto?). Há algumas mudanças que eu mencionei e planejei por 1 a 2 anos e precisei de vários sprints / lançamentos para obter talvez 85% deles implementados. Mas se você tiver apenas <1 milhão de linhas e não houver muito código associado a essas tabelas, provavelmente poderá começar do lado mais ideal / "puro".

Lembre-se de que, da maneira 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 parecesse a melhor ideia da época (o que significa que você também precisa se permitir aceitar estragar tudo - todos nós fazemos - para que você possa avaliar honestamente os pontos de dor ) 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 :-).

Solomon Rutzky
fonte
17

De acordo com Martin Fowler, existem 3 abordagens para o problema da herança de tabelas:

  • Herança de tabela única : uma tabela representa todos os tipos. Atributos não utilizados são NULLed.
  • Herança concreta da tabela : uma tabela por tipo concreto, cada coluna da tabela para cada atributo do tipo. Nenhuma relação entre tabelas.
  • Herança de tabela de classe : uma tabela por tipo, cada tabela possui atributos apenas para atributos novos, não herdados. As tabelas são relacionadas, refletindo a hierarquia real de herança de tipo.

Você pode começar com eles como ponto de partida para procurar prós e contras de cada abordagem. A essência disso é que todas as abordagens têm grandes desvantagens, e nenhuma tem nenhuma vantagem esmagadora. Mais conhecido como incompatibilidade de impedância relacional do objeto , esse problema ainda está para encontrar uma solução.

Pessoalmente, acho que os tipos de problemas que um projeto relacional ruim pode levar são ordens de magnitude mais sérias do que o tipo de problemas decorrentes de um projeto de tipo ruim . O design incorreto do banco de dados leva a consultas lentas, anomalias de atualização, explosão do tamanho dos dados, bloqueios e aplicativos que não respondem e dezenas a centenas de Gigabytes de dados afundados no formato errado . O design de tipo incorreto dificulta a manutenção e a atualização do código , não o tempo de execução. Portanto, em meu livro, o design relacional correto supera qualquer pureza do tipo OO repetidamente.

Remus Rusanu
fonte
@AlwaysLearningNewStuff Acho que esta pergunta é um acompanhamento de dba.stackexchange.com/questions/139092 , correto? Na implementação lá você fazer tem herança de tabela.
Remus Rusanu
Sim, antes de fazer essa pergunta, eu queria ter certeza de que entendi corretamente como implementar o design de tipo / subtipo primeiro. Agora, enfrento o problema descrito acima quando algumas subclasses (mas não todas!) Possuem atributos compartilhados. Eu queria saber se há algo que posso fazer para modelo de dados otimizar nesse caso, em vez de desconsiderar que nuance ...
AlwaysLearningNewStuff
6

De acordo com minha interpretação de suas especificações, você deseja encontrar um método para implementar duas estruturas de supertipo-subtipo diferentes (mas conectadas ) .

Para expor uma abordagem para alcançar a tarefa mencionada, adicionarei ao cenário em questão os dois tipos clássicos de entidades hipotéticas denominados Fooe Bar, que detalharei a seguir.

Regras do negócio

Aqui estão algumas instruçõ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ê pode baixá-lo do Dropbox como um PDF também):

Figura 1 - Modelo de dados de relações hipotéticas supertipo-subtipo

A adição de Foo e Bar

Não adicionei Fooe Barfiz o modelo parecer melhor, mas para torná-lo mais expressivo. Considero que são importantes devido ao seguinte:

  • Como Ae Bcompartilha o atributo nomeado E, esse recurso sugere que eles são tipos de subentidade de um tipo distinto (mas relacionado) de conceito , evento , pessoa , medida etc., que eu representei por meio do Bartipo de superentidade que, por sua vez, é um tipo de subentidade de Foo, que mantém o Datributo na parte superior da hierarquia.

  • Como Capenas compartilha um atributo com o restante dos tipos de entidade em discussão, ou seja, Desse aspecto insinua que é um tipo de subentidade de outro tipo de conceito , evento , pessoa , medida , etc., então eu descrevi essa circunstância em virtude de o Footipo de super entidade.

No entanto, essas são apenas suposições e, como um banco de dados relacional deve refletir com precisão a semântica de um determinado contexto de negócios , é necessário identificar e classificar todas as coisas de interesse em seu domínio específico, para que você possa captar com mais precisão .

Fatores importantes na fase de projeto

É bastante útil estar ciente do fato de que, deixando toda a terminologia de lado, um cluster exclusivo de supertipo-subtipo é um relacionamento comum. Vamos descrever a situação da seguinte maneira:

  • Cada ocorrência de tipo de superentidade exclusiva está relacionada a apenas um complemento de tipo de subentidade .

Assim, há uma correspondência (ou cardinalidade) de um para um (1: 1) nesses casos.

Como você sabe nas postagens anteriores, o atributo discriminador (coluna, quando implementado) desempenha um papel primordial ao criar uma associação dessa natureza, porque indica a instância correta do subtipo ao qual o supertipo está conectado . A migração da CHAVE PRIMÁRIA de (i) o supertipo para (ii) os subtipos também é de importância primordial.

Estrutura DDL em concreto

E então eu escrevi uma estrutura DDL baseada no modelo lógico apresentado acima:

CREATE TABLE FooType -- Look-up table.
(
    FooTypeCode     CHAR(2)  NOT NULL,
    Description     CHAR(90) NOT NULL, 
    CreatedDateTime DATETIME NOT NULL,
    CONSTRAINT PK_FooType             PRIMARY KEY (FooTypeCode),
    CONSTRAINT AK_FooType_Description UNIQUE      (Description)
);

CREATE TABLE Foo -- Supertype
(
    FooId           INT      NOT NULL, -- This PK migrates (1) to ‘Bar’ as ‘BarId’, (2) to ‘A’ as ‘AId’, (3) to ‘B’ as ‘BId’, and (4) to ‘C’ as ‘CId’.
    FooTypeCode     CHAR(2)  NOT NULL, -- Discriminator column.
    D               INT      NOT NULL, -- Column that applies to ‘Bar’ (and therefore to ‘A’ and ‘B’) and ‘C’.
    CreatedDateTime DATETIME NOT NULL,
    CONSTRAINT PK_Foo                 PRIMARY KEY (FooId),
    CONSTRAINT FK_from_Foo_to_FooType FOREIGN KEY (FooTypeCode)
        REFERENCES FooType (FooTypeCode)
);

CREATE TABLE BarType -- Look-up table.
(
    BarTypeCode CHAR(1)  NOT NULL,  
    Description CHAR(90) NOT NULL,  
    CONSTRAINT PK_BarType             PRIMARY KEY (BarTypeCode),
    CONSTRAINT AK_BarType_Description UNIQUE      (Description)
);

CREATE TABLE Bar -- Subtype of ‘Foo’.
(
    BarId       INT     NOT NULL, -- PK and FK.
    BarTypeCode CHAR(1) NOT NULL, -- Discriminator column. 
    E           INT     NOT NULL, -- Column that applies to ‘A’ and ‘B’.
    CONSTRAINT PK_Bar             PRIMARY KEY (BarId),
    CONSTRAINT FK_from_Bar_to_Foo FOREIGN KEY (BarId)
        REFERENCES Foo (FooId),
    CONSTRAINT FK_from_Bar_to_BarType FOREIGN KEY (BarTypeCode)
        REFERENCES BarType (BarTypeCode)    
);

CREATE TABLE A -- Subtype of ‘Bar’.
(
    AId INT NOT NULL, -- PK and FK.
    X   INT NOT NULL, -- Particular column.  
    CONSTRAINT PK_A             PRIMARY KEY (AId),
    CONSTRAINT FK_from_A_to_Bar FOREIGN KEY (AId)
        REFERENCES Bar (BarId)  
);

CREATE TABLE B -- (1) Subtype of ‘Bar’ and (2) supertype of ‘A’ and ‘B’.
(
    BId INT NOT NULL, -- PK and FK.
    Y   INT NOT NULL, -- Particular column.  
    CONSTRAINT PK_B             PRIMARY KEY (BId),
    CONSTRAINT FK_from_B_to_Bar FOREIGN KEY (BId)
        REFERENCES Bar (BarId)  
);

CREATE TABLE C -- Subtype of ‘Foo’.
(
    CId INT NOT NULL, -- PK and FK.
    Z   INT NOT NULL, -- Particular column.  
    CONSTRAINT PK_C             PRIMARY KEY (CId),
    CONSTRAINT FK_from_C_to_Foo FOREIGN KEY (FooId)
        REFERENCES Foo (FooId)  
);

Com essa estrutura, você evita o armazenamento de marcas NULL em suas tabelas (ou relações ) de base, o que introduziria ambiguidade em sua base de dados.

Integridade, consistência e outras considerações

Depois de implementar seu banco de dados, você deve garantir que (a) cada linha de supertipo exclusivo seja sempre complementada por sua contraparte de subtipo correspondente e, por sua vez, garanta que (b) essa linha de subtipo seja compatível com o valor contido na coluna discriminadora de supertipo . Portanto, é bastante conveniente empregar o ACID TRANSACTIONSpara garantir que essas condições sejam atendidas no seu banco de dados.

Você não deve abandonar a solidez lógica, a auto-expressividade e a precisão do seu banco de dados, esses são aspectos que decididamente tornam seu banco de dados mais sólido.

As duas respostas postadas anteriormente já incluem pontos pertinentes que certamente valem a pena levar em consideração ao projetar, criar e gerenciar seu banco de dados e seus programas aplicativos.

Recuperando dados por meio das definições de VIEW

Você pode configurar algumas visualizações que combinam colunas dos diferentes grupos de supertipos e subtipos , para recuperar os dados em mãos sem, por exemplo, escrever as cláusulas JOIN necessárias sempre. Dessa forma, você pode SELECIONAR diretamente DA VISTA (uma relação ou tabela derivada ) de interesse com facilidade.

Como você pode ver, "Ted" Codd foi, sem dúvida, um gênio. As ferramentas que ele legou são bastante fortes e elegantes e, é claro, estão bem integradas entre si.

Recursos relacionados

Se você deseja analisar algum banco de dados extenso que envolva relacionamentos supertipo-subtipo, você achará valiosas as respostas extraordinárias propostas por @PerformanceDBA para as seguintes perguntas sobre estouro de pilha:


Nota

1. A 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 ( NIST ) dos Estados Unidos . É solidamente baseado em (a) o material teórico inicial criado pelo Dr. EF Codd; em (b) a entidade-relacionamento vista de dados, desenvolvido por Dr. PP Chen ; e também (c) a Logical Database Design Technique, criada por Robert G. Brown. Vale ressaltar que o IDEF1X foi formalizado por meio de lógica de primeira ordem.

MDCCL
fonte
Meus empregadores mudaram a lógica do negócio, removendo Ecompletamente! A razão para aceitar a resposta do usuário srutzky é porque ela fornece bons pontos que me ajudam a tomar minha decisão de escolher a rota mais eficiente. Se não fosse por isso, eu aceitaria sua resposta. Eu votei sua resposta mais cedo. Obrigado novamente!
AlwaysLearningNewStuff