Database
 sql >> Base de données >  >> RDS >> Database

Gemmes T-SQL négligées

Mon bon ami Aaron Bertrand m'a inspiré pour écrire cet article. Il m'a rappelé comment parfois nous tenons les choses pour acquises quand elles nous semblent évidentes et ne prenons pas toujours la peine de vérifier toute l'histoire derrière elles. La pertinence pour T-SQL est que parfois nous supposons que nous savons tout ce qu'il y a à savoir sur certaines fonctionnalités de T-SQL, et ne prenons pas toujours la peine de vérifier la documentation pour voir s'il y en a plus. Dans cet article, je couvre un certain nombre de fonctionnalités T-SQL qui sont souvent totalement ignorées ou qui prennent en charge des paramètres ou des fonctionnalités souvent négligés. Si vous avez des exemples de gemmes T-SQL qui sont souvent négligés, veuillez les partager dans la section des commentaires de cet article.

Avant de commencer à lire cet article, demandez-vous ce que vous savez des fonctionnalités T-SQL suivantes :EOMONTH, TRANSLATE, TRIM, CONCAT et CONCAT_WS, LOG, variables de curseur et MERGE avec OUTPUT.

Dans mes exemples, j'utiliserai un exemple de base de données appelé TSQLV5. Vous pouvez trouver le script qui crée et remplit cette base de données ici, et son diagramme ER ici.

EOMONTH a un deuxième paramètre

La fonction EOMONTH a été introduite dans SQL Server 2012. Beaucoup de gens pensent qu'elle ne prend en charge qu'un seul paramètre contenant une date d'entrée et qu'elle renvoie simplement la date de fin de mois qui correspond à la date d'entrée.

Considérons un besoin un peu plus sophistiqué de calculer la fin du mois précédent. Par exemple, supposons que vous deviez interroger la table Sales.Orders et renvoyer les commandes passées à la fin du mois précédent.

Une façon d'y parvenir est d'appliquer la fonction EOMONTH à SYSDATETIME pour obtenir la date de fin de mois du mois en cours, puis d'appliquer la fonction DATEADD pour soustraire un mois du résultat, comme ceci :

USE TSQLV5; 
 
SELECT orderid, orderdate
FROM Sales.Orders
WHERE orderdate = EOMONTH(DATEADD(month, -1, SYSDATETIME()));

Notez que si vous exécutez réellement cette requête dans la base de données exemple TSQLV5, vous obtiendrez un résultat vide puisque la dernière date de commande enregistrée dans la table est le 6 mai 2019. Cependant, si la table avait des commandes avec une date de commande qui tombe le dernier jour du mois précédent, la requête les aurait retournés.

Ce que beaucoup de gens ne réalisent pas, c'est que EOMONTH prend en charge un deuxième paramètre où vous indiquez combien de mois ajouter ou soustraire. Voici la syntaxe [entièrement documentée] de la fonction :

EOMONTH ( start_date [, month_to_add ] )

Notre tâche peut être accomplie plus facilement et naturellement en spécifiant simplement -1 comme deuxième paramètre de la fonction, comme ceci :

SELECT orderid, orderdate
FROM Sales.Orders
WHERE orderdate = EOMONTH(SYSDATETIME(), -1);

TRANSLATE est parfois plus simple que REPLACE

De nombreuses personnes connaissent la fonction REPLACE et son fonctionnement. Vous l'utilisez lorsque vous souhaitez remplacer toutes les occurrences d'une sous-chaîne par une autre dans une chaîne d'entrée. Parfois, cependant, lorsque vous devez appliquer plusieurs remplacements, l'utilisation de REPLACE est un peu délicate et entraîne des expressions alambiquées.

Par exemple, supposons que vous receviez une chaîne d'entrée @s contenant un nombre au format espagnol. En Espagne, ils utilisent un point comme séparateur pour les groupes de milliers et une virgule comme séparateur décimal. Vous devez convertir l'entrée au format américain, où une virgule est utilisée comme séparateur pour les groupes de milliers et un point comme séparateur décimal.

En utilisant un seul appel à la fonction REPLACE, vous ne pouvez remplacer que toutes les occurrences d'un caractère ou d'une sous-chaîne par une autre. Pour appliquer deux remplacements (points aux virgules et virgules aux points), vous devez imbriquer les appels de fonction. La partie délicate est que si vous utilisez REPLACE une fois pour changer les points en virgules, puis une seconde fois contre le résultat pour changer les virgules en points, vous vous retrouvez avec seulement des points. Essayez-le :

DECLARE @s AS VARCHAR(20) = '123.456.789,00';
 
SELECT REPLACE(REPLACE(@s, '.', ','), ',', '.');

Vous obtenez le résultat suivant :

123.456.789.00

Si vous souhaitez vous en tenir à la fonction REPLACE, vous avez besoin de trois appels de fonction. Un pour remplacer les points par un caractère neutre que vous savez qui ne peut normalement pas apparaître dans les données (par exemple, ~). Un autre contre le résultat pour remplacer toutes les virgules par des points. Un autre contre le résultat pour remplacer toutes les occurrences du caractère temporaire (~ dans notre exemple) par des virgules. Voici l'expression complète :

DECLARE @s AS VARCHAR(20) = '123.456.789,00';
SELECT REPLACE(REPLACE(REPLACE(@s, '.', '~'), ',', '.'), '~', ',');

Cette fois, vous obtenez le bon résultat :

123,456,789.00

C'est un peu faisable, mais cela donne une expression longue et alambiquée. Et si vous aviez plus de remplaçants à appliquer ?

Beaucoup de gens ne savent pas que SQL Server 2017 a introduit une nouvelle fonction appelée TRANSLATE qui simplifie considérablement ces remplacements. Voici la syntaxe de la fonction :

TRANSLATE ( inputString, characters, translations )

La deuxième entrée (caractères) est une chaîne avec la liste des caractères individuels que vous souhaitez remplacer, et la troisième entrée (traductions) est une chaîne avec la liste des caractères correspondants avec lesquels vous souhaitez remplacer les caractères source. Cela signifie naturellement que les deuxième et troisième paramètres doivent avoir le même nombre de caractères. Ce qui est important à propos de la fonction, c'est qu'elle ne fait pas de passes séparées pour chacun des remplacements. Si c'était le cas, cela aurait potentiellement entraîné le même bogue que dans le premier exemple que j'ai montré en utilisant les deux appels à la fonction REPLACE. Par conséquent, gérer notre tâche devient une évidence :

DECLARE @s AS VARCHAR(20) = '123.456.789,00';
SELECT TRANSLATE(@s, '.,', ',.');

Ce code génère la sortie souhaitée :

123,456,789.00

C'est plutôt chouette !

TRIM est supérieur à LTRIM(RTRIM())

SQL Server 2017 a introduit la prise en charge de la fonction TRIM. Beaucoup de gens, moi y compris, supposent au départ qu'il ne s'agit que d'un simple raccourci vers LTRIM(RTRIM(input)). Cependant, si vous consultez la documentation, vous réalisez qu'elle est en fait plus puissante que cela.

Avant d'entrer dans les détails, considérez la tâche suivante :étant donné une chaîne d'entrée @s, supprimez les barres obliques de début et de fin (vers l'arrière et vers l'avant). Par exemple, supposons que @s contienne la chaîne suivante :

//\\ remove leading and trailing backward (\) and forward (/) slashes \\//

La sortie souhaitée est :

 remove leading and trailing backward (\) and forward (/) slashes 

Notez que la sortie doit conserver les espaces de début et de fin.

Si vous ne connaissiez pas toutes les fonctionnalités de TRIM, voici une façon de résoudre le problème :

DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//';
 
SELECT
  TRANSLATE(TRIM(TRANSLATE(TRIM(TRANSLATE(@s, ' /', '~ ')), ' \', '^ ')), ' ^~', '\/ ')
    AS outputstring;

La solution commence par utiliser TRANSLATE pour remplacer tous les espaces par un caractère neutre (~) et les barres obliques par des espaces, puis en utilisant TRIM pour supprimer les espaces de début et de fin du résultat. Cette étape supprime essentiellement les barres obliques de début et de fin, en utilisant temporairement ~ au lieu des espaces d'origine. Voici le résultat de cette étape :

\\~remove~leading~and~trailing~backward~(\)~and~forward~( )~slashes~\\

La deuxième étape utilise ensuite TRANSLATE pour remplacer tous les espaces par un autre caractère neutre (^) et les barres obliques inverses par des espaces, puis utilise TRIM pour supprimer les espaces de début et de fin du résultat. Cette étape supprime essentiellement les barres obliques inverses de début et de fin, en utilisant temporairement ^ au lieu d'espaces intermédiaires. Voici le résultat de cette étape :

~remove~leading~and~trailing~backward~( )~and~forward~(^)~slashes~

La dernière étape utilise TRANSLATE pour remplacer les espaces par des barres obliques inverses, ^ par des barres obliques et ~ par des espaces, générant le résultat souhaité :

 remove leading and trailing backward (\) and forward (/) slashes 

À titre d'exercice, essayez de résoudre cette tâche avec une solution compatible pré-SQL Server 2017 où vous ne pouvez pas utiliser TRIM et TRANSLATE.

De retour à SQL Server 2017 et versions ultérieures, si vous aviez pris la peine de consulter la documentation, vous auriez découvert que TRIM est plus sophistiqué que ce que vous pensiez au départ. Voici la syntaxe de la fonction :

TRIM ( [ characters FROM ] string )

Les caractères facultatifs DE part vous permet de spécifier un ou plusieurs caractères que vous souhaitez supprimer du début et de la fin de la chaîne d'entrée. Dans notre cas, tout ce que vous avez à faire est de spécifier '/\' comme cette partie, comme ceci :

DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//';
 
SELECT TRIM( '/\' FROM @s) AS outputstring;

C'est une amélioration assez significative par rapport à la solution précédente !

CONCAT et CONCAT_WS

Si vous travaillez avec T-SQL depuis un certain temps, vous savez à quel point il est difficile de gérer les valeurs NULL lorsque vous devez concaténer des chaînes. À titre d'exemple, considérons les données de localisation enregistrées pour les employés dans la table HR.Employees :

SELECT empid, country, region, city
FROM HR.Employees;

Cette requête génère la sortie suivante :

empid       country         region          city
----------- --------------- --------------- ---------------
1           USA             WA              Seattle
2           USA             WA              Tacoma
3           USA             WA              Kirkland
4           USA             WA              Redmond
5           UK              NULL            London
6           UK              NULL            London
7           UK              NULL            London
8           USA             WA              Seattle
9           UK              NULL            London

Notez que pour certains employés, la partie région n'est pas pertinente et qu'une région non pertinente est représentée par un NULL. Supposons que vous ayez besoin de concaténer les parties de localisation (pays, région et ville), en utilisant une virgule comme séparateur, mais en ignorant les régions NULL. Lorsque la région est pertinente, vous souhaitez que le résultat ait la forme <coutry>,<region>,<city> et lorsque la région n'est pas pertinente, vous voulez que le résultat ait la forme <country>,<city> . Normalement, concaténer quelque chose avec un NULL produit un résultat NULL. Vous pouvez modifier ce comportement en désactivant l'option de session CONCAT_NULL_YIELDS_NULL, mais je ne recommanderais pas d'activer le comportement non standard.

Si vous ne connaissiez pas l'existence des fonctions CONCAT et CONCAT_WS, vous auriez probablement utilisé ISNULL ou COALESCE pour remplacer un NULL par une chaîne vide, comme ceci :

SELECT empid, country + ISNULL(',' + region, '') + ',' + city AS location
FROM HR.Employees;

Voici le résultat de cette requête :

empid       location
----------- -----------------------------------------------
1           USA,WA,Seattle
2           USA,WA,Tacoma
3           USA,WA,Kirkland
4           USA,WA,Redmond
5           UK,London
6           UK,London
7           UK,London
8           USA,WA,Seattle
9           UK,London

SQL Server 2012 a introduit la fonction CONCAT. Cette fonction accepte une liste d'entrées de chaînes de caractères et les concatène, et ce faisant, elle ignore les valeurs NULL. Donc, en utilisant CONCAT, vous pouvez simplifier la solution comme ceci :

SELECT empid, CONCAT(country, ',' + region, ',', city) AS location
FROM HR.Employees;

Néanmoins, vous devez spécifier explicitement les séparateurs dans le cadre des entrées de la fonction. Pour nous faciliter encore la vie, SQL Server 2017 a introduit une fonction similaire appelée CONCAT_WS où vous commencez par indiquer le séparateur, suivi des éléments que vous souhaitez concaténer. Avec cette fonction, la solution est encore simplifiée comme suit :

SELECT empid, CONCAT_WS(',', country, region, city) AS location
FROM HR.Employees;

La prochaine étape est bien sûr la lecture mentale. Le 1er avril 2020, Microsoft prévoit de publier CONCAT_MR. La fonction acceptera une entrée vide et déterminera automatiquement les éléments que vous souhaitez concaténer en lisant votre esprit. La requête ressemblera alors à ceci :

SELECT empid, CONCAT_MR() AS location
FROM HR.Employees;

LOG a un deuxième paramètre

Semblable à la fonction EOMONTH, beaucoup de gens ne réalisent pas qu'à partir de SQL Server 2012, la fonction LOG prend en charge un deuxième paramètre qui vous permet d'indiquer la base du logarithme. Auparavant, T-SQL prenait en charge la fonction LOG(input) qui renvoie le logarithme népérien de l'entrée (en utilisant la constante e comme base) et LOG10(input) qui utilise 10 comme base.

Ne pas être au courant de l'existence du deuxième paramètre de la fonction LOG, lorsque les gens voulaient calculer Logb (x), où b est une base autre que e et 10, ils l'ont souvent fait sur le long chemin. Vous pouvez vous fier à l'équation suivante :

Logb (x) =Loga (x)/Loga (b)

Par exemple, pour calculer Log2 (8), vous vous basez sur l'équation suivante :

Journal2 (8) =Loge (8)/Loge (2)

Traduit en T-SQL, vous appliquez le calcul suivant :

DECLARE @x AS FLOAT = 8, @b AS INT = 2;
SELECT LOG(@x) / LOG(@b);

Une fois que vous vous rendez compte que LOG prend en charge un deuxième paramètre où vous indiquez la base, le calcul devient simplement :

DECLARE @x AS FLOAT = 8, @b AS INT = 2;
SELECT LOG(@x, @b);

Variable de curseur

Si vous travaillez avec T-SQL depuis un certain temps, vous avez probablement eu de nombreuses occasions de travailler avec des curseurs. Comme vous le savez, lorsque vous travaillez avec un curseur, vous suivez généralement les étapes suivantes :

  • Déclarer le curseur
  • Ouvrir le curseur
  • Parcourir les enregistrements du curseur
  • Fermer le curseur
  • Libérer le curseur

Par exemple, supposons que vous deviez effectuer une tâche par base de données dans votre instance. À l'aide d'un curseur, vous utiliseriez normalement un code semblable à celui-ci :

DECLARE @dbname AS sysname;
 
DECLARE C CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
  SELECT name FROM sys.databases;
 
OPEN C;
 
FETCH NEXT FROM C INTO @dbname;
 
WHILE @@FETCH_STATUS = 0
BEGIN
  PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...';
  /* ... do your thing here ... */
  FETCH NEXT FROM C INTO @dbname;
END;
 
CLOSE C;
DEALLOCATE C;

La commande CLOSE libère le jeu de résultats actuel et libère les verrous. La commande DEALLOCATE supprime une référence de curseur et, lorsque la dernière référence est désallouée, libère les structures de données comprenant le curseur. Si vous essayez d'exécuter le code ci-dessus deux fois sans les commandes CLOSE et DEALLOCATE, vous obtiendrez l'erreur suivante :

Msg 16915, Level 16, State 1, Line 4
A cursor with the name 'C' already exists.
Msg 16905, Level 16, State 1, Line 6
The cursor is already open.

Assurez-vous d'exécuter les commandes CLOSE et DEALLOCATE avant de continuer.

Beaucoup de gens ne réalisent pas que lorsqu'ils doivent travailler avec un curseur dans un seul lot, ce qui est le cas le plus courant, au lieu d'utiliser un curseur normal, vous pouvez travailler avec une variable de curseur. Comme toute variable, la portée d'une variable de curseur est uniquement le lot où elle a été déclarée. Cela signifie que dès qu'un lot se termine, toutes les variables expirent. À l'aide d'une variable de curseur, une fois qu'un lot est terminé, SQL Server se ferme et le libère automatiquement, vous évitant ainsi d'avoir à exécuter explicitement les commandes CLOSE et DEALLOCATE.

Voici le code révisé utilisant une variable de curseur cette fois :

DECLARE @dbname AS sysname, @C AS CURSOR;
 
SET @C = CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
  SELECT name FROM sys.databases;
 
OPEN @C;
 
FETCH NEXT FROM @C INTO @dbname;
 
WHILE @@FETCH_STATUS = 0
BEGIN
  PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...';
  /* ... do your thing here ... */
  FETCH NEXT FROM @C INTO @dbname;
END;

N'hésitez pas à l'exécuter plusieurs fois et notez que cette fois vous n'obtenez aucune erreur. C'est juste plus propre, et vous n'avez pas à vous soucier de conserver les ressources du curseur si vous avez oublié de fermer et de désallouer le curseur.

FUSIONNER avec OUTPUT

Depuis la création de la clause OUTPUT pour les instructions de modification dans SQL Server 2005, elle s'est avérée être un outil très pratique chaque fois que vous souhaitiez renvoyer des données à partir de lignes modifiées. Les gens utilisent régulièrement cette fonctionnalité à des fins telles que l'archivage, l'audit et de nombreux autres cas d'utilisation. L'une des choses ennuyeuses à propos de cette fonctionnalité, cependant, est que si vous l'utilisez avec des instructions INSERT, vous n'êtes autorisé à renvoyer que les données des lignes insérées, en préfixant les colonnes de sortie avec insert . Vous n'avez pas accès aux colonnes de la table source, même si parfois vous devez renvoyer des colonnes de la source à côté des colonnes de la cible.

Prenons l'exemple des tables T1 et T2, que vous créez et remplissez en exécutant le code suivant :

DROP TABLE IF EXISTS dbo.T1, dbo.T2;
GO
 
CREATE TABLE dbo.T1(keycol INT NOT NULL IDENTITY PRIMARY KEY, datacol VARCHAR(10) NOT NULL);
 
CREATE TABLE dbo.T2(keycol INT NOT NULL IDENTITY PRIMARY KEY, datacol VARCHAR(10) NOT NULL);
 
INSERT INTO dbo.T1(datacol) VALUES('A'),('B'),('C'),('D'),('E'),('F');

Notez qu'une propriété d'identité est utilisée pour générer les clés dans les deux tables.

Supposons que vous ayez besoin de copier certaines lignes de T1 vers T2 ; disons, ceux où keycol % 2 =1. Vous souhaitez utiliser la clause OUTPUT pour renvoyer les clés nouvellement générées dans T2, mais vous souhaitez également renvoyer à côté de ces clés les clés source respectives de T1. L'attente intuitive est d'utiliser l'instruction INSERT suivante :

INSERT INTO dbo.T2(datacol)
    OUTPUT T1.keycol AS T1_keycol, inserted.keycol AS T2_keycol
  SELECT datacol FROM dbo.T1 WHERE keycol % 2 = 1;

Malheureusement, comme mentionné, la clause OUTPUT ne vous permet pas de faire référence aux colonnes de la table source, vous obtenez donc l'erreur suivante :

Msg 4104, Niveau 16, État 1, Ligne 2
L'identifiant en plusieurs parties "T1.keycol" n'a pas pu être lié.

Beaucoup de gens ne réalisent pas que curieusement cette limitation ne s'applique pas à l'instruction MERGE. Donc, même si c'est un peu gênant, vous pouvez convertir votre instruction INSERT en une instruction MERGE, mais pour ce faire, vous avez besoin que le prédicat MERGE soit toujours faux. Cela activera la clause WHEN NOT MATCHED et y appliquera la seule action INSERT prise en charge. Vous pouvez utiliser une fausse condition factice telle que 1 =2. Voici le code converti complet :

MERGE INTO dbo.T2 AS TGT
USING (SELECT keycol, datacol FROM dbo.T1 WHERE keycol % 2 = 1) AS SRC 
  ON 1 = 2
WHEN NOT MATCHED THEN
  INSERT(datacol) VALUES(SRC.datacol)
OUTPUT SRC.keycol AS T1_keycol, inserted.keycol AS T2_keycol;

Cette fois, le code s'exécute avec succès, produisant la sortie suivante :

T1_keycol   T2_keycol
----------- -----------
1           1
3           2
5           3

Espérons que Microsoft améliorera la prise en charge de la clause OUTPUT dans les autres instructions de modification pour permettre également de renvoyer des colonnes à partir de la table source.

Conclusion

Ne présumez pas, et RTFM ! :-)