martes, 16 de febrero de 2016

Entendiendo el Procesamiento de Consulta Lógico

T-SQL tiene lados tanto lógicos como físicos. El lado lógico es la interpretación conceptual de la consulta que explica cuál es el resultado correcto de la consulta. El lado físico es el procesamiento de la consulta por el motor de base de datos. El procesamiento físico debe producir el resultado definido por el procesamiento de consulta lógico. Para lograr este objetivo, el motor de base de datos puede aplicar la optimización. La optimización puede reorganizar los pasos del procesamiento de consulta lógico o eliminar los pasos por completo, pero sólo mientras el resultado sigua siendo el definido por el procesamiento de consulta lógico. El enfoque de este post es el procesamiento de consulta lógico, la interpretación conceptual de la consulta que define el resultado correcto.

T-SQL Como un Lenguaje Declarativo Similar al Inglés

T-SQL, siendo basado en el SQL estándar, es un lenguaje declarativo similar al inglés. En este lenguaje, declarativo significa definir qué quiere, a diferencia de los lenguajes imperativos que definen también como obtener lo que quiere. El SQL estándar describe la interpretación lógica de la petición declarativa (la parte "qué"), pero es responsabilidad del motor de base de datos averiguar cómo procesar físicamente la solicitud (la parte "cómo").
Por esta razón, es importante no sacar conclusiones relacionadas al rendimiento, de lo que aprenda sobre el procesamiento de consulta lógico. Eso es debido a que el procesamiento de consulta lógico, sólo define la exactitud de la consulta. Cuando se abordan aspectos del rendimiento de la consulta, necesita entender cómo funciona la optimización. Como se ha mencionado, la optimización puede ser bastante diferente del procesamiento de consulta lógico, porque tiene permitido cambiar las cosas, siempre y cuando el resultado logrado sea el definido por el procesamiento de consulta lógico.
Es interesante notar que el lenguaje estándar SQL no fue originalmente llamado así; más bien, fue llamado SEQUEL; un acrónimo de "lenguaje de consulta de Ingles estructurado". Pero entonces, debido a una disputa de marca con una compañía aérea, el lenguaje fue renombrado como SQL, por "lenguaje de consulta estructurado". Aun así, el punto es que proporciona sus instrucciones de una manera similar al inglés. Por ejemplo, considere la instrucción: "Bring me a soda from the refrigerator". Observe que en la instrucción en inglés, el objeto viene antes de la ubicación. Considere la siguiente sentencia en T-SQL.
SELECT shipperid, phone, companyname
FROM Sales.Shippers;
Observe la similitud de lo tecleado en orden de la consulta, al inglés. La consulta primero indica la lista SELECT con los atributos que desea retornar y luego la cláusula FROM con la tabla que desea consultar.
Ahora trate de pensar en el orden en el cual la petición es interpretada lógicamente. Por ejemplo, ¿Cómo definiría las instrucciones a un robot en vez de a un humano? La instrucción en inglés original para obtener una soda del refrigerador probablemente necesitaría ser revisada para algo como: "Ir al refrigerador; abrir la puerta; obtener una soda; traérmela".
Del mismo modo, el procesamiento lógico de una consulta debería primero saber qué tabla está siendo consultada antes de que pueda saber qué atributos pueden ser retornados de esa tabla. Por lo tanto, contrariamente a lo tecleado en orden de la consulta anterior, el procesamiento de consulta lógico debería ser de la siguiente manera.
FROM Sales.Shippers
SELECT shipperid, phone, companyname
Este es un ejemplo básico con sólo dos cláusulas de consulta. Por supuesto, las cosas pueden ser más complejas. Si entiende bien el concepto del procesamiento de consulta lógico, será capaz de explicar muchas cosas de la forma en que se comporta el lenguaje, cosas que son muy difíciles de explicar de otro modo.

Fases del Procesamiento de Consulta Lógico

Esta sección abarca el procesamiento de consulta lógico y las fases involucradas.
La sentencia principal utilizada para recuperar datos en T-SQL es la sentencia SELECT. Luego están las cláusulas de consulta principales especificadas en el orden en que se supone son escritas (conocidas como "tecleados en orden"):
1.  SELECT
2.  FROM
3.  WHERE
4.  GROUP BY
5.  HAVING
6.  ORDER BY
Como es mencionado, el orden de procesamiento de consulta lógico, que es el orden de interpretación conceptual, es diferente. Se inicia con la cláusula FROM. Este es el orden de procesamiento de consulta lógico de las seis cláusulas de consulta principales:
1.  FROM
2.  WHERE
3.  GROUP BY
4.  HAVING
5.  SELECT
6.  ORDER BY
Cada fase opera sobre una o más tablas como entradas y retorna una tabla virtual como salida. La tabla de salida de una fase es considerada la entrada a la siguiente fase. Esto está de acuerdo con operaciones sobre las relaciones que producen una relación. Note que si una ORDER BY es especificada, el resultado es no relacional.
Considere la siguiente consulta de ejemplo.
SELECT country, YEAR(hiredate) AS annocontrato, COUNT(*) AS numempleados
FROM HR.Employees
WHERE hiredate >= '20030101'
GROUP BY country, YEAR(hiredate)
HAVING COUNT(*) > 1
ORDER BY country, annocontrato DESC;
Esta consulta es emitida sobre la tabla HR.Employees. Filtra sólo los empleados que fueron contratados en o después del año 2003. Agrupa los empleados por país y por año de contrato. Se mantiene sólo los grupos con más de un empleado. Para cada grupo de calificación, la consulta retorna el año de contrato y el recuento de empleados, ordenados por país y año de contrato, en orden descendente.
Las siguientes secciones proporcionan una breve descripción de lo que sucede en cada fase, de acuerdo con el procesamiento de consulta lógico.

1.    Evaluar la Cláusula FROM

En la primera fase, la cláusula FROM es evaluada. Ahí es donde se indica las tablas que desea consultar y los operadores de tabla como los joins si son aplicables. Si necesita consultar una sola tabla, indicar el nombre de tabla como la tabla de entrada en esta cláusula. Entonces, la salida de esta fase es una tabla de resultado con todas las filas de la tabla de entrada. Ese es el caso, en la siguiente consulta: la entrada es la tabla HR.Employees (nueve filas), y la salida es un resultado de tabla con todas las nueve filas (sólo un subconjunto de los atributos son mostrados).
empid  hiredate    country
------ ----------- --------
1      2002-05-01   USA
2      2002-08-14   USA
3      2002-04-01   USA
4      2003-05-03   USA
5      2003-10-17   UK
6      2003-10-17   UK
7      2004-01-02   UK
8      2004-03-05   USA
9      2004-11-15   UK

2.    Filtrar Filas Basadas en la Cláusula WHERE

La segunda fase filtra las filas basadas en el predicado, en la cláusula WHERE. Sólo las filas para el cual el predicado se evalúa como Verdadero son retornadas.
En esta consulta, la fase de filtrado WHERE filtra sólo las filas para los empleados contratados en o después del 01 de enero del 2003. Seis filas son retornadas de esta fase y son proporcionadas como entrada a la siguiente fase. Aquí está el resultado de esta fase.
empid  hiredate    country
------ ----------- --------
4      2003-05-03   USA
5      2003-10-17   UK
6      2003-10-17   UK
7      2004-01-02   UK
8      2004-03-05   USA
9      2004-11-15   UK
Un error típico hecho por personas que no entienden el procesamiento de consulta lógico es intentar referirse en la cláusula WHERE a un alias de columna definido en la cláusula SELECT. Esto no está permitido porque la cláusula WHERE es evaluada antes de la cláusula SELECT. Como un ejemplo, considere la siguiente consulta.
SELECT country, YEAR(hiredate) AS annocontrato
FROM HR.Employees
WHERE annocontrato >= 2003;
Esta consulta falla con el siguiente error.
Mens. 207, Nivel 16, Estado 1, Línea 3
El nombre de columna 'annocontrato' no es válido.
Si entiende que la cláusula WHERE es evaluada antes que la cláusula SELECT, notará que este intento es erróneo porque en esta fase, el atributo annocontrato no existe todavía. Puede indicar la expresión YEAR(hiredate) >= 2003 en la cláusula WHERE. Mejor aún, por razones de optimización, utilice la forma hiredate >= '20030101' como se hace en la consulta original.

3.    Agrupar Filas Basadas sobre la Cláusula GROUP BY

Esta fase define un grupo para cada combinación distinta de valores en los elementos agrupados de la tabla de entrada. Luego, asocia cada fila de entrada a su respectivo grupo. La consulta que ha estado trabajando agrupa las filas por country y por YEAR(hiredate). En las seis filas en la tabla de entrada, este paso identifica cuatro grupos. Aquí están los grupos y las filas de detalle que están asociados con ellos (información redundante eliminada para propósitos de ilustración).
grupo     grupo         detalle detalle  detalle
country   YEAR(hiredate) empid  country  hiredate
-------- -------------- ------  ------- ----------
UK        2003             5      UK       2003-10-17
6      UK       2003-10-17
UK        2004             7      UK       2004-01-02
9      UK       2004-11-15
USA       2003             4      USA      2003-05-03
USA       2004             8      USA      2004-03-05
Como puede ver, el grupo UK, 2003 tiene dos filas de detalles asociados con los empleados 5 y 6; el grupo para UK, 2004 también tiene dos filas de detalles asociados con los empleados 7 y 9; el grupo para USA, 2003 tiene una fila de detalle asociada con el empleado 4; el grupo para USA, 2004 también tiene una fila de detalle asociada con el empleado 8.
El resultado final de esta consulta tiene una fila representando cada grupo (a menos que sea filtrada). Por lo tanto, las expresiones en todas las fases que tienen lugar después de la fase de agrupamiento actual son algo limitadas. Todas las expresiones procesadas ​​en fases posteriores deben garantizar un único valor por grupo. Si se refiere a un elemento de la lista GROUP BY (por ejemplo, country), ya tiene como garantía, de que una referencia está permitida. Sin embargo, si desea referirse a un elemento que no forma parte de su lista GROUP BY (por ejemplo, empid), debería estar contenida en una función agregada como SUM o MAX. Esto se debe a que múltiples valores son posibles en el elemento dentro de un solo grupo, y la única manera de garantizar que sólo uno será retornado es la agregación de valores.

4.    Filtrar Filas Basada en la Cláusula HAVING

Esta fase es también responsable del filtrado de datos basados ​​en un predicado, pero es evaluado después de que los datos han sido agrupados; por lo tanto, es evaluado por grupo y por grupos de filtros en conjunto. Como es habitual en T-SQL, el predicado de filtrado se puede evaluar a Verdadero, Falso o Desconocido. Sólo los grupos para el cual el predicado se evalúa a Verdadero son retornados de esta fase. En este caso, la cláusula HAVING utiliza el predicado COUNT(*) > 1, lo que significa filtrar solo los grupos país y año de contrato que tienen más de un empleado. Si nos fijamos en el número de filas que fueron asociados con cada grupo en el paso anterior, notará que sólo los grupos UK, 2003 y UK, 2004 califican. Por lo tanto, el resultado de esta fase tiene los siguientes grupos restantes, que se muestran aquí con sus filas de detalle asociados.
grupo    grupo         detalle detalle  detalle
country  YEAR(hiredate) empid  country  hiredate
-------- -------------- ------ ------- ----------
UK        2003            5      UK      2003-10-17
6      UK      2003-10-17
UK        2004            7      UK      2004-01-02
9      UK      2004-11-15

5.    Procesar la cláusula SELECT

La quinta fase es la responsable del procesamiento de la cláusula SELECT. Lo que es interesante acerca de esto, es el punto en el procesamiento de consulta lógico donde es evaluado, casi al final. Eso es interesante considerando el hecho de que la cláusula SELECT aparece primero en la consulta.
Esta fase incluye dos pasos principales. El primer paso es evaluar las expresiones en la  lista SELECT y producir los atributos resultantes. Esto incluye la asignación de atributos con nombres si son derivados de expresiones. Recuerde que si una consulta es una consulta agrupada, cada grupo es representado por una sola fila en el resultado. En la consulta, dos grupos permanecen después del procesamiento del filtro HAVING. Por lo tanto, este paso genera dos filas. En este caso, la lista SELECT retorna para cada grupo country y annocontrato, una fila con los siguientes atributos: country, YEAR(hiredate) con alias annocontrato y COUNT(*) con alias numempleados.
El segundo paso en esta fase es aplicable si se indica la cláusula DISTINCT, en cuyo caso este paso elimina duplicados. Recuerde que T-SQL está basada en la teoría de multiconjuntos más que en la teoría de conjuntos, y por lo tanto, si los duplicados son posibles en el resultado, es su responsabilidad eliminarlos con la cláusula DISTINCT. En el caso de esta consulta, este paso no es aplicable. Aquí está el resultado de esta fase en la consulta.
country annocontrato numempleados
-------- ----------  ------------
UK       2003         2
UK       2004         2
Si necesita un recordatorio de como luce la consulta, aquí está de nuevo.
SELECT country, YEAR(hiredate) AS annocontrato, COUNT(*) AS numempleados
FROM HR.Employees
WHERE hiredate >= '20030101'
GROUP BY country, YEAR(hiredate)
HAVING COUNT(*) > 1
ORDER BY country, annocontrato DESC;
La quinta fase retorna un resultado relacional. Por lo tanto, el orden de las filas no está garantizado. En el caso de esta consulta, hay una cláusula ORDER BY que garantiza el orden en el resultado, pero esto se discutirá cuando se describa la siguiente fase. Lo importante a destacar es que el resultado de la fase que procesa la cláusula SELECT es todavía relacional.
Además, recuerde que esta fase asigna alias de columna, como annocontrato y numempleados. Esto significa que los alias de columnas de nueva creación no son visibles a las cláusulas procesadas en anteriores fases, como el FROM, WHERE, GROUP BY y HAVING.
Note que un alias creado por la fase SELECT no es visible a otras expresiones que aparecen en la misma lista SELECT. Como ejemplo, la siguiente consulta no es válida.
SELECT empid, country, YEAR(hiredate) AS annocontrato, annocontrato-1 AS annoprevio
FROM HR.Employees;
Esta consulta genera el siguiente error.
Mens. 207, Nivel 16, Estado 1, Línea 1
El nombre de columna 'annocontrato' no es válido.
La razón de que esto no está permitido es que, conceptualmente, T-SQL evalúa todas las expresiones que aparecen en la misma fase de procesamiento de consulta lógico en una manera de todo a la vez. Observe el uso de la palabra conceptualmente. SQL Server no necesariamente procesa físicamente todas las expresiones en el mismo punto en el tiempo, pero tiene que producir un resultado como si lo hiciera. Este comportamiento es diferente al de muchos otros lenguajes de programación donde las expresiones por lo general son evaluadas en un orden de izquierda a derecha, haciendo un resultado producido en una expresión visible, a una que aparece a su derecha. Pero T-SQL es diferente.

6.    Manejar el Orden de Presentación

La sexta fase es aplicable si la consulta tiene una cláusula ORDER BY. Esta fase es responsable de retornar el resultado en un orden de presentación específico de acuerdo a las expresiones que aparecen en la lista ORDER BY. La consulta indica que las filas de resultado deberían ser ordenadas primero por country (en orden ascendente por defecto) y, a continuación, por numemployees, descendente, produciendo la siguiente salida.
country  annocontrato numempleados
--------  ----------  ------------
UK        2004         2
UK        2003         2
Observe que la cláusula ORDER BY es la primera y la única cláusula que le es permitida referirse a los alias de columna definidas en la cláusula SELECT. Esto se debe a que la cláusula ORDER BY es la única a ser evaluada después de la cláusula SELECT.
A diferencia de las fases anteriores, donde el resultado era relacional, la salida de esta fase no es relacional porque tiene un orden garantizado. El resultado de esta fase es lo que el SQL estándar llama un cursor. Note que el uso del término cursor aquí es conceptual. T-SQL también soporta un objeto llamado un cursor que es definido basado en un resultado de una consulta, y que le permite traer filas, de una en una, en un orden especificado.
Puede preocuparse de retornar el resultado de una consulta en un orden específico, para propósitos de presentación o si el caller necesita consumir el resultado en esa manera a través de algunos mecanismos de cursor que recupera filas de una en una. Pero recuerde que tal procesamiento no es relacional. Si necesita procesar el resultado de la consulta en una manera relacional, por ejemplo, definir una expresión de tabla como una vista basada en la consulta, el resultado necesitará ser relacional. Además, el ordenamiento de datos puede agregar costo al procesamiento de consultas. Si no le preocupa el orden en que se retornen las filas de resultados, puede evitar este costo innecesario por no añadir una cláusula ORDER BY.
Una consulta puede especificar las opciones de filtrado TOP u OFFSET-FETCH. Si lo hace, la misma cláusula ORDER BY que normalmente es utilizada para definir el orden de presentación también define que filas filtrar para estas opciones. Es importante notar que, así como un filtro es procesado después de que la fase SELECT evalúa todas las expresiones y elimina los duplicados (en caso de que una cláusula DISTINCT sea especificada). Podría incluso considerar los filtros TOP y OFFSET-FETCH como siendo procesados en su propia fase número 7. La consulta no indica un filtro de este tipo, y por lo tanto, esta fase es inaplicable en este caso.

Ejercicio 1: Corregir un Problema con Agrupaciones

En este ejercicio, se le presentará una consulta agrupada que falla cuando intenta ejecutarla. Se le proporciona instrucciones sobre cómo solucionar la consulta.
1.    Abra el SSMS y conéctese a la base de datos de ejemplo TSQL2012.
2.    Escriba la consulta siguiente en la ventana de consulta y ejecútela.
SELECT custid, orderid
FROM Sales.Orders
GROUP BY custid;
La consulta supuestamente retorna para cada cliente el ID Cliente y el máximo ID Pedido para ese cliente, pero en lugar de esto falla. Trate de determinar por qué la consulta falló y lo que necesita ser revisado para que pueda retornar el resultado deseado.
3.    La consulta falló porque orderid no aparece en la lista GROUP BY ni dentro de una función agregada. Hay varios posibles valores orderid por cliente. Para corregir la consulta, necesita aplicar una función agregada al atributo orderid. La tarea es retornar el valor orderid máximo por cliente. Por lo tanto, la función agregada debería ser MAX. Su consulta debería tener el siguiente aspecto.
SELECT custid, MAX(orderid) AS maxorderid
FROM Sales.Orders
GROUP BY custid;

Ejercicio 2: Corregir un Problema con Alias

En este ejercicio, se le presentará otra consulta agrupada que falla, esta vez debido a un problema de alias. Al igual que en el primer ejercicio, se le proporciona instrucciones sobre cómo corregir la consulta.
1.    Borre la ventana de consulta, escriba la siguiente consulta, y ejecútela.
SELECT shipperid, SUM(freight) AS totalfreight
FROM Sales.Orders
WHERE freight > 20000.00
GROUP BY shipperid;
La consulta supuestamente retorna sólo distribuidores para los que el valor total del flete es superior a 20,000, pero en su lugar, retorna un conjunto vacío. Trate de identificar el problema en la consulta.
2.    Recuerde que la cláusula de filtrado WHERE es evaluada por fila, no por grupo. La consulta filtra los pedidos individuales con un valor de flete superior a 20,000, y no hay ninguno. Para corregir la consulta, es necesario aplicar el filtro por cada grupo de distribuidores, no por cada pedido. Necesita filtrar el total de todos los valores de flete por distribuidor. Esto puede ser obtenido utilizando el filtro HAVING. Intente corregir el problema utilizando la siguiente consulta.
SELECT shipperid, SUM(freight) AS totalfreight
FROM Sales.Orders
GROUP BY shipperid
HAVING totalfreight > 20000.00;
Pero esta consulta también falla. Trate de identificar por qué falla y lo que necesita ser revisado para lograr el resultado deseado.
3.    El problema ahora es que la consulta intenta referirse en la cláusula HAVING al alias totalfreight, el cual es definido en la cláusula SELECT. La cláusula HAVING es evaluada antes de la cláusula SELECT, y por lo tanto, el alias de la columna no le es visible. Para solucionar el problema, necesita referirse a la expresión SUM (freight) en la cláusula HAVING, de la siguiente manera.
SELECT shipperid, SUM(freight) AS totalfreight
FROM Sales.Orders
GROUP BY shipperid

HAVING SUM(freight) > 20000.00;

1 comentario:

  1. Este post nos muestra los fundamentos del lenguaje T-SQL, parte 2. Espero les sea de utilidad.

    ResponderBorrar