- Les antipatterns SQL rendent la maintenance des requêtes et des pipelines de données plus difficile, et entraînent des performances plus lentes que prévu
- Parmi les cas les plus représentatifs figurent l’abus de CASE WHEN, l’application de fonctions sur des colonnes indexées, SELECT *, l’abus de DISTINCT, les vues et sous-requêtes imbriquées, ainsi que des structures de dépendance trop profondes
- La plupart de ces problèmes proviennent de solutions de contournement temporaires dues à la pression sur les délais et la rapidité d’exécution ; à long terme, ils nuisent à la fois à la fiabilité des données et à la vitesse de développement
- Comme solutions, il faut notamment des définitions de jointure claires, l’utilisation de tables de dimensions, la suppression des imbrications inutiles et un nettoyage régulier des vues
- Il faut traiter SQL non pas comme un simple script, mais comme du code de production géré à l’échelle de l’équipe ; une conception initiale pensée pour la lisibilité réduit fortement les retouches
Introduction
- Aujourd’hui, nous allons nous concentrer sur quelques antipatterns SQL fréquents et particulièrement impactants
- Ces problèmes entraînent un cercle vicieux : baisse de la fiabilité des données, ralentissement du développement des requêtes, etc.
- La liste ci-dessous ne couvre pas tous les cas ; pour une compréhension plus approfondie, le livre de Bill Karwin est recommandé
Des instructions CASE WHEN excessivement complexes
- Dans les systèmes de grande taille, on utilise souvent des instructions CASE WHEN pour convertir des codes d’état (par ex. 1 = rupture de stock) en libellés lisibles par les humains
- Ajouter cette logique CASE WHEN à une seule vue pour développer rapidement un dashboard ou un rapport constitue, à long terme, un antipattern
- Cela provoque des duplications de logique par copier-coller, des divergences d’interprétation, et finit par rendre l’ensemble des requêtes confus
- La solution consiste à créer séparément une table de dimensions (dimension table) ou une vue partagée pour convertir les codes d’état, afin de garantir la réutilisabilité
Utiliser des fonctions sur des colonnes indexées
- Lorsque l’on applique une fonction à une colonne indexée, comme dans
WHERE UPPER(name) = 'ABC', l’efficacité de l’index disparaît
- Dans SQL Server, entre autres, cela peut entraîner un full table scan inutile
- La solution consiste soit à indexer séparément la colonne après transformation, soit à transformer la valeur d’entrée pour simplifier la condition de la requête
Utiliser SELECT * dans une vue
- Lors du développement d’une vue, SELECT * peut sembler pratique, mais si la structure (le schéma) change, la vue peut facilement casser
- Comme cela inclut aussi des colonnes inutiles, cela crée des dépendances involontaires et des problèmes de performance ; il faut donc sélectionner explicitement les colonnes
Abuser de DISTINCT pour « résoudre » les doublons
- Quand un mauvais
JOIN produit des résultats dupliqués, utiliser SELECT DISTINCT comme solution temporaire masque un problème d’intégrité des données
- La cause profonde réside dans des conditions de jointure incomplètes ou dans des erreurs de définition des relations (1:1, 1:N, etc.)
- La bonne solution consiste à renforcer la logique de jointure pour clarifier la définition des relations et à garantir la cohérence relationnelle avant les agrégations ou les rapports
Empilement excessif de vues (Excessive View Layer Stacking)
- Lorsque plusieurs équipes réutilisent des vues existantes et continuent d’empiler de nouvelles vues par-dessus, la chaîne de dépendances devient complexe et les performances se dégradent rapidement
- Le débogage devient difficile, et faire évoluer une requête revient presque à un travail d’« archéologie »
- Il faut périodiquement aplatir (flatten) la logique de transformation et, pour les calculs complexes, matérialiser (materialize) dans une vue ou une table de base clairement définie
Sous-requêtes trop profondément imbriquées
- Des sous-requêtes profondément imbriquées sur plus de 3 ou 4 niveaux dégradent la lisibilité et compliquent le débogage
- Il existe même des cas avec des sous-requêtes de plus de 5 000 lignes
- L’utilisation des CTE (Common Table Expression) facilite la séparation des étapes logiques et améliore la clarté (readability) de la requête
Conclusion
- En apparence, SQL semble simple, mais plus le système grossit, plus sa complexité tend à augmenter
- La plupart des antipatterns ne viennent pas d’une mauvaise intention, mais de compromis faits pour obtenir rapidement un résultat (vitesse, délais, solutions temporaires)
- Gérer SQL comme du code (gestion de versions, revue de code, conception claire) permet d’assurer à long terme à la fois la productivité et la fiabilité
- Investir quelques minutes dès la conception initiale pour réfléchir à la clarté et à la cohérence réduit fortement les retouches et la confusion futures
5 commentaires
Quand on n’a pas le temps, on se dit qu’on va juste éteindre l’incendie pour l’instant et réécrire ça plus tard, mais à force d’accumuler ce genre de décisions, on finit dans un véritable enfer de requêtes. J’en ai moi-même créé pas mal. Et pourtant, on sait très bien que ce « plus tard » où on les réécrira ne viendra jamais.
Bof...
« La plupart des problèmes viennent de solutions de fortune dues à la pression sur la vitesse et les délais »
Bouhou..
Avis Hacker News
DISTINCTdans une requête, je soupçonne que l’auteur ne comprend pas vraiment le modèle de données ou la théorie des ensembles, voire ni l’un ni l’autreDISTINCTpeut aussi être le signe d’un schéma trop normalisé. Par exemple, je ne pense pas qu’il faille créer à tout prix une tableaddresses_citiesjuste pour éviter d’enregistrer plusieurs fois le nom d’une villeJOINcorrectement faits, le fait d’ajouterDISTINCTdans un CTE a nettement amélioré les performances. J’ai l’impression que le query planner optimise mieux quand l’unicité des enregistrements est garantieLIMIT 1à une requête parce que je m’attendais à n’obtenir au plus qu’un seul résultat. Pourtant, sur de grandes tables (sqlite,mysql,postgresql), même après avoir trouvé l’enregistrement voulu, la base a tendance à continuer à parcourir toute la tableDISTINCTdans une requêteSELECT x FROM t. Même si, dans le schéma det, on voit quexa une contraintePRIMARYouUNIQUE, quelqu’un peut très bien supprimer cette contrainte peu après. Des doublons apparaîtront alors et on se demandera pourquoi. SQL n’est pas un langage d’ensembles (set), mais un langage de multisets (bag). À l’exécution, on cherche simplement la relationtet l’attributx, puis on les renvoie. Il peut y avoir des doublons, le type peut même changer. Si l’on veut vraiment unSet, il faut explicitement écrireDISTINCT. Le query planner, lui, saura à l’exécution qu’il n’a pas besoin de déduplication si c’estUNIQUEouPRIMARYneo4j, des nœuds dupliqués se retrouvent très facilement dans les résultats, doncDISTINCTest indispensable. En particulier avec des relations de longueur variable, sansDISTINCTça devient plus lent et il y a beaucoup de doublonsDISTINCThttps://kb.databasedesignbook.com/posts/systematic-design-of-join-queries/
!=ouNOT IN (...)est inefficace dans la grande majorité des cas (même si cela peut aller quand d’autres conditions ont déjà suffisamment réduit l’ensemble de résultats). Et il est important de comprendre comment la base traite lesnull. Est-ce quenullet la chaîne vide sont équivalents ? Est-ce quenull == null? Cela peut varier selon la base de donnéesnullet l’indexation, les bases que j’ai utilisées n’indexent pas les valeursnull, donc une requêteWHERE col IS NULLfonctionne de manière inefficace même si un index existe surcol. Si c’est vraiment nécessaire, je recommande de créer une colonnechar(1)oubitqui indique sicolestnull, puis d’indexer ce champ!=ouNOT IN (...)sont presque toujours inefficaces, mais je me demande pourquoi. Quand la valeur de droite est constante, cela devrait être une recherche dans une table de hachage et donc généralement efficace. Je me demande s’il existe une alternative plus performanteDISTINCTest nécessaire, c’est peut-être que la conception des clés primaires n’est pas correcte. Si l’on finit par empiler lesview, c’est au fond que la conception des tables de base est mauvaise. Une bonne modélisation de base de données prévient tous ces problèmes en amontsargablehttps://en.wikipedia.org/wiki/Sargable
https://www.brentozar.com/blitzcache/non-sargable-predicates/
sargableest réellement utilisé. Je fais du SQL depuis plus de 20 ans, mais je ne l’ai presque jamais vu dans les manuels, sur Stack Overflow ou sur HN. Je me demande s’il est surtout utilisé autour d’une base particulière, dans certaines entreprises ou dans une communauté open source précisesargable, cette réponse StackOverflow m’a aidé : https://dba.stackexchange.com/a/217983Le mot
sargableest un mot-valise formé à partir de « Search ARGument ABLE »CASE WHENpeuvent être résolus en centralisant la logique dans une UDF (User Defined Function)Utiliser une fonction sur une colonne indexée est un signe que la requête n’est pas
sargableAu lieu d’abuser de
DISTINCT, pour dédupliquer selon la granularité de la table au sein d’un fan-out dérivé d’unjoin, une requête du type peut être utile. Certaines bases prennent aussi en chargeQUALIFY, ce qui rend la requête beaucoup plus propreexplication de sargable
QUALIFY in Redshift
sargablepeuvent être résolus facilement avec des expression indexes. Du moins, je pense que c’est le cas avecsqliteviewest réellement nécessaire. Dans notre logiciel de POS, nous utilisons beaucoup deviewimbriquées pour créer uneviewdorsale qui permette de voir proprement les transactions d’un seul coup. Sinon, il faudrait écrire des clauseswheredifférentes pour chaque table et retraiter à chaque fois toutes sortes de conditions comme les annulations, retours, suppressions, etc. Au moindre changement, il faudrait corriger des dizaines deviewou de procédures. Dans notre cas, l’usage deviewimbriquées est bien plus pratiqueUPPER(name)) n’est pas la meilleure, du moins sous MS SQL Server. Je ne sais pas ce qu’il en est du support dans d’autres bases, mais une meilleure solution consiste à créer directement une colonne calculée insensible à la casse avecCOLLATE(à ajuster selon vos préférences)Le plus important manque, on dirait.