Muitas vezes vemos consultas SQL complexas mal escritas em execução em uma tabela ou tabelas em bancos de dados. Essas consultas tornam o tempo de execução muito longo e consomem enorme CPU e outros recursos. Ainda assim, consultas complexas fornecem informações valiosas para o aplicativo/pessoa que as executa em muitos casos. Portanto, eles são ativos úteis em todas as variedades de aplicações.
Consultas complexas são difíceis de depurar
Se olharmos atentamente para as consultas problemáticas, muitas delas são complexas, especialmente aquelas específicas usadas em relatórios.
As consultas complexas geralmente consistem em cinco ou mais tabelas grandes e são unidas por muitas subconsultas. Cada subconsulta possui uma cláusula WHERE que realiza cálculos simples a complexos e/ou transformações de dados enquanto une as tabelas relevantes das colunas.
Essas consultas podem se tornar um desafio para depurar sem consumir muitos recursos. A razão é que é difícil determinar se cada subconsulta e/ou subconsultas unidas produzem resultados corretos.
Um cenário típico é:eles ligam para você tarde da noite para resolver um problema em um servidor de banco de dados ocupado com uma consulta complexa envolvida e você precisa corrigi-lo rapidamente. Como desenvolvedor ou DBA, você pode ter tempo e recursos de sistema muito limitados disponíveis em uma hora tardia. Assim, a primeira coisa que você precisa é um plano sobre como depurar a consulta problemática.
Às vezes, o procedimento de depuração vai bem. Às vezes, leva muito tempo e esforço antes de você atingir a meta e resolver o problema.
Escrevendo consultas na estrutura CTE
Mas e se houvesse uma maneira de escrever consultas complexas para que se pudesse depurá-las rapidamente, peça por peça?
Existe tal maneira. É chamado de expressão de tabela comum ou CTE.
Common Table Expression é um recurso padrão na maioria dos bancos de dados modernos, como SQLServer, MySQL (a partir da versão 8.0), MariaDB (versão 10.2.1), Db2 e Oracle. Ele tem uma estrutura simples que encapsula uma ou várias subconsultas em um conjunto de resultados nomeado temporário. Você pode usar esse conjunto de resultados em outras CTEs ou subconsultas nomeadas.
Uma expressão de tabela comum é, até certo ponto, uma VIEW que só existe e é referenciada pela consulta no momento da execução.
Transformar uma consulta complexa em uma consulta de estilo CTE exige algum pensamento estruturado. O mesmo vale para OOP com encapsulamento ao reescrever uma consulta complexa em uma estrutura CTE.
Você precisa pensar sobre:
- Cada conjunto de dados que você está extraindo de cada tabela.
- Como elas são unidas para encapsular as subconsultas mais próximas em um conjunto de resultados nomeado temporário.
Repita-o para cada subconsulta e conjunto de dados restantes até chegar ao resultado final da consulta. Observe que cada conjunto de resultados nomeado temporário também é uma subconsulta.
A parte final da consulta deve ser um select bem “simples”, retornando o resultado final para a aplicação. Depois de chegar a essa parte final, você pode trocá-la por uma consulta que seleciona os dados de um conjunto de resultados temporário nomeado individualmente.
Dessa forma, a depuração de cada conjunto de resultados temporário se torna uma tarefa fácil.
Para entender como podemos construir nossas consultas do simples ao complexo, vejamos a estrutura do CTE. A forma mais simples é a seguinte:
WITH CTE_1 as (
select .... from some_table where ...
)
select ... from CTE_1
where ...
Aqui CTE_1 é um nome exclusivo que você dá ao conjunto de resultados nomeado temporário. Pode haver quantos conjuntos de resultados forem necessários. Com isso, o formulário se estende até, conforme mostrado abaixo:
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
....
A princípio, cada parte do CTE é criada separadamente. Em seguida, ele progride, à medida que os CTEs são vinculados para construir o conjunto de resultados final da consulta.
Agora, vamos examinar outro caso, consultando um banco de dados de vendas fictício. Queremos saber quais produtos, incluindo quantidade e vendas totais, foram vendidos em cada categoria no mês anterior e quais deles tiveram mais vendas totais do que no mês anterior.
Construímos nossa consulta em várias partes CTE, onde cada parte faz referência à anterior. Primeiro, construímos um conjunto de resultados para listar os dados detalhados que precisamos de nossas tabelas para formar o restante da 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
A próxima etapa é resumir os dados de quantidade e vendas totais por cada categoria e nomes de produtos:
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
A etapa final é criar dois conjuntos de resultados temporários representando os dados do mês anterior e do mês anterior. Depois disso, filtre os dados a serem retornados como o 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
Observe que no SQLServer você define getdate() em vez de CURRENT_DATE.
Dessa forma, podemos trocar a última parte por um select que consulta partes individuais do CTE para ver o resultado de uma parte selecionada. Como resultado, podemos depurar rapidamente o problema.
Além disso, executando uma explicação em cada parte do CTE (e em toda a consulta), estimamos o desempenho de cada parte e/ou da consulta inteira nas tabelas e dados.
Da mesma forma, você pode otimizar cada parte reescrevendo e/ou adicionando índices apropriados às tabelas envolvidas. Em seguida, você explica toda a consulta para ver o plano de consulta final e prosseguir com a otimização, se necessário.
Consultas recursivas usando estrutura CTE
Outro recurso útil do CTE é a criação de consultas recursivas.
As consultas SQL recursivas permitem alcançar coisas que você não imaginaria ser possível com esse tipo de SQL e sua taxa. Você pode resolver muitos problemas de negócios e até reescrever alguma lógica complexa de SQL/aplicativo para uma simples chamada de SQL recursiva para o banco de dados.
Existem pequenas variações na criação de consultas recursivas entre sistemas de banco de dados. No entanto, o objetivo é o mesmo.
Alguns exemplos da utilidade do CTE recursivo:
- Você pode usá-lo para encontrar lacunas nos dados.
- Você pode criar organogramas.
- Você pode criar dados pré-computados para usar em outra parte do CTE
- Finalmente, você pode criar dados de teste.
A palavra recursiva diz tudo. Você tem uma consulta que se chama repetidamente com algum ponto de partida e, EXTREMAMENTE IMPORTANTE, um ponto final (uma saída à prova de falhas como eu chamo).
Se você não tem uma saída à prova de falhas, ou sua fórmula recursiva vai além dela, você está em apuros. A consulta entrará em um loop infinito resultando em CPU muito alta e utilização de LOG muito alta. Isso levará ao esgotamento da memória e/ou armazenamento.
Se sua consulta der errado, você deve pensar muito rápido para desativá-la. Se você não puder fazer isso, alerte seu DBA imediatamente, para que ele evite que o sistema de banco de dados engasgue, matando o encadeamento descontrolado.
Veja o exemplo:
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 exemplo é uma sintaxe CTE recursiva MySQL/MariaDB. Com ele, produzimos mil datas aleatórias. O nível é nosso contador e saída à prova de falhas para sair da consulta recursiva com segurança.
Conforme demonstrado, a linha 2 é nosso ponto de partida, enquanto as linhas 4-5 são a chamada recursiva com o ponto final na cláusula WHERE (linha 6). As linhas 8 e 9 são as chamadas na execução da consulta recursiva e na recuperação dos dados.
Outro exemplo:
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 exemplo é uma sintaxe do SQLServer. Aqui, deixamos a parte DatesCTE produzir todas as datas entre hoje e 1º de janeiro do ano anterior. Usamos para devolver todas as faturas pertencentes a essas datas.
O ponto de partida é o @1stjanprevyear variável e a saída à prova de falhas @today . É possível um máximo de 730 dias. Assim, a opção de recursão máxima é definida como 1000 para garantir que ela pare.
Poderíamos até pular as MaxMinDates parte e escreva a parte final, como mostrado abaixo. Pode ser uma abordagem mais rápida, pois temos uma cláusula WHERE correspondente.
....
SELECT i.*
FROM InvoiceTable i, DatesCTE t
where i.INVOICE_DATE = t.CalendarDate
OPTION (MAXRECURSION 1000);
Conclusão
Ao todo, discutimos brevemente e mostramos como transformar uma consulta complexa em uma consulta estruturada CTE. Quando uma consulta é dividida em diferentes partes CTE, você pode usá-las em outras partes e chamar independentemente na consulta SQL final para fins de depuração.
Outro ponto importante é que o uso do CTE simplifica a depuração de uma consulta complexa quando ela é dividida em partes gerenciáveis, para retornar o conjunto de resultados correto e esperado. É importante perceber que executar uma explicação em cada parte da consulta e em toda a consulta é crucial para garantir que a consulta e o SGBD sejam executados da maneira mais otimizada possível.
Também ilustrei a escrita de uma consulta/parte CTE recursiva poderosa na geração de dados em tempo real para uso posterior em uma consulta.
Notavelmente, ao escrever uma consulta recursiva, tenha MUITO cuidado para NÃO esquecer a saída à prova de falhas . Certifique-se de verificar novamente os cálculos usados na saída à prova de falhas para produzir um sinal de parada e/ou usar a maxrecursion opção que o SQLServer fornece.
Da mesma forma, outros DBMS podem usar cte_max_recursion_depth (MySQL 8.0) ou max_recursive_iterations (MariaDB 10.3) como saídas à prova de falhas adicionais.
Leia também
Tudo o que você precisa saber sobre SQL CTE em um só lugar