Tutoriel de conception de base de données pour Google Calendar
(kb.databasedesignbook.com)- En prenant comme exemple la fonctionnalité d’agenda d’un clone de Google Calendar, l’article montre une démarche de conception qui consiste à terminer d’abord le modèle logique avant de le convertir en schéma SQL, au lieu de commencer directement par les tables
- La méthode clé consiste à identifier d’abord des ancres (anchors) comme User, DayEvent, TimeEvent et Timezone, puis à valider les exigences et la cardinalité à l’aide des attributs et de liens 1:N et M:N
- Les événements se divisent en événements sur toute la journée et en événements basés sur une heure ; pour ces derniers, on stocke les heures locales de début et de fin ainsi que leur Timezone respective afin de prendre en compte la possibilité de changements de fuseau horaire
- Les événements récurrents sont représentés par la fréquence, l’intervalle, le mode de répétition mensuelle, la condition de fin et les liens vers les jours de la semaine ; pour le rendu à l’écran et la modification d’instances individuelles, DaySlot et TimeSlot sont définis comme ancres distinctes
- La conception physique finale suit en principe la règle « une table par ancre », mais sans créer de table pour DayOfTheWeek, ce qui aboutit à un total de 8 tables SQL : 6 tables d’ancrage et 2 tables de liaison M:N
Approche de conception : créer d’abord le modèle logique
- L’objectif est de montrer un processus de conception des tables de base de données proche d’un projet réel, en prenant pour cible la fonctionnalité d’agenda d’un clone de Google Calendar
- Le flux global consiste à créer d’abord un modèle logique complet décrivant les données d’agenda à stocker, puis à convertir ce modèle en structure de tables physique
- La première partie traite d’un modèle logique indépendant d’une base de données particulière
- Les exigences métier sont modélisées indépendamment de MySQL, Postgres, des serveurs relationnels traditionnels, du NoSQL ou des bases de données cloud
- La seconde partie montre une approche possible pour convertir le modèle logique en tables physiques
- Pour un système dont les exigences ne sont pas extrêmement contraignantes, cela peut constituer une méthode de conception valable
Périmètre du problème : les fonctions essentielles d’agenda de Google Calendar
- Le système visé est un système multi-utilisateur, et les données liées aux utilisateurs ne sont modélisées qu’au minimum
- Les événements peuvent avoir des attributs comme le titre, la description ou le lieu, mais la partie la plus complexe concerne le temps et les dates
- Les événements à modéliser incluent les différences suivantes
- événements sur toute la journée et événements basés sur une heure
- événements récurrents et non récurrents
- événements sur toute la journée s’étendant sur plusieurs jours
- événements basés sur une heure associés à un fuseau horaire, dont les heures de début et de fin peuvent tomber à des dates différentes ou dans des fuseaux horaires différents
- Les événements récurrents incluent les formes suivantes
- répétition quotidienne ou tous les N jours
- répétition hebdomadaire certains jours précis, avec éventuellement un intervalle de 2 semaines ou plus
- répétition mensuelle à une date précise ou un certain jour d’une certaine semaine
- répétition annuelle
- répétition indéfinie, jusqu’à une date donnée ou pendant un nombre limité d’occurrences
- Une instance particulière d’un événement récurrent peut être déplacée à une autre date ou heure, ou supprimée
- Il doit être possible de modifier la récurrence elle-même même après qu’une partie des événements est déjà passée
Exprimer les exigences avec des ancres, attributs et liens
- La modélisation commence par l’identification des ancres (anchors)
- Une ancre est un élément dénombrable pouvant être ajouté à la base de données, comme User ou Event
- Si l’on peut dire naturellement « There are 200 Users in our database » ou « When this button is clicked, a new DayEvent is created », on peut le considérer comme une ancre
- Les attributs stockent les informations réelles sur une ancre
- User possède un attribut email
- DayEvent possède un nom, une date de début et une date de fin
- Même un événement d’un seul jour stocke la même valeur pour la date de début et la date de fin afin d’être traité avec la même structure qu’un événement sur plusieurs jours
- Lorsque deux ancres sont en relation, on utilise un lien plutôt qu’un attribut
- « Quel User a créé ce DayEvent ? » n’est pas un attribut logique consistant à mettre directement un ID utilisateur dans DayEvent, mais un lien entre User et DayEvent
- Les liens utilisent les cardinalités 1:N, M:N et 1:1, et leur signification bidirectionnelle est validée au moyen de deux phrases
- Il existe un lien 1:N entre User et DayEvent
- Un User peut créer plusieurs DayEvent
- Un DayEvent est créé par un seul User
Événements basés sur une heure et gestion des Timezone
- Pour les événements basés sur une heure, on ajoute les ancres Timezone et TimeEvent
- Le modèle Timezone est réduit au minimum pour les besoins du tutoriel et ne comporte qu’un nom lisible par un humain comme attribut
- Valeur d’exemple :
Europe/Kyiv - Une vraie définition de fuseau horaire nécessiterait l’offset UTC, l’usage de l’heure d’été, le début et la fin du DST, l’offset UTC appliqué pendant le DST, l’historique des définitions, le statut actif ou obsolète, etc., mais cela reste hors du cadre
- Valeur d’exemple :
- TimeEvent possède un nom, une date-heure locale de début et une date-heure locale de fin
- Valeurs d’exemple :
2024-01-14 12:30,2024-01-14 13:15
- Valeurs d’exemple :
- Les heures ne sont pas stockées uniquement en UTC, mais dans l’heure locale saisie par l’utilisateur
- Par exemple, pour une partie de billard prévue le 6 septembre 2058 de 09:30 à 11:00 à l’heure de Cologne, on ne peut pas connaître dès aujourd’hui l’offset UTC exact à cette date ; il faut donc conserver ce que l’utilisateur a saisi
- Si la législation locale change plus tard, il doit être possible de s’y adapter
- TimeEvent possède pour le fuseau horaire de début et le fuseau horaire de fin un lien 1:N distinct vers Timezone
- Comme dans l’exemple d’un billet d’avion entre Amsterdam et Londres, le départ et l’arrivée peuvent relever de fuseaux horaires différents
- La plupart des événements basés sur une heure utilisent le même fuseau au début et à la fin, mais les deux sont toujours spécifiés pour couvrir le cas général
Modéliser les événements récurrents sur toute la journée
- Les événements récurrents sur toute la journée sont représentés en ajoutant à DayEvent des attributs et des liens liés à la récurrence
- La fréquence de répétition est définie comme un attribut de type either/or/or
- Les valeurs possibles sont daily, weekly, monthly, annually
- Si aucune valeur n’est définie, l’événement est considéré comme non récurrent
- L’intervalle de répétition est un attribut entier qui n’a de sens que si l’événement est récurrent
- Valeur d’exemple :
2 - Il sert à exprimer une répétition tous les 2 jours, toutes les 2 semaines, tous les 2 mois, etc.
- Valeur d’exemple :
- La répétition mensuelle comporte un attribut de sélection distinct
same_daypour répéter à la même datesame_weekdaypour répéter le même jour de semaine de la même semaine que la date d’origine
- Les jours précis d’une répétition hebdomadaire ne sont pas traités comme un attribut tableau de chaînes, mais modélisés via une ancre DayOfTheWeek et un lien M:N
- Exemples d’ID DayOfTheWeek :
Mon,Tue,Wed,Thu,Fri,Sat,Sun - Un DayEvent peut avoir lieu sur plusieurs DayOfTheWeek, et un DayOfTheWeek peut appartenir à plusieurs DayEvent
- Exemples d’ID DayOfTheWeek :
- La condition de fin de répétition est aussi un attribut
- forever
- until_date
- N_repetitions
- dans le cas de until_date, on ajoute un attribut date de fin
- dans le cas de N_repetitions, on ajoute un attribut nombre de répétitions
Slots pour le rendu et la modification d’instances individuelles
- Si l’on ne stocke un événement récurrent que comme un seul événement source, les requêtes pour trouver les événements à afficher dans une vue hebdomadaire donnée peuvent devenir complexes
- Pour le rendu à l’écran et les modifications individuelles d’événements récurrents, l’article introduit le concept de Slot
- DaySlot représente une instance d’un événement sur toute la journée à une date donnée
- TimeSlot représente une instance d’un événement basé sur une heure à une date et une heure données
- DaySlot possède une date et un indicateur de saut
- La date d’un slot particulier peut être modifiée
- Si une réunion d’une semaine donnée est annulée, le DaySlot correspondant peut être marqué comme skipped
- DayEvent et DaySlot ont un lien 1:N
- Un DayEvent peut générer plusieurs DaySlot
- Un DaySlot correspond à un seul DayEvent
- Même pour un DayEvent non récurrent, on crée toujours un DaySlot afin de simplifier le code de rendu
- Les événements à répétition infinie posent un problème de portée de génération des slots
- Pour un événement comme un anniversaire qui se répète chaque année indéfiniment, on peut créer des slots jusqu’à une limite arbitraire, par exemple 100 ans
- Ou bien les créer à la demande lorsque l’utilisateur consulte une page de calendrier très éloignée dans le futur
- Les calculs de date demandent de l’attention
- anniversaires du 29 février
- événements mensuels tombant le 31 du mois
- Dans ce genre de cas, il faut décider s’il faut interdire cet usage ou déplacer l’événement d’un jour en avant ou en arrière
TimeSlot et fuseaux horaires
- La même approche que pour les événements sur toute la journée est appliquée aux événements récurrents basés sur une heure, avec l’ancre TimeSlot
- TimeSlot correspond à une instance précise d’un événement, à une date et une heure données, et un TimeEvent récurrent peut correspondre à plusieurs TimeSlot
- Les attributs de TimeSlot sont la date-heure locale de début, la date-heure locale de fin et l’indicateur de saut
- Comme un TimeSlot donné peut être déplacé à un autre jour, les informations d’heure de début et de fin sont conservées séparément
- TimeSlot possède aussi des liens 1:N vers Timezone pour le fuseau horaire de début et celui de fin
- Il existe un lien 1:N entre TimeEvent et TimeSlot, et même pour un TimeEvent non récurrent on crée un TimeSlot
Modèle logique finalisé
- Le nombre total d’ancres définies jusqu’ici est de 7
- User
- Timezone
- DayEvent
- TimeEvent
- DayOfTheWeek
- DaySlot
- TimeSlot
- Les attributs comprennent l’email de User, le nom de Timezone, le nom, la date de début, la date de fin et les informations de récurrence de DayEvent, les heures locales de début et de fin de TimeEvent, ainsi que le moment d’occurrence et le statut skipped de DaySlot et TimeSlot
- Les liens sont au nombre total de 10
- User et DayEvent, User et TimeEvent
- fuseaux horaires de début et de fin entre Timezone et TimeEvent
- DayEvent et DayOfTheWeek, TimeEvent et DayOfTheWeek
- DayEvent et DaySlot, TimeEvent et TimeSlot
- fuseaux horaires de début et de fin entre Timezone et TimeSlot
Conversion en tables SQL
- Pour des raisons pédagogiques, la conception physique utilise la stratégie une table par ancre
- Le modèle logique contient 7 ancres, 21 attributs et 10 liens
- En général, on obtiendrait 9 tables en ajoutant au nombre d’ancres le nombre de liens M:N, mais dans la conception finale DayOfTheWeek est traité de façon particulière et ne devient pas une table physique
- Le total final est donc de 8 tables SQL
userstimezonesday_eventstime_eventsday_slotstime_slotsday_event_dowstime_event_dows
- Les liens 1:N deviennent des colonnes de type clé étrangère dans la table du côté N
day_events.user_idtime_events.user_idtime_events.start_timezone_idtime_events.end_timezone_idday_slots.day_event_idtime_slots.time_event_idtime_slots.start_timezone_idtime_slots.end_timezone_id
- Les liens M:N sont transformés en tables séparées
day_event_dowsrelie DayEvent aux jours de la semainetime_event_dowsrelie TimeEvent aux jours de la semaine
- Les types physiques sont choisis en correspondance avec les types logiques
- les chaînes deviennent
VARCHAR - les dates deviennent
DATE - les dates-heures locales deviennent
DATETIME - les booléens yes/no deviennent
TINYINT UNSIGNED - les attributs imbriqués qui n’ont de sens que sous condition utilisent des types autorisant
NULL
- les chaînes deviennent
NULLet les valeurs sentinelles relèvent du schéma physique plutôt que du modèle logique
Index et attributs omis
- Le schéma SQL d’exemple n’inclut pas certains index qu’un développeur base de données expérimenté jugerait nécessaires
- Par exemple,
day_events.user_idpeut nécessiter un index
- Par exemple,
- Le choix des colonnes ou combinaisons de colonnes à indexer dépend de la manière dont l’application interroge les tables
- Pour le sujet des index, on peut consulter Use The Index, Luke
- Pendant l’explication du schéma logique, certains attributs ont été omis parce qu’ils suivent des motifs similaires
- nom de l’utilisateur
- hachage du mot de passe utilisateur
- lieu de l’événement
- liste des personnes invitées
- Ces éléments de données omis peuvent être ajoutés au tableau du modèle logique de la même manière, puis répercutés dans le schéma SQL
Procédure complète
- Commencer par un texte libre décrivant le problème métier
- Rédiger la liste des ancres
- Rédiger la liste des attributs en formulant clairement leur signification sous forme de questions
- Rédiger la liste des liens et vérifier la cardinalité à l’aide de phrases
- Si nécessaire, créer un schéma graphique à partir du modèle logique
- Rédiger le modèle physique en remplissant les noms de tables, noms de colonnes et types de données physiques
- Utiliser les informations des étapes précédentes pour écrire le schéma SQL avec des instructions
CREATE TABLE - Soumettre le schéma au serveur de base de données, corriger les fautes de frappe, puis le soumettre à nouveau
- Partager le modèle logique avec l’équipe
1 commentaires
Avis sur Hacker News
Si je sérialise un événement quelconque de mon calendrier en iCalendar, cela fait environ 740 octets ; même un calendrier extrêmement chargé, avec un événement toutes les 15 minutes entre 9 h et 17 h, ne représente que 11 680 entrées sur une année entière, soit environ 8 Mo
Donc, plutôt que de concevoir un schéma, le calendrier semble être un problème qu’il vaudrait peut-être bien mieux implémenter par simple scan séquentiel. En optimisant le parseur iCalendar pour parcourir une plage d’événements exportés avec un débit de l’ordre du Go/s, même le pire cas ci-dessus pourrait être scanné en quelques millisecondes à un chiffre
Optimiser un parseur est un problème bien plus simple, à résoudre une seule fois, que de modifier ou d’étendre un mauvais modèle de données après qu’il s’est propagé à de nombreux utilisateurs ; et, de toute façon, la première tâche d’un nouveau modèle de données sera probablement l’import/export iCalendar, donc cela paraît être un compromis assez intéressant
BETWEEN ... AND ...en SQL ? Idem pour trouver les événements des utilisateurs 1, 2, 3Dans une application réelle, on aurait déjà sans doute des choses comme les comptes utilisateurs dans une base de données relationnelle, et il faudrait soudain scanner les événements dans un répertoire puis rattacher les résultats à des enregistrements de base de données
Cela peut convenir à certaines applications, mais il y a des tâches précises que les bases de données font vraiment très bien, ce qui en fait un bon choix. Avec les bons index, on pourrait obtenir un débit du même ordre, voire meilleur. Sauf à créer une structure de répertoires astucieuse pour les événements, mais ce serait en fait l’équivalent d’un index unidimensionnel, alors qu’une base de données permet de créer des index sur plusieurs dimensions et combinaisons de dimensions
Au fond, c’est vrai, et c’est une question de compromis
Le format trahit son âge : on voit qu’il a été conçu avant que XML/JSON ne « décollent »
Référence : https://en.wikipedia.org/wiki/ICalendar
Voici un article sur la gestion des événements récurrents dans les applications ; personnellement, il m’a vraiment ouvert les yeux. Je recommande vivement de le lire : https://github.com/bmoeskau/Extensible/blob/master/recurrenc...
L’article original recommande de toujours stocker le fuseau horaire avec chaque date, tandis que l’article lié dit de tout convertir en UTC. Sur ce point, je suis d’accord avec l’article original
L’article original dit de créer une ligne de base de données pour chaque événement, tandis que l’article lié dit de ne pas le faire. Sur ce point, je suis d’accord avec l’article lié
Le plus gros raté que j’aie connu en entretien, c’était lorsqu’on m’a demandé de construire un système de réservation simple qui devait aussi gérer les réservations récurrentes
Depuis mes premières années d’université, je n’avais jamais été aussi perdu et désorienté en essayant de produire quelque chose avec du code. Aujourd’hui encore, dès que je dois travailler avec des dates/heures, ça m’angoisse ; je ne sais pas pourquoi, mais ça ne s’emboîte pas bien dans ma tête
Cela paraît simple en surface, mais dès qu’on veut que ça fonctionne de manière générale dans le monde réel, le problème devient d’une complexité quasiment impossible
C’est un problème trompeusement difficile, qui demande de très solides compétences en modélisation de données
La capacité à modéliser des systèmes est sous-estimée. Dans un nouveau domaine, il faut commencer par une analyse complète du problème afin de saisir à la fois la structure statique du système, c’est-à-dire le modèle de classes, et son comportement dynamique, c’est-à-dire les cas d’usage
Si l’on se précipite directement vers un modèle statique de base de données, on risque facilement de manquer le comportement dynamique. Cela peut passer pour de simples applications CRUD, mais dans des systèmes plus complexes, ce peut être une grosse erreur
La dernière question dont je me souvienne portait sur la manière de concevoir une table de produits avec des coupons. Au début, je me suis dit que deux tables suffiraient, mais ne faut-il pas aussi conserver l’historique ? Dans ce cas, il faut ajouter des dates/heures par produit et par coupon, et les mettre à jour. Il faut maintenant réfléchir aux index de tables, et se demander si une jointure pour obtenir le prix remisé est une bonne approche. La plupart des coupons ne peuvent être utilisés qu’une seule fois par personne : comment diable implémenter ça ?
Ils voulaient probablement juste une simple table produits + coupons, mais je suis parti tout seul dans une exploration délirante pendant un bon moment
Je trouve que c’est un domaine bien choisi pour montrer la modélisation, et un bon article d’introduction
Le terme « anchor » me paraît un peu étrange, mais l’explication est très concrète et ancrée dans le réel, comme une vraie ancre, donc ça fonctionne assez bien
L’idée de définir les attributs au moyen de questions est solide. Trop souvent, on saute directement vers un minimum de colonnes/noms d’attributs sans avoir défini à quelles questions on essaie de répondre ; dans ce cas, on ne dissipe pas le flou qui existe dans l’esprit du client. La définition des attributs par les questions est un bon moyen d’obtenir rapidement de la clarté
En plus, ce terme est extrêmement surchargé. En informatique, pratiquement tout est soit un objet, soit une entité
Le fait que l’idée de définir les attributs par des questions soit perçue comme solide constitue une validation importante de la pertinence de cette approche
Les fuseaux horaires sont particulièrement casse-tête autour des moments de transition
Supposons qu’un fuseau horaire avance d’une heure lors du passage à l’heure d’été, puis recule d’une heure lors du retour à l’heure standard
Quand l’heure saute d’une heure en avant, un événement d’une heure peut sembler s’étaler sur 2 heures à l’écran, et cette deuxième heure peut être inaccessible ou inexistante
Quand l’heure recule d’une heure, un événement d’une heure peut sembler durer 2 heures, ou 0 heure
Les fuseaux horaires sont une construction humaine, donc il ne faut pas coder leurs valeurs en dur. Elles changeront un jour
On peut franchir plusieurs fois des fuseaux horaires dans les deux sens, traverser la ligne de changement de date, et il est même possible que la date/heure locale de début soit postérieure à la date/heure locale de fin
Il vaudrait mieux se concentrer sur la suppression de la notion d’heure d’été dans les quelques régions qui l’utilisent encore. C’est elle qui provoque le plus de confusion entre fuseaux horaires, en particulier quand on planifie des événements après le prochain changement d’heure
Je ne veux pas stocker deux dates pour un événement. Il est plus simple de stocker l’heure de début et la durée de l’événement
Ainsi, la logique de mise à jour des événements est simplifiée. L’heure de fin peut toujours être calculée à partir de l’heure de début et de la durée
J’ai travaillé autrefois sur une application de calendrier avec des événements récurrents. Après beaucoup de recherches, nous avons décidé d’utiliser RRules pour les représenter, et j’en étais très satisfait. Ce travail initial, je l’avais fait lorsque j’étais en agence
Plus tard, quand j’ai rejoint cette entreprise à temps plein, j’ai été sidéré de voir qu’un prestataire d’une autre société avait supprimé RRules et remplacé cela par un système qui créait et supprimait les instances d’événements à la volée. Il n’y avait presque aucune tolérance aux pannes : un script, qui faisait aussi d’autres choses et échouait parfois, ratait de temps en temps la création de nouveaux événements. Résultat : il manquait des mois dans un événement récurrent mensuel
C’était extrêmement frustrant de voir quelqu’un supprimer quelque chose conçu après beaucoup de réflexion et de recherche, sans fournir un effort comparable. Au bout de quelques semaines dans cette entreprise, il était évident que le CEO s’attendait à ce que l’équipe d’ingénierie produise sans cesse, selon ses caprices, des fonctionnalités que personne n’utilisait ; à cause de l’incertitude du marché de l’emploi, j’y suis malheureusement resté 2 ans
À propos, quand j’ai cherché plus tard, j’ai trouvé sur Glassdoor de faux avis manifestement écrits par le CEO, tous dans le même style et sans le moindre mot négatif, ce qui m’a vraiment laissé un goût amer. Moi et plusieurs connaissances qui y avaient travaillé le détestions, mais je me console en me disant que j’y ai écrit quelques-uns de mes meilleurs essais. Le CTO n’était pas mieux
J’ai un temps implémenté un calendrier et un backend de contrôle des ressources pour une plateforme low-code
Le contrôle était très personnalisable, avec plusieurs vues au choix — quotidienne, mensuelle, annuelle, etc. — ainsi qu’une vue par ressources. On pouvait réserver des ressources avec des groupes personnalisés, et les regrouper par plugin, par ID de ressource, etc. On définissait des « plugins » dans la source de données, ainsi que les colonnes de début/fin, la colonne de titre et la colonne de ressource. Les ressources pouvaient venir d’une clé étrangère ou d’une relation 1:1, mais aussi d’une relation 1:N d’une source de données « enfant », ou de la même source/table de données
Nous avons aussi implémenté divers types de séries de rendez-vous : mensuelles, hebdomadaires, certains jours de la semaine, quotidiennes, etc., avec la possibilité de choisir quelles valeurs de colonnes copier. Nous gérions aussi les conflits de rendez-vous, avec la possibilité de ne considérer un conflit que lors de la réservation d’une même ressource. Il était également possible de définir un tampon avant ou après un rendez-vous, pendant lequel aucun autre rendez-vous ne pouvait être placé
C’était parfois difficile à cause des fuseaux horaires européens et de l’heure d’été/hiver, entre autres, mais c’était un travail assez amusant
Tout ce que je veux dans Google Calendar, c’est un journal des changements du calendrier lui-même. Par pitié, ajoutez ça à la base de données