web-dev-qa-db-fra.com

Quels sont les anti-modèles SQL les plus courants?

Tous ceux qui travaillent avec des bases de données relationnelles ont appris (ou apprenons) que SQL est différent. L'obtention des résultats souhaités, et ce de manière efficace, implique un processus fastidieux caractérisé en partie par l'apprentissage de paradigmes inconnus et par la découverte que certains de nos modèles de programmation les plus familiers ne fonctionnent pas ici. Quels sont les antipatterns communs que vous avez vus (ou que vous avez commis)?

223
dkretz

Je suis constamment déçu par la tendance de la plupart des programmeurs à mélanger leur logique d'interface utilisateur dans la couche d'accès aux données:

SELECT
    FirstName + ' ' + LastName as "Full Name",
    case UserRole
        when 2 then "Admin"
        when 1 then "Moderator"
        else "User"
    end as "User's Role",
    case SignedIn
        when 0 then "Logged in"
        else "Logged out"
    end as "User signed in?",
    Convert(varchar(100), LastSignOn, 101) as "Last Sign On",
    DateDiff('d', LastSignOn, getDate()) as "Days since last sign on",
    AddrLine1 + ' ' + AddrLine2 + ' ' + AddrLine3 + ' ' +
        City + ', ' + State + ' ' + Zip as "Address",
    'XXX-XX-' + Substring(
        Convert(varchar(9), SSN), 6, 4) as "Social Security #"
FROM Users

Normalement, les programmeurs agissent de la sorte parce qu'ils ont l'intention de lier leur jeu de données directement à une grille et qu'il est simplement pratique d'avoir le format SQL Server côté serveur plutôt que le format sur le client.

Les requêtes telles que celle présentée ci-dessus sont extrêmement fragiles car elles couplent étroitement la couche de données à la couche d'interface utilisateur. En plus de cela, ce style de programmation empêche complètement les procédures stockées d'être réutilisables.

148
Juliet

Voici mon top 3.

Nombre 1. Échec de la spécification d'une liste de champs. (Edit: pour éviter toute confusion: il s'agit d'une règle de code de production. Elle ne s'applique pas aux scripts d'analyse uniques - à moins que je ne sois l'auteur.)

SELECT *
Insert Into blah SELECT *

devrait être

SELECT fieldlist
Insert Into blah (fieldlist) SELECT fieldlist

Numéro 2. À l'aide d'un curseur et d'une boucle while, lorsqu'une boucle while avec une variable de boucle fera l'affaire.

DECLARE @LoopVar int

SET @LoopVar = (SELECT MIN(TheKey) FROM TheTable)
WHILE @LoopVar is not null
BEGIN
  -- Do Stuff with current value of @LoopVar
  ...
  --Ok, done, now get the next value
  SET @LoopVar = (SELECT MIN(TheKey) FROM TheTable
    WHERE @LoopVar < TheKey)
END

Nombre 3. DateLogic à travers des types de chaîne.

--Trim the time
Convert(Convert(theDate, varchar(10), 121), datetime)

Devrait être

--Trim the time
DateAdd(dd, DateDiff(dd, 0, theDate), 0)

J'ai récemment vu un pic de "Une requête vaut mieux que deux, une nuit?"

SELECT *
FROM blah
WHERE (blah.Name = @name OR @name is null)
  AND (blah.Purpose = @Purpose OR @Purpose is null)

Cette requête nécessite deux ou trois plans d'exécution différents en fonction des valeurs des paramètres. Un seul plan d'exécution est généré et collé dans le cache pour ce texte SQL. Ce plan sera utilisé quelle que soit la valeur des paramètres. Cela se traduit par des performances médiocres par intermittence. Il est bien préférable d’écrire deux requêtes (une requête par plan d’exécution prévu).

114
Amy B
  • Champs de mot de passe lisibles par l'homme, egad. Auto-explicatif.

  • En utilisant LIKE contre des colonnes indexées, je suis presque tenté de dire LIKE en général.

  • Recycler les valeurs de PK générées par SQL.

  • Surprise, personne n'a mentionné la table divine pour le moment. Rien ne dit "organique" comme 100 colonnes d'indicateurs de bits, de grandes chaînes et des nombres entiers.

  • Ensuite, il y a les "fichiers .ini qui me manquent" motif: stocker des fichiers CSV, des chaînes délimitées par des canaux ou d'autres données nécessaires à l'analyse dans de grands champs de texte.

  • Et pour le serveur MS SQL, l'utilisation de curseurs du tout . Il y a une meilleure façon de faire n'importe quelle tâche de curseur.

Edité parce qu'il y en a tellement!

68
annakata

Ne pas avoir à creuser profondément pour cela: ne pas utiliser les déclarations préparées.

60
stesch

Utiliser des alias de table sans signification:

from employee t1,
department t2,
job t3,
...

Rend la lecture d'une grande instruction SQL tellement plus difficile qu'elle ne doit l'être

56
Tony Andrews
var query = "select COUNT(*) from Users where UserName = '" 
            + tbUser.Text 
            + "' and Password = '" 
            + tbPassword.Text +"'";
  1. Confiance aveugle de la part de l'utilisateur
  2. Ne pas utiliser requêtes paramétrées
  3. mots de passe en clair
51
Will

Mes bugbears sont les tables d'accès à 450 colonnes qui ont été assemblées par le fils du meilleur ami du directeur général, le toiletteur de chiens du directeur général, et la table de recherche louche qui n'existe que parce que quelqu'un ne sait pas comment normaliser correctement une structure de données.

En règle générale, cette table de recherche ressemble à ceci:

 ID INT, 
 Nom NVARCHAR (132), 
 IntValue1 INT, 
 IntValue2 INT, 
 CharValue1 NVARCHAR (255), 
 CharValue2 NVARCHAR (255), 
 Date1 DATETIME, 
 Date2 DATETIME 

J'ai perdu le compte du nombre de clients que j'ai vus qui ont des systèmes qui reposent sur des abominations comme celle-ci.

46
Pete OHanlon

Ceux que je n'aime pas le plus sont

  1. Utiliser des espaces pour créer des tables, des sprocs, etc. Je suis d'accord avec CamelCase ou under_scores et singular ou plurals et UPPERCASE ou en minuscules, mais je dois faire référence à une table ou à une colonne [avec des espaces], en particulier si [elle est espacée de manière étrange] Je me suis heurté à cela) m'énerve vraiment.

  2. Données dénormalisées. Un tableau ne doit pas être parfaitement normalisé, mais lorsque je rencontre un tableau d'employés qui ont des informations sur leur score d'évaluation actuel ou leur principal élément, cela me dit que je devrais probablement créer un tableau séparé à un moment donné. puis essayez de les garder synchronisés. Je vais d'abord normaliser les données, puis si je vois un endroit où la dénormalisation est utile, je l'examinerai.

  3. Utilisation excessive de vues ou de curseurs. Les vues ont un but, mais quand chaque table est encapsulée dans une vue, c'est trop. J'ai dû utiliser des curseurs à quelques reprises, mais vous pouvez généralement utiliser d'autres mécanismes pour cela.

  4. Accès. Un programme peut-il être un anti-modèle? Nous avons SQL Server à mon travail, mais un certain nombre de personnes utilisent l’accès en raison de sa disponibilité, de sa "facilité d’utilisation" et de sa "convivialité" pour les utilisateurs non techniques. Il y a trop de choses ici pour y aller, mais si vous avez été dans un environnement similaire, vous savez.

28
Jamal Hansen

utilisez SP comme préfixe du nom de la procédure de stockage car il effectuera d'abord une recherche à l'emplacement des procédures système plutôt qu'à celles personnalisées.

26
Oscar Cabrero

Abus de tables temporaires et de curseurs.

25
Rockcoder

Pour stocker les valeurs horaires, seul le fuseau horaire UTC doit être utilisé. L'heure locale ne doit pas être utilisée.

24
Frank Schwieterman

utilisant @@ IDENTITY au lieu de SCOPE_IDENTITY ()

Cité de cette réponse :

  • @@ IDENTITY renvoie la dernière valeur d'identité générée pour toute table de la session en cours, pour toutes les portées. Vous devez faire attention ici, car cela couvre plusieurs domaines. Vous pouvez obtenir une valeur à partir d'un déclencheur, au lieu de votre déclaration actuelle.
  • SCOPE_IDENTITY renvoie la dernière valeur d'identité générée pour toute table de la session en cours et la portée en cours. Généralement ce que vous voulez utiliser.
  • IDENT_CURRENT renvoie la dernière valeur d’identité générée pour une table spécifique dans une session ou une portée quelconque. Cela vous permet de spécifier la table de laquelle vous voulez la valeur, au cas où les deux précédentes ne seraient pas tout à fait ce dont vous avez besoin (très rare). Vous pouvez l'utiliser si vous souhaitez obtenir la valeur IDENTITY actuelle d'une table dans laquelle vous n'avez pas inséré d'enregistrement.
23
Brann

Réutiliser un champ "mort" pour quelque chose pour lequel il n'était pas destiné (par exemple, stocker des données d'utilisateur dans un champ "Fax") - très tentant comme solution rapide!

23
FruitBreak
select some_column, ...
from some_table
group by some_column

et en supposant que le résultat sera trié par une colonne. J'ai un peu vu cela avec Sybase où l'hypothèse est valable (pour le moment).

21
Adrian Pronk
SELECT FirstName + ' ' + LastName as "Full Name", case UserRole when 2 then "Admin" when 1 then "Moderator" else "User" end as "User's Role", case SignedIn when 0 then "Logged in" else "Logged out" end as "User signed in?", Convert(varchar(100), LastSignOn, 101) as "Last Sign On", DateDiff('d', LastSignOn, getDate()) as "Days since last sign on", AddrLine1 + ' ' + AddrLine2 + ' ' + AddrLine3 + ' ' + City + ', ' + State + ' ' + Zip as "Address", 'XXX-XX-' + Substring(Convert(varchar(9), SSN), 6, 4) as "Social Security #" FROM Users

Ou, en mettant tout dans une seule ligne.

20
Jasper Bekkers
  • Le FROM TableA, TableB WHERE _ syntaxe pour JOINS plutôt que FROM TableA INNER JOIN TableB ON

  • En supposant que la requête sera retournée, elle est triée d'une certaine manière sans insérer de clause ORDER BY, simplement parce que c'est ce qui s'est passé lors des tests dans l'outil de requête.

17
Joel Coehoorn

Apprendre le SQL au cours des six premiers mois de leur carrière et n’apprendre rien d’autre au cours des 10 prochaines années. En particulier, ne pas apprendre ou utiliser efficacement les fonctionnalités SQL de fenêtrage/analytique. En particulier, l'utilisation de over () et de partition par.

Les fonctions de fenêtre, comme les fonctions d'agrégation, effectuent une agrégation sur un ensemble de lignes défini (un groupe), mais plutôt que de renvoyer une valeur par groupe, les fonctions de fenêtre peuvent renvoyer plusieurs valeurs pour chaque groupe.

Voir Annexe A du livre de recettes SQL d'O'Reilly pour un bon aperçu des fonctions de fenêtrage.

14
Brian

J'ai besoin de mettre ici mon favori actuel, juste pour compléter la liste. Mon anti-modèle préféré est ne pas tester vos requêtes.

Ceci s'applique lorsque:

  1. Votre requête implique plus d'une table.
  2. Vous pensez avoir une conception optimale pour une requête, mais ne vous donnez pas la peine de tester vos hypothèses.
  3. Vous acceptez la première requête qui fonctionne, sans savoir si elle est même proche d'optimisation.

Et tous les tests exécutés sur des données atypiques ou insuffisantes ne comptent pas. S'il s'agit d'une procédure stockée, placez l'instruction de test dans un commentaire et enregistrez-la avec les résultats. Sinon, mettez-le dans un commentaire dans le code avec les résultats.

12
dkretz

Abus de table temporaire.

Plus précisément ce genre de chose:

SELECT personid, firstname, lastname, age
INTO #tmpPeople
FROM People
WHERE lastname like 's%'

DELETE FROM #tmpPeople
WHERE firstname = 'John'

DELETE FROM #tmpPeople
WHERE firstname = 'Jon'

DELETE FROM #tmpPeople
WHERE age > 35

UPDATE People
SET firstname = 'Fred'
WHERE personid IN (SELECT personid from #tmpPeople)

Ne construisez pas de table temporaire à partir d'une requête, mais uniquement pour supprimer les lignes inutiles.

Et oui, j'ai vu des pages de code sous cette forme dans des bases de données de production.

10
geofftnz

Point de vue opposé: obsession excessive pour la normalisation.

La plupart des systèmes SQL/RBDB offrent une multitude de fonctionnalités (transactions, réplication) très utiles, même avec des données non normalisées. L’espace disque est bon marché et il peut parfois être plus simple (code plus facile, temps de développement plus rapide) de manipuler/filtrer/rechercher les données extraites, que de rédiger un schéma 1NF et de gérer tous les tracas qu’il comporte (jointures complexes, sous-sélections désagréables , etc).

J'ai constaté que les systèmes sur-normalisés constituaient souvent une optimisation prématurée, en particulier au cours des premières étapes de développement.

(plus de réflexions à ce sujet ... http://writeonly.wordpress.com/2008/12/05/simple-object-db-using-json-and-python-sqlite/ )

9
Gregg Lind

Je viens de mettre celui-ci ensemble, basé sur certaines des réponses SQL ici sur SO.

C’est un antipattern sérieux de penser que les déclencheurs s’appliquent aux bases de données comme les gestionnaires d’événements sont à la POO. Il y a cette perception que n'importe quelle ancienne logique peut être mise dans des déclencheurs, pour être déclenchée lorsqu'une transaction (événement) se produit sur une table.

Pas vrai. Une des grandes différences est que les déclencheurs sont synchrones - avec une vengeance, car ils sont synchrones sur une opération définie, pas sur une opération de ligne. Du côté OOP, c'est exactement l'inverse - les événements sont un moyen efficace d'implémenter des transactions asynchrones.

9
dkretz

Procédures stockées ou fonctions sans aucun commentaire ...

8
Bliek
  • La vue modifiée - Une vue qui est modifiée trop souvent et sans préavis ni raison. Le changement sera soit remarqué au moment le plus inapproprié, soit pire, il sera faux et ne sera jamais remarqué. Peut-être que votre application se cassera parce que quelqu'un a pensé à un meilleur nom pour cette colonne. En règle générale, les vues devraient étendre l'utilité des tables de base tout en maintenant un contrat avec les consommateurs. Résoudre les problèmes mais ne pas ajouter de fonctionnalités ou pire comportement de changement, pour que créer une nouvelle vue. Pour limiter les risques, ne partagez pas de vues avec d'autres projets et utilisez CTE lorsque les plates-formes le permettent. Si votre boutique dispose d'un DBA, vous ne pourrez probablement pas changer de vue, mais toutes vos vues seront obsolètes et/ou inutiles dans ce cas.

  • The! Paramed - Une requête peut-elle avoir plusieurs objectifs? Probablement, mais la prochaine personne qui le lit ne le saura pas avant une profonde méditation. Même si vous n'en avez pas besoin maintenant, il est probable que vous le ferez, même s'il est "juste" de déboguer. L'ajout de paramètres réduit le temps de maintenance et maintient les éléments au sec. Si vous avez une clause where, vous devriez avoir des paramètres.

  • Le cas pour non CASE -

    SELECT  
    CASE @problem  
      WHEN 'Need to replace column A with this medium to large collection of strings hanging out in my code.'  
        THEN 'Create a table for lookup and add to your from clause.'  
      WHEN 'Scrubbing values in the result set based on some business rules.'  
        THEN 'Fix the data in the database'  
      WHEN 'Formating dates or numbers.'   
        THEN 'Apply formating in the presentation layer.'  
      WHEN 'Createing a cross tab'  
        THEN 'Good, but in reporting you should probably be using cross tab, matrix or pivot templates'   
    ELSE 'You probably found another case for no CASE but now I have to edit my code instead of enriching the data...' END  
    
7
jason saldo

1) Je ne sais pas que c'est un anti-motif "officiel", mais je n'aime pas et j'essaie d'éviter les littéraux de chaîne comme valeurs magiques dans une colonne de base de données.

Un exemple tiré de la table 'image' de MediaWiki:

img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", 
    "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL,
img_major_mime ENUM("unknown", "application", "audio", "image", "text", 
    "video", "message", "model", "multipart") NOT NULL default "unknown",

(Je remarque juste un boîtier différent, une autre chose à éviter)

Je conçois des cas tels que des recherches int dans des tables ImageMediaType et ImageMajorMime avec des clés primaires int.

2) conversion de date/chaîne qui repose sur des paramètres NLS spécifiques

CONVERT(NVARCHAR, GETDATE())

sans identifiant de format

7
devio

Sous-requêtes identiques dans une requête.

7
EvilTeach

Les deux que je trouve le plus et qui peuvent avoir un coût significatif en termes de performances sont:

  • Utilisation de curseurs au lieu d'une expression basée sur un ensemble. Je suppose que cela se produit fréquemment lorsque le programmeur réfléchit de manière procédurale.

  • Utilisation de sous-requêtes corrélées, lorsqu'une jointure à une table dérivée peut effectuer le travail.

5
Mitch Wheat

Lorsque vous placez des éléments dans des tables temporaires, les personnes qui passent de SQL Server à Oracle, en particulier, ont l’habitude d’abuser des tables temporaires. Utilisez simplement des instructions select imbriquées.

5
tuinstoel

Développeurs qui écrivent des requêtes sans avoir une bonne idée de ce qui rend les applications SQL (requêtes individuelles et systèmes multi-utilisateurs) rapides ou lentes. Cela inclut l'ignorance concernant:

  • stratégies de minimisation physique des E/S, étant donné que le goulot d'étranglement de la plupart des requêtes est constitué d'E/S et non de CPU
  • l'impact de différents types d'accès au stockage physique (par exemple, beaucoup d'E/S séquentielles seront plus rapides que beaucoup de petites E/S aléatoires, mais moins si votre stockage physique est un SSD!)
  • comment ajuster à la main une requête si le SGBD génère un plan de requête médiocre
  • comment diagnostiquer de mauvaises performances de base de données, comment "déboguer" une requête lente et comment lire un plan de requête (ou EXPLAIN, selon le SGBD de votre choix)
  • stratégies de verrouillage pour optimiser le débit et éviter les blocages dans les applications multi-utilisateurs
  • importance du traitement par lots et autres astuces pour gérer le traitement des ensembles de données
  • conception de tables et d'index pour un meilleur équilibre entre espace et performances (par exemple, couvrir des index, garder les index aussi petits que possible, réduire les types de données à la taille minimale requise, etc.)
5
Justin Grant

Utilisation de SQL comme package glorifié ISAM (méthode d'accès séquentiel indexé)). En particulier, imbriquer des curseurs au lieu de combiner des instructions SQL en une seule instruction, bien que plus grande. optimiseur ", car l'optimiseur ne peut en réalité pas grand-chose à faire. Cela peut être combiné à des instructions non préparées pour une efficacité maximale:

DECLARE c1 CURSOR FOR SELECT Col1, Col2, Col3 FROM Table1

FOREACH c1 INTO a.col1, a.col2, a.col3
    DECLARE c2 CURSOR FOR
        SELECT Item1, Item2, Item3
            FROM Table2
            WHERE Table2.Item1 = a.col2
    FOREACH c2 INTO b.item1, b.item2, b.item3
        ...process data from records a and b...
    END FOREACH
END FOREACH

La solution correcte (presque toujours) consiste à combiner les deux instructions SELECT en une seule:

DECLARE c1 CURSOR FOR
    SELECT Col1, Col2, Col3, Item1, Item2, Item3
        FROM Table1, Table2
        WHERE Table2.Item1 = Table1.Col2
        -- ORDER BY Table1.Col1, Table2.Item1

FOREACH c1 INTO a.col1, a.col2, a.col3, b.item1, b.item2, b.item3
    ...process data from records a and b...
END FOREACH

Le seul avantage de la version à double boucle est que vous pouvez facilement repérer les ruptures entre les valeurs de Table1 car la boucle interne se termine. Cela peut être un facteur dans les rapports de rupture de contrôle.

En outre, le tri dans l'application est généralement un non-non.

3
Jonathan Leffler

Jointures d'application Il ne s'agit pas uniquement d'un problème SQL, mais de la recherche d'une description du problème et de la réponse à cette question, j'ai été surpris de constater qu'il ne figurait pas dans la liste.

Comme je l'ai entendu dire par expression, une application est utilisée lorsque vous extrayez un ensemble de lignes de chacune de deux tables ou plus, puis que vous les joignez à votre code (Java) avec une paire de boucles for imbriquées. Cela impose au système (votre application et la base de données) l'obligation d'identifier l'ensemble du produit, de le récupérer et de l'envoyer à l'application. En supposant que l'application puisse filtrer le produit croisé aussi rapidement que la base de données (douteuse), couper simplement le résultat défini plus tôt signifie moins de transfert de données.

3
Chris

J'ai vu trop de gens s'accrocher à la vie chère de IN (...) alors que totalement inconscients de EXISTS. Pour un bon exemple, voir Symfony Propel ORM.

3
sayap

Je viens de tomber sur la définition de vue comme ceci:

CREATE OR REPLACE FORCE VIEW PRICE (PART_NUMBER, PRICE_LIST, LIST_VERSION ...)
AS
  SELECT sp.MKT_PART_NUMBER,
    sp.PRICE_LIST,
    sp.LIST_VERSION,
    sp.MIN_PRICE,
    sp.UNIT_PRICE,
    sp.MAX_PRICE,
...

Il y a environ 50 colonnes dans la vue. Certains développeurs sont fiers de torturer les autres en ne fournissant pas d'alias de colonnes. Il est donc nécessaire de compter le décalage de colonne aux deux endroits pour pouvoir déterminer à quelle colonne correspond une colonne dans une vue.

3
Tegiri Nenashi

Utilisation de clés primaires en tant que substitut pour les adresses d’enregistrement et d’utilisation de clés étrangères en tant que substitut pour les pointeurs incorporés dans les enregistrements.

3
Walter Mitty
2
kristof

Ayant 1 table

code_1
value_1
code_2
value_2
...
code_10
value_10

Au lieu d'avoir 3 tables

code, valeur et code_value

Vous ne savez jamais quand vous aurez peut-être besoin de plus de 10 couples code, valeur.

Vous ne perdez pas d'espace disque si vous n'avez besoin que d'un couple.

2
Luc M

Mes anti-patterns SQL préférés:

JOIN sur des colonnes non uniques et en utilisant SELECT DISTINCT pour couper le résultat.

Créer une vue joignant plusieurs tables pour sélectionner quelques colonnes d’une table.

 CREATE VIEW my_view AS 
     SELECT * FROM table1
     JOIN table2 ON (...)
     JOIN table3 ON (...);

 SELECT col1, col2 FROM my_view WHERE col3 = 123;
2
Iwo Banas

re: en utilisant @@ IDENTITY au lieu de SCOPE_IDENTITY ()

vous ne devriez utiliser ni l'un ni l'autre; utiliser la sortie à la place

cf. https://connect.Microsoft.com/SQLServer/feedback/details/328811/scope-identity-sometimes-returns-incorrect-value

2
Denis Valeev

Joindre des tables redondantes dans une requête comme celle-ci:

select emp.empno, dept.deptno
from emp
join dept on dept.deptno = emp.deptno;
2
Tony Andrews

Ce n’est peut-être pas un anti-modèle, mais cela m’ennuie, c’est lorsque des DBA de certaines bases de données (ok, je parle d’Oracle ici) écrivent du code SQL Server en utilisant le style et les conventions de code d’Oracle et se plaignent de la situation. Assez avec les curseurs Oracle! SQL est censé être défini en fonction.

1
Craig

Ne pas utiliser la clause With ou une jointure appropriée et s'appuyer sur des sous-requêtes.

Anti-Pattern:

select 
 ...
from data
where RECORD.STATE IN (
          SELECT STATEID
            FROM STATE
           WHERE NAME IN
                    ('Published to test',
                     'Approved for public',
                     'Published to public',
                     'Archived'
                    ))

Mieux:
J'aime utiliser la clause with pour rendre mon intention plus lisible.

with valid_states as (
          SELECT STATEID
            FROM STATE
           WHERE NAME IN
                    ('Published to test',
                     'Approved for public',
                     'Published to public',
                     'Archived'
                    )
select  ... from data, valid_states
where data.state = valid_states.state

Best:

select 
  ... 
from data join states using (state)
where 
states.state in  ('Published to test',
                     'Approved for public',
                     'Published to public',
                     'Archived'
                    )
0
Brian