MySQL vs MongoDB pour le reporting et l’analytique : pourquoi les équipes reviennent à SQL

Cet article vous a aidé ?

Si vous avez déjà vu un PDG rafraîchir un tableau de bord pendant que votre téléphone d’astreinte chauffe, vous connaissez cette douleur : le trafic reporting ne se dégrade pas en douceur. Il arrive d’un coup, demande « encore une découpe », et votre base de données commence à émettre des bruits inconnus.

Les équipes adorent MongoDB pour livrer des fonctionnalités produit rapidement. Puis arrive le reporting. Soudain, vous déboguez des pipelines d’agrégation à 2 h du matin, et tout le monde développe une affection nostalgique pour les jointures SQL dont ils se plaignaient auparavant.

Pourquoi les équipes retournent à SQL (et pourquoi ce n’est pas de l’hypocrisie)

Le reporting et l’analytique sont un sport différent de l’OLTP. Votre base produit est conçue pour des lectures et écritures petites et prévisibles : récupérer un utilisateur, mettre à jour un panier, insérer une commande. Le reporting est conçu pour de larges scans, des regroupements lourds, des filtres larges et « comparer ce mois à celui d’avant sur six dimensions ». Il est aussi conçu pour des humains avec des fuseaux horaires, des avis et Excel.

MongoDB peut faire de l’analytique, surtout avec le framework d’agrégation, mais beaucoup d’équipes découvrent la même vérité inconfortable : les magasins de documents s’optimisent pour des accès façonnés par l’application, pas par l’analyste. Les analystes ne pensent pas en « documents » ; ils pensent en relations et dimensions. Ils poseront des questions auxquelles votre design documentaire d’origine résiste activement.

Les systèmes SQL (MySQL, Postgres, SQL Server, etc.) ont une longue et rude histoire d’avoir été battus en forme pour le reporting. Cette histoire compte. Optimiseurs, stratégies de jointure, sémantiques stables, outils matures — c’est une accumulation d’expérience opérationnelle. MongoDB s’améliore, mais quand votre VP veut un graphique de cohortes de rétention pour midi, vous voulez des chemins ennuyeux, lisibles et bien éprouvés.

Aussi : le reporting a de la politique. Les gens veulent des chiffres cohérents entre équipes. Cela signifie des définitions cohérentes. Les schémas SQL et les vues transforment les définitions en artefacts partagés et examinables. MongoDB les transforme souvent en extraits de code applicatif et fragments de pipeline éparpillés dans les dépôts comme des paillettes — pour toujours.

Une petite blague courte, pour la route : MongoDB est sans schéma comme la chambre d’un adolescent est sans meubles. Techniquement vrai, opérationnellement coûteux.

Contexte historique et faits intéressants (les éléments qu’on oublie)

Voici quelques faits concrets et éléments de chronologie qui expliquent pourquoi SQL continue de « regagner » les charges de reporting. Pas des impressions — de l’histoire réelle.

  1. La standardisation SQL a commencé dans les années 1980. Cela signifie des décennies d’outillage : JDBC/ODBC, connecteurs BI, planificateurs de requêtes, et un modèle mental partagé entre entreprises.
  2. MySQL a été livré au milieu des années 1990 et est devenu la base web par défaut pour une génération. Les patterns de reporting ont été appris en production à la dure : réplicas, tables de résumé, rollups et partitionnement.
  3. MongoDB est apparu autour de 2009 dans une vague de systèmes « NoSQL » conçus pour monter en charge horizontalement et aller vite. Beaucoup étaient pensés principalement pour des charges applicatives, pas pour l’analytique ad-hoc.
  4. L’essor du JSON (et plus tard du JSON en SQL) a estompé la ligne. Les systèmes SQL ont adopté des colonnes JSON et des fonctions ; MongoDB a adopté des capacités de requête et d’agrégation plus riches. La convergence a eu lieu, mais pas uniformément.
  5. Le « schéma à la lecture » est devenu populaire dans le big data (penser époque Hadoop). Cela fonctionnait pour le traitement batch, mais le reporting opérationnel veut des requêtes rapides et interactives. Le schéma à la lecture n’est rarement gratuit.
  6. Les stockages colonnaires et les entrepôts ont changé les attentes. Les équipes se sont habituées à des tableaux de bord qui scannent des millions de lignes rapidement. Une fois que vous y avez goûté, un magasin de documents faisant de larges group-by peut donner l’impression de pédaler un camion.
  7. Les outils BI ont évolué autour de SQL. Même quand les outils supportent MongoDB, les fonctionnalités « chemin doré » (couches sémantiques, cache, traçabilité, permissionnement) sont presque toujours SQL-first.
  8. Les schémas de réplication sont plus anciens que la plupart des microservices. La réplication et les patterns de montée en lecture de MySQL sont profondément compris opérationnellement, y compris les modes de défaillance et playbooks de récupération.

Tout cela se résume en une règle opérationnelle simple : le succès du reporting dépend moins de « la base peut-elle exécuter la requête » que de « votre organisation peut-elle faire fonctionner la base sous une demande humaine imprévisible ». Les systèmes SQL ont eu plus de temps dans cette arène.

Réalité des charges de reporting : ce que font les tableaux de bord aux bases

Les requêtes de reporting sont impolies. Elles n’utilisent pas vos clés primaires soigneusement conçues. Elles ne respectent pas votre cache. Elles adorent les plages temporelles. Elles veulent grouper par des colonnes que vous n’aviez pas indexées parce qu’elles n’existaient pas quand vous avez expédié la v1.

Les trois choses que font les tableaux de bord et qui brisent les hypothèses d’une « base applicative »

  • Ils causent du fan-out : un chargement de page déclenche de nombreuses requêtes (filtres, totaux, graphiques, drilldowns). Multipliez par une ruée du lundi matin.
  • Ils biaisent les patterns d’accès : au lieu de « récupérer un document par id », vous obtenez « scanner les 90 derniers jours et calculer la distribution ».
  • Ils exigent une cohérence des définitions : « utilisateur actif », « revenu », « churn », « conversion » signifient des choses différentes jusqu’à ce que vous les stabilisiez. Les schémas SQL, vues et logique stockée vous aident à faire respecter des définitions partagées.

L’analytique n’est pas que la vitesse de requête

La vitesse compte, mais il y a aussi :

  • Prédictibilité : performances de requête stables selon les valeurs des paramètres et la croissance des données.
  • Explicabilité : vous devez montrer comment le chiffre a été calculé, pas seulement le livrer.
  • Isolation opérationnelle : le reporting ne doit pas faire tomber votre flux de checkout.
  • Gouvernance du schéma : si vos « events » ont 19 formes, quelqu’un va calculer le « DAU » de travers.

Il y a une raison pour laquelle beaucoup d’équipes matures exécutent le reporting depuis des réplicas, des entrepôts ou des data marts séparés. Elles ont appris que mélanger OLTP et analytique ad-hoc, c’est comme laisser des clients dans votre cuisine pendant l’heure de pointe.

Pourquoi MySQL a tendance à gagner pour le reporting

1) Les jointures sont une fonctionnalité, pas une faute morale

Le reporting est relationnel par nature : les commandes se rapportent aux clients, les clients aux segments, les segments aux campagnes, les campagnes aux dépenses. Même si l’application stocke un « snapshot client » à l’intérieur d’un document de commande, les analystes demanderont toujours des attributs niveau client, des segmentations mises à jour et des corrections. Les jointures sont la façon de connecter faits et dimensions sans réécrire l’historique.

Le moteur de jointure, l’optimiseur et le modèle d’indexation de MySQL sont construits autour de cela. Quand vous écrivez une requête qui joint cinq tables avec des prédicats sélectifs, MySQL a une chance de faire quelque chose de sensé. Dans MongoDB, vous dénormaliserez souvent (et le paierez plus tard) ou vous utiliserez $lookup (et vous redécouvrirez les jointures, mais avec moins de garde-fous et parfois plus de problèmes mémoire).

2) Les plans de requête sont inspectables et opérationnellement exploitables

Dans MySQL, EXPLAIN, les histogrammes et les traces de l’optimiseur donnent des indices structurés. Vous pouvez demander « pourquoi a-t-il choisi cet index », « pourquoi a-t-il trié », « pourquoi a-t-il scanné », et obtenir une réponse qui correspond à des patterns de tuning connus.

Cela compte en réponse à incident. Quand les tableaux de bord font fondre la production, vous ne voulez pas de débats philosophiques. Vous voulez voir le plan, corriger l’index, ajouter un index couvrant, réécrire la requête ou déplacer la charge. Rapidement.

3) L’écosystème est résolument façonné SQL

Les outils BI, les outils de gouvernance, les patterns de contrôle d’accès, l’audit, le logging des requêtes, et même « comment former les nouvelles recrues au reporting » sont plus simples quand le stockage est SQL. Votre équipe finance ne veut pas apprendre des pipelines d’agrégation. Elle veut une vue nommée revenue_daily.

4) Réplicas, partitionnement et tables de résumé sont ennuyeux et efficaces

Le reporting sur MySQL finit généralement avec une combinaison de :

  • Réplicas en lecture pour le trafic des tableaux de bord
  • Tables de faits partitionnées (souvent par date)
  • Tables de résumé pré-agrégées pour les métriques chaudes
  • Rollups matérialisés (oui, vous construisez un mini-entrepôt)

Rien de tout cela n’est tendance. C’est, cependant, comment vous maintenez le produit en marche pendant que l’organisation pose des questions.

Où MongoDB est réellement efficace (et où il ne l’est pas)

MongoDB brille quand la forme du document correspond à la forme de la requête

Si votre reporting est « afficher le profil utilisateur avec préférences imbriquées et activité récente » et que vous avez construit des documents exactement pour cela, MongoDB peut être rapide et agréable. Les agrégations sur une seule collection avec des filtres sélectifs et des index corrects peuvent aussi bien fonctionner.

MongoDB peine quand le reporting devient inter-entités et évolutif

Le mode de défaillance ressemble à ceci :

  • Les données sont d’abord dénormalisées pour la vitesse.
  • Les définitions évoluent (« actif » change, la segmentation change, des corrections arrivent).
  • Maintenant vous devez backfiller des champs imbriqués, réconcilier des incohérences et expliquer quelle version de la vérité chaque graphique utilise.
  • Quelqu’un introduit des pipelines $lookup pour « joindre » des collections.
  • Les pics mémoire surviennent, les résultats intermédiaires gonflent et le cluster se comporte comme s’il était allergique à votre directeur financier.

Le reporting sur MongoDB peut être correct quand vous le traitez comme source, pas comme moteur de reporting

Une architecture stable très courante est : MongoDB pour les cas d’usage produit OLTP-ish → streaming/ETL vers un système SQL ou un entrepôt pour le reporting. Ce n’est pas une insulte à MongoDB. C’est de la spécialisation. De la même manière qu’on ne fait pas tourner les workloads du plan de contrôle Kubernetes sur les mêmes nœuds que vos jobs Spark batch à moins d’aimer le chaos.

Jointures vs embedding : la taxe du reporting que vous paierez toujours tôt ou tard

L’embedding est séduisant. Une lecture, tout ce dont vous avez besoin, pas de jointures. Super — jusqu’à ce que votre business veuille de l’historique et de la cohérence. À l’instant où vous imbriquez des attributs mutables (niveau client, adresse, segment) dans des documents de type fait (commandes, events), vous choisissez un modèle de versionnage. Généralement c’est « comme c’était à l’écriture ». Parfois c’est correct. Souvent ce ne l’est pas.

Les dimensions mutables sont là où la dénormalisation NoSQL fait mal

Le reporting veut la capacité de demander :

  • « Quel a été le revenu par segment actuel ? »
  • « Quel a été le revenu par segment au moment de l’achat ? »
  • « Que se passe-t-il si nous reclassons ces clients ? »

En SQL, c’est une décision de schéma : dimensions à changement lent, tables snapshot ou historique de type 2. Dans MongoDB, les équipes bricolent souvent en stockant plusieurs champs, en backfillant ou en écrivant des pipelines de réconciliation ad-hoc. Cela peut fonctionner, mais c’est moins explicite et plus facile à mal faire.

Deuxième petite blague (et on a fini) :

La dénormalisation, c’est comme faire sa valise en portant tous les t-shirts à la fois. Vous économiserez de la place jusqu’au moment où il faudra vous changer.

Plans de requête, index, et pourquoi « ça marchait en staging » est un mensonge

Les données de staging sont polies. Les données de production sont créatives. Les requêtes analytiques amplifient cette créativité car elles touchent plus de lignes, plus de plages et des coins bizarres.

MySQL : modes de défaillance prévisibles

Dans les incidents de reporting MySQL, les coupables habituels sont :

  • Indexes composites manquants pour les patterns courants filtre+group
  • Triage de gros ensembles dus à ORDER BY sur des colonnes non indexées
  • Mauvaises ordres de jointure quand les statistiques sont obsolètes ou biaisées
  • Débordements de tables temporaires (tables temp disque) lors d’agrégations
  • Retard de réplica quand les tableaux de bord martèlent les réplicas

La bonne nouvelle : ces problèmes sont diagnostiquables avec des outils standards et typiquement corrigeables sans redessiner totalement le modèle de données.

MongoDB : falaises de performance via agrégation et mémoire

MongoDB peut bien se comporter jusqu’à ce qu’il ne le fasse plus. Les falaises typiques :

  • $group avec des clés à haute cardinalité crée un état mémoire massif
  • $lookup fait exploser les résultats intermédiaires
  • L’utilisation d’index est correcte pour le premier $match, puis vous êtes dans le territoire des pipelines
  • Les requêtes ralentissent à mesure que les documents deviennent plus variés (documents larges, tableaux)
  • Les clés de sharding optimisées pour l’OLTP ne correspondent pas aux filtres de reporting

Une citation fiabilité (idée paraphrasée)

Idée paraphrasée, attribuée à Richard Cook : « Le succès en opérations cache souvent le travail qui fait que la défaillance n’arrive pas. »

Le reporting est l’endroit où le travail caché devient visible. Si vous voulez des tableaux de bord qui ne vous embarrassent pas, vous avez besoin d’un design explicite : schémas, index, isolation et maintenance routinière.

Trois mini-histoires d’entreprise venues des tranchées

Mini-histoire 1 : L’incident causé par une fausse hypothèse

La société A faisait tourner son produit principal sur MongoDB. Les commandes étaient des documents avec des snapshots client imbriqués : nom, email, niveau, région. L’hypothèse était simple : « Nous voudrons toujours l’info client telle qu’elle était au moment de la commande. » C’était vrai pour les reçus et le support client, donc tout le monde a hoché la tête et est passé à autre chose.

Puis l’entreprise a lancé un projet de reclassification motivé par la conformité. Les régions ont changé. Les clients ont été remappés. La finance voulait le revenu par région actuelle pour les huit trimestres précédents afin de coller aux nouveaux rapports. L’équipe dashboard a construit un pipeline d’agrégation sur la collection orders et a groupé par le champ region imbriqué. Il a renvoyé des chiffres. Ils étaient faux. Faux en silence.

Quand la divergence est apparue, l’ingénierie a essayé de « corriger les données » en backfillant le champ region imbriqué sur les commandes historiques. Cela a pris des jours, stressé le cluster, et a rompu l’hypothèse initiale pour le support client : le reçu affichait désormais une région où le client ne vivait pas au moment de l’achat. Deux vérités, un seul champ.

La réparation finale n’était pas glamour : normaliser les dimensions dans un store de reporting SQL, garder les faits de commande immuables, et modéliser la région comme une dimension à changement lent avec des jointures explicites « as-of ». La couche reporting a obtenu deux vues : « region_at_purchase » et « region_current ». Le dashboard a cessé de mentir. Les gens ont cessé de crier. Voilà à quoi ressemble l’âge adulte dans les systèmes de données.

Mini-histoire 2 : L’optimisation qui a mal tourné

La société B utilisait MySQL et a rencontré de la latence sur le reporting. Quelqu’un a proposé une optimisation : « Nous précalculerons tout chaque nuit dans une grande table wide pour que les dashboards ne joignent jamais. » Ça sonnait efficace. Ça sonnait aussi comme quelque chose qu’une équipe fatiguée dit après des mois d’astreintes.

La table wide a fonctionné environ deux semaines. Puis un lancement produit a introduit de nouveaux attributs. L’ETL a accumulé de la logique conditionnelle. Les colonnes ont proliféré. La table est devenue si large que la plupart des requêtes lisaient bien plus de données que nécessaire. Le buffer pool InnoDB a été chahuté. Le retard de réplication a empiré parce que le job nocturne écrivait une énorme quantité de données en une courte fenêtre.

Pire : comme le job tournait la nuit, l’entreprise s’est mise à attendre les chiffres « du jour » à 10 h. Ils avaient ceux d’hier. Les équipes ont commencé à lancer des backfills partiels pendant la journée. Cela a heurté le trafic de pointe et créé une fenêtre d’incident quotidienne prévisible. L’« optimisation » est devenue un calendrier opérationnel de douleur.

La reprise a été un retour aux bases : garder des tables faits et dimensions normalisées, construire un petit ensemble de tables de résumé ciblées pour les métriques vraiment chaudes, et les rafraîchir de façon incrémentale (horaire ou streaming) avec une logique idempotente. Les tableaux de bord sont devenus plus rapides, et le système a cessé de produire une surprise quotidienne.

Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la journée

La société C avait MongoDB (produit) et MySQL (reporting). La pratique qui les a sauvés n’était pas une architecture sophistiquée. C’était une routine hebdomadaire : revoir le slow query log, examiner les graphes de retard de réplica, et exécuter une petite suite de « requêtes top dashboard » contre des données semblables à la production avec EXPLAIN/explain() capturés.

C’était ennuyeux. Cela a aussi créé un langage partagé entre analystes de données et SRE. Les analystes ont appris à formuler des questions qui n’exigeaient pas de scanner l’univers. Le SRE a appris quelles métriques étaient critiques pour l’entreprise et méritaient des tables de résumé ou des réplicas dédiés.

Quand l’entreprise a eu un pic de trafic dû à un lancement de partenariat inattendu, la charge reporting a augmenté fortement. L’ingénieur d’astreinte connaissait déjà les deux pires requêtes, les index nécessaires et les throttles sûrs. Ils ont activé un replica en lecture en mode read-only pour les tableaux de bord et limité le débit d’un endpoint « download CSV » qui avait la manie de faire des scans complets de table.

Pas d’héroïsme. Pas de réunion générale. Juste des opérations prévisibles construites à partir d’une habitude ennuyeuse. Le lancement du partenariat est devenu une histoire de revenu, pas une histoire d’indisponibilité.

Tâches pratiques (commandes, sorties et décisions)

Voici des tâches réelles que vous pouvez exécuter aujourd’hui. Chacune inclut : la commande, ce que signifie la sortie, et la décision à prendre. Utilisez-les comme checklist de travail, pas comme trivia.

Task 1: MySQL — identify the top time-consuming query patterns

cr0x@server:~$ sudo pt-query-digest /var/log/mysql/mysql-slow.log | head -n 40
# 120s user time, 2.1s system time, 28.50M rss, 120.00M vsz
# Query_time distribution
#   1us
#  10ms
# 100ms  ####
#    1s  ##########
#   10s  #####
# Profile
# Rank Query ID           Response time Calls R/Call  V/M   Item
# ==== ================== ============= ===== ======= ===== ============
#    1 0xA1B2C3D4E5F6A7B8  420.1234  1200 0.3501  0.02  SELECT orders...
#    2 0x1122334455667788  180.0000   200 0.9000  0.01  SELECT users...

Signification : Vous obtenez quelles formes de requêtes coûtent le plus de temps total, pas seulement la requête la plus lente. La douleur du reporting est généralement « une requête modérément lente appelée beaucoup ».

Décision : Corrigez les rangs #1–#3 en premier : ajoutez des index, réécrivez, ou déplacez-les vers un replica/table de résumé. Ignorez la requête rare « licorne » tant que le troupeau ne cesse pas de piétiner.

Task 2: MySQL — check replica lag before blaming the query

cr0x@server:~$ mysql -e "SHOW SLAVE STATUS\G" | egrep "Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running"
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 187

Signification : Votre replica de reporting a environ ~3 minutes de retard. Les tableaux de bord peuvent afficher des nombres « incorrects » et les analystes appelleront ça un bug de données.

Décision : Si le décalage est inacceptable, réduisez la pression d’écriture (ETL batch), augmentez la capacité du replica, ou orientez certains tableaux de bord vers des résultats mis en cache. Ne peaufinez pas une requête sur un replica qui étouffe sous le retard d’application.

Task 3: MySQL — verify what the optimizer is actually doing

cr0x@server:~$ mysql -e "EXPLAIN FORMAT=tree SELECT c.segment, DATE(o.created_at) d, SUM(o.total) s FROM orders o JOIN customers c ON c.id=o.customer_id WHERE o.created_at >= '2025-01-01' AND o.created_at < '2025-02-01' GROUP BY c.segment, d;"
EXPLAIN: -> Aggregate using temporary table  (cost=...)
    -> Nested loop inner join  (cost=...)
        -> Index range scan on orders using idx_orders_created_at  (cost=...)
        -> Single-row index lookup on customers using PRIMARY (id=o.customer_id)

Signification : La requête utilise un range scan sur orders.created_at, puis des lookups PK sur customers, puis une table temp pour le groupement.

Décision : Si la table temp est énorme, envisagez un index composite pour supporter les patterns group-by (ou une table de résumé). Si le range scan est trop large, partitionnez par date ou resserrez les prédicats.

Task 4: MySQL — find whether temp tables are spilling to disk

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%';"
+-------------------------+----------+
| Variable_name           | Value    |
+-------------------------+----------+
| Created_tmp_disk_tables | 48219    |
| Created_tmp_tables      | 51200    |
+-------------------------+----------+

Signification : Une grande partie des tables temporaires frappent le disque, souvent à cause de gros sorts/group-bys.

Décision : Peaufinez les requêtes/indexes pour réduire les tables temporaires, ou ajustez les limites mémoire soigneusement. Pour le reporting, préférez la pré-agrégation plutôt que « plus d’espace temporaire » comme stratégie.

Task 5: MySQL — check buffer pool pressure (are you caching anything?)

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';"
+--------------------------+-----------+
| Variable_name            | Value     |
+--------------------------+-----------+
| Innodb_buffer_pool_reads | 9812234   |
+--------------------------+-----------+
+----------------------------------+-------------+
| Variable_name                    | Value       |
+----------------------------------+-------------+
| Innodb_buffer_pool_read_requests | 2048123456  |
+----------------------------------+-------------+

Signification : Beaucoup de lectures logiques sont servies depuis la mémoire ; certaines lectures physiques ont encore lieu. Si Innodb_buffer_pool_reads monte rapidement pendant l’utilisation des dashboards, vous thrasherez.

Décision : Envisagez de séparer le reporting sur des replicas avec plus de mémoire, ajouter des index couvrants, ou décharger vers un entrepôt. Ne laissez pas les tableaux de bord évincer les pages chaudes OLTP.

Task 6: MySQL — confirm you have the right composite index (or don’t)

cr0x@server:~$ mysql -e "SHOW INDEX FROM orders;"
+--------+------------+------------------------+--------------+-------------+-----------+-------------+
| Table  | Key_name   | Column_name            | Seq_in_index | Cardinality | Non_unique| Index_type  |
+--------+------------+------------------------+--------------+-------------+-----------+-------------+
| orders | PRIMARY    | id                     | 1            | 50000000    | 0         | BTREE       |
| orders | idx_orders_created_at | created_at   | 1            | 3650        | 1         | BTREE       |
| orders | idx_orders_customer_id| customer_id  | 1            | 2000000     | 1         | BTREE       |
+--------+------------+------------------------+--------------+-------------+-----------+-------------+

Signification : Vous avez des index mono-colonne mais pas un index composite comme (created_at, customer_id) ou (created_at, status) qui correspond aux filtres de reporting.

Décision : Ajoutez des index composites pour les principaux prédicats de reporting. Faites-le d’abord sur un replica, mesurez, puis déployez. Les index ne sont pas gratuits — optimisez pour les requêtes que vous exécutez réellement.

Task 7: MongoDB — identify heavy aggregations and whether they spill

cr0x@server:~$ mongosh --quiet --eval 'db.orders.explain("executionStats").aggregate([{ $match: { createdAt: { $gte: ISODate("2025-01-01"), $lt: ISODate("2025-02-01") } } },{ $group: { _id: "$segment", revenue: { $sum: "$total" } } }], { allowDiskUse: true })'
{
  "stages": [
    { "$cursor": { "queryPlanner": { "winningPlan": { "stage": "IXSCAN" } }, "executionStats": { "totalDocsExamined": 4200000, "totalKeysExamined": 4200000 } } },
    { "$group": { "usedDisk": true, "spills": 3 } }
  ]
}

Signification : Le groupement a débordé sur le disque (usedDisk: true). C’est un témoin de latence et d’IO.

Décision : Réduisez la cardinalité des clés de groupement, pré-aggrégez, ou déplacez la charge hors de MongoDB. Si le spill disque est fréquent pour des dashboards centraux, vous faites de l’analytique au mauvais endroit.

Task 8: MongoDB — check if a query is using the intended index

cr0x@server:~$ mongosh --quiet --eval 'db.orders.find({createdAt: {$gte: ISODate("2025-01-01"), $lt: ISODate("2025-02-01")}, status: "paid"}).sort({createdAt: -1}).hint({createdAt: 1}).explain("executionStats").queryPlanner.winningPlan'
{
  "stage": "FETCH",
  "inputStage": {
    "stage": "IXSCAN",
    "keyPattern": { "createdAt": 1 },
    "direction": "forward"
  }
}

Signification : Le planner peut utiliser l’index createdAt, mais votre filtre status n’est pas dans l’index ; il peut examiner plus de documents que nécessaire.

Décision : Ajoutez un index composite comme {status: 1, createdAt: 1} si c’est une requête importante. Si vous triez aussi par createdAt, choisissez l’ordre de l’index avec soin.

Task 9: MongoDB — find current operations causing pain

cr0x@server:~$ mongosh --quiet --eval 'db.currentOp({ "active": true, "secs_running": { $gte: 5 } }).inprog.map(op => ({secs: op.secs_running, ns: op.ns, op: op.op, command: op.command && Object.keys(op.command)[0]}))'
[
  { "secs": 42, "ns": "prod.orders", "op": "command", "command": "aggregate" },
  { "secs": 19, "ns": "prod.events", "op": "command", "command": "find" }
]

Signification : Les opérations longue durée incluent une agrégation sur orders. C’est souvent un dashboard ou une exportation.

Décision : Tuez ou ralentissez l’auteur connu, ajoutez des garde-fous (timeouts, maxTimeMS), et déplacez cette requête vers un store de reporting.

Task 10: OS — check disk latency when analytics “randomly slows down”

cr0x@server:~$ iostat -x 1 3
avg-cpu:  %user %nice %system %iowait  %steal  %idle
          12.00  0.00   6.00    18.00    0.00  64.00

Device            r/s     w/s   rkB/s   wkB/s  await  svctm  %util
nvme0n1         820.0  1100.0  64000   72000   28.5   0.9   97.0

Signification : await est élevé et %util est saturé. Votre base est IO-bound en ce moment, pas « lente CPU ».

Décision : Réduisez les scans larges (indexes/partitionnement), déplacez le reporting sur des réplicas avec stockage séparé, ou planifiez les jobs lourds hors-peak. Ne continuez pas à optimiser la syntaxe des requêtes si le disque est saturé.

Task 11: OS — confirm whether memory pressure is causing swap storms

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  1  524288  10240  12000  98000   15   25  1800  2400  900 1200 20  8 50 22  0

Signification : Swap-in/out (si/so) est non-nul sous charge. C’est un tueur de performances pour les bases, surtout pour les agrégations.

Décision : Arrêtez d’exécuter le reporting sur des nœuds sous-dimensionnés. Ajoutez de la mémoire, isolez le reporting, ou limitez la concurrence. Une base qui swappe, c’est une façon polie de dire « je refuse ».

Task 12: MySQL — measure connection and thread saturation

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW GLOBAL STATUS LIKE 'Connections'; SHOW VARIABLES LIKE 'max_connections';"
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| Threads_running | 184   |
+-----------------+-------+
+---------------+----------+
| Variable_name | Value    |
+---------------+----------+
| Connections   | 9812231  |
+---------------+----------+
+-----------------+------+
| Variable_name   | Value|
+-----------------+------+
| max_connections | 500  |
+-----------------+------+

Signification : Beaucoup de threads concurrents ; vous n’êtes pas au max de connections, mais vous pouvez subir des changements de contexte CPU ou des contentions internes.

Décision : Ajoutez du pooling de connexions, réduisez le parallélisme des tableaux de bord, ou orientez vers un replica. Si le trafic reporting cause des pics de threads, isolez-le.

Task 13: MySQL — find the biggest tables and index bloat (storage reality check)

cr0x@server:~$ mysql -e "SELECT table_name, ROUND((data_length+index_length)/1024/1024/1024,2) AS size_gb FROM information_schema.tables WHERE table_schema='prod' ORDER BY (data_length+index_length) DESC LIMIT 10;"
+------------+---------+
| table_name | size_gb |
+------------+---------+
| events     | 420.55  |
| orders     | 88.12   |
| users      | 12.40   |
+------------+---------+

Signification : Si votre plus grande table alimente aussi les tableaux de bord, vous scannez beaucoup de données que vous l’admettiez ou non.

Décision : Partitionnez par date, archivez, ou déplacez les events vers un entrepôt. Si « events » fait 400+ GB et sert au reporting interactif, arrêtez de prétendre que c’est juste de l’OLTP.

Task 14: MongoDB — check index inventory and spot missing compound indexes

cr0x@server:~$ mongosh --quiet --eval 'db.orders.getIndexes().map(i => i.key)'
[
  { "_id": 1 },
  { "createdAt": 1 },
  { "customerId": 1 }
]

Signification : Seuls des index mono-champs existent. Les agrégations filtrant par plusieurs champs examineront trop de documents.

Décision : Ajoutez des index composés alignés sur vos principaux filtres de dashboard (par ex. {status: 1, createdAt: 1}). Si votre analytique nécessite une douzaine de tels index, c’est un indice que vous devriez plutôt ETL vers SQL/entrepôt.

Task 15: MySQL — validate binlog/replication settings for reporting replicas

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'binlog_format'; SHOW VARIABLES LIKE 'sync_binlog';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| sync_binlog   | 1     |
+---------------+-------+

Signification : Le binlogging durable est activé. Excellent pour la conformité, mais cela peut ajouter de l’overhead d’écriture. Votre stratégie de reporting ne devrait pas reposer sur des écritures agressives pendant les pics.

Décision : Si l’ETL de reporting martèle le primaire, déplacez l’ETL hors du chemin primaire (CDC, stream, ou extraction basée sur replica). Ne « optimisez » pas les paramètres de durabilité parce que les tableaux de bord sont lents.

Playbook de diagnostic rapide : trouver le goulot d’étranglement en minutes

Voici le playbook que je veux avoir sur le mur quand un incident de dashboard survient. Vous n’avez pas le temps d’être élégant. Vous avez le temps d’être correct.

Premier : décidez si c’est le coût de la requête, la concurrence ou l’infrastructure

  1. Vérifiez la saturation : CPU, IO, mémoire, réseau. Si l’IO est saturé ou s’il y a du swapping, les indexes ne vous sauveront pas pendant l’incident.
  2. Vérifiez la concurrence : nombre de threads/ops en cours. Le reporting échoue souvent à cause de trop de requêtes « raisonnables » en parallèle.
  3. Vérifiez les principaux coupables : slow query digest (MySQL) ou currentOp/slow logs (MongoDB).

Deuxième : isolez la charge

  1. Orientez les tableaux de bord vers un replica en lecture ou un nœud reporting dédié si vous en avez un.
  2. Limitez le débit des exports et endpoints « download CSV » (ce sont des jobs batch discrets).
  3. Activez le cache pour les graphiques principaux si la correction tolère une légère obsolescence.

Troisième : prouvez le plan et corrigez la chose à fort levier

  • MySQL : lancez EXPLAIN, ajoutez l’index composite manquant, ou réécrivez pour réduire les lignes scannées. Si le groupement est énorme, construisez une table de rollup.
  • MongoDB : lancez explain("executionStats") pour le pipeline, regardez les docs examinés vs renvoyés, et les spills disque. Si ça déborde et que la cardinalité est élevée, arrêtez et déchargez.

Quatrième : prévenir la récurrence

  • Créez un « budget de requêtes dashboard » (temps max, docs scannés max, concurrence max).
  • Ajoutez une revue périodique des slow queries.
  • Définissez les métriques partagées dans des vues/tables, pas dans des fragments de code applicatif.

Erreurs courantes : symptômes → cause racine → correctif

1) Symptom: dashboards get slower every week without code changes

Cause racine : croissance des données + absence de partitionnement/archivage ; les requêtes scannent des plages temporelles de plus en plus larges.

Correctif : partitionnez les tables faits par date (MySQL), construisez des tables de résumé, ou ETL vers un entrepôt. Dans MongoDB, introduisez des collections par bucket temporel ou déchargez l’analytique.

2) Symptom: the “same query” is fast sometimes and terrible other times

Cause racine : sensibilité aux paramètres et biais. Un client/segment a 100× de données ; l’optimiseur choisit un plan correct pour les petits segments mais catastrophique pour les gros.

Correctif : dans MySQL, ajoutez des index composites alignés sur les prédicats sélectifs ; considérez des réécritures de requêtes ; maintenez des stats à jour. Dans MongoDB, assurez-vous que le premier $match est sélectif et indexé ; évitez les $group haute-cardinalité sans pré-agrégation.

3) Symptom: reporting “works” but numbers don’t match across teams

Cause racine : définitions de métriques implémentées ad-hoc dans des pipelines ou du code applicatif ; aucune couche sémantique partagée.

Correctif : définissez les métriques centralement (vues SQL / transformations contrôlées) ; versionnez-les ; testez avec des jeux de données fixes. Traitez les définitions de métriques comme des contrats d’API.

4) Symptom: MongoDB cluster becomes unstable during business reviews

Cause racine : grosses agrégations avec spills disque ; $lookup ; indexes inadéquats ; clé de sharding optimisée pour les écritures, pas pour le reporting.

Correctif : déplacez l’analytique vers SQL/entrepôt ; pré-aggrégez ; redessinez la clé de shard pour les requêtes analytiques uniquement si vous acceptez les compromis OLTP. La plupart des équipes ne devraient pas mener ce combat dans MongoDB.

5) Symptom: MySQL primary slows down when analysts run queries

Cause racine : exécution du reporting sur le primaire ; lectures massives et tables temp évincant le cache OLTP ; verrous/contention.

Correctif : séparez le reporting via des replicas ; appliquez des comptes en lecture seule ; limitez le temps d’exécution des requêtes ; utilisez des tables de résumé. Protégez le primaire comme s’il payait votre salaire (parce que c’est le cas).

6) Symptom: replica lag spikes during ETL or “refresh metrics” jobs

Cause racine : rafales d’écritures lourdes, grosses transactions, ou ETL qui met à jour beaucoup de lignes ; le replica ne peut pas appliquer assez vite.

Correctif : mises à jour incrémentales, chunking, loads idempotents et planification. Envisagez CDC vers un store reporting séparé plutôt que de marteler MySQL avec des updates.

7) Symptom: adding indexes “did nothing”

Cause racine : mauvais ordre d’index, faible sélectivité, fonctions sur colonnes (prédicats non-sargables), ou la requête est en fait limitée par le groupement/le tri.

Correctif : inspectez les plans ; évitez d’enrouler les colonnes indexées dans des fonctions ; ajoutez des index couvrants/composés ; ou pré-aggrégez. Les index ne sont pas magiques ; ce sont des mathématiques avec conséquences.

Checklists / plan étape par étape

Checklist de décision : le reporting doit-il tourner directement sur MongoDB ?

  • La plupart des rapports sont-ils sur une seule collection avec des prédicats sélectifs ? Si oui, peut-être.
  • Avez-vous besoin de jointures fréquentes entre entités, de définitions changeantes, ou de modélisation dimensionnelle ? Si oui, prévoyez SQL/entrepôt.
  • Avez-vous besoin d’une compatibilité BI avec friction minimale ? SQL gagne.
  • Voyez-vous des spills disque d’agrégation ou de la pression mémoire ? C’est votre panneau de sortie.

Plan étape par étape : construire un chemin reporting sain sans drame

  1. Inventaire des tableaux de bord principaux : listez les 20 requêtes principales par fréquence et importance métier.
  2. Classifiez chaque requête : OLTP-ish (points lookup), analytique (scan/group), export/batch (gros dumps).
  3. Choisissez une stratégie d’isolation :
    • MySQL : réplicas en lecture pour les tableaux de bord, replica séparé pour les exports.
    • MongoDB : nœud analytics séparé uniquement si vous tolérez la cohérence éventuelle ; sinon ETL dehors.
  4. Modélisez les données explicitement :
    • Définissez faits (orders, events) et dimensions (customer, product, campaign).
    • Décidez quelles dimensions ont besoin d’historique (slowly changing) vs snapshots.
  5. Créez une couche sémantique minimale : vues SQL pour les définitions partagées de métriques et filtres.
  6. Ajoutez les bons index : basés sur des patterns de requêtes réels, pas sur l’intuition.
  7. Pré-aggrégez les métriques chaudes : résumés journaliers/horaire, mis à jour de façon incrémentale.
  8. Mettez des garde-fous :
    • Timeouts de requête, limites de concurrence, taille max d’export.
    • Rôles séparés pour analystes vs comptes de service.
  9. Opérationnalisez :
    • Cadence de revue des slow queries.
    • Alertes et runbooks pour le lag de réplica.
    • Planification de capacité liée à l’usage des tableaux de bord.
  10. Migrer progressivement : commencez par un tableau de bord, validez les chiffres, puis étendez.

Checklist de migration : déplacer l’analytique de MongoDB vers MySQL (ou une couche SQL)

  • Définissez un schéma canonique d’événements/commandes (avec types et nullabilité).
  • Décidez des clés idempotence pour les loads (ex. event_id).
  • Backfill des données historiques par chunks ; validez les totaux journaliers.
  • Exécutez dual-write ou sync basé CDC jusqu’à ce que la confiance soit élevée.
  • Gelez les définitions dans des vues ; exigez une revue pour tout changement.
  • Basculez les tableaux de bord un par un ; gardez l’ancien pipeline pour une fenêtre de rollback.

FAQ

1) MongoDB est-il « mauvais » pour l’analytique ?

Non. Il est bon pour l’analytique qui correspond à ses forces : requêtes sur une seule collection avec bons index et cardinalité de regroupement raisonnable. Il est moins adapté comme backend BI généraliste sous jointures ad-hoc et définitions métier évolutives.

2) Pourquoi les jointures importent-elles autant pour le reporting ?

Parce que les questions de reporting sont relationnelles : faits plus dimensions. Si vous évitez les jointures en tout imbriquant, vous choisissez un modèle de versionnement et vous payez en backfills, duplication et vérité incohérente.

3) MongoDB ne peut-il pas simplement utiliser $lookup et fonctionner ?

$lookup est des jointures, mais dans un modèle pipeline qui peut créer de larges résultats intermédiaires et une pression mémoire. Cela peut fonctionner pour des jointures petites. Cela devient souvent fragile à l’échelle, surtout sous requêtes ad-hoc.

4) Pourquoi ne pas exécuter le reporting sur le primaire MySQL si c’est « assez rapide » ?

Parce que « assez rapide » est un mensonge que vous vous racontez jusqu’à ce qu’une présentation du conseil soit due. La charge reporting est en rafales et imprévisible, et elle peut évincer les pages chaudes OLTP, augmenter la contention de verrous, et déclencher une latence en cascade. Utilisez des replicas ou un store reporting séparé.

5) Quel est le gain le plus rapide pour la performance reporting MySQL ?

Généralement : ajoutez l’index composite correct pour le pattern de requête principal du tableau de bord, et orientez le trafic reporting vers un replica en lecture. Le deuxième gain le plus rapide est une table de résumé pour une métrique chaude.

6) Quel est le gain le plus rapide pour la performance reporting MongoDB ?

Rendez le premier $match sélectif et indexé ; ajoutez des index composés qui correspondent aux patterns de filtre ; utilisez allowDiskUse quand c’est approprié. Si la requête déborde encore et scanne des millions de documents, arrêtez d’optimiser et déchargez.

7) Devons-nous stocker des events analytiques dans MongoDB du tout ?

Si votre cas d’usage event est centré application (« afficher le fil d’activité utilisateur »), MongoDB peut convenir. Pour de l’analytique grade BI, vous voudrez probablement un système optimisé pour les scans, agrégations et compression. Beaucoup d’équipes gardent MongoDB comme source et répliquent les events vers l’extérieur.

8) Comment garder des chiffres de reporting cohérents entre équipes ?

Définissez les métriques centralement (vues SQL / transformations contrôlées), versionnez les changements, et testez avec des jeux de données fixes. Évitez que la logique métrique vive dans du code applicatif dispersé ou des pipelines one-off.

9) MySQL suffit-il, ou avons-nous besoin d’un entrepôt ?

Si votre reporting est opérationnel (fenêtres temporelles récentes, volume de données modéré, dashboards interactifs), MySQL avec réplicas et tables de résumé peut suffire. Si vous faites de l’historique profond, des scans larges et des découpages multi-dimensionnels lourds, un entrepôt réduira la douleur.

10) Et si notre équipe connaît déjà MongoDB et pas SQL ?

Apprenez assez de SQL pour exploiter le reporting sainement. Le reporting est une interface métier, pas un concours de préférences d’ingénierie. La maîtrise du SQL paie le loyer.

Conclusion : prochaines étapes pragmatiques

Si vous choisissez entre MySQL et MongoDB spécifiquement pour le reporting et l’analytique, choisissez SQL à moins que vous ne puissiez clairement prouver que vos questions de reporting sont en forme de document et le resteront. Elles ne le resteront pas. L’organisation changera d’avis. Toujours.

Faites ceci ensuite :

  1. Déplacez le reporting hors du chemin primaire de la base : réplicas, un cluster reporting, ou un store SQL séparé.
  2. Notez vos 20 requêtes reporting principales et traitez-les comme des citoyens de première classe : index, plans et budgets.
  3. Arrêtez d’imbriquer des dimensions mutables dans des faits immuables sauf si vous voulez explicitement la « vérité telle qu’écrite ». Modélisez l’historique intentionnellement.
  4. Construisez une petite couche sémantique (vues/tables) pour que l’entreprise puisse débattre des définitions sans débattre des pipelines.
  5. Opérationnalisez les habitudes ennuyeuses : revue des slow queries, alertes de lag de réplica et tests de charge sur des données similaires à la production.

Les équipes « reviennent à SQL » parce que SQL est plus ancien, oui — mais surtout parce qu’il est opérationnellement lisible sous pression. Quand le reporting devient un système critique pour l’entreprise, la lisibilité bat l’ingéniosité. À chaque fois.

← Précédent
Échecs SPF des e-mails : 5 erreurs d’enregistrement qui cassent la délivrabilité (et comment les corriger)
Suivant →
Reset CSS moderne pour 2026 : minimal, sûr pour formulaires et médias

Laisser un commentaire