Помилка продуктивності індексу часу SQL Server 2008


11

Ми використовуємо SQL Server 2008 R2 і маємо дуже велику таблицю (100 М + рядків) з основним індексом ідентифікатора та 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також називається, коли не використовується "порядок", і результат також повертається миттєво.

Ми досить гучно розглядаємо цю проблему, і я бачу подібні випуски, опубліковані від інших, але жодна, яка не підводить її до цього рівня.

Тож інші були свідками такої поведінки? Хтось має рішення краще, ніж ставити безглузді SQL перед оператором select, щоб змінити поведінку? Оскільки SQL Server повинен викликати замовлення після збору даних, то, певно, здається, що це помилка на сервері, яка зберігається тривалий час. Ми виявили, що така поведінка є послідовною у багатьох наших великих таблицях і є відтворюваною.

Зміни:

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

Мені слід додати, щоб допомогти пошуковим користувачам, викинута помилка часу очікування ODBC: [Microsoft] [ODBC Driver 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

Я також повинен зазначити, що ми шукали плани запитів як функціонуючих, так і непрацюючих запитів. Вони обидва повторно використовуються відповідно до підрахунку виконання. Промивання кешованих планів та повторний запуск не змінює успіх запиту.


Що станеться, якщо ви спробуєте 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. Я перевірив і вже не міг побачити нічого, що було б пов’язане з цим.


Завтра я змушу DBA набрати сценарій, щоб створити середовище для відтворення. Я не думаю, що це так складно. Я також опублікую його тут, якщо хтось зацікавиться спробувати його самі.
DBtheDBA

Опублікуйте елемент підключення, коли він відкриється. Таким чином, якщо хтось інший має цю проблему, вони вказують право на це. І кожен, хто переглядає це питання, може захотіти проголосувати за питання, тому Microsoft, швидше за все, зверне на нього увагу.
cfradenburg

0

Моя гіпотеза полягає в тому, що ви керуєтесь кешем плану запитів. (Ремус може говорити те саме, що і я, але по-іншому.)

Ось детальна інформація про те, як SQL планує кешування .

Перегляньте деталі: хтось запустив цей запит раніше для певного [деякого числа]. SQL переглянув надане значення, індекси та статистику для відповідної таблиці / стовпців тощо і побудував план, який добре працював для цієї конкретної [деякої кількості]. Потім він кеширував план, запускав його і повертав результати, що викликають.

Пізніше хтось інший виконує той самий запит для іншого значення [деякої кількості]. Це конкретне значення призводить до різко різної кількості рядків результатів, і двигун повинен створити інший план для цього примірника запиту. Але це не працює таким чином. Натомість SQL приймає запит і (більш-менш) виконує облік регістру кешу запитів у регістрі, шукаючи попередньо існуючу версію запиту. Коли він знайде той, який раніше, він просто використовує цей план.

Ідея полягає в тому, що це економить час, необхідний для вирішення плану та його побудови. Дірка в ідеї - це той самий запит, який виконується зі значеннями, що дають дивовижні результати. Вони повинні мати різні плани, але вони не мають. Хто першим керував запитом, допомагає встановити поведінку для всіх, хто виконує його після.

Швидкий приклад: виберіть * з [люди], де прізвище = "SMITH" - дуже популярне прізвище в США GO select * з [люди], де прізвище = "BONAPARTE" - НЕ популярне прізвище в США

Коли буде запущений запит на BONAPARTE, план, побудований для SMITH, буде повторно використаний. Якщо SMITH спричинив сканування таблиці (що може бути добре , якщо рядки в таблиці складають 99% SMITH), BONAPARTE також отримає сканування таблиці. Якщо BONAPARTE запускався перед SMITH, план, що використовує індекс, може бути побудований та використаний, а потім знову використаний для SMITH (що може бути краще при скануванні таблиці). Люди, можливо, не помічають, що ефективність для SMITH є поганою, оскільки вони очікують поганої продуктивності, оскільки вся таблиця повинна бути прочитана, і зчитування індексу та перескакування до таблиці безпосередньо не помічено.

Що стосується ваших змін - що треба змінити - будь-що, я підозрюю, що SQL просто сприймає це як абсолютно інший запит і будує новий план, характерний для вашого значення [деякої кількості].

Щоб перевірити це, внесіть безглузді зміни до запиту, наприклад додайте пробіли між FOR та назвою таблиці або поставте коментар у кінці. Це швидко? Якщо так, то це тому, що цей запит дещо відрізняється від того, що є в кеші, тому SQL робив те, що робить для "нових" запитів.

Для вирішення я би роздивився три речі. По-перше, переконайтеся, що ваша статистика актуальна. Це дійсно має бути першим, що ви робите, коли запит здається дивним чи випадковим. Ваш DBA повинен робити це, але все відбувається. Звичайний спосіб забезпечити актуальну статистику - це повторне індексування таблиць, що не обов'язково - це легка річ, але є також варіанти просто оновити статистику.

Друге, що варто подумати, - це додавати індекси відповідно до пропозицій Ремуса. З кращим / іншим індексом одне значення порівняно з іншим може бути стабільнішим і не змінюватися настільки дико.

Якщо це не допомагає, третє, що потрібно спробувати, - це примушувати новий план кожного разу при запуску оператора, використовуючи ключове слово RECOMPILE:

виберіть * з [великої таблиці], де serial_number = [деяка кількість] замовлення на тест_дачі опису OPTION (RECOMPILE)

Тут є стаття, що описує подібну ситуацію . Чесно кажучи, я раніше бачив лише RECOMPILE, застосований до збережених процедур, але, схоже, це працює з "регулярними" операторами SELECT до. Кімберлі Тріпп ніколи не керувала мене неправильно.

Ви також можете заглянути в функцію під назвою " керівництво по плану ", але вона є більш складною і може бути зайвою.


Щоб висвітлити деякі з цих проблем: 1. Статистика була оновлена, оновлюється. 2. Ми спробували індексувати декількома способами (охоплюючи індекси тощо), але, здається, проблема більшою мірою пов'язана з order byвикористанням щодо індексу конкретного часу. 3. Щойно спробував вашу ідею за допомогою опції RECOMPILE, вона все-таки провалилася, що мене трохи здивувало, я сподівався, що це спрацює, хоча я не знаю, чи це рішення для виробництва.
DBtheDBA
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.