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

Principes fondamentaux des expressions de table, Partie 2 – Tables dérivées, considérations logiques

Le mois dernier, j'ai fourni un aperçu des expressions de table dans T-SQL. J'ai expliqué le contexte à partir de la théorie relationnelle et du standard SQL. J'ai expliqué comment une table en SQL est une tentative de représenter une relation à partir de la théorie relationnelle. J'ai également expliqué qu'une expression relationnelle est une expression opérant sur une ou plusieurs relations en tant qu'entrées et résultant en une relation. De même, en SQL, une expression de table est une expression opérant sur une ou plusieurs tables d'entrée et résultant en une table. L'expression peut être une requête, mais ce n'est pas obligatoire. Par exemple, l'expression peut être un constructeur de valeur de table, comme je l'expliquerai plus loin dans cet article. J'ai également expliqué que dans cette série, je me concentre sur quatre types spécifiques d'expressions de table nommées prises en charge par T-SQL :les tables dérivées, les expressions de table communes (CTE), les vues et les fonctions de table en ligne (TVF).

Si vous travaillez avec T-SQL depuis un certain temps, vous êtes probablement tombé sur plusieurs cas où vous deviez utiliser des expressions de table, ou c'était en quelque sorte plus pratique par rapport aux solutions alternatives qui ne les utilisent pas. Voici quelques exemples de cas d'utilisation qui me viennent à l'esprit :

  • Créez une solution modulaire en décomposant les tâches complexes en étapes, chacune représentée par une expression de table différente.
  • Mélanger les résultats des requêtes groupées et des détails, au cas où vous décidez de ne pas utiliser les fonctions de fenêtre à cette fin.
  • Traitement logique des requêtes gère les clauses de requête dans l'ordre suivant :FROM>WHERE>GROUP BY>HAVING>SELECT>ORDER BY. Par conséquent, au même niveau d'imbrication, les alias de colonne que vous définissez dans la clause SELECT ne sont disponibles que pour la clause ORDER BY. Ils ne sont pas disponibles pour le reste des clauses de requête. Avec les expressions de table, vous pouvez réutiliser les alias que vous définissez dans une requête interne dans n'importe quelle clause de la requête externe, et ainsi éviter la répétition d'expressions longues/complexes.
  • Les fonctions de fenêtre ne peuvent apparaître que dans les clauses SELECT et ORDER BY d'une requête. Avec les expressions de table, vous pouvez affecter un alias à une expression basée sur une fonction de fenêtre, puis utiliser cet alias dans une requête sur l'expression de table.
  • Un opérateur PIVOT implique trois éléments :le regroupement, la répartition et l'agrégation. Cet opérateur identifie implicitement l'élément de regroupement par élimination. À l'aide d'une expression de table, vous pouvez projeter exactement les trois éléments censés être impliqués et faire en sorte que la requête externe utilise l'expression de table comme table d'entrée de l'opérateur PIVOT, contrôlant ainsi quel élément est l'élément de regroupement.
  • Les modifications avec TOP ne prennent pas en charge une clause ORDER BY. Vous pouvez contrôler quelles lignes sont choisies indirectement en définissant une expression de table basée sur une requête SELECT avec le filtre TOP ou OFFSET-FETCH et une clause ORDER BY, et appliquer la modification à l'expression de table.

Cette liste est loin d'être exhaustive. Je vais démontrer certains des cas d'utilisation ci-dessus et d'autres dans cette série. Je voulais juste mentionner ici quelques cas d'utilisation pour illustrer l'importance des expressions de table dans notre code T-SQL et pourquoi il vaut la peine d'investir dans la compréhension de leurs principes fondamentaux.

Dans l'article de ce mois-ci, je me concentre spécifiquement sur le traitement logique des tables dérivées.

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

Tables dérivées

Le terme table dérivée est utilisé dans SQL et T-SQL avec plusieurs significations. Je veux donc d'abord préciser à laquelle je fais référence dans cet article. Je fais référence à une construction de langage spécifique que vous définissez généralement, mais pas uniquement, dans la clause FROM d'une requête externe. Je fournirai la syntaxe de cette construction sous peu.

L'utilisation plus générale du terme table dérivée en SQL est la contrepartie d'une relation dérivée de la théorie relationnelle. Une relation dérivée est une relation de résultat qui est dérivée d'une ou plusieurs relations de base d'entrée, en appliquant des opérateurs relationnels de l'algèbre relationnelle comme la projection, l'intersection et autres à ces relations de base. De même, au sens général, une table dérivée en SQL est une table de résultats dérivée d'une ou plusieurs tables de base, en évaluant des expressions par rapport à ces tables de base d'entrée.

En passant, j'ai vérifié comment le standard SQL définit une table de base et j'ai immédiatement été désolé d'avoir dérangé.

4.15.2 Tableaux de base

Une table de base est soit une table de base persistante, soit une table temporaire.

Une table de base persistante est soit une table de base persistante normale, soit une table versionnée par le système.

Une table de base régulière est soit une table de base persistante régulière, soit une table temporaire.”

Ajouté ici sans autre commentaire…

Dans T-SQL, vous pouvez créer une table de base avec une instruction CREATE TABLE, mais il existe d'autres options, par exemple, SELECT INTO et DECLARE @T AS TABLE.

Voici la définition standard des tables dérivées au sens général :

4.15.3 Tables dérivées

Une table dérivée est une table dérivée directement ou indirectement d'une ou plusieurs autres tables par l'évaluation d'une expression, telle qu'une

, une
, une ou une . Une expression peut contenir une facultative. L'ordre des lignes de la table spécifiée par la n'est garanti que pour la qui contient immédiatement la .”

Il y a quelques choses intéressantes à noter ici sur les tables dérivées au sens général. L'un a à voir avec le commentaire sur la commande. J'y reviendrai plus tard dans l'article. Une autre est qu'une table dérivée en SQL peut être une expression de table autonome valide, mais n'a pas à l'être. Par exemple, l'expression suivante représente une table dérivée, et est également considérée comme une expression de table autonome valide (vous pouvez l'exécuter) :

SELECT custid, companynameFROM Sales.CustomersWHERE country =N'USA'

Inversement, l'expression suivante représente une table dérivée, mais n'est pas une expression de table autonome valide :

T1 INNER JOIN T2 ON T1.keycol =T2.keycol

T-SQL prend en charge un certain nombre d'opérateurs de table qui génèrent une table dérivée, mais ne sont pas pris en charge en tant qu'expressions autonomes. Ce sont :JOIN, PIVOT, UNPIVOT et APPLY. Vous avez besoin d'une clause pour qu'ils fonctionnent à l'intérieur (généralement FROM, mais aussi la clause USING de l'instruction MERGE) et d'une requête hôte.

À partir de maintenant, j'utiliserai le terme table dérivée pour décrire une construction de langage plus spécifique et non dans le sens général décrit ci-dessus.

Syntaxe

Une table dérivée peut être définie dans le cadre d'une instruction SELECT externe dans sa clause FROM. Il peut également être défini dans le cadre des instructions DELETE et UPDATE dans leur clause FROM, et dans le cadre d'une instruction MERGE dans sa clause USING. Je fournirai plus de détails sur la syntaxe lorsqu'elle est utilisée dans les instructions de modification plus loin dans cet article.

Voici la syntaxe d'une requête SELECT simplifiée sur une table dérivée :

SELECT
FROM ( ) [ AS ] [ () ];

La définition de la table dérivée apparaît là où une table de base peut normalement apparaître, dans la clause FROM de la requête externe. Il peut s'agir d'une entrée pour un opérateur de table tel que JOIN, APPLY, PIVOT et UNPIVOT. Lorsqu'elle est utilisée comme entrée correcte d'un opérateur APPLY, la partie de la table dérivée est autorisée à avoir des corrélations avec les colonnes d'une table externe (plus d'informations à ce sujet dans un futur article dédié de la série). Sinon, l'expression de table doit être autonome.

L'instruction externe peut avoir tous les éléments de requête habituels. Dans le cas d'une instruction SELECT :WHERE, GROUP BY, HAVING, ORDER BY et, comme mentionné, les opérateurs de table dans la clause FROM.

Voici un exemple de requête simple sur une table dérivée représentant des clients américains :

SELECT custid, companynameFROM ( SELECT custid, companyname FROM Sales.Customers WHERE country =N'USA' ) AS UC ;

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

nom de l'entreprise client------- ---------------32 Client YSIQX36 Client LVJSO43 Client UISOJ45 Client QXPPT48 Client DVFMB55 Client KZQZT65 Client NYUHS71 Client LCOUJ75 Client XOJYP77 Client LCYBZ78 Client NLTYP82 Client EYHKM89 Client YBQTI

Il existe trois parties principales à identifier dans une instruction impliquant une définition de table dérivée :

  1. L'expression de table (la requête interne)
  2. Le nom de la table dérivée, ou plus précisément, ce qui, dans la théorie relationnelle, est considéré comme une variable de plage
  3. La déclaration externe

L'expression de table est censée représenter une table et, en tant que telle, doit satisfaire certaines exigences qu'une requête normale n'a pas nécessairement besoin de satisfaire. Je fournirai les détails sous peu dans la section "Une expression de table est une table".

Quant au nom de la table dérivée cible ; une hypothèse courante parmi les développeurs T-SQL est qu'il s'agit simplement d'un nom ou d'un alias que vous attribuez à la table cible. De même, considérez la requête suivante :

SELECT custid, companynameFROM Sales.Customers AS CWHERE country =N'USA';

Ici également, l'hypothèse courante est que AS C est simplement un moyen de renommer, ou d'alias, la table Customers aux fins de cette requête, en commençant par l'étape de traitement de la requête logique où le nom est attribué et au-delà. Cependant, du point de vue de la théorie relationnelle, il y a un sens plus profond à ce que représente C. C est ce qu'on appelle une variable de plage. C est une variable de relation dérivée qui s'étend sur les tuples de la variable de relation d'entrée Customers. Dans l'exemple ci-dessus, C parcourt les tuples dans Customers et évalue le prédicat country =N'USA'. Les tuples pour lesquels le prédicat est évalué comme vrai font partie de la relation de résultat C.

Une expression de table est une table

Avec le contexte que j'ai fourni jusqu'à présent, ce que je vais vous expliquer ensuite ne devrait pas vous surprendre. La partie d'une définition de table dérivée est une table . C'est le cas même si c'est exprimé sous forme de requête. Vous souvenez-vous de la propriété de fermeture de l'algèbre relationnelle ? Il en va de même pour le reste des expressions de table nommées susmentionnées (CTE, vues et TVF en ligne). Comme vous l'avez déjà appris, la table de SQL est le pendant de la relation de la théorie relationnelle , bien qu'il ne s'agisse pas d'une contrepartie parfaite. Ainsi, une expression de table doit satisfaire certaines exigences pour s'assurer que le résultat est une table, celles qu'une requête qui n'est pas utilisée comme expression de table n'a pas nécessairement à respecter. Voici trois exigences spécifiques :

  • Toutes les colonnes de l'expression de table doivent avoir des noms
  • Tous les noms de colonne de l'expression de table doivent être uniques
  • Les lignes de l'expression de table n'ont pas d'ordre

Décomposons ces exigences une par une, en discutant de la pertinence à la fois de la théorie relationnelle et de SQL.

Toutes les colonnes doivent avoir des noms

Rappelez-vous qu'une relation a un titre et un corps. L'en-tête d'une relation est un ensemble d'attributs (colonnes en SQL). Un attribut a un nom et un nom de type, et est identifié par son nom. Une requête qui n'est pas utilisée comme expression de table ne doit pas nécessairement attribuer des noms à toutes les colonnes cibles. Considérez la requête suivante comme exemple :

SELECT empid, prénom, nom, CONCAT_WS(N'/', pays, région, ville)FROM HR.Employees ;

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

empid firstname lastname (Aucun nom de colonne)------ ---------- ---------- ------------- ----1 Sara Davis États-Unis/WA/Seattle2 Don Funk États-Unis/WA/Tacoma3 Judy Lew États-Unis/WA/Kirkland4 Yael Peled États-Unis/WA/Redmond5 Sven Mortensen Royaume-Uni/Londres6 Paul Suurs Royaume-Uni/Londres7 Russell King Royaume-Uni/Londres8 Maria Cameron États-Unis/WA/Seattle9 Patricia Doyle Royaume-Uni/Londres

La sortie de la requête comporte une colonne anonyme résultant de la concaténation des attributs d'emplacement à l'aide de la fonction CONCAT_WS. (Au fait, cette fonction a été ajoutée dans SQL Server 2017, donc si vous exécutez le code dans une version antérieure, n'hésitez pas à remplacer ce calcul par un calcul alternatif de votre choix.) Cette requête, par conséquent, ne retourner une table, pour ne pas parler d'une relation. Par conséquent, il n'est pas valide d'utiliser une telle requête comme partie expression de table/requête interne d'une définition de table dérivée.

Essayez-le :

SELECT *FROM ( SELECT empid, firstname, lastname, CONCAT_WS(N'/', country, region, city) FROM HR.Employees ) AS D ;

Vous obtenez l'erreur suivante :

Msg 8155, Niveau 16, État 2, Ligne 50
Aucun nom de colonne n'a été spécifié pour la colonne 4 de 'D'.

En aparté, remarquez quelque chose d'intéressant dans le message d'erreur ? Il se plaint de la colonne 4, soulignant la différence entre les colonnes en SQL et les attributs en théorie relationnelle.

La solution est, bien sûr, de s'assurer que vous attribuez explicitement des noms aux colonnes qui résultent des calculs. T-SQL prend en charge un certain nombre de techniques de nommage de colonnes. Je vais en citer deux.

Vous pouvez utiliser une technique de dénomination en ligne dans laquelle vous attribuez le nom de la colonne cible après le calcul et une clause AS facultative, comme dans < expression > [ AS ] < column name > , comme ceci :

SELECT empid, firstname, lastname, custlocationFROM ( SELECT empid, firstname, lastname, CONCAT_WS(N'/', country, region, city) AS custlocation FROM HR.Employees ) AS D ;

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

empid firstname lastname custlocation------ ---------- ---------- ----------------1 Sara Davis États-Unis/WA/Seattle2 Don Funk États-Unis/WA/Tacoma3 Judy Lew États-Unis/WA/Kirkland4 Yael Peled États-Unis/WA/Redmond5 Sven Mortensen Royaume-Uni/Londres6 Paul Suurs Royaume-Uni/Londres7 Russell King Royaume-Uni/Londres8 Maria Cameron États-Unis/WA/Seattle9 Patricia Doyle Royaume-Uni/Londres

En utilisant cette technique, il est très facile lors de la révision du code de dire quel nom de colonne cible est attribué à quelle expression. De plus, vous n'avez qu'à nommer les colonnes qui n'ont pas encore de nom autrement.

Vous pouvez également utiliser une technique de dénomination de colonne plus externe dans laquelle vous spécifiez les noms de colonne cible entre parenthèses juste après le nom de la table dérivée, comme ceci :

SELECT empid, firstname, lastname, custlocationFROM ( SELECT empid, firstname, lastname, CONCAT_WS(N'/', country, region, city) FROM HR.Employees ) AS D(empid, firstname, lastname, custlocation); 

Avec cette technique cependant, vous devez répertorier les noms de toutes les colonnes, y compris celles qui ont déjà des noms. L'attribution des noms de colonne cible se fait par position, de gauche à droite, c'est-à-dire que le premier nom de colonne cible représente la première expression dans la liste SELECT de la requête interne ; le nom de la deuxième colonne cible représente la deuxième expression ; etc.

Notez qu'en cas d'incohérence entre les noms de colonne interne et externe, par exemple en raison d'un bogue dans le code, la portée des noms internes est la requête interne ou, plus précisément, la variable de plage interne (ici implicitement HR.Employees AS Employees) - et la portée des noms externes est la variable de plage externe (D dans notre cas). La portée des noms de colonne est un peu plus impliquée dans le traitement logique des requêtes, mais c'est un élément pour des discussions ultérieures.

Le potentiel de bogues avec la syntaxe de nommage externe est mieux expliqué avec un exemple.

Examinez le résultat de la requête précédente, avec l'ensemble complet d'employés de la table HR.Employees. Ensuite, considérez la requête suivante, et avant de l'exécuter, essayez de déterminer quels employés vous vous attendez à voir dans le résultat :

SELECT empid, firstname, lastname, custlocationFROM ( SELECT empid, firstname, lastname, CONCAT_WS(N'/', country, region, city) FROM HR.Employees WHERE lastname LIKE N'D%' ) AS D(empid, nom, prénom, custlocation)WHERE prénom LIKE N'D%';

Si vous vous attendez à ce que la requête renvoie un ensemble vide pour les exemples de données donnés, puisqu'il n'y a actuellement aucun employé dont le nom et le prénom commencent par la lettre D, il vous manque le bogue dans le code.

Exécutez maintenant la requête et examinez le résultat réel :

empid firstname lastname custlocation------ ---------- --------- ---------------1 Davis Sara États-Unis/WA/Seattle9 Doyle Patricia Royaume-Uni/Londres

Que s'est-il passé ?

La requête interne spécifie firstname comme deuxième colonne et lastname comme troisième colonne dans la liste SELECT. Le code qui attribue les noms de colonne cible de la table dérivée dans la requête externe spécifie le nom de famille en deuxième et le prénom en troisième. Les noms de code firstname as lastname et lastname as firstname dans la variable de plage D. En fait, vous ne filtrez que les employés dont le nom commence par la lettre D. Vous ne filtrez pas les employés dont le nom et le prénom commencent par avec la lettre D.

La syntaxe d'alias en ligne n'est pas sujette à de tels bogues. D'une part, vous n'utilisez normalement pas d'alias pour une colonne qui a déjà un nom qui vous convient. Deuxièmement, même si vous souhaitez affecter un alias différent à une colonne qui a déjà un nom, il est peu probable qu'avec la syntaxe AS vous affectiez le mauvais alias. Pensez-y; quelle est la probabilité que vous écriviez comme ceci :

SELECT empid, firstname, lastname, custlocationFROM ( SELECT empid AS empid, firstname AS lastname, lastname AS firstname, CONCAT_WS(N'/', country, region, city) AS custlocation FROM HR.Employees WHERE lastname LIKE N'D %' ) AS DWHERE prénom LIKE N'D%';

Évidemment, peu probable.

Tous les noms de colonne doivent être uniques

Revenons au fait que l'en-tête d'une relation est un ensemble d'attributs, et étant donné qu'un attribut est identifié par son nom, les noms d'attributs doivent être uniques pour une même relation. Dans une requête donnée, vous pouvez toujours faire référence à un attribut en utilisant un nom en deux parties avec le nom de la variable de plage comme qualificatif, comme dans .. Lorsque le nom de la colonne sans le qualificatif est sans ambiguïté, vous pouvez omettre le préfixe du nom de la variable de plage. Ce qu'il est important de retenir, cependant, c'est ce que j'ai dit plus tôt à propos de la portée des noms de colonne. Dans le code qui implique une expression de table nommée, avec à la fois une requête interne (l'expression de table) et une requête externe, la portée des noms de colonne dans la requête interne correspond aux variables de plage internes et la portée des noms de colonne dans la requête externe. query sont les variables de la plage extérieure. Si la requête interne implique plusieurs tables source avec le même nom de colonne, vous pouvez toujours faire référence à ces colonnes de manière non ambiguë en ajoutant le nom de la variable de plage comme préfixe. Si vous n'affectez pas explicitement un nom de variable de plage, vous en obtenez un implicitement, comme si vous utilisiez AS .

Considérez la requête autonome suivante comme exemple :

SELECT C.custid, O.custid, O.orderidFROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid;

Cette requête n'échoue pas avec une erreur de nom de colonne en double car une colonne custid est en fait nommée C.custid et l'autre O.custid dans la portée de la requête actuelle. Cette requête génère la sortie suivante :

custid custid orderid----------- ----------- -----------1 1 106431 1 106921 1 107021 1 108351 1 109521 1 110112 2 103082 2 106252 2 107592 2 10926...

Cependant, essayez d'utiliser cette requête comme expression de table dans la définition d'une table dérivée nommée CO, comme ceci :

SELECT *FROM ( SELECT C.custid, O.custid, O.orderid FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid ) AS CO ;

En ce qui concerne la requête externe, vous avez une variable de plage nommée CO, et la portée de tous les noms de colonne dans la requête externe est cette variable de plage. Les noms de toutes les colonnes d'une variable de plage donnée (rappelez-vous qu'une variable de plage est une variable de relation) doivent être uniques. Par conséquent, vous obtenez l'erreur suivante :

Msg 8156, Niveau 16, État 1, Ligne 80
La colonne 'custid' a été spécifiée plusieurs fois pour 'CO'.

La solution consiste bien sûr à attribuer des noms de colonne différents aux deux colonnes custid en ce qui concerne la variable de plage CO, comme ceci :

SELECT *FROM ( SELECT C.custid AS custid, O.custid AS ordercustid, O.orderid FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid ) AS CO; 

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

custcustid ordercustid orderid----------- ----------- -----------1 1 106431 1 106921 1 107021 1 108351 1 109521 1 110112 2 103082 2 106252 2 107592 2 10926...

Si vous suivez les bonnes pratiques, vous répertoriez explicitement les noms de colonne dans la liste SELECT de la requête la plus externe. Puisqu'il n'y a qu'une seule variable de plage impliquée, vous n'avez pas besoin d'utiliser le nom en deux parties pour les références de colonne externes. Si vous souhaitez utiliser le nom en deux parties, vous préfixez les noms de colonne avec le nom de variable de plage externe CO, comme ceci :

SELECT CO.custcustid, CO.ordercustid, CO.orderidFROM ( SELECT C.custid AS custid, O.custid AS ordercustid, O.orderid FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid ) AS CO;

Pas de commande

Il y a beaucoup de choses que j'ai à dire sur les expressions de table nommées et l'ordre - assez pour un article à part entière - donc je consacrerai un futur article à ce sujet. Pourtant, je voulais aborder brièvement le sujet ici car il est si important. Rappelons que le corps d'une relation est un ensemble de tuples, et de même, le corps d'une table est un ensemble de lignes. Un ensemble n'a pas d'ordre. Pourtant, SQL permet à la requête la plus externe d'avoir une clause ORDER BY servant une signification d'ordre de présentation, comme le montre la requête suivante :

SELECT orderid, valFROM Sales.OrderValuesORDER BY val DESC ;

Ce que vous devez comprendre, cependant, c'est que cette requête ne renvoie pas de relation en conséquence. Même du point de vue de SQL, la requête ne renvoie pas de table en conséquence, et donc elle ne l'est pas considérée comme une expression de table. Par conséquent, il n'est pas valide d'utiliser une telle requête comme partie d'expression de table d'une définition de table dérivée.

Essayez d'exécuter le code suivant :

SELECT orderid, valFROM ( SELECT orderid, val FROM Sales.OrderValues ​​ORDER BY val DESC ) AS D ;

Vous obtenez l'erreur suivante :

Msg 1033, niveau 15, état 1, ligne 124
La clause ORDER BY n'est pas valide dans les vues, les fonctions en ligne, les tables dérivées, les sous-requêtes et les expressions de table communes, sauf si TOP, OFFSET ou FOR XML est également spécifié.

J'aborderai le sauf si une partie du message d'erreur sous peu.

Si vous voulez que la requête la plus externe renvoie un résultat ordonné, vous devez spécifier la clause ORDER BY dans la requête la plus externe, comme suit :

SELECT orderid, valFROM ( SELECT orderid, val FROM Sales.OrderValues ​​) AS DORDER BY val DESC ;

Quant au sauf si une partie du message d'erreur ; T-SQL prend en charge le filtre TOP propriétaire ainsi que le filtre standard OFFSET-FETCH. Les deux filtres s'appuient sur une clause ORDER BY dans la même étendue de requête pour définir pour eux les premières lignes à filtrer. C'est malheureusement le résultat d'un piège dans la conception de ces fonctionnalités, qui ne sépare pas l'ordre de présentation de l'ordre des filtres. Quoi qu'il en soit, Microsoft avec son filtre TOP et la norme avec son filtre OFFSET-FETCH permettent de spécifier une clause ORDER BY dans la requête interne tant qu'elle spécifie également le filtre TOP ou OFFSET-FETCH, respectivement. Donc, cette requête est valide, par exemple :

SELECT orderid, valFROM ( SELECT TOP (3) orderid, val FROM Sales.OrderValues ​​ORDER BY val DESC ) AS D ;

Lorsque j'ai exécuté cette requête sur mon système, elle a généré le résultat suivant :

id_commande-------- ---------10865 16387.5010981 15810.0011030 12615.05

Ce qu'il est important de souligner cependant, c'est que la seule raison pour laquelle la clause ORDER BY est autorisée dans la requête interne est de prendre en charge le filtre TOP. C'est la seule garantie que vous obtenez en ce qui concerne la commande. Étant donné que la requête externe n'a pas non plus de clause ORDER BY, vous n'obtenez aucune garantie pour un ordre de présentation spécifique à partir de cette requête, quel que soit le comportement observé. C'est à la fois le cas dans T-SQL, ainsi que dans la norme. Voici une citation de la norme traitant de cette partie :

"L'ordre des lignes de la table spécifiée par n'est garanti que pour l' qui contient immédiatement la ."

Comme mentionné, il y a beaucoup plus à dire sur les expressions de table et l'ordre, ce que je ferai dans un prochain article. Je fournirai également des exemples démontrant comment l'absence de clause ORDER BY dans la requête externe signifie que vous n'obtenez aucune garantie de classement de présentation.

Ainsi, une expression de table, par exemple une requête interne dans une définition de table dérivée, est une table. De même, une table dérivée (au sens spécifique) est elle-même aussi une table. Ce n'est pas une table basse, mais c'est quand même une table. Il en va de même pour les CTE, les vues et les TVF en ligne. Ce ne sont pas des tables de base, plutôt des tables dérivées (au sens le plus général), mais ce sont néanmoins des tables.

Défauts de conception

Les tables dérivées présentent deux défauts principaux dans leur conception. Les deux sont liés au fait que la table dérivée est définie dans la clause FROM de la requête externe.

Un défaut de conception a à voir avec le fait que si vous devez interroger une table dérivée à partir d'une requête externe, et à son tour utiliser cette requête comme expression de table dans une autre définition de table dérivée, vous finissez par imbriquer ces requêtes de table dérivée. En informatique, l'imbrication explicite de code impliquant plusieurs niveaux d'imbrication a tendance à aboutir à un code complexe difficile à maintenir.

Voici un exemple très basique le démontrant :

SELECT orderyear, numcustsFROM ( SELECT orderyear, COUNT(DISTINCT custid) AS numcusts FROM ( SELECT YEAR(orderdate) AS orderyear, custid FROM Sales.Orders ) AS D1 GROUP BY orderyear ) AS D2WHERE numcusts> 70 ;

Ce code renvoie les années de commande et le nombre de clients qui ont passé des commandes au cours de chaque année, uniquement pour les années où le nombre de clients qui ont passé des commandes était supérieur à 70.

La principale motivation pour utiliser des expressions de table ici est de pouvoir se référer plusieurs fois à un alias de colonne. La requête la plus interne utilisée comme expression de table pour la table dérivée D1 interroge la table Sales.Orders et attribue le nom de colonne orderyear à l'expression YEAR(orderdate) et renvoie également la colonne custid. La requête sur D1 regroupe les lignes de D1 par orderyear et renvoie orderyear ainsi que le nombre distinct de clients qui ont passé des commandes au cours de l'année en question, alias numcusts. Le code définit une table dérivée appelée D2 basée sur cette requête. La requête la plus à l'extérieur des requêtes D2 et filtre uniquement les années où le nombre de clients ayant passé des commandes était supérieur à 70.

Une tentative de révision de ce code ou de dépannage en cas de problème est délicate en raison des multiples niveaux d'imbrication. Au lieu d'examiner le code de la manière la plus naturelle de haut en bas, vous vous retrouvez à devoir l'analyser en commençant par l'unité la plus interne et en allant progressivement vers l'extérieur, car c'est plus pratique.

L'intérêt de l'utilisation de tables dérivées dans cet exemple était de simplifier le code en évitant d'avoir à répéter des expressions. Mais je ne suis pas sûr que cette solution atteigne cet objectif. Dans ce cas, vous feriez probablement mieux de répéter certaines expressions, en évitant d'avoir à utiliser des tables dérivées, comme ceci :

SELECT YEAR(orderdate) AS orderyear, COUNT(DISTINCT custid) AS numcustsFROM Sales.OrdersGROUP BY YEAR(orderdate)HAVING COUNT(DISTINCT custid)> 70 ;

Gardez à l'esprit que je montre ici un exemple très simple à des fins d'illustration. Imaginez un code de production avec plus de niveaux d'imbrication, et avec un code plus long et plus élaboré, et vous pouvez voir comment il devient beaucoup plus compliqué à maintenir.

Un autre défaut dans la conception des tables dérivées concerne les cas où vous devez interagir avec plusieurs instances de la même table dérivée. Considérez la requête suivante comme exemple :

SELECT CUR.orderyear, CUR.numorders, CUR.numorders - PRV.numorders AS diffFROM ( SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders FROM Sales.Orders GROUP BY YEAR(orderdate) ) AS CUR LEFT OUTER JOIN ( SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders FROM Sales.Orders GROUP BY YEAR(orderdate) ) AS PRV ON CUR.orderyear =PRV.orderyear + 1;

Ce code calcule le nombre de commandes traitées chaque année, ainsi que la différence par rapport à l'année précédente. Ignorez le fait qu'il existe des moyens plus simples d'accomplir la même tâche avec les fonctions de fenêtre - j'utilise ce code pour illustrer un certain point, donc la tâche elle-même et les différentes façons de la résoudre ne sont pas significatives.

Une jointure est un opérateur de table qui traite ses deux entrées comme un ensemble, ce qui signifie qu'il n'y a pas d'ordre entre elles. Ils sont appelés les entrées gauche et droite afin que vous puissiez marquer l'un d'eux (ou les deux) comme une table préservée dans une jointure externe, mais il n'y a toujours pas de premier et de deuxième parmi eux. Vous êtes autorisé à utiliser des tables dérivées comme entrées de jointure, mais le nom de la variable de plage que vous affectez à l'entrée de gauche n'est pas accessible dans la définition de l'entrée de droite. C'est parce que les deux sont conceptuellement définis dans la même étape logique, comme s'ils étaient au même moment. Par conséquent, lors de la jointure de tables dérivées, vous ne pouvez pas définir deux variables de plage basées sur une expression de table. Malheureusement, vous devez répéter le code, en définissant deux variables de plage basées sur deux copies identiques du code. Cela complique bien sûr la maintenabilité du code et augmente la probabilité de bogues. Chaque modification que vous apportez à une expression de table doit également être appliquée à l'autre.

Comme je l'expliquerai dans un prochain article, les CTE, dans leur conception, n'encourent pas ces deux défauts que les tables dérivées encourent.

Constructeur de valeur de table

Un constructeur de valeur de table vous permet de construire une valeur de table basée sur des expressions scalaires autonomes. Vous pouvez ensuite utiliser une telle table dans une requête externe, tout comme vous utilisez une table dérivée basée sur une requête interne. Dans un prochain article, je discuterai des tables dérivées latérales et les corrélations en détail, et je montrerai des formes plus sophistiquées de constructeurs de valeurs de table. Dans cet article, cependant, je vais me concentrer sur une forme simple basée uniquement sur des expressions scalaires autonomes.

The general syntax for a query against a table value constructor is as follows:

SELECT
) AS
(
);

The table value constructor is defined in the FROM clause of the outer query.

The table’s body is made of a VALUES clause, followed by a comma separated list of pairs of parentheses, each defining a row with a comma separated list of expressions forming the row’s values.

The table’s heading is a comma separated list of the target column names. I’ll talk about a shortcoming of this syntax regarding the table’s heading shortly.

The following code uses a table value constructor to define a table called MyCusts with three columns called custid, companyname and contractdate, and three rows:

SELECT custid, companyname, contractdateFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate);

The above code is equivalent (both logically and in performance terms) in T-SQL to the following alternative:

SELECT custid, companyname, contractdateFROM ( SELECT 2, 'Cust 2', '20200212' UNION ALL SELECT 3, 'Cust 3', '20200118' UNION ALL SELECT 5, 'Cust 5', '20200401' ) AS MyCusts(custid, companyname, contractdate);

The two are internally algebrized the same way. The syntax with the VALUES clause is standard whereas the syntax with the unified FROMless queries isn’t, hence I prefer the former.

There is a shortcoming in the design of table value constructors in both standard SQL and in T-SQL. Remember that the heading of a relation is made of a set of attributes, and an attribute has a name and a type name. In the table value constructor’s syntax, you specify the column names, but not their data types. Suppose that you need the custid column to be of a SMALLINT type, the companyname column of a VARCHAR(50) type, and the contractdate column of a DATE type. It would have been good if we were able to define the column types as part of the definition of the table’s heading, like so (this syntax isn’t supported):

SELECT custid, companyname, contractdateFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid SMALLINT, companyname VARCHAR(50), contractdate DATE);

That’s of course just wishful thinking.

The way it works in T-SQL, is that each literal that is based on a constant has a predetermined type irrespective of context. For instance, can you guess what the types of the following literals are:

  • 1
  • 2147483647
  • 2147483648
  • 1E
  • '1E'
  • '20200212'

Is 1 considered BIT, INT, SMALLINT, other?

Is 1E considered VARBINARY(1), VARCHAR(2), other?

Is '20200212' considered DATE, DATETIME, VARCHAR(8), CHAR(8), other?

There’s a simple trick to figure out the default type of a literal, using the SQL_VARIANT_PROPERTY function with the 'BaseType' property, like so:

SELECT SQL_VARIANT_PROPERTY(2147483648, 'BaseType');

What happens is that SQL Server implicitly converts the literal to SQL_VARIANT—since that’s what the function expects—but preserves its base type. It then reports the base type as requested.

Similarly, you can query other properties of the input value, like the maximum length (MaxLength), Precision, Scale, and so on.

Try it with the aforementioned literal values, and you will get the following:

  • 1:INT
  • 2147483647:INT
  • 2147483648:NUMERIC(10, 0)
  • 1E:FLOAT
  • '1E':VARCHAR(2)
  • '20200212':VARCHAR(8)

As you can see, SQL Server has default assumptions about the data type, maximum length, precision, scale, and so on.

There are some cases where you need to specify a literal of a certain type, but you cannot do it directly in T-SQL. For example, you cannot specify a literal of the following types directly:BIT, TINYINT, BIGINT, all date and time types, and quite a few others. Unfortunately, T-SQL doesn’t provide a selector property for its types, which would have served exactly the needed purpose of selecting a value of the given type. Of course, you can always convert an expression’s type explicitly using the CAST or CONVERT function, as in CAST(5 AS SMALLINT). If you don’t, SQL Server will sometimes need to implicitly convert some of your expressions to a different type based on its implicit conversion rules. For example, when you try to compare values of different types, e.g., WHERE datecol ='20200212', assuming datecol is of a DATE type. Another example is when you specify a literal in an INSERT or an UPDATE statement, and the literal’s type is different than the target column’s type.

If all this is not confusing enough, set operators like UNION ALL rely on data type precedence to define the target column types—and remember, a table value constructor is algebrized like a series of UNION ALL operations. Consider the table value constructor shown earlier:

SELECT custid, companyname, contractdateFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate);

Each literal here has a predetermined type. 2, 3 and 5 are all of an INT type, so clearly the custid target column type is INT. If you had the values 1000000000, 3000000000 and 2000000000, the first and the third are considered INT and the second is considered NUMERIC(10, 0). According to data type precedence NUMERIC (same as DECIMAL) is stronger than INT, hence in such a case the target column type would be NUMERIC(10, 0).

If you want to figure out which data types SQL Server chooses for the target columns in your table value constructor, you have a few options. One is to use a SELECT INTO statement to write the table value constructor’s data into a temporary table, and then query the metadata for the temporary table, like so:

SELECT custid, companyname, contractdateINTO #MyCustsFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate); SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlengthFROM tempdb.sys.columnsWHERE OBJECT_ID =OBJECT_ID(N'tempdb..#MyCusts');

Here’s the output of this code:

colname typename maxlength------------- ---------- ---------custid int 4companyname varchar 6contractdate varchar 8

You can then drop the temporary table for cleanup:

DROP TABLE IF EXISTS #MyCusts;

Another option is to use the SQL_VARIANT_PROPERTY, which I mentioned earlier, like so:

SELECT TOP (1) SQL_VARIANT_PROPERTY(custid, 'BaseType') AS custid_typename, SQL_VARIANT_PROPERTY(custid, 'MaxLength') AS custid_maxlength, SQL_VARIANT_PROPERTY(companyname, 'BaseType') AS companyname_typename, SQL_VARIANT_PROPERTY(companyname, 'MaxLength') AS companyname_maxlength, SQL_VARIANT_PROPERTY(contractdate, 'BaseType') AS contractdate_typename, SQL_VARIANT_PROPERTY(contractdate, 'MaxLength') AS contractdate_maxlengthFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate);

This code generates the following output (formatted for readability):

custid_typename custid_maxlength-------------------- ---------------- int 4 companyname_typename companyname_maxlength -------------------- --------------------- varchar 6 contractdate_typename contractdate_maxlength--------------------- ----------------------varchar 8

So, what if you need to control the types of the target columns? As mentioned earlier, say you need custid to be SMALLINT, companyname VARCHAR(50), and contractdate DATE.

Don’t be misled to think that it’s enough to explicitly convert just one row’s values. If a corresponding value’s type in any other row is considered stronger, it would dictate the target column’s type. Here’s an example demonstrating this:

SELECT custid, companyname, contractdateINTO #MyCusts1FROM ( VALUES( CAST(2 AS SMALLINT), CAST('Cust 2' AS VARCHAR(50)), CAST('20200212' AS DATE)), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate); SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlengthFROM tempdb.sys.columnsWHERE OBJECT_ID =OBJECT_ID(N'tempdb..#MyCusts1');

Ce code génère la sortie suivante :

colname typename maxlength------------- --------- ---------custid int 4companyname varchar 50contractdate date 3

Notice that the type for custid is INT.

The same applies never mind which row’s values you explicitly convert, if you don’t convert all of them. For example, here the code explicitly converts the types of the values in the second row:

SELECT custid, companyname, contractdateINTO #MyCusts2FROM ( VALUES( 2, 'Cust 2', '20200212'), ( CAST(3 AS SMALLINT), CAST('Cust 3' AS VARCHAR(50)), CAST('20200118' AS DATE) ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate); SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlengthFROM tempdb.sys.columnsWHERE OBJECT_ID =OBJECT_ID(N'tempdb..#MyCusts2');

Ce code génère la sortie suivante :

colname typename maxlength------------- --------- ---------custid int 4companyname varchar 50contractdate date 3

As you can see, custid is still of an INT type.

You basically have two main options. One is to explicitly convert all values, like so:

SELECT custid, companyname, contractdateINTO #MyCusts3FROM ( VALUES( CAST(2 AS SMALLINT), CAST('Cust 2' AS VARCHAR(50)), CAST('20200212' AS DATE)), ( CAST(3 AS SMALLINT), CAST('Cust 3' AS VARCHAR(50)), CAST('20200118' AS DATE)), ( CAST(5 AS SMALLINT), CAST('Cust 5' AS VARCHAR(50)), CAST('20200401' AS DATE)) ) AS MyCusts(custid, companyname, contractdate); SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlengthFROM tempdb.sys.columnsWHERE OBJECT_ID =OBJECT_ID(N'tempdb..#MyCusts3');

This code generates the following output, showing all target columns have the desired types:

colname typename maxlength------------- --------- ---------custid smallint 2companyname varchar 50contractdate date 3

That’s a lot of coding, though. Another option is to apply the conversions in the SELECT list of the query against the table value constructor, and then define a derived table against the query that applies the conversions, like so:

SELECT custid, companyname, contractdateINTO #MyCusts4FROM ( SELECT CAST(custid AS SMALLINT) AS custid, CAST(companyname AS VARCHAR(50)) AS companyname, CAST(contractdate AS DATE) AS contractdate FROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS D(custid, companyname, contractdate) ) AS MyCusts; SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlengthFROM tempdb.sys.columnsWHERE OBJECT_ID =OBJECT_ID(N'tempdb..#MyCusts4');

Ce code génère la sortie suivante :

colname typename maxlength------------- --------- ---------custid smallint 2companyname varchar 50contractdate date 3

The reasoning for using the additional derived table is due to how logical query processing is designed. The SELECT clause is evaluated after FROM, WHERE, GROUP BY and HAVING. By applying the conversions in the SELECT list of the inner query, you allow expressions in all clauses of the outermost query to interact with the columns with the proper types.

Back to our wishful thinking, clearly, it would be good if we ever get a syntax that allows explicit control of the types in the definition of the table value constructor’s heading, like so:

SELECT custid, companyname, contractdateFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid SMALLINT, companyname VARCHAR(50), contractdate DATE);

Lorsque vous avez terminé, exécutez le code suivant pour le nettoyage :

DROP TABLE IF EXISTS #MyCusts1, #MyCusts2, #MyCusts3, #MyCusts4;

Used in modification statements

T-SQL allows you to modify data through table expressions. That’s true for derived tables, CTEs, views and inline TVFs. What gets modified in practice is some underlying base table that is used by the table expression. I have much to say about modifying data through table expressions, and I will in a future article dedicated to this topic. Here, I just wanted to briefly mention the types of modification statements that specifically support derived tables, and provide the syntax.

Derived tables can be used as the target table in DELETE and UPDATE statements, and also as the source table in the MERGE statement (in the USING clause). They cannot be used in the TRUNCATE statement, and as the target in the INSERT and MERGE statements.

For the DELETE and UPDATE statements, the syntax for defining the derived table is a bit awkward. You don’t define the derived table in the DELETE and UPDATE clauses, like you would expect, but rather in a separate FROM clause. You then specify the derived table name in the DELETE or UPDATE clause.

Here’s the general syntax of a DELETE statement against a derived table:

DELETE [ FROM ]

FROM (
) [ AS ]
[ () ]
[ WHERE ];

As an example (don’t actually run it), the following code deletes all US customers with a customer ID that is greater than the minimum for the same region (the region column represents the state for US customers):

DELETE FROM UCFROM ( SELECT *, ROW_NUMBER() OVER(PARTITION BY region ORDER BY custid) AS rownum FROM Sales.Customers WHERE country =N'USA' ) AS UCWHERE rownum> 1;

Here’s the general syntax of an UPDATE statement against a derived table:

UPDATE

SET
FROM (
) [ AS ]
[ () ]
[ WHERE ];

As you can see, from the perspective of the definition of the derived table, it’s quite similar to the syntax of the DELETE statement.

As an example, the following code changes the company names of US customers to one using the format N'USA Cust ' + rownum, where rownum represents a position based on customer ID ordering:

BEGIN TRAN; UPDATE UC SET companyname =newcompanyname OUTPUT inserted.custid, deleted.companyname AS oldcompanyname, inserted.companyname AS newcompanynameFROM ( SELECT custid, companyname, N'USA Cust ' + CAST(ROW_NUMBER() OVER(ORDER BY custid) AS NVARCHAR(10)) AS newcompanyname FROM Sales.Customers WHERE country =N'USA' ) AS UC; ROLLBACK TRAN;

The code applies the update in a transaction that it then rolls back so that the change won't stick.

This code generates the following output, showing both the old and the new company names:

custid oldcompanyname newcompanyname------- --------------- ----------------32 Customer YSIQX USA Cust 136 Customer LVJSO USA Cust 243 Customer UISOJ USA Cust 345 Customer QXPPT USA Cust 448 Customer DVFMB USA Cust 555 Customer KZQZT USA Cust 665 Customer NYUHS USA Cust 771 Customer LCOUJ USA Cust 875 Customer XOJYP USA Cust 977 Customer LCYBZ USA Cust 1078 Customer NLTYP USA Cust 1182 Customer EYHKM USA Cust 1289 Customer YBQTI USA Cust 13

That’s it for now on the topic.

Résumé

Derived tables are one of the four main types of named table expressions that T-SQL supports. In this article I focused on the logical aspects of derived tables. I described the syntax for defining them and their scope.

Remember that a table expression is a table and as such, all of its columns must have names, all column names must be unique, and the table has no order.

The design of derived tables incurs two main flaws. In order to query one derived table from another, you need to nest your code, causing it to be more complex to maintain and troubleshoot. If you need to interact with multiple occurrences of the same table expression, using derived tables you are forced to duplicate your code, which hurts the maintainability of your solution.

You can use a table value constructor to define a table based on self-contained expressions as opposed to querying some existing base tables.

You can use derived tables in modification statements like DELETE and UPDATE, though the syntax for doing so is a bit awkward.