Graphes – Optimisation et index (LNH)¶
Thème : classement des équipes de la LNH (division Atlantique 2025-26)
Ligue nationale de hockey (LNH) : matchs entre équipes de la division Atlantique, saison 2025-26.
Modèle de données¶
Nœuds :
Relations :
(:Team)-[:HOME_TEAM { goals, outcome, win, points }]->(:Game)
(:Team)-[:AWAY_TEAM { goals, outcome, win, points }]->(:Game)
outcome:'win','loss','overtime_loss'win: 1 si victoire, 0 sinonpoints: 2 (victoire), 1 (défaite en prolongation), 0 (défaite)
Schéma simplifié :
(Bruins)-[:HOME_TEAM {goals:3, outcome:'win', win:1, points:2}]->(Game#42)
(Canadiens)-[:AWAY_TEAM {goals:2, outcome:'loss', win:0, points:0}]->(Game#42)
Partie 1 – Mise en place de la base de données¶
Exécutez ces requêtes dans Neo4j Browser dans l'ordre indiqué.
Étape 1 : Contraintes d'unicité¶
CREATE CONSTRAINT FOR (n:Team) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT FOR (n:Game) REQUIRE n.id IS UNIQUE;
Étape 2 : Création des équipes¶
CREATE
(:Team {id:'team-buf', nickname:'Sabres', slug:'nhl-buf-sabres'}),
(:Team {id:'team-tbl', nickname:'Lightning', slug:'nhl-tbl-lightning'}),
(:Team {id:'team-mtl', nickname:'Canadiens', slug:'nhl-mtl-canadiens'}),
(:Team {id:'team-bos', nickname:'Bruins', slug:'nhl-bos-bruins'}),
(:Team {id:'team-ott', nickname:'Senators', slug:'nhl-ott-senators'}),
(:Team {id:'team-det', nickname:'Red Wings', slug:'nhl-det-red-wings'}),
(:Team {id:'team-tor', nickname:'Maple Leafs', slug:'nhl-tor-maple-leafs'}),
(:Team {id:'team-fla', nickname:'Panthers', slug:'nhl-fla-panthers'});
Étape 3 : Création des matchs et relations HOME_TEAM / AWAY_TEAM¶
Requête de création des 30 matchs
WITH [
{id:'g01', home:'team-bos', away:'team-buf', hg:4, ag:2, ot:false, year:2025, month:10, day:10, att:17565},
{id:'g02', home:'team-tbl', away:'team-mtl', hg:3, ag:1, ot:false, year:2025, month:10, day:11, att:19092},
{id:'g03', home:'team-ott', away:'team-det', hg:3, ag:2, ot:true, year:2025, month:10, day:14, att:18652},
{id:'g04', home:'team-tor', away:'team-fla', hg:1, ag:4, ot:false, year:2025, month:10, day:16, att:18819},
{id:'g05', home:'team-buf', away:'team-mtl', hg:3, ag:0, ot:false, year:2025, month:10, day:18, att:16325},
{id:'g06', home:'team-det', away:'team-bos', hg:2, ag:3, ot:true, year:2025, month:10, day:21, att:19515},
{id:'g07', home:'team-fla', away:'team-tbl', hg:2, ag:5, ot:false, year:2025, month:10, day:23, att:17921},
{id:'g08', home:'team-mtl', away:'team-ott', hg:4, ag:3, ot:false, year:2025, month:10, day:25, att:21105},
{id:'g09', home:'team-bos', away:'team-tor', hg:2, ag:1, ot:false, year:2025, month:10, day:28, att:17565},
{id:'g10', home:'team-buf', away:'team-det', hg:5, ag:3, ot:false, year:2025, month:10, day:30, att:16402},
{id:'g11', home:'team-tbl', away:'team-ott', hg:3, ag:2, ot:true, year:2025, month:11, day:1, att:19201},
{id:'g12', home:'team-fla', away:'team-mtl', hg:1, ag:3, ot:false, year:2025, month:11, day:4, att:17654},
{id:'g13', home:'team-det', away:'team-tor', hg:4, ag:2, ot:false, year:2025, month:11, day:6, att:19515},
{id:'g14', home:'team-ott', away:'team-buf', hg:3, ag:4, ot:false, year:2025, month:11, day:8, att:18321},
{id:'g15', home:'team-mtl', away:'team-tbl', hg:2, ag:4, ot:false, year:2025, month:11, day:11, att:21302},
{id:'g16', home:'team-tor', away:'team-bos', hg:0, ag:3, ot:false, year:2025, month:11, day:13, att:18950},
{id:'g17', home:'team-fla', away:'team-det', hg:3, ag:2, ot:false, year:2025, month:11, day:15, att:17812},
{id:'g18', home:'team-buf', away:'team-tbl', hg:1, ag:2, ot:false, year:2025, month:11, day:18, att:16789},
{id:'g19', home:'team-ott', away:'team-mtl', hg:2, ag:1, ot:false, year:2025, month:11, day:20, att:18654},
{id:'g20', home:'team-bos', away:'team-fla', hg:4, ag:3, ot:true, year:2025, month:11, day:22, att:17565},
{id:'g21', home:'team-det', away:'team-buf', hg:1, ag:2, ot:false, year:2025, month:12, day:2, att:19515},
{id:'g22', home:'team-tor', away:'team-ott', hg:3, ag:2, ot:false, year:2025, month:12, day:4, att:18945},
{id:'g23', home:'team-mtl', away:'team-bos', hg:2, ag:4, ot:false, year:2025, month:12, day:6, att:21207},
{id:'g24', home:'team-tbl', away:'team-det', hg:3, ag:1, ot:false, year:2025, month:12, day:9, att:19002},
{id:'g25', home:'team-fla', away:'team-ott', hg:4, ag:1, ot:false, year:2025, month:12, day:11, att:17543},
{id:'g26', home:'team-buf', away:'team-tor', hg:3, ag:2, ot:true, year:2025, month:12, day:13, att:16851},
{id:'g27', home:'team-bos', away:'team-mtl', hg:3, ag:2, ot:true, year:2025, month:12, day:16, att:17565},
{id:'g28', home:'team-det', away:'team-fla', hg:3, ag:2, ot:false, year:2025, month:12, day:18, att:19515},
{id:'g29', home:'team-ott', away:'team-tor', hg:4, ag:3, ot:false, year:2025, month:12, day:20, att:18710},
{id:'g30', home:'team-tbl', away:'team-buf', hg:3, ag:2, ot:false, year:2025, month:12, day:23, att:19128}
] AS matchs
UNWIND matchs AS m
CREATE (g:Game {id:m.id, year:m.year, month:m.month, day:m.day, attendance:m.att})
WITH m, g
MATCH (h:Team {id: m.home})
CREATE (h)-[:HOME_TEAM {
goals: m.hg,
outcome: CASE WHEN m.hg > m.ag THEN 'win'
WHEN m.hg < m.ag THEN 'loss'
ELSE CASE WHEN m.ot THEN 'overtime_loss' ELSE 'draw' END END,
win: CASE WHEN m.hg > m.ag THEN 1 ELSE 0 END,
points: CASE WHEN m.hg > m.ag THEN 2
WHEN m.hg < m.ag THEN 0
WHEN m.ot THEN 1 ELSE 0 END
}]->(g)
WITH m, g
MATCH (a:Team {id: m.away})
CREATE (a)-[:AWAY_TEAM {
goals: m.ag,
outcome: CASE WHEN m.ag > m.hg THEN 'win'
WHEN m.ag < m.hg THEN 'loss'
ELSE CASE WHEN m.ot THEN 'overtime_loss' ELSE 'draw' END END,
win: CASE WHEN m.ag > m.hg THEN 1 ELSE 0 END,
points: CASE WHEN m.ag > m.hg THEN 2
WHEN m.ag < m.hg THEN 0
WHEN m.ot THEN 1 ELSE 0 END
}]->(g);
Vérification¶
Résultat attendu : 8 équipes et 30 matchs.
Partie 2 – Requêtes typiques de l'application¶
Requête A – Compter les nœuds par type¶
Requête B – Afficher les matchs d'une équipe spécifique (les Bruins)¶
Requête C – Calculer le pourcentage de victoires de chaque équipe¶
MATCH (n:Team)-[r]->(g:Game)
WITH n.nickname AS equipe, sum(r.win) AS victoires, count(r) AS matchs
RETURN equipe, victoires, matchs, (victoires * 1.0 / matchs * 1.0) AS pct_victoires
Requête D – Répartition des matchs à domicile et à l'extérieur par équipe¶
MATCH (t:Team)-[r]->(x:Game)
WITH t.nickname AS equipe,
CASE TYPE(r) WHEN 'AWAY_TEAM' THEN 1 ELSE 0 END AS match_ext,
CASE TYPE(r) WHEN 'HOME_TEAM' THEN 1 ELSE 0 END AS match_dom,
r
RETURN equipe,
sum(match_ext) AS matchs_exterieur,
sum(match_dom) AS matchs_domicile,
count(r) AS total_matchs
ORDER BY total_matchs DESC
Requête E – Différentiel de buts par équipe¶
MATCH (t1:Team)-[r1]->(g1:Game)<-[r2]-(adversaire:Team)
WITH t1.nickname AS equipe, sum(r1.goals) AS buts_pour, sum(r2.goals) AS buts_contre
RETURN equipe, buts_pour, buts_contre, buts_pour - buts_contre AS differentiel
ORDER BY differentiel DESC
Requête F – Retrouver une équipe par son identifiant unique (slug)¶
Requête G – Retrouver les matchs d'un mois précis (novembre 2025)¶
Requête H – Trouver les équipes dont le surnom contient 'ings'¶
Requête I – Trouver les équipes dont le slug commence par 'nhl-mtl'¶
Partie 3 – Questions¶
Q1. Pour chacune des requêtes A à I, identifiez :
- Quelle(s) propriété(s) de nœud sont utilisées comme critère de filtrage (dans
WHEREou{…}) ? - Quel type d'index serait le plus approprié ? (
RANGE/TEXT/ aucun index utile) - Rédigez la commande
CREATE INDEXcorrespondante, si applicable.
Q2. Les propriétés outcome, win et points se trouvent sur les relations HOME_TEAM et AWAY_TEAM (pas sur les nœuds). Dans les requêtes C et D, ces propriétés sont utilisées via sum(r.win) et TYPE(r).
- Peut-on créer un index sur une propriété de relation dans Neo4j ?
- Quelle modification du modèle de données permettrait d'indexer ces valeurs si elles étaient très souvent filtrées ?
Q3. Comparez les requêtes H (CONTAINS) et I (STARTS WITH) sur Team.slug / Team.nickname. Si vous ne pouvez créer qu'un seul index sur Team.nickname, lequel choisissez-vous : RANGE ou TEXT ? Justifiez.
Q4. Utilisez EXPLAIN sur la requête B avant et après avoir créé votre index.
- Quel opérateur apparaît avant l'index ? (ex. :
NodeByLabelScan) - Quel opérateur le remplace après la création de l'index ?
Syntaxe :