sql >> Base de Datos >  >> RDS >> Database

Fundamentos de las expresiones de tabla, Parte 2 - Tablas derivadas, consideraciones lógicas

El mes pasado proporcioné información básica sobre las expresiones de tablas en T-SQL. Expliqué el contexto de la teoría relacional y el estándar SQL. Expliqué cómo una tabla en SQL es un intento de representar una relación de la teoría relacional. También expliqué que una expresión relacional es una expresión que opera en una o más relaciones como entradas y da como resultado una relación. De manera similar, en SQL, una expresión de tabla es una expresión que opera en una o más tablas de entrada y da como resultado una tabla. La expresión puede ser una consulta, pero no tiene por qué serlo. Por ejemplo, la expresión puede ser un constructor de valores de tabla, como explicaré más adelante en este artículo. También expliqué que en esta serie, me enfoco en cuatro tipos específicos de expresiones de tablas con nombre que admite T-SQL:tablas derivadas, expresiones de tablas comunes (CTE), vistas y funciones con valores de tablas en línea (TVF).

Si ha estado trabajando con T-SQL durante algún tiempo, probablemente se topó con bastantes casos en los que tuvo que usar expresiones de tabla o de alguna manera fue más conveniente en comparación con soluciones alternativas que no las usan. Estos son solo algunos ejemplos de casos de uso que se me ocurren:

  • Cree una solución modular dividiendo tareas complejas en pasos, cada uno representado por una expresión de tabla diferente.
  • Mezclar resultados de consultas agrupadas y detalles, en caso de que decida no utilizar funciones de ventana para este propósito.
  • Procesamiento lógico de consultas maneja las cláusulas de consulta en el siguiente orden:FROM>WHERE>GROUP BY>HAVING>SELECT>ORDER BY. Como resultado, en el mismo nivel de anidamiento, los alias de columna que define en la cláusula SELECT solo están disponibles para la cláusula ORDER BY. No están disponibles para el resto de las cláusulas de consulta. Con las expresiones de tabla, puede reutilizar los alias que define en una consulta interna en cualquier cláusula de la consulta externa y, de esta manera, evitar la repetición de expresiones largas/complejas.
  • Las funciones de ventana solo pueden aparecer en las cláusulas SELECT y ORDER BY de una consulta. Con las expresiones de tabla, puede asignar un alias a una expresión basada en una función de ventana y luego usar ese alias en una consulta contra la expresión de tabla.
  • Un operador PIVOT involucra tres elementos:agrupación, dispersión y agregación. Este operador identifica el elemento de agrupación implícitamente por eliminación. Usando una expresión de tabla, puede proyectar exactamente los tres elementos que se supone que están involucrados y hacer que la consulta externa use la expresión de tabla como la tabla de entrada del operador PIVOT, controlando así qué elemento es el elemento de agrupación.
  • Las modificaciones con TOP no admiten una cláusula ORDER BY. Puede controlar qué filas se eligen indirectamente definiendo una expresión de tabla basada en una consulta SELECT con el filtro TOP o OFFSET-FETCH y una cláusula ORDER BY, y aplicar la modificación contra la expresión de tabla.

Esto está lejos de ser una lista exhaustiva. Demostraré algunos de los casos de uso anteriores y otros en esta serie. Solo quería mencionar algunos casos de uso aquí para ilustrar cuán importantes son las expresiones de tabla en nuestro código T-SQL y por qué vale la pena invertir en comprender bien sus fundamentos.

En el artículo de este mes me centro específicamente en el tratamiento lógico de las tablas derivadas.

En mis ejemplos, usaré una base de datos de muestra llamada TSQLV5. Puede encontrar el script que lo crea y lo completa aquí, y su diagrama ER aquí.

Tablas derivadas

El término tabla derivada se usa en SQL y T-SQL con más de un significado. Así que primero quiero dejar claro a cuál me refiero en este artículo. Me refiero a una construcción de lenguaje específica que normalmente define, pero no solo, en la cláusula FROM de una consulta externa. Proporcionaré la sintaxis para esta construcción en breve.

El uso más general del término tabla derivada en SQL es la contraparte de una relación derivada de la teoría relacional. Una relación derivada es una relación de resultado que se deriva de una o más relaciones base de entrada, aplicando operadores relacionales del álgebra relacional como proyección, intersección y otros a esas relaciones base. De manera similar, en el sentido general, una tabla derivada en SQL es una tabla de resultados que se deriva de una o más tablas base, evaluando expresiones contra esas tablas base de entrada.

Aparte, verifiqué cómo el estándar SQL define una tabla base e inmediatamente lamenté haberme molestado.

4.15.2 Tablas base

Una tabla base es una tabla base persistente o una tabla temporal.

Una tabla base persistente es una tabla base persistente regular o una tabla versionada por el sistema.

Una tabla base regular es una tabla base persistente regular o una tabla temporal.”

Agregado aquí sin más comentarios...

En T-SQL, puede crear una tabla base con una declaración CREATE TABLE, pero hay otras opciones, por ejemplo, SELECT INTO y DECLARE @T AS TABLE.

Esta es la definición estándar para tablas derivadas en sentido general:

4.15.3 Tablas derivadas

Una tabla derivada es una tabla derivada directa o indirectamente de una o más tablas mediante la evaluación de una expresión, como una , , o . Una puede contener una opcional. El orden de las filas de la tabla especificada por la está garantizado solo para la que contiene inmediatamente la .”

Hay un par de cosas interesantes a tener en cuenta aquí sobre las tablas derivadas en el sentido general. Uno tiene que ver con el comentario sobre ordenar. Llegaré a esto más adelante en el artículo. Otra es que una tabla derivada en SQL puede ser una expresión de tabla independiente válida, pero no tiene por qué serlo. Por ejemplo, la siguiente expresión representa una tabla derivada y es también se considera una expresión de tabla independiente válida (puede ejecutarla):

SELECT custid, companyname
FROM Sales.Customers
WHERE country = N'USA'

Por el contrario, la siguiente expresión representa una tabla derivada, pero no lo es una expresión de tabla independiente válida:

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

T-SQL admite varios operadores de tabla que producen una tabla derivada, pero no se admiten como expresiones independientes. Esos son:JOIN, PIVOT, UNPIVOT y APPLY. Necesita una cláusula para que operen dentro (generalmente FROM, pero también la cláusula USING de la declaración MERGE) y una consulta de host.

De ahora en adelante, usaré el término tabla derivada para describir una construcción de lenguaje más específica y no en el sentido general descrito anteriormente.

Sintaxis

Una tabla derivada se puede definir como parte de una instrucción SELECT externa en su cláusula FROM. También se puede definir como parte de las declaraciones DELETE y UPDATE en su cláusula FROM, y como parte de una declaración MERGE en su cláusula USING. Proporcionaré más detalles sobre la sintaxis cuando se use en instrucciones de modificación más adelante en este artículo.

Aquí está la sintaxis para una consulta SELECT simplificada contra una tabla derivada:

SELECCIONE
FROM ( ) [ AS ] [ () ];

La definición de la tabla derivada aparece donde normalmente puede aparecer una tabla base, en la cláusula FROM de la consulta externa. Puede ser una entrada para un operador de tabla como JOIN, APPLY, PIVOT y UNPIVOT. Cuando se usa como la entrada correcta para un operador APPLY, la parte

de la tabla derivada puede tener correlaciones con las columnas de una tabla externa (más sobre esto en un artículo futuro dedicado de la serie). De lo contrario, la expresión de la tabla debe ser independiente.

La declaración externa puede tener todos los elementos de consulta habituales. En un caso de declaración SELECT:WHERE, GROUP BY, HAVING, ORDER BY y, como se mencionó, operadores de tabla en la cláusula FROM.

Aquí hay un ejemplo de una consulta simple en una tabla derivada que representa a los clientes de EE. UU.:

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

Esta consulta genera el siguiente resultado:

custid  companyname
------- ---------------
32      Customer YSIQX
36      Customer LVJSO
43      Customer UISOJ
45      Customer QXPPT
48      Customer DVFMB
55      Customer KZQZT
65      Customer NYUHS
71      Customer LCOUJ
75      Customer XOJYP
77      Customer LCYBZ
78      Customer NLTYP
82      Customer EYHKM
89      Customer YBQTI

Hay tres partes principales para identificar en una declaración que involucra una definición de tabla derivada:

  1. La expresión de la tabla (la consulta interna)
  2. El nombre de la tabla derivada, o más exactamente, lo que en la teoría relacional se considera una variable de rango
  3. La declaración exterior

Se supone que la expresión de tabla representa una tabla y, como tal, debe satisfacer ciertos requisitos que una consulta normal no necesariamente debe cumplir. Proporcionaré los detalles en breve en la sección "Una expresión de tabla es una tabla".

En cuanto al nombre de la tabla derivada de destino; una suposición común entre los desarrolladores de T-SQL es que es simplemente un nombre o alias que asigna a la tabla de destino. Del mismo modo, considere la siguiente consulta:

SELECT custid, companyname
FROM Sales.Customers AS C
WHERE country = N'USA';

También aquí, la suposición común es que AS C es solo una forma de cambiar el nombre o alias de la tabla Clientes para los fines de esta consulta, comenzando con el paso de procesamiento de consulta lógica donde se asigna el nombre y en adelante. Sin embargo, desde el punto de vista de la teoría relacional, lo que representa C tiene un significado más profundo. C es lo que se conoce como una variable de rango. C es una variable de relación derivada que se extiende sobre las tuplas en la variable de relación de entrada Clientes. En el ejemplo anterior, C se extiende sobre las tuplas en Clientes y evalúa el predicado país =N'USA'. Las tuplas para las que el predicado se evalúa como verdadero pasan a formar parte de la relación de resultado C.

Una expresión de tabla es una tabla

Con los antecedentes que proporcioné hasta ahora, lo que voy a explicar a continuación no debería sorprenderte. La parte de una definición de tabla derivada es una tabla . Ese es el caso incluso si se expresa como una consulta. ¿Recuerdas la propiedad de cierre del álgebra relacional? Lo mismo se aplica al resto de las expresiones de tabla mencionadas anteriormente (CTE, vistas y TVF en línea). Como ya aprendiste, la tabla de SQL es la contraparte de la relación de la teoría relacional , aunque no una contraparte perfecta. Por lo tanto, una expresión de tabla debe satisfacer ciertos requisitos para garantizar que el resultado sea una tabla, requisitos que una consulta que no se usa como expresión de tabla no necesariamente tiene que cumplir. Estos son tres requisitos específicos:

  • Todas las columnas de la expresión de la tabla deben tener nombres
  • Todos los nombres de columna de la expresión de la tabla deben ser únicos
  • Las filas de la expresión de la tabla no tienen orden

Analicemos estos requisitos uno por uno, discutiendo la relevancia tanto para la teoría relacional como para SQL.

Todas las columnas deben tener nombres

Recuerda que una relación tiene un encabezado y un cuerpo. El encabezado de una relación es un conjunto de atributos (columnas en SQL). Un atributo tiene un nombre y un nombre de tipo, y se identifica por su nombre. Una consulta que no se usa como una expresión de tabla no necesariamente tiene que asignar nombres a todas las columnas de destino. Considere la siguiente consulta como ejemplo:

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

Esta consulta genera el siguiente resultado:

empid  firstname  lastname   (No column name)
------ ---------- ---------- -----------------
1      Sara       Davis      USA/WA/Seattle
2      Don        Funk       USA/WA/Tacoma
3      Judy       Lew        USA/WA/Kirkland
4      Yael       Peled      USA/WA/Redmond
5      Sven       Mortensen  UK/London
6      Paul       Suurs      UK/London
7      Russell    King       UK/London
8      Maria      Cameron    USA/WA/Seattle
9      Patricia   Doyle      UK/London

El resultado de la consulta tiene una columna anónima resultante de la concatenación de los atributos de ubicación mediante la función CONCAT_WS. (Por cierto, esta función se agregó en SQL Server 2017, por lo que si está ejecutando el código en una versión anterior, siéntase libre de reemplazar este cálculo con un cálculo alternativo de su elección). Por lo tanto, esta consulta no devolver una tabla, por no hablar de una relación. Por lo tanto, no es válido utilizar una consulta como expresión de tabla/parte de consulta interna de una definición de tabla derivada.

Pruébalo:

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

Obtiene el siguiente error:

Mensaje 8155, Nivel 16, Estado 2, Línea 50
No se especificó ningún nombre de columna para la columna 4 de 'D'.

Aparte, ¿notas algo interesante sobre el mensaje de error? Se queja de la columna 4, destacando la diferencia entre las columnas en SQL y los atributos en la teoría relacional.

La solución es, por supuesto, asegurarse de que asigna explícitamente nombres a las columnas que resultan de los cálculos. T-SQL admite bastantes técnicas de nomenclatura de columnas. Mencionaré dos de ellos.

Puede usar una técnica de nomenclatura en línea en la que asigne el nombre de la columna de destino después del cálculo y una cláusula AS opcional, como en < expression > [ AS ] < column name > , así:

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

Esta consulta genera el siguiente resultado:

empid  firstname  lastname   custlocation
------ ---------- ---------- ----------------
1      Sara       Davis      USA/WA/Seattle
2      Don        Funk       USA/WA/Tacoma
3      Judy       Lew        USA/WA/Kirkland
4      Yael       Peled      USA/WA/Redmond
5      Sven       Mortensen  UK/London
6      Paul       Suurs      UK/London
7      Russell    King       UK/London
8      Maria      Cameron    USA/WA/Seattle
9      Patricia   Doyle      UK/London

Usando esta técnica, es muy fácil al revisar el código saber qué nombre de columna de destino se asigna a qué expresión. Además, solo necesita nombrar las columnas que aún no tienen nombres.

También puede usar una técnica de nomenclatura de columnas más externa en la que especifica los nombres de las columnas de destino entre paréntesis justo después del nombre de la tabla derivada, así:

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

Sin embargo, con esta técnica, debe enumerar los nombres de todas las columnas, incluidas las que ya tienen nombres. La asignación de los nombres de las columnas de destino se realiza por posición, de izquierda a derecha, es decir, el primer nombre de la columna de destino representa la primera expresión en la lista SELECCIONAR de la consulta interna; el nombre de la segunda columna de destino representa la segunda expresión; y así.

Tenga en cuenta que en caso de incoherencia entre los nombres de las columnas interna y externa, por ejemplo, debido a un error en el código, el alcance de los nombres internos es la consulta interna o, más precisamente, la variable de rango interno (aquí implícitamente HR.Employees AS Employees), y el alcance de los nombres externos es la variable de rango externo (D en nuestro caso). Hay un poco más de implicación en el alcance de los nombres de las columnas que tiene que ver con el procesamiento de consultas lógicas, pero ese es un tema para discusiones posteriores.

El potencial de errores con la sintaxis de nomenclatura externa se explica mejor con un ejemplo.

Examine el resultado de la consulta anterior, con el conjunto completo de empleados de la tabla HR.Employees. Luego, considere la siguiente consulta y, antes de ejecutarla, intente averiguar qué empleados espera ver en el resultado:

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

Si espera que la consulta devuelva un conjunto vacío para los datos de muestra dados, dado que actualmente no hay empleados con un apellido y un nombre que comiencen con la letra D, se está perdiendo el error en el código.

Ahora ejecute la consulta y examine el resultado real:

empid  firstname  lastname  custlocation
------ ---------- --------- ---------------
1      Davis      Sara      USA/WA/Seattle
9      Doyle      Patricia  UK/London

¿Qué pasó?

La consulta interna especifica firstname como la segunda columna y lastname como la tercera columna en la lista SELECT. El código que asigna los nombres de las columnas de destino de la tabla derivada en la consulta externa especifica el segundo apellido y el tercero el nombre. El código nombra firstname como lastname y lastname como firstname en la variable de rango D. Efectivamente, solo está filtrando empleados cuyo apellido comienza con la letra D. No está filtrando empleados con un apellido y un nombre que comienzan con la letra D.

La sintaxis de alias en línea no es propensa a tales errores. Por un lado, normalmente no crea un alias para una columna que ya tiene un nombre con el que está satisfecho. En segundo lugar, incluso si desea asignar un alias diferente para una columna que ya tiene un nombre, no es muy probable que con la sintaxis AS asigne el alias incorrecto. Piénsalo; ¿Qué tan probable es que escribas así:

SELECT empid, firstname, lastname, custlocation
FROM ( 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 D
WHERE firstname LIKE N'D%';

Obviamente, no es muy probable.

Todos los nombres de columna deben ser únicos

Volviendo al hecho de que el encabezado de una relación es un conjunto de atributos, y dado que un atributo se identifica por su nombre, los nombres de los atributos deben ser únicos para la misma relación. En una consulta determinada, siempre puede hacer referencia a un atributo utilizando un nombre de dos partes con el nombre de la variable de rango como calificador, como en .. Cuando el nombre de la columna sin el calificador no es ambiguo, puede omitir el prefijo del nombre de la variable de rango. Sin embargo, lo que es importante recordar es lo que dije anteriormente sobre el alcance de los nombres de las columnas. En el código que involucra una expresión de tabla con nombre, con una consulta interna (la expresión de tabla) y una consulta externa, el alcance de los nombres de columna en la consulta interna son las variables de rango internas y el alcance de los nombres de columna en la consulta externa. consulta son las variables de rango externo. Si la consulta interna implica varias tablas de origen con el mismo nombre de columna, aún puede hacer referencia a esas columnas sin ambigüedades agregando el nombre de la variable de rango como prefijo. Si no asigna un nombre de variable de rango explícitamente, obtiene uno asignado implícitamente, como si usara AS .

Considere la siguiente consulta independiente como ejemplo:

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

Esta consulta no falla con un error de nombre de columna duplicado, ya que una columna de custid en realidad se llama C.custid y la otra O.custid dentro del alcance de la consulta actual. Esta consulta genera el siguiente resultado:

custid      custid      orderid
----------- ----------- -----------
1           1           10643
1           1           10692
1           1           10702
1           1           10835
1           1           10952
1           1           11011
2           2           10308
2           2           10625
2           2           10759
2           2           10926
...

Sin embargo, intente usar esta consulta como una expresión de tabla en la definición de una tabla derivada llamada CO, así:

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 lo que respecta a la consulta externa, tiene una variable de rango llamada CO, y el alcance de todos los nombres de columna en la consulta externa es esa variable de rango. Los nombres de todas las columnas en una variable de rango determinada (recuerde, una variable de rango es una variable de relación) deben ser únicos. Por lo tanto, obtiene el siguiente error:

Mensaje 8156, Nivel 16, Estado 1, Línea 80
La columna 'custid' se especificó varias veces para 'CO'.

La solución es, por supuesto, asignar diferentes nombres de columna a las dos columnas custid en lo que respecta a la variable de rango CO, así:

SELECT *
FROM ( SELECT C.custid AS custcustid, 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;

Esta consulta genera el siguiente resultado:

custcustid  ordercustid orderid
----------- ----------- -----------
1           1           10643
1           1           10692
1           1           10702
1           1           10835
1           1           10952
1           1           11011
2           2           10308
2           2           10625
2           2           10759
2           2           10926
...

Si sigue las buenas prácticas, enumera explícitamente los nombres de las columnas en la lista SELECT de la consulta más externa. Dado que solo hay una variable de rango involucrada, no tiene que usar el nombre de dos partes para las referencias de las columnas externas. Si desea utilizar el nombre de dos partes, prefije los nombres de columna con el nombre de variable de rango exterior CO, así:

SELECT CO.custcustid, CO.ordercustid, CO.orderid
FROM ( SELECT C.custid AS custcustid, 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;

Sin orden

Tengo mucho que decir sobre las expresiones de tabla con nombre y el orden, suficiente para un artículo por derecho propio, por lo que dedicaré un artículo futuro a este tema. Aún así, quería tocar el tema brevemente aquí ya que es muy importante. Recuerde que el cuerpo de una relación es un conjunto de tuplas y, de manera similar, el cuerpo de una tabla es un conjunto de filas. Un conjunto no tiene orden. Aún así, SQL permite que la consulta más externa tenga una cláusula ORDER BY que sirva un significado de orden de presentación, como lo demuestra la siguiente consulta:

SELECT orderid, val
FROM Sales.OrderValues
ORDER BY val DESC;

Sin embargo, lo que debe comprender es que esta consulta no devuelve una relación como resultado. Incluso desde la perspectiva de SQL, la consulta no devuelve una tabla como resultado y, por lo tanto, no lo es. considerada una expresión de tabla. En consecuencia, no es válido utilizar una consulta de este tipo como parte de la expresión de tabla de una definición de tabla derivada.

Intenta ejecutar el siguiente código:

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

Obtiene el siguiente error:

Mensaje 1033, Nivel 15, Estado 1, Línea 124
La cláusula ORDER BY no es válida en vistas, funciones en línea, tablas derivadas, subconsultas y expresiones de tablas comunes, a menos que también se especifique TOP, OFFSET o FOR XML.

Me ocuparé de a menos que parte del mensaje de error en breve.

Si desea que la consulta más externa devuelva un resultado ordenado, necesita especificar la cláusula ORDER BY en la consulta más externa, así:

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

En cuanto al a menos que parte del mensaje de error; T-SQL admite el filtro TOP patentado, así como el filtro OFFSET-FETCH estándar. Ambos filtros se basan en una cláusula ORDER BY en el mismo ámbito de consulta para definir qué filas superiores filtrar. Desafortunadamente, este es el resultado de una trampa en el diseño de estas funciones, que no separa el orden de presentación del orden de filtro. Sea como fuere, tanto Microsoft con su filtro TOP, como el estándar con su filtro OFFSET-FETCH, permiten especificar una cláusula ORDER BY en la consulta interna siempre que también especifique el filtro TOP o OFFSET-FETCH, respectivamente. Entonces, esta consulta es válida, por ejemplo:

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

Cuando ejecuté esta consulta en mi sistema, generó el siguiente resultado:

orderid  val
-------- ---------
10865    16387.50
10981    15810.00
11030    12615.05

Sin embargo, lo que es importante destacar es que la única razón por la que se permite la cláusula ORDER BY en la consulta interna es para admitir el filtro TOP. Esa es la única garantía que obtienes en lo que respecta al pedido. Dado que la consulta externa tampoco tiene una cláusula ORDER BY, no obtiene una garantía para ningún orden de presentación específico de esta consulta, independientemente del comportamiento observado. Ese es el caso tanto en T-SQL como en el estándar. Aquí hay una cita del estándar que aborda esta parte:

"El orden de las filas de la tabla especificada por la está garantizado solo para la que contiene inmediatamente la ".

Como se mencionó, hay mucho más que decir sobre las expresiones de tabla y el orden, lo cual haré en un artículo futuro. También proporcionaré ejemplos que demuestren cómo la falta de la cláusula ORDER BY en la consulta externa significa que no obtiene ninguna garantía de orden de presentación.

Entonces, una expresión de tabla, por ejemplo, una consulta interna en una definición de tabla derivada, es una tabla. De manera similar, una tabla derivada (en el sentido específico) también es una tabla. No es una mesa base, pero sin embargo es una mesa. Lo mismo se aplica a CTE, vistas y TVF en línea. No son tablas base, sino derivadas (en el sentido más general), pero no obstante son tablas.

Defectos de diseño

Las tablas derivadas tienen dos defectos principales en su diseño. Ambos tienen que ver con el hecho de que la tabla derivada se define en la cláusula FROM de la consulta externa.

Una falla de diseño tiene que ver con el hecho de que si necesita consultar una tabla derivada de una consulta externa y, a su vez, usar esa consulta como una expresión de tabla en otra definición de tabla derivada, termina anidando esas consultas de tabla derivadas. En informática, el anidamiento explícito de código que involucra múltiples niveles de anidamiento tiende a resultar en un código complejo que es difícil de mantener.

Aquí hay un ejemplo muy básico que demuestra esto:

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

Este código devuelve los años de pedido y la cantidad de clientes que realizaron pedidos durante cada año, solo para los años en los que la cantidad de clientes que realizaron pedidos fue superior a 70.

La principal motivación para usar expresiones de tabla aquí es poder hacer referencia a un alias de columna varias veces. La consulta más interna utilizada como una expresión de tabla para la tabla derivada D1 consulta la tabla Sales.Orders y asigna el nombre de columna orderyear a la expresión YEAR(orderdate), y también devuelve la columna custid. La consulta contra D1 agrupa las filas de D1 por año de pedido y devuelve el año de pedido, así como la cantidad distinta de clientes que realizaron pedidos durante el año en cuestión con el alias numcusts. El código define una tabla derivada llamada D2 basada en esta consulta. La consulta más externa que las consultas D2 y filtra solo los años en los que el número de clientes que realizaron pedidos fue superior a 70.

Un intento de revisar este código o solucionarlo en caso de problemas es complicado debido a los múltiples niveles de anidamiento. En lugar de revisar el código de la manera más natural de arriba a abajo, tendrá que analizarlo comenzando con la unidad más interna y yendo gradualmente hacia afuera, ya que eso es más práctico.

El objetivo de usar tablas derivadas en este ejemplo era simplificar el código evitando la necesidad de repetir expresiones. Pero no estoy seguro de que esta solución logre este objetivo. En este caso, probablemente sea mejor que repita algunas expresiones, evitando la necesidad de usar tablas derivadas por completo, así:

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

Tenga en cuenta que estoy mostrando un ejemplo muy simple aquí con fines ilustrativos. Imagine un código de producción con más niveles de anidamiento y con un código más largo y elaborado, y podrá ver cómo se vuelve sustancialmente más complicado de mantener.

Otro defecto en el diseño de las tablas derivadas tiene que ver con los casos en los que necesita interactuar con varias instancias de la misma tabla derivada. Considere la siguiente consulta como ejemplo:

SELECT CUR.orderyear, CUR.numorders,
  CUR.numorders - PRV.numorders AS diff
FROM ( 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;

Este código calcula el número de pedidos procesados ​​en cada año, así como la diferencia con el año anterior. Ignore el hecho de que hay formas más sencillas de lograr la misma tarea con funciones de ventana:estoy usando este código para ilustrar un punto determinado, por lo que la tarea en sí y las diferentes formas de resolverla no son significativas.

Una combinación es un operador de tabla que trata sus dos entradas como un conjunto, lo que significa que no hay orden entre ellas. Se conocen como las entradas izquierda y derecha para que pueda marcar una de ellas (o ambas) como una tabla preservada en una combinación externa, pero aún así, no hay una primera y una segunda entre ellas. Puede usar tablas derivadas como entradas de unión, pero el nombre de la variable de rango que asigna a la entrada izquierda no está accesible en la definición de la entrada derecha. Eso es porque ambos se definen conceptualmente en el mismo paso lógico, como si estuvieran en el mismo momento. En consecuencia, al unir tablas derivadas, no puede definir dos variables de rango basadas en una expresión de tabla. Desafortunadamente, debe repetir el código, definiendo dos variables de rango basadas en dos copias idénticas del código. Esto, por supuesto, complica la capacidad de mantenimiento del código y aumenta la probabilidad de errores. Cada cambio que realice en una expresión de tabla también debe aplicarse a la otra.

Como explicaré en un próximo artículo, los CTE, en su diseño, no incurren en estos dos defectos que incurren las tablas derivadas.

Constructor de valores de tabla

A table value constructor allows you to construct a table value based on self-contained scalar expressions. You can then use such a table in an outer query just like you use a derived table that is based on an inner query. In a future article I discuss lateral derived tables and correlations in detail, and I’ll show more sophisticated forms of table value constructors. In this article, though, I’ll focus on a simple form that is based purely on self-contained scalar expressions.

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, contractdate
FROM ( 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, contractdate
FROM ( 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, contractdate
FROM ( 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, contractdate
FROM ( 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, contractdate
INTO #MyCusts
FROM ( 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 maxlength
FROM tempdb.sys.columns
WHERE OBJECT_ID = OBJECT_ID(N'tempdb..#MyCusts');

Here’s the output of this code:

colname       typename   maxlength
------------- ---------- ---------
custid        int        4
companyname   varchar    6
contractdate  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_maxlength
FROM ( 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, contractdate
INTO #MyCusts1
FROM ( 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 maxlength
FROM tempdb.sys.columns
WHERE OBJECT_ID = OBJECT_ID(N'tempdb..#MyCusts1');

This code generates the following output:

colname       typename  maxlength
------------- --------- ---------
custid        int       4
companyname   varchar   50
contractdate  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, contractdate
INTO #MyCusts2
FROM ( 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 maxlength
FROM tempdb.sys.columns
WHERE OBJECT_ID = OBJECT_ID(N'tempdb..#MyCusts2');

This code generates the following output:

colname       typename  maxlength
------------- --------- ---------
custid        int       4
companyname   varchar   50
contractdate  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, contractdate
INTO #MyCusts3
FROM ( 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 maxlength
FROM tempdb.sys.columns
WHERE 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  2
companyname   varchar   50
contractdate  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, contractdate
INTO #MyCusts4
FROM ( 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 maxlength
FROM tempdb.sys.columns
WHERE OBJECT_ID = OBJECT_ID(N'tempdb..#MyCusts4');

This code generates the following output:

colname       typename  maxlength
------------- --------- ---------
custid        smallint  2
companyname   varchar   50
contractdate  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, contractdate
FROM ( VALUES( 2, 'Cust 2', '20200212' ),
             ( 3, 'Cust 3', '20200118' ),
             ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid SMALLINT, companyname VARCHAR(50), contractdate DATE);

When you’re done, run the following code for cleanup:

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 UC
FROM ( SELECT *, ROW_NUMBER() OVER(PARTITION BY region ORDER BY custid) AS rownum
       FROM Sales.Customers
       WHERE country = N'USA' ) AS UC
WHERE 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 newcompanyname
FROM ( 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 1
36      Customer LVJSO  USA Cust 2
43      Customer UISOJ  USA Cust 3
45      Customer QXPPT  USA Cust 4
48      Customer DVFMB  USA Cust 5
55      Customer KZQZT  USA Cust 6
65      Customer NYUHS  USA Cust 7
71      Customer LCOUJ  USA Cust 8
75      Customer XOJYP  USA Cust 9
77      Customer LCYBZ  USA Cust 10
78      Customer NLTYP  USA Cust 11
82      Customer EYHKM  USA Cust 12
89      Customer YBQTI  USA Cust 13

That’s it for now on the topic.

Summary

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.