A menudo vemos consultas SQL complejas mal escritas que se ejecutan en una tabla o tablas en bases de datos. Esas consultas hacen que el tiempo de ejecución sea muy largo y consumen una gran cantidad de CPU y otros recursos. Aún así, las consultas complejas brindan información valiosa a la aplicación/persona que las ejecuta en muchos casos. Por lo tanto, son activos útiles en todas las variedades de aplicaciones.
Las consultas complejas son difíciles de depurar
Si observamos de cerca las consultas problemáticas, muchas de ellas son complejas, especialmente aquellas específicas que se usan en los informes.
Las consultas complejas a menudo constan de cinco o más tablas grandes y están unidas por muchas subconsultas. Cada subconsulta tiene una cláusula WHERE que lleva a cabo cálculos simples o complejos y/o transformaciones de datos mientras une las columnas de las tablas relevantes.
Estas consultas pueden volverse difíciles de depurar sin consumir muchos recursos. La razón es que es difícil determinar si cada subconsulta y/o subconsultas combinadas producen resultados correctos.
Un escenario típico es:lo llaman a altas horas de la noche para resolver un problema en un servidor de base de datos ocupado con una consulta compleja involucrada, y necesita solucionarlo rápidamente. Como desarrollador o DBA, es posible que tenga un tiempo y recursos del sistema muy limitados disponibles a última hora. Por lo tanto, lo primero que necesita es un plan sobre cómo depurar la consulta problemática.
A veces, el procedimiento de depuración va bien. A veces, se necesita mucho tiempo y esfuerzo antes de alcanzar la meta y resolver el problema.
Escribir Consultas en estructura CTE
Pero, ¿y si hubiera una manera de escribir consultas complejas para poder depurarlas rápidamente, pieza por pieza?
Hay tal manera. Se llama expresión de tabla común o CTE.
Common Table Expression es una característica estándar en la mayoría de las bases de datos modernas como SQLServer, MySQL (a partir de la versión 8.0), MariaDB (versión 10.2.1), Db2 y Oracle. Tiene una estructura simple que encapsula una o varias subconsultas en un conjunto de resultados con nombre temporal. Puede utilizar este conjunto de resultados en otros CTE o subconsultas con nombre.
Una expresión de tabla común es, hasta cierto punto, una VISTA que solo existe y a la que hace referencia la consulta en el momento de la ejecución.
Transformar una consulta compleja en una consulta de estilo CTE requiere un pensamiento estructurado. Lo mismo ocurre con la programación orientada a objetos con encapsulación al reescribir una consulta compleja en una estructura CTE.
Tienes que pensar en:
- Cada conjunto de datos que está extrayendo de cada tabla.
- Cómo se unen para encapsular las subconsultas más cercanas en un conjunto de resultados con nombre temporal.
Repítalo para cada subconsulta y conjunto de datos restantes hasta llegar al resultado final de la consulta. Tenga en cuenta que cada conjunto de resultados con nombre temporal también es una subconsulta.
La parte final de la consulta debe ser una selección muy “simple”, devolviendo el resultado final a la aplicación. Una vez que haya llegado a esta parte final, puede intercambiarla con una consulta que seleccione los datos de un conjunto de resultados temporal nombrado individualmente.
De esta manera, la depuración de cada conjunto de resultados temporal se convierte en un trabajo fácil.
Para comprender cómo podemos construir nuestras consultas de simples a complejas, veamos la estructura CTE. La forma más simple es la siguiente:
WITH CTE_1 as (
select .... from some_table where ...
)
select ... from CTE_1
where ...
Aquí CTE_1 es un nombre único que le da al conjunto de resultados con nombre temporal. Puede haber tantos conjuntos de resultados como sea necesario. Por eso, el formulario se extiende a, como se muestra a continuación:
WITH CTE_1 as (
select .... from some_table where ...
), CTE_2 as (
select .... from some_other_table where ...
)
select ... from CTE_1 c1,CTE_2 c2
where c1.col1 = c2.col1
....
Al principio, cada parte CTE se crea por separado. Luego avanza, a medida que los CTE se vinculan para construir el conjunto de resultados final de la consulta.
Ahora, examinemos otro caso, consultando una base de datos de ventas ficticia. Queremos saber qué productos, incluida la cantidad y las ventas totales, se vendieron en cada categoría el mes anterior y cuáles obtuvieron más ventas totales que el mes anterior.
Construimos nuestra consulta en varias partes CTE, donde cada parte hace referencia a la anterior. Primero, construimos un conjunto de resultados para enumerar los datos detallados que necesitamos de nuestras tablas para formar el resto de la consulta:
WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
)
select dt.*
from detailed_data dt.
order by dt.order_date desc, dt.category_name, dt.product_name
El siguiente paso es resumir los datos de cantidad y ventas totales por cada categoría y nombre de producto:
WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
), product_sales as (
select year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name,sum(dt.quantity) total_quantity, sum(dt.listprice * (1 - dt.discount)) total_product_sales
from detailed_data dt
group by year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name
)
select ps.*
from product_sales ps
order by ps.year desc, ps.month desc, ps.category_name,ps.product_name
El último paso es crear dos conjuntos de resultados temporales que representen los datos del último mes y del mes anterior. Después de eso, filtre los datos que se devolverán como conjunto de resultados final:
WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
), product_sales as (
select year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name,sum(dt.quantity) total_quantity, sum(dt.listprice * (1 - dt.discount)) total_product_sales
from detailed_data dt
group by year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name
), last_month_data (
select ps.*
from product_sales ps.
where ps.year = year(CURRENT_DATE) -1
and ps.month = month(CURRENT_DATE) -1
), prev_month_data (
select ps.*
from product_sales ps.
where ps.year = year(CURRENT_DATE) -2
and ps.month = month(CURRENT_DATE) -2
)
select lmd.*
from last_month_data lmd, prev_month_data pmd
where lmd.category_name = pmd.category_name
and lmd.product_name = pmd.product_name
and ( lmd.total_quantity > pmd.total_quantity
or lmd.total_product_sales > pmd.total_product_sales )
order by lmd.year desc, lmd.month desc, lmd.category_name,lmd.product_name, lmd.total_product_sales desc, lmd.total_quantity desc
Tenga en cuenta que en SQLServer configura getdate() en lugar de CURRENT_DATE.
De esta forma, podemos intercambiar la última parte con una selección que consulta partes individuales del CTE para ver el resultado de una parte seleccionada. Como resultado, podemos solucionar el problema rápidamente.
Además, al ejecutar una explicación en cada parte de CTE (y toda la consulta), estimamos qué tan bien funcionará cada parte y/o toda la consulta en las tablas y los datos.
En consecuencia, puede optimizar cada parte reescribiendo y/o agregando índices adecuados a las tablas involucradas. Luego explica la consulta completa para ver el plan de consulta final y continúa con la optimización si es necesario.
Consultas recursivas con estructura CTE
Otra función útil de CTE es la creación de consultas recursivas.
Las consultas SQL recursivas le permiten lograr cosas que no imaginaba posibles con este tipo de SQL y su velocidad. Puede resolver muchos problemas comerciales e incluso reescribir alguna lógica de aplicación/SQL compleja en una simple llamada SQL recursiva a la base de datos.
Existen ligeras variaciones en la creación de consultas recursivas entre sistemas de bases de datos. Sin embargo, el objetivo es el mismo.
Algunos ejemplos de la utilidad de CTE recursivo:
- Puede usarlo para encontrar lagunas en los datos.
- Puedes crear organigramas.
- Puede crear datos precalculados para usarlos en otra parte de CTE
- Finalmente, puede crear datos de prueba.
La palabra recursivo lo dice todo Tiene una consulta que se llama a sí misma repetidamente con algún punto de partida y, MUY IMPORTANTE, un punto final (una salida a prueba de fallas como yo lo llamo).
Si no tiene una salida a prueba de fallas, o su fórmula recursiva va más allá, está en serios problemas. La consulta entrará en un bucle infinito lo que resulta en una CPU muy alta y una utilización de LOG muy alta. Conducirá al agotamiento de la memoria y/o del almacenamiento.
Si su consulta se vuelve loca, debe pensar muy rápido para deshabilitarla. Si no puede hacerlo, avise a su DBA de inmediato, para que eviten que el sistema de la base de datos se ahogue y elimine el hilo desbocado.
Ver el ejemplo:
with RECURSIVE mydates (level,nextdate) as (
select 1 level, FROM_UNIXTIME(RAND()*2147483647) nextdate from DUAL
union all
select level+1, FROM_UNIXTIME(RAND()*2147483647) nextdate
from mydates
where level < 1000
)
SELECT nextdate from mydates
);
Este ejemplo es una sintaxis CTE recursiva de MySQL/MariaDB. Con él, producimos mil fechas aleatorias. El nivel es nuestro contador y salida a prueba de fallas para salir de la consulta recursiva de manera segura.
Como se demostró, la línea 2 es nuestro punto de partida, mientras que las líneas 4 y 5 son la llamada recursiva con el punto final en la cláusula WHERE (línea 6). Las líneas 8 y 9 son las llamadas para ejecutar la consulta recursiva y recuperar los datos.
Otro ejemplo:
DECLARE @today as date;
DECLARE @1stjanprevyear as date;
select @today = DATEADD(DAY, 0, DATEDIFF(DAY, 0, getdate())),
@1stjanprevyear = DATEFROMPARTS(YEAR(GETDATE())-1, 1, 1) ;
WITH DatesCTE as (
SELECT @1stjanprevyear as CalendarDate
UNION ALL
SELECT dateadd(day , 1, CalendarDate) AS CalendarDate FROM DatesCTE
WHERE dateadd (day, 1, CalendarDate) < @today
), MaxMinDates as (
SELECT Max(CalendarDate) MaxDate,Min(CalendarDate) MinDate
FROM DatesCTE
)
SELECT i.*
FROM InvoiceTable i, MaxMinDates t
where i.INVOICE_DATE between t.MinDate and t.MaxDate
OPTION (MAXRECURSION 1000);
Este ejemplo es una sintaxis de SQLServer. Aquí, dejamos que la parte DatesCTE produzca todas las fechas entre hoy y el 1 de enero del año anterior. Lo usamos para devolver todas las Facturas pertenecientes a esas fechas.
El punto de partida es @1stjanprevyear variable y la salida a prueba de fallas @today . Es posible un máximo de 730 días. Por lo tanto, la opción de recursividad máxima se establece en 1000 para asegurarse de que se detenga.
Incluso podríamos omitir las MaxMinDates part y escriba la parte final, como se muestra a continuación. Puede ser un enfoque más rápido, ya que tenemos una cláusula WHERE coincidente.
....
SELECT i.*
FROM InvoiceTable i, DatesCTE t
where i.INVOICE_DATE = t.CalendarDate
OPTION (MAXRECURSION 1000);
Conclusión
En conjunto, hemos discutido brevemente y mostrado cómo transformar una consulta compleja en una consulta estructurada CTE. Cuando una consulta se divide en diferentes partes de CTE, puede usarlas en otras partes y llamar de forma independiente en la consulta SQL final con fines de depuración.
Otro punto clave es que el uso de CTE simplifica la depuración de una consulta compleja cuando se divide en partes manejables, para devolver el conjunto de resultados correcto y esperado. Es importante darse cuenta de que ejecutar una explicación en cada parte de la consulta y en la consulta completa es crucial para garantizar que la consulta y el DBMS se ejecuten de la mejor manera posible.
También he ilustrado la escritura de una poderosa consulta/parte CTE recursiva en la generación de datos sobre la marcha para usar más en una consulta.
En particular, al escribir una consulta recursiva, tenga MUCHO cuidado de NO olvidar la salida a prueba de fallas . Asegúrese de verificar dos veces los cálculos utilizados en la salida a prueba de fallas para producir una señal de alto y/o use la recursión máxima opción que proporciona SQLServer.
De manera similar, otros DBMS pueden usar cte_max_recursion_ depth (MySQL 8.0) o max_recursive_iterations (MariaDB 10.3) como salidas de seguridad adicionales.
Leer también
Todo lo que necesita saber sobre SQL CTE en un solo lugar