Производительность запросов в SQL Server: ошибка #3
Большое количество таблиц
Оптимизатор запросов в SQL Server сталкивается с той же проблемой, что и любой другой оптимизатор: Ему необходимо найти хороший план выполнения при наличии множества вариантов за очень короткий промежуток времени. По сути, он играет в шахматную игру и просчитывает ход за ходом. При каждой оценке он либо отбрасывает кусок планов, похожих на субоптимальный план, либо выбирает один из них в качестве плана-кандидата. Большее количество таблиц в запросе равносильно большей шахматной доске. При значительно большем количестве доступных вариантов у SQL Server появляется больше работы, но определение плана для использования не должно длиться дольше.
Каждая таблица, добавленная к запросу, увеличивает его сложность на факториальную величину. Хотя оптимизатор обычно принимает правильные решения даже при наличии большого количества таблиц, с каждой добавленной в запрос таблицей мы увеличиваем риск неэффективных планов. Это не значит, что запросы с большим количеством таблиц плохи, но мы должны проявлять осторожность при увеличении размера запроса. Для каждого набора таблиц необходимо определить порядок соединения, тип соединения, а также как/когда применять фильтры и агрегацию.
В зависимости от того, как соединяются таблицы, запрос будет иметь одну из двух основных форм:
- Left-Deep Tree: A соединяется с B, B соединяется с C, C соединяется с D, D соединяется с E и т.д. Это запрос, в котором большинство таблиц соединяются последовательно друг за другом.
- Bushy Tree: A соединяется с B, A соединяется с C, B соединяется с D, C соединяется с E и т.д. Это запрос, в котором таблицы разветвляются на множество логических единиц в пределах каждой ветви дерева.
На рисунке ниже представлено графическое изображение bushy tree, в котором соединения ведут вверх к итоговому результату:

Аналогично, вот представление того, как будет выглядеть Left-Deep Tree:

Поскольку left-deep tree более естественно упорядочено на основе того, как соединены таблицы, количество планов-кандидатов на выполнение запроса меньше, чем для bushy tree. Выше приводится математика, лежащая в основе: то есть, сколько планов будет сгенерировано в среднем для данного типа запроса.
Чтобы подчеркнуть масштабность вычислений, в зависимости от количества таблиц, рассмотрим запрос, который обращается к 12 таблицам:
SELECT TOP 25 PRODUCT.ProductID, PRODUCT.Name AS ProductName, PRODUCT.ProductNumber, CostMeasure.UnitMeasureCode, CostMeasure.Name as CostMeasureName, ProductVendor.AverageLeadTime, ProductVendor.StandardPrice, ProductReview.ReviewerName, ProductReview.Rating, ProductCategory.Name as CategoryName, ProductSubCategory.Name as SubCategoryName FROM Production.Product PRODUCT INNER JOIN Production.ProductSubCategory ON ProductSubCategory.ProductSubcategoryID = Product.ProductSubcategoryID INNER JOIN Production.ProductCategory ON ProductCategory.ProductCategoryID = ProductSubCategory.ProductCategoryID INNER JOIN Production.UnitMeasure SizeUnitMeasureCode ON Product.SizeUnitMeasureCode = SizeUnitMeasureCode.UnitMeasureCode INNER JOIN Production.UnitMeasure WeightUnitMeasureCode ON Product.WeightUnitMeasureCode = WeightUnitMeasureCode.UnitMeasureCode INNER JOIN Production.ProductModel ON ProductModel.ProductModelID = Product.ProductModelID LEFT JOIN Production.ProductModelIllustration ON ProductModel.ProductModelID = ProductModelIllustration.ProductModelID LEFT JOIN Production.ProductModelProductDescriptionCulture ON ProductModelProductDescriptionCulture.ProductModelID = ProductModel.ProductModelID LEFT JOIN Production.ProductDescription ON ProductDescription.ProductDescriptionID = ProductModelProductDescriptionCulture.ProductDescriptionID LEFT JOIN Production.ProductReview ON ProductReview.ProductID = Product.ProductID LEFT JOIN Purchasing.ProductVendor ON ProductVendor.ProductID = Product.ProductID LEFT JOIN Production.UnitMeasure CostMeasure ON ProductVendor.UnitMeasureCode = CostMeasure.UnitMeasureCode ORDER BY Product.ProductID DESC;
При 12 таблицах математика будет выглядеть следующим образом:
(2n-2)! / (n-1)! = (2*12-1)! / (12-1)! = 28,158,588,057,600 возможных планов.
Если бы запрос был более линейным по своей природе, то мы бы получили:
n! = 12! = 479,001,600 возможных планов.
Это только для 12 таблиц! Представьте себе запрос на 20, 30 или 50 таблиц! Оптимизатор часто может сокращать эти числа очень быстро, устраняя целые блоки неоптимальных вариантов, но вероятность того, что он сможет сделать это и сгенерировать хороший план, уменьшается по мере увеличения количества таблиц.
Каковы основные способы оптимизации запроса, который страдает из-за слишком большого количества таблиц?
- Переместите метаданные или таблицы в отдельный запрос, который поместит эти данные во временную таблицу.
- JOINы, которые используются для возврата одной константы, можно переместить в параметр или переменную.
- Разбейте большой запрос на более мелкие запросы, наборы данных которых впоследствии можно объединить.
- Для очень часто используемых запросов рассмотрите возможность использования индексированного представления, чтобы упростить постоянный доступ к важным данным.
- Удалите ненужные таблицы, подзапросы и соединения.
Разбиение большого запроса на более мелкие запросы требует, чтобы между этими запросами не происходило каких-либо изменений данных, которые могли бы каким-то образом исказить итоговый результат. Разбиение большого запроса на более мелкие запросы требует, чтобы между этими запросами не происходило каких-либо изменений данных, которые могли бы каким-то образом исказить итоговый результат. Если запрос должен быть целостным, то для обеспечения целостности данных может потребоваться сочетание уровней изоляции и транзакций.
Чаще всего, когда мы объединяем большое количество таблиц вместе, мы можем разбить запрос на более мелкие логические единицы, которые могут быть выполнены отдельно. В примере с 12 таблицами мы можем легко удалить несколько неиспользуемых таблиц и разделить получение данных на два отдельных запроса:
SELECT TOP 25 Product.ProductId, Product.Name AS ProductName, Product.ProductNumber, ProductCategory.Name AS ProductCategory, ProductSubCategory.Name AS ProductSubCategory, Product.ProductModelID INTO #Product FROM Production.Product INNER JOIN Production.ProductSubCategory ON ProductSubCategory.ProductSubcategoryID = Product.ProductSubcategoryID INNER JOIN Production.ProductCategory ON ProductCategory.ProductCategoryID = ProductSubCategory.ProductCategoryID ORDER BY Product.ModifiedDate DESC; SELECT Product.ProductID, Product.ProductName, Product.ProductNumber, CostMeasure.UnitMeasureCode, CostMeasure.Name AS CostMeasureName, ProductVendor.AverageLeadTime, ProductVendor.StandardPrice, ProductReview.ReviewerName, ProductReview.Rating, Product.ProductCategory, Product.ProductSubCategory FROM #Product Product INNER JOIN Production.ProductModel ON ProductModel.ProductModelID = Product.ProductModelID LEFT JOIN Production.ProductReview ON ProductReview.ProductID = Product.ProductID LEFT JOIN Purchasing.ProductVendor ON ProductVendor.ProductID = Product.ProductID LEFT JOIN Production.UnitMeasure CostMeasure ON ProductVendor.UnitMeasureCode = CostMeasure.UnitMeasureCode; DROP TABLE #Product;
Это лишь одно из многих возможных решений, но это способ сократить большой, более сложный запрос до двух более простых. В качестве бонуса мы можем пересмотреть таблицы и удалить все ненужные таблицы, столбцы, переменные и все остальное, что возможно не нужно для возврата искомых данных.
Количество таблиц вносит значительный вклад в плохие планы на выполнение, поскольку оно заставляет оптимизатора запросов просеивать большой набор результатов и отбрасывать больше потенциально верных результатов в поисках оптимального плана менее чем за секунду. Если вы проверяете плохо работающий запрос с очень большим количеством таблиц, попробуйте разбить его на более мелкие запросы. Эта тактика не всегда обеспечивает значительное улучшение, но часто оказывается эффективной, когда другие пути уже испробованы, а в одном запросе много таблиц, которые читаются вместе.
Оригинал статьи: https://www.sqlshack.com/query-optimization-techniques-in-sql-server-tips-and-tricks/