Почему мои индексы SQL игнорируются?

У нас возникла проблема, когда индексы в наших таблицах игнорируются, а SQL Server 2000 вместо этого выполняет сканирование таблиц. Мы можем принудительно использовать индексы с помощью предложения WITH (INDEX=<index_name>), но предпочли бы не делать этого.

Как разработчик я хорошо знаком с SQL Server при написании T-SQL, но профилирование и настройка производительности не являются моей сильной стороной. Я ищу любые советы и рекомендации относительно того, почему это может происходить.

Обновление:

Я должен был сказать, что мы перестроили все индексы и обновили статистику индексов.

Определение таблицы для одного из виновников выглядит следующим образом:

CREATE TABLE [tblinvoices]
(
    [CustomerID] [int] NOT NULL,
    [InvoiceNo] [int] NOT NULL,
    [InvoiceDate] [smalldatetime] NOT NULL,
    [InvoiceTotal] [numeric](18, 2) NOT NULL,
    [AmountPaid] [numeric](18, 2) NULL 
        CONSTRAINT [DF_tblinvoices_AmountPaid]  DEFAULT (0),
    [DateEntered] [smalldatetime] NULL 
        CONSTRAINT [DF_tblinvoices_DateEntered]  DEFAULT (getdate()),
    [PaymentRef] [varchar](110),
    [PaymentType] [varchar](10),
    [SyncStatus] [int] NULL,
    [PeriodStart] [smalldatetime] NULL,
    [DateIssued] [smalldatetime] NULL 
        CONSTRAINT [DF_tblinvoices_dateissued]  DEFAULT (getdate()),
    CONSTRAINT [PK_tblinvoices] PRIMARY KEY NONCLUSTERED 
    (
        [InvoiceNo] ASC
    ) ON [PRIMARY]
) ON [PRIMARY]

В этой таблице есть еще один индекс (тот, который мы хотим использовать в SQL):

CustomerID (Non-Unique, Non-Clustered)

Следующий запрос выполняет сканирование таблицы вместо использования индекса CustomerID:

SELECT 
    CustomerID, 
    Sum(InvoiceTotal) AS SumOfInvoiceTotal, 
    Sum(AmountPaid) AS SumOfAmountPaid 
FROM tblInvoices 
WHERE CustomerID = 2112 
GROUP BY customerID

Обновлено:

В ответ на вопрос автократии оба эти запроса выполнить сканирование таблицы.

Обновлено:

В ответ на вопрос Quassnoi о DBCC SHOW_STATISTICS данные является:

RANGE_HI_KEY    RANGE_ROWS    EQ_ROWS    DISTINCT_RANGE_ROWS    AVG_RANGE_ROWS
1667            246           454        8                      27.33333
2112            911           3427       16                     56.9375
2133            914           775        16                     57.125

person Kev    schedule 09.06.2009    source источник
comment
Можете ли вы дать нам образец запроса и соответствующие индексы?   -  person Eric    schedule 09.06.2009
comment
опубликуйте определение таблицы (таблиц) и запрос, а также какие индексы у вас есть.   -  person Mitch Wheat    schedule 09.06.2009
comment
Я обновил образец таблицы и запрос, который вызывает у нас проблемы.   -  person Kev    schedule 09.06.2009
comment
Сколько строк в этой таблице??   -  person marc_s    schedule 09.06.2009
comment
И сколько строк выбирается для этого конкретного клиента? Если вы выберете слишком много строк, SQL Server будет дешевле выполнить полное сканирование таблицы, чем пытаться выбрать строки из таблицы.   -  person marc_s    schedule 09.06.2009
comment
@Mitch - удаление GROUP BY вызывает ошибку, потому что CustomerID «не содержится в агрегатной функции и нет предложения GROUP BY»   -  person Kev    schedule 09.06.2009
comment
@Marc_s - для этого CustomerID выбрано около 3400 строк   -  person Kev    schedule 09.06.2009
comment
Почему в таблице нет кластеризованного индекса?   -  person Eric Sabine    schedule 09.06.2009
comment
@esabine - эта таблица является устаревшей таблицей, созданной до того, как я начал работать в компании, поэтому не уверен в ее истории и т. Д.   -  person Kev    schedule 09.06.2009


Ответы (12)


Лучше всего сделать индекс покрывающим индексом, включив столбцы InvoiceTotal и AmountPaid в индекс CustomerID. (В SQL 2005 вы должны добавить их как "включенные" столбцы. В SQL 2000 вы должны добавить их как дополнительные ключевые столбцы.) Если вы это сделаете, я гарантирую оптимизатор запросов выберет ваш индекс*.

Объяснение. Индексы кажутся полезными всегда, но использование (непокрывающего) индекса сопряжено с дополнительными затратами, а именно с "поиском закладок". необходимо выполнить для извлечения любых других столбцов, которые могут понадобиться из основной таблицы. Этот поиск закладок является дорогостоящей операцией и является (одной из возможных) причиной, по которой оптимизатор запросов может не использовать ваш индекс.

Включая все необходимые столбцы в сам индекс, этот поиск закладок полностью избегается, и оптимизатору не нужно играть в эту маленькую игру, выясняя, «стоит ли» использование индекса.

(*) Или я верну вам баллы StackOverflow. Просто отправьте конверт с обратным адресом и маркой на...

Изменить: Да, если ваш первичный ключ НЕ является кластеризованным индексом, то, во что бы то ни стало, сделайте это тоже!! Но даже с этим изменением превращение вашего индекса CustomerID в покрывающий индекс должно повысить производительность на порядок (в 10 раз или лучше)!!

person BradC    schedule 09.06.2009
comment
Очень верно. Если это регулярно выполняемый запрос, пространство, используемое для этого большего индекса, окупится с лихвой. Кроме того, все, что ищет CustomerId, по-прежнему сможет использовать этот индекс, так что у вас не будет двух из них, блуждающих вокруг. +1 - person Jeff Ferland; 09.06.2009
comment
Брэд, мой администратор базы данных говорит, что наличие покрывающего индекса значительно улучшило его жизнь. Посмотрим, добавит ли кластеризованный индекс какие-либо преимущества. - person Kev; 09.06.2009
comment
Хотел бы я дать больше +15 всем, кто помог здесь. - person Kev; 09.06.2009
comment
Рад, что это помогло. Хотелось бы услышать, какое увеличение производительности дал ваш запрос. - person BradC; 09.06.2009
comment
Последний комментарий от администратора базы данных заключался в том, что мы перешли от 68000 операций чтения к 101, а время ЦП с 1047 мс до 15 мс (я предполагаю, что они получены из данных SQL Profiler - я не видел исходных данных плана выполнения). - person Kev; 09.06.2009
comment
Эй, я сказал это! Хотя не так хорошо. - person NeedHack; 10.06.2009

У нас возникла проблема, когда индексы в наших таблицах игнорируются, а SQL Server 2000 вместо этого выполняет сканирование таблиц.

Несмотря на то, что с Aug 29, 1997 прошло 4,302 дня, оптимизатор SQL Server еще не превратился в SkyNet и все еще может принимать неверные решения.

Индексные подсказки — это то, как вы, человек, помогаете искусственному интеллекту.

Если вы уверены, что собрали статистику, а оптимизатор все равно ошибается, то продолжайте, пользуйтесь подсказками.

Они законны, правильны, задокументированы и поддерживаются Microsoft способом обеспечения выполнения плана запроса, который вы хотите.

В твоем случае:

SELECT CustomerID, 
       SUM(InvoiceTotal) AS SumOfInvoiceTotal, 
       SUM(AmountPaid) AS SumOfAmountPaid 
FROM   tblInvoices 
WHERE  CustomerID = 2112 
GROUP BY
       CustomerID

, у оптимизатора есть два варианта:

  • Используйте индекс, который подразумевает вложенный цикл по индексу вместе с KEY LOOKUP для получения значений InvoiceTotal и AmountPaid.
  • Не используйте индекс и сканируйте все строки таблиц, что быстрее в rows fetched per second, но дольше с точки зрения общего количества строк.

Первый метод может быть или не быть быстрее, чем второй.

Оптимизатор пытается оценить, какой метод быстрее, просматривая статистику, которая сохраняет селективность индекса наряду с другими значениями.

Для выборочных индексов первый метод быстрее; для неизбирательных - последнее.

Не могли бы вы запустить этот запрос:

SELECT  1 - CAST(COUNT(NULLIF(CustomerID, 2112)) AS FLOAT) / COUNT(*)
FROM    tlbInvoices

Обновление:

Поскольку CustomerID = 2112 охватывает только 1,4% ваших строк, вы должны извлечь выгоду из использования индекса.

Теперь, не могли бы вы выполнить следующий запрос:

DBCC SHOW_STATISTICS ([tblinvoices], [CustomerID])

, найти две соседние строки в третьем наборе результатов, где RANGE_HI_KEY меньше и больше 2112, и опубликовать строки здесь?

Обновление 2:

Поскольку статистика вроде бы правильная, остается только догадываться, почему оптимизатор выбрал в данном случае полное сканирование таблицы.

Возможно (вероятно) это потому, что это самое значение (2112) встречается в RANGE_HI_KEY и оптимизатор видит, что оно необычно плотное (3427 значений только для 2112 против только 911 для всего диапазона от 1668 до 2111)

Не могли бы вы сделать еще две вещи:

  1. Запустите этот запрос:

    DBCC SHOW_STATISTICS ([tblinvoices], [CustomerID])
    

    и опубликуйте первые два набора результатов.

    • Запустите этот запрос:

      ВЫБЕРИТЕ TOP 1 CustomerID, COUNT (*) FROM tblinvoices, ГДЕ CustomerID МЕЖДУ 1668 И 2111

    , используйте верхний CustomerID из приведенного выше запроса в исходном запросе:

    SELECT CustomerID, 
           SUM(InvoiceTotal) AS SumOfInvoiceTotal, 
           SUM(AmountPaid) AS SumOfAmountPaid 
    FROM   tblInvoices 
    WHERE  CustomerID = @Top_Customer
    GROUP BY
           CustomerID
    

    и посмотрите, какой план он сгенерирует.

person Quassnoi    schedule 09.06.2009
comment
Подсказки индекса должны быть абсолютно последним средством и использоваться только в исключительных обстоятельствах. - person Mitch Wheat; 09.06.2009
comment
Это не Скайнет, но даже на 2000 очень хорошо. Моя ставка была бы на отсутствующую статистику - person erikkallen; 09.06.2009
comment
@Mitch: Я правильно понимаю, оптимизатор никогда не ошибается? - person Quassnoi; 09.06.2009
comment
@Quassnoi: я не говорю, что это НИКОГДА не так; просто редко. Индексные подсказки следует использовать в крайнем случае, ИМО. - person Mitch Wheat; 09.06.2009
comment
@Mitch: вы просто скажите мне: можно ли использовать подсказки, когда оптимизатор ошибается или нет? - person Quassnoi; 09.06.2009
comment
@Quassnoi: в этом случае мы точно не знаем, ошибается оптимизатор или нет (и я бы поставил на НЕ), поэтому подсказки индекса не подходят (пока) - person Mitch Wheat; 09.06.2009
comment
+1 за ссылку на SkyNet и за подсчет количества дней. Подождите, вы использовали для этого календарную таблицу, верно? - person Eric Sabine; 09.06.2009
comment
@esabine: SELECT DATEDIFF(SYSDATE(), CAST('1997-08-29' КАК ДАТА)) - person Quassnoi; 09.06.2009
comment
@Quassnoi: в SQL Server подсказки индекса должны быть последним средством. - person Mitch Wheat; 09.06.2009
comment
@Quassnoi: если, конечно, вы не знаете избирательность ваших данных, FK и индексов и не знаете, что они вряд ли изменятся быстро, то, возможно, подсказки INDEX - это то, что вам нужно, но это эксперт (не разработчик) области (не в обиду разработчикам, я один из них!) - person Mitch Wheat; 09.06.2009
comment
@Quassnio: результат - 0,014857559059556 - person Kev; 09.06.2009
comment
@Quassnio: я добавил эти данные к своему вопросу, потому что комментарии не очень хорошо форматируются. С благодарностью, Кев. - person Kev; 09.06.2009
comment
@Quassnoi - извините за неправильное написание вашего имени, приятель :). Похоже, проблема связана с а) отсутствием кластеризованного индекса или б) покрывающим индексом, как в ответе Брэда. На данный момент мы попробовали покрывающий индекс, и производительность значительно улучшилась. Я оставлю своего администратора баз данных для создания кластеризованного индекса в спокойное время на этой неделе. Большое спасибо за все время, которое вы потратили на это. Я хотел бы дать правильные ответы всем, потому что каждая мелочь помогла. Опять же, очень ценится. - person Kev; 09.06.2009

Наиболее распространенными причинами игнорирования индексов являются:

  • Задействованные столбцы недостаточно избирательны (оптимизатор решает, что сканирование таблиц будет быстрее из-за «посещения» большого количества строк)

  • В SELECT/GROUP BY/ORDER BY задействовано большое количество столбцов, и после использования индекса потребуется поиск в кластеризованном индексе.

  • Статистика устарела (или искажена большим количеством вставок или удалений)

Выполняется ли у вас регулярное задание по обслуживанию индекса? (довольно часто его отсутствие в среде разработки).

person Mitch Wheat    schedule 09.06.2009
comment
+1 за избирательность - только если индекс будет выбирать примерно менее 1-5% для данного значения, он действительно будет использоваться. Например. размещение индекса в столбце пола или столбце битов на самом деле не имеет большого смысла. - person marc_s; 09.06.2009
comment
Я думаю, что эмпирическое правило для SQL Server составляет 10% (приблизительно). - person Mitch Wheat; 09.06.2009
comment
Вокруг плавают разные числа - это определенно не работает с 50% избирательностью! (например, пол) - person marc_s; 09.06.2009
comment
Также зависит от размера строк — подробности см. здесь: sqlskills.com /BLOGS/KIMBERLY/category/Nonclustered-Indexes.aspx - person marc_s; 09.06.2009
comment
@marc_s: хорошая статья. Мне действительно нужно снова начать читать блог Кимберли. - person Mitch Wheat; 09.06.2009
comment
@Mitch - tblInvoices растет примерно на 100-150 строк в день. Он имеет ~ 260 000 строк. Основная часть строк добавляется при ежедневном выставлении счетов, остальные (скажем, 30%) добавляются в течение рабочего дня. Не уверен, что это поможет. - person Kev; 09.06.2009

Последний пост Кимберли как раз посвящен этой теме: http://www.sqlskills.com/BLOGS/KIMBERLY/post/The-Tipping-Point-Query-Answers.aspx

SQL Server использует оптимизатор, основанный на затратах, и если оптимизатор вычисляет, что стоимость поиска ключей индекса, а затем поиска кластеризованного индекса для извлечения остальных столбцов выше, чем стоимость сканирования table, то вместо этого он будет сканировать таблицу. Точка «перелома» на самом деле удивительно низка.

person Remus Rusanu    schedule 09.06.2009

Вы пытались добавить другие столбцы в свой индекс? то есть InvoiceTotal и AmountPaid.

Идея состоит в том, что запрос будет «покрыт» индексом, и ему не нужно будет обращаться к таблице.

person NeedHack    schedule 09.06.2009

Я бы начал тестирование, чтобы увидеть, можете ли вы изменить первичный ключ на кластеризованный индекс. Прямо сейчас таблица считается "кучей". Если вы не можете этого сделать, я бы также подумал о создании представления с кластеризованным индексом, но сначала вам нужно изменить столбец «AmountPaid» на NOT NULL. По умолчанию он уже равен нулю, поэтому это может быть легко изменить. Для представления я бы попробовал что-то похожее на это.

SET QUOTED_IDENTIFIER, ANSI_NULLS, ANSI_PADDING, ANSI_WARNINGS, ARITHABORT, CONCAT_NULL_YIELDS_NULL, QUOTED_IDENTIFIER ON
GO
SET NUMERIC_ROUNDABORT OFF
GO

IF EXISTS 
  (
         SELECT TABLE_NAME
           FROM INFORMATION_SCHEMA.VIEWS 
          WHERE TABLE_NAME = N'CustomerInvoiceSummary'
  )
           DROP VIEW dbo.CustomerInvoiceSummary
GO

CREATE VIEW dbo.CustomerInvoiceSummary WITH SCHEMABINDING
AS

  SELECT a.CustomerID
       , Sum(a.InvoiceTotal) AS SumOfInvoiceTotal
       , Sum(a.AmountPaid)   AS SumOfAmountPaid 
       , COUNT_BIG(*)                     AS CT
    FROM dbo.tblInvoices a
GROUP BY a.CustomerID

GO
CREATE UNIQUE CLUSTERED INDEX CustomerInvoiceSummary_CLI ON dbo.CustomerInvoiceSummary ( CustomerID )
GO
person Eric Sabine    schedule 09.06.2009
comment
Эсабина - большое спасибо за вклад и время, потраченное на это. Я собираюсь согласиться с ответом BradC, поскольку индекс покрытия исправил для нас много ошибок. - person Kev; 09.06.2009
comment
Приятно слышать, что у вас есть потенциальное решение, и спасибо за отзыв. Только что понял - то, что я не упомянул выше, - это то, что когда вы запускаете исходный запрос, оптимизатор рассмотрит возможность использования индекса в представлении (CustomerInvoiceSummary_CLI), даже если вы на самом деле не запрашиваете представление напрямую. - person Eric Sabine; 10.06.2009

Я думаю, что я только что нашел его. Я читал комментарии, опубликованные на ваш вопрос, прежде чем заметил, что два запроса, которые я вам дал, должны были вызвать сканирование таблицы, и мне просто нужен был результат. Тем не менее, меня заинтересовало, когда кто-то сказал, что у вас нет кластеризованных индексов. Я подробно прочитал ваш оператор создания SQL и с удивлением заметил, что это так. Вот почему он не использует ваш индекс CustomerId.

Ваш индекс CustomerId ссылается на ваш первичный ключ InvoiceNo. Ваш первичный ключ, однако, не кластеризован, поэтому вам придется заглянуть в этот индекс, чтобы найти, где на самом деле находится строка. Сервер SQL не будет выполнять два поиска по некластеризованному индексу, чтобы найти строку. Это будет просто сканирование таблицы.

Сделайте свой InvoiceNo кластеризованным индексом. Мы можем предположить, что они, как правило, будут вставляться в порядке возрастания, и, таким образом, стоимость вставки не будет намного выше. Однако стоимость вашего запроса будет намного ниже. Доллары за пончики, тогда он будет использовать ваш индекс.


Изменить: мне также нравится предложение BradC. Это обычная уловка DBA. Однако, как он говорит, сделайте этот первичный кластер в любом случае, поскольку это ПРИЧИНА вашей проблемы. Таблица без кластеризованного индекса встречается очень редко. Большую часть времени он не используется, это плохая идея. Тем не менее, его покрывающий индекс является улучшением НАВЕРХ кластеризации, которое следует сделать.

person Jeff Ferland    schedule 09.06.2009

Несколько других указали, что вашей базе данных может потребоваться обновление статистики индекса. У вас также может быть такой высокий процент строк в базе данных, что будет быстрее последовательно читать таблицу, чем искать каждую по диску. SQL Server имеет причудливый анализатор запросов с графическим интерфейсом, который сообщит вам, какова, по мнению базы данных, стоимость различных действий. Вы можете открыть его и увидеть, о чем он думал.

Мы можем дать вам более основательные ответы, если вы дадите нам:

Select * from tblinvoices;
Select * from tblinvoices where CustomerID = 2112;

Используйте этот анализатор запросов и обновите свою статистику. Последний совет: вы можете использовать подсказки индекса, чтобы заставить его использовать ваш индекс, если вы уверены, что это просто глупо после того, как вы сделали все остальное.

person Jeff Ferland    schedule 09.06.2009

Ты пытался

exec sp_recompile tblInvoices

... просто чтобы убедиться, что вы не используете кэшированный плохой план?

person SqlACID    schedule 09.06.2009

Вы также можете попробовать выполнить UPDATE STATISTICS для таблицы (или таблиц), участвующих в запросе. Не то чтобы я полностью разбирался в статистике в SQL, но я знаю, что иногда наши администраторы баз данных делают это (с запланированным еженедельным заданием по обновлению статистики в больших и часто изменяемых таблицах).

Статистика SQL

person Tim C    schedule 09.06.2009

Попробуйте обновить статистику. Эти статистические данные являются основой для решений, принимаемых компилятором о том, следует ли ему использовать индекс или нет. Они содержат такую ​​информацию, как кардинальность и количество строк для каждой таблицы.

Например, если статистика не обновлялась с тех пор, как вы выполнили большой объемный импорт, компилятор может по-прежнему считать, что в таблице всего 10 строк, и не беспокоиться об индексе.

person D'Arcy Rittich    schedule 09.06.2009

Вы используете "SELECT * FROM..."? Обычно это приводит к сканированию.

Нам понадобятся схема, индексы и образцы запросов, чтобы помочь больше

person gbn    schedule 09.06.2009