Arquitetura de dois bancos de dados: operacional e histórica


8

Pensei em uma estrutura de banco de dados incomum e me pergunto se alguém já viu isso em uso antes. É basicamente usando 2 bancos de dados:

  • O primeiro banco de dados retém apenas os dados atualmente válidos
  • O segundo banco de dados mantém o histórico de tudo que já foi inserido, atualizado ou excluído no primeiro banco de dados

Cenário

Estou trabalhando em um projeto no qual sou obrigado a registrar tudo o que acontece e onde os dados são alterados com frequência.

Exemplo (não o real)

Você precisa fazer o design do banco de dados para uma liga de futebol. Nesta liga existem jogadores e equipes. Os jogadores geralmente trocam de time.

  • Primeiro requisito : O banco de dados deve conter as informações necessárias para jogar a próxima partida. Isso significa uma lista de todos os jogadores, equipes e em qual equipe cada jogador está atualmente.
  • Segundo requisito : O banco de dados deve conter valores históricos que usaremos para gerar estatísticas. Isso significa a lista de todos os jogadores que fizeram parte de um time ou a lista de todos os times dos quais um jogador fez parte.

O problema

Esses dois requisitos são meio opostos um ao outro. Eu tentei fazer tudo no mesmo banco de dados, mas não faz sentido. O primeiro requisito se preocupa apenas em "jogar a próxima partida", enquanto o segundo requisito se preocupa apenas em "gerar estatísticas".

Para fazer tudo no mesmo banco de dados, fui com uma espécie de banco de dados "apenas inserção" usando a óbvia exclusão macia para excluir / atualizar informações ...

O que inicialmente parecia uma tarefa fácil, mantendo uma lista de jogadores, equipes e a equipe atual de cada jogador, de repente se torna muito mais difícil. A lógica do aplicativo necessária para executar a próxima partida já é bastante complicada, mas agora o banco de dados tem um design muito inútil, no qual o aplicativo é obrigado a adicionar a verificação "é excluída" em cada consulta apenas para executar a próxima partida.

Você gostaria de ser o treinador que grita "todos os jogadores do time, venham até mim" e depois 2000 jogadores vêm até você. Nesse ponto, você provavelmente gritará "todos os jogadores que não foram excluídos do time, venham até mim" (enquanto juram sobre esse design estúpido).

Minha conclusão

Eu me perguntei por que você precisa colocar tudo no mesmo banco de dados. A exclusão reversível não apenas faz um trabalho ruim no registro de tudo, a menos que você adicione muitas colunas (time_created, who_created_it, time_deleted, who_deleted_it), mas também complica tudo. Isso complica o design do banco de dados e o design do aplicativo.

Além disso, recebo esses 2 requisitos como parte de um único aplicativo que não pode ser dividido, mas fico pensando: são dois aplicativos completamente distintos. Por que estou tentando fazer tudo juntos.

Foi quando pensei em dividir o banco de dados em dois. Um banco de dados operacional usado apenas para executar a próxima partida e conter apenas as informações atualmente válidas e um banco de dados histórico que contém todas as informações que já existiram, quando foram criadas, excluídas e quem as fez.

O objetivo é manter o primeiro banco de dados (operacional) e o aplicativo o mais simples possível, mantendo o máximo de informações possível no segundo banco de dados (histórico).

Questões

  • Você já viu esse design antes? Tem nome?
  • Há alguma armadilha óbvia que estou perdendo?



EDIT 2015-03-16

Arquitetura atual

Você pode basicamente pensar em toda a arquitetura como um processo de 2 etapas.

Passo 1 :

  • O aplicativo está sendo executado e os usuários estão executando algumas ações
  • Cada vez que um evento acontece, ele é gravado automaticamente (solução de auditoria) em uma tabela de eventos
  • Em seguida, a linha correta, no banco de dados operacional, é atualizada

Passo 2 :

  • Um trabalho lê a inserção mais recente na tabela de eventos e insere esses novos dados no banco de dados histórico.
  • Os usuários consultam o banco de dados histórico para recuperar as informações necessárias.

Apenas na tabela de eventos, você pode reconstruir as informações para qualquer ponto no tempo. O problema é que essa tabela de eventos não é facilmente consultável. É aqui que o banco de dados histórico entra em ação; apresentar os dados de maneira que seja fácil recuperar exatamente o que queremos.

Problemas adicionais ao colocar tudo nas mesmas tabelas

Eu já expressei minha preocupação com a complexidade adicional de verificar "é excluído" em cada consulta. Mas há outra questão: integridade .

Faço uso pesado de chave estrangeira e restrição para garantir que, a qualquer momento, os dados que estão no meu banco de dados sejam válidos.

Vejamos um exemplo:

Restrição: Só pode haver um goleiro por equipe.

É fácil adicionar um índice único que verifique se existe apenas um goleiro por equipe. Mas então o que acontece quando você muda o goleiro? Você ainda precisa preservar as informações sobre a anterior, mas agora você tem 2 goleiros nas mesmas equipes, uma ativa e outra inativa, o que contradiz sua restrição.

Claro que é fácil adicionar uma verificação à sua restrição, mas é outra coisa para gerenciar e pensar.


Dê uma olhada em dimensões de mudança lenta para algum padrão se aproxima
Turch

1
Vale a pena conferir: fornecimento de eventos . Uma de suas tabelas é o log de eventos, fornecendo uma trilha de auditoria completa; parte dela pode ser arquivada posteriormente. Outra tabela é o estado de resumo atual criado pela aplicação de alterações em todos os eventos. Ele pode conter pontos de verificação anteriores para permitir a restauração de apenas uma parte do fluxo de eventos.
9000

Respostas:


7

Isso acontece com bastante frequência, embora o histórico (às vezes conhecido como registros de auditoria) seja mantido na mesma tabela ou em um separado no mesmo banco de dados.

Por exemplo, eu costumava trabalhar com um sistema em que quaisquer atualizações em uma tabela seriam implementadas como uma inserção, o antigo registro 'atual' teria um sinalizador dizendo que era um registro histórico e o carimbo de data e hora quando atualizado gravado em uma coluna.

Hoje trabalho em um sistema em que todas as alterações são gravadas em uma tabela de auditoria dedicada e a atualização ocorre na tabela.

O último é mais escalável, mas não tão fácil de implementar de maneira genérica.

A maneira mais fácil de atingir seu objetivo de simplificar as consultas e não exigir a adição do sinalizador 'é atual' é permitir apenas consultas de leitura por meio de um modo de exibição ou procedimento armazenado. Em seguida, você faz uma ligação para dizer "obtenha todos os jogadores" e o processo armazenado retornará apenas os jogadores atuais (você pode implementar um segundo procedimento para retornar jogadores com mais controle sobre quais são devolvidos). Isso funciona bem para escrever também. Um procedimento armazenado para atualizar um player pode, então, escrever os detalhes do histórico necessários e atualizar o player - sem que o cliente saiba qual é o mecanismo do histórico. Por esse motivo, os procedimentos armazenados são melhores do que uma exibição que retorna apenas os players atuais, pois mantém todo o mecanismo de acesso ao banco de dados o mesmo para leitura e gravação - tudo passa por um sproc.


Atualmente, eu prefiro o mais tarde. Pode não parecer muito importante executar todas as suas consultas por meio de procedimentos ou visualizações armazenadas, mas adiciona bastante complexidade para manter todas elas. O procedimento de exibição e armazenamento também apresenta muitas limitações. blog.sqlauthority.com/2010/10/03/… stackoverflow.com/questions/921190/…
Gudradain

1
Limitações são boas, é como dizer "variáveis ​​locais em objetos me limitam a usar globais". Restringir a superfície de acesso a dados significa que é mais seguro e você precisa projetar melhor. Essas são coisas boas. Os links: visualizações são limitantes, não foram projetadas para serem tabelas de substituição. Um sproc no qual você não pode participar significa que precisa de um sproc diferente. Escrever seu SQL em sprocs não é pior do que escrevê-lo no código do lado do cliente, você o mantém perfeitamente bem para poder manter seu código sproc com a mesma facilidade.
Gbjbaanb

1
@Gudradain Estou com gbjbaanb. Basear o ódio das boas práticas de servidor do cliente em uma postagem de blog mal escrita, referente a um RDBMS e um link irrelevante para uma pergunta no estouro de pilha, onde alguém está tentando fazer algo não natural com um procedimento armazenado e não pode. Basicamente, você lê as visualizações e escreve os procs armazenados. Sim, você precisa ter um processo de controle e liberação de versão - mas você precisa disso de qualquer maneira.
Mcottle 17/03/2015

@mcottle Passar pela visualização e sproc apenas parece impraticável quando você já tem um projeto usando um ORM e uma camada de acesso a dados. Além disso, se você colocar tudo no banco de dados, ele fará um mau trabalho ao encapsular a lógica do programa. Geralmente, você tem mais de um aplicativo usando o mesmo banco de dados. Simplesmente não faz sentido ter todos esses procedimentos armazenados relacionados a muitos aplicativos diferentes no mesmo local. Se eu tivesse que adotar essa abordagem de adicionar "é excluído" em qualquer lugar, faria na minha camada de acesso a dados (no meu aplicativo).
Gudradain

@Gudradain sim, ele faz como "muitos aplicativos diferentes" estão felizes em usar as mesmas tabelas e dados. Pense em um sproc como uma forma diferente de tabela, em vez da lógica do cliente colada na camada DB. Você ainda pode chamá-los por meio do ORM e, se realmente quiser que muitos aplicativos diferentes os usem, use esquemas para isolá-los - tornando o aplicativo de banco de dados específico, o que é realmente bom para segurança e manutenção (como um aplicativo pode alterar seu API de dados sem afetar outros aplicativos). Sprocs são práticas recomendadas, o código do cliente pode esquecer de adicionar "is delete", o sproc garante isso.
precisa

4

Ao dividir um banco de dados em dois, você perderá todos os benefícios das referências relacionais e da verificação de integridade referencial. Eu nunca tentei uma coisa dessas, mas meu palpite é que isso se tornaria um grande pesadelo.

Acredito que todo o conjunto de dados que descreve um determinado sistema pertence a um único banco de dados. Questões de conveniência no acesso aos dados quase nunca são um bom motivo para tomar decisões sobre a organização dos dados.

Questões de conveniência no acesso aos seus dados devem ser tratadas, utilizando os recursos de conveniência oferecidos pelo seu RDBMS.

Portanto, em vez de ter um banco de dados 'atual' e um banco de dados 'histórico', você deve ter apenas um banco de dados e todas as tabelas nele devem ser prefixadas com 'histórico'. Em seguida, você deve criar um conjunto de visualizações, uma para cada tabela que deseja ver como 'atual', e cada uma filtrar as linhas históricas que você não deseja ver e deixar passar apenas as atuais.

Essa é uma solução adequada para o seu problema, pois utiliza um recurso de conveniência do RDBMS para tratar de um problema de conveniência do programador, deixando intacto o design do banco de dados.

Um exemplo de um problema que você provavelmente encontrará (muito tempo para um comentário)

Suponha que você esteja olhando para uma tela que mostra as informações atuais sobre um time, digamos team.id = 10, team.name = "Manchester United" e clique no botão que diz "mostrar histórico". Nesse ponto, você desejará mudar para uma tela que mostra informações históricas sobre a mesma equipe. Então, você pega o id 10, que você sabe que no banco de dados "atual" significa "Manchester United" e terá que esperaresse número de identificação 10 também significa "Manchester United" no banco de dados histórico. Não existe uma regra de integridade referencial que imponha que o ID se refira exatamente à mesma entidade nos dois bancos de dados; portanto, essencialmente, você terá dois conjuntos de dados totalmente disjuntos com conexões implícitas que são apenas conhecidas, honradas e prometidas para serem mantidas. por, código fora do banco de dados.

E é claro que isso se aplica não apenas às mesas principais, como a tabela "Times", mas também à menor mesa que você terá ao lado, como "Posições dos jogadores: atacante, meio-campista, goleiro etc."

Alcançar histórico dentro do mesmo banco de dados

Existem vários métodos para manter a historicidade e, embora estejam além do escopo desta questão, que é basicamente o que armadilhas essa idéia em particular pode ter, aqui está uma idéia:

Você pode manter uma tabela de log contendo uma entrada para cada alteração que já foi feita no banco de dados. Dessa forma, todas as tabelas "atuais" podem ser completamente limpas dos dados e totalmente reconstruídas, repetindo as alterações registradas. Obviamente, se você pode reconstruir as tabelas "atuais", reproduzindo as alterações desde o início dos tempos até agora, também pode criar um conjunto temporário de tabelas para obter uma visão do banco de dados em uma coordenada de tempo específica, reproduzindo as alterações do início do tempo até a coordenada específica do tempo.

Isso é conhecido como "Fonte de Eventos" (artigo de Martin Fowler.)


Você quer dizer referências relacionais e integridade referencial entre os 2 bancos de dados ou dentro de um banco de dados? Uso chave estrangeira e restrição em todos os lugares onde é apropriado, porque apenas os dados que respeitam essas restrições devem ser inseridos no banco de dados.
Gudradain 16/03/2015

Bem, até onde eu sei, você só pode ter relações em um único banco de dados. Pode haver RDBMSs que permitem relações entre bancos de dados, mas isso não é esperado por outros produtos e, na minha humilde opinião, nem deve ser invocado, mesmo com o produto que anuncia esse recurso.
Mike Nakis

Portanto, dividindo o banco de dados em dois bancos de dados, você poderá ter relações dentro de cada banco de dados separadamente, mas não entre os dois bancos de dados. O que pode significar um desastre.
15135 Mike Nakis

1
Os dados no banco de dados histórico são provenientes exclusivamente do banco de dados operacional e nunca há atualização / exclusão nesse banco de dados. Do meu ponto de vista, esse banco de dados nem precisa de nenhuma relação ou restrição. Simplesmente retém os dados que estavam no banco de dados operacional em algum momento. Não há manipulação nem nada para verificar sobre esses dados. As únicas coisas que importam são: ela existia no outro banco de dados (sim / não). Eu meio que não conseguem ver o que é o problema com isso ...
Gudradain

Como isso seria muito longo para um comentário, eu o adicionei como uma emenda à minha resposta.
21415 Mike Nakis

2

Primeiramente , sua base de código já oferece uma separação clara de preocupações, onde a lógica de negócios (de escolher jogadores para jogar na próxima partida) se distingue da lógica de acesso ao banco de dados (uma camada que simplesmente se conecta ao banco de dados e mapeia suas estruturas de dados nas linhas do banco de dados e vice-versa)? A resposta para isso ajudará bastante a explicar por que você está lidando com isso:

Isso complica o design do banco de dados e o design do aplicativo.

Agora...

A lógica do aplicativo necessária para executar a próxima partida já é bastante complicada, mas agora o banco de dados tem um design muito inútil, no qual o aplicativo é obrigado a adicionar a verificação "é excluída" em cada consulta apenas para executar a próxima partida.

Supondo que você esteja falando sobre RDBMS, ainda é possível ter um banco de dados bitemporal que captura todos os dados válidos passados, presentes e possivelmente futuros e , em seguida, use uma biblioteca / estrutura de ORM robusta o suficiente para lidar com a lógica de consulta de banco de dados para você. Você pode até usar uma exibição de banco de dados para ajudar na sua seleção. Em seguida, as partes da lógica comercial do seu código não precisam conhecer os campos temporais subjacentes, o que eliminará o problema descrito acima.

Por exemplo, em vez de precisar codificar uma consulta SQL no seu aplicativo:

SELECT * FROM team_players WHERE team_id = ? AND valid_from >= ? AND valid_to <= ? AND ...

(usando ?como ligações de parâmetro)

Uma biblioteca hipotética de acesso ao banco de dados pode permitir que você consulte como (pseudo-código):

dbConnection.select(Table.TEAM_PLAYERS).match(Field.TEAM_ID, <value>).during(Season.NOW)

Ou usando a abordagem de exibição de banco de dados:

-- Using MySQL dialect for illustration
CREATE VIEW seasonal_players AS SELECT * FROM team_players 
    WHERE valid_from >= ... AND valid_to <= ... AND ...

Você pode consultar jogadores durante esse período como:

SELECT * FROM seasonal_players WHERE team_id = ?

De volta ao seu modelo de banco de dados sugerido , como você pretende lidar com a remoção de dados históricos do seu banco de dados operacional? Você simplesmente cria INSERTduas linhas semelhantes nos bancos de dados operacionais e históricos e executa uma DELETEno banco de dados operacional sempre que houver uma ação do usuário para excluir dados históricos?


Pense grande

Se você estiver falando sobre processamento de dados em larga escala, em que seu 'banco de dados' é uma solução de processamento de cluster / fluxo de banco de dados distribuído em escala, sua abordagem soará vagamente semelhante (provavelmente apenas em alguns dos termos definidos) ao Lambda Arquitetura , na qual os dados 'históricos' (isto é, em tempo real) são processados ​​em lote separadamente para executar o tipo de estatística que você está procurando, e os dados 'operacionais' (ou seja, em tempo real) permanecem com capacidade de consulta. um limite predefinido antes do processamento em lote persistir. No entanto, a base dessa abordagem é impulsionada mais pelas vantagens e limitações das implementações atuais de Big Data do que pela mera simplificação da lógica da aplicação.


editar (após a edição do OP)

Eu deveria ter respondido a isso antes, mas de qualquer maneira:

Além disso, recebo esses 2 requisitos como parte de um único aplicativo que não pode ser dividido, mas fico pensando: são dois aplicativos completamente distintos. Por que estou tentando fazer tudo juntos.

Isso geralmente ocorre porque os usuários finais tendem a pensar em termos de recursos, não no número de bancos de dados necessários .

Você também menciona que:

Passo 1:

  • Cada vez que um evento acontece, ele é gravado automaticamente (solução de auditoria) em uma tabela de eventos.
  • Em seguida, a linha correta, no banco de dados operacional, é atualizada.

Passo 2:

  • Um trabalho lê a inserção mais recente na tabela de eventos e insere esses novos dados no banco de dados histórico.

Ótimo! Então agora você tem uma tabela de eventos, que presumo ser o conceito de sourcing de eventos mencionado pelas outras respostas. No entanto, o que lê sua tabela de eventos e atualiza a linha correta no banco de dados operacional ? É o mesmo que o trabalho que lê o último evento e o insere no banco de dados histórico ?

Mais um ponto em relação ao seu exemplo de restrição:

Faço uso pesado de chave estrangeira e restrição para garantir que, a qualquer momento, os dados que estão no meu banco de dados sejam válidos.

Vejamos um exemplo:

Restrição: Só pode haver um goleiro por equipe. ( ativo em campo durante um jogo, apenas uma nota lateral )

"Algum ponto no tempo " refere-se a tempo válido ou tempo de transação ?

O que acontece quando temos um terceiro novo goleiro? Você cria uma restrição exclusiva nos campos temporais no banco de dados histórico para manter os dados de dois goleiros "antigos" válidos?


Antes de inserir / atualizar / excluir qualquer coisa, eu crio um evento que contém todas as informações necessárias para reconstruir os dados nesse momento e simplesmente atualizo o banco de dados operacional. Depois, adiciono as informações do evento ao banco de dados histórico. O processo é quase sempre automático e você quase não tem nada para manter.
Gudradain 16/03/2015

1
Os eventos são criados por gatilhos diretamente nas tabelas. O evento é salvo e a linha é criada / atualizada / excluída ou nada acontece. Então isso acontece ao mesmo tempo. Dessa forma, garante que o evento seja criado e que o evento esteja tentando fazer algo válido conforme especificado pela relação e restrição no banco de dados operacional.
Gudradain 16/03/2015

1

Na verdade, é semelhante à maneira como as transações do banco de dados geralmente são implementadas, exceto que os dados históricos geralmente são descartados após serem gravados no banco de dados operacional. O padrão de programação mais próximo que consigo pensar é a fonte de eventos .

Eu acho que dividir esses dois bancos de dados é a jogada certa. Mais especificamente, eu consideraria o banco de dados "operacional" como um cache, pois os dados históricos serão suficientes para reconstruir o banco de dados operacional a qualquer momento. Dependendo da natureza do aplicativo e dos requisitos de desempenho, pode ser desnecessário manter esse cache como um banco de dados separado se for razoável reconstruir o estado atual a partir dos dados históricos na memória toda vez que o programa for iniciado.

No que diz respeito às armadilhas, o principal problema com o qual você pode se deparar é se precisar de qualquer tipo de simultaneidade (no mesmo programa ou com vários clientes usando o banco de dados ao mesmo tempo). Nesse caso, você deseja garantir que as modificações nos bancos de dados históricos e operacionais sejam feitas atomicamente. No caso de simultaneidade dentro do mesmo programa, sua melhor aposta é provavelmente algum tipo de mecanismo de bloqueio. Para vários clientes interagindo com o mesmo banco de dados, a maneira mais fácil seria manter as duas tabelas no mesmo banco de dados e usar transações para manter o banco de dados consistente.


1
A terceirização de eventos é realmente muito próxima do que tenho atualmente. Uma trilha de auditoria é gerada automaticamente a partir de cada inserção / atualização / exclusão que pode ser comparada à lista de eventos.
Gudradain 16/03/2015

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.