Conception de base de données: comment gérer le problème des «archives»?

18

Je suis sûr que beaucoup d'applications, d'applications critiques, de banques, etc. le font quotidiennement.

L'idée derrière tout cela est:

  • toutes les lignes doivent avoir un historique
  • tous les liens doivent rester cohérents
  • il devrait être facile de faire des demandes pour obtenir des colonnes "actuelles"
  • les clients qui ont acheté des objets obsolètes devraient toujours voir ce qu'ils ont acheté même si ce produit ne fait plus partie du catalogue

etc.

Voici ce que je veux faire et je vais vous expliquer les problèmes auxquels je suis confronté.

Toutes mes tables auront ces colonnes:

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

Et voici les idées pour les opérations CRUD:

  • create = insérer une nouvelle ligne avec id_origin= id, date of creation= now, start date of validity= now, end date of validity= null (= signifie que c'est l'enregistrement actif en cours)
  • mise à jour =
    • lire = lire tous les enregistrements avec end date of validity== null
    • mettre à jour l'enregistrement "actuel" end date of validity= null avec end date of validity= maintenant
    • en créer un nouveau avec les nouvelles valeurs, et end date of validity= null (= signifie qu'il s'agit de l'enregistrement actif en cours)
  • delete = mettre à jour l'enregistrement "actuel" end date of validity = null avec end date of validity= maintenant

Voici donc mon problème: avec les associations plusieurs-à-plusieurs. Prenons un exemple avec des valeurs:

  • Tableau A (id = 1, id_origin = 1, start = now, end = null)
  • Table A_B (début = maintenant, fin = null, id_A = 1, id_B = 48)
  • Tableau B (id = 48, id_origin = 48, start = now, end = null)

Maintenant, je veux mettre à jour la table A, enregistrer id = 1

  • Je marque l'enregistrement id = 1 avec fin = maintenant
  • J'insère une nouvelle valeur dans la table A et ... putain j'ai perdu ma relation A_B à moins que je ne reproduise la relation aussi ... cela finirait par une table:

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

  • Tableau A (id = 2, id_origin = 1, start = now + 8mn, end = null)
  • Table A_B (début = maintenant, fin = null, id_A = 1, id_B = 48)
  • Table A_B (début = maintenant, fin = null, id_A = 2, id_B = 48)
  • Tableau B (id = 48, id_origin = 48, start = now, end = null)

Et ... eh bien j'ai un autre problème: la relation A_B: dois-je marquer (id_A = 1, id_B = 48) comme obsolète ou non (A - id = 1 est obsolète, mais pas B - 48)?

Comment y faire face?

Je dois concevoir cela à grande échelle: produits, partenaires, etc.

Quelle est votre expérience à ce sujet? Comment feriez-vous (comment avez-vous fait)?

-- Éditer

J'ai trouvé cet article très intéressant , mais il ne traite pas correctement de «l'obsolescence en cascade» (= ce que je demande en fait)

Olivier Pons
la source
Que diriez-vous de copier les données de l'enregistrement de mise à jour avant qu'il ne soit mis à jour vers un nouveau avec un nouvel identifiant en gardant la liste liée de l'historique avec le champ id_hist_prev. Donc, l'identifiant du record actuel ne change jamais
Plutôt que de réinventer la roue, avez-vous envisagé d'utiliser, par exemple, Flashback Data Archive sur Oracle?
Jack Douglas

Réponses:

4

Il n'est pas clair pour moi si ces exigences sont à des fins d'audit ou simplement pour une simple référence historique, comme avec le CRM et les paniers d'achat.

Dans les deux cas, envisagez d'avoir une table principale et main_archive pour chaque zone principale où cela est nécessaire. "Main" n'aura que les entrées actuelles / actives tandis que "main_archive" aura une copie de tout ce qui entre dans main. L'insertion / mise à jour dans main_archive peut être un déclencheur de l'insertion / mise à jour dans main. Les suppressions contre main_archive peuvent alors s'exécuter sur une plus longue période de temps, si jamais.

Pour les problèmes référentiels tels que Cust X a acheté le produit Y, le moyen le plus simple de résoudre votre problème référentiel de cust_archive -> product_archive est de ne jamais supprimer les entrées de product_archive. En règle générale, le taux de désabonnement devrait être beaucoup plus faible dans ce tableau afin que la taille ne soit pas trop préoccupante.

HTH.


la source
2
Excellente réponse, mais j'aimerais ajouter qu'un autre avantage d'avoir une table d'archives est qu'elles ont tendance à être dénormalisées, ce qui rend les rapports sur ces données beaucoup plus efficaces. Tenez également compte des besoins de génération de rapports de votre application avec cette approche.
maple_shaft
1
Dans la plupart des bases de données que je conçois, toutes les tables `` principales '' ont un préfixe comme le nom du produit LP_, et chaque table importante a un équivalent LH_, avec des déclencheurs insérant des lignes historiques lors de l'insertion, de la mise à jour, de la suppression. Cela ne fonctionne pas dans tous les cas, mais c'est un modèle solide pour ce que je fais.
Je suis d'accord - si la majorité des requêtes concernent les lignes "actuelles", vous obtiendrez probablement un avantage de performance en partitionnant le courant de l'historique dans deux tables. Une vue pourrait les rassembler, par commodité. De cette façon, les pages de données avec les lignes actuelles sont toutes ensemble et restent probablement mieux dans le cache, et vous n'avez pas à constamment qualifier les requêtes pour les données actuelles avec une logique de date.
onupdatecascade
1
@onupdatecascade: Notez que (au moins dans certains SGBDR) vous pouvez mettre des indices sur cette UNIONvue, ce qui vous permet de faire des choses sympas comme appliquer une contrainte unique sur les enregistrements actuels et historiques.
Jon of All Trades
5 ans plus tard, j'ai fait plein de choses et tout le temps je t'ai rappelé ton idée. La seule chose que j'ai changé, c'est que sur les tables d'historique, j'ai une colonne " id" et " id_ref". id_refest une référence à l'idée réelle de la table. Exemple: personet person_h. dans person_hj'ai " id", et " id_ref" où id_refest lié à ' person.id' donc je peux avoir plusieurs lignes avec le même person.id(= quand une ligne de personest modifiée) et idtoutes mes tables sont autoinc.
Olivier Pons
2

Cela a un certain chevauchement avec la programmation fonctionnelle; spécifiquement le concept d'immuabilité.

Vous avez une table appelée PRODUCTet une autre appelée PRODUCTVERSIONou similaire. Lorsque vous modifiez un produit, vous ne faites pas de mise à jour, vous insérez simplement une nouvelle PRODUCTVERSIONligne. Pour obtenir la dernière version, vous pouvez indexer la table par numéro de version (desc), horodatage (desc), ou vous pouvez avoir un indicateur ( LatestVersion).

Maintenant, si vous avez quelque chose qui fait référence à un produit, vous pouvez décider du tableau vers lequel il pointe. Pointe-t-il vers l' PRODUCTentité (se réfère toujours à ce produit) ou vers l' PRODUCTVERSIONentité (se réfère uniquement à cette version du produit)?

Ça se complique. Et si vous avez des photos du produit? Ils doivent pointer vers la table des versions, car ils pourraient être modifiés, mais dans de nombreux cas, ils ne le feront pas et vous ne voulez pas dupliquer les données inutilement. Cela signifie que vous avez besoin d'une PICTUREtable et d'une PRODUCTVERSIONPICTURErelation plusieurs-à-plusieurs.


la source
1

J'ai implémenté toutes les choses d' ici avec 4 champs qui sont sur toutes mes tables:

  • id
  • date_creation
  • date_validity_start
  • date_validity_end

Chaque fois qu'un enregistrement doit être modifié, je le duplique, marque l' enregistrement dupliqué comme "ancien" = date_validity_end=NOW()et l'actuel comme le bon date_validity_start=NOW()et date_validity_end=NULL.

L'astuce concerne les relations plusieurs à plusieurs et une à plusieurs: cela fonctionne sans les toucher! Il s'agit de requêtes plus complexes: pour interroger un enregistrement à une date précise (= pas maintenant), j'ai pour chaque jointure, et pour la table principale, d'ajouter ces contraintes:

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

Donc, avec des produits et des attributs (relation plusieurs à plusieurs):

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
la source
0

Que dis-tu de ça? Cela semble simple et assez efficace pour ce que j'ai fait dans le passé. Dans votre tableau "historique", utilisez un PK différent. Ainsi, votre champ "CustomerID" est le PK dans votre table Customer, mais dans la table "history", votre PK est "NewCustomerID". "CustomerID" devient juste un autre champ en lecture seule. Cela laisse "CustomerID" inchangé dans l'historique et toutes vos relations restent intactes.

Dimondwoof
la source
Très bonne idée. Ce que j'ai fait est très similaire: je duplique l'enregistrement et marque le nouveau comme "obsolète" afin que l'enregistrement actuel soit toujours le même. Remarque Je voulais créer un déclencheur sur chaque table mais mysql interdit les modifications d'une table lorsque vous êtes dans un déclencheur de cette table. PostGRESQL le fait. Le serveur SQL fait cela. Oracle le fait. Bref, MySQL a encore un très long chemin à parcourir, et la prochaine fois j'y réfléchirai à deux fois lors du choix de mon serveur de base de données.
Olivier Pons