Упорядочение запросов с помощью общих выражений таблицы
Подсказка
Дополнительные сведения см. на вкладке "Текст и изображения ".
При работе с сложными запросами часто необходимо разбить логику на управляемые части или ссылаться на один вложенный запрос несколько раз. Общие выражения таблиц (CTEs) предоставляют способ определения временных именованных результирующих наборов, которые существуют только во время одного запроса, что делает код более читаемым и обслуживаемым.
Общие сведения о синтаксисе CTE
Общее табличное выражение определяется с помощью предложения, за которым WITH следует имя CTE, необязательный список столбцов и запрос, определяющий результирующий набор. Затем можно ссылаться на CTE в последующих инструкциях SELECT, INSERT, UPDATE или DELETE.
WITH CTE_Name (Column1, Column2)
AS
(
-- CTE query definition
SELECT Column1, Column2
FROM SomeTable
WHERE SomeCondition = 'Value'
)
SELECT * FROM CTE_Name;
CTEs предлагают несколько преимуществ по сравнению с производными таблицами и вложенными запросами:
- Улучшенная удобочитаемость: сложные запросы становятся проще для понимания, когда их разбивают на именованные логические разделы
- Возможность самостоятельного обращения к данным: рекурсивные ТС могут ссылаться на себя, что позволяет выполнять иерархический обход данных.
- Несколько ссылок: ссылка на один И тот же CTE несколько раз во внешнем запросе без переопределения
- Модульное проектирование: пошагово создавайте сложные запросы путем определения нескольких общих табличных выражений (ТС)
Замечание
CTEs — это временные результирующие наборы, которые существуют только во время выполнения запроса. В отличие от традиционных таблиц, они не сохраняются за пределами инструкции, которая использует их и не требует явной очистки.
Создание нерекурсивных ТС
Нерекурсивные КТС определяют результирующий набор на основе простого запроса, который не ссылается на себя. Этот шаблон полезен для упрощения сложных соединений, разбиения многофакторных вычислений или улучшения организации кода.
В следующем примере CTE используется для вычисления метрик продаж перед присоединением к сведениям о продукте:
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;
Этот запрос сначала создает CTE с именем SalesSummary , который объединяет сведения о заказе по продукту, вычисляя общее количество проданных, общий доход и количество заказов. Затем основной запрос присоединяет этот CTE к Product таблице, чтобы отобразить имена продуктов вместе с их метриками продаж. Функция NULLIF предотвращает деление на ноль при расчете средней цены на единицу.
Вы можете определить несколько ТС в одном WITH предложении, разделив их запятыми. Более поздние обобщенные табличные выражения (CTEs) могут ссылаться на более ранние, позволяя прогрессивно преобразовывать данные.
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;
Подсказка
При создании запросов с несколькими ТСЗ начните с наиболее детализированных преобразований данных и постепенно агрегирования или фильтрации в последующих ТСЗ. Такой подход упрощает отладку, так как можно протестировать каждый CTE независимо.
Создайте рекурсивные СКВ
Рекурсивные выражения общего табличного выражения (CTE) обслуживают структуру данных, подобную иерархической или графовой, обращаясь к самим себе. Рекурсивный CTE состоит из двух частей: якорного элемента, предоставляющего начальный результирующий набор, и рекурсивного члена, ссылающегося на CTE для наращивания предыдущих результатов.
Общий синтаксис рекурсивного CTE:
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;
Распространенный вариант использования — обход иерархии организации. Рассмотрим таблицу сотрудников, в которой каждый сотрудник имеет руководителя:
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;
Это важно
Рекурсивные ТС могут вызвать бесконечные циклы, если условие завершения никогда не выполняется. SQL Server ограничивает рекурсию до 100 уровней по умолчанию. Используется OPTION (MAXRECURSION n) для изменения этого ограничения, где n максимальная глубина рекурсии (0 для неограниченной).
Создание последовательностей с рекурсивными ТС
Рекурсивные ТСВ преуспевают в создании последовательностей чисел или дат без необходимости в наличии таблицы чисел. Этот метод полезен для создания диапазонов дат, заполнения пробелов в данных или создания тестовых данных:
-- 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)
Вы можете объединить созданные последовательности с фактическими данными, чтобы определить пробелы или создать сводные отчеты:
-- 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)
Использование CTE с инструкциями изменения данных
CTE можно использовать с INSERT, UPDATE и DELETE операторами, а не только с SELECT. Эта возможность полезна, если логика изменения требует сложной фильтрации или вычислений:
-- 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);
Этот запрос использует CTE для идентификации продуктов, которые должны быть помечены как прекращенные. CTE находит продукты, где дата окончания продажи более двух лет назад, и которые не появились в каких-либо деталях заказа, измененных в течение прошлого года. Затем инструкция UPDATE задает DiscontinuedDate для этих продуктов. Разделив логику выбора на CTE, запрос становится проще читать и тестировать независимо.
Дополнительные сведения о распространенных выражениях таблиц см. в разделе WITH common_table_expression (Transact-SQL) и рекурсивные запросы с использованием общих выражений таблицы.