Ошибка производительности индекса даты и времени в SQL Server 2008

11

Мы используем SQL Server 2008 R2, и у нас есть очень большая (100M + строки) таблица с индексом первичного идентификатора и datetimeстолбец с некластеризованным индексом. Мы наблюдаем довольно необычное поведение клиент / сервер, основанное на использовании order byпредложения специально для индексированного столбца даты и времени .

Я прочитал следующий пост: /programming/1716798/sql-server-2008-ordering-by-datetime-is-too-slow, но с клиентом / сервером происходит больше, чем то, что есть начать описано здесь.

Если мы запустим следующий запрос (отредактированный для защиты некоторого содержимого):

select * 
from [big table] 
where serial_number = [some number] 
order by test_date desc

Время ожидания запроса каждый раз. В SQL Server Profiler выполненный запрос выглядит на сервере следующим образом:

exec sp_cursorprepexec @p1 output,@p2 output,NULL,N'select * .....

Теперь, если вы измените запрос на, скажите это:

declare @temp int;
select * from [big table] 
where serial_number = [some number] 
order by test_date desc

Профилировщик SQL Server показывает, что выполненный запрос выглядит на сервере следующим образом, и он работает мгновенно:

exec sp_prepexec @p1 output, NULL, N'declare @temp int;select * from .....

На самом деле, вы можете даже поставить пустой комментарий ('-;') вместо неиспользованного оператора объявления и получить тот же результат. Итак, изначально мы указывали на препроцессор sp в качестве основной причины этой проблемы, но если вы сделаете это:

select * 
from [big table] 
where serial_number = [some number] 
order by Cast(test_date as smalldatetime) desc

Он также работает мгновенно (вы можете привести его как любой другой datetimeтип), возвращая результат в миллисекундах. А профилировщик показывает запрос к серверу как:

exec sp_cursorprepexec @p1 output, @p2 output, NULL, N'select * from .....

Так что это несколько исключает sp_cursorprepexecпроцедуру из полной причины проблемы. Добавьте к этому тот факт, что также sp_cursorprepexecвызывается, когда не используется 'order by', и результат также мгновенно возвращается.

Мы довольно много гуглили по этой проблеме, и я вижу похожие проблемы, опубликованные другими, но ни одна из них не приводит к этому уровню.

Так другие были свидетелями этого поведения? У кого-нибудь есть решение лучше, чем помещать бессмысленный SQL перед оператором select, чтобы изменить поведение? Поскольку SQL Server должен вызывать порядок после того, как данные собраны, кажется, что это ошибка на сервере, которая сохраняется в течение длительного времени. Мы обнаружили, что это поведение согласованно во многих наших больших таблицах и воспроизводимо.

Редактирование:

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

Я должен добавить, чтобы помочь поисковикам, выдана ошибка тайм-аута ODBC: [Microsoft] [Драйвер ODBC SQL Server] Операция отменена

Добавлено 12.10.2012: Все еще выискивая основную причину (наряду с созданием образца для передачи в Microsoft, я буду отправлять здесь любые результаты после отправки). Я копался в файле трассировки ODBC между рабочим запросом (с добавленным оператором комментария / объявления) и нерабочим запросом. Фундаментальная разница трасс приведена ниже. Это происходит при вызове вызова SQLExtendedFetch после завершения всех обсуждений SQLBindCol. Вызов завершается неудачно с кодом возврата -1, и родительский поток затем вводит SQLCancel. Так как мы можем производить это как с Native Client, так и с устаревшими драйверами ODBC, я все еще указываю на некоторые проблемы совместимости на стороне сервера.

(clip)
MSSQLODBCTester 1664-1718   EXIT  SQLBindCol  with return code 0 (SQL_SUCCESS)
        HSTMT               0x001EEA10
        UWORD                       16 
        SWORD                        1 <SQL_C_CHAR>
        PTR                0x03259030
        SQLLEN                    51
        SQLLEN *            0x0326B820 (0)

MSSQLODBCTester 1664-1718   ENTER SQLExtendedFetch 
        HSTMT               0x001EEA10
        UWORD                        1 <SQL_FETCH_NEXT>
        SQLLEN                     1
        SQLULEN *           0x032677C4
        UWORD *             0x032679B0

MSSQLODBCTester 1664-1fd0   ENTER SQLCancel 
        HSTMT               0x001EEA10

MSSQLODBCTester 1664-1718   EXIT  SQLExtendedFetch  with return code -1 (SQL_ERROR)
        HSTMT               0x001EEA10
        UWORD                        1 <SQL_FETCH_NEXT>
        SQLLEN                     1
        SQLULEN *           0x032677C4
        UWORD *             0x032679B0

        DIAG [S1008] [Microsoft][ODBC SQL Server Driver]Operation canceled (0) 

MSSQLODBCTester 1664-1fd0   EXIT  SQLCancel  with return code 0 (SQL_SUCCESS)
        HSTMT               0x001EEA10

MSSQLODBCTester 1664-1718   ENTER SQLErrorW 
        HENV                0x001E7238
        HDBC                0x001E7B30
        HSTMT               0x001EEA10
        WCHAR *             0x08BFFC5C
        SDWORD *            0x08BFFF08
        WCHAR *             0x08BFF85C 
        SWORD                      511 
        SWORD *             0x08BFFEE6

MSSQLODBCTester 1664-1718   EXIT  SQLErrorW  with return code 0 (SQL_SUCCESS)
        HENV                0x001E7238
        HDBC                0x001E7B30
        HSTMT               0x001EEA10
        WCHAR *             0x08BFFC5C [       5] "S1008"
        SDWORD *            0x08BFFF08 (0)
        WCHAR *             0x08BFF85C [      53] "[Microsoft][ODBC SQL Server Driver]Operation canceled"
        SWORD                      511 
        SWORD *             0x08BFFEE6 (53)

MSSQLODBCTester 1664-1718   ENTER SQLErrorW 
        HENV                0x001E7238
        HDBC                0x001E7B30
        HSTMT               0x001EEA10
        WCHAR *             0x08BFFC5C
        SDWORD *            0x08BFFF08
        WCHAR *             0x08BFF85C 
        SWORD                      511 
        SWORD *             0x08BFFEE6

MSSQLODBCTester 1664-1718   EXIT  SQLErrorW  with return code 100 (SQL_NO_DATA_FOUND)
        HENV                0x001E7238
        HDBC                0x001E7B30
        HSTMT               0x001EEA10
        WCHAR *             0x08BFFC5C
        SDWORD *            0x08BFFF08
        WCHAR *             0x08BFF85C 
        SWORD                      511 
        SWORD *             0x08BFFEE6
(clip)

Добавлен кейс Microsoft Connect 10/12/2012:

https://connect.microsoft.com/SQLServer/feedback/details/767196/order-by-datetime-in-odbc-fails-for-clean-sql-statements#details

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

DBtheDBA
источник
Что произойдет, если вы попробуете select id, test_date from [big table] where serial_number = ..... order by test_date- мне просто интересно, если SELECT *это негативно влияет на вашу производительность. Если у вас есть некластеризованный индекс test_dateи кластеризованный индекс id(при условии, что он так и называется), этот запрос должен быть покрыт этим некластеризованным индексом и, следовательно, должен возвращаться довольно быстро
marc_s
Извините, хорошая мысль. Я должен был включить, что мы попытались изменить выделенное пространство столбца (удалив '*' и т. Д.) С помощью различных комбинаций. Поведение, описанное выше, сохранялось благодаря этим изменениям.
DBtheDBA
Я связал свои аккаунты с этим сайтом. Если модератор хочет переместить сообщение на этот сайт, я в порядке в любом случае. Один из моих разработчиков указал мне на этот сайт после того, как я разместил его здесь.
DBtheDBA
Какой клиентский стек используется здесь? Без всего текста трассировки это похоже на проблему. Попробуйте обернуть оригинальный вызов внутрь sp_executesqlи посмотрите, что произойдет.
Джон Зигель
1
Как выглядит план медленного выполнения? Параметр нюхает?
Мартин Смит

Ответы:

6

В этом нет ничего загадочного, вы получаете хороший (ошибочный) или (действительно) плохой план, в основном случайный, потому что не существует четкого выбора для использования индекса. Хотя для предложения ORDER BY и, таким образом, избегается сортировка, не кластеризованный индекс для столбца datetime является очень плохим выбором для этого запроса. То, что сделало бы намного лучший индекс для этого запроса, было бы одним на (serial_number, test_date). Более того, это было бы очень хорошим кандидатом для ключа кластеризованного индекса.

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

Ремус Русану
источник
Я немного запутался здесь. Почему план будет основан на the orderпункте? Не должен ли план ограничивать себя whereусловиями, так как упорядочение должно происходить только после извлечения строк? Зачем серверу пытаться отсортировать записи до полного набора результатов?
DBtheDBA
5
Это также не объясняет, почему добавление комментария в начале запроса влияет на продолжительность выполнения.
cfradenburg
Кроме того, наши таблицы почти всегда запрашиваются по серийному номеру, а не по test_date. У нас есть некластеризованные индексы на обоих, а кластеризованные только на столбце id в таблице. Это оперативное хранилище данных, и добавление кластерных индексов в другие столбцы приведет только к расщеплению страниц и снижению производительности.
DBtheDBA
1
@DBtheDBA: если вы хотите подать заявку на «ошибку», вам необходимо провести надлежащее расследование и раскрыть информацию. Точная схема вашей таблицы и экспортируемые статистику, следовать Как создать сценарий необходимых метаданных базы данных для создания базы данных статистики только для в SQL Server 2005 и SQL Server 2008 , в частности , все важные Script Статистика : Скрипт статистики и гистограммы . Добавьте их к информации поста вместе с шагами, которые воспроизводят проблему.
Ремус Русану
1
Мы читали об этом раньше во время наших поисков, и я понимаю, что вы говорите, но есть существенный недостаток в том, что сервер делает здесь. Мы перестроили таблицу и индексы и воспроизвели ее на новой таблице. Опция перекомпиляции не решает проблему, что является большой подсказкой, что что-то не так. Я не сомневаюсь, что кластеризованные индексы могут решить эту проблему, но это не решение основной причины, это обходной путь и дорогостоящий на большой таблице.
DBtheDBA
0

Документируйте детали того, как воспроизвести ошибку, и отправьте ее на connect.microsoft.com. Я проверил и не мог видеть ничего там, что будет связано с этим.

cfradenburg
источник
Завтра я заставлю своего администратора базы данных напечатать сценарий, чтобы создать среду для воспроизведения. Я не думаю, что это так сложно. Я опубликую это здесь также, если кто-то будет заинтересован попробовать это самостоятельно.
DBtheDBA
Опубликовать элемент подключения тоже, когда он открывается. Таким образом, если кто-то еще имеет эту проблему, они получают право на нее. Любой, кто наблюдает за этим вопросом, может захотеть проголосовать за этот пункт, поэтому Microsoft, скорее всего, обратит на него внимание.
cfradenburg
0

Моя гипотеза состоит в том, что вы работаете с кэшем плана запросов. (Ремус может говорить то же самое, что и я, но по-другому.)

Вот масса деталей о том, как SQL планирует кэширование .

Затенение деталей: кто-то выполнил этот запрос ранее, для определенного [некоторого числа]. SQL посмотрел на предоставленное значение, индексы и статистику для соответствующей таблицы / столбцов и т. Д. И создал план, который хорошо работал для этого конкретного [некоторого числа]. Затем он кэшировал план, запускал его и возвращал результаты вызывающей стороне.

Позже кто-то другой выполняет тот же запрос, для другого значения [некоторое число]. Это конкретное значение приводит к сильно различному количеству строк результатов, и механизм должен создать другой план для этого экземпляра запроса. Но это не работает таким образом. Вместо этого SQL принимает запрос и (более или менее) выполняет поиск в кеше запроса с учетом регистра в поисках уже существующей версии запроса. Когда он находит тот из ранее, он просто использует этот план.

Идея состоит в том, что это экономит время, необходимое для определения плана и его построения. Дыра в идее заключается в том, что один и тот же запрос выполняется со значениями, которые дают совершенно разные результаты. У них должны быть разные планы, но они этого не делают. Кто бы ни запускал запрос первым, он устанавливает поведение для всех, кто запускает его потом.

Быстрый пример: выберите * из [people], где lastname = 'SMITH' - очень популярная фамилия в США. GO выберите * из [people], где lastname = 'BONAPARTE' - НЕ популярная фамилия в США.

При выполнении запроса для BONAPARTE план, созданный для SMITH, будет использован повторно. Если SMITH вызвал сканирование таблицы (что может быть хорошо , если строки в таблице 99% SMITH), то BONAPARTE также получит сканирование таблицы. Если BONAPARTE был запущен до SMITH, план, использующий индекс, мог бы быть создан и использован, а затем снова использован для SMITH (что может быть лучше при сканировании таблицы). Люди могут не заметить, что производительность для SMITH плохая, поскольку они ожидают плохой производительности, поскольку вся таблица должна быть прочитана, а чтение индекса и переход к таблице не замечены напрямую.

Что касается ваших изменений, которые должны быть изменены, я подозреваю, что SQL просто рассматривает это как совершенно другой запрос и строит новый план, соответствующий вашему значению [некоторое число].

Чтобы проверить это, внесите бессмысленное изменение в запрос, например, добавив несколько пробелов между FOR и именем таблицы, или добавьте комментарий в конце. Это быстро? Если это так, то это потому, что этот запрос немного отличается от того, что находится в кэше, поэтому SQL сделал то, что он делает для «новых» запросов.

Для решения я бы посмотрел на три вещи. Во-первых, убедитесь, что ваша статистика актуальна. Это действительно должно быть первым, что вы делаете, когда запрос кажется странным или случайным. Ваш DBA должен делать это, но вещи случаются. Обычный способ обеспечить актуальную статистику - это переиндексировать ваши таблицы, что не обязательно облегчает задачу, но есть также варианты просто обновить статистику.

Второе, о чем стоит подумать, - это добавить индексы в соответствии с предложениями Ремуса. При более высоком / другом индексе одно значение по сравнению с другим может быть более стабильным и не так сильно изменяться.

Если это не помогает, третье, что нужно попробовать, это принудительно вводить новый план каждый раз, когда вы запускаете инструкцию, используя ключевое слово RECOMPILE:

выберите * из [большой таблицы], где serial_number = [некоторый номер], порядок по test_date desc OPTION (RECOMPILE)

Здесь есть статья, описывающая подобную ситуацию . Честно говоря, раньше я видел только применение RECOMPILE к хранимым процедурам, но, похоже, он работает с «обычными» операторами SELECT. Кимберли Трипп никогда не водила меня неправильно.

Вы также можете посмотреть на функцию, называемую « руководство по планированию », но она более сложна и может быть излишней.

пролив дарина
источник
Чтобы покрыть некоторые из этих проблем: 1. Статистика была обновлена, обновляется. 2. Мы пытались индексировать несколькими способами (охватывая индексы и т. Д.), Но проблема, похоже, больше связана с order byиспользованием именно индекса datetime. 3. Просто опробовал вашу идею с опцией RECOMPILE, но она все равно не удалась, что меня немного удивило, я надеялся, что она сработает, хотя я не знаю, является ли это решением для производства.
DBtheDBA