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)?
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.
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).
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!
Ne pas avoir à creuser profondément pour cela: ne pas utiliser les déclarations préparées.
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
var query = "select COUNT(*) from Users where UserName = '"
+ tbUser.Text
+ "' and Password = '"
+ tbPassword.Text +"'";
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.
Ceux que je n'aime pas le plus sont
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.
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.
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.
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.
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.
Abus de tables temporaires et de curseurs.
Pour stocker les valeurs horaires, seul le fuseau horaire UTC doit être utilisé. L'heure locale ne doit pas être utilisée.
utilisant @@ IDENTITY au lieu de SCOPE_IDENTITY ()
Cité de cette réponse :
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!
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).
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.
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.
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.
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:
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.
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.
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/ )
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.
Procédures stockées ou fonctions sans aucun commentaire ...
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
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
Sous-requêtes identiques dans une requête.
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.
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.
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:
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.
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.
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.
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.
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.
Juste pour soutenir Point 1 de la réponse de David check-out cet exemple de situation d'utilisation de select * peut produire des résultats inattendus
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.
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;
re: en utilisant @@ IDENTITY au lieu de SCOPE_IDENTITY ()
vous ne devriez utiliser ni l'un ni l'autre; utiliser la sortie à la place
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;
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.
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'
)