Терміни, які роблять запит не SARGable
Це кілька факторів, що роблять запит несумісним з індексами.
1. Використання стовпця всередині функції в WHERE клаузі.
select
s.SalesOrderID,
s.SalesPersonID,
s.OrderDate,
s.SalesOrderNumber
from Sales.SalesOrderHeader s
where YEAR(s.OrderDate) = 2011; -- Кількість рядків: 1607, Кількість прочитаних рядків: 31465
Примітка: Я створив покриваючий індекс на OrderDate
:
create nonclustered index IX_Sale_OrderDate on Sales.SalesOrderHeader(OrderDate)
Include
(
SalesOrderID,
SalesPersonID,
SalesOrderNumber
);
Розглянемо план виконання:
Є всього 1607 рядків для 2011 року, але загальна кількість прочитаних рядків 31465. Давайте поясню, що сталося. OrderDate
має некластеризований індекс, але база даних не оптимізує його, і цей запит буде читати всі рядки з таблиці. Причина в тому, що функція YEAR
оцінюється для кожного рядка, що викликає сканування некластеризованого індексу (сканується весь індекс, який містить всі необхідні стовпці завдяки покриваючому індексу). Якщо ви не розумієте різницю між скануванням і пошуком, я раджу вам ознайомитись з цією статтею.
Ми можемо переписати запит для оптимізації індексу на OrderDate
.
select
s.SalesOrderID,
s.SalesPersonID,
s.OrderDate,
s.SalesOrderNumber
from Sales.SalesOrderHeader s
where s.OrderDate>= '2011-01-01' and s.OrderDate<='2011-12-31'; -- Кількість рядків: 1607, Кількість прочитаних рядків: 1607
Тепер цей запит оптимізує індекс.
2. Використання провідних символів у ключовому слові LIKE
SELECT
b.*
FROM Book b
WHERE b.Title Like '%fairy%';
Запит, визначений вище, не використовує індекс, що визначений на стовпці ‘Title’. Він виконує «сканування індексу» і читає всі рядки. Це відбувається, тому що ми повинні відсканувати всі рядки, де заголовок містить рядок ‘fairy’.
Перепишемо запит:
SELECT
b.*
FROM Book b
WHERE b.Title Like 'fairy%';
Цей запит використовує індекс і читає лише відповідні рядки. Оскільки в таблиці ‘Book’ є лише 1 запис, що починається з заголовка ‘fairy’, буде прочитано лише 1 рядок.
3. Неявні перетворення (з боку таблиці)
Коли ми порівнюємо два значення з різними типами даних, SQL Server перетворює тип даних з нижчим пріоритетом на тип з вищим пріоритетом. Розглянемо приклад нижче (я взяв його з статті mssqltips і використовую базу даних AdventureWorks2022).
-- Примітка: `CardNumber` має тип `nvarchar`, є індекс на `CardNumber`
-- і ми порівнюємо його з цілим числом 11119775847802
SELECT
CreditCardID,
CardNumber
FROM Sales.CreditCard
WHERE CardNumber = 11119775847802;
Тут CardNumber
має тип nvarchar
, а ми порівнюємо його з цілим числом. CardNumber
має індекс. Є лише один запис з CardNumber=11119775847802, тому повинно прочитати лише 1 рядок для отримання цього запису. Але цього не відбувається. База даних SQL Server читає всі рядки, щоб знайти цей запис.
Ви питаєте чому? Дозвольте пояснити.
CardNumber
має тип nvarchar
, а ми шукаємо запис з CardNumber = 11119775847802
, який має тип ціле число. Замість того, щоб перетворити 11119775847802
на nvarchar
, SQL Server перетворює всі рядки CardNumber
на ціле число, що призводить до сканування індексу. Ви розумієте проблему? Нам потрібен лише один запис, у якому CardNumber
дорівнює 11119775847802
, але для цього сканується вся таблиця.
Отже, використовуйте ‘11119775847802’
замість 11119775847802
, що дозволяє уникнути неявного перетворення на стороні таблиці, як показано нижче:
SELECT
CreditCardID,
CardNumber
FROM Sales.CreditCard
WHERE CardNumber = '11119775847802';
Цей запит прочитає лише 1 рядок і ефективно використовуватиме індекс.
Примітка: Іноді SQL Server розумно обробляє неявне перетворення і уникає перетворення на стороні таблиці, як у цьому прикладі:
SELECT
soh.SalesOrderID,
soh.SalesOrderNumber,
soh.ModifiedDate
FROM Sales.SalesOrderHeader soh
WHERE soh.SalesOrderID= '43668';
Запит вище не виконує неявне перетворення на стороні таблиці. Замість того, щоб перетворювати всі SalesOrderId
на nvarchar
, він просто перетворює '43668' на ціле число.
Я протестував кілька запитів, де порівнював значення рядка (наприклад, ‘43668’) з цілими числами (наприклад, SalesOrderID). Що я знайшов, так це те, що SQL Server обробляє це дуже добре і перетворює лише порівнюване значення, а не всі рядки. Це призводить до пошуку індексу.
Отже, іноді оптимізатор SQL також оптимізує процеси.
4. Використання необов’язкового параметра (where column_name=@some_parameter or @some_parameter is NULL)
Я є жертвою цього. Я робив це все своє життя, коли писав сирі SQL запити (хоча більшість часу я використовував EF). І я навіть не усвідомлював, що це не найкращий запит.
Розглянемо приклад:
create or alter procedure GetBooks
@SearchTerm nvarchar(40)=null
as
begin
set nocount on;
select * from Book
where @SearchTerm is null or Title like @SearchTerm+'%';
end
go
-- виконуємо процедуру
exec GetBooks @SearchTerm='The Epic Of Gilgamesh'; -- повертає 1 рядок
Вищезгаданий запит не використовуватиме індекс на Title
і в результаті призведе до index scan
(сканування всіх рядків).
Ми можемо вирішити це двома способами:
(i). Використання динамічного SQL
create or alter procedure GetBooks
@SearchTerm nvarchar(40)=null
as
begin
set nocount on;
declare @SQL nvarchar(max);
set @SQL = N'SELECT * FROM Book WHERE 1 = 1';
if @SearchTerm is not null
begin
set @SearchTerm = @SearchTerm+'%';
set @SQL += N' AND Title LIKE @SearchTerm';
end
exec sp_executesql @SQL, N'@SearchTerm nvarchar(40)', @SearchTerm;
end
go
-- виконуємо процедуру
exec GetBooks @SearchTerm='The Epic Of Gilgamesh'; -- повертає 1 рядок
(ii). Використання OPTION (RECOMPILE)
create or alter procedure GetBooks
@SearchTerm nvarchar(40)=null
as
begin
set nocount on;
select * from Book
where @SearchTerm is null or Title like @SearchTerm+'%'
option (RECOMPILE);
end
-- виконуємо процедуру
exec GetBooks @SearchTerm='The Epic Of Gilgamesh'; -- повертає 1 рядок
RECOMPILE
може збільшити використання CPU. У більшості випадків це не буде великою проблемою. Якщо це так, то слід розглянути використання динамічного SQL. Але спочатку зверніть увагу на RECOMPILE. Якщо запит має довгий час компіляції і часто виконується, тоді можна використовувати динамічний SQL. Це детально пояснюється в статті Gail, рекомендую ознайомитись, вона має чудову глибину.
Використання OR в умовах WHERE
SELECT
SalesOrderID,
OrderDate,
SalesOrderNumber,
CustomerID
FROM
Sales.SalesOrderHeader
WHERE
CustomerID= 29642
OR
OrderDate < '2011-10-31';
У таблиці Sales.SalesOrderHeader
, стовпець CustomerId
є первинним ключем. І я створив покритий індекс для OrderDate (SalesOrderID, CustomerID)
.
CREATE NONCLUSTERED INDEX IX_SalesOrderHeader_OrderDate_Covering
ON Sales.SalesOrderHeader (OrderDate)
INCLUDE (SalesOrderID, CustomerID)
План виконання:
Цей запит шукає два різні умови і в кінцевому підсумку призводить до сканування індексу.
👉 Одне з рішень — використовувати підказку FORCESEEK
, як згадано тут:
SELECT
SalesOrderID,
OrderDate,
SalesOrderNumber,
CustomerID
FROM
Sales.SalesOrderHeader
WITH (FORCESEEK)
WHERE
CustomerID= 29642
OR
OrderDate < '2011-10-31';
👉 Інше рішення — використовувати union
:
SELECT
SalesOrderID,
OrderDate,
SalesOrderNumber,
CustomerID
FROM
Sales.SalesOrderHeader
WHERE CustomerID= 29642
UNION
SELECT
SalesOrderID,
OrderDate,
SalesOrderNumber,
CustomerID
FROM
Sales.SalesOrderHeader
WHERE OrderDate < '2011-10-31';
Ще одна річ
Це не зовсім стосується індексу, але має відношення до продуктивності. Не використовуйте OR разом з JOIN. Особисто я ніколи не стикався з цією ситуацією, але я знайшов її тут під час дослідження для цієї статті. Я змоделював ситуацію і представляю її вам.
SELECT DISTINCT c.name, o.order_id, o.amount, o.status
FROM customers c
LEFT JOIN orders o
ON c.id = o.customer_id
OR c.email = o.customer_email
План виконання:
Статистика:
Цей запит виконався дуже повільно, навіть при тому, що в кожній таблиці лише 10 тис. записів.
Ми можемо вирішити цю ситуацію за допомогою union
.
SELECT c.name, o.order_id, o.amount, o.status
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
UNION
SELECT c.name, o.order_id, o.amount, o.status
FROM customers c
left JOIN orders o ON c.email = o.customer_email
Цей запит виконався значно швидше і з меншим числом логічних зчитувань.
Підсумок
Жоден із цих пунктів не призводить до сканування таблиці завжди. Це трапляється лише тоді, коли у вас великий набір даних (я маю на увазі тисячі записів). Оптимізатор SQL Server досить розумний, щоб оптимізувати запити (ось чому його і найняли). Але завжди корисно слідувати цим рекомендаціям.
Підсумуємо, не робіть наступне:
- where somefunction(somecolumn)= some_value.
- where somecolumn like %searchstring%
- where somenvarcharcolumn= integer_value
- where columnname=@someparameter or @some_parameter is NULL
- where condition1 OR condition2;
Джерела
Ось список статей, які я використовував під час свого дослідження. Вони чудові і надають глибоке розуміння. Якщо ви хочете зануритися глибше, обов'язково ознайомтесь з ними.
- Non-SARGable Predicates (by Brent Ozar)
- SQL Server Implicit Conversions Performance Issues (by K.)
Браян Келлі на mssqltips.com)](https://www.mssqltips.com/sqlservertip/6207/sql-server-implicit-conversions-performance-issues/) - Коли продуктивність SQL Server погіршується: неявні перетворення (Філ Фактор на red-gate.com)
- SQL Server Index Scan при використанні оператора 'OR' (dba.stackexchange.com)
- Єдине, що гірше за необов'язкові параметри для продуктивності запитів SQL Server (Ерік Дарлінг на erikdarling.com)
- Перегляд всіх запитів (Гейл на sqlinthewild.co.za)
- Проблеми з продуктивністю при використанні OR в JOIN (Ерік Кобб на sqlnuggets.com)
Будь ласка, натисніть «аплодувати» та поділіться, якщо цей пост був корисним.
Зв'яжіться зі мною
Перекладено з: What makes your query Non-SARGable