Design do banco de dados: como lidar com o problema do “arquivamento”?

18

Tenho certeza de que muitos aplicativos, aplicativos críticos, bancos e outros fazem isso diariamente.

A ideia por trás de tudo o que é:

  • todas as linhas devem ter um histórico
  • todos os links devem permanecer coerentes
  • deve ser fácil fazer solicitações para obter colunas "atuais"
  • os clientes que compraram itens obsoletos ainda devem ver o que compraram, mesmo que este produto não faça mais parte do catálogo

e assim por diante.

Aqui está o que eu quero fazer e vou explicar os problemas que estou enfrentando.

Todas as minhas tabelas terão essas colunas:

  • id
  • id_origin
  • date of creation
  • start date of validity
  • start end of validity

E aqui estão as idéias para operações CRUD:

  • create = insere nova linha com id_origin= id, date of creation= agora, start date of validity= agora, end date of validity= nulo (= significa que é o registro ativo atual)
  • update =
    • read = lê todos os registros com end date of validity== null
    • atualize o registro "atual" end date of validity= null com end date of validity= now
    • crie um novo com os novos valores e end date of validity= null (= significa que é o registro ativo atual)
  • delete = atualize o registro "atual" end date of validity= null com end date of validity= now

Então, aqui está o meu problema: com associações muitos-para-muitos. Vamos dar um exemplo com valores:

  • Tabela A (id = 1, id_origin = 1, start = now, end = null)
  • Tabela A_B (início = agora, final = nulo, id_A = 1, id_B = 48)
  • Tabela B (id = 48, id_origin = 48, start = now, end = null)

Agora eu quero atualizar a tabela A, ID do registro = 1

  • Marque a identificação do registro = 1 com final = agora
  • Eu insiro um novo valor na tabela A e ... caramba, perdi minha relação A_B, a menos que eu duplique a relação também ... isso terminaria em uma tabela:

  • Tabela A (id = 1, id_origin = 1, start = now, end = now + 8mn)

  • Tabela A (id = 2, id_origin = 1, start = now + 8mn, end = null)
  • Tabela A_B (início = agora, final = nulo, id_A = 1, id_B = 48)
  • Tabela A_B (início = agora, final = nulo, id_A = 2, id_B = 48)
  • Tabela B (id = 48, id_origin = 48, start = now, end = null)

E ... bem, eu tenho outro problema: a relação A_B: devo marcar (id_A = 1, id_B = 48) como obsoleta ou não (A - id = 1 é obsoleta, mas não B - 48)?

Como lidar com isso?

Eu tenho que projetar isso em grande escala: produtos, parceiros e assim por diante.

Qual a sua experiência nisso? Como você faria (como você fez)?

- Editar

Encontrei este artigo muito interessante , mas ele não lida adequadamente com "obsolescência de casos" (= o que estou perguntando realmente)

Olivier Pons
fonte
Que tal copiar os dados do registro de atualização antes de serem atualizados para um novo com um novo ID, mantendo a lista vinculada do histórico com o campo id_hist_prev. Então, o id do registro atual não é alterado
Em vez de reinventar a roda, você já pensou em usar, por exemplo, o Flashback Data Archive no Oracle?
Jack Douglas

Respostas:

4

Não está claro para mim se esses requisitos são para fins de auditoria ou apenas uma referência histórica simples, como CRM e carrinhos de compras.

De qualquer maneira, considere ter uma tabela main e main_archive para cada área principal em que isso é necessário. "Main" terá apenas entradas atuais / ativas, enquanto "main_archive" terá uma cópia de tudo o que entra em main. Inserir / atualizar em main_archive pode ser um gatilho de inserir / atualizar em main. As exclusões no arquivo main_archive podem ser executadas por um longo período de tempo, se houver.

Para problemas referenciais, como o Cust X comprou o Produto Y, a maneira mais fácil de resolver sua preocupação referencial de cust_archive -> product_archive é nunca excluir as entradas do product_archive. Geralmente, a rotatividade deve ser muito menor nessa tabela, para que o tamanho não seja uma preocupação muito ruim.

HTH.


fonte
2
Ótima resposta, mas gostaria de acrescentar que outro benefício de ter uma tabela de arquivamento é que elas tendem a ser desnormalizadas, tornando os relatórios sobre esses dados muito mais eficientes. Considere também as necessidades de relatórios do seu aplicativo com essa abordagem.
Maple_shaft
1
Na maioria dos bancos de dados que eu design, todas as tabelas 'principais' têm um prefixo do nome do produto LP_, e todas as tabelas importantes têm um equivalente LH_, com gatilhos inserindo linhas históricas na inserção, atualização e exclusão. Não funciona para todos os casos, mas tem sido um modelo sólido para as coisas que faço.
Concordo - se a maioria das consultas for para as linhas "atuais", você provavelmente obterá uma vantagem de desempenho particionando a corrente do histórico em duas tabelas. Uma visão poderia uni-los novamente, como uma conveniência. Dessa forma, as páginas de dados com linhas atuais estão todas juntas e provavelmente permanecem melhor no cache, e você não precisa qualificar constantemente as consultas para dados atuais com lógica de data.
Onupdatecascade
1
@onupdatecascade: Observe que (pelo menos em alguns RDBMSs) você pode colocar índices nessa UNIONexibição, o que permite fazer coisas legais, como impor uma restrição única nos registros atuais e históricos.
Jon of All Trades
5 anos depois, eu fiz muitas coisas e sempre lhe dava sua ideia. A única coisa que mudei é que, nas tabelas de histórico, tenho as colunas " id" e " id_ref". id_refé uma referência à idéia real da tabela. Exemplo: persone person_h. em person_heu tenho " id" e " id_ref" onde id_refestá relacionado a ' person.id' para que eu possa ter muitas linhas com a mesma person.id(= quando uma linha de personé modificada) e todas idas minhas tabelas são autoinc.
Olivier Pons
2

Isso tem alguma sobreposição com a programação funcional; especificamente o conceito de imutabilidade.

Você tem uma tabela chamada PRODUCTe outra chamada PRODUCTVERSIONou similar. Quando você altera um produto, não faz uma atualização, basta inserir uma nova PRODUCTVERSIONlinha. Para obter as informações mais recentes, você pode indexar a tabela pelo número da versão (desc), carimbo de data / hora (desc) ou ter um sinalizador ( LatestVersion).

Agora, se você tem algo que faz referência a um produto, pode decidir para qual tabela ele aponta. Aponta para a PRODUCTentidade (sempre refere-se a este produto) ou para a PRODUCTVERSIONentidade (refere-se apenas a esta versão do produto)?

Isso fica complicado. E se você tiver fotos do produto? Eles precisam apontar para a tabela de versões, porque eles podem ser alterados, mas em muitos casos, eles não serão e você não deseja duplicar dados desnecessariamente. Isso significa que você precisa de uma PICTUREtabela e um relacionamento PRODUCTVERSIONPICTUREmuitos-para-muitos.


fonte
1

Eu implementei todas as coisas daqui com 4 campos que estão em todas as minhas tabelas:

  • Eu iria
  • date_creation
  • date_validity_start
  • date_validity_end

Cada vez que um registro precisa ser modificado, eu o duplico, marque o registro duplicado como "antigo" = date_validity_end=NOW()e o atual como o bom date_validity_start=NOW()e date_validity_end=NULL.

O truque é sobre as relações de muitos para muitos e de um para muitos: funciona sem tocá-los! É tudo sobre as consultas que são mais complexas: para consultar um registro em uma data precisa (= agora não), tenho para cada associação e para a tabela principal adicionar essas restrições:

WHERE (
  (date_validity_start<=:dateparam AND date_validity_end IS NULL)
  OR
  (date_validity_start<=:dateparam AND date_validity_start>=:dateparam)
)

Assim, com produtos e atributos (muitos para muitos):

SELECT p.*,a.*

FROM products p

JOIN products_attributes pa
ON pa.id_product = p.id
AND (
  (pa.date_validity_start<=:dateparam AND pa.date_validity_end IS NULL)
  OR
  (pa.date_validity_start<=:dateparam AND pa.date_validity_start>=:dateparam)
)

JOIN attributes a
ON a.id = pa.id_attribute
AND (
  (a.date_validity_start<=:dateparam AND a.date_validity_end IS NULL)
  OR
  (a.date_validity_start<=:dateparam AND a.date_validity_start>=:dateparam)
)

WHERE (
  (p.date_validity_start<=:dateparam AND p.date_validity_end IS NULL)
  OR
  (p.date_validity_start<=:dateparam AND p.date_validity_start>=:dateparam)
)
Olivier Pons
fonte
0

Que tal agora? Parece simples e bastante eficaz para o que fiz no passado. Na tabela "histórico", use um PK diferente. Portanto, o seu campo "CustomerID" é o PK na tabela Customer, mas na tabela "history", seu PK é "NewCustomerID". "CustomerID" se torna apenas outro campo somente leitura. Isso deixa "CustomerID" inalterado no Histórico e todos os seus relacionamentos permanecem intactos.

Dimondwoof
fonte
Idéia muito boa. O que fiz é muito semelhante: duplico o registro e marco o novo como "obsoleto", para que o registro atual ainda seja o mesmo. Note que eu queria criar um gatilho em cada tabela, mas o mysql proíbe modificações de uma tabela quando você estiver em um gatilho dessa tabela. O PostGRESQL faz isso. Servidor SQL faça isso. Oracle faça isso. Em resumo, o MySQL ainda tem um longo caminho a percorrer, e da próxima vez pensarei duas vezes ao escolher meu servidor de banco de dados.
Olivier Pons