Il y a une question qu'on nous pose souvent : "Mes données utilisateurs sont dans PostgreSQL et mes événements analytics dans MongoDB. Je peux les joindre ?"
Jusqu'ici, la réponse impliquait un script Python, un ETL, ou une migration. Avec la fédération inter-bases de QoreDB, la réponse est une requête SQL.
SELECT u.email, COUNT(e._id) as events
FROM prod_pg.public.users u
LEFT JOIN analytics_mongo.events e
ON e.user_id = u.id
GROUP BY u.email
ORDER BY events DESC
PostgreSQL à gauche (avec son schéma public), MongoDB à droite (directement la collection), un JOIN au milieu. C'est tout. Dans cet article, on explique comment ça fonctionne sous le capot — et pourquoi c'était loin d'être trivial à construire.
Le problème concret
Les architectures modernes fragmentent les données. La base transactionnelle est dans PostgreSQL. Les logs applicatifs finissent dans MongoDB. Le data warehouse tourne sur un SQL Server hérité. Et un jour, quelqu'un a besoin de croiser des données entre deux de ces systèmes.
Les solutions classiques sont lourdes : écrire un script qui exporte d'un côté, transforme, et charge de l'autre. Monter un pipeline ETL. Ou dupliquer les données dans un data lake. Pour une exploration ponctuelle, ces approches sont disproportionnées.
QoreDB est déjà connecté à toutes ces bases. L'idée de la fédération est simple : puisque les données sont accessibles, pourquoi ne pas les joindre directement ?
L'idée : DuckDB comme couche éphémère
La fédération repose sur une intuition architecturale : utiliser DuckDB — un moteur OLAP in-memory — comme table de travail temporaire.
Le principe est le suivant. QoreDB récupère les données depuis chaque source en parallèle, les charge dans une instance DuckDB éphémère créée en mémoire, puis exécute la requête fédérée dans DuckDB. Une fois les résultats retournés, l'instance DuckDB est libérée. Rien ne persiste.
DuckDB est idéal pour ce rôle : il est embarquable (pas de serveur), optimisé pour les requêtes analytiques (agrégations, joins, tris), et capable d'ingérer des données rapidement. C'est un moteur de requêtes temporaire, pas un stockage permanent.
Les quatre phases du pipeline
Phase 1 : parsing de la requête
Quand l'utilisateur écrit prod_pg.public.users, QoreDB détecte un identifiant composé dont la première partie correspond à un alias de connexion connu. C'est le signal qu'il s'agit d'une requête fédérée.
Le format s'adapte au moteur de la source. PostgreSQL utilise trois ou quatre parties (connexion.base.table ou connexion.base.schéma.table) pour refléter sa hiérarchie avec schémas. MySQL, MongoDB et les autres drivers utilisent deux parties (connexion.table), puisque la base est déjà sélectionnée dans la connexion. Le parser gère les deux formats de manière transparente.
Côté frontend, une détection rapide par regex affiche un badge "Federated Query" en temps réel pendant la frappe. Mais la vraie analyse se fait côté Rust, avec un parsing AST complet via le crate sqlparser. Le parser extrait toutes les références fédérées depuis les clauses FROM et JOIN, vérifie que la requête est un SELECT unique (les mutations sont rejetées), et valide que chaque alias de connexion correspond à une session active.
Ce parsing AST est essentiel — pas de regex fragiles, pas de hacks de chaînes de caractères. La requête est analysée comme un arbre syntaxique, ce qui permet de gérer correctement les sous-requêtes, les CTE, les expressions CASE, et toute la complexité du SQL réel.
Phase 2 : planification
Le planner prend les références extraites et construit un plan d'exécution. Pour chaque table source, il résout l'alias de connexion vers l'identifiant de session, détermine la requête à envoyer à la source, et génère un nom de table temporaire pour DuckDB (par exemple __fed_users_0, __fed_events_1).
Un détail important : le planner génère les requêtes sources avec un quoting adapté au dialecte SQL de chaque driver. PostgreSQL reçoit des identifiants entre guillemets doubles ("users"), MySQL reçoit des backticks (`profile`), et MongoDB reçoit une requête JSON structurée. Ce détail évite les erreurs de syntaxe qui surviendraient si on envoyait du SQL PostgreSQL à MySQL, ou inversement.
Puis il réécrit la requête originale en remplaçant les identifiants fédérés par les noms de tables temporaires DuckDB. Le SQL que l'utilisateur a écrit est transformé en SQL exécutable localement, tout en préservant la structure complète — joins, conditions, agrégations, tri.
Phase 3 : récupération parallèle des données
C'est ici que la concurrence de Rust entre en jeu. Le manager spawne une tâche tokio pour chaque source et les exécute en parallèle. Chaque tâche envoie une requête adaptée à sa base source : un SELECT avec le quoting du driver pour les bases SQL, et une requête JSON structurée pour MongoDB.
Chaque source a un timeout de 30 secondes, et le pipeline global est plafonné à 60 secondes. Un guard par défaut limite aussi chaque source à 100 000 lignes — au-delà, un warning prévient l'utilisateur que les résultats sont potentiellement tronqués.
Le parallélisme est un vrai gain de performance. Joindre deux tables de 10 000 lignes issues de bases différentes ? Les deux fetchs se font simultanément en ~500ms, au lieu de ~1s en séquentiel.
Phase 4 : exécution dans DuckDB
Les données récupérées sont chargées dans une instance DuckDB en mémoire. Pour chaque source, QoreDB crée une table temporaire avec le schéma détecté, puis insère les lignes par lots de 1 000.
Pour les sources MongoDB, une étape supplémentaire intervient : le flattening automatique. Les documents BSON, qui arrivent normalement sous forme d'une colonne JSON unique, sont automatiquement aplatis en colonnes individuelles — une par clé de premier niveau. Cela permet d'écrire e.profileId ou e.level dans la requête fédérée, comme si la collection MongoDB était une table relationnelle classique. Sans ce flattening, il faudrait utiliser des fonctions d'extraction JSON dans chaque clause — ce qui ruinerait l'ergonomie.
Les types sont convertis au passage via un mapping complet de plus de 60 types. Les tableaux PostgreSQL deviennent des VARCHAR (sérialisés en JSON), les UUID sont convertis en texte, les types réseau (inet, cidr) et géométriques (point, polygon) sont sérialisés en texte, et les types date/heure sont mappés vers leurs équivalents DuckDB avec gestion des fuseaux horaires. MySQL et ses types spécifiques (TINYINT, MEDIUMTEXT, YEAR) sont également couverts. Cette conversion est pragmatique — elle privilégie la compatibilité sur la fidélité parfaite des types.
Une fois toutes les tables chargées, DuckDB exécute la requête réécrite. C'est là que les agrégations, les tris et les joins se produisent réellement. DuckDB est optimisé pour ce type d'opérations analytiques, bien plus qu'aucune des bases sources individuelles ne le serait pour une requête cross-database.
Les résultats sont retournés avec des métadonnées détaillées : nombre de lignes par source, temps de fetch par source, temps d'exécution DuckDB, et temps total du pipeline.
L'interface : conçue pour l'exploration
La fédération ne serait pas utilisable sans une interface adaptée. QoreDB propose un source bar — une barre horizontale affichant toutes les connexions actives sous forme de pills cliquables. Chaque pill montre l'icône du driver et l'alias de la connexion.
En cliquant sur une connexion, un popover affiche l'arborescence de la base : schémas, tables, colonnes. Cliquer sur une table l'insère intelligemment dans l'éditeur selon le contexte du curseur — en tant que JOIN, LEFT JOIN, clause WHERE, ou simple référence. Des raccourcis clavier accélèrent le processus : J pour un JOIN, L pour un LEFT JOIN.
Pour les nouveaux utilisateurs, un état vide contextuel guide la prise en main. Si une seule connexion est active, QoreDB indique qu'il en faut au moins deux. Avec deux connexions ou plus, un template de requête est proposé avec un bouton "Essayer" qui pré-remplit l'éditeur.
Exemple concret : audit cross-database
Prenons un cas réel. Vous avez une base PostgreSQL de production avec les comptes utilisateurs, et une base MongoDB qui collecte les événements applicatifs. Votre PM veut savoir combien d'utilisateurs créés en 2025 ont généré plus de 50 événements.
SELECT
u.id,
u.email,
u.created_at,
COUNT(e._id) as event_count
FROM prod_pg.public.users u
LEFT JOIN analytics_mongo.events e
ON e.user_id = u.id
WHERE u.created_at > '2025-01-01'
GROUP BY u.id, u.email, u.created_at
HAVING COUNT(e._id) > 50
ORDER BY event_count DESC
Voici ce qui se passe en coulisses :
- Le parser identifie
prod_pgetanalytics_mongocomme références fédérées — trois parties pour PostgreSQL (avec schémapublic), deux parties pour MongoDB (collection directe). - Le planner génère deux requêtes sources adaptées au dialecte :
SELECT * FROM "users" LIMIT 100000avec guillemets doubles pour PostgreSQL, et une requête JSON structurée pour MongoDB. - Les deux fetchs s'exécutent en parallèle via tokio.
- Les documents MongoDB sont automatiquement aplatis en colonnes individuelles.
- Les résultats sont chargés dans DuckDB, qui exécute le GROUP BY, le HAVING et le ORDER BY.
- Le résultat final s'affiche dans la grille de résultats, avec les stats de chaque source.
La requête a pris 800ms. Sans fédération, vous auriez écrit un script, attendu qu'il tourne, debuggé les types incompatibles, et perdu une demi-heure.
Sécurité et garde-fous
La fédération est bridée par conception. Seules les requêtes SELECT sont autorisées — toute tentative d'INSERT, UPDATE ou DELETE sur une source fédérée est rejetée par le parser. Les requêtes multi-statements sont aussi bloquées.
Le parsing AST complet empêche les injections SQL : les identifiants de connexion sont validés contre la liste des sessions actives, et les requêtes sources sont construites programmatiquement avec un quoting adapté au dialecte, jamais par concaténation de chaînes.
Le plafond de 100 000 lignes par source protège contre les débordements mémoire. Et les timeouts (30s par source, 60s global) empêchent les requêtes zombies de bloquer l'application.
Ce qui reste à construire
La fédération v1 est volontairement conservatrice sur certains points. Les filtres WHERE ne sont pas encore "poussés" vers les sources — le filtrage se fait entièrement dans DuckDB après le chargement. En pratique, cela signifie que QoreDB charge potentiellement plus de données que nécessaire avant de filtrer localement.
De même, chaque source est actuellement chargée avec SELECT * — la projection des colonnes nécessaires uniquement n'est pas encore implémentée.
Ces optimisations (predicate pushdown et columnar projection) sont sur la roadmap. Elles réduiront le volume de données transférées et accéléreront les requêtes sur de gros volumes. Mais même sans elles, la fédération est déjà fonctionnelle et performante pour l'exploration de données du quotidien.
Pourquoi c'est important
La fédération inter-bases n'est pas une feature gadget. C'est la réponse à un problème que chaque développeur rencontre : les données ne vivent pas dans une seule base. Elles sont dispersées entre le relationnel, le NoSQL, le legacy, et parfois un fichier CSV qui traîne.
Les outils existants — DBeaver, pgAdmin, MongoDB Compass — excellent chacun sur leur moteur, mais s'arrêtent à la frontière de leur base. QoreDB franchit cette frontière. Pas en abstraisant les différences entre moteurs, mais en les faisant coopérer le temps d'une requête.
Le SQL reste le SQL. Les bases restent indépendantes. QoreDB crée juste un pont éphémère entre elles, le temps de trouver la réponse.
Restez informé des nouveautés
Rejoignez notre newsletter pour recevoir les mises à jour majeures, les nouveaux drivers et nos coulisses techniques.




