Organizar consultas com expressões de tabela comuns
Ao trabalhar com consultas complexas, muitas vezes você precisa dividir a lógica em partes gerenciáveis ou referenciar a mesma subconsulta várias vezes. As CTEs (Expressões comuns de tabela) fornecem uma maneira de definir conjuntos de resultados nomeados temporários que existem somente durante uma única consulta, tornando seu código mais legível e mantenedível.
Entender a sintaxe de CTE
Uma Expressão de Tabela Comum é definida usando a WITH cláusula, seguida pelo nome CTE, uma lista de colunas opcional e uma consulta que define o conjunto de resultados. Em seguida, a CTE pode ser referenciada na instrução subsequente SELECT, INSERT, UPDATE ou DELETE.
WITH CTE_Name (Column1, Column2)
AS
(
-- CTE query definition
SELECT Column1, Column2
FROM SomeTable
WHERE SomeCondition = 'Value'
)
SELECT * FROM CTE_Name;
Os CTEs oferecem várias vantagens em relação a tabelas e subconsultas derivadas:
- Legibilidade aprimorada: consultas complexas tornam-se mais fáceis de entender quando divididas em seções lógicas nomeadas
- Capacidade de auto-referência: CTEs recursivos podem referenciar a si mesmos, habilitando o percurso de dados hierárquicos
- Várias referências: referenciar o mesmo CTE várias vezes na consulta externa sem redefini-la
- Design modular: criar consultas complexas de forma incremental definindo várias CTEs
Observação
Ctes são conjuntos de resultados temporários que existem somente durante a execução da consulta. Ao contrário das tabelas tradicionais, elas não persistem além do comando que as usa e não exigem limpeza explícita.
Criar CTEs não recursivos
CTEs não recursivos definem um conjunto de resultados com base em uma consulta simples que não faz referência a si mesma. Esse padrão é útil para simplificar junções complexas, dividir cálculos de várias etapas ou melhorar a organização de código.
O exemplo a seguir usa um CTE para calcular as métricas de vendas antes de ingressar com as informações do produto:
WITH SalesSummary AS
(
SELECT
ProductID,
SUM(OrderQty) AS TotalQuantity,
SUM(LineTotal) AS TotalRevenue,
COUNT(DISTINCT SalesOrderID) AS OrderCount
FROM SalesLT.SalesOrderDetail
GROUP BY ProductID
)
SELECT
p.Name AS ProductName,
p.ProductNumber,
p.ListPrice,
ss.TotalQuantity,
ss.TotalRevenue,
ss.OrderCount,
ss.TotalRevenue / NULLIF(ss.TotalQuantity, 0) AS AverageUnitPrice
FROM SalesLT.Product AS p
INNER JOIN SalesSummary AS ss
ON p.ProductID = ss.ProductID
ORDER BY ss.TotalRevenue DESC;
Essa consulta primeiro cria um CTE chamado SalesSummary que agrega detalhes do pedido por produto, calculando a quantidade total vendida, a receita total e a contagem de pedidos. Em seguida, a consulta principal une esse CTE à tabela Product para exibir nomes de produtos junto com suas métricas de vendas. A NULLIF função impede a divisão por zero ao calcular o preço unitário médio.
Você pode definir vários CTEs em uma única cláusula WITH, separando-os com vírgulas. CTEs posteriores podem referenciar os anteriores, permitindo a progressiva transformação de dados:
WITH CategorySales AS
(
SELECT
p.ProductCategoryID,
SUM(sod.LineTotal) AS CategoryRevenue
FROM SalesLT.Product AS p
INNER JOIN SalesLT.SalesOrderDetail AS sod
ON p.ProductID = sod.ProductID
GROUP BY p.ProductCategoryID
),
RankedCategories AS
(
SELECT
ProductCategoryID,
CategoryRevenue,
RANK() OVER (ORDER BY CategoryRevenue DESC) AS RevenueRank
FROM CategorySales
)
SELECT
pc.Name AS CategoryName,
rc.CategoryRevenue,
rc.RevenueRank
FROM RankedCategories AS rc
INNER JOIN SalesLT.ProductCategory AS pc
ON rc.ProductCategoryID = pc.ProductCategoryID
WHERE rc.RevenueRank <= 5;
Dica
Ao criar consultas com vários CTEs, comece com as transformações de dados mais granulares e agrege ou filtre progressivamente em CTEs subsequentes. Essa abordagem facilita a depuração, pois você pode testar cada CTE de forma independente.
Criar CTEs recursivos
CTEs recursivos fazem referência a estruturas de dados hierárquicas ou semelhantes a gráfico. Um CTE recursivo consiste em duas partes: um membro de âncora que fornece o conjunto de resultados inicial e um membro recursivo que se refere ao CTE para se basear nos resultados anteriores.
A sintaxe geral de um CTE recursivo é:
WITH RecursiveCTE AS
(
-- Anchor member: starting point
SELECT columns
FROM table
WHERE starting_condition
UNION ALL
-- Recursive member: references the CTE
SELECT columns
FROM table
INNER JOIN RecursiveCTE
ON join_condition
)
SELECT * FROM RecursiveCTE;
Um caso de uso comum é percorrer uma hierarquia organizacional. Considere uma tabela de funcionários em que cada funcionário tenha um gerente:
WITH EmployeeHierarchy AS
(
-- Anchor: Start with top-level managers (no manager)
SELECT
EmployeeID,
FirstName,
LastName,
ManagerID,
0 AS Level,
CAST(FirstName + ' ' + LastName AS NVARCHAR(500)) AS HierarchyPath
FROM HumanResources.Employee
WHERE ManagerID IS NULL
UNION ALL
-- Recursive: Find employees who report to previously found employees
SELECT
e.EmployeeID,
e.FirstName,
e.LastName,
e.ManagerID,
eh.Level + 1,
CAST(eh.HierarchyPath + ' > ' + e.FirstName + ' ' + e.LastName AS NVARCHAR(500))
FROM HumanResources.Employee AS e
INNER JOIN EmployeeHierarchy AS eh
ON e.ManagerID = eh.EmployeeID
)
SELECT
EmployeeID,
FirstName,
LastName,
Level,
HierarchyPath
FROM EmployeeHierarchy
ORDER BY HierarchyPath;
Importante
CTEs recursivos podem causar loops infinitos se a condição de término nunca for atendida. O SQL Server limita a recursão a 100 níveis por padrão. Use OPTION (MAXRECURSION n) para alterar esse limite, onde n está a profundidade máxima de recursão (0 para ilimitado).
Gerar sequências com CTEs recursivos
Os CTEs recursivos se destacam na geração de sequências de números ou datas sem a necessidade de uma tabela de números físicos. Essa técnica é útil para criar intervalos de datas, preencher lacunas em dados ou gerar dados de teste:
-- Generate a sequence of dates for the current month
WITH DateSequence AS
(
-- Anchor: Get the first day of the current month
SELECT CAST(DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0) AS DATE) AS DateValue
UNION ALL
-- Recursive: Add one day until we reach the end of the month
SELECT DATEADD(DAY, 1, DateValue)
FROM DateSequence
WHERE DateValue < EOMONTH(GETDATE())
)
-- Output each date with its day name
SELECT DateValue, DATENAME(WEEKDAY, DateValue) AS DayName
FROM DateSequence
OPTION (MAXRECURSION 31); -- Allow up to 31 iterations (max days in a month)
Você pode combinar sequências geradas com dados reais para identificar lacunas ou criar relatórios de resumo:
-- Generate a numbers table from 1 to 1000
WITH Numbers AS
(
-- Anchor: Start with 1
SELECT 1 AS n
UNION ALL
-- Recursive: Increment until we reach 1000
SELECT n + 1 FROM Numbers WHERE n < 1000
),
-- Convert numbers to dates for the entire year
DateRange AS
(
SELECT DATEADD(DAY, n - 1, '2024-01-01') AS OrderDate
FROM Numbers
WHERE DATEADD(DAY, n - 1, '2024-01-01') <= '2024-12-31'
)
-- Count orders for each date, showing 0 for dates with no orders
SELECT
dr.OrderDate,
COALESCE(COUNT(soh.SalesOrderID), 0) AS OrderCount
FROM DateRange AS dr
LEFT JOIN SalesLT.SalesOrderHeader AS soh
ON CAST(soh.OrderDate AS DATE) = dr.OrderDate
GROUP BY dr.OrderDate
ORDER BY dr.OrderDate
OPTION (MAXRECURSION 366); -- Allow up to 366 iterations (leap year)
Usar CTEs com instruções de modificação de dados
CTEs podem ser usados com instruções INSERT, UPDATE e DELETE, e não apenas com SELECT. Essa funcionalidade é útil quando a lógica de modificação requer filtragem ou cálculos complexos:
-- Update using a CTE to identify target rows
WITH DiscontinuedProducts AS
(
SELECT ProductID
FROM SalesLT.Product
WHERE SellEndDate < DATEADD(YEAR, -2, GETDATE())
AND ProductID NOT IN (
SELECT DISTINCT ProductID
FROM SalesLT.SalesOrderDetail
WHERE ModifiedDate > DATEADD(YEAR, -1, GETDATE())
)
)
UPDATE SalesLT.Product
SET DiscontinuedDate = GETDATE()
WHERE ProductID IN (SELECT ProductID FROM DiscontinuedProducts);
Essa consulta usa um CTE para identificar produtos que devem ser marcados como descontinuados. O CTE encontra produtos cuja data de término de venda é superior a dois anos atrás e que não apareceram em nenhum dos detalhes de pedido modificados no último ano. Em seguida, a instrução UPDATE define o DiscontinuedDate para esses produtos. Ao separar a lógica de seleção em um CTE, a consulta fica mais fácil de ler e testar de forma independente.
Para obter mais informações sobre expressões comuns de tabela, consulte WITH common_table_expression (Transact-SQL) e consultas recursivas usando expressões de tabela comuns.