Sortir de l'enfer des jointures SQL avec Apache AGE et PostgreSQL
Vous avez déjà regardé une requête SQL de 140 lignes avec sept auto-jointures et vous êtes demandé s'il n'y avait pas une meilleure approche ? C'est exactement la situation décrite par Ahmet Gedik dans son article sur dev.to, et la solution qu'il a trouvée mérite qu'on s'y attarde sérieusement.
Le problème : quand le relationnel atteint ses limites
Certains problèmes sont fondamentalement des problèmes de graphe. La recommandation de contenu en est l'exemple parfait : trouver des vidéos regardées par des utilisateurs ayant aussi regardé telle vidéo, issues de chaînes dans une même grappe régionale, à deux sauts d'un tag thématique, et actuellement en tendance dans au moins une région... En SQL relationnel, cela se traduit par une cascade de jointures qui devient vite ingérable.
Le résultat concret dans le cas étudié : une requête de 380 ms, difficile à maintenir, impossible à faire évoluer sereinement. Le tout sur une architecture PHP 8.4 avec PostgreSQL comme base relationnelle principale.
Le constat est clair : les traversées multi-sauts sont la faiblesse structurelle du modèle relationnel. Chaque « hop » supplémentaire dans le graphe de relations coûte une jointure supplémentaire. À deux ou trois niveaux de profondeur, les performances s'effondrent et la lisibilité du code avec.
Apache AGE : du Cypher dans PostgreSQL
Apache AGE (A Graph Extension) est une extension PostgreSQL qui implémente le langage openCypher — le langage de requête standardisé des bases de données graphe — directement dans votre instance PostgreSQL existante.
L'intérêt architectural est immédiat : pas de nouvelle infrastructure à gérer. Pas de Neo4j à déployer, pas de synchronisation entre deux systèmes de stockage, pas de double source de vérité. Les données graphe cohabitent avec vos tables relationnelles dans la même base PostgreSQL.
La migration du layer relationnel vers AGE a permis de réécrire la requête monstrueuse en une requête Cypher lisible et expressive. Voici à quoi ressemble une traversée typique en Cypher :
MATCH (v:Video {id: $video_id})<-[:WATCHED]-(u:Viewer)-[:WATCHED]->(rec:Video)
MATCH (rec)-[:TAGGED]->(t:Tag)<-[:TAGGED]-(v)
MATCH (rec)-[:BELONGS_TO]->(c:Channel)-[:IN_REGION]->(r:Region)
WHERE rec.trending_score > 0.7
RETURN rec, count(u) AS shared_viewers
ORDER BY shared_viewers DESC
LIMIT 20
Cette lisibilité n'est pas qu'esthétique : elle réduit directement le coût de maintenance et le risque d'introduction de bugs lors des évolutions.
Ce que ça change côté PHP
Du côté applicatif PHP, l'intégration se fait via PDO ou toute abstraction compatible PostgreSQL. Apache AGE expose une fonction ag_catalog.cypher() que vous appelez depuis SQL standard :
$stmt = $pdo->prepare("
SELECT * FROM ag_catalog.cypher(
'video_graph',
$$
MATCH (v:Video {id: $video_id})<-[:WATCHED]-(u:Viewer)-[:WATCHED]->(rec:Video)
RETURN rec.id AS id, rec.title AS title, count(u) AS score
ORDER BY score DESC LIMIT 20
$$,
:params
) AS (id agtype, title agtype, score agtype)
");
$stmt->execute(['params' => json_encode(['video_id' => $videoId])]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
Le typage agtype est spécifique à AGE et nécessite une attention particulière lors du mapping vers vos entités PHP. C'est l'un des points d'adaptation à anticiper dans votre couche de persistance, notamment si vous utilisez Doctrine ORM.
Points d'attention avant de migrer
L'article source est honnête sur les frictions rencontrées, et c'est ce qui le rend précieux. Quelques éléments à intégrer dans votre évaluation :
La modélisation du graphe demande une réflexion initiale. Contrairement au schéma relationnel où l'on normalise des entités, le modèle graphe se pense en termes de nœuds et de relations. Il faut identifier quelles entités deviennent des nœuds et quelles associations deviennent des arêtes typées. Un mauvais modèle initial se paie cher en refactoring.
La gestion des types agtype en PHP est non triviale. AGE retourne ses valeurs dans un format propriétaire qui n'est pas nativement désérialisé par PDO. Vous devrez écrire ou intégrer une couche de conversion, surtout si vous manipulez des propriétés complexes (tableaux, objets imbriqués).
L'outillage est encore jeune. Apache AGE est un projet Apache relativement récent. Les outils de visualisation, de migration et de monitoring sont moins matures que ceux de l'écosystème PostgreSQL standard. Prévoyez du temps pour la mise en place de l'observabilité.
Les gains sont réels mais contextuels. Le passage de 380 ms à 22 ms est spectaculaire, mais il reflète un cas d'usage précis : des traversées multi-sauts avec filtres à chaque niveau. Pour des requêtes simples ou des accès directs par clé, PostgreSQL relationnel reste parfaitement adapté. AGE est un complément, pas un remplacement.
Conclusion
Apache AGE est une réponse architecturalement élégante à un problème réel : intégrer de la logique orientée graphe sans sortir de l'écosystème PostgreSQL. Pour les projets PHP/Symfony qui gèrent des données fortement interconnectées — recommandations, réseaux sociaux, gestion de dépendances, arbres de permissions — c'est une piste sérieuse à évaluer.
La vraie question n'est pas « faut-il utiliser une base graphe ? » mais « quelles parties de mon modèle de données sont naturellement des graphes ? ». Si la réponse implique des traversées récursives ou multi-sauts, AGE mérite une proof of concept.
Source originale : Using Apache AGE for Graph-Based Video Relationship Queries at Scale par Ahmet Gedik sur dev.to.