SQL (ANSI)ProgrammationIngénieur de données senior

Étant donné une exigence de linéariser les dépendances d'exécution entre des tâches interdépendantes modélisées sous la forme d'un graphe acyclique orienté, comment calculeriez-vous un ordre topologique valide en utilisant strictement des CTE récursifs ANSI SQL sans logique procédurale externe ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse à la question.

Le tri topologique provient de la théorie des graphes et des mathématiques de planification, développé spécifiquement pour établir des séquences d'exécution viables dans des graphes de dépendance où certains sommets doivent précéder d'autres. Dans les environnements de base de données, cette exigence émerge lors de l'orchestration des flux de travail ETL, des scripts de migration de schéma ou des transformations de données complexes où l'intégrité référentielle doit être respectée à travers les opérations hiérarchiques. Les CTE récursifs ANSI SQL offrent une solution déclarative en calculant la profondeur du chemin le plus long vers chaque nœud, qui sert de niveau topologique valide.

Le problème central implique deux structures relationnelles : une table tasks contenant des identifiants uniques et une table dependencies définissant les relations préalables. Un tri valide nécessite que chaque tâche apparaisse uniquement après que toutes ses dépendances ont été énumérées, nécessitant un rang numérique où pour chaque arête du nœud A au nœud B, rank(A) < rank(B). Le défi réside dans le calcul de ce rang basé sur l'ensemble sans itération procédurale ou variables mutables.

La solution calcule le niveau topologique comme un plus le niveau maximum de tous les prédécesseurs immédiats, propagé récursivement à travers le graphe. Les nœuds sources possédant aucune arête entrante s'initialisent à zéro. Cette méthode gère correctement les DAGs avec plusieurs chemins d'héritage parce que la chaîne la plus longue détermine la première position d'exécution sûre. Le CTE récursif s'associe de manière itérative contre le graphe de dépendance, regroupant par tâche enfant pour agréger le niveau parent maximum avant d'incrémenter.

WITH RECURSIVE topo_levels AS ( -- Ancre : Identifier les nœuds sources avec un degré d'entrée zéro SELECT t.task_id, 0 AS level, CAST(t.task_id AS VARCHAR(1000)) AS path_trace FROM tasks t WHERE NOT EXISTS ( SELECT 1 FROM dependencies d WHERE d.task_id = t.task_id ) UNION ALL -- Récursif : Calculer le niveau basé sur le niveau du prédécesseur max SELECT d.task_id, MAX(tl.level) + 1, MAX(tl.path_trace) || '->' || CAST(d.task_id AS VARCHAR(1000)) FROM dependencies d INNER JOIN topo_levels tl ON d.depends_on_task_id = tl.task_id WHERE tl.level < 1000 -- Protection contre la récursion GROUP BY d.task_id HAVING COUNT(DISTINCT d.depends_on_task_id) = ( -- Vérifier que toutes les dépendances pour cette tâche sont résolues SELECT COUNT(*) FROM dependencies d2 WHERE d2.task_id = d.task_id ) ) SELECT task_id, level, path_trace FROM topo_levels ORDER BY level, task_id;

Situation de la vie réelle

Une plateforme de gestion des risques financiers nécessitait le recalcul nocturne de plus de 800 modèles de tarification dérivée, où les options exotiques dépendaient des surfaces de volatilité, qui dépendaient des flux de données de marché bruts. Le suivi manuel Excel existant a échoué alors que les dépendances atteignaient six niveaux hiérarchiques, entraînant des erreurs de calcul fréquentes lorsque les processus en aval s'exécutaient avant l'achèvement des prérequis. L'équipe d'ingénierie avait besoin d'une solution atomique, native de la base de données pour séquencer ces tâches sans ajouter d'infrastructure d'orchestration externe.

Trois motifs architecturaux ont été évalués. Le premier proposait d'adopter Apache Airflow, offrant une surveillance robuste et des sémantiques de réessai mais introduisant une latence réseau, une gestion d'état externe et des coûts de licence significatifs pour l'environnement sécurisé sur site. Le second suggérait un pilote procédural Python utilisant psycopg2 pour interroger les dépendances et exécuter les tâches séquentiellement ; bien que flexible, cela créait une dépendance externe fragile vulnérable aux partitions réseau et manquant de cohérence transactionnelle avec le référentiel de métadonnées. La troisième approche a mis en œuvre le tri topologique uniquement au sein de PostgreSQL en utilisant des CTE récursifs, maintenant toute la logique d'orchestration à l'intérieur de la base de données où les définitions de tâches et les dépendances résidaient déjà.

L'équipe a choisi la solution pure SQL car elle maintenait la conformité ACID avec les métadonnées de flux de travail, éliminait les allers-retours réseau pour la résolution des dépendances, et exploitait l'optimiseur de la base de données pour identifier les candidats à l'exécution parallèle partageant des niveaux topologiques identiques. Suite à la mise en œuvre, la fenêtre de lot nocturne a été compressée de six heures à quarante-cinq minutes en exposant le parallélisme inhérent précédemment obscurci par la séquence manuelle, tandis que les échecs de production liés aux dépendances ont chuté à zéro au cours des six mois suivants.

Ce que les candidats oublient souvent

Comment vous protégez-vous contre la récursion infinie lorsque le graphe d'entrée contient un cycle accidentel, étant donné que les CTE récursifs ANSI SQL sur des graphes cycliques peuvent boucler indéfiniment ou provoquer des erreurs d'exécution ?

Les candidats supposent souvent que des garanties d'intégrité des données que les propriétés DAG sont appliquées au niveau de l'application. En production, des références circulaires orphelines (par exemple, la Tâche A dépend de B, B de C, et C de A) entraînent des CTE récursifs standard à dépasser les limites de récursion ou à consommer toutes les ressources. La solution robuste consiste à transporter une chaîne de suivi de chemin ou un tableau à travers la récursion, puis à filtrer dans le membre récursif pour exclure les lignes où le nœud actuel existe déjà dans le chemin accumulé. Alternativement, SQL:2023 introduit la clause CYCLE (SEARCH DEPTH FIRST ... CYCLE task_id SET is_cycle USING path), qui détecte automatiquement les cycles et définit un indicateur booléen, permettant à la requête de se terminer normalement plutôt que d'échouer.

Pourquoi l'étape récursive doit-elle agréger en utilisant MAX plutôt que simplement ajouter un à un niveau de prédécesseur arbitraire ?

De nombreux candidats proposent incorrectement de se joindre à n'importe quelle tâche parente et d'incrémenter son niveau d'un, ignorant que les nœuds dans un DAG possèdent souvent plusieurs arêtes entrantes de profondeurs diverses. Considérons la Tâche D dépendant à la fois de la Tâche A (niveau 0) et de la Tâche C (niveau 4). Utiliser MIN ou une jointure arbitraire assigne D au niveau 1, violant la contrainte que C doit être terminé avant D. L'agrégat MAX garantit que D reçoit le niveau 5, accommodant correctement la chaîne de dépendance la plus longue. Cette distinction est critique pour la justesse dans les graphes affichant des dépendances en forme de diamant ou des motifs de flux de travail convergents.

Comment optimiseriez-vous cette requête pour des graphes contenant des millions de nœuds où l'approche standard du CTE récursif présente une dégradation de performance quadratique en raison de scans de table complets répétitifs des dépendances ?

Pour des graphes massifs, l'approche naïve souffre de jointures répétées contre les tables de base sans stratégies d'indexation appropriées. Les candidats négligent souvent que les CTE récursifs bénéficient énormément des index sur les colonnes parent et enfant de la table des arêtes. Au-delà de l'indexation, une optimisation consiste à calculer d'abord une réduction transitive pour éliminer les arêtes redondantes, ou partitionner le graphe en composants connectés et les traiter indépendamment. Dans des cas extrêmes, reconnaissant que SQL est fondamentalement optimisé pour l'algèbre relationnelle plutôt que pour la traversée de graphe, la décision architecturale correcte consiste à exporter le graphe vers un moteur de traitement dédié (par exemple, GraphX, Neo4j, ou NetworkX) plutôt que de forcer une solution purement basée sur des ensembles, bien que comprendre la limitation de SQL démontre un jugement d'ingénierie mûr.