КулЛиб - Классная библиотека! Скачать книги бесплатно
Всего книг - 712479 томов
Объем библиотеки - 1400 Гб.
Всего авторов - 274474
Пользователей - 125061

Новое на форуме

Новое в блогах

Впечатления

Влад и мир про Владимиров: Ирландец 2 (Альтернативная история)

Написано хорошо. Но сама тема не моя. Становление мафиози! Не люблю ворьё. Вор на воре сидит и вором погоняет и о ворах книжки сочиняет! Любой вор всегда себя считает жертвой обстоятельств, мол не сам, а жизнь такая! А жизнь кругом такая, потому, что сам ты такой! С арифметикой у автора тоже всё печально, как и у ГГ. Простая задачка. Есть игроки, сдающие определённую сумму для участия в игре и получающие определённое количество фишек. Если в

  подробнее ...

Рейтинг: 0 ( 0 за, 0 против).
DXBCKT про Дамиров: Курсант: Назад в СССР (Детективная фантастика)

Месяца 3-4 назад прочел (а вернее прослушал в аудиоверсии) данную книгу - а руки (прокомментировать ее) все никак не доходили)) Ну а вот на выходных, появилось время - за сим, я наконец-таки сподобился это сделать))

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

В начале

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
DXBCKT про Стариков: Геополитика: Как это делается (Политика и дипломатия)

Вообще-то если честно, то я даже не собирался брать эту книгу... Однако - отсутствие иного выбора и низкая цена (после 3 или 4-го захода в книжный) все таки "сделали свое черное дело" и книга была куплена))

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

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
DXBCKT про Москаленко: Малой. Книга 3 (Боевая фантастика)

Третья часть делает еще более явный уклон в экзотерику и несмотря на все стсндартные шаблоны Eve-вселенной (базы знаний, нейросети и прочие девайсы) все сводится к очередной "ступени самосознания" и общения "в Астралях")) А уж почти каждодневные "глюки-подключения-беседы" с "проснувшейся планетой" (в виде галлюцинации - в образе симпатичной девчонки) так и вообще...))

В общем герою (лишь формально вникающему в разные железки и нейросети)

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
Влад и мир про Черепанов: Собиратель 4 (Боевая фантастика)

В принципе хорошая РПГ. Читается хорошо.Есть много нелогичности в механике условий, заданных самим же автором. Ну например: Зачем наделять мечи с поглощением душ и забыть об этом. Как у игрока вообще можно отнять душу, если после перерождении он снова с душой в своём теле игрока. Я так и не понял как ГГ не набирал опыта занимаясь ремеслом, особенно когда служба якобы только за репутацию закончилась и групповое перераспределение опыта

  подробнее ...

Рейтинг: 0 ( 0 за, 0 против).

SQL Server. Наладка и оптимизация для профессионалов. [Дмитрий Короткевич] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

Beijing

Boston Farnham Sebastopol

Tokyo

SQL Server
Наладка и оптимизация
для профессионалов

Дмитрий Короткевич

2023

ББК 32.988.02-018.2
УДК 004.383.2
К68

Короткевич Дмитрий
К68 SQL Server. Наладка и оптимизация для профессионалов. — СПб.: Питер,
2023. — 512 с.: ил. — (Серия «Библиотека программиста»).
ISBN 978-5-4461-2332-2
Исчерпывающий обзор лучших практик по устранению неисправностей и оптимизации производительности Microsoft SQL Server. Специалисты по базам данных, в том числе разработчики
и администраторы, научатся выявлять проблемы с производительностью, системно устранять
неполадки и расставлять приоритеты при тонкой настройке, чтобы достичь максимальной эффективности.
Автор книги Дмитрий Короткевич — Microsoft Data Platform MVP и Microsoft Certified
Master (MCM) — расскажет о взаимозависимостях между компонентами баз данных SQL Server.
Вы ­узнаете, как быстро провести диагностику системы и найти причину любой проблемы. Методы,
описанные в книге, совместимы со всеми версиями SQL Server и подходят как для локальных, так
и для облачных конфигураций SQL Server.

16+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.)

ББК 32.988.02-018.2
УДК 004.383.2

Права на издание получены по соглашению с O’Reilly. Все права защищены. Никакая часть данной книги
не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев
авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как
надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не
может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за
возможные ошибки, связанные с использованием книги. Издательство не несет ответственности за доступность материалов, ссылки на которые вы можете найти в этой книге. На момент подготовки книги к изданию
все ссылки на интернет-ресурсы были действующими.
В книге возможны упоминания организаций, деятельность которых запрещена на территории Российской
Федерации, таких как Meta Platforms Inc., Facebook, Instagram и др.

ISBN 978-1098101862 англ.

ISBN 978-5-4461-2332-2

Authorized Russian translation of the English edition of SQL Server
Advanced Troubleshooting and Performance Tuning, ISBN 9781098101923
© 2022 Dmitri Korotkevitch.
This translation is published and sold by permission of O’Reilly Media, Inc.,
which owns or controls all rights to publish and sell the same.
© Перевод на русский язык ООО «Прогресс книга», 2023
© Издание на русском языке, оформление ООО «Прогресс книга», 2023
© Серия «Библиотека программиста», 2023

Краткое содержание

Предисловие............................................................................................... 17
Глава 1. Установка и настройка SQL Server.............................................................. 24
Глава 2. Модель выполнения SQL Server и статистика ожидания................ 51
Глава 3. Производительность дисковой подсистемы........................................ 70
Глава 4. Неэффективные запросы............................................................................... 95
Глава 5. Хранение данных и настройка запросов..............................................131
Глава 6. Загрузка процессора......................................................................................188
Глава 7. Проблемы с оперативной памятью.........................................................214
Глава 8. Блокировки и конкурентный доступ......................................................249
Глава 9. Работа с базой данных tempdb и ее производительность...........296
Глава 10. Кратковременные блокировки..............................................................328
Глава 11. Журнал транзакций......................................................................................344
Глава 12. Группы доступности AlwaysOn................................................................370
Глава 13. Другие примечательные типы ожиданий..........................................398
Глава 14. Анализ схемы базы данных и индексов..............................................413

6  Краткое содержание
Глава 15. SQL Server в виртуализированных средах.........................................447
Глава 16. SQL Server в облаке.......................................................................................473
Приложение. Типы ожиданий.....................................................................................496
Об авторе...............................................................................................................................510
Иллюстрация на обложке...............................................................................................511

Оглавление

Предисловие............................................................................................................17
Для кого эта книга.........................................................................................................................18
Структура книги.............................................................................................................................18
Условные обозначения...............................................................................................................20
Использование исходного кода примеров.......................................................................21
Как связаться с автором......................................................................................................22
Благодарности................................................................................................................................22
От издательства..............................................................................................................................23
Глава 1. Установка и настройка SQL Server..........................................................24
Аппаратное обеспечение и операционная система.....................................................24
Центральный процессор....................................................................................................25
Оперативная память.............................................................................................................25
Дисковая подсистема...........................................................................................................26
Сеть...............................................................................................................................................27
Операционные системы и приложения.......................................................................28
Виртуализация и облачные технологии......................................................................29
Настройка SQL-сервера..............................................................................................................29
Версия SQL Server и уровень обновления..................................................................30
Мгновенная инициализация файлов............................................................................30
Настройка базы tempdb......................................................................................................32
Флаги трассировки................................................................................................................33
Параметры сервера...............................................................................................................35
Настройка баз данных.................................................................................................................38
Настройки базы данных......................................................................................................39

8  Оглавление
Настройки журнала транзакций......................................................................................40
Файлы данных и файловые группы................................................................................41
Анализ журнала ошибок SQL Server.....................................................................................43
Консолидация экземпляров и баз данных........................................................................46
Эффект наблюдателя...................................................................................................................47
Резюме................................................................................................................................................50
Чек-лист устранения неполадок.....................................................................................50
Глава 2. Модель выполнения SQL Server и статистика ожидания...................51
SQL Server: высокоуровневая архитектура.......................................................................51
SQLOS и модель выполнения...................................................................................................53
Статистика ожидания..................................................................................................................56
Динамические административные представления, связанные с моделью
выполнения......................................................................................................................................61
sys.dm_os_wait_stats.............................................................................................................61
sys.dm_exec_session_wait_stats........................................................................................62
sys.dm_os_waiting_tasks......................................................................................................63
sys.dm_exec_requests............................................................................................................64
sys.dm_os_schedulers............................................................................................................66
Обзор регулятора ресурсов.....................................................................................................67
Резюме................................................................................................................................................69
Чек-лист устранения неполадок.....................................................................................69
Глава 3. Производительность дисковой подсистемы.......................................70
Как устроена подсистема ввода/вывода SQL Server.....................................................70
Планирование и ввод/вывод............................................................................................71
Чтение данных.........................................................................................................................73
Запись данных..........................................................................................................................75
Подсистема хранения: целостный обзор...........................................................................76
Представление sys.dm_io_virtual_file_stats................................................................77
Счетчики производительности и метрики ОС..........................................................81
Виртуализация, хост-адаптер шины и уровни хранения.....................................86
Настройка контрольных точек................................................................................................87

Оглавление  9
Ожидания ввода/вывода...........................................................................................................91
Ожидание ASYNC_IO_COMPLETION ..............................................................................91
Ожидание IO_COMPLETION ...............................................................................................91
Ожидание WRITELOG ............................................................................................................92
Ожидание WRITE_COMPLETION .......................................................................................92
Ожидания PAGEIOLATCH......................................................................................................92
Резюме................................................................................................................................................94
Чек-лист устранения неполадок.....................................................................................94
Глава 4. Неэффективные запросы........................................................................95
Чем плохи неэффективные запросы....................................................................................95
Статистика выполнения на основе кэша планов............................................................96
Расширенные события и трассировки SQL.................................................................... 109
Хранилище запросов................................................................................................................ 115
Отчеты хранилища запросов в SSMS ......................................................................... 119
Работа с динамическими административными представлениями
хранилища запросов......................................................................................................... 122
Сторонние инструменты......................................................................................................... 128
Резюме............................................................................................................................................. 129
Чек-лист устранения неполадок.................................................................................. 130
Глава 5. Хранение данных и настройка запросов........................................... 131
Хранение данных и схемы доступа.................................................................................... 131
Таблицы на основе строк........................................................................................................ 132
Индексы на основе B-деревьев.................................................................................... 135
Составные индексы............................................................................................................ 140
Некластеризованные индексы...................................................................................... 141
Фрагментация индекса............................................................................................................ 146
Статистика и оценка количества элементов.................................................................. 150
Как вести статистику.......................................................................................................... 153
Модели оценки количества элементов..................................................................... 155
Анализ плана выполнения..................................................................................................... 157
Построчный и пакетный режим выполнения........................................................ 157

10  Оглавление
Динамическая статистика запросов и профилирование статистики
выполнения............................................................................................................................ 160
Характерные проблемы при настройке запросов..................................................... 164
Неэффективный код........................................................................................................... 164
Неэффективный поиск по индексу.............................................................................. 168
Неправильный тип соединения.................................................................................... 171
Избыточный поиск по ключу.......................................................................................... 180
Индексирование данных........................................................................................................ 182
Резюме............................................................................................................................................. 185
Чек-лист устранения неполадок.................................................................................. 186
Глава 6. Загрузка процессора............................................................................. 188
Неоптимизированные запросы и код T-SQL.................................................................. 188
Неэффективный код T-SQL.............................................................................................. 189
Сценарии для контроля за загрузкой ЦП................................................................. 190
Шаблоны неоптимизированных запросов.............................................................. 192
Компиляция запросов и кэширование планов............................................................ 193
Планы, чувствительные к параметрам...................................................................... 194
Независимость от значений параметров................................................................. 200
Компиляция и параметризация........................................................................................... 203
Автоматическая параметризация................................................................................ 205
Простая параметризация................................................................................................ 206
Принудительная параметризация............................................................................... 207
Параллелизм................................................................................................................................ 210
Резюме............................................................................................................................................. 212
Чек-лист устранения неполадок.................................................................................. 213
Глава 7. Проблемы с оперативной памятью.................................................... 214
Использование и конфигурация памяти в SQL Server.............................................. 214
Настройка памяти SQL Server........................................................................................ 217
Сколько нужно памяти?.................................................................................................... 219
Выделение памяти..................................................................................................................... 219
Клерки памяти...................................................................................................................... 222
Команда DBCC MEMORYSTATUS..................................................................................... 232

Оглавление  11
Выполнение запросов и предоставление памяти...................................................... 232
Оптимизация запросов, интенсивно использующих память.......................... 236
Обратная связь по предоставлению памяти.......................................................... 242
Управление объемом предоставленной памяти.................................................. 243
In-Memory OLTP и устранение неполадок...................................................................... 244
Резюме............................................................................................................................................. 248
Чек-лист устранения неполадок.................................................................................. 248
Глава 8. Блокировки и конкурентный доступ.................................................. 249
Типы блокировок и их поведение...................................................................................... 250
Основные типы блокировок.......................................................................................... 251
Совместимость блокировок........................................................................................... 255
Уровни изоляции транзакций и поведение блокировок.................................. 256
Проблемы блокирования....................................................................................................... 260
Устранение неполадок блокирования в реальном времени.......................... 261
Работа с отчетом о заблокированном процессе................................................... 266
Уведомления о событиях и Blocking Monitoring Framework............................ 270
Взаимные блокировки (Deadlocks).................................................................................... 271
Устранение взаимных блокировок............................................................................. 272
Блокировки и индексы...................................................................................................... 276
Оптимистичные уровни изоляции..................................................................................... 277
Уровень изоляции READ COMMITTED SNAPSHOT ................................................ 280
Уровень изоляции SNAPSHOT........................................................................................ 281
Блокировки схемы..................................................................................................................... 282
Укрупнение блокировок......................................................................................................... 284
Устранение неполадок с укрупнением блокировок........................................... 287
Ожидания, связанные с блокировками........................................................................... 290
Тип ожидания LCK_M_U ................................................................................................... 290
Тип ожидания LCK_M_S .................................................................................................... 291
Тип ожидания LCK_M_X ................................................................................................... 291
Типы ожидания LCK_M_SCH_S и LCK_M_SCH_M .................................................. 292
Типы ожидания интентной блокировки LCK_M_I* .............................................. 293
Типы ожидания блокировки диапазона LCK_M_R* ............................................ 293

12   Оглавление
Резюме............................................................................................................................................. 294
Чек-лист устранения неполадок.................................................................................. 295
Глава 9. Работа с базой данных tempdb и ее производительность............. 296
Временные объекты в tempdb............................................................................................. 296
Временные таблицы и табличные переменные.................................................... 297
Кэширование временных объектов........................................................................... 303
Параметры с табличным значением........................................................................... 306
Обычные таблицы в базе данных tempdb и протоколирование
транзакций............................................................................................................................. 307
Внутренние компоненты, использующие tempdb...................................................... 309
Хранилище версий............................................................................................................. 309
Переносы (spills)................................................................................................................... 312
Распространенные проблемы с tempdb......................................................................... 315
Состязания за доступ к системным страницам..................................................... 318
Нехватка места...................................................................................................................... 322
Конфигурация базы данных tempdb................................................................................. 325
Резюме............................................................................................................................................. 326
Чек-лист устранения неполадок.................................................................................. 327
Глава 10. Кратковременные блокировки......................................................... 328
Введение в кратковременные блокировки................................................................... 328
Кратковременные блокировки страниц......................................................................... 331
Решение проблем с горячими точками: параметр
индекса OPTIMIZE_FOR_SEQUENTIAL_KEY............................................................... 334
Решение проблем с горячими точками: хеш-разбиение.................................. 336
Решение проблем с горячими точками: In-Memory OLTP................................. 338
Другие типы кратковременных блокировок................................................................. 339
Резюме............................................................................................................................................. 342
Чек-лист устранения неполадок.................................................................................. 343
Глава 11. Журнал транзакций............................................................................. 344
Внутреннее устройство журнала транзакций............................................................... 344
Модификация данных и протоколирование транзакций................................. 345

Оглавление  13
Явные и автоматически фиксируемые транзакции и накладные
расходы журнала................................................................................................................. 350
Отложенная устойчивость.............................................................................................. 353
Протоколирование транзакций In-Memory OLTP ................................................ 354
Виртуальные файлы журналов..................................................................................... 355
Конфигурация журнала транзакций................................................................................. 358
Проблемы с усечением журнала......................................................................................... 359
Ожидание повторного использования
журнала LOG_BACKUP ...................................................................................................... 361
Ожидание повторного использования
журнала ACTIVE_TRANSACTION ................................................................................... 362
Ожидание повторного использования
журнала AVAILABILITY_REPLICA .................................................................................... 363
Ожидание повторного использования
журнала DATABASE_MIRRORING.................................................................................... 363
Ожидание повторного использования
журнала REPLICATION........................................................................................................ 364
Ожидание повторного использования
журнала ACTIVE_BACKUP_OR_RESTORE..................................................................... 364
Другие стратегии решения проблем с усечением журнала............................ 365
Ускоренное восстановление базы данных..................................................................... 365
Пропускная способность журнала транзакций........................................................... 366
Резюме............................................................................................................................................. 368
Чек-лист устранения неполадок.................................................................................. 369
Глава 12. Группы доступности AlwaysOn.......................................................... 370
Обзор групп доступности AlwaysOn.................................................................................. 370
Очереди групп доступности................................................................................................. 372
Синхронная репликация и опасное ожидание HADR_SYNC_COMMIT.............. 377
Расширенные события групп доступности............................................................. 380
Асинхронная репликация и доступные для чтения
вторичные реплики................................................................................................................... 385
Особенности доступных для чтения вторичных реплик.................................. 386
Параллельный повтор............................................................................................................. 390

14  Оглавление
Устранение неполадок аварийного переключения................................................... 391
Группы доступности и отказоустойчивая кластеризация
Windows Server..................................................................................................................... 391
Устранение неполадок аварийного переключения............................................ 394
Когда аварийное переключение не происходит.................................................. 395
Резюме............................................................................................................................................. 396
Чек-лист устранения неполадок.................................................................................. 397
Глава 13. Другие примечательные типы ожиданий....................................... 398
Ожидания ASYNC_NETWORK_IO ......................................................................................... 398
Ожидания THREADPOOL ......................................................................................................... 400
Ожидания, связанные с резервным копированием.................................................. 405
Повышение производительности резервного копирования......................... 405
Параметры BUFFERCOUNT и MAXTRANSFERSIZE................................................... 406
Частичные резервные копии базы данных............................................................. 407
HTBUILD и другие ожидания с префиксом HT*............................................................. 407
Вытесняющие ожидания......................................................................................................... 408
Тип ожидания PREEMPTIVE_OS_WRITEFILEGATHER .............................................. 408
Тип ожидания PREEMPTIVE_OS_WRITEFILE .............................................................. 409
Типы ожидания, связанные с аутентификацией................................................... 409
Ожидания OLEDB ................................................................................................................ 410
Типы ожидания: подводим итоги........................................................................................ 410
Резюме............................................................................................................................................. 411
Чек-лист устранения неполадок.................................................................................. 412
Глава 14. Анализ схемы базы данных и индексов.......................................... 413
Анализ схемы базы данных.................................................................................................... 413
Таблицы-кучи......................................................................................................................... 414
Индексы с типом данных uniqueidentifier................................................................ 417
Широкие и неуникальные кластеризованные индексы................................... 418
Недоверенные внешние ключи.................................................................................... 421
Неиндексированные внешние ключи........................................................................ 422
Избыточные индексы......................................................................................................... 424

Оглавление  15
Высокие значения идентификаторов........................................................................ 428
Анализ индекса........................................................................................................................... 431
Представление sys.dm_db_index_usage_stats....................................................... 432
Представление sys.dm_db_index_operational_stats............................................ 439
Целостное представление: sp_Index_Analysis.............................................................. 443
Резюме............................................................................................................................................. 445
Чек-лист устранения неполадок.................................................................................. 446
Глава 15. SQL Server в виртуализированных средах...................................... 447
Виртуализировать или не виртуализировать — вот в чем вопрос.................... 447
Настройка SQL Server в виртуализированных средах.............................................. 449
Планирование мощности................................................................................................ 449
Конфигурация ЦП................................................................................................................ 451
Память....................................................................................................................................... 457
Хранилище............................................................................................................................. 458
Сеть............................................................................................................................................ 460
Управление виртуальными дисками................................................................................. 461
Стратегия и инструменты резервного копирования................................................. 462
Устранение неполадок в виртуальных средах............................................................. 463
Недостаточная пропускная способность процессора....................................... 463
Нехватка памяти................................................................................................................... 468
Производительность дисковой подсистемы.......................................................... 469
Резюме............................................................................................................................................. 471
Чек-лист устранения неполадок.................................................................................. 472
Глава 16. SQL Server в облаке.............................................................................. 473
Облачные платформы с высоты птичьего полета....................................................... 473
Надежность платформы................................................................................................... 474
Лимитирование ресурсов............................................................................................... 474
Топология................................................................................................................................ 475
Связь и обработка случайных ошибок............................................................................. 476
Доступ к экземпляру базы данных.............................................................................. 476
Случайные ошибки............................................................................................................. 477

16  Оглавление
SQL Server в облачных виртуальных машинах.............................................................. 478
Настройка ввода/вывода и производительность................................................ 478
Настройка высокой доступности................................................................................. 480
Межрегиональная задержка.......................................................................................... 480
Управляемые службы Microsoft Azure SQL..................................................................... 482
Рекомендации по архитектуре и проектированию служб............................... 482
Подходы к устранению неполадок.............................................................................. 486
Amazon SQL Server RDS............................................................................................................ 489
CloudWatch............................................................................................................................. 490
Performance Insights........................................................................................................... 491
Google Cloud SQL........................................................................................................................ 493
Резюме............................................................................................................................................. 494
Чек-лист устранения неполадок.................................................................................. 495
Приложение. Типы ожиданий............................................................................ 496
Об авторе............................................................................................................... 510
Иллюстрация на обложке.................................................................................... 511

Предисловие

Прошло уже несколько лет с тех пор, как была издана моя предыдущая книга.
За это время многое изменилось. Выпущено несколько новых версий SQL Server.
Продукт стал более зрелым, кросс-платформенным, в нем появилась полноценная поддержка облачных технологий. Но я все равно не торопился публиковать
новое издание книги «Pro SQL Server Internals» («Внутреннее устройство SQL
Server для профессионалов»).
Тому было несколько причин. Как бы ни были хороши новые функции, они
не меняли фундаментальных принципов работы продукта. Материал из моих
старых книг по большей части подходит к SQL Server 2017, SQL Server 2019
и даже к свежему SQL Server 2022. Что еще более важно, я хотел написать книгу
по-другому.
Наверное, стоит уточнить. Как некоторые из вас, возможно, знают, я уже много
лет провожу курсы по SQL Server и использую собственные книги как методические пособия к этим курсам. Собственно, я и писать начал именно потому,
что хотел представить материал в более структурированном формате, чем
презентации PowerPoint. Я рад, что моим читателям это понравилось и книги
оказались полезными.
Все мои курсы были посвящены внутреннему устройству SQL Server. Я всегда
считал, что профессионал должен разбираться в своих инструментах, чтобы достичь успеха. Я рассказываю ученикам, как работает SQL Server, и помогаю им
применять эти знания и создавать эффективные системы. Но в какой-то момент
я обнаружил, что самой популярной темой на моих занятиях стали устранение
неполадок и настройка производительности: ученикам интересно, когда я описываю конкретную проблему, а затем объясняю, почему она возникла.
Изменив методику преподавания, я решил изменить и методику написания
книги. Прошло 18 месяцев — и результат перед вами. Лично мне нравится то, что
получилось. Книга по-прежнему посвящена внутреннему устройству SQL Server,
но теперь она лаконичнее и практичнее моих предыдущих работ1. Она научит вас
1

Название книги также изменилось. Теперь она называется «SQL Server Advanced
Troubleshooting and Performance Tuning» («SQL Server. Наладка и оптимизация для
профессионалов»). — Примеч. ред.

18  Предисловие
эффективно обнаруживать и устранять типичные проблемы при работе с SQL
Server, не перегружая лишней информацией. Книга также ­покажет, в каком направлении двигаться, если вы хотите узнать еще больше.
Здесь описана методология, которую используют многие консультанты мирового уровня по SQL Server. Вы научитесь собирать и анализировать данные,
выявлять узкие места и очаги неэффективности. Что еще более важно, я покажу,
как рассматривать систему целостно и «видеть лес за деревьями».
Содержание книги не привязано к какой-либо конкретной версии SQL Server.
За немногими исключениями оно актуально для всех версий от SQL Server 2005
до SQL Server 2022 и последующих версий. Материал также подходит для
управляемых служб SQL Server, работающих в облаке.

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

Структура книги
Книга состоит из 16 глав:
Глава 1 «Установка и настройка SQL Server» содержит принципы и лучшие
методики того, как выбирать оборудование и настраивать экземпляры SQL
Server.

Структура книги  19
Глава 2 «Модель выполнения SQL Server и статистика ожидания» описывает
SQLOS — очень важный компонент SQL Server. Здесь же вы познакомитесь
с таким распространенным методом устранения неполадок, как статистика
ожидания. На эту главу опирается весь остальной материал книги.
Глава 3 «Производительность дисковой подсистемы» дает представление о том,
как SQL Server взаимодействует с подсистемой ввода/вывода и как анализировать и оптимизировать ее производительность.
Глава 4 «Неэффективные запросы» демонстрирует несколько методов того,
как выявлять неэффективные запросы и выбирать целевые объекты для оптимизации запросов.
Глава 5 «Хранение данных и настройка запросов» объясняет, как SQL Server
работает с данными в базе данных, и дает рекомендации по настройке запросов.
Глава 6 «Загрузка процессора» рассматривает распространенные причины
высокой загрузки ЦП и учит бороться с узкими местами на уровне процессора.
Глава 7 «Проблемы с оперативной памятью» посвящена настройкам SQL
Server, относящимся к памяти, и описывает, как анализировать использование
памяти и решать связанные с ней проблемы.
Глава 8 «Блокировки и конкурентный доступ» рассказывает о модели конкурентного доступа, используемой в SQL Server, и о том, как обращаться с блокировками в системе.
Глава 9 «Работа с базой данных tempdb и ее производительность» описывает
использование системной базы данных tempdb и лучшие методики ее конфигурации. Кроме того, здесь содержатся рекомендации о том, как оптимально
использовать временные объекты и устранять распространенные узкие места
в tempdb.
Глава 10 «Кратковременные блокировки» посвящена кратковременным блокировкам в SQL Server. Рассматриваются случаи, когда они вызывают проблемы,
и способы решения этих проблем.
Глава 11 «Журнал транзакций» рассказывает о том, как устроен журнал транзак­
ций в SQL Server и как избавиться от распространенных узких мест и ошибок
в нем.
Глава 12 «Группы доступности AlwaysOn» рассматривает самую популярную
технологию высокой доступности SQL Server и частые проблемы, с которыми
можно столкнуться при ее использовании.
Глава 13 «Другие примечательные типы ожиданий» описывает несколько
распространенных типов ожиданий, которые не рассматривались в прочих
главах.

20  Предисловие
Глава 14 «Анализ схемы базы данных и индексов» дает ряд советов о том, как
обнаруживать неэффективные участки структуры базы данных, а также оценивать использование индексов и их работоспособность.
Глава 15 «SQL Server в виртуализированных средах» рассказывает о передовых методах настройки виртуальных экземпляров SQL Server и устранении
сопутствующих неполадок.
Глава 16 «SQL Server в облаке» описывает, как настраивать и использовать
SQL Server в облачных виртуальных машинах. В ней также представлен обзор
управляемых служб SQL Server, доступных в Microsoft Azure, Amazon Web
Services (AWS) и Google Cloud Platform (GCP).
В конце каждой главы приведен контрольный список наиболее важных шагов
по устранению неполадок, связанных с темой главы.
Наконец, приложение «Типы ожиданий» можно использовать как справочник
по распространенным типам ожиданий и методам устранения основных неполадок для каждого типа.

Условные обозначения
В этой книге используются следующие условные обозначения.
Курсив
Курсивом выделены новые термины или важные понятия.
Моноширинный шрифт

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

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

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

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

Использование исходного кода примеров  21
Совет или предложение.

Общее примечание.

Предупреждение или предостережение.

Использование исходного кода примеров
Вспомогательные материалы (примеры кода, упражнения и т. д.) доступны для
загрузки по адресу https://github.com/aboutsqlserver/code.
В папке Troubleshooting Scripts вы найдете записные книжки Azure Data
Studio1 с использованными в книге сценариями диагностики и устранения
неполадок. Примеры сценариев и приложений также есть в папке Companion
Materials (Books).
Если не указано иное, сценарии будут работать во всех версиях SQL Server, начиная с SQL Server 2005. В старых версиях могут не поддерживаться некоторые
столбцы динамических административных представлений, и вам придется закомментировать их.
Я планирую поддерживать и расширять библиотеку диагностических сценариев,
так что проверяйте обновления в репозитории.
Если у вас возникнут вопросы технического характера по использованию примеров кода, направляйте их по электронной почте на адрес bookquestions@oreilly.com.
В общем случае все примеры кода из книги вы можете использовать в своих
программах и в документации. Вам не нужно обращаться в издательство за
разрешением, если вы не собираетесь воспроизводить существенные части
программного кода. Если вы разрабатываете программу и используете в ней
несколько фрагментов кода из книги, вам не нужно обращаться за разрешением. Но для продажи или распространения примеров из книги вам потребуется
разрешение от издательства O’Reilly. Вы можете отвечать на вопросы, цитируя
1

Azure Data Studio можно скачать с сайта Microsoft: https://oreil.ly/zwwCf.

22  Предисловие
данную книгу или примеры из нее, но для включения существенных объемов
программного кода из книги в документацию вашего продукта потребуется
разрешение.
Мы рекомендуем, но не требуем добавлять ссылку на первоисточник при цитировании. Под ссылкой на первоисточник мы подразумеваем указание авторов,
издательства и ISBN.
За получением разрешения на использование значительных объемов программного кода из книги обращайтесь по адресу permissions@oreilly.com.

Как связаться с автором
Вы можете написать мне по адресу dk@aboutsqlserver.com, если у вас есть вопросы
по книге или по SQL Server в целом. Я всегда рад помочь, чем смогу.
Также загляните в мой блог по адресу https://aboutsqlserver.com. Обещаю писать
туда чаще, раз уж книга наконец вышла!

Благодарности
Прежде всего, как и всегда, я хотел бы поблагодарить свою семью за постоянную
помощь и поддержку. Писательство — это идеальное оправдание, чтобы избежать
домашних обязанностей. Я до сих пор не понимаю, почему мне все сошло с рук!
Кроме того, я чрезвычайно благодарен Эрланду Соммарскогу (Erland
Sommarskog), Томасу Грозеру (Thomas Grohser) и Уве Рикену (Uwe Ricken),
которые проделали большую работу, рецензируя эту книгу. Благодаря им она
стала значительно лучше и обрела окончательную форму.
Эрланд Соммарског работает с SQL Server уже тридцать лет и имеет статус
Microsoft Data Platform MVP с 2001 года. Он работает независимым консультантом в Стокгольме (Швеция). Эрланд с удовольствием делится знаниями
и опытом с сообществом. В свободное от SQL Server время он играет в бридж
и путешествует.
Томас Грозер работает IT-специалистом более 35 лет и уже 12 лет является
Microsoft Data Platform MVP. Он использует SQL Server с 1994 года и специа­
лизируется на архитектуре и реализации высокозащищенных, доступных, восстанавливаемых и эффективных баз данных, а также их базовой инфраструктуры. В свободное время Томас любит делиться знаниями, накопленными за
десятилетия, с сообществом SQL Server и платформ данных, выступая в группах
пользователей и на конференциях по всему миру.
Уве Рикен — Microsoft Data Platform MVP и Microsoft Certified Master (SQL
Server 2008) из Франкфурта (Германия). Уве работает с SQL Server с 2007 года

От издательства  23
и специализируется на внутреннем устройстве баз данных и индексировании,
а также на архитектуре и разработке баз данных. Он регулярно выступает на
конференциях и мероприятиях по SQL Server и ведет блог http://www.sqlmaster.de.
Спасибо вам, Эрланд, Томас и Уве! Работать с вами было очень круто!
Огромное спасибо моему коллеге Андре Фиано (Andre Fiano) — одному из самых
знающих специалистов по инфраструктуре, которых я когда-либо встречал.
Я многому научился у Андре, и он помог мне подготовить несколько наглядных
примеров для этой книги.
И конечно же, я хотел бы поблагодарить всю команду O’Reilly и особенно Сару
Грей (Sarah Grey), Элизабет Келли (Elizabeth Kelly), Кейт Дулли (Kate Dullea),
Кристен Браун (Kristen Brown) и Одри Дойл (Audrey Doyle). Спасибо вам за то,
что помогли привести мой английский в приемлемую форму и убедили меня,
будто я умею рисовать диаграммы!
Эта книга посвящена SQL Server, и я хочу поблагодарить команду Microsoft за
усердную работу над этим продуктом. Мне очень интересно, как он будет развиваться дальше.
И последнее, но не менее важное: отдельная благодарность — всем моим друзьям из сообщества #SQLFamily, которые поддерживали и подбадривали меня!
Писать для такой замечательной аудитории — сплошное удовольствие!
Спасибо вам всем!

От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com
(издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию
о наших книгах.

ГЛАВА 1

Установка и настройка SQL Server

Серверы баз данных функционируют не в безвоздушном пространстве. Они
входят в экосистему одного или нескольких клиентских приложений. Базы
данных приложений размещаются на одном или нескольких экземплярах SQL
Server, а они, в свою очередь, развернуты на физическом или виртуальном оборудовании. Данные хранятся на дисках, чей ресурс обычно приходится делить
с другими системами баз данных и не только. Наконец, всем компонентам нужна
сеть, чтобы обмениваться данными и сохранять их.
Бороться с неполадками в базах данных непросто из-за сложности их экосистем
и внутренних зависимостей. С точки зрения клиентов, большинство проблем
касаются производительности: приложения работают медленно и не отвечают на
запросы, время ожидания истекает, а иногда приложения вообще не подключаются к базе данных. Настоящая причина проблемы может скрываться где угодно.
Может быть, аппаратное обеспечение неисправно или неправильно настроено,
а может, в базе данных неэффективные индексы, схемы или код. Возможно, SQL
Server перегружен, а может, клиентское приложение работает с ошибками или
плохо спроектировано. Все это означает, что для поиска и устранения проблем
нужно целостное представление обо всей системе.
Эта книга посвящена устранению неполадок в SQL Server. Но устранение неполадок всегда стоит начинать с анализа экосистемы приложения и среды SQL
Server. В этой главе даны рекомендации о том, как выполнять этот анализ и обнаруживать наиболее распространенные проблемы в конфигурации SQL Server.
Сперва я расскажу о настройке оборудования и операционной системы. Затем
пойдет речь об установке SQL Server и конфигурации базы данных. Далее мы
коснемся темы консолидации SQL Server и дополнительных издержек, возникающих из-за средств мониторинга.

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

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

Центральный процессор
Самая затратная часть системы — это, безусловно, лицензия на коммерческое
ядро базы данных. Как правило, она значительно дороже оборудования, на котором предполагается разворачивать сервер БД. Поэтому рекомендую покупать
самый мощный ЦП, какой позволит ваш бюджет, особенно если вы используете
не версию SQL Server Enterprise Edition (эта версия не ограничивает количество
доступных ядер).
Обратите внимание на модель процессора. Каждое новое поколение процессоров
производительнее предыдущего. Можно получить прирост производительности
на 10–15 %, просто поставив новый ЦП, даже если у него такая же тактовая
частота, как у старого.
Иногда, когда стоимость лицензии — не главная проблема, приходится выбирать,
что лучше: более медленный процессор с большим количеством ядер или более
быстрый процессор с меньшим количеством ядер. В этом случае решение во
многом зависит от загруженности системы. Для систем оперативной обработки
транзакций (OLTP), особенно In-Memory OLTP, выгоднее будет одноядерный
высокопроизводительный процессор. С другой стороны, для хранилищ данных
и аналитических задач больше подойдет высокая степень параллелизма и большое количество ядер.

Оперативная память
В сообществе SQL Server бытует такая шутка:
— Сколько памяти нужно для SQL Server?
— Больше.
В этой шутке есть доля правды. Большой объем памяти позволяет SQL Server
кэшировать больше данных. Это, в свою очередь, сокращает количество дисковых операций ввода/вывода (I/O) и положительно сказывается на производительности. Поэтому увеличение объема памяти сервера — зачастую
самый дешевый и быстрый способ решить некоторые проблемы с производительностью.

26  Глава 1. Установка и настройка SQL Server
Например, предположим, что система страдает от неоптимизированных запросов. Казалось бы, их влияние можно уменьшить, если добавить памяти и таким
образом уменьшить чтение с физического диска для этих запросов. Но очевидно,
что это не решает основную проблему и к тому же опасно, потому что данные
могут разрастиcь до того, что перестанут помещаться в кэш. Тем не менее в качестве временного решения такой подход иногда годится.
У SQL Server Enterprise Edition объем используемой памяти не ограничен.
У других версий есть ограничения. Standard Edition (SQL Server 2014 и более
поздних версий) может использовать до 128 Гбайт ОЗУ для буферного пула,
32 Гбайт ОЗУ на каждую базу данных In-Memory OLTP и 32 Гбайт ОЗУ для
хранения сегментов индекса columnstore. В Web Edition доступно вдвое меньше
памяти, чем в Standard Edition. Учитывайте эти ограничения, когда собираете
или обновляете экземпляры SQL Server, отличные от Enterprise Edition. Не забудьте выделить дополнительную память для других компонентов SQL Server,
например кэша планов и менеджера блокировок.
Короче, добавьте столько памяти, сколько можете себе позволить. В наше время
это дешево. Если ваши базы данных небольшие, то чрезмерное количество памяти ни к чему, однако учитывайте, что в будущем объем данных может вырасти.

Дисковая подсистема
Для хорошей производительности SQL Server необходима исправная и быстрая
дисковая подсистема. SQL Server очень интенсивно занимается вводом/выводом, то есть постоянно считывает и записывает данные на диск.
Архитектуру дисковой подсистемы для SQL Server можно построить по-разному.
Главное — добиться, чтобы задержка запросов ввода/вывода была минимальной.
Для критически важных систем первого класса надежности я рекомендую, чтобы
задержка чтения и записи данных не превышала 3–5 мс, а для записи журнала
транзакций — 1–2 мс. К счастью, этих показателей легко достичь с помощью
флеш-накопителей.
Но есть загвоздка: анализируя производительность ввода/вывода в SQL Server,
нужно измерять время задержки на уровне самого SQL Server, а не на уровне
хранилища. В SQL Server задержки могут оказаться значительно дольше, чем
ключевые метрики производительности хранилища (KPI), потому что при интенсивном вводе/выводе могут возникать очереди. (В главе 3 мы рассмотрим,
как собирать и анализировать данные о производительности ввода/вывода.)
Если ваша подсистема хранения поддерживает несколько уровней производительности, я рекомендую разместить на самом быстром диске базу данных
tempdb, а на оставшихся — журнал транзакций и файлы данных. База данных
tempdb — это общий ресурс на сервере, и для нее важна хорошая пропускная
способность ввода/вывода.

Аппаратное обеспечение и операционная система  27
Записи в файлы журнала транзакций выполняются синхронно. Для этих файлов
важна низкая задержка записи. Записи в журнал транзакций также производятся
последовательно; однако помните, что размещение нескольких файлов журналов
и/или файлов данных на одном диске чревато режимом произвольного доступа
сразу в нескольких базах данных.
Я рекомендую помещать файлы данных и журналов на разные физические диски, потому что при этом базу удобнее обслуживать и легче восстанавливать.
Но стоит учитывать физическую конфигурацию хранилища. В некоторых случаях, когда в дисковых массивах недостаточно шпинделей, разделение массива
на несколько LUN может снизить производительность всего массива.
В своих системах я не разбиваю кластеризованные и некластеризованные
индексы по нескольким файловым группам, размещая их на разных дисках.
От этого редко увеличивается производительность ввода/вывода, если только
вы не разделяете полностью пути хранения по файловым группам. В то же время
такая конфигурация может значительно усложнить аварийное восстановление.
Наконец, помните, что для некоторых технологий SQL Server важна хорошая
эффективность последовательного ввода/вывода. Например, в In-Memory OLTP
вообще не используется произвольный доступ, и ограничивающим фактором при
запуске и восстановлении базы данных становится производительность последовательного чтения. Обход хранилища данных тоже зависит от последовательного
ввода/вывода, когда B-деревья и индексы columnstore не сильно фрагментированы.
У флеш-памяти разница между производительностью последовательного и произвольного ввода/вывода незначительна, а вот у магнитных дисков она довольно
велика.

Сеть
SQL Server связывается с клиентами и другими серверами по сети. Очевидно,
нужна достаточная пропускная способность сети, чтобы поддерживать эту связь.
Остановлюсь на нескольких важных деталях.
Во-первых, при устранении неполадок, связанных с производительностью сети,
необходимо анализировать топологию всей сети. Помните, что пропускная
способность сети ограничена скоростью ее самого медленного компонента. Например, у вас может быть 10-гигабитный восходящий канал от сервера, но если
где-то в сети оказался коммутатор на 1 Гбит/с, он ограничит общую пропускную
способность. Это особенно важно для сетевых хранилищ: убедитесь, что пути
доступа к дискам максимально эффективны.
Во-вторых, сложилась общепринятая практика выделять отдельную сеть для
передачи тактового импульса в отказоустойчивых кластерах AlwaysOn и группах
доступности AlwaysOn. Иногда стоит подумать о выделении отдельной сети
для всего трафика группы доступности. Этот подход повышает надежность

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

Операционные системы и приложения
Как правило, я рекомендую использовать самую свежую версию операционной
системы, которая поддерживает вашу версию SQL Server. Убедитесь, что и ОС,
и SQL Server обновлены до последних версий, и наладьте регулярную установку
обновлений.
Если вы используете старую версию SQL Server (до 2016), лучше устанавливать 64-разрядную ОС. В большинстве случаев 64-разрядная версия работает
эффективнее 32-разрядной и лучше переносит масштабирование оборудо­
вания.
Начиная с SQL Server 2017, сервер баз данных можно развертывать и на Linux.
С точки зрения производительности версии SQL Server для Windows и Linux
очень похожи. Выбор ОС зависит от корпоративной экосистемы и от того, какую систему вам удобнее поддерживать. Имейте в виду, что для развертывания
на Linux может потребоваться несколько иная стратегия высокой доступности
(HA, High Availability) по сравнению с Windows. Например, для автоматического
аварийного переключения, возможно, придется применять Pacemaker вместо
Windows Server Failover Cluster (WSFC).
По возможности лучше использовать выделенный хост SQL Server. Помните,
что проще и дешевле масштабировать серверы приложений и не тратить ценные
ресурсы на хост базы данных.
В то же время не следует запускать на сервере несущественные процессы. Например, многие специалисты по базам данных запускают SQL Server Management
Studio (SSMS) только на удаленных рабочих столах. Всегда лучше работать
удаленно и не потреблять ресурсы сервера.
Наконец, если на сервере должно работать антивирусное ПО, то все папки баз
данных нужно исключить из сканирования.

Настройка SQL-сервера  29

Виртуализация и облачные технологии
Современная IT-инфраструктура опирается на виртуализацию, которая обеспечивает дополнительную гибкость, упрощает управление и снижает затраты на
оборудование. Поэтому чаще всего вам придется работать с виртуализированной
инфраструктурой SQL Server.
Ничего плохого в этом нет. Грамотно реализованная виртуализация дает множество преимуществ при приемлемом снижении производительности. В случае
VMware vSphere vMotion или Hyper-V Live Migration виртуализация добавляет
еще один уровень высокой доступности. Виртуализация позволяет плавно обновлять аппаратное обеспечение и упрощает управление базой данных. Если вам
не требуется выжимать максимум из оборудования, то экосистему SQL Server
лучше виртуализировать.
На больших серверах с большим количеством ЦП накладные расходы на
виртуализацию увеличиваются. Однако во многих случаях это оказывается
вполне приемлемым.

Вместе с тем виртуализация добавляет лишний уровень сложности при устранении неполадок. Помимо показателей виртуальной машины, приходится
обращать внимание на работоспособность и нагрузку хоста. Что еще хуже,
влияние перегруженного хоста на производительность может быть незаметно
по показателям в гостевой ОС.
Мы рассмотрим несколько подходов к устранению неполадок на уровне виртуализации в главе 15. Но для начала можно проконсультироваться у специалистов
по инфраструктуре, не происходит ли на хосте избыточного резервирования
ресурсов. Обратите внимание на количество физических ЦП и выделенных
виртуальных ЦП на хосте, а также на физическую и выделенную память. Виртуальным машинам для критически важных экземпляров SQL Server нужно
выделять достаточно ресурсов, чтобы их производительность не пострадала.
Если не брать в расчет уровень виртуализации, то на виртуализированных экземплярах SQL Server неполадки устраняются так же, как на обычных. То же
самое относится и к облачным конфигурациям SQL Server на виртуальных
машинах. В конце концов, облако — это всего лишь особый центр обработки
данных, управляемый внешним провайдером.

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

30  Глава 1. Установка и настройка SQL Server

Версия SQL Server и уровень обновления
SELECT @@VERSION — это первая команда, которую я запускаю во время провер-

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

Последняя причина очень важна. Клиенты не раз просили меня устранить неполадки, которые уже были устранены в пакетах исправлений и накопительных
обновлениях. Всегда просматривайте примечания к выпуску обновлений, потому
что может оказаться, что ваша проблема уже решена.
Советую обновляться до новейшей версии SQL Server. В каждой версии улучшаются производительность, функциональность и масштабируемость. Разница
особенно заметна, если вы переходите на SQL Server 2016 или более позднюю
версию с более старых. Выпуск SQL Server 2016 был важной вехой в истории
продукта, и в этой версии появилось множество улучшений, влияющих на производительность. По моему опыту, само по себе обновление с SQL Server 2012 до
2016 или более поздней версии может повысить производительность на 20–40 %
без дополнительных усилий.
Стоит также отметить, что, начиная с SQL Server 2016 SP1, многие функции,
ранее предназначенные только для Enterprise Edition, появились и в более
дешевых версиях. Некоторые из них — например, сжатие данных — позволяют
SQL Server кэшировать больше данных в буферном пуле, что повышает производительность.
Очевидно, перед обновлением систему нужно протестировать: всегда существует вероятность, что после обновления она станет работать хуже. В случае
небольших патчей такой риск невелик, но с крупными обновлениями лучше
быть осторожнее. Некоторые риски можно снизить определенными настройками
базы данных, как вы увидите далее в этой главе.

Мгновенная инициализация файлов
Каждый раз, когда SQL Server увеличивает размер файлов или журналов транз­
акций — будь то автоматически или в рамках команды ALTER DATABASE, — он
заполняет свежевыделенную часть файла нулями. Этот процесс блокирует все
сеансы, которые пытаются записывать в соответствующий файл, а в случае журнала транзакций в нем прекращается создание записей. Также при этом может
произойти всплеск нагрузки на систему ввода/вывода.
Для файлов журналов транзакций это поведение нельзя изменить: SQL Server
всегда заполняет их нулями. Однако для файлов данных его можно отключить, если активировать мгновенную инициализацию файлов (IFI, instant file

Настройка SQL-сервера  31
initialization). Она ускоряет разрастание файла данных и сокращает время создания или восстановления баз данных.
Чтобы включить IFI, нужно предоставить стартовой учетной записи SQL
Server разрешение SA_MANAGE_VOLUME_NAME, также известное как Perform Volume
Maintenance Task (Выполнить обслуживание томов). Это можно сделать в приложении «Локальная политика безопасности» (Local Security Policy, secpol.msc).
Чтобы изменения вступили в силу, нужно перезапустить SQL Server.
В SQL Server 2016 и более поздних версиях это разрешение также можно предоставить в процессе установки SQL Server, как показано на рис. 1.1.

Рис. 1.1. Включение IFI во время установки SQL Server
Чтобы узнать, включена ли IFI, нужно посмотреть на столбец instant_file_
initialization_enabled в динамическом представлении1 (DMV) sys.dm_server_
services. Этот столбец доступен в SQL Server 2012 с пакетом обновления 4
(SP4), SQL Server 2016 с пакетом обновления 1 (SP1) и более поздних версиях.
В старых версиях можно запустить код, показанный в листинге 1.1.
1

https://oreil.ly/58Vd7

32  Глава 1. Установка и настройка SQL Server
Листинг 1.1. Проверка того, включена ли мгновенная инициализация файлов
(для старых версий SQL Server)
DBCC TRACEON(3004,3605,-1);
GO
CREATE DATABASE Dummy;
GO
EXEC sp_readerrorlog 0,1,N'Dummy';
GO
DROP DATABASE Dummy;
GO
DBCC TRACEOFF(3004,3605,-1);
GO

Если IFI не включена, то в журнале SQL Server будет написано, что SQL Server
обнуляет файл данных .mdf и файл журнала .ldf (рис. 1.2). Когда IFI включена,
обнуляется только файл журнала .ldf.

Рис. 1.2. Проверка настройки мгновенной инициализации файла
С этой настройкой связана небольшая угроза безопасности. Если IFI включена,
то администраторы БД могут видеть данные из ранее удаленных файлов в ОС,
просматривая свежевыделенные страницы в базе данных. Но для большинства
систем это некритично.

Настройка базы tempdb
База tempdb — это системная база данных, предназначенная для хранения временных объектов, которые создают пользователи и сам SQL Server. Эта база очень
активно используется и часто становится источником состязаний за ресурсы
в системе. Как устранять проблемы, связанные с tempdb, я расскажу в главе 9,
а пока поговорим о настройке.
Как уже упоминалось, базу данных tempdb стоит размещать на самом быстром
диске. В общем случае этому диску не требуется резервное копирование или
другие меры предохранения данных: tempdb создается заново при каждом запуске
SQL Server, так что для нее вполне подойдет локальный SSD-накопитель или

Настройка SQL-сервера  33
облачное хранилище. Но помните, что если база данных tempdb будет недоступна,
то SQL Server перестанет работать.
Если у вас не Enterprise версия SQL Server и в системе больше памяти, чем он
потребляет, то можно поместить tempdb на RAM-диск. Но с SQL Server Enterprise
Edition так поступать не следует: вы добьетесь большей производительности,
если используете эту память для буферного пула.
Предварительно выделяйте для файлов tempdb место, равное максимальному размеру RAM-диска, и создавайте дополнительные небольшие файлы
данных и журналов на диске, чтобы предотвратить нехватку места. SQL
Server не будет использовать небольшие файлы на диске, пока RAM-диск
не заполнится.

В базе данных tempdb всегда должно быть несколько файлов данных. К сожалению, конфигурация по умолчанию, которая создается во время установки
SQL Server, неоптимальна, особенно в старых версиях. В главе 9 я расскажу, как
точно настроить количество файлов данных в tempdb, а пока можно опираться
на эмпирические правила:
Если на сервере восемь или меньше ядер ЦП, создайте такое же количество
файлов данных, сколько и ядер.
Если на сервере больше восьми ядер ЦП, создайте либо восемь файлов данных, либо четверть от числа ядер — в зависимости от того, что больше, —
округляя до пакетов по четыре файла. Например, на 24-ядерном сервере
нужно 8 файлов данных, а на 40-ядерном — 12 файлов.
Наконец, убедитесь, что у всех файлов данных tempdb одинаковый начальный
размер и что параметры автоувеличения указаны в мегабайтах, а не в процентах. Это позволит SQL Server сбалансированно использовать файлы данных
и уменьшить состязания за использование ресурсов в системе.

Флаги трассировки
Флаги трассировки в SQL Server позволяют активировать некоторые функции
или изменить их поведение. В новых версиях SQL Server появляется все больше
параметров конфигурации базы данных и сервера, но флаги трассировки попрежнему широко используются. Вам нужно будет изучить, какие флаги есть
в системе, и, возможно, включить некоторые из них.
Чтобы получить список включенных флагов трассировки, выполните команду
DBCC TRACESTATUS. Флаги можно включить в диспетчере конфигурации SQL
Server и/или с помощью параметра -T при запуске SQL Server.

34  Глава 1. Установка и настройка SQL Server
Посмотрим на некоторые часто используемые флаги трассировки.
Т1118

Этот флаг запрещает использовать в SQL Server смешанные экстенты1. Это
позволяет повысить пропускную способность tempdb в SQL Server 2014
и более ранних версиях, потому что уменьшается количество изменений и,
следовательно, состязаний за ресурсы в системных каталогах tempdb. Этот
флаг не нужен в SQL Server 2016 и более поздних версиях, где tempdb по
умолчанию не использует смешанные экстенты.
Т1117

Если этот флаг установлен, то SQL Server автоматически увеличивает все
файлы данных в файловой группе, когда в одном из файлов заканчивается
место. Это позволяет более сбалансированно распределять ввод/вывод по
файлам данных. В старых версиях SQL Server этот флаг стоит включить,
чтобы улучшить пропускную способность tempdb, но лучше проверить, есть
ли в базах данных пользователей файловые группы с несколькими файлами
данных несбалансированного размера. Как и в случае с T1118, этот флаг не
нужен в SQL Server 2016 и более поздних версиях, где tempdb по умолчанию
автоматически увеличивает все файлы данных.
Т2371

По умолчанию SQL Server автоматически обновляет статистику только после
того, как в индексе изменилось 20 % данных. Это означает, что для больших
таблиц статистика редко обновляется автоматически. Флаг трассировки T2371
делает динамическим пороговое значение, при котором обновляется статистика: чем больше таблица, тем меньший процент изменений необходим для обновления статистики. Начиная с SQL Server 2016, это поведение также можно
контролировать с помощью уровня совместимости базы данных. Тем не менее
я все равно рекомендую включать этот флаг трассировки, если только у всех
баз данных на сервере уровень совместимости не составляет 130 или выше.
Т3226

Когда этот флаг включен, SQL Server не заносит в журнал ошибок записи об
успешном создании резервных копий базы данных. Это помогает уменьшить
размер журналов, чтобы с ними было удобнее работать.
Т1222

Этот флаг заносит граф взаимных блокировок в журнал ошибок SQL Server.
Он бывает полезен, но читать и анализировать журналы SQL Server становится сложнее. К тому же он избыточен, потому что граф взаимных блоки1

https://oreil.ly/CnPxm

Настройка SQL-сервера  35
ровок при необходимости можно получить из сеанса расширенного события
System_Health. Я обычно отключаю этот флаг.
Т4199

Этот флаг и параметр базы данных QUERY_OPTIMIZER_HOTFIXES (в SQL Server
2016 и более поздних версиях) управляют поведением исправлений оптимизатора запросов. Если флаг включен, то будут использоваться исправления из
пакетов исправлений и накопительных обновлений. Это поможет устранить
некоторые ошибки оптимизатора запросов и повысить производительность
запросов, но увеличивает риск регрессии планов после исправлений. Обычно
я не включаю этот флаг в промышленных экземплярах, если только нет возможности тщательно протестировать систему на предмет регрессий перед
тем, как применять исправления.
Т7412

Этот флаг включает упрощенное профилирование инфраструктуры в SQL
Server 2016 и 2017. Он позволяет собирать планы выполнения и множество
метрик выполнения запросов, не перегружая ЦП. Я расскажу об этом подробнее в главе 5.
Резюмируем: в SQL Server 2014 и более ранних версиях включайте T1118, T2371
и, возможно, T1117. В SQL Server 2016 и более поздних версиях включайте
T2371, кроме случаев, когда у всех баз данных на сервере уровень совместимости составляет 130 или выше. После этого посмотрите на все остальные
флаги трассировки в системе и разберитесь, что они делают. Некоторые флаги
устанавливаются без вашего ведома сторонними средствами и могут ухудшить
производительность сервера.

Параметры сервера
У SQL Server есть множество параметров конфигурации. Я подробно опишу
многие из них позже, но некоторые параметры рассмотрим сейчас.

Оптимизация для нерегламентированной рабочей нагрузки
Первый параметр конфигурации, о котором я расскажу, — Optimize for Ad-hoc
Workloads (Оптимизировать для нерегламентированной рабочей нагрузки). От
него зависит, как SQL Server кэширует планы выполнения нерегламентированных (непараметризованных) запросов. Когда этот параметр отключен (по
умолчанию), SQL Server кэширует полные планы выполнения этих инструкций,
отчего кэшу планов может понадобиться существенно больше памяти. Когда
параметр включен, SQL Server сначала кэширует небольшую структуру (всего
несколько сотен байтов) — так называемую заглушку плана, — а если запрос
выполняется во второй раз, то заменяет заглушку полным планом выполнения.

36  Глава 1. Установка и настройка SQL Server
В большинстве случаев нерегламентированные запросы выполняются однократно, поэтому имеет смысл включить Optimize for Ad-hoc Workloads. От этого может
значительно сократиться использование памяти кэша планов — правда, изредка
нерегламентированные запросы будут дополнительно перекомпилироваться.
Очевидно, что этот параметр не влияет на кэширование параметризованных
запросов и кода базы данных T-SQL.
Начиная с SQL Server 2019 и баз данных Azure SQL, параметр Optimize for
Ad-hoc Workloads можно регулировать на уровне базы данных с помощью
настройки OPTIMIZE_FOR_AD_HOC_WORKLOADS.

Максимальная память сервера
Второй важный параметр — Max Server Memory, который определяет, сколько
памяти может потреблять SQL Server. Специалисты по базам данных любят
спорить о том, как правильно настроить этот параметр, и существуют разные подходы к его вычислению. Многие даже предлагают оставить значение по умолчанию и разрешить SQL Server управлять им автоматически. На мой взгляд, лучше
всего настроить его самостоятельно, но делать это нужно грамотно (подробнее
в главе 7). Неудачно настроенный параметр может ухудшить быстродействие
существеннее, чем значение по умолчанию.
На практике я часто сталкиваюсь с тем, что этому параметру уделяют недостаточно внимания. Иногда его забывают изменить после обновления оборудования
или виртуальной машины, а иногда его неправильно рассчитывают в средах, где
SQL Server работает на сервере совместно с другими приложениями. В обоих
случаях, чтобы повысить производительность, можно для начала просто увеличить параметр Max Server Memory или даже перенастроить его на значение по
умолчанию, а полноценным анализом заняться позже.

Маска соответствия
Стоит проверить процессорное соответствие SQL Server и, возможно, установить
маску соответствия (affinity mask), если SQL Server работает на оборудовании
с несколькими узлами неоднородного доступа к памяти (NUMA — non-uniform
memory access). В современном аппаратном обеспечении каждый физический
ЦП обычно становится отдельным узлом NUMA. Если вы разрешаете SQL Server
использовать не все физические ядра, то нужно равномерно распределить процессоры SQL Server (или планировщики — см. главу 2) по NUMA.
Например, если SQL Server работает на сервере с двумя 18-ядерными процессорами Xeon и вы ограничиваете SQL Server до 24 ядер, то нужно установить
маску привязки, которая задействует по 12 ядер от каждого физического ЦП.
Производительность будет лучше, чем если бы SQL Server задействовал 18 ядер
от первого процессора и 6 от второго.

Настройка SQL-сервера  37
В листинге 1.2 показано, как анализировать распределение планировщиков
SQL Server (ЦП) между узлами NUMA. Обратите внимание на количество
планировщиков для каждого столбца parent_node_id на выходе.

Листинг 1.2. Проверка распределения планировщиков узлов NUMA
SELECT
parent_node_id
,COUNT(*) as [Schedulers]
,SUM(current_tasks_count) as [Current]
,SUM(runnable_tasks_count) as [Runnable]
FROM sys.dm_os_schedulers
WHERE status = 'VISIBLE ONLINE'
GROUP BY parent_node_id;

Параллелизм
Важно проверить настройки параллельных операций в системе. Настройки
по умолчанию, например MAXDOP = 0 и Cost Threshold for Parallelism = 5, в современных системах работают плохо. Как и в случае с максимальной памятью
сервера, лучше подобрать параметры в соответствии с рабочей нагрузкой системы (в главе 6 обсудим это подробно). Могу предложить эмпирическое правило:
Установите MAXDOP равным четверти количества доступных ЦП в OLTP
и половине количества доступных ЦП в хранилище данных. На очень больших серверах OLTP оставьте MAXDOP равным 16 или ниже. Не превышайте
количество планировщиков в узле NUMA.
Cost Threshold for Parallelism установите равным 50.

Начиная с SQL Server 2016, и в серверных базах данных Azure SQL можно установить MAXDOP на уровне базы данных с помощью команды ALTER DATABASE SCOPED
CONFIGURATION SET MAXDOP. Это полезно, когда на одном сервере размещаются
базы данных с разными рабочими нагрузками.

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

1

https://oreil.ly/nsLMW

38  Глава 1. Установка и настройка SQL Server
Листинг 1.3. Поиск изменений в настройках конфигурации сервера
DECLARE
@defaults TABLE
(
name SYSNAME NOT NULL PRIMARY KEY,
def_value SQL_VARIANT NOT NULL
)
INSERT INTO @defaults(name,def_value)
VALUES('backup compression default',0);
INSERT INTO @defaults(name,def_value)
VALUES('cost threshold for parallelism',5);
INSERT INTO @defaults(name,def_value)
VALUES('max degree of parallelism',0);
INSERT INTO @defaults(name,def_value)
VALUES('max server memory (MB)',2147483647);
INSERT INTO @defaults(name,def_value)
VALUES('optimize for ad hoc workloads',0);
/* Прочие параметры опущены в этой книге */
SELECT
c.name, c.description, c.value_in_use, c.value
,d.def_value, c.is_dynamic, c.is_advanced
FROM
sys.configurations c JOIN @defaults d ON
c.name = d.name
WHERE
c.value_in_use d.def_value OR
c.value d.def_value
ORDER BY
c.name;

На рис. 1.3 приведен пример вывода предыдущего кода. Если столбцы value
и value_in_use не совпадают, это указывает на заготовленные изменения конфигурации, которые вступят в силу после перезагрузки. Столбец is_dynamic
показывает, можно ли изменить параметр конфигурации без перезапуска.

Рис. 1.3. Измененные параметры конфигурации сервера

Настройка баз данных
На следующем шаге надо проверить некоторые настройки базы данных и параметры конфигурации. Давайте изучим их.

Настройка баз данных  39

Настройки базы данных
SQL Server позволяет регулировать многие настройки базы данных и управлять
ее поведением в зависимости от нагрузки на систему и других требований.
О многих из них мы поговорим позже в этой книге, но несколько настроек рассмотрим прямо сейчас.
Первый параметр — Auto Shrink (Автоматическое сжатие). Когда он включен,
SQL Server периодически сжимает базу данных и возвращает высвободившееся
пространство операционной системе. Этот параметр выглядит привлекательно
и вроде бы позволяет оптимизировать использование дискового пространства,
но он также может вызвать проблемы.
Алгоритм сжатия базы данных работает на физическом уровне. Он находит пустое пространство в начале файла и переносит размещенные экстенты из конца
файла в это пустое пространство, не учитывая, кто владелец экстента. Это создает
заметную нагрузку и приводит к существенной фрагментации индекса. Более
того, во многих случаях сжатие бесполезно: файлы рано или поздно все равно
увеличатся снова. Всегда лучше управлять файловым пространством вручную
и отключать автоматическое сжатие.
Параметр Auto Close (Автоматическая очистка) управляет тем, как SQL Server
кэширует данные из базы данных. Когда он включен, SQL Server удаляет страницы данных из буферного пула и планы выполнения из кэша планов, если нет
активных подключений к базе. Это снижает производительность новых сеансов,
когда данные нужно опять кэшировать, а запросы — компилировать заново.
Как правило, Auto Close следует отключать. Но бывают исключения: например,
экземпляры, на которых размещено большое количество редко используемых
баз данных. Хотя даже в этом случае я бы подумал о том, чтобы оставить эту настройку отключенной и разрешить SQL Server очищать кэш обычным способом.
Убедитесь, что для параметра Page Verify (Верификация страниц) установлено
значение CHECKSUM. Это позволяет эффективнее обнаруживать ошибки согласованности и исправлять повреждения базы данных.
Обратите внимание на модель восстановления базы данных (database recovery
model). Если используется режим восстановления SIMPLE, то в случае аварии
будет невозможно восстановить базу из резервных копий, сделанных позже, чем
последняя полная (FULL) копия. Если вы обнаружили, что база работает в таком
режиме, немедленно обсудите это с заинтересованными сторонами и убедитесь,
что они понимают риски потери данных.
Параметр Database Compatibility Level (Уровень совместимости БД) управляет
совместимостью и поведением SQL Server на уровне базы данных. Например,
если вы используете SQL Server 2019 и у вас есть база данных с уровнем совместимости 130 (SQL Server 2016), то SQL Server будет вести себя так, как если бы

40  Глава 1. Установка и настройка SQL Server
база работала на SQL Server 2016. Если держать базы данных на более низких
уровнях совместимости, то SQL Server будет проще обновлять, не опасаясь
уменьшения производительности. Однако при этом также не будут доступны
некоторые новые функции и улучшения.
Как правило, базу данных лучше запускать на последнем уровне совместимости, соответствующем версии SQL Server. Изменяйте уровень с осторожностью, потому что это, как любая смена версии, может снизить производительность. Перед изменениями протестируйте систему и убедитесь, что при
необходимости вы сможете откатить изменение, особенно если база данных
имеет уровень совместимости 110 (SQL Server 2012) или ниже. На уровне
совместимости 120 (SQL Server 2014) или выше включается новая модель
оценки количества элементов и могут существенно измениться планы выполнения запросов. Тщательно протестируйте систему, чтобы понять, к чему
приведут изменения.
Чтобы SQL Server использовал устаревшие модели оценки количества элементов с новыми уровнями совместимости базы данных, в SQL Server 2016 и более
поздних версиях установите для параметра базы данных LEGACY_CARDINALITY_
ESTIMATION значение ON, а в SQL Server 2014 включите флаг трассировки на
уровне сервера T9481. Этот подход позволит внедрять обновления или менять
уровни совместимости поэтапно, сглаживая влияние на систему. (В главе 5 мы
подробнее рассмотрим оценку количества элементов и обсудим, как снизить
риски при обновлении SQL Server и изменениях уровня совместимости базы
данных.)

Настройки журнала транзакций
SQL Server ведет журнал с опережающей записью, сохраняя информацию обо
всех изменениях базы данных в журнале транзакций. SQL Server обрабатывает
журналы транзакций последовательно, по принципу карусели. В большинстве
случаев нет нужды заводить сразу несколько файлов журналов в системе: при
этом администрировать базу данных становится сложнее, а производительность
не растет.
На внутреннем уровне SQL Server разбивает журналы транзакций на фрагменты, называемые виртуальными файлами журнала (VLF — Virtual Log Files),
и управляет ими как цельными единицами. Например, SQL Server не может
усечь и повторно использовать VLF, если он содержит только одну активную
запись журнала. Следите за количеством VLF в базе данных. При слишком
малом количестве очень больших VLF управление журналом и его усечение
будут неоптимальными. При слишком большом количестве небольших VLF
снизится производительность операций с журналом транзакций. Стремитесь,
чтобы в промышленных системах накапливалось не больше нескольких сотен VLF.

Настройка баз данных  41
Количество VLF, которые SQL Server добавляет при увеличении журнала, зависит от версии SQL Server и размера увеличения. В большинстве случаев создается 8 VLF, если увеличение составляет от 64 Мбайт до 1 Гбайт, или 16 VLF,
если увеличение превышает 1 Гбайт. Не следует полагаться на автоматическую
настройку, основанную на проценте от увеличения, потому что при этом генерируется множество VLF неравномерного размера. Вместо этого измените параметр автоувеличения журнала, чтобы файл увеличивался пошагово. Я обычно
использую шаги по 1024 Мбайт, что дает 128 Мбайт VLF, если только мне не
нужен очень большой журнал транзакций.
В SQL Server 2016 и более поздних версиях можно подсчитать число VLF в базе
данных с помощью представления sys.dm_db_log_info. В более старых версиях
SQL Server эту информацию можно получить командой DBCC LOGINFO. Если журнал транзакций настроен неправильно, его имеет смысл перестроить. Для этого
можно сократить журнал до минимального размера и увеличивать его шагами
от 1024 Мбайт до 4096 Мбайт.
Не сжимайте файлы журналов транзакций автоматически. Они снова вырастут
и снизят производительность, когда SQL Server обнулит файл. Лучше заранее
выделить место и управлять размером файла журнала вручную. Однако не
ограничивайте максимальный размер и автоувеличение (autogrowth), иначе
журналы не смогут автоматически увеличиваться в случае чрезвычайных ситуаций. (В главе 11 мы подробнее поговорим о том, как устранять проблемы
с журналом транзакций.)

Файлы данных и файловые группы
По умолчанию SQL Server создает новые базы данных, используя файловую
группу PRIMARY, состоящую из одного файла, и один файл журнала транзакций.
К сожалению, эта конфигурация неоптимальна с точки зрения производительности, управления базами данных и доступности.
SQL Server отслеживает, как файлы данных используют дисковое пространство,
с помощью системных страниц, называемых картами распределения. В системах
с очень изменчивыми данными карты распределения могут становиться источником состязания за ресурсы: SQL Server обеспечивает строго последовательный
доступ к ним во время модификации (подробнее об этом в главе 10). У каждого
файла данных есть собственный набор страниц карт распределения, и вы можете
уменьшить состязания, если создадите несколько файлов в файловой группе
с активно изменяемыми данными.
Убедитесь, что данные равномерно распределены по всем файлам в каждой
файловой группе. В SQL Server используется алгоритм пропорционального
заполнения, который записывает большую часть данных в файл, в котором
больше свободного места. Файлы данных одинакового размера позволяют сба-

42  Глава 1. Установка и настройка SQL Server
лансировать эти операции записи, уменьшив состязания карт распределения.
Поэтому проверьте, что у всех файлов данных в файловой группе одинаковый
размер и шаг автоматического увеличения, указанный в мегабайтах.
Также вы можете включить параметр файловой группы AUTOGROW_ALL_FILES
(доступен в SQL Server 2016 и более поздних версиях), который запускает автоувеличение для всех файлов в файловой группе одновременно. В предыдущих
версиях SQL Server для этого можно использовать флаг трассировки T1117,
однако имейте в виду, что он устанавливается на уровне сервера и влияет на все
базы данных и файловые группы в системе.
Изменять структуру существующих баз данных обычно нецелесообразно
или невозможно. Но может потребоваться создавать новые файловые группы
и перемещать данные, чтобы оптимизировать производительность. Приведем
несколько советов, как делать это эффективно:
Создайте несколько файлов данных в файловых группах с изменчивыми
данными. Обычно я начинаю с четырех файлов и увеличиваю их количество, если вижу проблемы с кратковременной блокировкой (см. главу 10).
Убедитесь, что у всех файлов данных одинаковый размер и параметры автоувеличения; включите параметр AUTOGROW_ALL_FILES. Для файловых групп,
данные в которых предназначены только для чтения, обычно достаточно
одного файла данных.
Не разбивайте кластеризованные индексы, некластеризованные индексы
или большие объекты (LOB) по разным файловым группам. Это редко помогает повысить производительность, зато может привести к проблемам
в случае повреждения базы данных.
Помещайте связанные сущности (например, Orders и OrderLineItems)
в одну и ту же файловую группу. Это упростит управление базой данных
и аварийное восстановление.
По возможности оставляйте пустой файловую группу PRIMARY.
На рис 1.4 показан пример структуры базы данных для гипотетической системы
электронной коммерции. Данные разбиты на секции и распределены по нескольким файловым группам, чтобы свести к минимуму время простоя и получить
возможность использовать хотя бы часть базы данных в случае аварии1. Это
также позволяет улучшить стратегию резервного копирования: можно копировать базу данных по частям и исключить из полных резервных копий данные,
предназначенные только для чтения.

1

Чтобы глубже погрузиться в стратегии разбиения данных и аварийного восстановления,
читайте мою книгу «Pro SQL Server Internals, Second Edition» (Apress, 2016).

Анализ журнала ошибок SQL Server  43

Файл журнала
PRIMARY
[Каталоги]

Только системные объекты

MDF
NDF

Клиенты, товары, ...

NDF
NDF

[Заказы текущего года]

NDF
NDF
NDF

[Прошлогодние заказы]
[Архив заказов]

NDF
NDF
NDF

Только для чтения

Должны быть онлайн, чтобы система работала

Рис. 1.4. Структура базы данных для системы электронной коммерции

Анализ журнала ошибок SQL Server
Журнал ошибок SQL Server — еще одно место, куда я обязательно заглядываю
в начале устранения неполадок. Ошибки, зафиксированные в этом журнале,
часто указывают на конкретные очаги проблем. Например, ошибки 823 и 824
могут свидетельствовать о проблемах с дисковой подсистемой и/или повреждением базы данных.
Просмотреть содержимое журнала ошибок можно средствами SSMS. Его также можно получить программно с помощью системной хранимой процедуры
xp_readerrorlog. Проблема здесь заключается в количестве данных в журнале: полезные данные могут затеряться среди множества информационных
сообщений.
Код в листинге 1.4 помогает решить эту проблему. Он отфильтровывает ненужный шум и позволяет сосредоточиться на сообщениях об ошибках. Управлять
поведением кода можно с помощью следующих переменных:

44  Глава 1. Установка и настройка SQL Server
@StartDate и @EndDate

Задают диапазон времени для анализа.
@NumErrorLogs

Указывает количество файлов журналов для чтения, если SQL Server переключается на файлы продолжения.
@ExcludeLogonErrors

Опускает сообщения аудита входа в систему.
@ShowSurroundingEvents и @ExcludeLogonSurroundingEvents

Позволяют получать информационные сообщения в ближайшей окрестности записей об ошибках из журнала. Временн о е окно для этих сообщений управляется переменными @SurroundingEventsBeforeSeconds
и @SurroundingEventsAfterSeconds.
Сценарий выдает два результата. Первый — записи из журнала ошибок, которые
содержат слово error. Когда параметр @ShowSurroundingEvents включен, сценарий также выводит записи журнала в ближайшей окрестности этих error-строк.
Некоторые записи, содержащие слово error, можно исключить из вывода, если
вставить их в таблицу @ErrorsToIgnore.

Листинг 1.4. Анализ журнала ошибок SQL Server
IF OBJECT_ID('tempdb..#Logs',N'U') IS NOT NULL DROP TABLE #Logs;
IFOBJECT_ID('tempdb..#Errors',N'U') IS NOT NULL DROP TABLE #Errors;
GO
CREATE TABLE #Errors
(
LogNum INT NULL,
LogDate DATETIME NULL,
ID INT NOT NULL identity(1,1),
ProcessInfo VARCHAR(50) NULL,
[Text] NVARCHAR(MAX) NULL,
PRIMARY KEY(ID)
);
CREATE TABLE #Logs
(
[LogDate] DATETIME NULL,
ProcessInfo VARCHAR(50) NULL,
[Text] NVARCHAR(MAX) NULL
);
DECLARE
@StartDate DATETIME = DATEADD(DAY,-7,GETDATE())
,@EndDate DATETIME = GETDATE()
,@NumErrorLogs INT = 1

Анализ журнала ошибок SQL Server  45
,@ExcludeLogonErrors BIT = 1
,@ShowSurroundingEvents BIT = 1
,@ExcludeLogonSurroundingEvents BIT = 1
,@SurroundingEventsBeforeSecond INT = 5
,@SurroundingEventsAfterSecond INT = 5
,@LogNum INT = 0;
DECLARE
@ErrorsToIgnore TABLE
(
ErrorText NVARCHAR(1024) NOT NULL
);
INSERT INTO @ErrorsToIgnore(ErrorText)
VALUES
(N'Registry startup parameters:%'),
(N'Logging SQL Server messages in file%'),
(N'CHECKDB for database%finished without errors%');
WHILE (@LogNum 0
UPDATE #Errors SET LogNum = @LogNum WHERE LogNum IS NULL;
SET @LogNum += 1;
END;
IF @ExcludeLogonErrors = 1
DELETE FROM #Errors WHERE ProcessInfo = 'Logon';
DELETE FROM e
FROM #Errors e
WHERE EXISTS
(
SELECT *
FROM @ErrorsToIgnore i
WHERE e.Text LIKE i.ErrorText
);
-- Только ошибки
SELECT * FROM #Errors ORDER BY LogDate DESC;
IF @@ROWCOUNT > 0 AND @ShowSurroundingEvents = 1
BEGIN
DECLARE
@LogDate DATETIME
,@ID INT = 0
WHILE 1 = 1
BEGIN
SELECT TOP 1 @LogNum = LogNum, @LogDate = LogDate, @ID = ID
FROM #Errors

46  Глава 1. Установка и настройка SQL Server
WHERE ID > @ID
ORDER BY ID;
IF @@ROWCOUNT = 0
BREAK;
SELECT
@StartDate = DATEADD(SECOND, -@SurroundingEventsBeforeSecond, @LogDate)
,@EndDate = DATEADD(SECONd, @SurroundingEventsAfterSecond, @LogDate);
INSERT INTO #Logs(LogDate,ProcessInfo,Text)
EXEC [master].[dbo].[xp_readerrorlog]
@LogNum, 1, NULL, NULL, @StartDate, @EndDate;
END;
IF @ExcludeLogonSurroundingEvents = 1
DELETE FROM #Logs WHERE ProcessInfo = ‚Logon';
DELETE FROM e
FROM #Logs e
WHERE EXISTS
(
SELECT *
FROM @ErrorsToIgnore i
WHERE e.Text LIKE i.ErrorText
);
SELECT * FROM #Logs ORDER BY LogDate DESC;
END

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

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

https://oreil.ly/AntEt

Эффект наблюдателя  47
Не существует универсальной стратегии консолидации, которая подошла бы
к любому проекту. Принимая решение о консолидации, нужно учесть объем
данных, нагрузку, конфигурацию оборудования, а также требования бизнеса
и безопасности. Однако в общем случае следует избегать консолидации баз данных OLTP и хранилища данных/отчетов на одном сервере, если они работают
под большой нагрузкой. (А если они уже консолидированы, их стоит разделить.)
Запросы к хранилищу данных обычно оперируют большими объемами данных,
что приводит к интенсивному вводу/выводу и сбросу содержимого буферного
пула. В совокупности это негативно сказывается на производительности других
систем.
Кроме того, при консолидации баз данных следует проанализировать требования
к безопасности. Некоторые функции безопасности, такие как аудит, затрагивают
весь сервер и ограничивают производительность всех баз данных на сервере.
Еще один пример — прозрачное шифрование данных (TDE, Transparent Data
Encryption). Хотя TDE — функция уровня базы данных, тем не менее SQL Server
шифрует tempdb, если TDE включено хотя бы на одной базе. Это ограничивает
производительность всех остальных систем.
Как правило, на одном и том же экземпляре SQL Server не стоит хранить базы
данных с разными требованиями к безопасности. Следует проанализировать
тенденции и выбросы в показателях и при необходимости отделить базы друг
от друга. (Позже в этой книге я покажу код, который поможет проанализировать использование ЦП, операции ввода/вывода и затраты памяти для каждой
базы данных.)
Я предлагаю использовать виртуализацию и консолидировать несколько виртуальных машин на одном или нескольких хостах вместо того, чтобы размещать
несколько независимых и активных баз данных на одном экземпляре SQL
Server. Такой подход обеспечивает гораздо большую гибкость, управляемость
и взаимную изоляцию систем, особенно если несколько экземпляров SQL Server
работают на одном сервере. Используя виртуализацию, гораздо проще контролировать потребление ими ресурсов.

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

48  Глава 1. Установка и настройка SQL Server
даром ничего не дается, и любой мониторинг увеличивает нагрузку на систему.
В некоторых случаях эти накладные расходы оказываются незначительными
и приемлемыми, но иногда они существенно влияют на производительность
сервера.
За свою карьеру консультанта по SQL Server я видел множество примеров неэффективного мониторинга. Например, один клиент использовал инструмент,
который предоставлял информацию о фрагментации индекса, вызывая функцию
sys.dm_db_index_physical_stats в режиме DETAILED каждые четыре часа для
каждого индекса в базе данных. Это приводило к огромным пикам ввода/вывода и очистке буферного пула, что сильно било по производительности. Другой
клиент применял инструмент, который постоянно опрашивал различные DMV,
значительно увеличивая нагрузку ЦП на сервере.
К счастью, во время устранения неполадок в системе такие запросы часто
удается проанализировать, чтобы оценить их влияние. Но с другими технологиями это не всегда получается. Пример такого случая — мониторинг на основе
расширенных событий (xEvents — Extended Events). Extended Events — это
отличная технология, которая позволяет устранять сложные проблемы в SQL
Server, но она плохо подходит в качестве инструмента профилирования. Некоторые события весьма тяжеловесны и ведут к большим накладным расходам
в загруженных средах.
Рассмотрим пример кода, который создает сеанс расширенных событий. Этот
сеанс фиксирует запросы, выполняемые в системе, см. листинг 1.5.

Листинг 1.5. Создание сеанса расширенных событий для захвата запросов в системе
CREATE EVENT SESSION CaptureQueries ON SERVER
ADD EVENT sqlserver.rpc_completed
(
SET collect_statement=(1)
ACTION
(
sqlos.task_time
,sqlserver.client_app_name
,sqlserver.client_hostname
,sqlserver.database_name
,sqlserver.nt_username
,sqlserver.sql_text
)
),
ADD EVENT sqlserver.sql_batch_completed
(
ACTION
(
sqlos.task_time
,sqlserver.client_app_name
,sqlserver.client_hostname

Эффект наблюдателя  49
,sqlserver.database_name
,sqlserver.nt_username
,sqlserver.sql_text

)
),
ADD EVENT sqlserver.sql_statement_completed
ADD TARGET package0.event_file
(SET FILENAME=N'C:\PerfLogs\LongSql.xel',MAX_FILE_SIZE=(200))
WITH
(
MAX_MEMORY =4096 KB
,EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS
,MAX_DISPATCH_LATENCY=5 SECONDS
);

Попробуйте выполнить этот код на сервере, который работает под большой нагрузкой с большим количеством одновременных запросов. Измерьте пропускную
способность системы с запущенным сеансом расширенных событий и без него.
Конечно же, будьте осторожны и не запускайте код на рабочем сервере!
На рис. 1.5 показана загрузка ЦП и количество пакетных запросов в секунду
в обоих сценариях на одном из моих серверов. Как видите, включение сеанса
расширенных событий уменьшило пропускную способность примерно на 20 %.
Что еще хуже, обнаружить само существование этого сеанса на сервере довольно
сложно.

Рис. 1.5. Пропускная способность сервера с активным сеансом расширенных событий и без него
Очевидно, степень воздействия будет зависеть от рабочей нагрузки системы.
В любом случае, при устранении неполадок следует проверить, нет ли в системе
ненужных инструментов мониторинга или сбора данных.
В общем, оценивайте стратегию мониторинга и, в частности, ее накладные расходы, особенно когда на сервере размещено несколько баз данных. Например,
расширенные события работают на уровне сервера. Хотя можно фильтровать
события на основе поля database_id, эта фильтрация происходит после запуска
события, так что расширенные события все равно влияют на все базы данных
на сервере.

50  Глава 1. Установка и настройка SQL Server

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

Чек-лист устранения неполадок
Выполнить высокоуровневый анализ оборудования, сети и дисковой подсистемы.
В виртуализированных средах — обсудить со специалистами по инфраструктуре конфигурацию хоста и нагрузку.
Изучить версии ОС и SQL Server, номер выпуска и установленные исправления.
Проверить, включена ли мгновенная инициализация файла.
Проанализировать флаги трассировки.
Включить оптимизацию для нерегламентированных рабочих нагрузок.
Проверить настройки памяти и параллелизма на сервере.
Изучить настройки tempdb (включая количество файлов); в версиях SQL
Server до 2016 года проверить флаг трассировки T1118 и, возможно, T1117.
Отключить автоматическое сжатие баз данных.
Проверить настройки файлов журнала данных и транзакций.
Проверить число VLF в файлах журнала транзакций.
Проверить ошибки в журнале SQL Server.
Проверить наличие ненужного мониторинга в системе.

ГЛАВА 2

Модель выполнения SQL Server
и статистика ожидания

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

SQL Server: высокоуровневая архитектура
Как известно, SQL Server — это очень сложный продукт, состоящий из десятков
компонентов и подсистем, которые невозможно полноценно охватить в одной
книге. В этом разделе приведем их обзор на самом общем уровне. Для удобства
я разделю эти компоненты и подсистемы на несколько категорий, как показано
на рис. 2.1. Давайте поговорим о них.
Уровень протокола обеспечивает связь между SQL Server и клиентскими приложениями. Этот уровень использует внутренний формат Tabular Data Stream
(TDS, Поток табличных данных), чтобы передавать данные через сетевые протоколы, такие как TCP/IP или именованные каналы. Если клиентское приложение и SQL Server работают на одном компьютере, можно использовать другой
протокол — Shared Memory (Общая память).

52  Глава 2. Модель выполнения SQL Server и статистика ожидания
Уровень протокола (связь с клиентом)
Обработчик запросов
Оптимизация запросов
Выполнение запросов (параллелизм,
(генерация планов, анализ затрат,
выделение памяти и т. д.)
статистика и т. д.)
Подсистема хранения
(доступ к данным, управление
блокировкой, протоколирование
транзакций и т. д.)

Обработчик In-Memory OLTP

Вспомогательные
средства (DBCC,
резервное
копирование,
средства
восстановления,
BCP и т. д.)

SQLOS/PAL (планирование, управление ресурсами, обнаружение
взаимных блокировок и т. д.)
Рис. 2.1. Основные компоненты SQL Server
Устраняя проблемы, касающиеся соединения между клиентом и сервером,
стоит проверить, какие протоколы включены. В некоторых версиях SQL
Server, например Express и Developer, TCP/IP и именованные каналы по
умолчанию отключены, так что сервер не принимает удаленных клиентских
подключений, пока вы не включите сетевые протоколы в Диспетчере конфигурации (Configuration Manager) SQL Server.

Уровень обработчика запросов (Query Processor) отвечает за оптимизацию и выполнение запросов. Он осуществляет синтаксический анализ, оптимизирует
запросы и управляет скомпилированными планами запросов, а также координирует все аспекты выполнения запросов.
Подсистема хранения (Storage Engine) отвечает за доступ к данным и их обработку в SQL Server. Она взаимодействует с данными на диске, управляет журналами
транзакций и обрабатывает транзакции, блокировки и конкурентный доступ,
а также выполняет некоторые другие задачи.
Обработчик In-Memory OLTP (In-Memory OLTP Engine) поддерживает In-Memory
OLTP в SQL Server. Он работает с таблицами, оптимизированными для памяти, и отвечает за управление данными и доступ к этим таблицам, компиляцию
в собственном коде, сохраняемость данных и прочие аспекты этой технологии.
Между компонентами существуют уровни абстракции. Например, Взаимодействие с запросами (Query Interop) (не показанное на рис. 2.1) позволяет обработчику запросов работать как с таблицами на основе строк, так и с таблицами,
оптимизированными для памяти: запросы прозрачно перенаправляются либо
в хранилище, либо в обработчик In-Memory OLTP.
Самый важный уровень абстракции — операционная система SQL Server
(SQLOS), которая изолирует другие компоненты SQL Server от нижележащих
операционных систем и занимается планированием, управлением ресурсами

SQLOS и модель выполнения  53
и их мониторингом, обработкой исключений и многими другими аспектами
работы SQL Server. Например, когда для какого-нибудь компонента SQL
Server требуется выделить память, он не вызывает функции API ОС напрямую, а запрашивает память у SQLOS. Это позволяет SQL Server детально
контролировать выполнение задач и использование внутренних ресурсов, не
полагаясь на ОС.
Наконец, с появлением поддержки Linux в SQL Server 2017 возник еще один
компонент под названием Platform Abstraction Layer (PAL, Уровень платформенной абстракции) — прослойка между SQLOS и операционными системами. За
исключением отдельных ситуаций, критичных для производительности, SQLOS
не обращается напрямую к API ОС, а работает через PAL. При этом код самого
SQL Server под Linux практически идентичен коду под Windows, что значительно ускоряет разработку и усовершенствование продукта.
С точки зрения устранения неполадок разница между SQL Server в Windows
и Linux невелика. Конечно, приходится по-разному анализировать экосистему
SQL Server и конфигурацию ОС. Но когда дело касается проблем внутри SQL
Server, обе платформы ведут себя одинаково, поэтому в этой книге я не буду
останавливаться на различиях между ними.
Давайте подробнее поговорим о SQLOS.

SQLOS и модель выполнения
Серверы баз данных должны обрабатывать большое количество пользовательских запросов, и SQL Server — не исключение. На верхнем уровне он распределяет запросы по отдельным потокам, чтобы запросы выполнялись одновременно. Если сервер не простаивает, количество активных потоков больше,
чем количество ЦП в системе. Эффективное планирование — залог высокой
производительности сервера.
В ранних версиях SQL Server использовался планировщик Windows. К сожалению, Windows (и Linux) — это операционные системы общего назначения,
а значит, в них используется вытесняющее планирование. Это значит, что ОС
выделяет на выполнение потока определенный временной интервал, или квант
времени, а когда он истекает, переключается на другие потоки. Это затратная
операция: она требует переключаться между пользовательским режимом и режимом ядра, что отрицательно сказывается на производительности.
В SQL Server 7.0 Microsoft представила первую версию планировщика пользовательского режима (UMS, User Mode Scheduler): это был тонкий слой между
Windows и SQL Server, отвечающий за планирование. В нем использовалось
совместное планирование: потоки SQL Server были запрограммированы так,
чтобы добровольно уступать управление каждые 4 мс, давая другим потокам

54  Глава 2. Модель выполнения SQL Server и статистика ожидания
возможность поработать. Такой подход значительно сократил затраты на переключение режимов.
Отдельные процессы SQL Server, такие как расширенные хранимые процедуры, подпрограммы CLR, внешние языки и некоторые другие процессы,
по-прежнему могут выполняться в режиме вытесняющего планирования.

Microsoft продолжила совершенствовать UMS в SQL Server 2000, а в SQL Server
2005 переработала его, создав более надежную систему SQLOS. В последующих
версиях SQL Server SQLOS стала отвечать за планирование, управление памятью и вводом/выводом, обработку исключений, интеграцию с CLR и внешними
языками, а также некоторые другие задачи.
Когда вы запускаете процесс SQL Server, SQLOS создает набор планировщиков,
которые распределяют рабочую нагрузку между процессорами. Количество
планировщиков совпадает с количеством логических ЦП в системе, и еще один
планировщик создается для выделенного административного соединения (DAC,
Dedicated Admin Connection). Например, если у вас два четырехъядерных физических процессора с гиперпоточностью, то SQL Server создаст 17 планировщиков. На практике можно считать, что планировщик — то же самое, что процессор,
и я буду использовать эти термины взаимозаменяемо на протяжении всей книги.
DAC — это крайняя мера устранения неполадок подключения. Оно позволяет получить доступ к SQL Server, если он перестал отвечать и не принимает обычные соединения. Я расскажу об этом подробнее в главе 13.

Каждый планировщик может находиться в состоянии ONLINE или OFFLINE в зависимости от настроек маски соответствия и модели лицензирования на основе
ядер. Планировщики обычно не перемещаются с одного процессора на другой,
хотя такая миграция возможна, особенно при большой нагрузке. Чаще всего это
не влияет на процесс устранения неполадок.
Планировщики управляют набором рабочих потоков, которые также называются
исполнителями (workers). Максимальное количество исполнителей в системе
определяется параметром конфигурации Max Worker Thread. Значение по умолчанию равно нулю, и при этом SQL Server вычисляет максимальное количество
исполнителей, исходя из количества планировщиков в системе. Обычно менять
значение по умолчанию не требуется да и не рекомендуется, если только вы не
знаете точно, что делаете.
Каждый раз, когда появляется задача, она назначается свободному исполнителю.
Если таких исполнителей нет, планировщик создает нового. Он также уничтожает
простаивающих исполнителей через 15 минут бездействия или когда не хватает

SQLOS и модель выполнения  55
памяти. Каждый исполнитель занимает область в памяти, выделенной для стека
потоков: 512 Кбайт в 32-разрядной версии и 2 Мбайт в 64-разрядной версии
SQL Server. Исполнителей можно рассматривать как логическое представление
потоков в ОС, а задачи — как единицы работы, выполняемые этими потоками.
Исполнители не перемещаются от одного планировщика к другому, а задачи не
перемещаются между исполнителями. Однако SQLOS может создавать дочерние
задачи и назначать их разным исполнителям, например в случае параллельного
плана выполнения. Это объясняет ситуации, когда некоторые планировщики
нагружены сильнее других: отдельные рабочие процессы могут время от времени
получать более ресурсоемкие задачи.
По умолчанию SQL Server назначает задачи исполнителям, обходя узлы NUMA
в циклическом режиме и не учитывая количество планировщиков в этих узлах.
Неравномерное распределение планировщиков по узлам NUMA приводит
к тому, что работа рассредоточивается между планировщиками несбалансированно (я покажу пример такой ситуации в главе 15).
Чаще всего при устранении неполадок мы исследуем именно задачи. Но бывают
исключения, когда задача находится в состоянии PENDING, то есть она создана
и ожидает доступного исполнителя. Это нормальное явление, и обычно исполнители быстро забирают задачи. Однако это также может сигнализировать о весьма
опасном состоянии, когда в системе не хватает рабочих процессов для обработки
запросов. В главе 13 я расскажу, как обнаруживать и решать эту проблему.
Помимо PENDING, задача может находиться в пяти других состояниях:
RUNNING

Задача в данный момент выполняется в планировщике.
RUNNABLE

Задача ожидает, пока планировщик ее запустит.
SUSPENDED

Задача ожидает внешнего события или ресурса.
SPINLOOP

Задача обрабатывает спин-блокировку. Спин-блокировки — это объекты
синхронизации, служащие для защиты некоторых внутренних объектов.
SQL Server может использовать спин-блокировки, когда предполагается,
что доступ к объекту будет предоставлен очень быстро, без переключения
контекста для исполнителей.
DONE

Задача завершена.

56  Глава 2. Модель выполнения SQL Server и статистика ожидания
Первые три состояния — самые важные и распространенные. У каждого планировщика единовременно может быть не более одной задачи в состоянии
RUNNING. Кроме того, у него есть две разные очереди: одна — для задач в состоянии
RUNNABLE и одна — для задач в состоянии SUSPENDED. Когда задаче в состоянии
RUNNING требуются ресурсы — например, страница данных с диска, — она подает запрос ввода/вывода и переходит в состояние SUSPENDED. Задача находится
в очереди SUSPENDED до тех пор, пока запрос не будет выполнен и страница не
будет прочитана. После этого, когда задача готова выполняться дальше, она
перемещается в очередь RUNNABLE.
Возможно, ближайшая аналогия этого процесса из реальной жизни — общая
очередь на кассовом узле в магазине. Здесь кассиры — это планировщики, а покупатели — задачи в очереди RUNNABLE. Покупатель, которого в данный момент
обслуживают на кассе, аналогичен задаче в состоянии RUNNING.
Если на товаре нет штрихкода, кассир посылает работника магазина проверить
цену. Кассир приостанавливает обслуживание текущего покупателя и просит его отойти в сторону (в очередь SUSPENDED). Когда работник возвращается
с информацией о цене, покупатель переходит в конец очереди на кассе (то есть
конец очереди RUNNABLE).
Конечно, на SQL Server эти процедуры выполняются намного эффективнее,
чем в реальном магазине, где клиентам приходится терпеливо ждать в стороне,
пока проверяют цену. (Наверное, покупатель в конце очереди RUNNABLE мечтал
бы о таком же быстродействии, как в SQL Server!)

Статистика ожидания
Если не считать процедур инициализации и утилизации, задача на протяжении
своего жизненного цикла переключается между состояниями RUNNING, SUSPENDED
и RUNNABLE, как показано на рис. 2.2. Общее время выполнения складывается из
трех компонентов: время в состоянии RUNNING, когда задача фактически выполняется, время в состоянии RUNNABLE, когда задача ожидает, пока планировщик
(ЦП) ее запустит, и время в состоянии SUSPENDED, когда задача ожидает ресурсов.
Ожидание свободного
планировщика

Runnable

Running

Suspended
Ожидание ресурса или события

Рис. 2.2. Жизненный цикл задачи

Выполняется
в планировщике

Статистика ожидания  57
В целом главная задача настройки производительности — в том, чтобы повысить
пропускную способность системы, сократив время выполнения запросов. Этого
можно добиться, если уменьшить время нахождения задач запросов в любом из
трех перечисленных состояний.
Время запроса в состоянии RUNNING можно сократить, если модернизировать
аппаратное обеспечение и перейти на более быстрые ЦП, а также если уменьшить
объем задач благодаря оптимизации запросов. Чтобы сократить время RUNNABLE,
можно добавить больше ресурсов ЦП или снизить нагрузку на систему. Но наибольшей экономии обычно удается достичь, если оптимизировать время, когда
задача находится в состоянии SUSPENDED, ожидая ресурсов.
SQL Server отслеживает совокупное время, которое задачи проводят в состоянии SUSPENDED для различных типов ожидания. Эти данные можно просмотреть
в представлении sys.dm_os_wait_stats1, чтобы получить общую картину узких
мест в системе и уточнить стратегию устранения неполадок.
В коде листинга 2.1 показаны типы ожидания, которые занимают больше всего
времени. (Опущены некоторые безобидные типы, в основном связанные с внутренними процессами SQL Server, которые проводят большую часть времени
в ожидании.) Данные собираются с момента последнего перезапуска SQL Server
или с момента последней очистки с помощью команды DBCC SQLPERF('sys.dm_
os_wait_stats', CLEAR). В каждой новой версии SQL Server появляются новые
типы ожидания. Одни из них полезны для устранения неполадок, а другие не
имеет смысл учитывать2.

Листинг 2.1. Получение основных типов ожидания в системе (SQL Server 2012
и более поздние версии)
;WITH Waits
AS
(
SELECT
wait_type, wait_time_ms, waiting_tasks_count,signal_wait_time_ms
,wait_time_ms – signal_wait_time_ms AS resource_wait_time_ms
,100. * wait_time_ms / SUM(wait_time_ms) OVER() AS Pct
,100. * SUM(wait_time_ms) OVER(ORDER BY wait_time_ms DESC) /
NULLIF(SUM(wait_time_ms) OVER(), 0) AS RunningPct
,ROW_NUMBER() OVER(ORDER BY wait_time_ms DESC) AS RowNum
FROM sys.dm_os_wait_stats WITH (NOLOCK)
WHERE
wait_type NOT IN /* Исключаем не интересующие нас системные ожидания */
(N'BROKER_EVENTHANDLER',N'BROKER_RECEIVE_WAITFOR',N'BROKER_TASK_STOP'
1

https://oreil.ly/Nh4s2

2

Код, приведенный в листинге 2.1, подходит для версий SQL Server 2019 и более старых.
Чтобы исключить другие типы ожидания в будущих версиях, обратитесь к документации Microsoft: https://oreil.ly/O4tzq.

58  Глава 2. Модель выполнения SQL Server и статистика ожидания
,N'BROKER_TO_FLUSH',N'BROKER_TRANSMITTER',N'CHECKPOINT_QUEUE',N'CHKPT'
,N'CLR_SEMAPHORE',N'CLR_AUTO_EVENT',N'CLR_MANUAL_EVENT'
,N'DBMIRROR_DBM_EVENT',N'DBMIRROR_EVENTS_QUEUE',N'DBMIRROR_WORKER_QUEUE'
,N'DBMIRRORING_CMD',N'DIRTY_PAGE_POLL',N'DISPATCHER_QUEUE_SEMAPHORE'
,N'EXECSYNC',N'FSAGENT',N'FT_IFTS_SCHEDULER_IDLE_WAIT',N'FT_IFTSHC_MUTEX'
,N'HADR_CLUSAPI_CALL',N'HADR_FILESTREAM_IOMGR_IOCOMPLETION'
,N'HADR_LOGCAPTURE_WAIT',N'HADR_NOTIFICATION_DEQUEUE'
,N'HADR_TIMER_TASK',N'HADR_WORK_QUEUE',N'KSOURCE_WAKEUP',N'LAZYWRITER_SLEEP'
,N'LOGMGR_QUEUE',N'ONDEMAND_TASK_QUEUE'
,N'PARALLEL_REDO_WORKER_WAIT_WORK',N'PARALLEL_REDO_DRAIN_WORKER'
,N'PARALLEL_REDO_LOG_CACHE',N'PARALLEL_REDO_TRAN_LIST'
,N'PARALLEL_REDO_WORKER_SYNC',N'PREEMPTIVE_SP_SERVER_DIAGNOSTICS'
,N'PREEMPTIVE_OS_LIBRARYOPS',N'PREEMPTIVE_OS_COMOPS', N'PREEMPTIVE_OS_PIPEOPS'
,N'PREEMPTIVE_OS_GENERICOPS',N'PREEMPTIVE_OS_VERIFYTRUST'
,N'PREEMPTIVE_OS_FILEOPS',N'PREEMPTIVE_OS_DEVICEOPS'
,N'PREEMPTIVE_OS_QUERYREGISTRY',N'PREEMPTIVE_XE_CALLBACKEXECUTE'
,N'PREEMPTIVE_XE_DISPATCHER',N'PREEMPTIVE_XE_GETTARGETSTATE'
,N'PREEMPTIVE_XE_SESSIONCOMMIT',N'PREEMPTIVE_XE_TARGETINIT'
,N'PREEMPTIVE_XE_TARGETFINALIZE',N'PWAIT_ALL_COMPONENTS_INITIALIZED'
,N'PWAIT_DIRECTLOGCONSUMER_GETNEXT',N'PWAIT_EXTENSIBILITY_CLEANUP_TASK'
,N'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP',N'QDS_ASYNC_QUEUE'
,N'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP'
,N'REQUEST_FOR_DEADLOCK_SEARCH',N'RESOURCE_QUEUE',N'SERVER_IDLE_CHECK'
,N'SLEEP_BPOOL_FLUSH',N'SLEEP_DBSTARTUP',N'SLEEP_DCOMSTARTUP'
,N'SLEEP_MASTERDBREADY',N'SLEEP_MASTERMDREADY',N'SLEEP_MASTERUPGRADED'
,N'SLEEP_MSDBSTARTUP',N'SLEEP_SYSTEMTASK',N'SLEEP_TASK'
,N'SLEEP_TEMPDBSTARTUP',N'SNI_HTTP_ACCEPT',N'SOS_WORK_DISPATCHER'
,N'SP_SERVER_DIAGNOSTICS_SLEEP',N'SQLTRACE_BUFFER_FLUSH'
,N'SQLTRACE_INCREMENTAL_FLUSH_SLEEP',N'SQLTRACE_WAIT_ENTRIES'
,N'STARTUP_DEPENDENCY_MANAGER',N'WAIT_FOR_RESULTS'
,N'WAITFOR',N'WAITFOR_TASKSHUTDOWN',N'WAIT_XTP_HOST_WAIT'
,N'WAIT_XTP_OFFLINE_CKPT_NEW_LOG',N'WAIT_XTP_CKPT_CLOSE',N'WAIT_XTP_RECOVERY'
,N'XE_BUFFERMGR_ALLPROCESSED_EVENT',N'XE_DISPATCHER_JOIN',N'XE_DISPATCHER_WAIT'
,N'XE_LIVE_TARGET_TVF',N'XE_TIMER_EVENT')

)
SELECT
w1.wait_type AS [Wait Type]
,w1.waiting_tasks_count AS [Wait Count]
,CONVERT(DECIMAL(12,3), w1.wait_time_ms / 1000.0) AS [Wait Time]
,CONVERT(DECIMAL(12,1), w1.wait_time_ms / w1.waiting_tasks_count)
AS [Avg Wait Time]
,CONVERT(DECIMAL(12,3), w1.signal_wait_time_ms / 1000.0)
AS [Signal Wait Time]
,CONVERT(DECIMAL(12,1), w1.signal_wait_time_ms / w1.waiting_tasks_count)
AS [Avg Signal Wait Time]
,CONVERT(DECIMAL(12,3), w1.resource_wait_time_ms / 1000.0)
AS [Resource Wait Time]
,CONVERT(DECIMAL(12,1), w1.resource_wait_time_ms / w1.waiting_tasks_count)
AS [Avg Resource Wait Time]
,CONVERT(DECIMAL(6,3), w1.Pct)
AS [Percent]
,CONVERT(DECIMAL(6,3), w1.RunningPct)

Статистика ожидания  59
AS [Running Percent]
FROM
Waits w1
WHERE
w1.RunningPct 50, хотя в современных конфигурациях бывают и системные сеансы с идентификаторами, превышающими 50. Можно
также отфильтровать системные процессы: для этого объедините данные с представлением sys.dm_exec_sessions и используйте там столбец is_user_process.
Наиболее полезные столбцы в этом представлении:
session_id

Идентификатор сеанса. В отличие от sys.dm_os_waiting_tasks, каждому
сеансу соответствует одна строка, если только вы не используете несколько
активных результирующих наборов (MARS, Multiple Active Result Set).
start_time

Время начала запроса.
total_elapsed_time

Продолжительность запроса.
status

Текущий статус запроса (RUNNING, RUNNABLE, SUSPENDED, SLEEPING). Состояние
SLEEPING означает, что соединение простаивает.
wait_type, wait_time, wait_resource, blocking_session_id

Выводятся, если запрос в данный момент приостановлен. Как и в случае
с sys.dm_os_waiting_tasks, столбец blocking_session_id полезен при устранении неполадок активной блокировки в системе.
cpu_time, logical_reads, reads, writes, granted_query_memory, dop

Выводят метрики выполнения.
sql_handle, plan_handle

Позволяют получить инструкцию и план ее выполнения.
1

https://oreil.ly/wJJe2

Динамические административные представления, связанные с моделью выполнения  65
В листинге 2.3 приведен код, который возвращает информацию о текущих запросах, интенсивно использующих ЦП, а также данные о соединении. Этот код
работает в версиях SQL Server 2016 и новее. Для более старых версий можно
удалить столбец ep.dop. Кроме того, для версий ниже, чем SQL Server 2012,
удалите функцию TRY_CONVERT.

Листинг 2.3. Использование представления sys.dm_exec_requests
SELECT
er.session_id
,er.request_id
,DB_NAME(er.database_id) as [database]
,er.start_time
,CONVERT(DECIMAL(21,3),er.total_elapsed_time / 1000.) AS [duration]
,er.cpu_time
,SUBSTRING(
qt.text,
(er.statement_start_offset / 2) + 1,
((CASE er.statement_end_offset
WHEN -1 THEN DATALENGTH(qt.text)
ELSE er.statement_end_offset
END – er.statement_start_offset) / 2) + 1
) AS [statement]
,er.status
,er.wait_type
,er.wait_time
,er.wait_resource
,er.blocking_session_id
,er.last_wait_type
,er.reads
,er.logical_reads
,er.writes
,er.granted_query_memory
,er.dop
,er.row_count
,er.percent_complete
,es.login_time
,es.original_login_name
,es.host_name
,es.program_name
,c.client_net_address
,ib.event_info AS [buffer]
,qt.text AS [sql]
,TRY_CONVERT(XML,p.query_plan) as [query_plan]
FROM
sys.dm_exec_requests er WITH (NOLOCK)
OUTER APPLY sys.dm_exec_input_buffer
(er.session_id, er.request_id) ib
OUTER APPLY sys.dm_exec_sql_text(er.sql_handle) qt
OUTER APPLY
sys.dm_exec_text_query_plan
(
er.plan_handle

66  Глава 2. Модель выполнения SQL Server и статистика ожидания
,er.statement_start_offset
,er.statement_end_offset

) p
LEFT JOIN sys.dm_exec_connections c WITH (NOLOCK) ON
er.session_id = c.session_id
LEFT JOIN sys.dm_exec_sessions es WITH (NOLOCK) ON
er.session_id = es.session_id

WHERE
er.status 'background' AND er.session_id > 50
ORDER BY
er.cpu_time desc
OPTION (RECOMPILE, MAXDOP 1);

Получать план выполнения запроса с помощью функции sys.dm_exec_text_
query_plan — весьма ресурсоемко. Закомментируйте ее, если ваш сервер
работает с большой нагрузкой на процессор.

sys.dm_os_schedulers
Представлением sys.dm_os_schedulers1 я пользуюсь лишь время от времени. Как
можно догадаться по названию, оно содержит информацию о планировщиках
в системе. Его можно применять, чтобы получить сведения о распределении
планировщиков по узлам NUMA, а также чтобы анализировать показатели отдельных планировщиков.
В главе 1 я привел код для первого варианта использования, а теперь рассмотрим
расширенную версию этого сценария (см. листинг 2.4). Проверьте количество
активных планировщиков на каждом узле NUMA, чтобы убедиться, что соответствие ЦП установлено правильно.

Листинг 2.4. Статистика планировщика узла NUMA
SELECT
parent_node_id AS [NUMA Node]
,COUNT(*) AS [Schedulers]
,SUM(IIF(status = N'VISIBLE ONLINE',1,0)) AS [Online Schedulers]
,SUM(IIF(status = N'VISIBLE OFFLINE',1,0)) AS [Offline Schedulers]
,SUM(current_tasks_count) AS [Current Tasks]
,SUM(runnable_tasks_count) AS [Runnable Tasks]
FROM

sys.dm_os_schedulers WITH (NOLOCK)
WHERE
status IN (N'VISIBLE ONLINE',N'VISIBLE OFFLINE')
GROUP BY
parent_node_id
OPTION (RECOMPILE, MAXDOP 1);
1

https://oreil.ly/8CfB0

Обзор регулятора ресурсов  67
В столбцах current_tasks_count и runnable_tasks_count указано количество
задач в очередях RUNNING и RUNNABLE на каждом узле. Большое число runnable_
tasks_count может говорить об узком месте на уровне процессора. Но не забывайте, что эти числа отражают состояние системы в данный конкретный момент
и могут стать неактуальными для следующего момента. Более полную картину
даст сводная информация: например, процентная доля ожидания сигнала
(см. листинг 2.2) или время загрузки ЦП (см. главу 6).
В других столбцах в этом представлении приведены показатели, специфические
для планировщика, например статус, количество исполнителей и задач в разных
состояниях, количество переключений контекста, уровень потребления ЦП
и некоторые другие. Более подробные сведения можно найти в документации.

Обзор регулятора ресурсов
Регулятор ресурсов (Resource Governor) — это инструмент Enterprise Edition, позволяющий разделять и лимитировать различные рабочие нагрузки на сервере.
Он появился довольно давно, но для меня так и остался нишевой функцией:
я редко сталкиваюсь с ним в реальной жизни. (Можно даже пропустить этот раздел и вернуться к нему позже, если понадобится.) Однако не забудьте проверить,
настроен ли регулятор ресурсов в системе, с которой вы работаете, потому что его
неправильная конфигурация может серьезно ухудшить быстродействие сервера.
Когда регулятор ресурсов включен, он разделяет сеансы между различными группами рабочей нагрузки путем вызова функции классификатора во время входа в сеанс. Функция классификатора — это простая пользовательская функция, которая
позволяет выбирать группу рабочей нагрузки по различным свойствам соединения
(идентификатор учетной записи, имя приложения, IP-адрес клиента и т. д.).
У каждой группы рабочей нагрузки есть несколько параметров, например
MAXDOP, максимально допустимое процессорное время на выполнение запроса и максимально допустимое количество одновременных запросов в группе.
Группы также связаны с пулом ресурсов, который позволяет регулировать потребление ресурсов.
В документации по SQL Server пулы ресурсов описаны как «виртуальные экземпляры SQL Server внутри экземпляра SQL Server». Мне это определение
кажется неточным, потому что пулы ресурсов недостаточно изолированы друг от
друга. Тем не менее с ними можно контролировать и ограничивать пропускную
способность ЦП и соответствие планировщиков, а также выделение памяти для
запросов (см. главу 7).
Начиная с SQL Server 2014, можно также регулировать пропускную способность диска, ограничивая для пула ресурсов количество операций ввода/вывода в единицу времени (IOPS). Правда, нельзя контролировать использование
буферного пула: он является общим для всех пулов.

68  Глава 2. Модель выполнения SQL Server и статистика ожидания
Существуют две системные группы рабочих нагрузок: внутренняя группа и группа по умолчанию. С каждой из них связан собственный пул ресурсов. Как вы
можете догадаться, первая группа обрабатывает внутреннюю рабочую нагрузку,
а вторая отвечает за всю неклассифицированную нагрузку. Можно изменять
параметры группы рабочей нагрузки по умолчанию, не создавая лишних пользовательских групп рабочей нагрузки и соответствующих пулов.
На рис. 2.6 показана конфигурация регулятора ресурсов в случае, когда вы хотите
разделить рабочие нагрузки OLTP и отчетов. В результате запросы отчетов будут
меньше влиять на критические транзакции OLTP, которые грозили выжечь ваш
ЦП и устройства ввода/вывода.
Сеанс 1
IMPORTANCE=HIGH
GROUP_MAX_REQUESTS=15
IMPORTANCE=LOW
MAX_DOP=4
GROUP_MAX_REQUESTS=5

Классификация

Внутренняя

По умолчанию

Клиенты

Внутренняя

По умолчанию

OLTP

Административная

Отчеты

Отчеты

Группы рабочих
нагрузок
Пулы ресурсов

MAX_IOPS_PERVOLUME=200
CAP_CPU_PERCENT=50
MIN_IOPS_PER_VOLUME=500

Рис. 2.6. Пример конфигурации регулятора ресурсов
Регулятор ресурсов — полезная штука, но настраивать и обслуживать его непросто.
Если вы захотите лимитировать использование ресурсов для нескольких высоконагруженных пулов ресурсов, это потребует специального планирования и расчетов.
Также нужно время от времени перепроверять настройки, потому что требования
к оборудованию и рабочей нагрузке могут меняться. Недавно мне пришлось
устранять неполадки в ситуации, когда обновление главной дисковой подсистемы не улучшило производительность. Мы обнаружили, что скорость ввода/
вывода в системе была вручную лимитирована параметром MAX_IOPS_PER_VOLUME
в пуле ресурсов.
В заключение замечу, что регулятор ресурсов удобен, когда нужно разделить
различные рабочие нагрузки в одной и той же базе данных на отдельном сервере
или в экземпляре, где используется отказоустойчивая кластеризация. Регулятор ресурсов также помогает уменьшить влияние операций по обслуживанию

Резюме  69
базы данных. Например, с его помощью можно ограничить расход ресурсов
ЦП на сжатие резервных копий или лимитировать нагрузку ввода/вывода от
обслуживания индексов, запуская эти задачи в пуле ресурсов, отдельном от
пользовательских сеансов.
Рекомендую обратить внимание на альтернативные технологии, когда нужно разделить рабочую нагрузку в группах доступности Always On. Вторичные реплики
могут обеспечить лучшую масштабируемость в долгосрочной перспективе. Кроме
того, если требуется разделить рабочие нагрузки нескольких баз данных, работающих на одном экземпляре SQL Server, обычно лучшим решением будет распределить базы данных по нескольким экземплярам и, возможно, виртуализировать их.

Резюме
SQLOS — жизненно важная подсистема, отвечающая за планирование и управление ресурсами в SQL Server. Во время запуска она создает планировщики — по
одному на каждый логический ЦП — и выделяет каждому планировщику пул
рабочих потоков. Пользовательские и системные задачи назначаются рабочим
потокам, которые выполняют всю работу.
В SQL Server используется совместное планирование, когда исполнители добровольно уступают управление через каждые 4 мс. Задачи постоянно перемещаются между состояниями RUNNING (выполняется на ЦП), SUSPENDED (ожидает ЦП)
и RUNNABLE (ожидает ресурсов). SQL Server отслеживает различные типы ожидания
и выводит статистику по ним в представлении sys.dm_os_wait_tasks. Анализировать наиболее выделяющиеся типы ожидания и выявлять узкие места в системе
можно с помощью методики, которая называется статистикой ожидания.
Анализируя ожидания, проявляйте осторожность и не делайте поспешных выводов. Проблемы производительности часто связаны друг с другом и маскируют друг
друга. В ходе анализа важно выявить и подтвердить основную причину проблемы.
В следующей главе мы углубимся в поиск и устранение конкретных неполадок,
начиная с проблем дисковой подсистемы.

Чек-лист устранения неполадок
Пронаблюдать ожидания в системе. Убедиться, что статистика ожидания
репрезентативна.
Проанализировать процентные доли ожидания сигналов и ресурсов.
Проверить конфигурацию регулятора ресурсов, если он используется.
Отсортировать типы ожиданий по продолжительности, чтобы было легче
найти узкие места.

ГЛАВА 3

Производительность
дисковой подсистемы

SQL Server очень интенсивно занимается вводом/выводом: данные постоянно
считываются с диска и записываются на него. Для производительности и исправной работы SQL Server важна хорошая пропускная способность ввода/
вывода. К сожалению, во многих экземплярах SQL Server, даже с современными хранилищами на основе флеш-памяти, быстродействие ограничено именно
вводом/выводом.
В этой главе я покажу, как анализировать и устранять проблемы с производительностью дисковой подсистемы. Вы познакомитесь с внутренними механизмами обработки запросов в SQL Server и научитесь обнаруживать возможные
узкие места во всем стеке ввода/вывода: на уровне SQL Server, ОС, виртуализации и хранилища.
Затем пойдет речь о настройке контрольных точек, с которыми связаны типичные узкие места ввода/вывода в высоконагруженных системах OLTP.
Наконец, я перечислю наиболее распространенные типы ожидания, связанные
с вводом/выводом.

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

Как устроена подсистема ввода/вывода SQL Server  71
В SQL Server используются кратковременные блокировки (latches), которые
защищают внутренние объекты в памяти от повреждения, если несколько потоков пытаются изменять их одновременно. Два самых распространенных типа
кратковременных блокировок — монопольные (EX latch), которые блокируют
любой доступ к объекту, и совмещаемые (SH latch), которые разрешают одновременное чтение, но не дают изменять объекты.
Концептуально кратковременные блокировки похожи на критические секции
или мьютексы в программировании. Подробнее о кратковременных блокировках
я расскажу в главе 10.
В общем случае страницы данных в буферном поле размещаются не в том порядке, в котором они хранятся в файлах базы данных. Несмотря на это, SQL Server
эффективно находит страницы данных в буферном пуле. Когда SQL Server обращается к странице в пуле, он выполняет логическое чтение. Если страницы нет
в памяти и ее нужно прочитать с диска, происходит также физическое чтение.
Когда нужно изменить данные, SQL Server создает записи журнала и заносит их
в файл журнала транзакций, а затем модифицирует страницы в буферном пуле,
помечая их как «грязные». Сервер асинхронно сохраняет «грязные» страницы
в файлы данных в контрольных точках, а иногда в процессах отложенной записи.
Мы обсудим оба этих процесса позже в этой главе, а журналы транзакций —
в главе 11. Имейте в виду: чтобы модифицировать данные, SQL Server вынужден
считывать страницы данных с диска, если только они не были кэшированы.
Теперь подробнее поговорим о том, как SQL Server работает с вводом/выводом.

Планирование и ввод/вывод
В главе 2 мы говорили, что в SQL Server используется совместное планирование,
и на процессорах поочередно работает по несколько исполнителей, или рабочих
процессов (workers). Они добровольно уступают управление по истечении 4 мс,
позволяя другим исполнителям продолжить работу. Эта схема требует, чтобы
ввод/вывод в SQL Server происходил максимально асинхронно: если бы исполнители ждали результатов запросов ввода/вывода, они не давали бы работать
другим исполнителям.
По умолчанию все планировщики SQL Server обрабатывают запросы ввода/
вывода. Это поведение можно перенастроить: привязать ввод/вывод к определенным ЦП, установив маску соответствия ввода/вывода. Теоретически это
может улучшить пропускную способность ввода/вывода в высоконагруженных
системах OLTP, однако чаще всего мне эта мера не кажется нужной. Обычно
гораздо лучшие результаты приносят оптимизация и снижение нагрузки на
ЦП и ввод/вывод.

72  Глава 3. Производительность дисковой подсистемы
О масках соответствия можно почитать в документации Microsoft1.

У каждого планировщика есть своя выделенная очередь ввода/вывода. Когда
исполнителю надо выполнить операцию ввода/вывода, он создает структуру
запроса ввода/вывода, добавляет ее в очередь планировщика и выполняет
асинхронный вызов ввода/вывода через API операционной системы. После
этогоисполнитель уже не ждет, пока запрос завершится: он продолжает работать, занимаясь другими делами, или приостанавливает работу и перемещается
в очередь SUSPENDED.
Когда в планировщике начинает выполняться новый рабочий процесс (переключаясь в состояние RUNNING ), он проходит через очередь ввода/вывода
планировщика. Структуры запроса ввода/вывода содержат всю необходимую
информацию, чтобы проверить, завершился ли асинхронный вызов API ОС,
и хранят указатель на функцию обратного вызова, которую исполнитель вызывает по завершении запроса.
Ну да, звучит сложно. Пожалуйста, еще немного терпения: мы рассмотрим
подробности в следующем разделе. Вот ключевые моменты, которые я советую
запомнить:
Все активные планировщики по умолчанию обрабатывают запросы ввода/
вывода.
Большинство запросов ввода/вывода в SQL Server работают через асинхронные вызовы API ОС. Это касается даже протоколирования с опережающей записью: исполнитель, который инициировал инструкцию COMMIT,
может быть приостановлен, пока запись журнала не запишется на диск; однако команда записи API ОС выполнится асинхронно.
Запрос ввода/вывода может выполнять не тот исполнитель, который вызвал этот запрос.
Список ожидающих запросов ввода/вывода можно найти в представлении sys.
dm_io_pending_io_requests. В столбце io_pending_ms_ticks показана продолжительность запроса. Столбец io_pending указывает, завершился ли вызов API
ОС и ожидает ли запрос, чтобы исполнитель его закончил. Это может помочь
разобраться, влияет ли загрузка ЦП на задержку запросов.
А теперь, как я и обещал, давайте еще раз рассмотрим процедуру ввода/вывода
на более конкретных примерах чтения страниц данных с диска.

1

https://oreil.ly/DXXmP

Как устроена подсистема ввода/вывода SQL Server  73

Чтение данных
Когда SQL Server нужна страница данных, он проверяет, есть ли она уже в буферном пуле. Если нет, исполнитель выделяет для страницы буфер, защищая
ее монопольной кратковременной блокировкой. Это не позволяет рабочим
процессам обращаться к странице, пока она не будет прочитана.
После этого исполнитель создает структуру запроса ввода/вывода, помещает ее
в очередь ввода/вывода планировщика и инициирует запрос чтения через API
ОС. Затем он пытается получить еще одну общую кратковременную блокировку
в буфере, но буфер уже заблокирован несовместимой монопольной блокировкой.
Поэтому рабочий процесс приостанавливается, переходя в состояние ожидания
PAGEIOLATCH (это показано на рис. 3.1).
Когда другой исполнитель переходит в состояние RUNNING, он проверяет, нет ли
в очереди планировщика выполненных запросов ввода/вывода. Если есть, то
исполнитель вызывает функцию обратного вызова для завершения операции:
функция проверяет, что страница не повреждена, и удаляет монопольную крат­
ковременную блокировку из буфера. Исполнитель, который отправил запрос
ввода/вывода, может возобновить работу и получить доступ к странице данных
(рис. 3.2).
Буфер
1:5369

Исполнитель
1
Установить монопольную
кратковременную
блокировку на буфере
2

bPage
bStats

Страница
в 8 Кбайт
данных

bLatch

Поместить запрос
ввода/вывода в очередь
3
Установить совмещаемую
кратковременную
блокировку на буфере

Приостановка работы
с ожиданием PAGEIOLATCH

BUF
ReadFileScatter()
Функция
обратного вызова

I/O request

MDF
Асинхронный
вызов ввода-вывода

Очередь ввода/вывода
планировщика
sys.dm_io_pending_io_requests

Рис. 3.1. Как инициируется чтение страницы данных с диска

74  Глава 3. Производительность дисковой подсистемы

Буфер
1:5369

Исполнитель

bPage

Страница
в 8 Кбайт
данных

Исполнитель

bStats
bLatch

5

Проверить,
завершился ли вызов
6

Установить совмещаемую
кратковременную
блокировку на буфере

Вызвать функцию обратного
вызова, если запрос выполнен

8

7

Перейти в очередь RUNNABLE
и продолжить выполнение

BUF
ReadFileScatter()

Завершить запрос
и снять монопольную
кратковременную блокировку

Функция
обратного вызова
Запрос ввода/вывода
Очередь ввода/вывода
планировщика

Рис. 3.2. Как завершается чтение страницы данных с диска
Во время запросов ввода/вывода могут возникнуть определенные ошибки. Все
они серьезные, и для них нужно настроить уведомления.
Ошибка 823 означает, что вызов API ввода/вывода ОС завершился неудачно. Часто это признак аппаратных проблем.
Ошибки 605 и 824 указывают на проблемы с логической согласованностью
страниц данных. Если вы столкнулись с одной из этих ошибок, немедленно
проверьте с помощью команды DBCC CHECKDB, не повреждена ли база данных.
Эти ошибки также бывают из-за неисправных драйверов ввода/вывода, которые могут повредить страницы данных во время передачи.
Ошибка 833 означает, что возврат из запроса ввода/вывода (вызов API ОС)
занял больше 15 секунд. Это ненормально: в случае такой ошибки проверьте, исправна ли дисковая подсистема.
Ошибка 825 означает, что запрос ввода/вывода не сработал, и его надо повторить. Как и в случае с ошибкой 833, проверьте дисковую подсистему.
Устраняя эти ошибки, подробности можно уточнять в журнале ошибок SQL
Server (с помощью кода из листинга 1.4) и в журнале системных событий.
SQL Server часто читает несколько страниц данных одним запросом ввода/
вывода. Например, он использует логику упреждающего чтения, считы-

Как устроена подсистема ввода/вывода SQL Server  75
вая несколько страниц данных во время просмотра. В результате запрос
может выполнять тысячи логических операций чтения всего за несколько
физических операций. Еще один пример — чтение с ускорением (ramp-up
reads), когда во время запуска SQL Server считывает большое количество
страниц при ­каждом запросе ввода/вывода, пытаясь быстро заполнить буферный пул.

Запись данных
SQL Server обрабатывает запись данных примерно так же, как чтение. В большинстве случаев запись выполняется асинхронно с помощью очередей ввода/
вывода планировщика, как было показано в предыдущих примерах. Очевидно,
что в разных операциях ввода/вывода функция обратного вызова реализуется
по-разному.
Когда вы модифицируете данные в базе, SQL Server модифицирует страницы
данных в буферном пуле, при необходимости читая страницы с диска. Он регистрирует изменения и заносит соответствующие записи в журнал транзакций.
Транзакция не считается принятой, пока записи журнала не сохранятся на
диске. Хотя формально можно рассматривать опережающее протоколирование
как синхронную запись, в SQL Server для записи в журнал используется асинхронный ввод/вывод.
SQL Server асинхронно записывает измененные страницы данных с помощью
процесса контрольных точек (check-point process): он находит «грязные» страницы данных в буферном пуле и сохраняет их на диск. Он пытается свести
к минимуму количество запросов к диску, по возможности объединяя смежные
измененные страницы и записывая их за одну операцию ввода/вывода.
Существует и другой процесс SQL Server, называемый отложенной записью,
который периодически очищает буферный пул: он удаляет страницы данных,
к которым в последнее время не обращались, и тем самым высвобождает память.
В нормальных условиях процесс отложенной записи не обрабатывает «грязные»
страницы данных, однако иногда может тоже записывать их на диск, если в системе не хватает памяти.
Как всегда, бывают исключения. Например, во время операции массового
импорта SQL Server выделяет набор буферов в буферном пуле и использует
их повторно, записывая данные в базу вне контрольной точки. Он сохраняет
содержимое буферного пула, чтобы тот не очищался при импорте больших
объемов данных.
Ввод/вывод в контрольных точках может вызывать проблемы в высоконагруженных системах. Я расскажу о настройке контрольных точек позже в этой главе,
но сначала давайте целостно рассмотрим всю подсистему хранения.

76  Глава 3. Производительность дисковой подсистемы

Подсистема хранения: целостный обзор
Бороться с медленным вводом/выводом в SQL Server — непростая задача.
Я видел немало жарких споров между специалистами по базам данных и по
инфраструктуре. Специалисты по базам данных обычно жалуются на низкую производительность диска, а специалисты по устройствам хранения
анализируют микроскопические показатели задержки от устройств SAN
и настаивают, что во всем виноват SQL Server. На самом деле и те и другие
неправы. Обычно они совершают одну и ту же ошибку: рассматривают систему
хранения ­чересчур упрощенно, будто она состоит всего из пары компонентов.
Это не так.
На рис. 3.3 изображена схема сетевой подсистемы хранения данных, показанная
на самом верхнем уровне, без особых подробностей. (На схеме также упоминаются отдельные средства устранения неполадок, которые мы рассмотрим позже.
На них пока можно не заострять внимания.) Смысл здесь в том, что плохую
производительность ввода/вывода может вызвать любой компонент, поэтому
нужно проанализировать все уровни в стеке.
Количество операций в секунду: страниц контрольных точек, фоновых записей страниц,
отложенных записей, чтений страниц, очищенных байтов журнала
SQL server
Задержка файлов хранилища,
IOPS и метрики
пропускной способности
Длина очереди

ОС
Хост виртуальной
машины
Хост-адаптер
шины

Сеть

Хранилище

Средняя длина очереди на диске
Среднее время чтения
Среднее время записи
Количество чтений в секунду
Байтов чтения в секунду
Количество записей в секунду
Байтов записи в секунду

Задержка хранилища данных,
IOPS и метрики пропускной способности

Рис. 3.3. Сетевая подсистема хранения
Также можно использовать хранилище с прямым подключением (DAS, directattached storage). В этой конфигурации устройство хранения либо монтируется
локально на сервере (советую использовать контроллеры NVMe), либо подключается к нему напрямую. Эта схема исключает сеть из пути к хранилищу
и может повысить производительность ввода/вывода. Но есть и недостаток: вы
лишаетесь гибкости, которая во внешнем хранилище позволяла бы добавлять
дисковое пространство и выполнять обслуживание прозрачно для сервера.

Подсистема хранения: целостный обзор  77
У каждой подсистемы хранения есть переломный момент, после которого задержка запросов ввода/вывода начинает экспоненциально расти с увеличением
пропускной способности и IOPS. Например, при рабочей нагрузке 1000 IOPS
вы можете получить ответ за 1 мс, а при нагрузке 50 000 IOPS — уже за 3 мс.
А затем, когда на уровне 100 000 IOPS наступит переломный момент, значения
задержки станут двузначными или даже трехзначными.
У каждого компонента в стеке — свой переломный момент. Например, при малой
емкости очереди в хост-адаптере шины могут возникнуть очереди на уровне
контроллера, если будет расти количество запросов ввода/вывода. В этом случае
SQL Server будет страдать от высоких задержек и плохой производительности
ввода/вывода, но при этом никакие метрики SAN не покажут ни малейших
задержек.
Чтобы проверять производительность подсистемы хранения, можно использовать утилиту DiskSpd, которая эмулирует рабочую нагрузку SQL Server в системе. Утилиту можно скачать с GitHub1.
Как я уже отмечал, при борьбе с плохой производительностью ввода/вывода
нужно проверить все компоненты подсистемы хранения. При этом стоит начать с того, чтобы проанализировать общую задержку хранилища и количество
данных, которые SQL Server считывает и записывает в единицу времени. В этом
поможет представление sys.dm_io_virtual_file_stats.

Представление sys.dm_io_virtual_file_stats
Представление sys.dm_io_virtual_file_stats2 — самый важный инструмент
для устранения неполадок с производительностью ввода/вывода. Оно дает
статистику ввода/вывода по файлу базы данных, включая количество операций
ввода/вывода, объем прочитанных и записанных данных, а также информацию
о задержках, связанных с выполнением запросов ввода/вывода.
Данные в этом представлении кумулятивны и рассчитываются с момента перезапуска SQL Server. Сделайте два снимка данных и вычислите разницу между
ними (для этого можно использовать код из листинга 3.1). Этот код отфильтровывает файлы базы данных с малой активностью ввода/вывода, потому что их
показатели обычно искажены и не очень полезны.

Листинг 3.1. Использование представления sys.dm_io_virtual_file_stats
DROP TABLE IF EXISTS #Snapshot;
GO

1

https://aka.ms/diskspd

2

https://oreil.ly/mJCKm

78  Глава 3. Производительность дисковой подсистемы
CREATE TABLE #Snapshot
(
database_id SMALLINT NOT NULL,
file_id SMALLINT NOT NULL,
num_of_reads BIGINT NOT NULL,
num_of_bytes_read BIGINT NOT NULL,
io_stall_read_ms BIGINT NOT NULL,
num_of_writes BIGINT NOT NULL,
num_of_bytes_written BIGINT NOT NULL,
io_stall_write_ms BIGINT NOT NULL
);
INSERT INTO #Snapshot(database_id,file_id,num_of_reads,num_of_bytes_read
,io_stall_read_ms,num_of_writes,num_of_bytes_written,io_stall_write_ms)
SELECT database_id,file_id,num_of_reads,num_of_bytes_read
,io_stall_read_ms,num_of_writes,num_of_bytes_written,io_stall_write_ms
FROM sys.dm_io_virtual_file_stats(NULL,NULL)
OPTION (RECOMPILE);
-- Set test interval (1 minute).
WAITFOR DELAY '00:01:00.000';
;WITH Stats(db_id, file_id, Reads, ReadBytes, Writes
,WrittenBytes, ReadStall, WriteStall)
as
(
SELECT
s.database_id, s.file_id
,fs.num_of_reads – s.num_of_reads
,fs.num_of_bytes_read – s.num_of_bytes_read
,fs.num_of_writes – s.num_of_writes
,fs.num_of_bytes_written – s.num_of_bytes_written
,fs.io_stall_read_ms – s.io_stall_read_ms
,fs.io_stall_write_ms – s.io_stall_write_ms
FROM
#Snapshot s JOIN sys.dm_io_virtual_file_stats(NULL, NULL) fs ON
s.database_id = fs.database_id and s.file_id = fs.file_id
)
SELECT
s.db_id AS [DB ID], d.name AS [Database]
,mf.name AS [File Name], mf.physical_name AS [File Path]
,mf.type_desc AS [Type], s.Reads
,CONVERT(DECIMAL(12,3), s.ReadBytes / 1048576.) AS [Read MB]
,CONVERT(DECIMAL(12,3), s.WrittenBytes / 1048576.) AS [Written MB]
,s.Writes, s.Reads + s.Writes AS [IO Count]
,CONVERT(DECIMAL(5,2),100.0 * s.ReadBytes /
(s.ReadBytes + s.WrittenBytes)) AS [Read %]
,CONVERT(DECIMAL(5,2),100.0 * s.WrittenBytes /
(s.ReadBytes + s.WrittenBytes)) AS [Write %]
,s.ReadStall AS [Read Stall]
,s.WriteStall AS [Write Stall]
,CASE WHEN s.Reads = 0

Подсистема хранения: целостный обзор  79
THEN 0.000
ELSE CONVERT(DECIMAL(12,3),1.0 * s.ReadStall / s.Reads)
END AS [Avg Read Stall]
,CASE WHEN s.Writes = 0
THEN 0.000
ELSE CONVERT(DECIMAL(12,3),1.0 * s.WriteStall / s.Writes)
END AS [Avg Write Stall]

FROM

Stats s JOIN sys.master_files mf WITH (NOLOCK) ON
s.db_id = mf.database_id and
s.file_id = mf.file_id
JOIN sys.databases d WITH (NOLOCK) ON
s.db_id = d.database_id
WHERE -- Only display files with more than 20MB throughput
(s.ReadBytes + s.WrittenBytes) > 20 * 1048576
ORDER BY
s.db_id, s.file_id
OPTION (RECOMPILE);

На рис. 3.4 показан вывод этого представления.
Нужно, чтобы показатели задержек (stalls) оставались как можно ниже. Невозможно определить пороговые значения, которые подходили бы ко всем системам, но я обычно стараюсь, чтобы задержки записи журналов транзакций не
превышали 1–2 мс, а задержки чтения и записи файлов данных — 3–5 мс, если
используется сетевое хранилище. Если применять современные накопители
с прямым подключением, то задержки могут стать даже меньше миллисекунды.
Затем проанализируйте пропускную способность системы. Высокие задержки
в сочетании с низкой пропускной способностью обычно указывают на проблемы,
не связанные непосредственно с SQL Server. Не забудьте изучить пропускную
способность для всех файлов, которые используют один и тот же диск или контроллер. Высокая пропускная способность для одних файлов может повлиять
на показатели других файлов, использующих тот же ресурс.
Обычно пропускная способность коррелирует с задержками: чем больше данных читается или записывается, тем выше задержка. Эта корреляция обычно
линейна, пока не будет достигнут переломный момент, после которого задержка
быстро увеличивается.
Большое количество операций чтения и задержек чтения в файлах данных часто сопровождается значительным процентом ожиданий PAGEIOLATCH и низкой
ожидаемой продолжительностью существования страниц (Page Life Expectancy).
Это признак того, что с диска постоянно считываются большие объемы данных.
Нужно разобраться, почему это происходит. В большинстве случаев это связано
с неоптимизированными запросами, которые злоупотребляют слишком объемными операциями просмотра при чтении данных с диска. Как обнаружить эти
запросы, мы поговорим в главе 4.

80  Глава 3. Производительность дисковой подсистемы

Рис. 3.4. Пример вывода представления sys.dm_io_virtual_file_stats
Не отвергайте и вероятность того, что серверу не хватает ресурсов и на нем недостаточно памяти, чтобы разместить активный набор данных. В любом случае
добавить дополнительную память — вполне приемлемый способ снизить нагрузку на ввод/вывод и повысить производительность системы. Очевидно, что
этот способ — не наилучший, но часто бывает проще и дешевле решить проблему
с помощью аппаратных средств.
В базах данных пользователей большое количество операций записи и задержек
записи в файлы данных часто свидетельствует о том, что контрольные точки
настроены неэффективно. Можно добиться улучшений, если отрегулировать
конфигурацию контрольных точек, как я покажу позже в этой главе. В долгосрочной перспективе стоит проанализировать, получится ли уменьшить ко-

Подсистема хранения: целостный обзор  81
личество страниц данных, которые SQL Server записывает на диск. Для этого
можно удалить ненужные индексы, сократить количество разбиений страниц
(используйте для этого параметр FILLFACTOR, а также настройте стратегию обслуживания индекса), уменьшить количество страниц данных за счет сжатия
данных и, возможно, провести рефакторинг схемы базы данных и приложений.
Если высокая пропускная способность сопровождается задержками в базе
данных tempdb — определите, что их вызывает. Три наиболее распространенные
причины — это активность хранилища версий, массовые переносы в базу данных
tempdb и злоупотребление временными объектами. Я расскажу об этом в главе 9.
Наконец, можно получить представление о задержке ввода/вывода, изучив время
ожидания ресурса в PAGEIOLATCH и другие ожидания, связанные с вводом/выводом.
При этом вы не получите подробной информации по каждому файлу, но это полезно, чтобы оценить производительность ввода/вывода в масштабе всей системы.

Счетчики производительности и метрики ОС
Представление sys.dm_io_virtual_file_stats содержит полезную и подробную информацию и подсказывает направление для дальнейшего устранения
неполадок ввода/вывода, но у него есть ограничение: оно усредняет данные за
некоторый интервал времени.
Это приемлемо, когда задержка ввода/вывода невелика. Однако если значения
задержки высоки, то нужно определить, тормозит система в принципе или
данные искажены из-за отдельных всплесков активности. Для этого можно
просмотреть метрики, показывающие связь между производительностью SQL
Server и диска.
Процесс устранения неполадок в Windows и Linux немного различается.
В Windows самое простое средство анализа метрик — известная утилита PerfMon
(Монитор ресурсов). Можно параллельно просматривать счетчики производительности SQL Server и ввода/вывода и сопоставлять их показатели.
В табл. 3.1 приведены счетчики производительности, которые нужно проанализировать.
Первым делом я обычно смотрю на задержки Avg Disk sec/Read и Avg Disk sec/
Write, а также Avg Disk Queue Length. Если на графиках этих показателей наблюдаются пики, то я добавляю счетчики, специфичные для SQL Server, чтобы
понять, какие процессы приводят ко всплескам активности.
На рис. 3.5 приведен пример, где можно увидеть корреляцию между Checkpoint
pages/sec, Avg Disk sec/Write и Avg Disk Queue Length. Отсюда легко заключить,
что подсистема ввода/вывода не справляется со всплесками операций записи,
вызванных процессом контрольной точки.

82  Глава 3. Производительность дисковой подсистемы
Таблица 3.1. Счетчики производительности, связанные с вводом/выводом
Объект

Счетчики производительности

Описание

Физический
диск

Avg Disk Queue Length Avg

Среднее количество запросов ввода/вывода
(всего, чтения и записи соответственно), поставленных в очередь в течение выборочного
интервала. Эти показатели должны быть как
можно ниже. Пики соответствуют запросам
ввода/вывода, которые ставятся в очередь
на уровне ОС

Disk Read Queue Length Avg
Disk Write Queue Length

Current Disk Queue Length

Длина очереди ввода/вывода в момент измерения

Avg Disk sec/Transfer

Средняя задержка дисковых операций за выборочный интервал времени. Эти цифры, как
правило, аналогичны показателям задержки
из представления sys.dm_io_virtual_
file_stats за тот же период. Однако sys.
dm_io_virtual_file_stats обычно измеряется за
большие интервалы времени, а перечисленные
здесь показатели дают понять, типичны ли высокие задержки в целом или они проявляются
отдельными пиками

Avg Disk sec/Read
Avg Disk sec/Write

Disk Transfers/sec
Disk Reads/sec
Disk Writes/sec
Disk Bytes/sec
Disk Read Bytes/sec

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

Disk Write Bytes/sec
Avg Disk Bytes/Transfer
Avg Disk Bytes/Read
Avg Disk Bytes/Write

SQL Server:
Диспетчер
буфера

Checkpoint pages/sec
Background writer pages/sec

Средний размер запросов ввода/вывода; позволяет анализировать характерные схемы ввода/
вывода в системе
Число «грязных» страниц, записанных процессом
контрольных точек

Lazy writer/sec

Число страниц, записанных процессом отложенной записи

Page reads/sec

Количество физических операций чтения
и записи

Page writes/sec
Readahead pages/sec

Число страниц, прочитанных процессом упреждающего чтения

Подсистема хранения: целостный обзор  83

Объект

Счетчики производительности

Описание

SQL Server:
Базы данных

Log Bytes Flushed/sec

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

Log Flush Write Time (ms)
Log Flushes/sec

SQL Server:
Статистика
SQL

Batch Requests/sec

SQL Server:
Базы данных

Transactions/sec

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

Рис. 3.5. Контрольные точки и дисковая очередь
Обратите внимание, какие еще приложения установлены на сервере. Возможно,
именно они виноваты во всплесках активности ввода/вывода или других проблемах.
В Linux нет утилиты PerfMon, зато доступно множество других бесплатных и коммерческих средств мониторинга. Еще можно использовать такие инструменты,
как iostat, dstat и iotop, которые включены в основные дистрибутивы Linux.

84  Глава 3. Производительность дисковой подсистемы
Они позволяют оценить показатели производительности диска для каждого
процесса или всей системы.
На стороне SQL Server счетчики производительности можно посмотреть
в представлении sys.dm_os_performance_counters1. В листинге 3.2 показано, как это сделать. Измените префикс SQLserver в столбце object_name на
MSSQL$ , если вы запускаете код на именованном
экземпляре SQL Server.

Листинг 3.2. Использование представления sys.dm_os_performance_counters
DROP TABLE IF EXISTS #PerfCntrs;
GO
CREATE TABLE #PerfCntrs
(
collected_time DATETIME2(7) NOT NULL DEFAULT SYSDATETIME(),
object_name SYSNAME NOT NULL,
counter_name SYSNAME NOT NULL,
instance_name SYSNAME NOT NULL,
cntr_value BIGINT NOT NULL,
PRIMARY KEY (object_name, counter_name, instance_name)
);
;WITH Counters(obj_name, ctr_name)
AS
(
SELECT C.obj_name, C.ctr_name
FROM
(
VALUES
('SQLServer:Buffer Manager','Checkpoint pages/sec')
,('SQLServer:Buffer Manager','Background writer pages/sec')
,('SQLServer:Buffer Manager','Lazy writes/sec')
,('SQLServer:Buffer Manager','Page reads/sec')
,('SQLServer:Buffer Manager','Page writes/sec')
,('SQLServer:Buffer Manager','Readahead pages/sec')
,('SQLServer:Databases','Log Flushes/sec') -- For all DBs
,('SQLServer:Databases','Log Bytes Flushed/sec') -- For all DBs
,('SQLServer:Databases','Log Flush Write Time (ms)') -- For all DBs
,('SQLServer:Databases','Transactions/sec') -- For all DBs
,('SQLServer:SQL Statistics','Batch Requests/sec')
) C(obj_name, ctr_name)
)
INSERT INTO #PerfCntrs(object_name,counter_name,instance_name,cntr_value)
SELECT
pc.object_name, pc.counter_name, pc.instance_name, pc.cntr_value
FROM
sys.dm_os_performance_counters pc WITH (NOLOCK) JOIN Counters c ON
1

https://oreil.ly/DJ4f5

Подсистема хранения: целостный обзор  85
pc.counter_name = c.ctr_name AND pc.object_name = c.obj_name;
WAITFOR DELAY '00:00:01.000';
;WITH Counters(obj_name, ctr_name)
AS
(
SELECT C.obj_name, C.ctr_name
FROM
(
VALUES
('SQLServer:Buffer Manager','Checkpoint pages/sec')
,('SQLServer:Buffer Manager','Background writer pages/sec')
,('SQLServer:Buffer Manager','Lazy writes/sec')
,('SQLServer:Buffer Manager','Page reads/sec')
,('SQLServer:Buffer Manager','Page writes/sec')
,('SQLServer:Buffer Manager','Readahead pages/sec')
,('SQLServer:Databases','Log Flushes/sec') -- For all DBs
,('SQLServer:Databases','Log Bytes Flushed/sec') -- For all DBs
,('SQLServer:Databases','Log Flush Write Time (ms)') -- For all DBs
,('SQLServer:Databases','Transactions/sec') -- For all DBs
,('SQLServer:SQL Statistics','Batch Requests/sec')
) C(obj_name, ctr_name)
)
SELECT
pc.object_name, pc.counter_name, pc.instance_name
,CASE pc.cntr_type
WHEN 272696576 THEN
(pc.cntr_value – h.cntr_value) * 1000 /
DATEDIFF(MILLISECOND,h.collected_time,SYSDATETIME())
WHEN 65792 THEN
pc.cntr_value
ELSE NULL
END as cntr_value
FROM
sys.dm_os_performance_counters pc WITH (NOLOCK) JOIN Counters c ON
pc.counter_name = c.ctr_name AND pc.object_name = c.obj_name
JOIN #PerfCntrs h ON
pc.object_name = h.object_name AND
pc.counter_name = h.counter_name AND
pc.instance_name = h.instance_name
ORDER BY
pc.object_name, pc.counter_name, pc.instance_name
OPTION (RECOMPILE);

Можно также добавить к анализу представление sys.dm_io_virtual_file_
stats, собирая из него данные и счетчики производительности ежесекундно.
Подход будет аналогичен тому, который мы только что обсуждали: изучить
корреляцию между задержкой диска и активностью операций и оценить общую производительность подсистемы ввода/вывода, определяя критические
точки в нагрузке.

86  Глава 3. Производительность дисковой подсистемы

Виртуализация, хост-адаптер шины и уровни хранения
В стеке хранилища имеет смысл проанализировать еще несколько уровней, помимо ОС. Это уровень виртуализации, конфигурация контроллера HBA/SCSI
и само физическое устройство хранения.
SQL Server, как правило, работает в общих средах. Он делит устройства хранения и сетевую инфраструктуру с другими клиентами, а при виртуализации
работает на одном физическом хосте с другими виртуальными машинами. Как
я уже говорил ранее в этой книге, при использовании виртуализации следует
убедиться, что хост не перегружен, иначе вы рискуете столкнуться с самыми
разными проблемами производительности.
За исключением самых простых конфигураций SQL Server, использующих
локальное хранилище, запросы ввода/вывода сериализуются и отправляются
по сети. Здесь бывают две типичные проблемы.
Первая — недостаточная емкость очереди где-то на пути ввода/вывода. К сожалению, вместимости очереди по умолчанию может не хватить для очень
ресурсоемкой рабочей нагрузки ввода/вывода. Вам нужно будет проверить и,
возможно, увеличить этот параметр в настройках хранилища данных, контроллера vSCSI и хост-адаптера шины (HBA). Типичный признак недостаточной
емкости очереди — низкая задержка в хранилище в сочетании с гораздо более
высокой задержкой в виртуальной машине и/или ОС при наличии дисковой
очереди.
Вторая проблема — «шумные соседи». Когда на одном хосте работает несколько
виртуальных машин с интенсивным вводом/выводом, они могут влиять друг на
друга. Аналогично, если несколько серверов с высокой пропускной способностью используют общую сеть и общее хранилище, эти ресурсы могут оказаться
перегружены. К сожалению, устранение неполадок, связанных с «шумными
соседями», — это всегда сложная задача, в рамках которой приходится анализировать многие компоненты инфраструктуры.
У каждого хранилища есть ограничение на количество ожидающих запросов,
которые оно может обработать. Если увеличить емкость очереди на высоконагруженном сервере, от этого может вырасти число ожидающих запросов
в хранилище. Таким образом вы просто перенесете узкое место с одного
уровня на другой, особенно если хранилище обслуживает запросы от большого числа высоконагруженных систем.

Хост виртуализации и хранилище предоставляют данные о своей пропускной
способности, числе операций ввода/вывода в секунду и задержках. На уровнях
виртуализации эти метрики могут различаться в зависимости от используемой
технологии. Например, в Hyper-V можно использовать обычные показатели

Настройка контрольных точек  87
производительности диска на хосте. В VMware можно получить данные с помощью утилиты ESXTOP, о которой я расскажу в главе 15. В любом случае подход к устранению неполадок очень похож на то, что мы уже обсуждали: нужно
изучить доступные метрики, сопоставить их показания и найти узкие места на
пути ввода/вывода.
Наконец, проверьте конфигурацию хранилища. Производители устройств
хранения обычно публикуют рекомендации о том, как настраивать SQL Server
для работы с их продукцией: эти рекомендации послужат хорошей отправной
точкой. Однако обратите внимание на то, чтобы размер единицы распределения
дискового пространства согласовывался с шириной полосы чередования RAID
и смещением раздела.
Например, смещение раздела 1024 Мбайт, блок диска 4 Кбайт, единица распределения 64 Кбайт и полосы RAID по 128 Кбайт идеально согласуются друг
с другом, и при этом каждый запрос ввода/вывода обслуживается одним диском.
Однако полосы RAID по 96 Кбайт будут распределять 64-килобайтовые единицы по двум дискам, что приведет к дополнительным запросам ввода/вывода
и может серьезно ухудшить производительность.
Снова подчеркнем, что всегда полезно работать совместно со специалистами по
инфраструктуре и устройствам хранения. Будучи экспертами в своей предметной области, они могут помочь вам найти основную причину проблемы быстрее,
чем если вы работаете в одиночку.
Наконец, лучший способ добиться предсказуемой производительности в критически важных системах — это выделенная среда. Запускайте SQL Server на
выделенном оборудовании с напрямую подключенными устройствами хранения,
чтобы максимизировать производительность.

Настройка контрольных точек
Как мы уже знаем, в SQL Server используется протоколирование с опережающей
записью. Транзакции считаются принятыми только после того, как в журналах
транзакций появятся соответствующие записи. SQL Server не обязательно
одновременно с этим сохраняет «грязные» страницы данных на диск, потому
что при необходимости он может повторно применить изменения, воспроизведя
записи журнала.
Процесс контрольных точек сохраняет страницы данных в файлы данных. Основная задача контрольной точки — сократить время восстановления в случае
сбоя SQL или аварийного переключения: чем меньше изменений приходится
воспроизводить, тем быстрее выполнится восстановление. Максимальное желаемое время восстановления контролируется на уровне либо сервера, либо базы
данных. По умолчанию и то и другое равно 60 секундам.

88  Глава 3. Производительность дисковой подсистемы
Не рассматривайте целевое время восстановления как жестко заданное значение. Часто база данных будет восстанавливаться гораздо быстрее. И наоборот,
всплески активности и длительные транзакции могут продлить время восстановления.
Контрольные точки бывают четырех различных типов.
Внутренние контрольные точки
Возникают во время некоторых операций SQL Server, таких как запуск
резервного копирования базы данных или создание моментального снимка
базы данных.
Ручные контрольные точки
Возникают, когда пользователи создают их с помощью команды CHECKPOINT.
Автоматические контрольные точки
Исторически в SQL Server использовались автоматические контрольные
точки, а интервал восстановления контролировался сервером. Процесс
контрольной точки активируется один или несколько раз в течение каждого
интервала восстановления и сбрасывает «грязные» страницы данных на диск.
К сожалению, такой подход может привести к всплескам записи данных, что
чревато проблемами в высоконагруженных системах.
Косвенные контрольные точки
Этот метод, доступный в SQL Server 2012 и более поздних версиях, пытается сбалансировать нагрузку ввода/вывода, создавая контрольные точки
гораздо чаще, а иногда и вовсе непрерывно. Это помогает смягчить всплески записи данных и уравновесить нагрузку ввода/вывода. Используйте
именно этот тип вместо автоматических контрольных точек всегда, когда
возможно.
Косвенная контрольная точка управляется каждой базой данных отдельно
и по умолчанию применяется в базах, созданных в SQL Server 2016 и более
поздних версиях. Но SQL Server не активирует косвенную контрольную точку автоматически при обновлении экземпляра SQL Server и в версиях SQL
Server 2012 и 2014. Ее можно включить вручную, настроив целевое время восстановления на уровне базы данных с помощью команды ALTER DATABASE SET
TARGET_RECOVERY_TIME.
Вот пример из одной системы, с которой я работал. Выборка данных из
представления sys.dm_io_virtual_file_stats за 1 минуту показывала очень
большую задержку записи для файлов данных. Однако выборки меньшей
продолжительности (от 1 до 3 секунд) вообще редко проявляли какую-либо
активность.

Настройка контрольных точек  89
На рис. 3.6 показаны соответствующие данные: вверху — выборка продолжительностью в 1 минуту, внизу — в 1 секунду.

Рис. 3.6. Выборочные данные представления sys.dm_io_virtual_file_stats
с автоматической контрольной точкой
Такое поведение навело меня на мысль о том, что проблема связана с контрольной точкой. Гипотеза подтвердилась, когда я взглянул на счетчики производительности Checkpoint page/sec, Disk Writes/sec и Avg Disk Queue Length.
На рис. 3.5, приведенном ранее в этой главе, показан график из PerfMon, на
котором хорошо виден всплеск записи на диск от процесса контрольной точки.
Хотя в этом примере использовался SQL Server 2016, в нем применялась
автоматическая контрольная точка, потому что все базы данных были обновлены с более ранней версии SQL Server. Когда я включил косвенную
контрольную точку, схема ввода/вывода сразу же стала гораздо более сбалансированной.
Новые метрики производительности показаны на рис. 3.7. Обратите внимание, что в случае с косвенной контрольной точкой нужно смотреть на счетчик
Background writer pages/sec, а не Checkpoint pages/sec.
На рис. 3.8 показаны метрики выборки продолжительностью 1 минуту в представлении sys.dm_io_virtual_file_stats. Как видите, задержка пришла в норму.

90  Глава 3. Производительность дисковой подсистемы

Рис. 3.7. Метрики производительности косвенных контрольных точек

Рис. 3.8. Пример представления sys.dm_io_virtual_file_stats
с косвенной контрольной точкой
Косвенные контрольные точки не избавляют от всех всплесков ввода/вывода.
Всплески все равно могут происходить, особенно если в системе наблюдаются
пики модификации данных. Однако все равно всплески встречаются намного
реже, чем в случае автоматических контрольных точек.
Целевое время восстановления нужно настроить так, чтобы максимально сбалансировать нагрузку ввода/вывода. В предыдущем случае я добился наилучших
результатов, задав время 90 секунд. Разумеется, при более высоких значениях
может увеличиться время восстановления.

Ожидания ввода/вывода  91

Ожидания ввода/вывода
В SQL Server с операциями ввода/вывода связано несколько типов ожидания.
Иногда они проявляются все сразу, если дисковая подсистема работает недостаточно быстро. Рассмотрим пять самых распространенных из них: ASYNC_IO_
COMPLETION, IO_COMPLETION, WRITELOG, WRITE_COMPLETION и PAGEIOLATCH.

Ожидание ASYNC_IO_COMPLETION
Этот тип ожидания возникает, когда SQL Server ожидает завершения асинхронных операций ввода/вывода (чтения или записи) для страниц не из буферного
пула. Например:
Обычные контрольные точки
Внутренние контрольные точки, возникающие, когда запускается резервное
копирование базы данных или команда DBCC CHECKDB
Чтение страниц глобальной карты распределения (GAM) из файлов данных
Чтение страниц данных из базы данных во время ее резервного копирования
(к сожалению, эта операция искажает среднее время ожидания и затрудняет
анализ).
Когда я вижу много ожиданий ASYNC_IO_COMPLETION и PAGEIOLATCH, то выполняю общий поиск неполадок ввода/вывода. Если ожиданий PAGEIOLATCH нет,
то я смотрю, как часто возникает ASYNC_IO_COMPLETION. Это ожидание можно
игнорировать, если его процент не очень значителен, а задержка диска мала.

Ожидание IO_COMPLETION
Ожидание IO_COMPLETION возникает во время синхронного чтения и записи
в файлы данных и во время некоторых операций чтения из журнала транзакций.
Вот несколько примеров:
Чтение страниц карты распределения из базы данных.
Чтение журнала транзакций во время восстановления базы данных.
Запись данных в tempdb во время переносов сортировки.
Если наблюдается значительная доля этого типа ожидания, выполните общую
диагностику производительности диска. Обратите особое внимание на задержку
и пропускную способность tempdb. По моему опыту, плохая производительность
tempdb служит наиболее распространенной причиной такого ожидания. Подробнее об устранении неполадок tempdb я расскажу в главе 9.

92  Глава 3. Производительность дисковой подсистемы

Ожидание WRITELOG
Как можно догадаться по названию, это ожидание происходит, когда SQL Server
вносит записи в журнал транзакций. Оно появляется в любой системе, и это
нормально, но слишком большой его процент может указывать на узкое место
в журнале транзакций.
Посмотрите на среднее время ожидания и задержку записи в журнал транзакций в представлении sys.dm_io_vir tual_file_stats. Высокие значения могут
влиять на пропускную способность системы.
Чтобы сократить ожидания этого типа, можно оптимизировать пропускную
способность дисковой подсистемы, а также сделать еще несколько вещей, которые мы обсудим в главе 11.

Ожидание WRITE_COMPLETION
Это ожидание происходит во время синхронных операций записи в файлы базы
данных и журналов. По моему опыту, чаще всего оно появляется при снимках
базы данных.
Чтобы поддерживать моментальные снимки базы, SQL Server сохраняет версии
страниц данных, которые существовали на момент снимка. В контрольной точке
SQL Server считывает старые копии страниц данных из файлов данных и сохраняет их в виде снимка перед тем, как записывать измененные страницы на диск.
Это может значительно увеличить количество операций ввода/вывода в системе.
Если появляется такое ожидание, проверьте наличие моментальных снимков
базы данных. Учтите, что некоторые внутренние процессы, такие как DBCC
CHECKDB, тоже создают снимки.
Если в системе есть моментальные снимки БД и они действительно нужны, то
следует подумать, как улучшить производительность диска. В прочих случаях
снимки можно удалить, если из-за них тормозят устройства хранения.

Ожидания PAGEIOLATCH
Как вы уже знаете, ожидания PAGEIOLATCH происходят, когда SQL Server считывает страницы данных с диска. Эти ожидания очень распространены и встречаются в каждой системе. Формально существуют шесть ожиданий такого типа,
но реально заметны только три из них:
PAGEIOLATCH_EX

Происходит, когда рабочий процесс хочет обновить страницу данных и ожидает, пока она будет прочитана с диска в буферный пул.

Ожидания ввода/вывода  93
PAGEIOLATCH_SH

Происходит, когда рабочий процесс хочет прочитать страницу данных и ожидает, пока она будет прочитана с диска в буферный пул.
PAGEIOLATCH_UP

Происходит, когда рабочий процесс хочет обновить системную страницу (например, карту распределения) и ожидает, пока она будет прочитана с диска
в буферный пул.
Чрезмерное количество ожиданий PAGEIOLATCH говорит о том, что SQL Server
постоянно читает данные с диска. Обычно это происходит в двух случаях.
Первый случай — это недостаточно мощный SQL Server, где активные данные
не помещаются в памяти.
Второй и более распространенный случай — неоптимизированные запросы,
которые просматривают ненужные данные, очищая содержимое буферного
пула.
Чтобы перепроверить данные, можно взглянуть на счетчик производительности
Page Life Expectancy, который показывает, как долго страницы данных хранятся
в буферном пуле. В качестве ориентира можно взять значение 300 с на каждые
4 Гбайт памяти буферного пула: например, 7500 с на сервере с буферным пулом
100 Гбайт.
Значение Page Life Expectancy можно узнать с помощью утилиты PerfMon или
представления sys.dm_os_performance_counters, как показано в листинге 3.3.
Это же представление возвращает показатели для отдельных узлов NUMA.

Листинг 3.3. Получение значения Page Life Expectancy
SELECT object_name, counter_name, instance_name, cntr_value as [PLE(sec)]
FROM sys.dm_os_performance_counters WITH (NOLOCK)
WHERE counter_name = 'Page life expectancy'
OPTION (RECOMPILE);

Большой процент ожиданий PAGEIOLATCH всегда нужно устранять. С одной
стороны, ожидания этого типа не обязательно создают проблемы для клиентов,
особенно с дисковыми массивами на основе флеш-памяти с низкой задержкой.
Но с другой стороны, если данные разрастутся так, что перестанут умещаться
на диск, это быстро скажется на всей системе.
Чтобы уменьшить влияние ожиданий PAGEIOLATCH, можно обновить дисковую
подсистему или добавить на сервер больше памяти. Однако лучше всего обнаружить и оптимизировать неэффективные запросы, чтобы уменьшить объем
данных, считываемых с диска. В следующей главе вы узнаете, как выявлять
эти запросы.

94  Глава 3. Производительность дисковой подсистемы

Резюме
В SQL Server используются совместное планирование и в большинстве случаев —
асинхронный ввод/вывод для чтения и записи данных. По умолчанию каждый планировщик обрабатывает ввод/вывод и имеет собственную очередь ввода/вывода.
В представлении sys.dm_io_virtual_file_stats можно найти показатели пропускной способности ввода/вывода и задержки для каждого файла базы данных.
В правильно настроенной системе с сетевым хранилищем задержка записи
журнала транзакций не должна превышать 1–2 мс, а задержка чтения и записи
файлов данных — 3–5 мс. При прямом подключении хранилища (DAS) эти задержки должны быть еще ниже.
Устраняя неполадки с производительностью ввода/вывода, изучите весь стек
ввода/вывода. Проблема может оказаться где угодно: в ОС, виртуализации,
сетевом пути или на уровне устройств хранения.
Зачастую высокие задержки возникают из-за всплесков активности ввода/вывода. Проанализируйте и настройте процесс контрольных точек. Это одна из самых
распространенных ситуаций в высоконагруженных системах. Во многих случаях
снижение активности диска поможет уменьшить его задержку и производительность системы. Оптимизация запросов — один из лучших способов добиться этого.
В следующей главе мы рассмотрим, как выявлять неоптимизированные запросы.

Чек-лист устранения неполадок
Проанализировать задержки дисковой подсистемы в представлении
sys.dm_io_virtual_file_stats.
Проверить, не вызвана ли высокая задержка всплесками активности ввода/
вывода, изучив счетчики производительности SQL Server и ОС.
Проверить показатели ввода/вывода на уровне виртуальной машины и хранилища, обращая внимание на «шумных соседей».
Проверить настройки емкости дисковой очереди в стеке ввода/вывода.
Устранить неполадки с производительностью контрольных точек SQL
Server и настроить косвенные контрольные точки.
Устранить проблемы с производительностью журнала, если наблюдаются
значительные ожидания WRITELOG (см. главу 11).
Устранить проблемы с производительностью базы данных tempdb, если наблюдается много ожиданий IO_COMPLETION, а база tempdb используется чересчур активно и вызывает задержки (см. главу 9).
Выявить и оптимизировать неэффективные запросы, если наблюдаются
значительные ожидания PAGEIOLATCH.

ГЛАВА 4

Неэффективные запросы

Неэффективные запросы существуют в любой системе. Их воздействие на производительность проявляется по-разному: в первую очередь это увеличение
нагрузки ввода/вывода, использования ЦП и блокировок. Такие запросыважно
обнаружить и оптимизировать.
В этой главе обсуждается, как неэффективные запросы влияют на систему, а также даны рекомендации, как их обнаружить. В первую очередь рассмотрим, как
для этого использовать статистику выполнения на основе кэша планов. Затем
мы изучим расширенные события (Extended Events), трассировку SQL (SQL
Traces) и хранилище запросов (Query Store), а закончим главу замечаниями
о сторонних инструментах мониторинга. В следующих главах мы разберем
стратегии оптимизации неэффективных запросов.

Чем плохи неэффективные запросы
За время своей карьеры в области баз данных я еще не видел системы, где оптимизация запросов не пошла бы на пользу. Я уверен, что такие совершенные
примеры существуют, но меня не зовут анализировать системы, где все в порядке. В любом случае, идеальных систем очень мало, а во всех остальных есть
что улучшать и оптимизировать.
Не каждую компанию вообще заботит оптимизация запросов. Это трудоемкий
и утомительный процесс. Чтобы быстрее разработать и вывести продукт на
рынок, во многих случаях проще добавить аппаратных мощностей, чем тратить
долгие часы, копаясь в запросах.
Но с определенного момента такой подход приводит к проблемам с масштабируемостью. Плохо оптимизированные запросы влияют на систему со многих
сторон, но самый очевидный пример — это производительность диска. Если
подсистема ввода/вывода не справляется с нагрузкой объемных операций просмотра, то ухудшается производительность всей системы.

96  Глава 4. Неэффективные запросы
До некоторой степени эту проблему можно замаскировать, добавив на сервер
больше памяти. Это увеличит размер буферного пула и позволит SQL Server
кэшировать больше данных, сокращая физический ввод/вывод. Но по мере того,
как объем данных в системе растет, такой подход может стать непрактичным или
даже невозможным, особенно в отличных от Enterprise выпусках SQL Server,
где максимальный размер буферного пула ограничен.
Еще один эффект заключается в том, что неоптимизированные запросы сильно
нагружают ЦП на серверах. Чем больше данных обрабатывается, тем больше
ресурсов ЦП потребляется. На одну операцию логического чтения страницы
данных или ее просмотра в памяти у сервера может уходить всего несколько
микросекунд, но суммарные затраты времени на все операции быстро растут,
когда количество операций увеличивается.
Опять же, можно замаскировать проблему, добавив на сервер больше процессоров. (Хотя при этом придется платить за дополнительные лицензии, причем
в версиях, отличных от Enterprise, максимальное количество ЦП ограничено.)
Более того, дополнительные ЦП не всегда помогают, потому что неоптимизированные запросы все равно будут вызывать блокировку. Существуют способы
уменьшить блокировку без тонкой настройки запросов, но такие меры могут
повлиять на работу системы в целом и ее производительность.
Суть такова: когда вы устраняете неполадки, всегда анализируйте систему на
предмет неоптимизированных запросов. После этого оцените, насколько сильно
такие запросы влияют на систему.
Хотя оптимизация запросов всегда идет на пользу, это сложное занятие и усилия
не обязательно окупаются. Но в большинстве случаев имеет смысл привести
в порядок хотя бы некоторые запросы.
К примеру, я перерабатываю запросы, когда вижу большую загруженность диска,
блокировки или высокую нагрузку ЦП. Но если данные кэшируются в буферном
пуле, а нагрузка ЦП приемлема, то я предпочитаю сперва сосредоточиться на
других очагах проблем. Тем не менее я не теряю бдительности и учитываю, что
может произойти по мере роста объема данных. Не исключено, что в один прекрасный момент активные данные перестанут помещаться в буферный пул, что
приведет к внезапному и серьезному падению производительности.
К счастью, оптимизация запросов не требует подхода «все или ничего». Производительность можно значительно повысить, если оптимизировать всего несколько
часто выполняемых запросов. Рассмотрим ряд способов, как их обнаружить.

Статистика выполнения на основе кэша планов
Как правило, SQL Server кэширует и повторно использует планы выполнения
запросов. Для каждого кэшированного плана доступна статистика выполнения,

Статистика выполнения на основе кэша планов  97
в том числе количество запусков запроса, совокупное время ЦП и нагрузка
ввода/вывода. Эти данные можно использовать, чтобы быстро выявить самые
ресурсоемкие запросы, требующие оптимизации. (О кэшировании мы подробнее
поговорим в главе 6.)
Анализировать статистику выполнения на основе кэша планов — не самый
исчерпывающий метод обнаружения неоптимизированных запросов, и у него
немало ограничений. Тем не менее он очень прост в использовании, и им часто
удается обойтись. Этот метод работает во всех версиях SQL Server и всегда доступен в системе. Для него не надо настраивать дополнительный мониторинг
и специально собирать данные.
Получить статистику выполнения можно с помощью представления sys.
dm_exec_query_stats1, как показано в листинге 4.1. Этот запрос немного упрощен, зато он демонстрирует, как работает представление, и позволяет изучить
несколько метрик из него. Далее в этой главе я приведу более сложную версию
кода, основанную на этом запросе. В зависимости от версии SQL Server и установленных обновлений у вас могут не поддерживаться некоторые столбцы,
которые я использую здесь и далее. Если так, то удалите их.
Этот код предоставляет планы выполнения запросов. Есть две функции, которые
позволяют их получить:
sys.dm_exec_query_plan

Эта функция2 возвращает план выполнения всего пакета выполнения в формате XML. Из-за внутренних ограничений функции размер результирующего
XML не может превышать 2 Мбайт, а для сложных планов функция может
возвращать NULL.
sys.dm_exec_text_query_plan

Эта функция3, которая используется в листинге 4.1, возвращает текстовое
представление плана выполнения. Его можно получить для всего пакета
или для определенной инструкции из пакета, передав смещение инструкции
в качестве параметра функции.
В листинге 4.1 планы преобразуются в XML-представление с помощью функции TRY_CONVERT , которая возвращает NULL , если размер XML превышает
2 Мбайт. TRY_CONVERT можно удалить, если вы работаете с большими планами
или запускаете код в SQL Server версий с 2005 по 2008R2.

1

https://oreil.ly/waA0W

2

https://oreil.ly/xeQEf

3

https://oreil.ly/utOjM

98  Глава 4. Неэффективные запросы
Листинг 4.1. Использование представления sys.dm_exec_query_stats
;WITH Queries
AS
(
SELECT TOP 50
qs.creation_time AS [Cached Time]
,qs.last_execution_time AS [Last Exec Time]
,qs.execution_count AS [Exec Cnt]
,CONVERT(DECIMAL(10,5),
IIF
(
DATEDIFF(SECOND,qs.creation_time, qs.last_execution_time) = 0
,NULL
,1.0 * qs.execution_count /
DATEDIFF(SECOND,qs.creation_time, qs.last_execution_time)
)
) AS [Exec Per Second]
,(qs.total_logical_reads + qs.total_logical_writes) /
qs.execution_count AS [Avg IO]
,(qs.total_worker_time / qs.execution_count / 1000)
AS [Avg CPU(ms)]
,qs.total_logical_reads AS [Total Reads]
,qs.last_logical_reads AS [Last Reads]
,qs.total_logical_writes AS [Total Writes]
,qs.last_logical_writes AS [Last Writes]
,qs.total_worker_time / 1000 AS [Total Worker Time]
,qs.last_worker_time / 1000 AS [Last Worker Time]
,qs.total_elapsed_time / 1000 AS [Total Elapsed Time]
,qs.last_elapsed_time / 1000 AS [Last Elapsed Time]
,qs.total_rows AS [Total Rows]
,qs.last_rows AS [Last Rows]
,qs.total_rows / qs.execution_count AS [Avg Rows]
,qs.total_physical_reads AS [Total Physical Reads]
,qs.last_physical_reads AS [Last Physical Reads]
,qs.total_physical_reads / qs.execution_count
AS [Avg Physical Reads]
,qs.total_grant_kb AS [Total Grant KB]
,qs.last_grant_kb AS [Last Grant KB]
,(qs.total_grant_kb / qs.execution_count)
AS [Avg Grant KB]
,qs.total_used_grant_kb AS [Total Used Grant KB]
,qs.last_used_grant_kb AS [Last Used Grant KB]
,(qs.total_used_grant_kb / qs.execution_count)
AS [Avg Used Grant KB]
,qs.total_ideal_grant_kb AS [Total Ideal Grant KB]
,qs.last_ideal_grant_kb AS [Last Ideal Grant KB]
,(qs.total_ideal_grant_kb / qs.execution_count)
AS [Avg Ideal Grant KB]
,qs.total_columnstore_segment_reads
AS [Total CSI Segments Read]
,qs.last_columnstore_segment_reads
AS [Last CSI Segments Read]
,(qs.total_columnstore_segment_reads / qs.execution_count)

Статистика выполнения на основе кэша планов  99
AS [AVG CSI Segments Read]
,qs.max_dop AS [Max DOP]
,qs.total_spills AS [Total Spills]
,qs.last_spills AS [Last Spills]
,(qs.total_spills / qs.execution_count) AS [Avg Spills]
,qs.statement_start_offset
,qs.statement_end_offset
,qs.plan_handle
,qs.sql_handle

FROM

sys.dm_exec_query_stats qs WITH (NOLOCK)
ORDER BY
[Avg IO] DESC
)
SELECT
SUBSTRING(qt.text, (qs.statement_start_offset/2)+1,
((
CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(qt.text)
ELSE qs.statement_end_offset
END – qs.statement_start_offset)/2)+1) AS SQL
,TRY_CONVERT(xml,qp.query_plan) AS [Query Plan]
,qs.*
FROM
Queries qs
OUTER APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
OUTER APPLY
sys.dm_exec_text_query_plan
(
qs.plan_handle
,qs.statement_start_offset
,qs.statement_end_offset
) qp
OPTION (RECOMPILE, MAXDOP 1);

В зависимости от целей настройки можно сортировать данные по-разному: по
устройствам ввода/вывода, когда задача — уменьшить нагрузку на диск, по ЦП
в системах с привязкой к ЦП и т. д.
На рис. 4.1 частично показан вывод кода на одном из серверов. Как видите,
запросы для оптимизации легко отбираются по частоте их выполнения и по
данным о потреблении ресурсов.
Планы выполнения, которые отражены в выводе, не содержат фактических показателей выполнения. В этом отношении они аналогичны расчетным планам
выполнения. Учитывайте это во время оптимизации (подробнее об этом я расскажу в главе 5).
Этой проблемы можно избежать в SQL Server 2019 и более поздних версиях,
а также в базах данных Azure SQL, где можно включить сбор последнего фактического плана выполнения инструкций в базах данных с уровнем совместимо-

100  Глава 4. Неэффективные запросы
сти 150. Понадобится также включить параметр базы данных LAST_QUERY_PLAN_
STATS. Как и в случае любого сбора данных, этот параметр увеличит нагрузку на
систему, хотя обычно это увеличение несущественно.

Рис. 4.1. Частичный вывод представления sys.dm_exec_query_stats
Последний фактический план выполнения доступен через функцию sys.
dm_exec_query_plan_stats1. Во всех примерах кода в этой главе ее можно
использовать вместо sys.dm_exec_text_query_plan, и код останется работоспособным.
Следует учитывать еще несколько важных ограничений. Прежде всего вы не
увидите никаких данных о запросах, для которых не кэшированы планы выполнения. Могут оказаться пропущенными некоторые редко выполняемые запросы,
1

https://oreil.ly/Rdf3U

Статистика выполнения на основе кэша планов  101
чьих планов уже нет в кэше. Обычно это не проблема, потому что такие запросы
чаще всего не нуждаются в оптимизации в начале настройки.
Есть и еще один нюанс. SQL Server не кэширует планы выполнения, если вы
используете перекомпиляцию на уровне инструкций с нерегламентированными
инструкциями или выполняете хранимые процедуры с предложением RECOMPILE.
Эти запросы придется захватывать с помощью хранилища запросов или расширенных событий, о которых я расскажу позже в этой главе.
Если вы используете перекомпиляцию на уровне инструкций в хранимых процедурах или других модулях T-SQL, то SQL Server кэширует план выполнения
инструкции. Правда, план не будет использоваться повторно, а в статистике
выполнения отразится только одно последнее выполнение.
Вторая проблема связана с тем, как долго планы хранятся в кэше. Это зависит
от плана, и результаты могут исказиться при сортировке данных по общим показателям. Например, запрос с меньшим средним временем потребления ЦП
может показать большее общее количество выполнений и время ЦП, чем запрос
с более высоким средним временем ЦП, — в зависимости от того, когда каждый
из планов был закэширован.
Можно использовать любую из этих метрик, но у каждой свои недостатки. Если
сортировать данные по средним значениям, то наверху списка могут оказаться
редко выполняемые запросы. Например, так происходит с ресурсоемкими задачами по обслуживанию в ночное время. В то же время сортировка по общим
значениям может пропустить запросы с недавно кэшированными планами.
Можно изучить столбцы creation_time и last_execution_time, в которых содержится время последнего кэширования и выполнения планов соответственно.
Я обычно просматриваю данные, отсортированные как по общим, так и по средним показателям, учитывая частоту выполнения (общее и среднее количество
выполнений в секунду). Я сопоставляю данные из обоих источников, прежде
чем решить, что оптимизировать.
Бывает проблема сложнее: для одного и того же запроса или нескольких похожих
запросов можно получить несколько результатов. Это может случиться с нерегламентированными рабочими нагрузками, а также когда клиенты используют
разные настройки SET в своих сеансах, когда пользователи запускают одни и те
же запросы с немного различающимся форматированием, и во многих других
случаях. Проблема также встречается в базах данных с уровнем совместимости
160 (SQL Server 2022) из-за функции оптимизации плана с учетом параметров
(подробнее об этом в главе 6).
К счастью, эту проблему можно преодолеть с помощью столбцов query_hash
и query_plan_hash, которые отображаются в представлении sys.dm_exec_query_

102  Глава 4. Неэффективные запросы
stats. Идентичные значения в этих столбцах сигнализируют о похожих запросах

и планах выполнения. Эти столбцы можно использовать, чтобы группировать
данные.
Оператор DBCC FREEPROCCACHE очищает кэш планов, чтобы уменьшить
объем вывода. Не запускайте код из листинга 4.2 на рабочих серверах!

Рассмотрим простой пример. Код из листинга 4.2 выполняет три запроса, а затем изучает содержимое кэша планов. Первые два запроса одинаковы, просто
отформатированы по-разному. Третий запрос от них отличается.

Листинг 4.2. Столбцы query_hash и query_plan_hash в действии
DBCC FREEPROCCACHE -- Не используйте в промышленной системе!
GO
SELECT /*V1*/ TOP 1 object_id FROM sys.objects WHERE object_id = 1;
GO
SELECT /*V2*/ TOP 1 object_id
FROM sys.objects
WHERE object_id = 1;
GO
SELECT COUNT(*) FROM sys.objects
GO
SELECT
qs.query_hash, qs.query_plan_hash, qs.sql_handle, qs.plan_handle,
SUBSTRING(qt.text, (qs.statement_start_offset/2)+1,
((
CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(qt.text)
ELSE qs.statement_end_offset
END – qs.statement_start_offset)/2)+1
) as SQL
FROM
sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
ORDER BY query_hash
OPTION (MAXDOP 1, RECOMPILE);

Результаты показаны на рис. 4.2. В выводимых данных три плана выполнения. В последних двух строках содержатся одинаковые значения query_hash
и query_plan_hash, но разные sql_handle и plan_handle.
В листинге 4.3 приведена более сложная версия сценария из листинга 4.1: теперь
статистика по похожим запросам группируется. Инструкция и планы выполнения выбираются случайным образом из первого запроса в каждой группе;
учитывайте это при анализе.

Статистика выполнения на основе кэша планов  103

Рис. 4.2. Несколько планов с одинаковыми значениями query_hash и query_plan_hash

Листинг 4.3. Использование представления sys.dm_exec_query_stats с группировкой
по query_hash
;WITH Data
AS
(
SELECT TOP 50
qs.query_hash
,COUNT(*) as [Plan Count]
,MIN(qs.creation_time) AS [Cached Time]
,MAX(qs.last_execution_time) AS [Last Exec Time]
,SUM(qs.execution_count) AS [Exec Cnt]
,SUM(qs.total_logical_reads) AS [Total Reads]
,SUM(qs.total_logical_writes) AS [Total Writes]
,SUM(qs.total_worker_time / 1000) AS [Total Worker Time]
,SUM(qs.total_elapsed_time / 1000) AS [Total Elapsed Time]
,SUM(qs.total_rows) AS [Total Rows]
,SUM(qs.total_physical_reads) AS [Total Physical Reads]
,SUM(qs.total_grant_kb) AS [Total Grant KB]
,SUM(qs.total_used_grant_kb) AS [Total Used Grant KB]
,SUM(qs.total_ideal_grant_kb) AS [Total Ideal Grant KB]
,SUM(qs.total_columnstore_segment_reads)
AS [Total CSI Segments Read]
,MAX(qs.max_dop) AS [Max DOP]
,SUM(qs.total_spills) AS [Total Spills]
FROM
sys.dm_exec_query_stats qs WITH (NOLOCK)
GROUP BY
qs.query_hash
ORDER BY
SUM((qs.total_logical_reads + qs.total_logical_writes) /
qs.execution_count) DESC
)
SELECT
d.[Cached Time]
,d.[Last Exec Time]
,d.[Plan Count]

104  Глава 4. Неэффективные запросы

FROM

,sql_plan.SQL
,sql_plan.[Query Plan]
,d.[Exec Cnt]
,CONVERT(DECIMAL(10,5),
IIF(datediff(second,d.[Cached Time], d.[Last Exec Time]) = 0,
NULL,
1.0 * d.[Exec Cnt] /
datediff(second,d.[Cached Time], d.[Last Exec Time])
)
) AS [Exec Per Second]
,(d.[Total Reads] + d.[Total Writes]) / d.[Exec Cnt] AS [Avg IO]
,(d.[Total Worker Time] / d.[Exec Cnt] / 1000) AS [Avg CPU(ms)]
,d.[Total Reads]
,d.[Total Writes]
,d.[Total Worker Time]
,d.[Total Elapsed Time]
,d.[Total Rows]
,d.[Total Rows] / d.[Exec Cnt] AS [Avg Rows]
,d.[Total Physical Reads]
,d.[Total Physical Reads] / d.[Exec Cnt] AS [Avg Physical Reads]
,d.[Total Grant KB]
,d.[Total Grant KB] / d.[Exec Cnt] AS [Avg Grant KB]
,d.[Total Used Grant KB]
,d.[Total Used Grant KB] / d.[Exec Cnt] AS [Avg Used Grant KB]
,d.[Total Ideal Grant KB]
,d.[Total Ideal Grant KB] / d.[Exec Cnt] AS [Avg Ideal Grant KB]
,d.[Total CSI Segments Read]
,d.[Total CSI Segments Read] / d.[Exec Cnt] AS [AVG CSI Segments Read]
,d.[Max DOP]
,d.[Total Spills]
,d.[Total Spills] / d.[Exec Cnt] AS [Avg Spills]
Data d
CROSS APPLY
(
SELECT TOP 1
SUBSTRING(qt.text, (qs.statement_start_offset/2)+1,
((
CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(qt.text)
ELSE qs.statement_end_offset
END – qs.statement_start_offset)/2)+1
) AS SQL
,TRY_CONVERT(XML,qp.query_plan) AS [Query Plan]
FROM
sys.dm_exec_query_stats qs
OUTER APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
OUTER APPLY sys.dm_exec_text_query_plan
(
qs.plan_handle
,qs.statement_start_offset
,qs.statement_end_offset

Статистика выполнения на основе кэша планов  105
) qp
WHERE
qs.query_hash = d.query_hash AND ISNULL(qt.text,'') ''
) sql_plan

ORDER BY
[Avg IO] DESC
OPTION (RECOMPILE, MAXDOP 1);

Начиная с SQL Server 2008, можно получать статистику выполнения хранимых процедур с помощью представления sys.dm_exec_procedure_stats1. Также
можно использовать код из листинга 4.4. Как и в представлении sys.dm_exec_
query_stats, данные можно сортировать по разным показателям выполнения
в зависимости от вашей стратегии оптимизации. Учтите, что в статистику выполнения входят метрики из динамического SQL и других вложенных модулей
(хранимых процедур, функций, триггеров), вызываемых из хранимых процедур.

Листинг 4.4. Использование представления sys.dm_exec_procedure_stats
SELECT TOP 50
IIF (ps.database_id = 32767,
'mssqlsystemresource',
DB_NAME(ps.database_id)
) AS [DB]
,OBJECT_NAME(
ps.object_id,
IIF(ps.database_id = 32767, 1, ps.database_id)
) AS [Proc Name]
,ps.type_desc AS [Type]
,ps.cached_time AS [Cached Time]
,ps.last_execution_time AS [Last Exec Time]
,qp.query_plan AS [Plan]
,ps.execution_count AS [Exec Count]
,CONVERT(DECIMAL(10,5),
IIF(datediff(second,ps.cached_time, ps.last_execution_time) = 0,
NULL,
1.0 * ps.execution_count /
datediff(second,ps.cached_time, ps.last_execution_time)
)
) AS [Exec Per Second]
,(ps.total_logical_reads + ps.total_logical_writes) /
ps.execution_count AS [Avg IO]
,(ps.total_worker_time / ps.execution_count / 1000)
AS [Avg CPU(ms)]
,ps.total_logical_reads AS [Total Reads]
,ps.last_logical_reads AS [Last Reads]
,ps.total_logical_writes AS [Total Writes]
,ps.last_logical_writes AS [Last Writes]
,ps.total_worker_time / 1000 AS [Total Worker Time]

1

https://oreil.ly/B2Xyg

106  Глава 4. Неэффективные запросы
,ps.last_worker_time / 1000 AS [Last Worker Time]
,ps.total_elapsed_time / 1000 AS [Total Elapsed Time]
,ps.last_elapsed_time / 1000 AS [Last Elapsed Time]
,ps.total_physical_reads AS [Total Physical Reads]
,ps.last_physical_reads AS [Last Physical Reads]
,ps.total_physical_reads / ps.execution_count AS [Avg Physical Reads]
,ps.total_spills AS [Total Spills]
,ps.last_spills AS [Last Spills]
,(ps.total_spills / ps.execution_count) AS [Avg Spills]

FROM

sys.dm_exec_procedure_stats ps WITH (NOLOCK)
CROSS APPLY sys.dm_exec_query_plan(ps.plan_handle) qp
ORDER BY
[Avg IO] DESC
OPTION (RECOMPILE, MAXDOP 1);

На рис. 4.3 частично показан вывод этого кода. Как видите, таким образом можно
получить планы выполнения хранимых процедур. Во внутреннем представлении
планы выполнения хранимых процедур и других модулей T-SQL представляют
собой просто наборы отдельных планов каждой инструкции. В некоторых случаях — например, когда размер плана выполнения превышает 2 Мбайт — план
не попадает в выходные данные.

Рис. 4.3. Частичный вывод представления sys.dm_exec_procedure_stats
Листинг 4.5 помогает решить эту проблему. С помощью этого кода можно получить кэшированные планы выполнения и их метрики для отдельных инструкций
из модулей T-SQL. Имя модуля указывается в предложении WHERE при запуске
скрипта.

Статистика выполнения на основе кэша планов  107

Листинг 4.5. Получение плана выполнения и статистики для операторов
хранимой процедуры
SELECT
qs.creation_time AS [Cached Time]
,qs.last_execution_time AS [Last Exec Time]
,SUBSTRING(qt.text, (qs.statement_start_offset/2)+1,
((
CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(qt.text)
ELSE qs.statement_end_offset
END – qs.statement_start_offset)/2)+1) AS SQL
,TRY_CONVERT(XML,qp.query_plan) AS [Query Plan]
,CONVERT(DECIMAL(10,5),
IIF(datediff(second,qs.creation_time, qs.last_execution_time) = 0,
NULL,
1.0 * qs.execution_count /
datediff(second,qs.creation_time, qs.last_execution_time)
)
) AS [Exec Per Second]
,(qs.total_logical_reads + qs.total_logical_writes) /
qs.execution_count AS [Avg IO]
,(qs.total_worker_time / qs.execution_count / 1000)
AS [Avg CPU(ms)]
,qs.total_logical_reads AS [Total Reads]
,qs.last_logical_reads AS [Last Reads]
,qs.total_logical_writes AS [Total Writes]
,qs.last_logical_writes AS [Last Writes]
,qs.total_worker_time / 1000 AS [Total Worker Time]
,qs.last_worker_time / 1000 AS [Last Worker Time]
,qs.total_elapsed_time / 1000 AS [Total Elapsed Time]
,qs.last_elapsed_time / 1000 AS [Last Elapsed Time]
,qs.total_rows AS [Total Rows]
,qs.last_rows AS [Last Rows]
,qs.total_rows / qs.execution_count AS [Avg Rows]
,qs.total_physical_reads AS [Total Physical Reads]
,qs.last_physical_reads AS [Last Physical Reads]
,qs.total_physical_reads / qs.execution_count
AS [Avg Physical Reads]
,qs.total_grant_kb AS [Total Grant KB]
,qs.last_grant_kb AS [Last Grant KB]
,(qs.total_grant_kb / qs.execution_count)
AS [Avg Grant KB]
,qs.total_used_grant_kb AS [Total Used Grant KB]
,qs.last_used_grant_kb AS [Last Used Grant KB]
,(qs.total_used_grant_kb / qs.execution_count)
AS [Avg Used Grant KB]
,qs.total_ideal_grant_kb AS [Total Ideal Grant KB]
,qs.last_ideal_grant_kb AS [Last Ideal Grant KB]
,(qs.total_ideal_grant_kb / qs.execution_count)
AS [Avg Ideal Grant KB]
,qs.total_columnstore_segment_reads

108  Глава 4. Неэффективные запросы
AS [Total CSI Segments Read]
,qs.last_columnstore_segment_reads
AS [Last CSI Segments Read]
,(qs.total_columnstore_segment_reads / qs.execution_count)
AS [AVG CSI Segments Read]
,qs.max_dop AS [Max DOP]
,qs.total_spills AS [Total Spills]
,qs.last_spills AS [Last Spills]
,(qs.total_spills / qs.execution_count) AS [Avg Spills]

FROM

sys.dm_exec_query_stats qs WITH (NOLOCK)
OUTER APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
OUTER APPLY sys.dm_exec_text_query_plan
(
qs.plan_handle
,qs.statement_start_offset
,qs.statement_end_offset
) qp
WHERE
OBJECT_NAME(qt.objectid, qt.dbid) =
-- Замените на имя хранимой процедуры
ORDER BY
qs.statement_start_offset, qs.statement_end_offset
OPTION (RECOMPILE, MAXDOP 1);

Начиная с SQL Server 2016, можно получать статистику выполнения для триггеров и скалярных пользовательских функций, используя представления sys.
dm_exec_trigger_stats1 и sys.dm_exec_function_stats2 соответственно. Можно
использовать тот же код, что и в листинге 4.4, просто заменив в нем имя динамического административного представления. Код также можно скачать из
сопроводительных материалов этой книги.
Наконец, стоит отметить, что SQL Server может кэшировать тысячи планов
выполнения. Кроме того, функции получения планов запросов и инструкций
SQL весьма трудоемкие, поэтому я использую параметр запроса MAXDOP 1, чтобы
уменьшить накладные расходы. В некоторых случаях стоит сохранять содержимое кэша планов в отдельную базу данных с помощью операторов SELECT INTO
и анализировать данные на тестовых серверах.
У статистики выполнения на основе кэша планов есть определенные ограничения, и некоторые запросы могут быть пропущены. Но этот подход годится
в качестве отправной точки. Важнее всего, что нужные данные собираются
автоматически и доступны сразу без дополнительных инструментов мониторинга.
1

https://oreil.ly/EVobu

2

https://oreil.ly/mZEJO

Расширенные события и трассировки SQL  109

Расширенные события и трассировки SQL
Уверен, что каждый специалист по SQL Server знает о трассировках SQL
(SQL Traces) и расширенных событиях (Extended Events или xEvents). Они
позволяют захватывать различные события в системе для анализа и устранения неполадок в режиме реального времени. С их помощью также можно
«отлавливать» длительные и ресурсоемкие запросы, в том числе те, которые
не кэшируют планы выполнения и поэтому не попадают в представление sys.
dm_exec_query_stats.
Тем не менее мне хотелось бы начать этот раздел с предупреждения: не используйте для захвата запросов трассировки SQL и расширенные события
без крайней необходимости. Захватывать выполняющиеся инструкции — это
ресурсоемкая операция, которая может значительно ухудшить производительность в высоконагруженных системах. (В главе 1 мы уже видели такой
пример.)
Неважно, сколько данных вы хотите собрать. Большую часть инструкций можно
исключить из вывода, то есть отфильтровать запросы с низким потреблением
ресурсов. Но SQL Server все равно вынужден захватывать все инструкции, чтобы
вычислять их, фильтровать и отбрасывать лишние события.
Не собирайте ненужную информацию для событий, которые вы анализируете,
или для захватываемых действий расширенных событий. Некоторые действия,
например callstack, обходятся очень дорого и существенно сказываются на
производительности системы. По возможности используйте расширенные события вместо трассировки SQL. Они легче и влекут за собой меньше накладных
расходов.
В табл. 4.1 показано несколько расширенных событий и событий трассировки SQL, с помощью которых можно обнаруживать неэффективные запросы.
Каждому из этих событий, кроме sqlserver.attention, соответствует некоторое другое событие, которое срабатывает в начале исполнения. Иногда эти
события тоже нужно захватывать, чтобы сопоставить рабочие нагрузки из
нескольких сеансов.
Какие события нужно захватывать? Это зависит от рабочей нагрузки системы,
структуры уровня доступа к данным и вашей стратегии устранения неполадок. Например, события sqlserver.sql_statement_completed и sqlserver.
sp_statement_completed позволяют обнаруживать неэффективные нерегламентированные запросы и запросы модулей T-SQL. А неэффективные пакеты и хранимые процедуры можно выловить с помощью событий sqlserver.
sql_batch_completed и sqlserver.rpc_completed, не создавая чрезмерной дополнительной нагрузки на систему.

110  Глава 4. Неэффективные запросы
Таблица 4.1. Расширенные события и события трассировки SQL для обнаружения
неэффективных запросов
Расширенное событие

Событие трассировки SQL

Описание

sqlserver.sql_statement
_completed

SQL:StmtCompleted

Возникает, когда заканчивается
выполнение инструкции

sqlserver.sp_statement
_completed

SP:StmtCompleted

Возникает, когда заканчивается выполнение инструкции SQL в модуле
T-SQL

sqlserver.rpc_completed

RPC:Completed

Возникает, когда заканчивается
выполнение удаленного вызова
процедуры (RPC). RPC — это параметризованные SQL-запросы, такие
как вызовы хранимых процедур
или параметризованные пакеты, направленные из сторонних приложений. Многие клиентские библиотеки
запускают запросы через функцию
sp_executesql, и ее вызовы захватываются этим событием

sqlserver.module_end

SP:Completed

Возникает, когда заканчивается выполнение модуля T-SQL

sqlserver.sql_batch

SQL:BatchCompleted

Возникает, когда заканчивается выполнение пакета SQL

Error:Attention

Возникает, когда клиент отменяет
выполнение запроса по истечении
времени ожидания или явным
образом (например, с помощью
красной кнопки отмены в SSMS)

_completed
sqlserver.attention

Таким же образом решайте, какие расширенные действия захватывать. Например, можно игнорировать информацию о пользователе и клиентском приложении, если она не нужна для устранения неполадок. В то же время можно собирать
действия query_hash и query_plan_hash и использовать их, чтобы оценивать
совокупное влияние похожих запросов и планов выполнения.
Обычно я диагностирую неэффективные запросы в двух ситуациях. Во-первых,
я могу запустить сеанс на несколько минут, записывая результаты в целевой объект ring_buffer. Обычно я так поступаю, когда нагрузка в системе относительно
статична и ее можно репрезентативно отразить простой выборкой. Во-вторых,
я могу запустить расширенный сеанс на несколько часов, используя event_file
в качестве целевого объекта.

Расширенные события и трассировки SQL  111
В листинге 4.6 показан второй способ, который сохраняет данные в папку C:\
ExtEvents (в вашей системе путь может быть другим). Этот код захватывает
инструкции, которые потребляют более 5000 мс процессорного времени или
производят более 50 000 логических операций чтения или записи. Код в листингах 4.6 и 4.7 будет работать в SQL Server 2012 и более поздних версиях,
однако его придется модифицировать для SQL Server 2008, который подругому работает с целевым файлом и не поддерживает действия query_hash
и query_plan_hash.
Должен предупредить, что этот код добавляет накладные расходы, масштаб которых зависит от рабочей нагрузки и объема собираемых данных. Не оставляйте
этот сеанс активным, когда не занимаетесь устранением неполадок производительности. Кроме того, подберите пороговые значения cpu_time, logical_reads
и writes в соответствии со своей рабочей нагрузкой и не захватывайте слишком
много запросов.
Похожим образом составьте список действий расширенных событий на основе
вашей стратегии устранения неполадок. К примеру, нет необходимости собирать plan_handle, если вы планируете выполнять анализ на другом сервере и не
сможете получить планы выполнения из кэша.

Листинг 4.6. Захват запросов с интенсивным использованием ЦП и операций ввода/вывода
CREATE EVENT SESSION [Expensive Queries]
ON SERVER
ADD EVENT sqlserver.sql_statement_completed
(
ACTION
(
sqlserver.client_app_name
,sqlserver.client_hostname
,sqlserver.database_id
,sqlserver.plan_handle
,sqlserver.query_hash
,sqlserver.query_plan_hash
,sqlserver.sql_text
,sqlserver.username
)
WHERE
(
(
cpu_time >= 5000000 or -- Время в микросекундах
logical_reads >= 50000 or
writes >= 50000
) AND
sqlserver.is_system = 0
)
)
,ADD EVENT sqlserver.sp_statement_completed
(

112  Глава 4. Неэффективные запросы
ACTION
(
sqlserver.client_app_name
,sqlserver.client_hostname
,sqlserver.database_id
,sqlserver.plan_handle
,sqlserver.query_hash
,sqlserver.query_plan_hash
,sqlserver.sql_text
,sqlserver.username
)
WHERE
(
(
cpu_time >= 5000000 or -- Время в микросекундах
logical_reads >= 50000 or
writes >= 50000
) AND
sqlserver.is_system = 0
)

)
ADD TARGET package0.event_file
(
SET FILENAME = 'C:\ExtEvents\Expensive Queries.xel'
)
WITH
(
event_retention_mode=allow_single_event_loss
,max_dispatch_latency=30 seconds
);

В листинге 4.7 приведен код для анализа собранных данных. Сперва он загружает собранные события во временную таблицу, используя функцию sys.
fn_xe_file_target_read_file1. Звездочка в конце имени файла заставляет SQL
Server загрузить все файлы продолжения (rollover files) из сеанса расширенного
события.
Затем код анализирует собранные события, сохраняя результаты в другую
временную таблицу. Возможно, вам придется изменить код в обобщенном табличном выражении EventInfo в зависимости от полей расширенных событий
и действий, которые вас интересуют. Не анализируйте неважную информацию,
ведь разбирать XML — затратная и долгая процедура.
Наконец, если вы запускаете этот код в SQL Server 2016 или более ранних версиях, его нужно модифицировать, чтобы получать время события из столбца
event_data, содержащего данные в формате XML. В SQL Server 2017 функция
sys.fn_xe_file_target_read_file возвращает его сама.

1

https://oreil.ly/L6sG8

Расширенные события и трассировки SQL  113

Листинг 4.7. Анализ собранных данных расширенного события
CREATE TABLE #EventData
(
event_data XML NOT NULL,
file_name NVARCHAR(260) NOT NULL,
file_offset BIGINT NOT NULL,
timestamp_utc datetime2(7) NOT NULL -- SQL Server 2017+
);
INSERT INTO #EventData(event_data, file_name, file_offset, timestamp_utc)
SELECT CONVERT(XML,event_data), file_name, file_offset, timestamp_utc
FROM sys.fn_xe_file_target_read_file
('c:\extevents\Expensive Queries*.xel',NULL,NULL,NULL);
;WITH EventInfo([Event],[Event Time],[DB],[Statement],[SQL],[User Name]
,[Client],[App],[CPU Time],[Duration],[Logical Reads]
,[Physical Reads],[Writes],[Rows],[Query Hash],[Plan Hash]
,[PlanHandle],[Stmt Offset],[Stmt Offset End],File_Name,File_Offset)
AS
(
SELECT
event_data.value('/event[1]/@name','SYSNAME') AS [Event]
,timestamp_utc AS [Event Time] -- SQL Server 2017+
/*,event_data.value('/event[1]/@timestamp','DATETIME')
AS [Event Time] -- Версии до SQL Server 2017 */
,event_data.value
('((/event[1]/action[@name="database_id"]/value/text())[1])'
,'INT') AS [DB]
,event_data.value
('((/event[1]/data[@name="statement"]/value/text())[1])'
,'NVARCHAR(MAX)') AS [Statement]
,event_data.value
('((/event[1]/action[@name="sql_text"]/value/text())[1])'
,'NVARCHAR(MAX)') AS [SQL]
,event_data.value
('((/event[1]/action[@name="username"]/value/text())[1])'
,'NVARCHAR(255)') AS [User Name]
,event_data.value
('((/event[1]/action[@name="client_hostname"]/value/text())[1])'
,'NVARCHAR(255)') AS [Client]
,event_data.value
('((/event[1]/action[@name="client_app_name"]/value/text())[1])'
,'NVARCHAR(255)') AS [App]
,event_data.value
('((/event[1]/data[@name="cpu_time"]/value/text())[1])'
,'BIGINT') AS [CPU Time]
,event_data.value
('((/event[1]/data[@name="duration"]/value/text())[1])'
,'BIGINT') AS [Duration]
,event_data.value
('((/event[1]/data[@name="logical_reads"]/value/text())[1])'
,'INT') AS [Logical Reads]

114  Глава 4. Неэффективные запросы
,event_data.value
('((/event[1]/data[@name="physical_reads"]/value/text())[1])'
,'INT') AS [Physical Reads]
,event_data.value
('((/event[1]/data[@name="writes"]/value/text())[1])'
,'INT') AS [Writes]
,event_data.value
('((/event[1]/data[@name="row_count"]/value/text())[1])'
,'INT') AS [Rows]
,event_data.value(
'xs:hexBinary(((/event[1]/action[@name="query_hash"]/value/text())[1]))'
,'BINARY(8)') AS [Query Hash]
,event_data.value(
'xs:hexBinary(((/event[1]/action[@name="query_plan_hash"]/value/text())[1]))'
,'BINARY(8)') AS [Plan Hash]
,event_data.value(
'xs:hexBinary(((/event[1]/action[@name="plan_handle"]/value/text())[1]))'
,'VARBINARY(64)') AS [PlanHandle]
,event_data.value
('((/event[1]/data[@name="offset"]/value/text())[1])'
,'INT') AS [Stmt Offset]
,event_data.value
('((/event[1]/data[@name="offset_end"]/value/text())[1])'
,'INT') AS [Stmt Offset End]
,file_name
,file_offset
FROM
#EventData

)
SELECT
ei.*
,TRY_CONVERT(XML,qp.Query_Plan) AS [Plan]
INTO #Queries
FROM
EventInfo ei
OUTER APPLY
sys.dm_exec_text_query_plan
(
ei.PlanHandle
,ei.[Stmt Offset]
,ei.[Stmt Offset End]
) qp
OPTION (MAXDOP 1, RECOMPILE);

Теперь можно работать с сырыми данными из таблицы #Queries и находить
самые неэффективные запросы, требующие оптимизации. Часто также полезно
группировать данные на основе оператора, хеша запроса или хеша плана, анализируя совокупное влияние запросов.
В сопутствующих материалах этой книги приведен сценарий, с помощью которого можно захватывать рабочую нагрузку в целевой объект ring_buffer. Но тут

Хранилище запросов  115
есть особенность: представление sys.dm_xe_session_targets, которое отображает данные, собранные от целевого объекта, способно выводить только 4 Мбайт
XML-данных. В результате вы можете просто не увидеть некоторые события.
Повторюсь: остерегайтесь накладных расходов, которые возникают из-за расширенных событий и трассировки SQL. Не надо постоянно создавать или запускать их сеансы. Часто можно получить достаточно данных для устранения
неполадок, если запустить сеанс всего на несколько минут.

Хранилище запросов
Мы обсудили уже два подхода к обнаружению неэффективных запросов, и у обоих есть ограничения. Статистика на основе кэша планов может пропускать некоторые запросы; трассировки SQL и расширенные события требуют сложного
анализа выходных данных и способны сильно ухудшить производительность
в высоконагруженных системах.
Хранилище запросов, представленное в SQL Server 2016, помогает избавиться
от этих ограничений. Оно похоже на «черный ящик» в самолете. Когда хранилище запросов включено, SQL Server собирает и сохраняет статистику времени
выполнения и планы выполнения запросов. В результате видно, как работают
планы выполнения и как они меняются со временем. Наконец, при этом можно принудительно задавать конкретные планы выполнения для запросов. Это
уменьшает проблемы сканирования параметров, о которых мы еще поговорим
в главе 6.
В локальной версии SQL Server вплоть до SQL Server 2019 хранилище запросов по умолчанию отключено. Зато оно по умолчанию включено в базах
данных и управляемых экземплярах Azure SQL и новых базах данных, созданных в SQL Server 2022.

Хранилище запросов полностью интегрировано в конвейер обработки запросов,
как показано на рис. 4.4.
Когда нужно выполнить запрос, SQL Server ищет план выполнения в кэше
планов. Если план найден, SQL Server проверяет, нужно ли перекомпилировать
запрос (из-за обновлений статистики или других факторов), был ли создан
новый принудительный план и удален ли старый принудительный план из
хранилища.
Во время компиляции SQL Server проверяет, есть ли для запроса принудительный план. Если да, то запрос фактически компилируется с принудительным
планом, как и в случае указания USE PLAN. Если результирующий план действителен (valid), он сохраняется в кэше планов для повторного использования.

116  Глава 4. Неэффективные запросы

Запрос

Искать план в кэше

найден

Извлечь план из кэша

не найден

Перекомпилировать, существует Проверить, существует ли
используя
принудительный план
принудительный план

да

Проверить, нужна ли
перекомпиляция

Устаревшая статистика
Изменения схемы
Появление нового плана

не существует
нет
Действителен ли
Компилировать и
принудительный план?
оптимизировать запрос
да

Выполнить

Запрос
и план

Хранилище
запросов

нет
Статистика
выполнения

Конец
Рис. 4.4. Конвейер обработки запросов
Когда принудительный план больше недействителен (например, если пользователь удалил индекс, на который ссылается план), SQL Server все равно
выполнит запрос, однако при компиляции не будет создавать и кэшировать
принудительный план. При этом в хранилище запросов останутся оба плана,
но принудительный будет помечен как недействительный. Все это происходит
прозрачно для приложений.
В SQL Server 2022 и базах данных Azure SQL хранилище запросов позволяет
добавлять указания на уровне запроса с помощью хранимой процедуры sp_
query_store_set_hints1. С указаниями хранилища запросов SQL Server будет
компилировать и выполнять запросы так же, как если бы вы добавляли указания
в предложении OPTION в запросе. Это дает больше простора для маневра при настройке запросов и позволяет не модифицировать приложения.
Несмотря на тесную интеграцию с конвейером обработки запросов и внутренние
факторы оптимизации, хранилище запросов все равно увеличивает нагрузку на
систему. Масштаб этого увеличения зависит от двух основных факторов:
Количество компиляций в системе
Чем больше компиляций выполняет SQL Server, тем больше нагрузки ложится на хранилище запросов. Из-за этого оно может не лучшим образом
1

https://oreil.ly/xI3nA

Хранилище запросов  117
работать в системах с очень большой, нерегламентированной, непараметризованной рабочей нагрузкой.
Настройки сбора данных
В настройках хранилища запросов можно указать, нужно ли собирать все
запросы или только ресурсоемкие, а также задать интервалы группировки
и параметры резервирования данных. Чем больше данных вы собираете
и/или чем меньше интервалы группировки, тем больше накладные расходы.
Обратите особое внимание на параметр QUERY_CAPTURE_MODE: он определяет,
какие запросы захватываются. Если QUERY_CAPTURE_MODE=ALL (по умолчанию
в SQL Server 2016 и 2017), то хранилище запросов захватывает все запросы
в системе. Это сказывается на производительности, особенно при нерегламентированной рабочей нагрузке.
В случае QUERY_CAPTURE_MODE=AUTO (по умолчанию в SQL Server 2019 и более
поздних версиях) хранилище запросов не захватывает небольшие или редко
выполняемые запросы. Обычно этот вариант лучше.
Наконец, начиная с SQL Server 2019, можно установить QUERY_CAPTURE_
MODE=CUSTOM и вручную настроить критерии захвата запросов.
При грамотной настройке накладные расходы, связанные с хранилищем запросов, будут относительно невелики. Но иногда они оказываются значительными.
Например, я использовал хранилище запросов, когда оптимизировал производительность одного процесса, состоящего из очень большого количества небольших нерегламентированных запросов. Я перехватил все запросы в системе
в режиме QUERY_CAPTURE_MODE=ALL, собрав почти 10 Гбайт данных в хранилище
запросов. С включенным хранилищем процесс выполнялся 8 часов, а без него —
всего 2,5 часа.
Но если ваша система способна немного поднапрячься, я рекомендую включить
хранилище запросов. С ним, в частности, эффективнее работают некоторые
функции интеллектуальной обработки запросов (IQP). Также хранилище
упрощает настройку запросов и может сэкономить вам много часов работы,
если оно включено.
Отслеживайте ожидания QDS*, когда включено хранилище запросов. Слишком большие ожидания QDS* могут сигнализировать о том, что хранилище
создает высокую нагрузку. Ожидания QDS_PERSIST_TASK_MAIN_LOOP_SLEEP
и QDS_ASYNC_QUEUE можно игнорировать.

Есть два важных флага трассировки, которые стоит включить, если используется
хранилище запросов:

118  Глава 4. Неэффективные запросы
Т7745

Чтобы сократить накладные расходы, SQL Server периодически кэширует
некоторые данные из хранилища запросов в памяти, сбрасывая их в базу данных. Интервал сброса управляется параметром DATA_FLUSH_INTERVAL_SECONDS,
от которого зависит, сколько данных из хранилища запросов вы рискуете
потерять в случае сбоя SQL Server. Впрочем, обычно SQL Server сбрасывает
данные хранилища запросов из памяти во время завершения работы SQL
Server или аварийного переключения.
Если включен флаг трассировки T7745, SQL Server не сбрасывает данные на
диск при выключении и аварийном переключении, что позволяет сэкономить
время этих операций в высоконагруженных системах. Потеря небольшого
количества данных телеметрии обычно допустима.
Т7752 (SQL Server 2016 и 2017)

При запуске базы данных SQL Server загружает некоторые данные хранилища запросов в память, пока сама база еще недоступна. Если хранилища
запросов имеют большой объем, это может увеличить время перезапуска
или аварийного переключения SQL Server и затруднить работу пользователей.
Флаг трассировки T7752 заставляет SQL Server асинхронно загружать данные хранилища, позволяя параллельно с этим выполнять запросы. Во время
загрузки не будет собираться телеметрия, но обычно это приемлемо ради
более быстрого запуска.
Можно проанализировать влияние синхронной загрузки хранилища запросов, просмотрев время ожидания для типа ожидания QDS_LOADDB. Это
ожидание происходит только при запуске базы данных, поэтому, чтобы получить нужное значение, надо запросить представление sys.dm_os_wait_stats
и отфильтровать вывод по типу ожидания.
Как правило, не следует создавать слишком большое хранилище запросов. Кроме
того, рекомендую отслеживать размер хранилища, особенно в высоконагруженных системах. Иногда SQL Server не успевает очищать данные достаточно
быстро, особенно если вы используете режим сбора QUERY_CAPTURE_MODE=ALL.
Наконец, лучше установить последние обновления SQL Server, особенно если вы
используете SQL Server 2016 и 2017. С тех пор как хранилище запросов впервые
появилось в SQL Server, для него было выпущено много обновлений, которые
улучшали масштабируемость и исправляли ошибки.
Работать с хранилищем запросов можно двумя способами: через графический
интерфейс в SSMS или напрямую запрашивая динамические административные
представления. Давайте сначала посмотрим на графический интерфейс.

Хранилище запросов  119

Отчеты хранилища запросов в SSMS
Если в базе данных включено хранилище запросов, вы увидите папку Query Store
на панели Object Explorer (рис. 4.5). Количество отчетов в папке будет зависеть от
версий SQL Server и SSMS в вашей системе. В оставшейся части этого раздела
вы познакомитесь с семью отчетами, показанными на рис. 4.5.

Рис. 4.5. Отчеты хранилища запросов в SSMS

Регрессивные запросы
В отчете, показанном на рис. 4.6, отображаются запросы, производительность
которых со временем упала. Для анализа можно настроить временные рамки
и критерии регрессии: например, дисковые операции, потребление ЦП и количество выполнений.
Выберите запрос на графике в верхней левой части отчета. В верхней правой части
отчета перечислены собранные планы выполнения для этого запроса. Можно нажимать на точки, соответствующие различным планам выполнения, и просматривать сами планы внизу. Также можно сравнивать планы выполнения между собой.
Кнопка Force Plan позволяет принудительно назначить план запросу. На внутреннем уровне она вызывает хранимую процедуру sys.sp_query_store_force_plan1.
Аналогично кнопка Unforce Plan отменяет принудительный план, вызывая
хранимую процедуру sys.sp_query_store_unforce_plan2.
1

https://oreil.ly/pnBff

2

https://oreil.ly/wa4i9

120  Глава 4. Неэффективные запросы

Рис. 4.6. Отчет Regressed Queries
Отчет Regressed Queries — отличный инструмент для устранения неполадок,
связанных со сканированием параметров, которое мы обсудим в главе 6. Эти
проблемы легко исправить, принудительно задав конкретные планы выполнения.

Наиболее ресурсоемкие запросы
Этот отчет (рис. 4.7) позволяет обнаружить самые ресурсоемкие запросы в системе. Данные в нем аналогичны представлению sys.dm_exec_query_stats, однако
не зависят от кэша планов. Здесь можно настроить метрики, используемые для
сортировки данных, а также временной интервал.

Общее потребление ресурсов
Этот отчет содержит статистику рабочей нагрузки и использования ресурсов
за указанные интервалы времени. Он позволяет обнаруживать и анализировать
всплески использования ресурсов и детализировать запросы, вызывающие такие
всплески. На рис. 4.8 показан вывод отчета.

Хранилище запросов  121

Рис. 4.7. Отчет Resource Consuming Queries

Рис. 4.8. Отчет Overall Resource Consumption

122  Глава 4. Неэффективные запросы

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

Запросы с принудительными планами
В отчете Queries With Forced Plans показаны запросы, для которыхпринудительно задан план выполнения.

Статистика ожидания запроса
Отчет Query Wait Statistics позволяет обнаруживать запросы с большими ожиданиями. Данные сгруппированы по нескольким категориям (таким, как ЦП,
диск и блокировка) в зависимости от типа ожидания. В документации Microsoft1
можно найти подробную информацию о том, что означает каждый тип ожидания.

Отслеживаемые запросы
Отчет Tracked Queries позволяет отслеживать планы выполнения и статистики
для отдельных запросов. Он содержит ту же информацию, что и отчеты Regressed
Queries и Top Resource Consuming Queries, но на уровне отдельных запросов.
Все эти отчеты предоставляют большой объем данных для анализа. Однако время от времени приходится использовать T-SQL и напрямую работать с данными
хранилища запросов. Давайте посмотрим, как этого добиться.

Работа с динамическими административными
представлениями хранилища запросов
Динамические административные представления хранилища запросов (Query
Store DMVs) сильно нормализованы, как показано на рис. 4.9. Статистика выполнения отслеживается для каждого плана выполнения и группируется по
интервалам сбора, которые задаются параметром INTERVAL_LENGTH_MINUTES.
В большинстве случаев годится заданный по умолчанию интервал в 60 минут.
Нетрудно догадаться, что чем меньше интервалы, тем больше данных будет собрано в хранилище запросов. То же самое относится к рабочей нагрузке системы:
чрезмерное количество нерегламентированных запросов может раздуть объем
хранилища. Не забывайте об этом, когда настраиваете хранилище запросов
в своей системе.
1

https://oreil.ly/HmiJy

Хранилище запросов  123

Рис. 4.9. Динамические административные представления хранилища запросов
Динамические административные представления логически относятся либо
к хранилищу планов, либо к статистике времени выполнения. Представления
хранилища планов таковы:
sys.query_store_query1

Информация о запросах и статистика их компиляции, а также время последнего запуска.
sys.query_store_query_text2

Сведения о тексте запроса.
1

https://oreil.ly/arFQS

2

https://oreil.ly/Add1d

124  Глава 4. Неэффективные запросы
sys.query_context_setting1

Настройки контекста, связанные с запросом. Это представление содержит
параметры SET, схему сеанса по умолчанию, язык и другие атрибуты. При
разных наборах этих настроек SQL Server может генерировать и кэшировать
разные планы выполнения для одного и того же запроса.
sys.query_store_plan2

Информация о планах выполнения запросов. Столбец is_forced_plan указывает, является ли план принудительным. Столбец last_force_failure_reason
говорит, почему принудительный план не был применен к запросу.
Как видите, у каждого запроса может быть по несколько записей в представлениях sys.query_store_query и sys.query_store_plan. Это зависит от параметров
контекста вашего сеанса, перекомпиляции и других факторов.
Три других представления относятся к статистике времени выполнения:
sys.query_store_runtime_stats_interval3

Информация об интервалах сбора статистики.
sys.query_store_runtime_stats4

Это представление ссылается на sys.query_store_plan и содержит информацию о статистике времени выполнения для того или иного плана в течение определенного интервала sys.query_store_runtime_stats_interval.
Оно предоставляет сведения о количестве запусков, процессорном времени
и длительности вызовов, статистике логических и физических операций
ввода/вывода, использовании журнала транзакций, степени параллелизма,
размере выделяемой памяти, а также некоторые другие полезные показатели.
sys.query_store_wait_stats5

Начиная с SQL Server 2017, с помощью этого представления можно получить информацию об ожиданиях запросов. Данные собираются для каждого
плана и временного интервала и группируются по нескольким категориям
ожидания, включая ЦП, память и блокировку.
Рассмотрим несколько сценариев работы с данными хранилища запросов.

1

https://oreil.ly/gcu4z

2

https://oreil.ly/EnY5C

3

https://oreil.ly/Aa8Wy

4

https://oreil.ly/Icoax

5

https://oreil.ly/HmiJy

Хранилище запросов  125
В листинге 4.8 показан код, который возвращает информацию о 50 запросах с самым интенсивным вводом/выводом. Поскольку хранилище запросов сохраняет
статистику выполнения за множество отдельных временных интервалов, нужно
агрегировать данные из нескольких строк sys.query_store_runtime_stats. В результате вы увидите данные для всех интервалов, которые закончились в течение
последних 24 часов, сгруппированные по запросам и их планам выполнения.
Стоит отметить, что информация о дате и времени в хранилище запросов представлена типом datetimeoffset. Имейте это в виду, когда будете фильтровать
данные.

Листинг 4.8. Получение информации о ресурсоемких запросах из хранилища запросов
SELECT TOP 50
q.query_id, qt.query_sql_text, qp.plan_id, qp.query_plan
,SUM(rs.count_executions) AS [Execution Cnt]
,CONVERT(INT,SUM(rs.count_executions *
(rs.avg_logical_io_reads + avg_logical_io_writes)) /
SUM(rs.count_executions)) AS [Avg IO]
,CONVERT(INT,SUM(rs.count_executions *
(rs.avg_logical_io_reads + avg_logical_io_writes))) AS [Total IO]
,CONVERT(INT,SUM(rs.count_executions * rs.avg_cpu_time) /
SUM(rs.count_executions)) AS [Avg CPU]
,CONVERT(INT,SUM(rs.count_executions * rs.avg_cpu_time)) AS [Total CPU]
,CONVERT(INT,SUM(rs.count_executions * rs.avg_duration) /
SUM(rs.count_executions)) AS [Avg Duration]
,CONVERT(INT,SUM(rs.count_executions * rs.avg_duration))
AS [Total Duration]
,CONVERT(INT,SUM(rs.count_executions * rs.avg_physical_io_reads) /
SUM(rs.count_executions)) AS [Avg Physical Reads]
,CONVERT(INT,SUM(rs.count_executions * rs.avg_physical_io_reads))
AS [Total Physical Reads]
,CONVERT(INT,SUM(rs.count_executions * rs.avg_query_max_used_memory) /
SUM(rs.count_executions)) AS [Avg Memory Grant Pages]
,CONVERT(INT,SUM(rs.count_executions * rs.avg_query_max_used_memory))
AS [Total Memory Grant Pages]
,CONVERT(INT,SUM(rs.count_executions * rs.avg_rowcount) /
SUM(rs.count_executions)) AS [Avg Rows]
,CONVERT(INT,SUM(rs.count_executions * rs.avg_rowcount)) AS [Total Rows]
,CONVERT(INT,SUM(rs.count_executions * rs.avg_dop) /
SUM(rs.count_executions)) AS [Avg DOP]
,CONVERT(INT,SUM(rs.count_executions * rs.avg_dop)) AS [Total DOP]
FROM
sys.query_store_query q WITH (NOLOCK)
JOIN sys.query_store_plan qp WITH (NOLOCK) ON
q.query_id = qp.query_id
JOIN sys.query_store_query_text qt WITH (NOLOCK) ON
q.query_text_id = qt.query_text_id
JOIN sys.query_store_runtime_stats rs WITH (NOLOCK) ON
qp.plan_id = rs.plan_id
JOIN sys.query_store_runtime_stats_interval rsi WITH (NOLOCK) ON

126  Глава 4. Неэффективные запросы
rs.runtime_stats_interval_id = rsi.runtime_stats_interval_id
WHERE
rsi.end_time >= DATEADD(DAY,-1,SYSDATETIMEOFFSET())
GROUP BY
q.query_id, qt.query_sql_text, qp.plan_id, qp.query_plan
ORDER BY
[Avg IO] DESC
OPTION (MAXDOP 1, RECOMPILE);

Очевидно, что данные можно сортировать не только по среднему количеству
операций ввода/вывода, но и по другим критериям. Можно также добавить
предикаты в предложения WHERE и/или HAVING запроса, чтобы ограничить результаты. Например, можно отфильтровать результаты по столбцам DOP, если
требуется обнаружить запросы, которые используют параллелизм в среде OLTP,
и тонко настроить параметр Cost Threshold for Parallelism.
Еще один пример — поиск запросов, которые раздувают кэш планов. Код в листинге 4.9 содержит информацию о запросах, которые создают несколько планов
выполнения из-за разных настроек контекста. Чаще всего это происходит в двух
случаях: либо в сеансах используются различные параметры SET, либо запросы
ссылаются на объекты без имен схем.

Листинг 4.9. Запросы с разными настройками контекста
SELECT
q.query_id, qt.query_sql_text
,COUNT(DISTINCT q.context_settings_id) AS [Context Setting Cnt]
,COUNT(DISTINCT qp.plan_id) AS [Plan Count]
FROM
sys.query_store_query q WITH (NOLOCK)
JOIN sys.query_store_query_text qt WITH (NOLOCK) ON
q.query_text_id = qt.query_text_id
JOIN sys.query_store_plan qp WITH (NOLOCK) ON
q.query_id = qp.query_id
GROUP BY
q.query_id, qt.query_sql_text
HAVING
COUNT(DISTINCT q.context_settings_id) > 1
ORDER BY
COUNT(DISTINCT q.context_settings_id)
OPTION (MAXDOP 1, RECOMPILE);

В листинге 4.10 показано, как найти похожие запросы по значению query_hash
(SQL в выходных данных отображает один случайно выбранный запрос из
каждой группы). Обычно эти запросы относятся к непараметризованной нерегламентированной рабочей нагрузке в системе. В коде эти запросы можно
параметризовать. Если это не удается, попробуйте принудительную параметризацию, о которой я расскажу в главе 6.

Хранилище запросов  127

Листинг 4.10. Обнаружение запросов с одинаковыми значениями query_hash
;WITH Queries(query_hash, [Query Count], [Exec Count], qtid)
AS
(
SELECT TOP 100
q.query_hash
,COUNT(DISTINCT q.query_id)
,SUM(rs.count_executions)
,MIN(q.query_text_id)
FROM
sys.query_store_query q WITH (NOLOCK)
JOIN sys.query_store_plan qp WITH (NOLOCK) ON
q.query_id = qp.query_id
JOIN sys.query_store_runtime_stats rs WITH (NOLOCK) ON
qp.plan_id = rs.plan_id
GROUP BY
q.query_hash
HAVING
COUNT(DISTINCT q.query_id) > 1
)
SELECT
q.query_hash
,qt.query_sql_text AS [Sample SQL]
,q.[Query Count]
,q.[Exec Count]
FROM
Queries q CROSS APPLY
(
SELECT TOP 1 qt.query_sql_text
FROM sys.query_store_query_text qt WITH (NOLOCK)
WHERE qt.query_text_id = q.qtid
) qt
ORDER BY
[Query Count] DESC, [Exec Count] DESC
OPTION(MAXDOP 1, RECOMPILE);

В общем, возможности хранилища запросов безграничны. Используйте его, если
ваша система способна выдержать сопутствующую нагрузку.
Наконец, команда DBCC CLONEDATABASE1 позволяет создать клон базы данных,
содержащий только схему, чтобы с его помощью исследовать проблемы с производительностью. По умолчанию клон включает в себя данные хранилища запросов, так что его можно восстановить и выполнить анализ на другом сервере,
чтобы не перегружать промышленный экземпляр.

1

https://oreil.ly/2jYGV

128  Глава 4. Неэффективные запросы

Сторонние инструменты
Как вы убедились, SQL Server предлагает богатый набор инструментов, чтобы
обнаруживать неэффективные запросы. Тем не менее иногда бывают полезны
и средства мониторинга от других поставщиков. Большинство из этих средств
перечисляет самые ресурсоемкие запросы для анализа и оптимизации. Многие
сторонние инструменты также способны задать отправную точку для анализа
тенденций и обнаружения регрессивных запросов.
Я не буду описывать конкретные инструменты, но хочу предложить несколько
советов о том, как их выбирать и использовать.
Чтобы эффективно применять тот или иной инструмент, в нем нужно разбираться. Изучите, как он работает, каковы его ограничения и какие данные
он может упустить. Например, если инструмент получает данные, опрашивая
представление sys.dm_exec_requests по расписанию, он может пропустить множество мелких, но часто выполняемых запросов, которые запускаются между
опросами. Вместе с тем, если инструмент определяет неэффективные запросы
по времени ожидания сеанса, то результаты будут сильно зависеть от рабочей
нагрузки системы, объема кэшированных данных в буферном пуле и многих
других факторов.
В зависимости от ваших конкретных потребностей эти ограничения могут
быть приемлемыми. Помните принцип Парето: не обязательно оптимизировать
абсолютно все неэффективные запросы в системе, чтобы добиться желаемой
(или приемлемой) окупаемости. Всегда полезно взглянуть на проблему в целом
и с разных сторон. Например, легко сверить список неэффективных запросов,
который выводит инструмент, со статистикой выполнения на основе кэша планов и таким образом получить более полный список.
Разбираться в инструменте важно еще затем, чтобы оценивать накладные расходы, которые он может вызвать. Некоторые динамические административные представления очень ресурсоемки. Например, если инструмент вызывает
функцию sys.dm_exec_query_plan при каждом опросе sys.dm_exec_requests,
это может ощутимо увеличить нагрузку и без того высоконагруженной системы. Также нередко инструменты плодят трассировки и сеансы расширенных
событий без вашего ведома.
Не стоит слепо доверять официальной документации и поставщикам, когда они
заявляют, что инструмент безопасен. В разных системах он может проявить
себя по-разному. Имеет смысл протестировать накладные расходы на рабочей
нагрузке, сравнив показатели системы с инструментом и без него. Имейте в виду,
что накладные расходы не обязательно постоянны и могут увеличиваться по
мере изменения рабочей нагрузки.

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

Резюме
Неэффективные запросы пагубно влияют на производительность SQL Server
и могут перегрузить дисковую подсистему. Даже если в системе достаточно
памяти, чтобы кэшировать данные в буферном пуле, эти запросы расходуют
ресурсы ЦП, плодят блокировки и снижают качество обслуживания клиентов.
SQL Server отслеживает показатели выполнения каждого кэшированного плана
и выводит их в представлении sys.dm_exec_query_stats. Также можно получать
статистику выполнения для хранимых процедур, триггеров и пользовательских
скалярных функций с помощью представлений sys.dm_exec_procedure_stats,
sys.dm_exec_trigger_stats и sys.dm_exec_function_stats соответственно.
Статистика выполнения на основе кэша планов не отслеживает показатели времени выполнения для планов, а также запросы, планы которых не кэшировались.
Учитывайте это, когда анализируете и настраиваете запросы.
Неэффективные запросы можно захватывать в режиме реального времени
с помощью расширенных событий и трассировок SQL. Оба подхода создают
существенные накладные расходы, особенно в высоконагруженных системах.
Кроме того, они выводят сырые данные, которые придется обрабатывать и агрегировать для дальнейшего анализа.
В SQL Server 2016 и более поздних версиях можно использовать хранилище
запросов. Это отличный инструмент, который не зависит от кэша планов и позволяет быстро выявлять регрессии плана. Хранилище запросов тоже добавляет
накладные расходы, но они обычно приемлемы (хотя лучше оценить их в каждом
конкретном случае).
Наконец, неэффективные запросы можно искать с помощью сторонних инструментов мониторинга. Важно разбираться в том, как работает тот или иной
инструмент, и представлять себе его ограничения и накладные расходы.
В следующей главе я расскажу о распространенных методах, с помощью которых
можно оптимизировать неэффективные запросы.

130  Глава 4. Неэффективные запросы

Чек-лист устранения неполадок
Получить перечень неэффективных запросов из представления sys.dm_
exec_query_stats. Отсортировать данные в соответствии с вашей страте­
гией устранения неполадок (ЦП, ввод/вывод и т. д.).
Определить самые ресурсоемкие хранимые процедуры, используя представление sys.dm_exec_procedure_stats.
По возможности включить хранилище запросов и анализировать данные,
собранные с его помощью. (Это не всегда возможно, если вы уже используете внешние инструменты мониторинга.)
Включить флаги трассировки T7745 и T7752, чтобы повысить производительность запуска и завершения работы SQL Server, когда используется
хранилище запросов.
Проанализировать данные сторонних инструментов мониторинга и сверить их с данными SQL Server.
Проанализировать накладные расходы, возникающие из-за неэффективных запросов. Изучить, как запросы потребляют ресурсы, и сверить эти показатели со статистикой ожидания и нагрузкой на сервер.
При необходимости оптимизировать запросы.

ГЛАВА 5

Хранение данных
и настройка запросов

Оптимизации и настройке запросов можно посвятить целую книгу. И таких книг
уже много, так что советую почитать их всем, кто хочет улучшить свои навыки.
Не пытаясь дублировать другие книги, в этой главе я рассмотрю некоторые из
наиболее важных концепций, относящихся к настройке запросов.
Нельзя освоить оптимизацию запросов, если не разбираться во внутренней
структуре индекса и принципах, которые SQL Server использует для доступа к данным. Поэтому глава начинается с общего обзора индексов на основе
B-деревьев, а также операций поиска и просмотра.
Затем мы обсудим статистику и оценку количества элементов, а также узнаем,
как читать и анализировать планы выполнения.
Наконец, я расскажу о распространенных проблемах, с которыми можно столкнуться в процессе настройки запросов, и объясню, как их решать и как индексировать данные.

Хранение данных и схемы доступа
Современные версии SQL Server поддерживают три технологии хранения и обработки данных. Самый старый и востребованный метод — хранение на основе
строк (row-based storage). При таком методе хранения все столбцы таблицы
объединяются в строки данных, которые размещаются на страницах данных размером 8 Кбайт. Логически эти строки представлены либо индексами на основе
B-деревьев, либо кучами (мы обсудим эти структуры чуть позже).
Начиная с SQL Server 2012, некоторые индексы или целые таблицы можно хранить в формате столбцов, используя хранилище на основе столбцов (column-based
storage) и индекс columnstore. Данные в таких индексах сильно сжаты и хранятся
для каждого столбца отдельно. Эта технология оптимизирована так, чтобы обес­

132  Глава 5. Хранение данных и настройка запросов
печивать высокую производительность в случае аналитических запросов только
для чтения, когда просматриваются большие объемы данных. К сожалению, она
плохо масштабируется в рабочей нагрузке OLTP.
Наконец, начиная с SQL Server 2014, доступен механизм In-Memory OLTP,
который хранит данные в таблицах, оптимизированных для памяти. Данные
в таких таблицах полностью размещаются в памяти и отлично подходят для
высоких рабочих нагрузок OLTP.
Можно использовать все три технологии вместе — таблицы на основе строк,
таблицы на основе столбцов и таблицы, оптимизированные для памяти, —
распределив данные между ними. Этот подход чрезвычайно полезен, когда
в одной и той же системе нужно поддерживать и ресурсоемкие OLTP, и аналитические рабочие нагрузки. Эта архитектура подробно описана в моей
книге Pro SQL Server Internals (Apress, 2016).

По умолчанию используется хранение на основе строк, и это, безусловно, самый
распространенный формат в SQL Server. Операторы CREATE TABLE и CREATE INDEX
сохраняют данные в строках, если явно не указано иное. Хранение на основе
строк хорошо справляется с умеренными рабочими нагрузками OLTP и аналитическими задачами. Оно добавляет меньше административных издержек, чем
индексы columnstore и In-Memory OLTP.
В этой главе мы поговорим о хранении на основе строк и запросах, которые
работают с индексами на основе B-деревьев. Начнем с того, как SQL Server
хранит данные на основе строк.

Таблицы на основе строк
Внутренняя структура таблицы на основе строк состоит из нескольких элементов и внутренних объектов, как показано на рис. 5.1.
Данные в таблицах либо хранятся полностью несортированными (такие таблицы
называются кучами), либо сортируются по значению ключа кластеризованного
индекса, если он определен.
Не углубляясь в детали, скажу лишь, что обычно лучше не хранить данные в кучах, а определить для таблиц кластеризованные индексы. Только в некоторых
особых случаях таблицы-кучи оказываются эффективнее кластеризованных
индексов.
Помимо одного кластеризованного индекса, у каждой таблицы может быть набор некластеризованных индексов: это отдельные структуры, в которых хранятся
копии данных из таблицы, отсортированные по ключевым столбцам индекса. Например, если столбец включен в два некластеризованных индекса, то SQL Server

Таблицы на основе строк  133
сохранит эти данные трижды: один раз в кластеризованном индексе или куче
и по одному разу в каждом из двух некластеризованных индексов.

Таблица

1

Кластеризованный
индекс или куча
Некластеризованный
индекс

0...N

Некластеризованный
индекс

Страница
данных

IN_ROW_DATA

Раздел 1
Раздел N

Страница
данных

ROW_OVERFLOW_DATA
(optional)

Раздел 1
Раздел 1
Раздел N

Страница
данных

LOB_DATA
(optional)

Единицы распределения

1...N

Рис. 5.1. Внутренняя структура таблицы на основе строк
Хотя SQL Server позволяет создавать много некластеризованных индексов,
лучше так не делать, особенно в системах OLTP с изменчивыми данными.
Это чревато не только накладными расходами на хранение, но и тем, что при
модификации данных SQL Server будет вставлять, обновлять или удалять
строки в каждом некластеризованном индексе, поддерживая несколько копий данных.
Большое количество некластеризованных индексов — плохая идея, но это не
значит, что вам стоит немедленно искоренять лишние индексы. У таблицы
должно быть столько индексов, чтобы нагрузка системы была оптимальна.
В главе 14 я расскажу, как анализировать использование индексов.

Всякий индекс (как и куча) состоит из одного или нескольких разделов. Каждый
раздел — это внутренняя структура данных (индекс или куча), независимая от
других разделов таблицы. Каждый индекс в таблице можно разбивать на разделы по-своему, хотя обычно полезно разбивать все индексы одинаково, чтобы
они были выровнены относительно друг друга.
Как я упоминал ранее, фактические данные хранятся в строках данных на страницах данных размером 8 Кбайт, из которых пользователям доступно 8060 байт.
Данные из всех столбцов хранятся вместе, кроме случаев, когда данные столбца
не помещаются на странице.

134  Глава 5. Хранение данных и настройка запросов
Страницы данных делятся на три группы, называемые единицами распределения.
На страницах единиц распределения IN_ROW_DATA хранятся строковые объекты
основных данных: внутренние атрибуты и данные из столбцов фиксированной
длины (таких, как int, datetime, float и т. д.). Внутренняя часть строки должна
умещаться на одной странице данных, поэтому ее размер не может превышать
8060 байт. Данные из столбцов переменной длины, таких как (n)var char(max),
varbinary(max), xml и других, тоже могут храниться в основных строковых объектах, если удовлетворяют этому ограничению.
Когда данные переменной длины не помещаются в строке, SQL Server сохраняет их вне строки на других страницах данных и ссылается на них с помощью
указателей в строке. Данные переменной длины, размер которых превышает
8000 байт, размещаются на страницах данных, относящихся к единицам распределения LOB_DATA (LOB означает «large objects» — «большие объекты»).
В остальных случаях данные размещаются на страницах единиц распределения
ROW_OVERFLOW_DATA.
Данные типа (n)text и image по умолчанию хранятся в единицах распределения LOB_DATA. Эту настройку можно регулировать с помощью хранимой
процедуры sp_tableoption1.

Здесь стоит повторить известный совет: не извлекайте в инструкциях SELECT ненужные столбцы, особенно с помощью SELECT *. Для этого могут понадобиться
дополнительные операции ввода/вывода, чтобы получить данные со страниц
вне строки, а использование покрывающих индексов будет отложено, как вы
увидите далее в этой главе.
Наконец, SQL Server логически группирует блоки по 8 страниц в единицы распределения по 64 Кбайт, называемые экстентами. Существуют два типа экстентов. В смешанных (mixed) экстентах хранятся данные, относящиеся к разным
объектам. Однородные (uniform) экстенты хранят данные для одного и того же
объекта. По умолчанию при создании нового объекта SQL Server сохраняет первые 8 страниц объекта в смешанных экстентах. После этого все дополнительное
пространство для этого объекта выделяется однородными экстентами.
Выделение смешанных экстентов можно отключить с помощью флага трассировки T1118 на уровне сервера. В SQL Server 2016 и более поздних версиях можно
управлять выделением смешанных экстентов на уровне базы данных с помощью
параметра MIXED_PAGE_ALLOCATION. Если отключить смешанные экстенты, то при
создании каждой новой таблицы понадобится меньше изменений в системных
таблицах. Обычно это не дает заметных преимуществ в пользовательских базах
данных, но может значительно улучшить пропускную способность базы данных
1

https://oreil.ly/7WTZi

Таблицы на основе строк  135
tempdb в высоконагруженных системах OLTP. Отключать выделение смешанных
экстентов с помощью флага трассировки T1118 имеет смысл в старых версиях
SQL Server (до 2016). Начиная с SQL Server 2016, смешанные экстенты в tempdb

уже не используются, поэтому этот флаг неактуален.
Теперь посмотрим на структуру индексов.

Индексы на основе B-деревьев
Кластеризованные и некластеризованные индексы хранятся в очень похожих
внутренних форматах B-деревьев. Для примера создадим таблицу Customers, как
показано в листинге 5.1. В таблице заданы индексы: кластеризованный индекс
для столбца CustomerId и некластеризованный — для столбца Name.

Листинг 5.1. Таблица Customers
CREATE TABLE dbo.Customers
(
CustomerId INT NOT NULL,
Name NVARCHAR(64) NOT NULL,
Phone VARCHAR(32) NULL,
/* Прочие столбцы */
);
CREATE UNIQUE CLUSTERED INDEX IDX_Customers_CustomerId
ON dbo.Customers(CustomerId);
CREATE NONCLUSTERED INDEX IDX_Customers_Name
ON dbo.Customers(Name);

ОГРАНИЧЕНИЯ ИЛИ ИНДЕКСЫ?
Как вы могли заметить, я определил для таблицы кластеризованный индекс, вместо того чтобы
создать ограничение типа «первичный ключ». Это сделано намеренно. Я всегда рассматриваю
ограничения как часть логической структуры базы данных, которая определяет сущности и их
ключевые атрибуты. Индексы же относятся к физической структуре базы данных.
По умолчанию SQL Server создает уникальные кластеризованные индексы для первичных ключей.
Однако можно, а часто даже нужно помечать первичные ключи как некластеризованные, чтобы
они стали уникальными некластеризованными индексами.
За исключением нескольких ситуаций, когда SQL Server требует задавать первичные ключи, выбор
между ограничениями и индексами — дело вкуса. Ограничения Primary (первичный) и Unique
(уникальный) на внутреннем уровне реализованы как индексы и ведут себя соответствующим образом. Во время настройки производительности мы будем работать с индексами, поэтому в этой
книге ограничениям не уделяется специального внимания. Но это не означает, будто вам не надо
их использовать.

136  Глава 5. Хранение данных и настройка запросов
Логическая структура кластеризованного индекса показана на рис 5.2.
(null)
350
813
...
93533

(null)
6
13
...
344

1 Victor
2 Brian
3 Lisa
4 Dmitri
5 Anton

1:176
1:177
1:179
1:944

6 Perry
7 Boris
8 Ashley
9 Alyson
10 Greg

350
357
368
375
...

1:945
1:946
1:1724
1:947

1:170
1:171
1:1342

Корневой
уровень

1:1221

93533
93540
...
93701
93710

93701 Kevin
93702 Mike
93705 Andy
93707 Bob
93708 Mary

1:6945
1:6946
1:7022
1:7022

93710 Serg
93711 John

Промежуточный
уровень

Листовой
уровень

Рис. 5.2. Индекс на основе B-дерева
Нижний уровень индекса называется листовым. На нем хранятся данные, отсор­
тированные по ключу индекса. Если индекс кластеризованный, то на листовом
уровне хранятся все данные из таблицы, отсортированные по кластеризованному
ключу. Если быть точнее, листовой уровень содержит только данные IN_ROW,
которые могут ссылаться на данные столбца вне строки, размещенные на других
страницах.
Если все данные в индексе умещаются на одной странице данных, то индекс
будет состоять из этой единственной листовой страницы. В противном случае
SQL Server начнет создавать промежуточные уровни индекса. Каждая строка
на странице промежуточного уровня ссылается на страницу уровнем ниже и содержит минимальное значение ключа на той странице, а также ее физический
адрес (FileId:PageId) в базе данных. Единственное исключение — самая первая
строка, в которой хранится NULL вместо минимального значения ключа.
SQL Server создает промежуточные уровни, пока не достигнет уровня с единственной страницей. Этот уровень называется корневым и служит точкой входа
в индекс.
Страницы на каждом уровне индекса объединены в двусвязный список. Каждая страница ссылается на предыдущую и следующую страницы в индексе.
Это позволяет SQL Server просматривать индексы вперед и назад. (Однако

Таблицы на основе строк  137
имейте в виду, что обратный просмотр может быть менее эффективным, потому
что в этой операции SQL Server не использует параллелизм.)
SQL Server может получить доступ к данным в индексе посредством просмотра индекса или поиска по индексу. Просмотр индекса осуществляется двумя
способами.
Первый способ — просмотр порядка распределения (allocation order scan). С помощью системных страниц, называемых картами распределения индексов (IAM,
Index Allocation Map), SQL Server отслеживает экстенты, относящиеся к каждому
индексу в базе данных. Он в случайном порядке считывает страницы данных из
индекса в соответствии с IAM. В SQL Server этот метод используется только в особых случаях, потому что может вызвать проблемы с согласованностью данных.
Второй, более распространенный метод называется упорядоченным просмотром (ordered scan). Предположим, вы запускаете запрос SELECT Name FROM
dbo.Customers. Все строки данных находятся на листовом уровне индекса, так
что SQL Server может просмотреть этот уровень и вернуть строки клиенту.
SQL Server начинает с корневой страницы индекса и считывает оттуда первую
строку. Эта строка ссылается на промежуточную страницу с минимальным
значением ключа из таблицы. SQL Server считывает эту страницу и повторяет
процесс, пока не дойдет до первой страницы на листовом уровне. Затем SQL
Server считывает строки одну за другой, перемещаясь по связанному списку
страниц, пока не прочитает все строки (рис. 5.3).
(null)
350
813
...
93533
(null)
6
13
...
344

1 Victor
2 Brian
3 Lisa
4 Dmitri
5 Anton

1:176
1:177
1:179
1:944

6 Perry
7 Boris
8 Ashley
9 Alyson
10 Greg

350
357
368
375
...

1:170
1:171
1:1342
1:1221

1:945
1:946
1:1724
1:947

93533
93540
...
93701
93710

93701 Kevin
93702 Mike
93705 Andy
93707 Bob
93708 Mary
Рис. 5.3. Просмотр индекса

1:6945
1:6946
1:7022
1:7022

93710 Serg
93711 John

138  Глава 5. Хранение данных и настройка запросов
Очевидно, в реальной жизни все может оказаться сложнее. Например, в некоторых случаях запрос может одновременно просматривать разные части индекса
в соответствии с параллельными планами выполнения. А когда несколько запросов выполняются одновременно, SQL Server может объединять соответствующие просмотры индекса в один просмотр физического индекса. В любом случае,
если в плане выполнения есть оператор Index Scan, можно предположить, что
он извлечет все данные из индекса.
При этом есть одно исключение — когда в плане просмотр индекса идет сразу
после оператора TOP . В этом случае оператор просмотра остановится после
того, как вернет количество строк, указанное в TOP, и не будет перебирать всю
таблицу. Обычно так происходит, если в запросе нет предложения ORDER BY или
если ORDER BY соответствует ключу индекса.
На рис. 5.4 показана часть плана выполнения запроса SELECT TOP 3 Name FROM
dbo.Customers ORDER BY CustomerId. Свойства Actual Rows Read и Actual Rows
оператора Index Scan показывают, что просмотр останавливается после того,
как прочитаны три строки.

Рис. 5.4. Операторы Top и Index Scan
Как вы наверняка догадались, чтение всех данных из большого индекса — затратная операция. К счастью, SQL Server может обратиться к подмножеству данных,
используя операцию поиска по индексу. Допустим, вы запускаете запрос SELECT
Name FROM dbo.Customers WHERE CustomerId BETWEEN 4 AND 7. На рис. 5.5 показано,
как SQL Server может его обработать.
Чтобы прочитать диапазон строк из таблицы, SQL Server должен найти строку
с минимальным значением ключа из диапазона (в данном случае 4). SQL Server

Таблицы на основе строк  139
начинает с корневой страницы, где вторая строка ссылается на страницу с минимальным значением ключа 350. Это больше, чем искомое значение ключа,
поэтому SQL Server считывает страницу данных промежуточного уровня (1:170),
на которую ссылается первая строка корневой страницы.

(null)
350
813
...
93533
(null)
6
13
...
344

1 Victor
2 Brian
3 Lisa
4 Dmitri
5 Anton

1:176
1:177
1:179
1:944

6 Perry
7 Boris
8 Ashley
9 Alyson
10 Greg

350
357
368
375
...

1:170
1:171
1:1342
1:1221

1:945
1:946
1:1724
1:947

93533
93540
...
93701
93710

93701 Kevin
93702 Mike
93705 Andy
93707 Bob
93708 Mary

1:6945
1:6946
1:7022
1:7022

93710 Serg
93711 John

Рис. 5.5. Оператор Index Seek (Поиск по индексу)
Аналогично промежуточная страница приводит к первой листовой странице
(1:176). SQL Server считывает эту страницу, затем строки с CustomerId, равными
4 и 5, и, наконец, считывает две оставшиеся строки со второй страницы.
Формально существует два вида операций поиска по индексу:
Точечный поиск
При точечном поиске (также называемом одноэлементным поиском) SQL
Server ищет и возвращает одну строку. Например, предикат WHERE CustomerId
= 2 — это операция точечного поиска.
Просмотр диапазона
При просмотре диапазона SQL Server ищет наименьшее или наибольшее
значение ключа и просматривает набор строк (вперед или назад), пока
диапазон не кончится. Предикат WHERE CustomerId BETWEEN 4 AND 7 приводит
к просмотру диапазона. В планах выполнения оба случая фигурируют как
операторы Index Seek.

140  Глава 5. Хранение данных и настройка запросов
Нетрудно догадаться, что поиск по индексу более эффективен, чем просмотр
индекса, потому что при поиске по индексу SQL Server обычно обрабатывает
лишь подмножество строк и страниц данных, а не обходит весь индекс. Однако
оператор Index Seek в плане выполнения может сбивать с толку: это происходит,
когда он обозначает неэффективный просмотр диапазона, при котором считывается большое количество строк или даже весь индекс. О такой ситуации мы
поговорим позже в этой главе.
В реляционных базах данных существует концепция предикатов с поддержкой
поиска и аргументов (SARG, Search Argument-able). Такие предикаты позволяют
изолировать подмножество ключа индекса для последующей обработки. Благодаря SARG-предикатам SQL Server может отобрать одно значение или диапазон
значений ключа индекса, которые будут считаны при вычислении предиката,
и использовать поиск по индексу, если индекс существует.
Очевидно, при составлении запросов лучше использовать предикаты с поддержкой поиска и аргументов, а также по возможности задействовать поиск
по индексу. Это делается с помощью операторов, в том числе =, >, >=, = @Date AND Column <

CHAR(10),Column,121)) = @Date

DATEADD(DAY,1,@Date)

DATEPART(YEAR,Column) = @Year

Column >= @Year AND Column <
DATEADD(YEAR,1,@Year)

DATEADD(DAY,7,Column) > GET

Column > DATEADD(DAY,-7,GETDATE())

DATE()

Поиск по префиксу

LEFT(Column,3) = 'ABC'

Column LIKE 'ABC%'

Поиск по подстроке

Column LIKE '%ABC%'

Используйте полнотекстовый поиск
или другие технологии

Обращайте внимание на типы данных в предикатах. Операция неявного преобразования — это по сути вызов системной функции CONVERT_IMPLICIT, которая во
многих случаях препятствует поиску по индексу. Не забудьте проанализировать
предикаты JOIN и предложения WHERE. Частый источник проблем — несоответствие типов данных в столбцах, по которым происходит объединение.

166  Глава 5. Хранение данных и настройка запросов

Пользовательские функции
Как я уже говорил, системные и невстроенные скалярные пользовательские
функции могут помешать SQL Server применять поиск по индексу. Более того,
они значительно снижают производительность (особенно пользовательские
функции). SQL Server вызывает их для каждой обрабатываемой строки, и это
подобно вызовам хранимых процедур (в этом можно убедиться, если перехватить
расширенное событие rpc_starting или событие трассировки SP:Starting).
SQL Server оптимизирует код в невстроенных пользовательских функциях из
нескольких инструкций (скалярных и с табличным значением) отдельно от
запроса вызывающего объекта. Обычно это приводит к менее эффективным
планам выполнения. Но вот что еще важнее: когда SQL Server оценивает количество строк, которое возвратит функция с табличным значением из нескольких
инструкций, то оценочное значение всегда равно либо 1, либо 100 строк — в зависимости от версии SQL Server, уровня совместимости базы данных и параметров конфигурации. Это может полностью свести на нет оценку количества
элементов и породить крайне неэффективные планы.
В SQL Server 2017 это положение несколько выправилось. Одна из функций интеллектуальной обработки запросов — выполнение с чередованием — откладывает
окончательную компиляцию запроса до времени выполнения, когда SQL Server
может подсчитать фактическое количество строк, возвращаемых функцией,
и завершить оптимизацию, используя эти данные. Сейчас это работает только
с запросами SELECT, однако в будущем ситуация может измениться.
Тем не менее лучше избегать функций из нескольких инструкций и по возможности использовать встроенные функции с табличным значением. SQL
Server встраивает и оптимизирует их вместе с запросами вызывающего объекта.
К счастью, во многих случаях скалярные функции и функции из нескольких
инструкций с табличным значением можно минимальными усилиями преобразовать во встроенные функции с табличным значением.

Временные таблицы и табличные переменные
Временные таблицы и табличные переменные оказывают неоценимую помощь
при оптимизации запросов. Их можно использовать, чтобы сохранять промежуточные результаты запросов. Это позволяет упростить запросы, отчего
улучшается оценка количества элементов, а планы выполнения получаются
более эффективными.
С временными таблицами и табличными переменными связаны две распространенные ошибки.
Первая ошибка заключается в том, что некоторые специалисты предпочитают
табличные переменные временным таблицам из-за распространенного заблуждения, будто табличные переменные — это объекты в памяти, которые не

Характерные проблемы при настройке запросов  167
используют базу данных tempdb и поэтому более эффективны, чем временные
таблицы.
Это не так. Оба объекта обращаются к tempdb. Несмотря на то что табличные
переменные немного эффективнее, чем временные таблицы, эта эффективность
получается за счет ограничения: они не поддерживают статистику по первичным
ключам и индексам.
С другой стороны, временные таблицы ведут себя как обычные таблицы. Они
поддерживают статистику по индексам и разрешают SQL Server использовать
ее во время оптимизации. Хотя в некоторых специальных случаях табличные
переменные лучше, в большинстве ситуаций временные таблицы будут безопаснее. За свою карьеру я нередко добивался отличных результатов, заменяя
табличные переменные на временные таблицы без каких-либо дополнительных
изменений кода или индексации.
Вторая распространенная ошибка — не индексировать временные таблицы. Это
плохо влияет на оценку количества элементов и может привести к неэффективным просмотрам таблиц. Рассматривайте временные таблицы как обычные
и индексируйте их ради поддержки эффективных запросов, особенно когда
в таблицах содержатся значительные объемы данных.
В грамотно проиндексированной временной таблице можно сохранять результаты функций из нескольких инструкций с табличным значением. Это улучшит
оценку количества элементов, особенно в старых версиях SQL Server, где нет
выполнения с чередованием.
Очевидно, временные таблицы и табличные переменные имеют свою цену.
Их создание и заполнение связано с накладными расходами. Если применять
их разумно, то преимущества могут преобладать над недостатками, но вряд
ли стоит хранить в них миллионы строк. Мы поговорим об этом подробнее
в главе 9.

Хранимые процедуры и ORM-фреймворки
Хотя эта тема напрямую не связана с настройкой запросов, нельзя не упомянуть
фреймворки объектно-реляционного отображения (ORM, Object Relational
Mapping). Сейчас они чрезвычайно распространены, и можно без преувеличения
сказать, что все специалисты по базам данных их ненавидят. Эти фреймворки
генерируют запросы, которые чрезвычайно сложны и плохо поддаются оптимизации.
К сожалению, приходится признать, что фреймворки упрощают разработку и сокращают ее время и стоимость. В большинстве случаев нереально и неразумно
настаивать на том, чтобы разработчики приложений ими не пользовались. Что
еще более важно, во многих случаях вполне можно мириться с неидеальными
запросами, которые генерируют фреймворки.

168  Глава 5. Хранение данных и настройка запросов
Однако это не относится к запросам, критичным с точки зрения производительности. Их может быть не очень много, но все-таки эти немногие запросы
потребуют скрупулезной настройки и оптимизации. В таких случаях автоматически сгенерированные и/или нерегламентированные запросы — неудачное
решение. Лучше использовать хранимые процедуры, которые обеспечивают
полную гибкость и поддерживают более широкий набор методов оптимизации.
Чтобы перейти на хранимые процедуры, может понадобиться модифицировать
код приложения, но во многих случаях этот переход сокращает время и стоимость настройки.

Неэффективный поиск по индексу
Как вы уже знаете, операция поиска по индексу обычно эффективнее, чем просмотр индекса. Однако это не означает, что всякий поиск по индексу эффективен.
SQL Server использует поиск по индексу, когда предикаты запроса позволяют
изолировать диапазон строк данных от индекса во время выполнения запроса.
Если этот диапазон очень велик, эффективность операции может снизиться.
Рассмотрим простой пример: я создам таблицу и заполню ее данными. Затем
я выполню две инструкции SELECT — с предложением WHERE и без него, как показано в листинге 5.6.

Листинг 5.6. Неэффективность поиска по индексу
CREATE TABLE dbo.T1
(
IndexedCol INT NOT NULL,
NonIndexedCol INT NOT NULL
);
CREATE UNIQUE CLUSTERED INDEX IDX_T1
ON dbo.T1(IndexedCol);
;WITH N1(C) AS (SELECT 0 UNION ALL SELECT 0) -,N2(C) AS (SELECT 0 FROM N1 AS T1 CROSS JOIN N1
,N3(C) AS (SELECT 0 FROM N2 AS T1 CROSS JOIN N2
,N4(C) AS (SELECT 0 FROM N3 AS T1 CROSS JOIN N3
,N5(C) AS (SELECT 0 FROM N4 AS T1 CROSS JOIN N4
,N6(C) AS (SELECT 0 FROM N3 AS T1 CROSS JOIN N5
,IDs(ID) AS (SELECT ROW_NUMBER() OVER (ORDER BY
INSERT INTO dbo.T1(IndexedCol, NonIndexedCol)
SELECT ID, ID FROM IDs;

2 строки
AS T2) -- 4 строки
AS T2) -- 16 строк
AS T2) -- 256 строк
AS T2) -- 65,536 строк
AS T2) -- 1,048,576 строк
(SELECT NULL)) FROM N6)

SET STATISTICS IO ON
SELECT COUNT(*) FROM dbo.T1;
SELECT COUNT(*) FROM dbo.T1 WHERE IndexedCol > 0;

На рис. 5.15 показаны планы выполнения и статистика ввода/вывода обоих запросов. У всех строк в таблице положительные значения IndexedCol, поэтому

Характерные проблемы при настройке запросов  169
оба запроса вынуждены просматривать весь индекс. В целом поиск по индексу
оказался идентичен просмотру индекса.
Неэффективный поиск по индексу часто встречается в многопользовательских
системах. Возьмем, к примеру, систему обработки заказов, где данные обычно
распределены по относительно небольшому количеству складов. Как правило,
идентификатор пользователя (или идентификатор склада в этом примере)
становится крайним левым столбцом в ключах индекса.

Рис. 5.15. Неэффективный поиск по индексу
Запросы в этих системах обычно имеют дело с данными от одного пользователя,
который указывается в предикате в предложении WHERE, а это закономерно приводит к поиску по индексу в плане выполнения. Но если для каждого пользователя (то есть склада) хранится слишком много данных, вы можете получить
идеальный на вид, но неэффективный план выполнения даже без просмотров.
Вам понадобится применять другие предикаты, чтобы поиск по индексу стал
выборочным и производительность повысилась.
Можно проанализировать эффективность поиска по индексу в плане выполнения, если изучить свойства оператора. Запустим запрос, показанный в листинге 5.7, на таблице из листинга 5.6.

Листинг 5.7. Пример запроса
SELECT IndexedCol, NonIndexedCol
FROM dbo.T1
WHERE
IndexedCol BETWEEN 100 AND 150 AND
NonIndexedCol % 2 = 0;

170  Глава 5. Хранение данных и настройка запросов
На рис. 5.16 показано несколько ключевых свойств для анализа. Этот скриншот
сделан в Plan Explorer, но в SSMS отображаются те же данные.

Рис. 5.16. Свойства поиска по индексу
Рассмотрим самые важныесвойства оператора Index Seek.
Seek predicate (Предикат поиска)
Один или несколько предикатов, которые SQL Server использует, чтобы
ограничить диапазон строк во время поиска по индексу. Чем избирательнее
этот предикат, тем эффективнее поиск.
Predicate
Дополнительные критерии фильтрации, которые SQL Server применяет
к каждой строке, прочитанной оператором Index Seek. Они не уменьшают
размер данных, которые обрабатывает оператор, но могут сократить в плане
выполнения количество строк, которые оператор возвращает. В данном случае оператор прочитал 51 строку из индекса и вернул 26 строк следующему
оператору в плане выполнения.
Всегда целесообразнее уменьшать размер данных с помощью эффективного
предиката поиска. Если предикаты поиска недостаточно избирательны, попробуйте реструктурировать индекс так, чтобы SQL Server мог использовать
обычные предикаты в качестве предикатов поиска.

Характерные проблемы при настройке запросов  171
Actual rows и Actual rows read
В SSMS эти свойства называются Actual Number of Rows и Number of Rows
Read. Они показывают, сколько строк вернул оператор и сколько строк было
обработано во время выполнения соответственно. Большое значение Number
of Rows Read говорит о том, что поиск по индексу обработал большой объем
данных; это может потребовать дальнейшего изучения. Если эти два значения
существенно расходятся, то, возможно, индекс неэффективен, потому что
значительная часть данных отфильтровывается свойством Predicate, а не
Seek Predicate.
Estimated rows и Estimated rows to be read
Эти свойства в SSMS называются Estimated Number of Rows и Estimated
Number of Rows to be Read. Как уже отмечалось, можно сравнить оценочные
и фактические показатели в плане выполнения, чтобы понять качество оценки количества элементов. Если количество элементов оценивается с большой
ошибкой, это может указывать на неправильный выбор индекса и/или типа
соединения (подробнее об этом позже).
Увидев большую ошибку оценки количества элементов, убедитесь, что статистика актуальна. Проверьте запрос и удалите конструкции, которые могут
повлиять на оценку количества элементов (например, функции и табличные переменные). В некоторых случаях, особенно при сложных запросах,
подумайте об их рефакторинге и/или разделении — возможно, используя
временные таблицы для промежуточных данных.
Очевидно, анализировать план выполнения гораздо проще, если у вас есть
фактические метрики выполнения. Хотя оценочные метрики полезны для
первоначального анализа, ошибочная оценка количества элементов может дать
очень неправильную или неполную картину. Имея дело с оценочными планами
выполнения, проведите дополнительный анализ и посмотрите, как распределены
данные в таблицах.

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

172  Глава 5. Хранение данных и настройка запросов

Соединение в цикле
Соединение в цикле (или соединение с вложенным циклом) — это простейший
алгоритм соединения. Как и любой тип соединения, он принимает два входных
операнда, которые называются внешней и внутренней таблицами. Алгоритм соединения очень прост (см. листинг 5.8). SQL Server обходит внешнюю таблицу
и для каждой ее строки ищет во внутренней таблице строки для объединения.

Листинг 5.8. Алгоритм соединения в цикле (псевдокод)
/* Inner join */
for each row R1 in outer table
find row(s) R2 in inner table
if R1 joins with R2
return join (R1, R2)
/* Outer join */
for each row R1 in outer table
find row(s) R2 in inner table
if R1 joins with R2
return join (R1, R2)
else
return join (R1, NULL)

Ресурсоемкость соединения зависит от двух факторов, первый из которых — размер внешней таблицы. SQL Server обходит каждую строку внешней таблицы,
находя во внутренней таблице соответствующие строки для объединения. Чем
больше данных нужно обработать, тем затратнее процедура.
Второй фактор — эффективность поиска по внутренней таблице. Когда столбец
(или столбцы) соединения во внутренней таблице правильно проиндексирован,
SQL Server может использовать эффективную операцию поиска по индексу.
В этом случае издержки поиска по внутренней таблице на каждой итерации будут относительно низкими. Без индекса SQL Server, возможно, будет вынужден
просматривать внутреннюю таблицу несколько раз: по одному разу для каждой
строки из внешней таблицы. Как нетрудно догадаться, это крайне неэффективно.
Соединение в цикле оптимизировано для ситуаций, когда одна из таблиц небольшая, а у другой есть индекс, поддерживающий операцию поиска по индексу
для соединения. Нельзя точно определить момент, после которого соединение
станет неэффективным. Оно может хорошо работать с тысячами, а иногда
и с десятками тысяч строк внешней таблицы, но с миллионами строк могут возникнуть проблемы. Тем не менее в подходящих условиях этот тип соединения
чрезвычайно эффективен. Он требует мало ресурсов для запуска, не использует
tempdb и не потребляет больших объемов памяти.
Наконец, соединение в цикле — это единственный тип соединения, которому не
требуется предикат равенства. SQL Server может вычислить предикат соедине-

Характерные проблемы при настройке запросов  173
ния между любыми двумя строками из обеих таблиц, но этот предикат может
и вообще отсутствовать. Например, инструкция CROSS JOIN приведет к физическому соединению с вложенным циклом, в результате которого каждая строка
внешней таблицы будет соединена с каждой строкой внутренней. Очевидно,
SQL Server не сможет использовать поиск по индексу, если предикат соединения не поддерживает SARG, а это чревато крайне неэффективной обработкой
больших входных данных.

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

Листинг 5.9. Алгоритм соединения слиянием (псевдокод)
/* Inputs I1 and I2 are sorted */
get first row R1 from input I1
get first row R2 from input I2
while not end of either input
begin
if R1 joins with R2
begin
return join (R1, R2)
get next row R2 from I2
end
else if R1 < R2
get next row R1 from I1
else /* R1 > R2 */
get next row R2 from I2
end

Соединение слиянием оптимизировано для средних и больших данных, когда
обе входные таблицы отсортированы. Это значит, что входные данные должны
быть проиндексированы по столбцам предиката соединения. Однако на практике SQL Server может заняться сортировкой входных данных уже во время
выполнения запроса, и тогда не исключено, что сортировка потребует гораздо
больше ресурсов, чем само слияние. Проверьте, так ли это, и учтите ресурсо­
емкость оператора Sort в ходе анализа.
Есть еще одно предостережение. Слияние менее эффективно в сценариях соединения «многие ко многим», когда у обеих входных таблиц есть дубликаты
в значениях предиката соединения. В таких случаях SQL Server сохраняет повторяющиеся значения в рабочей таблице в tempdb, что может ухудшить производительность, когда дубликатов много. Чтобы определить, выполняется ли
соединение слиянием в этом режиме, можно просмотреть свойство Many to Many
оператора соединения в плане выполнения.

174  Глава 5. Хранение данных и настройка запросов
К сожалению, с этим мало что можно поделать. Поскольку столбцы предикатов
объединения слиянием обычно индексируются, убедитесь, что индексы определены как уникальные — если данные, на которых построены эти индексы,
должны быть уникальными.

Хеш-соединение
Хеш-соединение предназначено для обработки больших объемов несортированных входных данных. Его алгоритм состоит из двух этапов.
На первом этапе, или на этапе сборки, хеш-соединение просматривает одну из
входных таблиц (обычно меньшую), вычисляет хеш-значения ключа соединения
и помещает их в хеш-таблицу. На втором этапе (зондирование) алгоритм сканирует вторую входную таблицу и проверяет, есть ли в хеш-таблице хеш-значение
ключа соединения из второй таблицы. Если есть, то SQL Server вычисляет предикат соединения для строки из второй таблицы и всех строк из первой таблицы,
которые принадлежат к одному и тому же контейнеру кэша. Алгоритм показан
в листинге 5.10.

Листинг 5.10. Алгоритм внутреннего хеш-соединения (псевдокод)
/* Build Phase */
for each row R1 in input I1
begin
calculate hash value on R1 join key
insert hash value to appropriate bucket in hash table
end
/* Probe Phase */
for each row R2 in input I2
begin
calculate hash value on R2 join key
for each row R1 in hash table bucket
if R1 joins with R2
return join (R1, R2)
end

Хеш-соединению требуется память, чтобы хранить хеш-таблицу. При нехватке
памяти соединение сохраняет часть контейнеров хеш-таблицы в tempdb. Этот
эффект называется переносом (spill) и может сильно повлиять на производительность соединения, потому что доступ к базе данных tempdb работает значительно
медленнее.
Переносы часто происходят из-за неправильной оценки необходимого объема
памяти, что, в свою очередь, может быть вызвано ошибочной оценкой количества элементов. В этом случае убедитесь, что статистика актуальна; если это не
помогает, то попробуйте упростить или переработать запрос.

Характерные проблемы при настройке запросов  175
В интеллектуальной обработке запросов (IPQ) в SQL Server 2017 Enterprise
Edition появилась обратная связь по выделению памяти (memory grant feedback),
которая увеличивает или уменьшает объем памяти, выделяемой для запроса,
в зависимости от того, как память использовалась в предыдущих выполнениях.
В SQL Server 2017 эта функция доступна только в пакетном режиме, а начиная
с SQL Server 2019, она также включена в построчном режиме.
Ознакомьтесь с подробностями в документации Microsoft1 и подумайте о том,
чтобы перейти на уровень совместимости базы данных, который поддерживает
обратную связь по выделению памяти. Это может снизить количество переносов
в tempdb. Я расскажу об этом подробнее в главах 7 и 9.

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

Таблица 5.4. Сравнение типов соединений
Соединение в цикле

Соединение слиянием

Хеш-соединение

Лучший случай использования

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

Средние или крупные
таблицы, отсортированные по ключу индекса

Средние или крупные таблицы

Требуется сортировка
входных данных

Нет

Да

Нет

Требуется предикат
равенства

Нет

Да

Да

Блокирующий оператор

Нет

Нет

Да (на этапе
сборки)

Использует память

Нет

Нет

Да

Использует tempdb

Нет

Нет (при сортировке возможен перенос в tempdb)

Да, в случае переноса

Сохраняет порядок

Да (внешняя таблица)

Да

Нет

В средствах интеллектуальной обработки запросов в SQL Server 2017 появилась
концепция адаптивного соединения (adaptive join). При таком соединении SQL
Server во время выполнения выбирает, использовать цикл или хеш-соединение,
1

https://oreil.ly/qyyNx

176  Глава 5. Хранение данных и настройка запросов
в зависимости от размера входных данных. К сожалению, в SQL Server 2017
и 2019 это работает только в пакетном режиме выполнения, который в большинстве случаев активируется индексами columnstore. Чтобы в SSMS адаптивное
соединение отображалось в плане выполнения, нужно включить динамическую
статистику запросов.
Я только что упоминал, что каждый тип соединения оптимизирован для конкретных случаев и может плохо работать в других случаях. Давайте сравним
производительность разных типов соединения на простом примере. В листинге 5.11 мы создадим еще одну таблицу (аналогичную таблице из листинга 5.6)
и заполним ее теми же данными. В обеих таблицах по два столбца, для одного
из которых определен кластеризованный индекс.

Листинг 5.11. Эффективность соединения: создание таблицы
CREATE TABLE dbo.T2
(
IndexedCol INT NOT NULL,
NonIndexedCol INT NOT NULL
);
CREATE UNIQUE CLUSTERED INDEX IDX_T2
ON dbo.T2(IndexedCol);
INSERT INTO dbo.T2(IndexedCol, NonIndexedCol)
SELECT IndexedCol, NonIndexedCol FROM dbo.T1;

Затем сравним производительность разных типов соединения с помощью кода из
листинга 5.12. Здесь я принудительно применяю конкретные типы соединений
с помощью указаний соединения (подробнее об этом позже). Время выполнения
инструкций в моей тестовой среде указано в комментариях к коду.

Листинг. 5.12. Эффективность производительности: тестовые примеры
-- Соединение в цикле с поиском по индексу во внутренней таблице
-- Время выполнения: 137 мс
SELECT COUNT(*)
FROM dbo.T1 INNER LOOP JOIN dbo.T2 ON
T1.IndexedCol = T2.IndexedCol
WHERE
T1.NonIndexedCol i1.index_id AND
ic2.column_id = ic1.column_id AND

426  Глава 14. Анализ схемы базы данных и индексов
i2.type in (1,2) AND
i2.is_disabled = 0 AND
i2.is_hypothetical = 0 AND
(
i1.has_filter = i2.has_filter AND
ISNULL(i1.filter_definition,'') =
ISNULL(i2.filter_definition,'')
)
) dupIdx
CROSS APPLY
(
SELECT
(
SELECT
col.name AS [text()]
,IIF(icol_meta.is_descending_key = 1, ‚ DESC','')
AS [text()]
,',' AS [text()]
FROM
sys.index_columns icol_meta WITH (NOLOCK)
JOIN sys.columns col WITH (NOLOCK) ON
icol_meta.object_id = col.object_id AND
icol_meta.column_id = col.column_id
WHERE
icol_meta.object_id = i1.object_id AND
icol_meta.index_id = i1.index_id AND
icol_meta.is_included_column = 0
ORDER BY
icol_meta.key_ordinal
FOR XML PATH(‚')
) AS key_col
,(
SELECT
col.name AS [text()]
,',' AS [text()]
FROM
sys.index_columns icol_meta WITH (NOLOCK)
JOIN sys.columns col WITH (NOLOCK) ON
icol_meta.object_id = col.object_id AND
icol_meta.column_id = col.column_id
WHERE
icol_meta.object_id = i1.object_id AND
icol_meta.index_id = i1.index_id AND
icol_meta.is_included_column = 1
ORDER BY
col.name
FOR XML PATH(‚')
) AS included_col
) i1_col
CROSS APPLY
(
SELECT
(

Анализ схемы базы данных  427
SELECT
col.name AS [text()]
,IIF(icol_meta.is_descending_key = 1, ‚ DESC','')
AS [text()]
,',' AS [text()]
FROM
sys.index_columns icol_meta WITH (NOLOCK)
JOIN sys.columns col WITH (NOLOCK) ON
icol_meta.object_id = col.object_id AND
icol_meta.column_id = col.column_id
WHERE
icol_meta.object_id = t.object_id AND
icol_meta.index_id = dupIdx.index_id AND
icol_meta.is_included_column = 0
ORDER BY
icol_meta.key_ordinal
FOR XML PATH(‚')
) AS key_col
,(
SELECT
col.name AS [text()]
,',' AS [text()]
FROM
sys.index_columns icol_meta WITH (NOLOCK)
JOIN sys.columns col WITH (NOLOCK) ON
icol_meta.object_id = col.object_id AND
icol_meta.column_id = col.column_id
WHERE
icol_meta.object_id = t.object_id AND
icol_meta.index_id = dupIdx.index_id AND
icol_meta.is_included_column = 1
ORDER BY
col.name
FOR XML PATH(‚')
) AS included_col

) i2_col
WHERE
i1.is_disabled = 0 AND
i1.is_hypothetical = 0 AND
i1.type in (1,2)
ORDER BY
s.name, t.name, i1.index_id
OPTION (RECOMPILE, MAXDOP 1);

Обычно можно удалить полностью избыточные индексы (обратите внимание
на фильтры индексов и неключевые столбцы) и дополнительно поискать
кандидатов для консолидации индексов. Например, индексы IDX3 и IDX4 ,
определенные в листинге 14.8, можно консолидировать в IDX5. Обращайте
внимание на статистику использования индексов (я расскажу об этом позже
в этой главе), потому что она содержит сведения о том, какие индексы используются редко.

428  Глава 14. Анализ схемы базы данных и индексов
Листинг 14.8. Примеры консолидации индексов
CREATE
CREATE
--IDX3
CREATE

INDEX IDX3 ON T(LastName, FirstName) INCLUDE(Phone);
INDEX IDX4 ON T(LastName) INCLUDE (SSN);
и IDX4 можно консолидировать в IDX5
INDEX IDX5 ON T(LastName, FirstName) INCLUDE(Phone,SSN);

Прежде чем удалять или отключать индексы, проанализируйте схему базы
данных и запросы: убедитесь, что эти индексы не упоминаются в индексных
указаниях!

Высокие значения идентификаторов
Существует дурацкая, но в то же время опасная ситуация, с которой вы можете
столкнуться: исчерпание емкости целочисленных столбцов. Представьте таблицу
с первичным ключом INT IDENTITY PRIMARY KEY, который достигает 2 147 483 647,
то есть максимального значения для типа данных INT. Все дальнейшие операции
INSERT в таблице завершатся ошибкой, что может привести к аварии на промышленном сервере.
Что еще хуже, эту ситуацию сложно исправить. Можно изменить тип данных
с INT на BIGINT, но выполнение инструкции ALTER TABLE ALTER COLUMN для больших
таблиц может занять часы или даже дни. Более того, на время этой операции
таблица окажется под блокировкой модификации схемы (Sch-M). К сожалению,
я не раз видел, как люди теряли работу, когда происходила такая беда.
Код из листинга 14.9 ищет столбцы идентификаторов и последовательности,
у которых вскоре исчерпается мощность. Регулярно запускайте этот сценарий
в своих системах и заблаговременно устраняйте риски, если видите проблемы.

Листинг 14.9. Поиск столбцов идентификаторов с заканчивающейся емкостью
DECLARE
@Types TABLE
(
type_id INT NOT NULL PRIMARY KEY,
name SYSNAME NOT NULL,
max_val DECIMAL(38) NOT NULL
)
INSERT INTO @Types(type_id, name, max_val)
VALUES
(48,'TINYINT',255)
,(52,'SMALLINT',32767)
,(56,'INT',2147483647)
,(127,'BIGINT',9223372036854775807)
,(108,'NUMERIC',99999999999999999999999999999999999999) -- 10^38-1
,(106,'DECIMAL',99999999999999999999999999999999999999); -- 10^38-1

Анализ схемы базы данных  429
DECLARE
@percentThreshold INT = 50;
;WITH CTE
AS
(
SELECT
s.name + '.' + t.name AS [table]
,c.name AS [column]
,tp.name + IIF(tp.type_id IN (106,108), '(' +
CONVERT(VARCHAR(2),c.precision) + ')','') AS [type]
,CONVERT(DECIMAL(38),IDENT_CURRENT(t.name)) AS [identity]
,CASE
WHEN tp.type_id IN (106,108)
THEN
CASE
WHEN c.precision < 38
THEN POWER(CONVERT(DECIMAL(38),10),c.precision) – 1
ELSE tp.max_val
END
ELSE
tp.max_val
END AS [max value]
FROM
sys.tables t WITH (NOLOCK)
JOIN sys.schemas s WITH (NOLOCK) ON
t.schema_id = s.schema_id
JOIN sys.columns c WITH (NOLOCK) ON
c.object_id = t.object_id
JOIN @Types tp ON
tp.type_id = c.system_type_id
WHERE
c.is_identity = 1
)
SELECT
*
,CONVERT(DECIMAL(6,3),[identity] / [max value] * 100.)
AS [percent full]
FROM
CTE
WHERE
CONVERT(DECIMAL(6,3),[identity] / [max value] * 100.) >
@percentThreshold
ORDER BY
[percent full] DESC;
-- Sequences
;WITH CTE
AS
(
SELECT
s.name + ‚.' + seq.name AS [sequence]
,tp.name AS [type]

430  Глава 14. Анализ схемы базы данных и индексов
,CASE tp.type_id
WHEN 48 THEN
CONVERT(DECIMAL(38),CONVERT(TINYINT,seq.current_value))
WHEN 52 THEN
CONVERT(DECIMAL(38),CONVERT(SMALLINT,seq.current_value))
WHEN 56 THEN
CONVERT(DECIMAL(38),CONVERT(INT,seq.current_value))
WHEN 127 THEN
CONVERT(DECIMAL(38),CONVERT(BIGINT,seq.current_value))
WHEN 106 THEN
CONVERT(DECIMAL(38),seq.current_value)
WHEN 108 THEN
CONVERT(DECIMAL(38),seq.current_value)
END as [current]
,CASE tp.type_id
WHEN 48 THEN
CONVERT(DECIMAL(38),CONVERT(TINYINT,seq.maximum_value))
WHEN 52 THEN
CONVERT(DECIMAL(38),CONVERT(SMALLINT,seq.maximum_value))
WHEN 56 THEN
CONVERT(DECIMAL(38),CONVERT(INT,seq.maximum_value))
WHEN 127 THEN
CONVERT(DECIMAL(38),CONVERT(BIGINT,seq.maximum_value))
WHEN 106 THEN
CONVERT(DECIMAL(38),seq.maximum_value)
WHEN 108 THEN
CONVERT(DECIMAL(38),seq.maximum_value)
END as [max value]
FROM
sys.sequences seq WITH (NOLOCK)
JOIN sys.schemas s WITH (NOLOCK) ON
seq.schema_id = s.schema_id
JOIN @Types tp ON
tp.type_id = seq.system_type_id

)
SELECT
*
,CONVERT(DECIMAL(6,3), [current] / [max value] * 100.) as [percent full]
FROM
CTE
WHERE
CONVERT(DECIMAL(6,3), [current] / [max value] * 100.) >
@percentThreshold
ORDER BY
[percent full] DESC;

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

Анализ индекса  431
Мы рассмотрели лишь несколько примеров того, как с помощью представлений
каталога можно выявлять очаги неэффективности в схемах баз данных. Исследуйте другие представления и не забывайте проявлять творческий подход!
Теперь рассмотрим еще один важнейший источник информации: статистику
использования индекса и операций.

Анализ индекса
Всем известно, что индексы помогают повысить производительность запросов.
Но все имеет свою цену: индексы увеличивают объем данных в базе, потребляют
дополнительную память и добавляют накладные расходы при модификации данных. От большого количества неэффективных и/или неиспользуемых индексов
производительность системы может значительно снизиться.
Оптимизация запросов — это важная часть настройки производительности,
в ходе которой в базе данных часто создаются новые индексы. Однако, когда
вы работаете не в аварийной ситуации, я всегда рекомендую уделять время
анализу и удалению неэффективных индексов перед тем, как создавать новые.
Это устранит их накладные расходы и упростит процесс оптимизации.
В SQL Server есть два динамических административных представления для
анализа использования индексов. Первое, sys.dm_db_index_usage_stats1, показывает количество запросов, которые использовали индекс для операций
поиска, просмотра, обновления и уточнения. Второе представление, sys.dm_db_
index_operational_stats2, дает информацию о доступе на уровне строк и операционные метрики, включая показатели ввода/вывода, сведения о блокировках
и некоторые другие данные.
Способы сбора этих метрик существенно различаются. Представление sys.dm_
db_index_usage_stats обеспечивает информацию на уровне запросов, а sys.dm_
db_index_operational_stats работает на уровне строк. Например, если запрос
вставляет в таблицу 1000 строк, то в первом представлении число user_updates
увеличится на 1, а во втором представлении столбец leaf_insert_count вырастет на 1000.
SQL Server не сохраняет статистику использования индекса. Метрики в этих
представлениях очищаются при перезапуске SQL Server или отключении базы
данных. Более того, в некоторых старых сборках SQL Server (SQL Server 2012 до
SP2 CU12 и SP3 CU3; SQL Server 2014 до SP2) метрики очищаются, когда выполняется операция перестроения индекса. Это нужно учитывать в ходе анализа:
убеждайтесь, что статистика репрезентативна и не пропускает важные, но редко
1

https://oreil.ly/IcA15

2

https://oreil.ly/AWvEX

432  Глава 14. Анализ схемы базы данных и индексов
выполняемые запросы, такие как, например, критический процесс в банковской
системе, который запускается по расписанию один раз в месяц.
Также следует изучить использование индекса на доступных для чтения вторичных репликах в группах доступности. Нередко бывает, что отдельные индексы
поддерживают запросы на вторичных узлах, а на первичном узле эти индексы
могут отображаться как неиспользуемые.
Давайте рассмотрим оба представления подробнее и разберемся, как интерпретировать полученную из них информацию.

Представление sys.dm_db_index_usage_stats
Представление sys.dm_db_index_usage_stats — один из основных инструментов
анализа использования индекса. Оно содержит информацию о том, как часто
используется индекс или, если быть точнее, сколько раз запросы задействовали
индекс и он появлялся в планах выполнения.
Данные группируются по методам доступа, и вы увидите отдельные метрики
для операций поиска (seek), просмотра (scan) и уточнения (lookup). Наконец,
в представлении также показано, как часто индекс обновлялся, что помогает
оценить связанные с ним затраты на обновление.
В листинге 14.10 показан код, использующий это представление. Я переименовал
некоторые выходные столбцы, чтобы сделать их компактнее. В этой главе я буду
попеременно использовать и имена столбцов представления, и мои псевдонимы.

Листинг 14.10. Использование представления sys.dm_db_index_usage_stats
SELECT
t.object_id
,i.index_id
,s.name + '.' + t.name AS [Table]
,i.name AS [Index]
,i.type_desc
,i.has_filter AS [Filtered]
,i.is_unique AS [Unique]
,p.rows AS [Rows]
,ius.user_seeks AS [Seeks]
,ius.user_scans AS [Scans]
,ius.user_lookups AS [Lookups]
,ius.user_seeks + ius.user_scans + ius.user_lookups AS [Reads]
,ius.user_updates AS [Updates]
,ius.last_user_seek AS [Last Seek]
,ius.last_user_scan AS [Last Scan]
,ius.last_user_lookup AS [Last Lookup]
,ius.last_user_update AS [Last Update]
FROM
sys.tables t WITH (NOLOCK)
JOIN sys.indexes i WITH (NOLOCK) ON

Анализ индекса  433
t.object_id = i.object_id
JOIN sys.schemas s WITH (NOLOCK) ON
t.schema_id = s.schema_id
CROSS APPLY
(
SELECT SUM(p.rows) AS [rows]
FROM sys.partitions p WITH (NOLOCK)
WHERE
i.object_id = p.object_id AND
i.index_id = p.index_id
) p
LEFT OUTER JOIN sys.dm_db_index_usage_stats ius ON
ius.database_id = DB_ID() AND
ius.object_id = i.object_id AND
ius.index_id = i.index_id
WHERE
i.is_disabled = 0 AND
i.is_hypothetical = 0 AND
t.is_memory_optimized = 0 AND
t.is_ms_shipped = 0
ORDER BY
s.name, t.name, i.index_id
OPTION (RECOMPILE, MAXDOP 1);

На рис. 14.2 показан вывод кода.

Рис. 14.2. Вывод представления sys.dm_db_index_usage_stats
Рассмотрим столбцы, которые выводит код.
database_id, object_id и index_id

В столбцах database_id, object_id и index_id указаны база данных, таблица
и индекс соответственно. Их можно использовать для фильтрации вывода,
а также объединять с представлениями sys.databases, sys.tables и sys.indexes.

434  Глава 14. Анализ схемы базы данных и индексов
user_seek, user_scan и user_lookup

Столбцы user_seek, user_scan и user_lookup показывают, как часто запросы
использовали индекс в операторах Index Seek, Index Scan, Key Lookup и RID
Lookup. Эффективные индексы на основе B-дерева в системах OLTP должны
в первую очередь использовать поиск по индексу.
Однако не забывайте о том, что я говорил в главе 5: операторы поиска по
индексу не всегда эффективны и могут просматривать очень большой диапазон строк в зависимости от предиката поиска и данных.
user_update

В столбце user_update показано, сколько раз индекс был изменен операциями вставки, обновления, удаления или слияния. Эта информация позволяет
оценить накладные расходы на обслуживание индекса во время модификации
данных.
Важно помнить, что user_update показывает, сколько раз выполнялась
операция, а не сколько строк она изменила. Например, один вызов DELETE
всегда увеличивает значение user_update на 1 независимо от того, сколько
строк было удалено.
last_user_seek, last_user_scan, last_user_lookup и last_user_update

Столбцы last_user_* сообщают время последней соответствующей операции
в индексе. Эти данные полезны, если SQL Server давно не перезапускался
и метрики использования собирались в течение длительного времени.
system_seek, system_scan, system_lookup, system_update, last_system_seek,
last_system_scan, last_system_lookup и last_system_update

Столбцы system_* (не показаны в сценарии и на рис. 14.2) содержат статистику использования индекса системными процессами. Сюда относятся
обновление статистики, обслуживание индекса и некоторые другие процессы.
В большинстве случаев об этих показателях не нужно беспокоиться.
Рассмотрим несколько характерных закономерностей в выходных данных представления и разберемся, какие выводы из них можно сделать.

Неиспользуемые индексы
Я проводил много проверок работоспособности SQL Server, и мне редко встречались системы, в которых не было бы неиспользуемых и ненужных индексов.
Эти индексы легко обнаружить благодаря тому, что представление sys.dm_db_
index_usage_stats не показывает по ним никакой активности чтения (user_
seek, user_scan и user_lookup).

Анализ индекса  435
На рис. 14.3 показан пример такого вывода. (Я удалил некоторые столбцы из
результатов листинга 14.10, чтобы вывод стал лаконичнее.) Как видите, на индексах с index_id 43 и 47 не выполняется никаких операций чтения.

Рис. 14.3. Неиспользуемые индексы
Как правило, с такими индексами проще всего разбираться. Их можно отключить и/или удалить без особого риска, однако перед этим не забудьте проверить
несколько вещей.
Во-первых, как я уже упоминал, убедитесь, что индексы не используются ни
на одном узле группы доступности. Очень часто индексы, не используемые на
первичном узле, активно задействованы для создания отчетов на доступных
для чтения вторичных узлах.
Во-вторых, убедитесь, что статистика использования репрезентативна и что
вы не упускаете из виду редкие процессы, которым нужен этот индекс. Тут все
не так просто. Необходимо проанализировать преимущества и недостатки сохранения индекса. В некоторых случаях можно попробовать удалить индекс
и таким образом устранить его накладные расходы, но тогда редкие процессы
будут выполняться неэффективно. Можно также отключить индекс и включать
его по расписанию, когда необходимо.
Наконец, уникальные индексы заслуживают особого внимания. Они могут
поддерживать уникальные ограничения, и если их удалить, качество данных
может испортиться.
Тем не менее во многих случаях неиспользуемые индексы — это «легкая добыча»,
и их удаление моментально пойдет системе на пользу.

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

436  Глава 14. Анализ схемы базы данных и индексов
пользу, но сперва стоит проанализировать, как они применяются, и оценить
негативные последствия того, что редкие запросы будут выполняться без них.
Я не могу указать формальный порог, после которого следует анализировать индексы. Однако есть несколько критериев, которыми я пользуюсь. Прежде всего
я проверяю индексы с небольшим количеством чтений, особенно с небольшим
количеством поисков (например, индекс с index_id=40 на рис. 14.3). Затем я смотрю на индексы, у которых количество обновлений значительно (на порядок или
несколько порядков) превышает количество чтений (index_id=53 на рис. 14.3).
С помощью кода из листинга 14.11 можно просмотреть некоторые запросы,
использующие индекс. Этот код анализирует данные кэша планов и может пропустить запросы, план выполнения которых не кэширован. Если у вас включено
хранилище запросов, код можно настроить для работы с представлением sys.
query_store_plan и другими представлениями каталога1 хранилища запросов.
Сразу оговорюсь, что этот код еще и медленный. Вы можете попробовать сбросить содержимое кэша планов в таблицу служебной базы данных и провести
анализ в непромышленной среде.

Листинг 14.11. Обнаружение запросов, использующих индекс
DECLARE
@IndexName SYSNAME = QUOTENAME('');
;WITH XMLNAMESPACES
(DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan')
,CachedData
AS
(
SELECT DISTINCT
obj.value('@Database','SYSNAME') AS [Database]
,obj.value('@Schema','SYSNAME') + '.' + obj.value('@Table','SYSNAME')
AS [Table]
,obj.value('@Index','SYSNAME') AS [Index]
,obj.value('@IndexKind','VARCHAR(64)') AS [Type]
,stmt.value('@StatementText', 'NVARCHAR(MAX)') AS [Statement]
,CONVERT(NVARCHAR(MAX),qp.query_plan) AS query_plan
,cp.plan_handle
FROM
sys.dm_exec_cached_plans cp WITH (NOLOCK)
CROSS APPLY sys.dm_exec_query_plan(plan_handle) qp
CROSS APPLY query_plan.nodes
('/ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple')
batch(stmt)
CROSS APPLY stmt.nodes
('.//IndexScan/Object[@Index=sql:variable("@IndexName")]') idx(obj)
)
1

https://oreil.ly/ub7Ej

Анализ индекса  437
SELECT
cd.[Database]
,cd.[Table]
,cd.[Index]
,cd.[Type]
,cd.[Statement]
,CONVERT(XML,cd.query_plan) AS query_plan
,qs.execution_count
,(qs.total_logical_reads + qs.total_logical_writes) / qs.execution_count
AS [Avg IO]
,qs.total_logical_reads
,qs.total_logical_writes
,qs.total_worker_time
,qs.total_worker_time / qs.execution_count / 1000 AS [Avg Worker Time (ms)]
,qs.total_rows
,qs.creation_time
,qs.last_execution_time
FROM
CachedData cd
OUTER APPLY
(
SELECT
SUM(qs.execution_count) AS execution_count
,SUM(qs.total_logical_reads) AS total_logical_reads
,SUM(qs.total_logical_writes) AS total_logical_writes
,SUM(qs.total_worker_time) AS total_worker_time
,SUM(qs.total_rows) AS total_rows
,MIN(qs.creation_time) AS creation_time
,MAX(qs.last_execution_time) AS last_execution_time
FROM sys.dm_exec_query_stats qs WITH (NOLOCK)
WHERE qs.plan_handle = cd.plan_handle
) qs
OPTION (RECOMPILE, MAXDOP 1);

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

Неэффективное чтение
Еще один объект анализа в средах OLTP — индексы с большим количеством
просмотров, особенно когда оно значительно выше, чем количество операций
поиска. На рис. 14.4 показан такой пример.
Можно использовать подход, аналогичный тому, что рассматривался в предыдущем разделе, и найти запросы, которые используют эти индексы. Я обычно
начинаю с индексов, в которых операций поиска очень мало или вообще нет.
Запросы, которые просматривают эти индексы, обычно нужно оптимизировать,
отчего неэффективное использование индексов может прекратиться.

438  Глава 14. Анализ схемы базы данных и индексов

Рис. 14.4. Индексы с неэффективным чтением
Также обратите внимание, является ли индекс отфильтрованным. Просмотр
отфильтрованных индексов может быть совершенно нормальным явлением,
особенно если индекс небольшой.

Неэффективные кластеризованные индексы и кучи
В таблицах с кластеризованными индексами (index_id=1) высокое значение
user_lookup указывает на чрезмерное количество операций поиска по ключу
(Key Lookup). Обычно это говорит о том, что либо кластеризованные индексы
неэффективны, либо есть некластеризованные индексы, которые не охватывают
часто выполняемые запросы.
На рис. 14.5 показан пример. С кластеризованным индексом связано много
уточняющих операций (lookup), но не связано ни одной операции поиска (seek).
Эта комбинация очень распространена, когда у таблицы есть синтетический
CLUSTERED PRIMARY KEY, определенный в столбце IDENTITY, а запросы используют
разные столбцы и индексы для доступа к данным.

Рис. 14.5. Неэффективный кластеризованный индекс
Некоторые из подобных проблем очень легко анализировать и устранять. Например, в ситуации на рис. 14.5 индекс с index_id=2 явно не охватывает часто
выполняемые запросы и порождает операции поиска по ключу. Можно попробовать сделать его кластеризованным индексом, особенно если он не очень широкий. Кроме того, можно включить в индекс дополнительные столбцы, чтобы
он охватывал нужные запросы.
К сожалению, не все случаи просты и очевидны. Например, если вы посмотрите
на статистику, показанную на рис. 14.4, то увидите, что кластеризованный индекс

Анализ индекса  439
используется для поиска как по индексу, так и по ключу. При этом неочевидно,
какие некластеризованные индексы могут отвечать за поиск по ключу. В этом
случае, вероятно, будет лучше сохранить существующий кластеризованный
индекс и проанализировать, к каким некластеризованным индексам обращаются
запросы — чтобы, возможно, сделать эти индексы покрывающими.
Наконец, большое количество операций поиска по ключу в таблицах-кучах
(index_id=0) указывает на чрезмерно интенсивный поиск по RID (RID Lookup).
Самый распространенный способ решить эту проблему — превратить один из
часто используемых некластеризованных индексов в кластеризованный.

Итоги
Представление sys.dm_db_index_usage_stats — отличный инструмент для обнаружения и удаления неэффективных индексов. Однако, как я уже советовал
много раз, убедитесь, что вы работаете с данными, которые собраны со всех
серверов в группе доступности и охватывают репрезентативный период времени.
Также очень полезно включить в анализ данные представления sys.dm_db_
operational_index_stats. Рассмотрим его подробнее.

Представление sys.dm_db_index_operational_stats
Представление sys.dm_db_index_operational_stats содержит низкоуровневую
статистику методов доступа к индексу, блокировок, ввода/вывода и некоторых
других областей. Эти данные очень помогают устранять неполадки с производительностью индекса и выявлять узкие места, связанные с обычными и кратковременными блокировками.
В листинге 14.12 показан код, в котором используется это представление. Я привожу его просто для демонстрации, потому что на самом деле лучше объединять
эти данные с sys.dm_db_index_usage_stats и другими представлениями, о которых я вскоре расскажу.

Листинг 14.12. Представление sys.dm_db_index_operational_stats
SELECT
t.object_id
,i.index_id
,s.name + '.' + t.name AS [Table]
,i.name AS [Index]
,i.type_desc
,i.has_filter AS [Filtered]
,i.is_unique AS [Unique]
,p.rows AS [Rows]
,ous.*
FROM
sys.tables t WITH (NOLOCK)

440  Глава 14. Анализ схемы базы данных и индексов
JOIN sys.indexes i WITH (NOLOCK) ON
t.object_id = i.object_id
JOIN sys.schemas s WITH (NOLOCK) ON
t.schema_id = s.schema_id
CROSS APPLY
(
SELECT SUM(p.rows) AS [rows]
FROM sys.partitions p WITH (NOLOCK)
WHERE
i.object_id = p.object_id AND
i.index_id = p.index_id
) p
OUTER APPLY sys.dm_db_index_operational_stats
(DB_ID(),i.object_id,i.index_id,NULL) ous

WHERE
i.is_disabled = 0 AND
i.is_hypothetical = 0 AND
t.is_memory_optimized = 0 AND
t.is_ms_shipped = 0
ORDER BY
s.name, t.name, i.index_id
OPTION (RECOMPILE, MAXDOP 1);

На рис. 14.6 показан вывод кода. Имена столбцов позволяют понять, какую
информацию дает это представление.
Мы не будем обсуждать все эти столбцы. Их подробное описание можно найти
в документации Microsoft1.
Однако мы рассмотрим несколько категорий этих столбцов и обсудим, как их
использовать.

Статистика модификации данных
Столбцы leaf_insert_count, leaf_update_count, leaf_delete_count, leaf_
ghost_count и leaf_allocation_count содержат информацию об изменениях
данных в индексе. В отличие от представления sys.dm_db_index_usage_stats,
где подсчитывается количество операций, sys.dm_db_index_operational_stats
выводит количество затронутых строк. Можно сопоставить информацию из
обоих представлений, чтобы лучше оценить накладные расходы на обслуживание индекса.
Кроме того, представление sys.dm_db_index_operational_stats содержит
в столбцах nonleaf_* вышеперечисленные метрики для корневого и промежуточных уровней индексов на основе B-дерева. Эти данные можно использовать,
чтобы устранять неполадки кратковременной блокировки ACCESS_METHODS_HOBT_
VIRTUAL_ROOT, вызванные разбиением корневых страниц и состязанием.
1

https://oreil.ly/Ze2pc

Анализ индекса  441

Рис. 14.6. Вывод представления sys.dm_db_index_operational_stats

Статистика доступа к данным
Столбцы singleton_lookup и range_scan_count содержат данные метода доступа. В первом столбце подсчитываются операции поиска и уточнения, которые
возвращают отдельные строки. Во втором столбце подсчитываются операции
поиска по индексу, которые просматривают диапазон строк вместе с просмотрами индекса. Эти данные можно использовать, чтобы оценить эффективность
поиска по индексу на основе значения singleton_lookup. Однако невозможно
понять, насколько велики диапазоны просмотра, опираясь только на значение
range_scan_count.
Столбец forwarded_fetch_count содержит количество чтений указателей переадресации в таблицах-кучах. Кучи с высоким значением этого столбца неэффективны, и их нужно перестраивать. Этот столбец можно использовать вместе со
столбцом forwarded_record_count представления sys.dm_db_index_physical_
stats, которое мы рассматривали ранее в этой главе.

442  Глава 14. Анализ схемы базы данных и индексов
В столбцах lob_fetch_in_pages, lob_fetch_in_bytes, row_overflow_fetch_in_
pages и row_overflow_fetch_in_bytes содержится статистика доступа к столбцам
вне строки. Запросы к этим таблицам могут выбирать ненужные столбцы — возможно, с помощью антишаблона SELECT *.

Информация о блокировках
Столбцы row_lock_count, row_lock_wait_count, row_lock_wait_in_ms, page_lock_
count, page_lock_wait_count и page_lock_wait_in_ms содержат статистику блокировок на уровне строк и страниц. С помощью этой информации можно обнаруживать
индексы и таблицы, которые больше всего страдают от проблем с блокировками.
Столбцы index_lock_promotion_attempt_count и index_lock_promotion_count
содержат количество попыток укрупнения блокировок и количество их успешных укрупнений на индексе. Последний столбец полезен, когда наблюдается
высокий процент ожиданий интентных блокировок (LCK_M_I*), которые часто
вызываются укрупнением блокировок.
Обычно я не использую представление sys.dm_db_index_operational_stats
в качестве основного инструмента для устранения неполадок с блокировкой
и блокированием. Однако оно полезно для перекрестной проверки данных, собранных из других источников, о которых шла речь в главе 8.

Информация о кратковременных блокировках
Столбцы page_latch_wait_count, page_latch_wait_in_ms, tree_page_latch_wait_
count и tree_page_latch_wait_in_ms содержат статистику кратковременных
блокировок страниц для индекса. Первые два столбца отображают данные для
индексных страниц листового уровня, последние два — для промежуточных
и корневых страниц.
Эти метрики чрезвычайно полезны, когда наблюдается высокий процент ожиданий PAGELATCH, сгенерированных в базах данных пользователей. Индексы
с наибольшим количеством ожиданий кратковременной блокировки страницы,
вероятно, укажут на «горячие точки» и узкие места.

Информация о вводе/выводе
Подобно предыдущим категориям, столбцы page_io_latch_wait_count, page_io_
latch_wait_in_ms, tree_page_io_latch_wait_count и tree_page_io_latch_wait_in_
ms указывают индексы, на которые приходится больше всего ожиданий PAGEIOLATCH.
Как и при устранении неполадок с блокировками, не стоит использовать представление sys.dm_db_index_operational_stats в качестве основного источника
данных для устранения неполадок производительности ввода/вывода. Тем не
менее оно помогает анализировать индексы с наибольшим количеством ожида-

Целостное представление: sp_Index_Analysis  443
ний PAGEIOLATCH. Их можно удалить, если они не используются, а можно сжать,
чтобы немедленно облегчить нагрузку на ввод/вывод.

Итоги
Представление sys.dm_db_index_operational_stats дает массу полезной информации для устранения неполадок. Возможность изучать низкоуровневую
статистику по каждому индексу позволяет взглянуть на систему под новым
углом, чтобы проверить свои догадки и подтвердить предполагаемые первопричины проблем. Однако не используйте это представление в качестве основного
источника выводов.
Представьте, что вы устраняете неполадки производительности дисковой
подсистемы. Вы можете обнаружить и удалить некластеризованные индексы
с самыми высокими значениями page_io_latch_wait_in_ms, но тогда неоптимизированные запросы просто начнут сканировать другие индексы. Более того,
это может привести к еще более интенсивному вводу/выводу, если SQL Server
начнет просматривать большие кластеризованные индексы.
Правильнее было бы убедиться, что именно неоптимизированные запросы создают узкие места ввода/вывода, а затем обнаружить и оптимизировать запросы,
которые особенно активно эксплуатируют ввод/вывод. При оптимизации запросов данные sys.dm_db_index_operational_stats могут даже не понадобиться, но
в некоторых случаях они помогают выявить операторы с интенсивным вводом/
выводом в планах выполнения.
Есть одно исключение: кратковременные блокировки страниц. Представление
sys.dm_db_index_operational_stats может быть основным инструментом для
выявления горячих точек в индексах. Очевидно, понадобится также проанализировать схему и рабочую нагрузку, чтобы подтвердить предположение. В любом
случае это представление обычно указывает верное направление и ускоряет
устранение неполадок.
Как я уже неоднократно говорил, следует смотреть на систему целостно и использовать все доступные вам инструменты. Представления использования
индексов и операционной статистики — отличные средства, которые обязательно
должны быть в вашем арсенале.

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

444  Глава 14. Анализ схемы базы данных и индексов

Рис. 14.7. Вывод sp_Index_Analysis

Резюме  445
Я для себя решил эту проблему, написав хранимую процедуру sp_Index_Analysis,
которая объединяет информацию из различных представлений и возвращает ее
в виде одного вывода. Ее код можно загрузить из сопутствующих материалов
этой книги или с моего сайта1.
Хранимая процедура содержит большой объем информации, в том числе:
определение индекса и его метаданные;
размер индекса на диске и в буферном пуле;
статистику использования индекса;
операционную статистику индекса;
статистическую информацию.
Пример вывода процедуры показан на рис. 14.7.
Моя хранимая процедура собирает информацию из одной или нескольких баз
данных и позволяет сохранить выходные данные в таблице для дальнейшего
анализа. Я обычно свожу в одну таблицу данные, собранные со всех узлов группы
доступности, прежде чем начинать их исследовать.
Естественно, вы не обязаны пользоваться этой хранимой процедурой. Однако
она ускорит устранение неполадок и упростит вашу работу.

Резюме
Представления каталога и динамические административные представления SQL
Server — это богатейшие источники информации, которые помогают выявить
очаги неэффективности в конструкции базы данных и стратегии индексирования. Имеет смысл анализировать схему базы данных и использование индекса
в рамках проверки работоспособности системы.
Чтобы обнаружить дефекты конструкции базы данных, можно использовать
основные системные каталоги, такие как sys.tables, sys.indexes, sys.index_
columns, sys.foreign_keys идр. В числе возможных проблем могут быть неэффективные таблицы-кучи и кластеризованные индексы, неиндексированные
ограничения внешних ключей и избыточные индексы.
Всегда проверяйте, не приближаются ли столбцы IDENTITY к максимальной
емкости типа данных. Если эта емкость исчерпается, это может привести к длительному простою.

1

https://aboutsqlserver.com/

446  Глава 14. Анализ схемы базы данных и индексов
Представление sys.dm_db_index_usage_stats позволяет анализировать и обнаруживать неэффективные индексы в базе данных. Сюда относятся, в частности,
неиспользуемые и неоптимальные индексы, индексы с высокими затратами на
обслуживание и неэффективные кластеризованные индексы. Еще одно представление — sys.dm_db_index_operational_stats — содержит низкоуровневую
статистику доступа, блокировок и ввода/вывода. Оно помогает обнаружить
«горячие точки», связанные с кратковременными блокировками страницы,
и найти индексы, которые порождают другие проблемы с производительностью.
Оба представления очищаются при перезапуске SQL Server и при отключении
базы данных. Они не отражают использование индексов на вторичных репликах
группы доступности. Во время анализа убедитесь, что вы работаете с репрезентативной информацией.
Наконец, хранимая процедура sp_Index_Analysis предоставляет целостную информацию об индексах, включая их метаданные, размер на диске и в буферном
пуле, а также метрики использования. Процедуру sp_Index_Analysis можно
загрузить из сопутствующих материалов книги.
Настало время обсудить виртуализацию, а также устранение неполадок SQL
Server в виртуализированной среде.

Чек-лист устранения неполадок
Выявить потенциальные дефекты схемы базы данных с помощью представлений каталога.
Просмотреть текущие значения и оставшуюся емкость столбцов IDENTITY.
Проанализировать использование индексов с помощью представлений sys.
dm_db_index_usage_stats и sys.dm_db_index_operational_stats и решить
возможные проблемы.

ГЛАВА 15

SQL Server
в виртуализированных средах

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

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

448  Глава 15. SQL Server в виртуализированных средах
Одно из преимуществ виртуализации — в том, что она снижает затраты на
инфраструктуру. Можно развернуть несколько виртуальных машин на одном
физическом оборудовании и использовать его гораздо экономичнее.
Кроме того, виртуализация значительно упрощает обслуживание системы.
Можно увеличивать или уменьшать мощность виртуальных машин с помощью
простых изменений конфигурации. Можно без особых усилий модернизировать
оборудование, переместив виртуальную машину на новый, более мощный хост.
Можно без простоев перенести данные в более быстрое хранилище, просто
скопировав виртуальные диски.
Наконец, виртуализация может обеспечить еще один уровень высокой доступности (HA) за счет перезапуска или даже перемещения виртуальных машин
на другие хосты в случае аппаратного сбоя. При разработке стратегии высокой
доступности я не рекомендую полагаться исключительно на виртуализацию,
но она может работать как дополнительный уровень защиты в сочетании с собственными технологиями высокой доступности SQL Server.
К сожалению, эти преимущества связаны со снижением производительности.
Величина накладных расходов бывает разной: они невелики в небольших системах, но растут вместе с размером виртуальных машин. Например, в крупных
виртуальных машинах с десятками ядер и терабайтами памяти накладные расходы могут достигать 10 % или даже больше.
Виртуализация также влияет на стоимость лицензирования. В SQL Server
Enterprise Edition виртуализация сэкономит вам немало денег, если вы лицензируете SQL Server на уровне хоста и выделяете избыточные ресурсы (о которых я скоро расскажу). В то же время, если вы используете лицензирование
по количеству ядер и/или хотите избежать избыточного выделения ресурсов,
то накладные расходы на производительность виртуализации заставят вас
добавить больше ЦП виртуальной машине, для чего понадобится покупать дополнительные лицензии.
В конце концов, вам нужно исходить из того, на какую стоимость лицензии
вы ориентируетесь, и решить, перевешивают ли преимущества виртуализации
связанные с ней потери производительности. Тут все зависит от конкретной
ситуации, и для правильного решения вам необходимо опираться на свой опыт
работы с базами данных.
На мой взгляд, виртуализация — это обычно правильное решение, за исключением редких особых случаев и критических систем, где нужно получить
максимальную отдачу от оборудования.
Есть одна задача, для которой я считаю виртуализацию особенно полезной:
консолидация баз данных. Гораздо проще поддерживать несколько виртуальных машин SQL Server, консолидированных на общем хосте, чем несколько баз
данных, работающих на одном экземпляре SQL Server.

Настройка SQL Server в виртуализированных средах  449
В этом сценарии виртуализация изолирует системы друг от друга. Можно использовать разные версии SQL Server и разные ОС, применять отдельные графики обслуживания, поддерживать требования безопасности и соответствия,
специфичные для той или иной системы, а также уменьшить влияние, которое
оказывают на производительность функции уровня экземпляра, такие как прозрачное шифрование данных (которое также шифрует базу данных tempdb) или
аудит.
Также учитывайте вашу модель лицензирования SQL Server. Standard Edition
требует лицензировать логические ядра в виртуальных машинах, что может
оказаться дороже, чем один физический экземпляр SQL Server. Вместе с тем
Enterprise Edition позволяет лицензировать физические ядра на хосте независимо
от того, сколько логических ядер выделено для виртуальных машин баз данных.
Давайте обсудим оснащение виртуальных машин и устранение соответствующих
неполадок, начиная с настройки виртуальных машин SQL Server.

Настройка SQL Server
в виртуализированных средах
Накладные расходы на виртуализацию довольно малы, но только если виртуальные машины правильно оснащены и настроены. Из-за неграмотно реализованной виртуализации производительность серверов баз данных может
серьезно снизиться.
Налаживая виртуализацию, необходимо обратить внимание на несколько
аспектов, среди которых — планирование мощности, конфигурация ЦП, память,
хранилище и сеть. Рассмотрим их по порядку.

Планирование мощности
Одна из основных целей виртуализации — снизить затраты на оборудование.
Специалисты по инфраструктуре и администраторы виртуализации стараются
размещать на хостах как можно больше виртуальных машин, используя физическое оборудование по максимуму, но эта стратегия не всегда оптимальна
для развертывания баз данных. Залог успешной виртуализации SQL Server —
правильное планирование мощности на уровне как хоста, так и виртуальной
машины.
Все гостевые виртуальные машины совместно используют ресурсы хоста
и конкурируют за них. Можно выделять и резервировать ресурсы (главным
образом ЦП и ОЗУ) для отдельных виртуальных машин, но администраторы
виртуализации предпочитают этого не делать и часто перевыделяют (overcommit)

450  Глава 15. SQL Server в виртуализированных средах
ресурсы. Например, на хосте с 32 физическими ядрами и 512 Гбайт ОЗУ может
работать два десятка виртуальных машин, на которые в совокупности выделено
вдвое больше логических ядер и ОЗУ.
Перевыделение ресурсов снижает потребность в физическом оборудовании
и экономит деньги. Это может быть приемлемо для многих некритических баз
данных, работающих с небольшой нагрузкой, но в то же время перевыделение
может серьезно снизить производительность критически важных баз данных,
потому что виртуальные машины конкурируют за ресурсы. Могут возникнуть
ненужные задержки, пока виртуальные машины ожидают доступности ЦП,
а это приводит к нехватке памяти в виртуальных машинах и к другим узким
местам. Для критически важных виртуальных машин лучше резервировать
ресурсы.
Однако у резервирования ресурсов тоже есть негативные последствия. Например, в некоторых ситуациях оно ограничивает возможность перемещать
виртуальную машину на другой хост, не выключая ее. Обсудите это со своими
специалистами по инфраструктуре: возможно, они предложат другие варианты
того, как расставить приоритеты рабочей нагрузки и использовать ресурсы на
критически важных виртуальных машинах.
Беседуя со специалистами по инфраструктуре, будьте осторожны с терминоло­
гией. Например, термин перевыделение (overcommitment) означает разные вещи
для специалистов по базам данных и по инфраструктуре. Когда я впервые обсуждал эту тему с администраторами виртуализации, я имел в виду, что количество
выделенных логических ресурсов превышает доступные физические ресурсы
хоста. Но оказалось, что администратор виртуализации не считал ресурсы «перевыделенными» до тех пор, пока они не преодолели соотношение 3:1.
Базы данных нужно анализировать с точки зрения их рабочей нагрузки, соглашения об уровне обслуживания (SLA) и критичности для бизнеса. Обычно
я выделяю три уровня:
Уровень 1
Критически важные базы данных, работающие под большой нагрузкой.
Виртуальные машины для этих баз не должны работать на хостах с перевыделением, или у них должно быть включено резервирование ресурсов.
На этом уровне также нужно обратить внимание на конфигурацию процессора, о которой я вскоре расскажу.
Уровень 2
Промышленные базы данных, работающие со средней нагрузкой. Они часто
нормально функционируют при некотором уровне перевыделения на хосте.
Они также могут потреблять ресурсы в приоритетном порядке по сравнению
с менее важными виртуальными машинами на том же хосте.

Настройка SQL Server в виртуализированных средах  451
Уровень 3
Базы данных, которые не являются критически важными, работают при небольшой нагрузке и не имеют соглашений об уровне производительности.
На хостах с такими базами ресурсы обычно перевыделены.
Если вы используете подобную классификацию баз данных, кооперируйтесь
с администраторами виртуализации, чтобы разработать правильную топологию
виртуальных машин и их конфигурацию. Очевидно, что критически важные
базы данных уровня 1 требуют максимального внимания и самого тщательного
планирования.
В общем случае следует избегать перевыделения ресурсов для гостевых машин
в виртуальных средах. Лучше и проще выделить виртуальным машинам столько
ресурсов, сколько нужно для текущей нагрузки, а затем добавлять дополнительные ресурсы по мере роста нагрузки. Убедитесь, что у вас настроен эффективный
мониторинг, и заблаговременно определяйте, когда пора наращивать мощность.
К счастью, цена ошибок и неверных конфигураций в виртуальных средах намного ниже, чем при использовании физического оборудования. Не составляет труда добавить дополнительных ресурсов к «обделенным» виртуальным
машинам и перераспределить виртуальные машины между хостами по мере
необходимости. Имейте это в виду при планировании мощности.

Конфигурация ЦП
Я начну с контринтуитивного заявления: в общих средах добавление процессоров к виртуальным машинам может снизить их производительность, а не
улучшить ее. Во многих случаях виртуальная машина с меньшим количеством
ядер будет работать быстрее, чем с большим, — однако при условии, что у выделенных ядер хватает пропускной способности, чтобы обрабатывать нагрузку.
Все дело в том, как в гипервизорах устроено планирование. В этом отношении
алгоритмы работы Hyper-V и VMware немного различаются.
Накладные расходы на планирование и особенности реализации приводят к простому правилу: в общих средах, особенно с перевыделением, выделяйте ровно столько ЦП, сколько нужно для имеющейся нагрузки. После этого следите за производительностью и добавляйте ЦП по мере роста нагрузки. Это полезно и в VMware,
и в Hyper-V, хотя VMware более чувствительна к избыточному выделению ЦП.
Тем не менее избыточное оснащение ресурсами может не вызывать проблем для
критически важных виртуальных машин, если они используют резервирование
ресурсов или запускаются на хостах без перевыделения. Нужно только проверить, что топология со временем не меняется. Мне встречались администраторы
виртуализации, которые добавляли новые виртуальные машины к критическим
хостам, не сообщая об этом специалистам по базам данных.

452  Глава 15. SQL Server в виртуализированных средах

ПАРА СЛОВ О ПЛАНИРОВАНИИ
Внедрить эффективное планирование для многопроцессорных виртуальных машин — это
всегда трудная задача. Нужно синхронизировать потоки, работающие на разных ЦП, и невозможно выполнять рабочую нагрузку виртуальной машины только на подмножестве ЦП.
В первой версии гипервизора VMware использовалась концепция бригадного планирования
(gang scheduling). В этой модели гипервизор не позволяет виртуальной машине выполняться,
если у него не хватает физических ЦП, чтобы назначить их всем ЦП в виртуальной машине.
Например, машина, на которую выделено четыре ЦП, будет оставаться приостановленной
до тех пор, пока на хосте для нее не найдется четырех свободных процессоров.
Бригадное планирование довольно хорошо работало с небольшими виртуальными машинами, но плохо масштабировалось на крупные многопроцессорные виртуальные системы,
поэтому в VMware появилась другая модель планирования. При ослабленном совместном
планировании (relaxed co-scheduling) гипервизор позволяет процессорам в крупных виртуальных машинах работать с большей степенью независимости. При этом гипервизор
отслеживает работу процессоров и лимитирует их при необходимости.
Но и эта модель планирования неидеальна. Производительность по-прежнему ограничена процессором с самой низкой частотой и пропускной способностью, и все еще бывают
случаи, когда гипервизору приходится планировать одновременную работу всех ЦП, как
при бригадном планировании.
В то же время в Hyper-V используется другая модель, которая опирается на возможности
гостевой ОС. Современные версии Windows и Linux понимают, когда они работают из-под
виртуальной машины, и ядро в гостевой ОС запрашивает у гипервизора процессорное
время. Эта модель оптимизирована так, чтобы уменьшить (но не полностью устранить)
необходимость синхронизировать планирование между процессорами. Она более эффективна в случае больших виртуальных машин и перевыделения ЦП.
В конфигурации ЦП в виртуализированных средах участвуют несколько компонентов. Вот важные компоненты хоста:
Физический ЦП (pCPU)
Это физический процессорный чип, установленный на сервере. Его иногда
называют физическим сокетом (pSocket).
Физическое ядро (pCore)
Это независимое вычислительное ядро в физическом ЦП.
Логическое ядро (lCore)
Это логическая вычислительная единица на физическом ядре, которая также
называется логическим процессором. При гиперпоточности на каждое физи-

Настройка SQL Server в виртуализированных средах  453
ческое ядро приходятся два логических ядра. Общее количество логических
процессоров на хосте можно рассчитать как (количество pCPU) * (количество
pCore на один pCPU) * (количество lCore на один pCore).
Например, у хоста с двумя процессорами Intel Xeon Gold 6346 с включенной гиперпоточностью будет 2 pCPU * 16 pCore * (2 lCore на один pCore) =
= 64 логических ядра.
А это важные компоненты виртуальной машины:
Виртуальный сокет (vSocket) и виртуальное ядро (vCore)
Виртуальный сокет — это виртуализированная микросхема ЦП, которая
может быть сконфигурирована с одним или несколькими виртуальными
ядрами. В виртуальных машинах виртуальные сокеты обрабатываются как
физические сокеты и, в свою очередь, как отдельные узлы NUMA. (Подробнее
об этом чуть позже.)
Виртуальный ЦП (vCPU)
Это виртуальный процессор, назначенный виртуальной машине. Как вы
наверняка догадались, общее количество виртуальных ЦП в виртуальной
машине можно рассчитать как (общее количество vSocket) * (количество
vCore на один vSocket).
Есть несколько вопросов, которые следует рассмотреть при настройке виртуальных машин для SQL Server. Первый вопрос — включать ли гиперпоточность.
На него не так просто ответить, когда вы имеете дело с виртуализацией.
Гиперпоточность может повысить пропускную способность хоста, но также
может снизить пропускную способность отдельных виртуальных машин. Теоретически хост учитывает гиперпоточность во время планирования, назначая
виртуальные ЦП физическим ядрам. Но на практике все может зависеть от
загруженности хоста.
Как обычно, стоит протестировать производительность с включенной гиперпоточностью и без нее. К сожалению, эффект в виртуальных средах трудно измерить, потому что нагрузка на хост быстро меняется. Гиперпоточность может
неплохо подходить для некритических рабочих нагрузок, но на виртуальных
машинах SQL Server уровня 1 безопаснее оставить ее отключенной.
Второй важный вопрос — конфигурация NUMA. Windows и SQL Server
Enterprise Edition поддерживают NUMA. Согласование конфигурации виртуальных NUMA на гостевых виртуальных машинах с конфигурацией физического
NUMA (pNuma) может повысить производительность.
Очевидно, лучше протестировать и отрегулировать конфигурацию в соответствии с вашей реальной рабочей нагрузкой. Мое эмпирическое правило для

454  Глава 15. SQL Server в виртуализированных средах
начальной конфигурации заключается в том, что с конфигурацией NUMA нужно согласовывать от 8 до 12 виртуальных ядер на каждый виртуальный сокет.
Занимаясь этим согласованием, помните, что Enterprise Edition по умолчанию
использует программный NUMA, если на узле NUMA больше восьми планировщиков.
Мой подход может измениться, если на хосте нет перевыделения, а требования
к ЦП и ОЗУ виртуальной машины укладываются в один узел NUMA. В этом
случае я могу держать виртуальную машину в пределах одного физического
узла NUMA.
Рассмотрим несколько примеров. Предположим, что у вас есть сервер с двумя
16-ядерными процессорами и 256 Гбайт ОЗУ на каждый узел NUMA. В табл. 15.1
показаны возможные конфигурации виртуальных машин в зависимости от их
требований.

Таблица 15.1. Возможные конфигурации виртуальных машин
Конфигурация
виртуальной
машины

Количество vCore и vSocket

Примечание

8 ЦП
128 Гбайт ОЗУ

1 vSocket
8 vCores на vSocket

Все укладывается в один узел pNuma

12 ЦП
192 Гбайт ОЗУ

1 vSocket, 8 vCores на vSocket
или
2 vSocket и 6 vCores на vSocket

Протестируйте и выберите оптимальную
конфигурацию

16 ЦП
256 Гбайт ОЗУ

2 vSocket, 8 vCores на vSocket
или
1 vSocket и 16 vCores на vSocket

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

8 ЦП
384 Гбайт ОЗУ

2 vSocket
4 vCores на vSocket

Требует памяти от двух узлов NUMA

20 ЦП
256 Гбайт ОЗУ

2 vSocket
10 vCores на vSocket

Требует ядер от двух узлов NUMA

Убедитесь, что все узлы NUMA в SQL Server правильно согласованы и имеют
одинаковое количество процессоров. Как упоминалось в главе 2, SQL Server
назначает соединения с узлами NUMA в циклическом режиме. Неравномерное распределение ЦП между узлами приведет к проблемам с планированием,
потому что некоторые планировщики в этой конфигурации будут выполнять

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

Листинг 15.1. Проверка распределения планировщиков в узлах NUMA
SELECT
parent_node_id AS [NUMA Node]
,COUNT(*) AS [Schedulers]
,SUM(IIF(status = N'VISIBLE ONLINE',1,0))
AS [Online Schedulers]
,SUM(IIF(status = N'VISIBLE OFFLINE',1,0))
AS [Offline Schedulers]
,SUM(current_tasks_count)
AS [Current Tasks]
,SUM(runnable_tasks_count)
AS [Runnable Tasks]
FROM
sys.dm_os_schedulers WITH (NOLOCK)
WHERE
status IN (N'VISIBLE ONLINE',N'VISIBLE OFFLINE')
GROUP BY
parent_node_id
OPTION (RECOMPILE, MAXDOP 1);

Некоторое время назад мне пришлось устранять неполадки с производительностью в виртуальной машине, которая работала на хосте с двумя 18-ядерными процессорами. Машина была снабжена 17 виртуальными сокетами по 2 виртуальных
ядра на каждый сокет. В этой конфигурации Windows увидела 3 физических
узла NUMA с 16, 16 и 2 виртуальными ЦП соответственно. SQL Server, в свою
очередь, использовал программный NUMA и разделил первые 2 узла пополам.
На рис. 15.1 показан вывод листинга 15.1 в этой конфигурации. На пятом узле
NUMA оказалось два планировщика, которые были нагружены значительно
сильнее остальных.

Рис. 15.1. Узел NUMA и планировщики в несбалансированной конфигурации
На рис. 15.2 показан график загрузки процессора на сервере. На графике видно,
что эти два ЦП были загружены до предела, что приводило к периодическим
проблемам с производительностью и скачкам ожиданий HADR_SYNC_COMMIT.

456  Глава 15. SQL Server в виртуализированных средах

Рис. 15.2. Нагрузка на ЦП в несбалансированной конфигурации
Мы переконфигурировали виртуальную машину: настроили на ней 2 виртуальных сокета по 17 виртуальных ядер на сокет и согласовали ее с конфигурацией физического оборудования. В этом режиме Windows видела два узла
NUMA, однако SQL Server разделил ЦП на два 5-ядерных и четыре 6-ядерных
процессора. Конфигурация снова получилась несовершенной, но все же более
сбалансированной, чем первоначальная.
В конце концов мы облегчили виртуальную машину до 32 виртуальных ЦП,
что дало четыре 8-ядерных программных узла NUMA, идеально согласованных
с оборудованием. Сперва мы думали сохранить 34 виртуальных ЦП и отключить
программный NUMA, но отказались от этого, потому что у виртуальной машины
хватало мощности для рабочей нагрузки.
Соотношение виртуальных сокетов и виртуальных ядер на сокет (vSocket/
vCore) особенно важно в версиях SQL Server, отличных от Enterprise, в которых
количество сокетов ограниченно. Неправильные настройки могут помешать
им использовать все виртуальные ЦП в виртуальной машине. Эту ситуацию
легко обнаружить, наблюдая за загрузкой ЦП на сервере: вы увидите, что одни
ЦП заняты, а другие бездействуют. Эту информацию также можно получить
из столбца [OfflineSchedulers] в выходных данных листинга 15.1. Кроме
того, можно изучить столбец status в представлении sys.dm_os_schedulers. На
рис. 15.3 показан вывод этого представления в Standard Edition с несколькими
отключенными планировщиками.

Настройка SQL Server в виртуализированных средах  457

Рис. 15.3. Статус планировщиков
Проблема с несбалансированными конфигурациями NUMA и планировщиками
может возникнуть и у невиртуализированных экземпляров SQL Server, если
некоторые планировщики были отключены с помощью маски соответствия или
если используется версия Enterprise Edition с лицензиями клиентского доступа,
которые ограничивают ее до 20 физических ядер. Но гораздо чаще эта проблема проявляется в виртуальных средах, позволяющих создавать конфигурации
виртуальных машин с мощностями, которых нет на физическом оборудовании.
Не забудьте проконтролировать это во время проверки работоспособности.

Память
Как и в случае с конфигурацией ЦП, виртуальным машинам (VM) можно выделить больше памяти, чем ее физически есть на хосте, однако это может привести
к серьезным проблемам с производительностью серверов баз данных.
Если вы не используете резервирование ресурсов, хост не выделяет память для
VM сразу: она выделяется только тогда, когда приложения в VM ее запрашивают. Однако, когда приложения освобождают эту память для ОС, хост об этом не
знает, поэтому виртуальные машины продолжают потреблять память. По мере
того, как объем выделенной памяти на хосте растет, хост начинает отзывать
ее у гостевых ОС с помощью драйвера накачки (balloon driver). Этот драйвер
устанавливается на гостевой виртуальной машине и запрашивает память у своей
ОС, возвращая ее хосту. Это может привести к нехватке внешней памяти в SQL
Server и, возможно, к подкачке внутри ОС.
По умолчанию SQL Server реагирует на нехватку памяти, сокращая ее потреб­
ление и освобождая память для операционной системы, но это все равно ведет

458  Глава 15. SQL Server в виртуализированных средах
к снижению производительности. Ситуация усугубляется, если у вас включена
блокировка страниц в памяти (LPIM, lock pages in memory) или неправильно
установлен минимальный объем памяти сервера. Эти настройки могут помешать
SQL Server высвобождать память, что может привести к нестабильности ОС
и аварийным переключениям.
С этим мало что можно поделать, помимо правильного планирования мощности.
Не выделяйте лишнюю память для виртуальных машин, если только вы не используете резервирование ресурсов или не можете гарантировать, что на хосте
не будет перевыделения. SQL Server лучше управляется с меньшим объемом
доступной памяти, чем с регулярной нехваткой памяти из-за накачки.
Будьте осторожны с LPIM и другими функциями, которые могут помешать SQL
Server реагировать на нехватку памяти. Не забывайте, что хост может внезапно
оказаться перевыделенным, если на него мигрируют другие виртуальные машины, что бывает при балансировке нагрузки виртуальных машин или во время
аварии, когда другие хосты в инфраструктуре выходят из строя.
Наконец, не забывайте, что конфигурация памяти может влиять на конфигурацию ЦП в виртуальной машине. Если вашей виртуальной машине требуется
память из нескольких узлов pNuma, убедитесь, что конфигурация vSocket/vCore
соответствует этой топологии.

Хранилище
У всех платформ виртуализации есть различные варианты настройки хранилища. Чаще всего виртуальные машины используют виртуальные диски (vmdk
в VMware и vhdx в Hyper-V), представленные как отдельные диски внутри
гостевых виртуальных машин. Хост размещает виртуальные диски либо непосредственно в физическом хранилище, либо, как VMware vSphere, в другом
хранилище данных, оптимизированном для виртуальных дисков (рис. 15.4).

LUN
Хранилище VMFS
VMDK

VMDK

RAID-массив физических дисков

Рис. 15.4. Стек хранилища VMware vSphere

Настройка SQL Server в виртуализированных средах  459
Можно напрямую отобразить физическое хранилище на виртуальную машину,
минуя виртуальные диски, но я редко рекомендую этот вариант. Он не дает заметного прироста производительности по сравнению с правильно настроенным
виртуальным хранилищем, зато вы теряете гибкость виртуализированного
хранилища и возможность перемещать данные между массивами хранилища.
В основном конфигурация хранилища зависит от инфраструктуры системы
и настраивается за пределами виртуальной машины SQL Server, но в системах
уровня 1 нужно разделять рабочую нагрузку ввода/вывода между различными
контроллерами SCSI и хранилищами данных. Это звучит похоже на старинную
рекомендацию распределять объекты базы данных по нескольким дискам, но
цель в другом: такой прием помогает сбалансировать запросы ввода/вывода
и уменьшает насыщение дисковых очередей во время всплесков рабочей нагрузки ввода/вывода. Одним из примеров служит процесс контрольной точки,
который может генерировать огромное количество запросов на запись и увеличивать задержку ввода/вывода (см. главу 3).
Держите все связанные хранилища данных в одной группе защиты хранилища, чтобы избежать повреждения данных в случае аварии.

Существует ограничение на количество виртуальных контроллеров SCSI, которые может поддерживать каждая виртуальная машина. Например, в VMware
можно использовать только четыре контроллера PVSCSI на каждую виртуальную машину. Обычно я помещаю диск журнала транзакций на один из
контроллеров, объединяю диски tempdb и ОС на другом, а два оставшихся
контроллера использую для файлов данных. Однако иногда я поступаю подругому в зависимости от количества баз данных на сервере, количества файлов
данных в базах данных и распределения рабочей нагрузки ввода/вывода между
файлами. Выбирая оптимальную конфигурацию, можно проанализировать
счетчики производительности диска и выборочные данные в представлении
sys.dm_io_virtual_file_stats.
Увеличьте и отрегулируйте глубину очереди в контроллерах SCSI и используйте
последние доступные версии драйверов. Убедитесь, что вы применяете оптимальный тип контроллера для конфигурации хранилища. Например, VMware
vSphere предоставляет контроллер vNVMe, оптимизированный для локальных
флеш-накопителей.
Как и в случае с процессором и памятью, у хоста и хранилища должно хватать
пропускной способности для обработки нагрузки ввода/вывода. Не рекомендуется объединять несколько виртуальных машин с интенсивным вводом/
выводом на одном хосте или размещать их в одних и тех же хранилищах данных, потому что они могут влиять друг на друга. Имейте в виду, что топология

460  Глава 15. SQL Server в виртуализированных средах
виртуальной машины может меняться со временем, и не забывайте следить за
производительностью.
Для снижения рисков можно использовать решения Quality of Service (QoS),
например Virtual Volumes (VVols) в vSphere или Storage QoS в Hyper-V. Они
могут смягчить влияние «шумных соседей» и сделать рабочую нагрузку ввода/
вывода более предсказуемой.
Большинство проблем с конфигурацией возникает, когда виртуальные машины
SQL Server оснащаются на основании общих соображений, без учета поведения SQL Server и характерных шаблонов ввода/вывода. Этих проблем легко
избежать, если вы сотрудничаете с администраторами систем виртуализации
и хранения данных.
Всегда рекомендуется по возможности проверять производительность ввода/
вывода на сервере. Утилита DISKPD1 может эмулировать стандартную рабочую
нагрузку SQL Server.

Сеть
Мы не будем тратить много времени на обсуждение конфигурации сети в виртуализированных средах, потому что в большинстве случаев она диктуется
физической инфраструктурой и конфигурацией на уровне хоста, и у вас мало
возможностей ее контролировать. Но об одном аспекте я все же хотел бы упомянуть.
В виртуальных машинах очень легко создавать виртуальные сетевые адаптеры
(vNIC ), но они не гарантируют разделения трафика на физическом уровне.
В зависимости от конфигурации трафик от разных виртуальных машин и/или
виртуальных адаптеров может направляться через один и тот же физический
сетевой адаптер на хосте. Чаще всего от этого страдает сеть проверки работоспособности в отказоустойчивом кластере Windows. Отсутствие физического
разделения трафика может вызвать проблемы со стабильностью кластера и неожиданные аварийные переключения на хостах с виртуальными машинами
с интенсивным сетевым трафиком.
Настройка виртуальной сети — это еще одна область, где вам необходимо тесно
сотрудничать с коллегами из области виртуализации и инфраструктуры. Например, не стоит просто так запрашивать новые виртуальные адаптеры для
вашей кластерной сети. Объясните, чего вы пытаетесь достичь, и попросите
коллег разработать решение QoS, которое будет отдавать приоритет трафику
пульса кластера.
1

https://oreil.ly/r0M29

Управление виртуальными дисками  461

Управление виртуальными дисками
Как вы знаете, виртуализация абстрагирует конфигурацию хранилища от виртуальных машин. В виртуальной машине виртуальные диски отображаются как
полностью подготовленные, но на уровне хоста их необходимо настроить одним
из следующих способов.
Тонкая подготовка (thin provisioning)
В этом режиме виртуальный диск не занимает на хосте никакого места свыше того, которое используется в данный момент. При этом есть риск, что на
хосте окажется выделено слишком много места и пространство исчерпается,
если объем виртуальных дисков на нескольких виртуальных машинах превысит физическую емкость хранилища. В Hyper-V этот режим называется
динамическими дисками, а в VMware — тонкой подготовкой.
Толстая подготовка без обнуления (thick provisioning without zeroing out)
На хосте заранее выделяется пространство для виртуальных дисков, но эти
диски не обнуляются. Блоки на диске обнуляются при первой записи. Это
позволяет быстро расширить диск за счет накладных расходов на первую
запись. VMware называет этот режим zeroedthick provisioning.
Толстая подготовка с обнулением (thick provisioning with zeroing out)
Для виртуальных дисков на хосте заранее выделяется пространство, которое
заполняется нулями при выделении. При этом нет накладных расходов на
первую запись, однако расширение диска может занять некоторое время
и породить нагрузку ввода/вывода. В Hyper-V этот режим называется дисками фиксированного размера, а в VMware — eagerzeroedthick provisioning
(«толстая подготовка с жадным обнулением»).
Решение о том, в каком режиме подготавливать диски для виртуальной машины, зависит от многих факторов. Например, в тонкой настройке нет ничего
плохого, если накладные расходы на первую запись приемлемы и налажен
эффективный мониторинг емкости хранилища. Однако при расширении виртуального диска виртуальная машина может на некоторое время «подвиснуть».
При тонкой подготовке это время обычно измеряется миллисекундами, но если
нужно обнулить большой объем пространства, это может занять секунды или
даже минуты, что грозит проблемами или даже аварийным переключением
в промышленной среде.
Обязательно включите управление дисковым пространством в свою стратегию
обслуживания. Безопаснее расширять диски во время планового обслуживания
системы и отключать автоматическое аварийное переключение, когда происходит расширение дисков на первичных узлах в кластере.

462  Глава 15. SQL Server в виртуализированных средах

Стратегия и инструменты резервного
копирования
Существует множество инструментов для резервного копирования на уровне
виртуальных машин. Многие из них заявляют, что они совместимы с SQL
Server и могут заменить собственные резервные копии SQL Server. Правда ли
это и стоит ли их использовать — частая тема споров между специалистами по
инфраструктуре и по базам данных. Инфраструктурщики любят эти инструменты, потому что они основаны на моментальных снимках, что экономит дисковое пространство. Специалисты по базам данных часто не доверяют им и не
одобряют их ограничений.
Как и во многих других случаях, здесь все тоже зависит от обстоятельств. В некоторых ситуациях эти инструменты оказываются вполне приемлемыми, но
каждый инструмент нужно тщательно анализировать, чтобы убедиться, что он
соответствует вашим требованиям. В частности, нужно проверить такие возможности:
Способность достижения целевых показателей точки восстановления
(RPO) и целевого времени восстановления (RTO).
Способность восстановления базы данных в новой системе с другой конфигурацией хранилища.
Совместимость с такими функциями баз данных, как FILESTREAM и OLTP
в памяти.
Полноценная поддержка восстановления на определенный момент времени (а также желательно восстановление на уровне страниц — полезная
функция для борьбы с повреждением базы данных).
Автоматизированный процесс проверки резервной копии (который должен
быть простым в реализации и использовании).
Операционное воздействие: если инструмент может заблокировать работу
VM, так ли он нужен?
Конечно, не забудьте проверить, что инструмент работает правильно и выдержит аварийное восстановление (DR, Disaster Recovery) посреди резервного копирования. Также убедитесь, что инструмент сам не повреждает
базу данных. Обычно я внедряю проверку согласованности базы данных
и запускаю DBCC CHECKDB в ходе проверки резервной копии.
Не стоит сразу сбрасывать со счетов сторонние инструменты резервного копирования, сперва изучите их преимущества и недостатки. Эта информация поможет
вам вести плодотворные обсуждения и принимать решения в сотрудничестве
с другими командами.

Устранение неполадок в виртуальных средах  463

Устранение неполадок в виртуальных
средах
Исправлять проблемы и настраивать производительность экземпляров SQL
Server в виртуальных средах можно примерно так же, как на «голом железе».
Проблемы с экземплярами, базами данных и приложениями проявляются
одинаково независимо от того, виртуализирован SQL Server или нет. Чтобы
обнаруживать и устранять эти проблемы, можно применять одни и те же подходы во всех средах.
Но виртуализация добавляет к устранению неполадок еще один уровень сложности. В частности, приходится проверять, что конфигурация и производительность на уровне хоста не влияют на проблему. Что еще хуже, размещение
виртуальных машин и их рабочая нагрузка редко бывают статичными, поэтому
сегодняшний анализ может оказаться неактуальным завтра. К сожалению, очень
немногие метрики видны внутри гостевых виртуальных машин. Чтобы обнаружить проблемы, нужно изучать хост.
Это еще одна причина привлекать администраторов виртуализации к устранению неполадок, особенно если необъяснимые проблемы с производительностью
возникают внезапно без каких-либо изменений в рабочей нагрузке. Причиной
этих проблем могут быть изменения на уровне хоста, а администраторы виртуализации умеют анализировать метрики на этом уровне. (Тем не менее помните, что возможны и другие причины: например, к тому же результату может
привести деградация из-за сканирования параметров в планах выполнения,
чувствительных к параметрам.)
Давайте обсудим, как устранять наиболее распространенные проблемы, связанные с виртуализацией.

Недостаточная пропускная способность процессора
Узкие места, связанные с ЦП, — это, пожалуй, самая сложная область для
устранения неполадок в виртуализированных средах. Нужно анализировать
как гостевую виртуальную машину, так и нагрузку хоста и устранять проблемы
на обеих сторонах.
Информация о нагрузке ЦП в гостевой виртуальной машине — неполная. Гостевая машина отображает нагрузку виртуального ЦП в ОС, но не учитывает
нагрузку на уровне хоста. Например, бывает, что гостевая ОС показывает низкую нагрузку ЦП, но система все равно страдает из-за узкого места ЦП, если
сам хост перегружен.
Существует ключевая метрика, которая показывает, как долго виртуальная машина ожидает свободного ЦП для выполнения: она называется временем готов-

464  Глава 15. SQL Server в виртуализированных средах
ности (ready time) ЦП в VMware и временем ожидания (wait time) ЦП в Hyper-V.
Концептуально эти метрики похожи на ожидания SOS_SCHEDULER_YIELD в SQL
Server, которые происходят, когда рабочий процесс задерживается в очереди
выполнения, ожидая доступного планировщика.
Большое значение времени готовности ЦП указывает на проблемы с планированием на хосте. Это случается, когда хосту не хватает пропускной способности
ЦП для обработки нагрузки. Проблему могут усугубить виртуальные машины
с избыточным выделением ресурсов, потому что накладные расходы на планирование увеличиваются с количеством виртуальных ЦП.
Какое значение времени готовности процессора можно считать приемлемым?
Этот вопрос тоже горячо обсуждается. Это время может варьироваться в зависимости от степени критичности виртуальных машин. В VMware меня устраивает, когда его доля составляет менее 1 % в системах уровня 1 и менее 2–3 %
в системах уровня 2. Для Hyper-V задать пороговое значение немного сложнее,
как я покажу в следующем разделе.
Если время готовности ЦП велико, проверьте конфигурацию виртуальной
машины и нагрузку на хост. (Скоро я продемонстрирую, как узнать время готовности.) При необходимости переместите некоторые виртуальные машины
на другие хосты. Не выделяйте для виртуальных машин избыточные ресурсы:
экономичные машины, скорее всего, будут работать лучше, чем избыточно оснащенные. Однако не доходите до крайности и назначьте достаточное количество
виртуальных ЦП для обработки нагрузки.
Давайте посмотрим, как получить метрики, связанные с процессором, в Hyper-V
и VMware.

Анализ показателей ЦП в Hyper-V
На уровне хоста доступно несколько метрик производительности, связанных
с ЦП. В Hyper-V это следующие показатели:
Объект производительности Hyper-V Hypervisor Logical Processor
Счетчики производительности в объекте Hyper-V Hypervisor Logical
Processor предоставляют статистику о логических процессорах на хосте.
Счетчик % Total Run Time показывает общее использование логических процессоров на хосте. Высокие значения (более 90 %) могут говорить о том, что
хост перегружен. В этом случае попробуйте перенести некоторые виртуальные машины на другие хосты.
Счетчики % Guest Run Time и % Hypervisor Run Time показывают, сколько времени логический ЦП тратит на обработку кода гостевой ОС и гипервизора
соответственно.

Устранение неполадок в виртуальных средах  465
Счетчик производительности Hyper-V Hypervisor Virtual Processor\% Total
Run Time

Этот счетчик показывает общее использование виртуальных ЦП в гостевой
ОС. Высокие значения (выше 85 или 90 %) для всех виртуальных ЦП указывают на недостаточную пропускную способность ЦП в виртуальной машине.
Если перегружены только некоторые виртуальные ЦП, это может свидетельствовать либо о несбалансированной рабочей нагрузке из-за неравномерного
распределения планировщиков в узлах NUMA, либо о неэффективной конфигурации NUMA и/или ввода/вывода в Windows. Некоторые параметры
ввода/вывода NUMA можно настроить — см. подробности в документации
Microsoft1.
Существуют также счетчики % Guest Run Time и % Hypervisor Run Time, которые похожи на показатели объекта производительности Hyper-V Hypervisor
Logical Processor.
Счетчик производительности Hyper-V Hypervisor Virtual Processor\CPU Wait
Time Per Dispatch

Этот счетчик показывает время ожидания ЦП (время готовности ЦП)
в Hyper-V. К сожалению, он отображает не процент времени, в течение котороговиртуальный ЦП находится в состоянии ожидания, а просто среднее
время ожидания в наносекундах.
Трудно сказать, какие значения CPU Wait Time Per Dispatch можно считать признаком проблемы, потому что чем мощнее ЦП, тем этот счетчик будет ниже.
Можно с уверенностью сказать, что в критических системах его значение не
должно превышать нескольких микросекунд. Я также рекомендую принять
некоторое значение счетчика за базовое и следить, как этот показатель изменяется со временем.
Наконец, обратите внимание на этот счетчик, если наблюдается высокое значение Hyper-V Hypervisor Logical Processor\% Total Run Time. Виртуализация
предполагает, что хост будет работать под большой нагрузкой. Эта нагрузка
не всегда отражает влияние на производительность виртуальных машин,
и анализ метрики CPU Wait Time Per Dispatch помогает его оценить. Высокие
значения обоих счетчиков указывают, что хост перегружен. Однако высокое
значение % Total Run Time в сочетании с низким CPU Wait Time Per Dispatch
можно считать приемлемым.

Анализ метрик ЦП в VMware ESXi
Существуют два подхода к оценке показателей производительности в VMware.
Обзор всех метрик, доступных в VMware vSphere, выходит за рамки этой книги,
1

https://oreil.ly/omnUc

466  Глава 15. SQL Server в виртуализированных средах
поэтому за дополнительной информацией обратитесь к документации VMware1.
А здесь мы рассмотрим несколько наиболее важных показателей.
Во-первых, диаграммы производительности в клиенте vSphere позволяют в общих чертах анализировать использование ресурсов на хосте. Но значения на
диаграммах усреднены по временным интервалам и не показывают всплесков
и аномалий нагрузки.
Во-вторых, в vSphere есть утилита ESXTOP, которая в режиме реального времени
показывает, что происходит на хосте. Она предоставляет крайне подробную
информацию об использовании ресурсов и позволяет устранять проблемы
с производительностью в реальном времени. Утилиту ESXTOP можно запустить
из сеанса консоли или сеанса SSH на хосте. В ней есть несколько представлений, между которыми можно переключаться клавишами, соответствующими
символам в скобках: например, ЦП (c), память (m), сеть (n) и статистика дисков
виртуальной машины (v).
Также может быть полезно отфильтровать вывод, чтобы отображались только
данные виртуальных машин. Для этого наберите заглавную букву (V). По умолчанию ESXTOP производит выборку данных каждые пять секунд. Чтобы изменить
интервал, можно нажать (s) и ввести нужное количество секунд.
На рис. 15.5 показано представление ЦП в ESXTOP, собранное на одном из хостов.

Рис. 15.5. ESXTOP: представление ЦП
В верхней строке экрана отображается статистика хоста. Три значения CPU
Load Average отражают использование ЦП за последние 1, 5 и 15 минут соответственно. Значение 1.0 означает, что физические ЦП на хосте нагружены
1

https://oreil.ly/YlzzM

Устранение неполадок в виртуальных средах  467
полностью. Если CPU Load Average постоянно превышает 1.0, это значит, что
хосту не хватает пропускной способности ЦП для обработки нагрузки.
Параметры PCPU USER(%) и PCPU UTIL(%) содержат информацию об использовании
логического ядра (lCore), а CORE UTIL (%) — об использовании физического ядра
(pCore). Эти метрики позволяют анализировать нагрузку ЦП хоста в режиме
реального времени.
К сожалению, метрики использования ЦП для отдельных виртуальных машин
труднее интерпретировать. ESXTOP вычисляет их на основе процентной доли
базовой частоты ЦП на хосте, соответствующей различным виртуальным ЦП,
которые назначены виртуальной машине. На расчеты также могут повлиять
такие факторы, как гиперпоточность и Turbo Boost.
Тем не менее есть несколько показателей, на которые стоит обратить пристальное внимание.
Столбец %MLMTD
В этом столбце указан процент времени, в течение которого виртуальная
машина была готова выполняться, но виртуальные ЦП не были запланированы из-за настроек пула ресурсов на хосте. Если вы видите значения выше 0,
особенно на критически важных виртуальных машинах, обсудите выделение
и лимитирование ресурсов с администратором виртуализации.
Столбец %RDY
В этом столбце показано время готовности ЦП для виртуальных машин, которое, как уже отмечалось, должно быть менее 1 % для критически важных
виртуальных машин и не должно превышать 2–3 % для виртуальных машин
уровней 2 и 3. Высокое время готовности ЦП может быть признаком перегруженного хоста, избыточного оснащения виртуальных машин или того и другого.
Время готовности ЦП также может накапливаться из-за лимитирования
ресурсов, и в этом случае вы увидите ненулевые значения %MLMTD.
Столбец %CSTP
В этом столбце показано время совместной остановки виртуальной машины.
В режиме облегченного совместного планирования VMware может приостановить виртуальную машину, если работа распределена неравномерно между
виртуальными ЦП. Как и в случае с %RDY, значение %CSTP должно быть как
можно меньше.
Время совместной остановки обычно не является проблемой для виртуальных машин с серверами баз данных, потому что нагрузка на все виртуальные
ЦП сбалансирована. Если наблюдаются высокие значения %CSTP, проверьте
планировщики и конфигурацию NUMA, а также убедитесь, что внутри виртуальной машины не запущены ненужные приложения.

468  Глава 15. SQL Server в виртуализированных средах

Нехватка памяти
Вы уже знаете, что из-за перевыделения памяти на хосте драйвер накачки может
вызвать нехватку памяти, что приведет к проблемам с производительностью
и стабильностью SQL Server. Эта ситуация является распространенной причиной аварийного переключения в отказоустойчивых кластерах и группах
доступности AlwaysOn, особенно если на узлах включена функция LPIM. SQL
Server не высвобождает достаточно памяти, поэтому ОС перестает отвечать на
запросы и не проходит проверку кластера IsAlive.
Эту проблему также можно обнаружить, проанализировав журналы ошибок
отказоустойчивого кластера SQL Server и метрики накачанной памяти на
уровне хоста и гостевой виртуальной машины. К сожалению, здесь нет простого решения, кроме того, чтобы лучше планировать мощность для хоста
и гостевых виртуальных машин. Как и в случае с конфигурацией ЦП, не
выделяйте виртуальным машинам слишком много памяти, если на узле недостаточно ресурсов.
Устранять проблемы с памятью на виртуальных машинах SQL Server можно
с помощью методов, описанных в главе 7. В Hyper-V на уровне хоста можно посмотреть метрики Memory\Available Mbytes и Hyper-V Dynamic Memory Balancer\
Available Memory. Их низкие значения указывают на нехватку памяти на хосте,
что может привести к накачке памяти и другим проблемам.
Несколько метрик в утилите ESXTOP заслуживают особого внимания. Первая
связана с подкачкой страниц виртуальной машины. Виртуальные машины
с критически важными серверами баз данных не должны выполнять подкачку
на уровне хоста. Столбец %SWPWT в представлении ЦП (рис. 15.5) показывает
время, затраченное хостом на подкачку. Подробные метрики доступны в представлении памяти (рис 15.6), куда можно переключиться, нажав m. Существует
несколько счетчиков памяти, и здесь показаны не все из них. Между ними можно
переключаться, нажимая f.

Рис. 15.6. ESXTOP: представление памяти

Устранение неполадок в виртуальных средах  469
Давайте посмотрим на параметры, которые наиболее важны для устранения
неполадок.
Столбцы, связанные с подкачкой страниц
Столбцы SWR/s и SWW/s отражают текущую активность подкачки виртуальной
машины. Ненулевые значения указывают на то, что виртуальная машина
в настоящее время выполняет подкачку. В виртуальных машинах баз данных
не должно быть подкачки, потому что она ухудшает их производительность.
Столбец SWCUR показывает объем памяти виртуальной машины, которая
в данный момент подкачана. Это значение тоже должно быть нулевым в виртуальных машинах баз данных, хотя можно наблюдать ненулевые значения,
если некоторый неиспользуемый объем памяти виртуальной машины был
подкачан какое-то время назад и с тех пор не использовался. Низкое значение
в столбце SWCUR и отсутствие подкачки в столбцах SWR/s и SWW/s может быть
приемлемым в некритических системах.
Важно помнить, что эти столбцы указывают на подкачку на уровне хоста
и не видны внутри виртуальной машины.
Накачка памяти
Есть несколько столбцов MCTL, связанных с накачкой. Столбец MCTL? указывает, установлен ли драйвер накачки в ОС. Столбцы MCTLZ (MB) и MCTLTGT (MB)
отображают объем отозванной памяти и объем памяти, которую хост хочет
отозвать. Наконец, столбец MCTLMAX (MB) показывает максимальный объем
памяти, который может отозвать драйвер накачки.
Опять же, накачка чрезвычайно опасна для виртуальных машин баз данных.
Чтобы избежать проблем, правильно настройте виртуальные машины и хосты, а также не перевыделяйте память.

Производительность дисковой подсистемы
Как вы помните из главы 3, для устранения неполадок дисковой подсистемы
нужно просмотреть весь стек хранилища от SQL Server до физического массива
хранения, проанализировать метрики каждого компонента и выявить узкие места.
Виртуализация — это просто еще один уровень в этом стеке, и следует убедиться,
что он не приводит к заметным накладным расходам на производительность
ввода/вывода. В Hyper-V можно сравнивать счетчики производительности
дисков хостовой и гостевой ОС. В VMware можно использовать информацию
утилиты ESXTOP.
На рис. 15.7 показано представление виртуального диска виртуальной машины
в утилите ESXTOP. Столбцы отображают статистику операций ввода/вывода
и задержки для виртуальных дисков.

470  Глава 15. SQL Server в виртуализированных средах

Рис. 15.7. ESXTOP: представление виртуального диска
Также можно изучить представление дискового адаптера (для этого нажмите
d), показанное на рис. 15.8. Здесь можно увидеть, насколько операции ввода/
вывода сбалансированы между дисковыми адаптерами, что поможет устранить
проблемы с высокой задержкой виртуального диска. Столбцы DAVG, KAVG, QAVG
и GAVG представляют устройства, ядро VMware, очереди адаптера и задержки
ОС виртуальных машин для запросов ввода/вывода соответственно.
Убедитесь, что в гостевых виртуальных машинах используются последние версии инструментов виртуализации и что все драйверы хоста и гостевой системы
обновлены. Разделите рабочую нагрузку между несколькими адаптерами vSCSI.
При необходимости увеличьте глубину очередей.

Рис. 15.8. ESXTOP: представление дискового адаптера
Убедитесь, что в виртуальных машинах нет моментальных снимков
(snapshots). Они добавляют значительные накладные расходы ввода/вывода, которые могут повлиять на высоконагруженные системы. Кроме того,
некоторые задачи обслуживания, такие как клонирование виртуальных
машин или перенос больших виртуальных дисков в другое хранилище, могут вызвать внезапное и труднообъяснимое падение производительности
виртуальных машин.
Нет сомнений, что виртуализация усложняет устранение неполадок и может
привести к проблемам, но их обычно удается решить. Большинство проблем,

Резюме  471
с которыми я сталкиваюсь в виртуальных средах, связано с неправильной настройкой виртуализации или неэффективной стратегией устранения неполадок,
которая не учитывает экосистему в целом. Здесь помогает целостный подход
и сотрудничество между специалистами разного профиля.
Бывают случаи, когда нужно получить максимальную отдачу от оборудования,
и тогда виртуализация — не лучший выбор. За гибкость, которую она обеспечивает, вы расплачиваетесь накладными расходами. Помните об этом, принимая
решения.

Резюме
Виртуализация стала чрезвычайно распространенным явлением. Она снижает
стоимость оборудования и упрощает обслуживание. Но она также добавляет
накладные расходы, которые увеличиваются с размером виртуальных машин,
хотя во многих случаях оказываются приемлемыми.
Правильное планирование мощности крайне важно для виртуализации. Не перевыделяйте ресурсы на узлах с критически важными виртуальными машинами
баз данных уровня 1. Используйте резервирование ресурсов, когда это возможно.
Вместе с тем виртуальные машины уровней 2 и 3 могут выдерживать некоторую
степень перевыделения.
Как правило, не следует выделять виртуальным машинам избыточные ресурсы,
особенно в средах с перевыделением. В меньших виртуальных машинах легче
настраивать планирование и управление ресурсами, и они часто обеспечивают
более высокую производительность, чем более крупные.
Обращайте внимание на согласование ЦП, памяти и NUMA в конфигурации
виртуальной машины. Убедитесь, что планировщики равномерно распределены по узлам NUMA в SQL Server. Правильно настройте конфигурацию
vSocket/vCore, особенно если вы используете выпуски SQL Server, отличные
от Enterprise.
Хотя неполадки производительности SQL Server в виртуальной среде устраняются примерно так же, как в обычной, принимайте в расчет уровень виртуализации. Обращайте внимание на время готовности ЦП, накачку памяти и дополнительные накладные расходы, которые могут быть вызваны неправильно
настроенной виртуализацией. Устраняя неполадки, сотрудничайте с администраторами виртуализации.
В следующей главе мы поговорим об SQL Server в облачных средах и о проблемах, которые могут возникнуть в облачной конфигурации.

472  Глава 15. SQL Server в виртуализированных средах

Чек-лист устранения неполадок
Проверить конфигурацию виртуальной машины, уделяя особое внимание
конфигурации ЦП и памяти, а также согласованию NUMA.
Убедиться, что планировщики равномерно распределены по узлам NUMA
в SQL Server.
Убедиться, что установлены последние версии инструментов и драйверов
гостевой ОС.
Разделить рабочую нагрузку ввода/вывода между несколькими контроллерами vSCSI и при необходимости увеличить глубину очереди.
Проверить конфигурацию хоста. Убедиться, что хост не перевыделен, если
на нем работают критически важные системы уровня 1.
Не выделять виртуальной машине SQL Server избыточные ресурсы, если
в ней не используется резервирование ресурсов и нет выделенных ресурсов
на хосте.
Настроить параметры памяти SQL Server и LPIM в соответствии с вашей
средой.
Убедиться, что на промышленных виртуальных машинах нет моментальных снимков.

ГЛАВА 16

SQL Server в облаке

Облачные вычисления — уже давно не экзотика. Компании сокращают площадь
дата-центров и либо создают гибридные решения, либо полностью переходят
в облако. Таким образом, устранение неполадок и настройка облачных систем
баз данных стали обычной задачей для специалистов по БД.
Экземпляры облачных баз данных функционируют на той же платформе SQL
Server, и при работе с ними можно использовать знакомые вам инструменты
и приемы. Но есть и небольшие различия, которые мы рассмотрим в этой главе.
Мы начнем с высокоуровнего обзора облачных приложений, а затем перейдем
к облачным возможностям SQL Server. Сперва пойдет речь об экземплярах SQL
Server, работающих на облачных виртуальных машинах, а затем об управляемых
службах баз данных, которые доступны в Microsoft Azure, Amazon AWS и Google
Cloud Platform. Я также в общих чертах опишу архитектуру служб и инструменты мониторинга платформ и расскажу об их ограничениях.

Облачные платформы с высоты птичьего полета
Давным-давно, когда облачные вычисления только зарождались, на одной конференции мне попалась наклейка (рис. 16.1). На ней было написано: «Облака
не существует, это просто чей-то чужой компьютер».

Рис. 16.1. Облака не существует

474  Глава 16. SQL Server в облаке
Тогда это можно было считать лучшим определением облачных вычислений,
однако со временем все стало сложнее. Сейчас облака — это по-прежнему чьито чужие дата-центры, но они эволюционировали и теперь предоставляют
широкий ассортимент возможностей: помогают создавать приложения, предлагают готовые решения сложных проблем и зачастую значительно ускоряют
реализацию проектов.
Все основные поставщики облачных служб предлагают услуги управляемых
баз данных, принимая на себя рутинные задачи по их администрированию
и обеспечивая высокую доступность баз данных. Эти службы также позволяют
оснащать и запускать обычные виртуальные машины и переносить локальную
инфраструктуру в облако.
На мой взгляд, подход «взять и перенести» редко приводит к лучшим долгосрочным результатам. В некоторых случаях это оправданно, и часто это самый быстрый
способ миграции сложных локальных систем. Тем не менее я искренне считаю,
что если вы хотите получить максимальную отдачу от облака, то систему нужно
специально проектировать для работы в облаке. Только тогда вам удастся использовать преимущества облачных служб и учесть особенности облачных платформ.
Существует несколько ключевых различий между облачной и локальной инфраструктурами.

Надежность платформы
Любое облако работает на потребительском оборудовании. Сбои в облаках происходят чаще, чем на локальном оборудовании, поэтому приложения и серверы
баз данных должны уметь справляться с отказами.
Сразу приведу пример. У Amazon AWS есть соглашение об уровне обслуживания
(SLA), предусматривающее 99,5 % времени бесперебойной работы для виртуальных машин EC2, что означает до 3,6 часа простоя в месяц. Что гораздо хуже, в случае нарушения SLA единственное, что вы получите обратно, — это компенсацию
за обслуживание, которая не покроет убытки бизнеса, возникшие из-за простоя.
Вопросы надежности платформы выходят за рамки уровня базы данных. Приложения должны быть устойчивыми и уметь обрабатывать случайные ошибки. Они
также должны быть идемпотентными. Например, если база данных или очередь
сообщений становится недоступной посреди бизнес-транзакции, у системы должна быть возможность повторно обработать транзакцию после восстановления.

Лимитирование ресурсов
Второй ключевой фактор — лимитирование ресурсов (throttling). Облачные
сервисы и ресурсы применяют жесткое лимитирование. Например, если вы
платите за тома эластичного блочного хранилища (EBS, elastic block storage)

Облачные платформы с высоты птичьего полета  475
с определенным количеством операций ввода/вывода в секунду (IOPS) в AWS,
вы не получите ни на одну IOPS больше, чем оплатили. В локальной виртуализации ситуация другая: на критические виртуальные машины могут влиять
«шумные соседи» и общая нагрузка хоста, но к ним редко применяется лимитирование со стороны QoS.
Наличие лимитирования означает, что при оснащении облачных систем нужно
тщательно планировать мощность. К счастью, последствия ошибок и недостаточно выделенной мощности менее серьезны, чем при неподходящей мощности
локального оборудования. В большинстве случаев облачные сервисы можно
быстро масштабировать, хотя иногда это влетает в копеечку.
К сожалению, некоторые ограничения оказываются очень жесткими. Например,
у виртуальных машин задается предельный уровень трафика ввода/вывода
и сети, службы управляемых баз данных могут ограничивать скорость пополнения журнала транзакций и т. д. Проектируя облачную инфраструктуру, необходимо учитывать эти ограничения. Облако может поддерживать почти любые
рабочие нагрузки, однако, если вы перемещаете в облако высоконагруженную
систему, ее почти наверняка лучше перепроектировать.

Топология
Я уверен, что вы знакомы с облачной топологией, но тем не менее перечислю
ее ключевые элементы.
Регион
Каждый облачный регион соответствует отдельному дата-центру в определенном географическом регионе. Например, дата-центр Microsoft Azure West
US находится в Калифорнии, а West US 2 — в штате Вашингтон.
Распределение служб и баз данных по регионам позволяет создавать геоизбыточные системы. Однако при проектировании систем следует помнить
о задержке межрегиональной связи. В крайних случаях, когда вы распределяете серверы по разным континентам, задержка может достигать сотен
миллисекунд. Из-за межрегиональных обращений к базам данных может
значительно увеличиться продолжительность транзакций. По возможности
лучше реализовать асинхронную связь на основе сообщений.
Зоны доступности
Зоны доступности (AZ, availability zones) — это изолированные участки
инфраструктуры в каждом регионе. Их можно представить как отдельные
здания одного дата-центра или даже как отдельные дата-центры в одном
городе, независимые друг от друга. Они обеспечивают необходимую избыточность в пределах каждого региона: проблемы в одной зоне доступности
теоретически не должны затрагивать другие зоны.

476  Глава 16. SQL Server в облаке
Поскольку зоны доступности разделены физически, у них разная сетевая
инфраструктура и связь между ними происходит с задержкой. Эта задержка
обычно очень мала — менее 1–2 мс, — но все же может увеличить нагрузку
на синхронную репликацию групп доступности в высоконагруженных системах OLTP.
Большинство управляемых облачных служб «из коробки» обеспечивают
избыточное присутствие в различных зонах доступности или могут быть
настроены так, чтобы работать в нескольких зонах. В то же время каждая
облачная виртуальная машина находится только в одной зоне доступности,
поэтому для облачной виртуализации необходимо внедрять эффективные
решения высокой доступности.
Прежде чем мы перейдем к вопросам оснащения облачного SQL Server и устранения соответствующих неполадок, я хотел бы кратко затронуть тему подключения к экземплярам облачной базы данных.

Связь и обработка случайных ошибок
Проблемы со связью — один из самых распространенных источников ошибок
при развертывании облачных систем. К счастью, чаще всего с этими ошибками
относительно легко справиться. Проблемы со связью бывают двух типов: отсутствие доступа к экземпляру базы данных и случайные ошибки.

Доступ к экземпляру базы данных
Первая проблема — отсутствие доступа к экземпляру базы данных. При подготовке облачного ресурса его возможности связи по умолчанию ограниченны.
Вы не можете подключиться к нему извне, и другие облачные службы тоже не
могут, пока не настроены соответствующие правила доступа. Эта ситуация будет
проявляться как общая ошибка подключения, когда клиент не может подключиться к экземпляру SQL Server.
Решение проблемы существенно зависит от топологии. Если вам нужно подключиться к базе данных через общедоступный интернет, откройте для базы
публичный доступ и разрешите IP-адреса клиентов. В более сложных случаях,
таких как подключение облачных ресурсов к локальной инфраструктуре, скооперируйтесь со специалистами по облачным и сетевым технологиям и вместе
с ними добейтесь, чтобы экземпляр базы данных был включен в нужную виртуальную сеть или виртуальное частное облако (VPC, virtual private cloud)
и чтобы группы безопасности (правила сетевого брандмауэра) были правильно
настроены.

Связь и обработка случайных ошибок  477

Случайные ошибки
Второй тип проблем связан со случайными ошибками. Помимо состояния аварийного переключения, сервер базы данных может не позволить подключиться
к нему, если исчерпаны ограничения тарифного плана. Например, сервер может
принудительно завершать запросы или даже соединения с интенсивным использованием ресурсов, если сеанс создает слишком много записей журнала
транзакций.
Когда возникают случайные ошибки, то управляемые службы баз данных
Microsoft Azure предоставляют дополнительные сведения; коды и описания
ошибок можно найти в документации Microsoft1. Вместе с тем управляемые
службы Amazon Web Services (AWS) и Google Cloud Platform (GCP) не обеспечивают настолько информативных сообщений об ошибках, и неполадки могут
проявляться в виде нехватки доступных исполнителей (ожидания THREADPOOL)
или общих проблем с производительностью.
Поскольку приложение должно быть устойчивым, оно должно реализовывать
логику повторных попыток при обработке ошибок. По возможности перед
повторной попыткой лучше открыть соединение заново, потому что из-за некоторых случайных ошибок сеанс может стать неработоспособным. Кроме того,
в облачных службах предусмотрены меры по предотвращению DoS-атак, поэтому при повторном открытии соединения не заваливайте экземпляр запросами на
подключение. Если не удается повторно подключиться немедленно, подождите
несколько секунд перед новой попыткой.
Предостережение: избегайте конфигураций «клиент — сервер», где удаленные
клиентские приложения обращаются к экземплярам облачной базы данных.
Такие конфигурации подвержены высоким задержкам, случайным ошибкам
и проблемам с подключением. Они могут быть приемлемы для некритичных
приложений, но недостаточно устойчивы для использования в критических
системах. В таких системах ожидания ASYNC_NETWORK_IO обычно не покидают
вершины списка ожиданий из-за задержек между клиентом и сервером. К сожалению, рабочие потоки, ожидающие в этом состоянии, недоступны для
других сеансов, что может привести к нехватке рабочих потоков и ожиданиям
THREADPOOL. Обращайте внимание на эти ожидания при устранении неполадок
в облачных системах.
Наконец, не забудьте увеличить значение времени ожидания соединения
и команды, когда вы подключаетесь к SQL Server удаленно, особенно если вы
планируете запускать клиентские приложения в медленных сетях.
Теперь давайте рассмотрим варианты подготовки SQL Server в облаке.
1

https://oreil.ly/WMF71

478  Глава 16. SQL Server в облаке

SQL Server в облачных виртуальных машинах
Каждый поставщик облачных услуг предлагает свои управляемые службы SQL
Server, но некоторые команды предпочитают сами управлять экземплярами
и размещать SQL Server на облачных виртуальных машинах. Это обычно происходит в таких случаях:
Миграции в стиле «взять и перенести», когда вся инфраструктура перемещается в облако «как есть» с минимальными изменениями.
Сложные требования к инфраструктуре SQL Server или рабочие нагрузки,
которые не поддерживаются управляемыми службами.
Ситуации, когда специалисты, работающие с базами данных, хотят иметь
полный контроль над средой.
Проприетарные реализации баз данных как услуги, которые могут оказаться значительно дешевле, чем управляемые службы с профессиональными
командами сопровождения.
Поддерживать экземпляры SQL Server на облачных виртуальных машинах
и устранять их неполадки можно так же, как в случае с виртуализированными
локальными экземплярами, о которых говорилось в главе 15. По сути, в обоих
случаях вы имеете дело с одним и тем же продуктом и одним и тем же набором
инструментов. Облачные порталы позволяют получить метрики использования
ресурсов на уровне гипервизора, что тоже концептуально похоже на локальную
виртуальную машину.
Тем не менее в поведении платформ есть несколько различий, на которые нужно
обратить внимание. Давайте рассмотрим их подробнее.

Настройка ввода/вывода и производительность
Современное аппаратное обеспечение позволяет создавать очень мощные серверы, которые могут скрывать проблемы с производительностью. Например,
влияние неоптимизированных запросов, чрезмерное протоколирование и другие
проблемы компенсируются дополнительными ЦП, большим объемом ОЗУ для
кэширования данных и хранилищем с задержкой менее миллисекунды. Однако
иногда эти проблемы всплывают вновь при перемещении SQL Server в облако.
Во время планирования мощности легко оценить требования к вычислительным ресурсам (ЦП и ОЗУ) виртуальных машин, потому что производительность виртуальных машин одинакового размера в локальной среде и в облаке
сопоставима. Но в облаке нужно обращать внимание на классы экземпляров
виртуальных машин и поколение оборудования, потому что они определяют
базовое оснащение.

SQL Server в облачных виртуальных машинах  479
Хранилище — отдельная тема. В локальной среде масштабируемость хранилища
обычно зависит от оборудования. Современные хранилища могут выдерживать
нагрузку в сотни тысяч операций ввода/вывода в секунду, обладают пропускной
способностью в несколько гигабайт в секунду и могут масштабироваться при
всплесках рабочей нагрузки SQL Server.
Облачное хранилище устроено иначе. Каждый облачный поставщик поддерживает несколько типов и уровней хранилища и ограничивает IOPS и пропускную
способность каждого из них. Некоторые уровни хранилищ позволяют превысить ограничения в случае всплесков нагрузки, но потом сильно лимитируют
мощность.
Что еще хуже, быстрые хранилища и большие виртуальные машины стоят дорого, поэтому часто на виртуальную инфраструктуру выделяется гораздо меньше
ресурсов по сравнению с локальной. Виртуальные машины с медленным вводом/
выводом и недостаточным размером могут столкнуться с проблемами производительности, которые не были заметны на быстром локальным оборудовании.
Конечно, лучше устранять первопричины проблем и налаживать систему. Однако, если это невозможно или нецелесообразно, убедитесь, что ваша облачная
инфраструктура, а особенно хранилище, должным образом подготовлена. Я не
могу дать универсального совета, который подходил бы ко всем системам, но
предложу несколько рекомендаций.
Перед миграцией в облако проанализируйте пропускную способность ввода/вывода в своих локальных системах. В качестве целевых показателей облачной конфигурации используйте ключевые индикаторы производительности (KPI) на уровне хранилища.
Используйте типы дисков с гарантированными характеристиками производительности, способные обрабатывать вашу нагрузку.
Размещайте файлы данных и журналов на разных дисках, чтобы по мере необходимости масштабировать пропускную способность. То же самое относится к различным файловым группам и в особых случаях к файлам данных.
Попробуйте настроить в своей ОС RAID-массивы. Например, в Microsoft
Azure два диска премиум-класса P30 емкостью 1 Тбайт в массиве RAID-0
обес­печат больше IOPS и лучшую пропускную способность, чем один диск
P40 емкостью 2 Тбайт.
Помните, что виртуальные машины ограничивают количество операций
ввода/вывода в секунду и пропускную способность.
Чтобы устранять неполадки с производительностью диска, можно использовать те же подходы, которые рассматривались в главе 3. Также можно изучить
метрики ввода/вывода и оценить, насколько вы приблизились к пределам использования ресурсов облачного портала.

480  Глава 16. SQL Server в облаке

Настройка высокой доступности
Грамотная реализация высокой доступности — обязательное требование для
SQL Server, работающего на облачной виртуальной машине. Обычно для этого
специалисты по базам данных внедряют группы доступности, но можно использовать и отказоустойчивые кластеры.
Это особенно важно при подготовке виртуальных машин в разных зонах доступности для избыточности. Нужно будет создать и настроить отказоустойчивый
кластер Windows Server (WSFC) с несколькими подсетями. Это приведет к небольшой, обычно приемлемой задержке синхронной репликации.
Если инфраструктура SQL Server должна распространяться на несколько ре­
гионов, никогда не используйте межрегиональную синхронную репликацию. Сетевая задержка приведет к чрезвычайно высоким ожиданиям HADR_SYNC_COMMIT
и может создать масштабное блокирование в базе данных.
Если стратегия высокой доступности в вашей организации требует, чтобы система могла выдержать аварию на уровне региона и продолжать работу во вспомогательном регионе после аварийного переключения, то во вспомогательном
регионе имеет смысл подготовить несколько реплик. Для этого обычно лучше
использовать распределенные группы доступности, чем отдельные реплики.
Они позволяют сократить сетевой трафик между регионами и, на мой взгляд,
ими проще управлять с точки зрения высокой доступности. Имейте в виду, что
в случае аварии на уровне региона для распределенных групп доступности потребуется другая стратегия восстановления после аварийного переключения по
сравнению с обычными группами доступности.
Попробуйте использовать облачную службу DNS (Azure DNS, Amazon Route53,
Google Сloud DNS) для подключения приложений. Это упростит аварийное
переключение между регионами, особенно если у вас настроены распределенные
группы доступности. Также может принести пользу архитектура, в которой применяются отдельные пулы соединений для первичных и доступных для чтения
вторичных реплик в мультирегиональных приложениях типа «активный — активный». Службы в каждом регионе будут подключаться к локальным репликам
SQL Server и выполнять запросы только для чтения, когда допустимы задержка
асинхронной репликации и доступ к слегка устаревшим данным.

Межрегиональная задержка
Облачная инфраструктура практически провоцирует создавать геоизбыточные
системы с приложениями, которые обслуживают запросы клиентов в нескольких
регионах. Но это не такая уж простая задача, если вы используете реляционную
базу данных, которая выполняет операции записи на одном первичном узле.
Проблема — в задержке межрегиональных вызовов.

SQL Server в облачных виртуальных машинах  481
Эта задержка зависит от расстояния между дата-центрами. Если вы распределили серверы между регионами Azure East US и East US2, задержка вряд ли превысит несколько миллисекунд, но когда ваши дата-центры находятся на разных
концах земного шара, задержка каждого вызова может достигать 200–300 мс.
Спроектируйте систему так, чтобы уменьшить влияние задержек. Этому могут
помешать приложения, которые выполняют много отдельных запросов в бизнестранзакциях. Суммарная продолжительность вызовов быстро увеличивается, что
приводит к проблемам с производительностью и ухудшению пользовательских
качеств.
Для примера представьте, что приложению, расположенному в регионе Azure
West US, нужно вставить 100 строк в базу данных, размещенную в дата-центре
в Западной Европе. Реализация со 100 отдельными вызовами INSERT займет
около 15 секунд и может привести к ощутимому блокированию, потому что
монопольные блокировки (X) будут удерживаться в течение длительного времени. Вместо этого можно передать хранимой процедуре все 100 строк в одном
пакете параметров с табличным значением (TVP), и получится всего один
межрегиональный вызов, что намного эффективнее.
Некоторые проблемы с задержкой можно решить, если использовать для нагрузки чтения локальные доступные для чтения вторичные узлы в регионе.
Но помните, что репликация в группах доступности тоже подвержена задержкам.
Не используйте доступные для чтения вторичные реплики, если вам нужно прочитать свежие данные. И, что еще важнее, не смешивайте в одной бизнес-транзакции операции чтения из вторичных узлов и операции записи в первичный узел.
А что если приложение вставляет данные в первичный узел и сразу перезагружает
их из реплики, ожидая, что они будут обновлены? Это не сработает: проблема в задержке, связанной с межрегиональной репликацией. В одном регионе так тоже не
стоит делать: эта система может сработать в обычной ситуации, но любые всплески
протоколирования или заторы в очередях отправки и повтора могут всё сломать.
Наконец, не забывайте о задержке вызовов аутентификации SQL Server к контроллеру домена. В главе 13 мы узнали, что эти вызовы могут ощутимо влиять
на систему, если их задержка велика. Если вам нужно использовать аутентификацию Windows, попробуйте в каждом регионе обеспечить контроллер домена
или использовать облачные службы, которые интегрируются с Active Directory.
И все же в остальном запуск SQL Server на облачных виртуальных машинах
очень похож на запуск виртуализированных экземпляров SQL Server в локальной среде. Для управления, настройки и устранения неполадок применяется
один и тот же набор инструментов и методов, которые одинаково работают со
всеми облачными поставщиками.
Теперь посмотрим, какие аспекты служб управляемых баз данных различаются
у разных поставщиков.

482  Глава 16. SQL Server в облаке

Управляемые службы Microsoft Azure SQL
Помимо SQL Server, работающего на виртуальных машинах Azure, семейство
Microsoft Azure SQL включает базы данных Azure SQL, управляемые экземпляры Azure SQL и Azure SQL для периферийных вычислений (версия SQL Server,
оптимизированная для интернета вещей (IoT) и работающая на периферийных
устройствах на базе ARM и x86). В этом разделе я сосредоточусь на управляемых
экземплярах и базах данных Azure SQL.
На внутреннем уровне все эти технологии работают на одном и том же ядре SQL
Server, используют одну и ту же кодовую базу и ведут себя одинаково. Методы
настройки и устранения неполадок, описанные в этой книге, будут работать во
всех этих системах. Правда, в некоторых случаях, возможно, будут различаться
методы сбора данных. Например, управляемые облачные службы абстрагируют
вас от нижележащей ОС, поэтому, чтобы изучить потребление ресурсов, вместо
использования утилиты PerfMon нужно заходить на портал Azure или запрашивать динамические административные представления. Однако подходы к анализу
и устранению неполадок не зависят от того, какую технологию вы используете.

Рекомендации по архитектуре и проектированию служб
Microsoft представила базы данных Azure SQL в 2010 году. Их можно рассматривать как «БД как услугу» (DBaaS), предоставляющую вам базу данных SQL
Server. Эта технология поддерживает большинство возможностей баз данных
и обеспечивает высокую доступность, резервное копирование и обслуживание
SQL Server и нижележащей ОС. Однако у нее довольно много ограничений,
особенно на уровне экземпляра. Например, нельзя выполнять запросы между базами данных, использовать CLR, брокер служб (Service Broker) или SQL Agent.
В 2018 году Microsoft добавила в эту линейку новый продукт — управляемые экземпляры Azure SQL (SQL MI, SQL Managed Instances). Если говорить коротко,
то SQL MI — это виртуализированный экземпляр SQL Server, который поддерживает большинство стандартных функций SQL Server. Чаще всего локальные
экземпляры SQL Server можно без особых усилий перемещать на SQL MI.
Не забывайте учитывать ограничения ресурсов разных технологий. Например, ограничения пропускной способности ввода/вывода и скорости пополнения журналов могут быть слишком строгими для рабочей нагрузки
высокопроизводительной системы OLTP.

Как и базы данных Azure SQL, SQL MI — это управляемый облачный сервис,
который берет на себя задачи резервного копирования, высокой доступности
и обновлений ПО. В обеих технологиях вам по-прежнему придется выполнять
административные задачи, такие как обслуживание индексов и статистики.

Управляемые службы Microsoft Azure SQL  483
Мощность базового оборудования можно понижать или повышать, указав количество логических ЦП (виртуальных ядер) для того или иного экземпляра.
От количества виртуальных ядер также зависит объем памяти. Например, при
использовании SQL MI каждое виртуальное ядро предоставляет вам 5,1, 7 или
13,6 Гбайт ОЗУ в зависимости от типа экземпляра. (Учтите, что по мере появления новых типов оборудования эти цифры могут меняться.)
У обеих технологий есть несколько уровней обслуживания. Уровни General
Purpose и Business Critical поддерживаются как в базах данных, так и в управляемых экземплярах Azure SQL. Третий уровень, Hyperscale, доступен только
в базах данных Azure SQL.
От уровня сервиса зависит соглашение об уровне обслуживания (SLA) и доступность некоторых функций: например, In-Memory OLTP доступна только
на уровне Business Critical. Однако более важными характеристиками уровня
являются хранилище и реализация высокой доступности.
Базы данных Azure SQL поддерживают другую модель покупки, которая
основана на единицах пропускной способности базы данных (DTU, Database
Throughput Units) и поставляется с тремя другими уровнями обслуживания.
Уровни Basic и Standard похожи на General Purpose, уровень Premium аналогичен Business Critical.

На рис. 16.2 показана конфигурация вычислительных ресурсов и хранилища
на уровне General Purpose. Слой вычислений с процессом SQL Server не сохраняет состояния. Он содержит только временные данные, такие как tempdb,
в локальном хранилище SSD. Файлы базы данных и резервные копии хранятся
в избыточном хранилище Azure Blob.
Вычисления
Экземпляр SQL Server

Аварийное Экземпляр SQL Server
переключение

TempDB

TempDB

Хранилище
.MDF/.LDF
Хранилище класса Premium
Хранилище класса Standard

Резервные копии
Рис. 16.2. Конфигурация вычислительных ресурсов и хранилища на уровне
обслуживания General Purpose

484  Глава 16. SQL Server в облаке
В случае аварийного переключения другой экземпляр SQL Server подключается
к тем же файлам базы данных и берет на себя рабочую нагрузку. Концептуально
он ведет себя так же, как отказоустойчивый кластер AlwaysOn SQL Server, но
внутренняя реализация отличается и работает на Azure Service Fabric.
В базе данных Azure SQL можно настроить избыточность по зонам, когда
хранилище реплицируется в нескольких зонах доступности. В управляемых
экземплярах Azure SQL на момент написания этого раздела такая функция еще
не поддерживалась.
На уровне Business Critical реализация основана на группах доступности
AlwaysOn. В конфигурации, показанной на рис. 16.3, база данных или управляемый экземпляр Azure SQL состоит из нескольких реплик, в которых файлы
базы данных хранятся на локальных накопителях SSD. Аварийное переключение
в этой конфигурации — это просто обычное аварийное переключение группы
доступности AlwaysOn.
Первичный узел

Реплика

Экземпляр SQL Server

Экземпляр SQL Server

TempDB

.MDF

.LDF

TempDB

.MDF

.LDF

Реплика
Экземпляр SQL Server
TempDB

.MDF

.LDF

Хранилище Azure класса Standard
Резервные копии
Рис. 16.3. Конфигурация вычислительных ресурсов и хранилища на уровне
обслуживания Business Critical
Нетрудно догадаться, что от нижележащей топологии зависят некоторые характеристики системы. Например, диск на уровне General Purpose работает
медленнее, но позволяет эксплуатировать большие базы данных, потому что
не ограничен объемом локальных SSD. Вместе с тем уровень Business Critical
обеспечивает меньшую задержку ввода/вывода и позволяет масштабировать
рабочую нагрузку чтения с помощью доступных для чтения вторичных реплик.
Но взамен вы получаете накладные расходы на синхронную репликацию и по-

Управляемые службы Microsoft Azure SQL  485
тенциальные проблемы с ожиданиями HADR_SYNC_COMMIT (об этом мы говорили
в главе 12).
Как и уровень General Purpose, Business Critical может быть избыточен по зонам.
В этом режиме реплики распределяются по зонам доступности. При этом обес­
печивается более высокая доступность за счет небольших накладных расходов
во время синхронной репликации.
Уровень сервиса Hyperscale поддерживает очень большие базы данных (VLDB,
very large databases). Его архитектура (рис. 16.4) устроена сложнее и содержит
следующие компоненты:
Серверы страниц, которые управляют файлами данных, хранящимися
в Azure Blob.
Вычислительные узлы на основе групп доступности, обрабатывающие запросы.
Систему управления журналом транзакций Log Service, которая принимает
потокзаписей журнала от первичного узла и распределяет его по серверам
страниц и вторичным вычислительным узлам.
Первичный узел

Реплика

Реплика

Кэш

Кэш

Кэш

Log service
Сервер
страниц
Кэш

.MDF

Сервер
страниц

.MDF

Кэш

Рис. 16.4. Архитектура уровня обслуживания Hyperscale
По состоянию на декабрь 2021 года уровень Hyperscale поддерживает базы
данных размером до 100 Тбайт. В теории серверы страниц позволяют масштабировать базы данных практически неограниченно, но их производительность
сильно зависит от рабочей нагрузки.
У каждого вычислительного узла есть локальный кэш на основе SSD для горячих (активных) данных, опирающийся на технологию Buffer Pool Extension.
Когда активные данные помещаются в кэш, узлы могут очень быстро получать
к ним доступ. Но получение данных с удаленных серверов страниц происходит
гораздо менее эффективно.

486  Глава 16. SQL Server в облаке
Протестируйте свою рабочую нагрузку, чтобы оценить, пригодится ли для нее
архитектура Hyperscale. Она хорошо подходит для крупных баз данных OLTP,
которые работают с относительно небольшим объемом горячих данных и нетребовательной рабочей нагрузкой. В то же время большие аналитические
рабочие нагрузки обычно обрабатывают много данных, которые не помещаются
в локальный кэш, а серверы страниц могут добавить накладные расходы, отчего
снизится производительность.
Размер данных тоже имеет значение. Хотя это плохо задокументировано, каждый
сервер страниц поддерживает до 1 Тбайт данных. Таким образом, эта архитектура
не нужна для небольших баз данных, которые не будут должным образом масштабироваться на уровне хранилища. Но есть интересный сценарий использования:
поскольку данные не сохраняются на вычислительных узлах, можно очень быстро
подключать новые узлы и масштабировать рабочую нагрузку чтения. Так можно
снизить затраты, если рабочая нагрузка в системе непостоянна.
В конечном итоге все продукты и уровни обслуживания Azure SQL работают на
основе SQL Server. Поговорим о том, как устранять неполадки в этих системах.

Подходы к устранению неполадок
При устранении неполадок в управляемых облачных службах на основе
SQL Server используется тот же набор метрик, что и для обычных экземпляров
SQL Server, хотя собирать эти метрики приходится немного иначе. В обычном SQL Server устранение неполадок, как правило, начинается с анализа экосистемы и потребления ресурсов ОС. В управляемых облачных службах у вас
нет доступа к метрикам ОС, но данные о потреблении ресурсов можно получить
на портале Azure или через динамические административные представления.
На рис. 16.5 показан снимок экрана с метриками базы данных Azure SQL с портала Azure. (Я обрезал и увеличил нужные фрагменты, поэтому не удивляйтесь,
что линии на графике разрываются.)
Аналогичную информацию можно получить из представления sys.dm_db_
resource_stats1, которое собирает данные каждые 15 секунд и хранит их примерно в течение часа. На рис. 16.6 показан пример его вывода.
В представлении sys.resource_stats 2 в базах данных Azure SQL или sys.
server_resource_stats3 в управляемых экземплярах Azure SQL можно посмотреть данные с меньшим разрешением, но за более долгое время (рис. 16.7).
1

https://oreil.ly/wyaZN

2

https://oreil.ly/luAvK

3

https://oreil.ly/koZ6j

Управляемые службы Microsoft Azure SQL  487

Рис. 16.5. Потребление ресурсов базы данных Azure SQL

Рис. 16.6. Вывод представления sys.dm_db_resource_stats

Рис. 16.7. Вывод представления sys.resource_stats

488  Глава 16. SQL Server в облаке
Стабильно максимальный уровень потребления ресурсов в выходных данных
указывает на то, что у базы данных или управляемого экземпляра не хватает пропускной способности для рабочей нагрузки, поэтому они лимитируются. В этом
случае можно либо увеличить мощность экземпляра, чтобы повысить пропускную
способность, либо отрегулировать систему, чтобы уменьшить нагрузку.
Как вы узнали в главе 6, в SQL Server 2017 и более поздних версиях можно
включить автоматическое регулирование и разрешить SQL Server исправлять
регрессивные планы, зависящие от параметров. Эта функция включена по
умолчанию в базах данных Azure SQL, и ее можно включить в базах данных
управляемых экземпляров. Можно также разрешить базе данных Azure SQL
автоматически создавать индексы, чтобы бороться с неэффективными запросами. Система будет отслеживать эффективность индексов и сохранять или
удалять их в зависимости от настроек (рис. 16.8).

Рис. 16.8. Автоматическое регулирование в базе данных Azure SQL
Устранение неполадок на основе статистики ожидания работает с обеими
управляемыми службами (в базе данных Azure SQL используйте представление
sys.dm_db_wait_stats1 вместо sys.dm_os_wait_stats). Вы увидите те же самые
типы ожидания, что и в обычных экземплярах SQL Server. Другие динамические
административные представления, рассмотренные в этой книге, тоже будут
работать. Также можно использовать хранилище запросов, которое включено
в базах данных Azure SQL по умолчанию и может быть включено в управляемых
экземплярах.
Чтобы проанализировать задержки ввода/вывода, можно использовать представление sys.dm_io_virtual_file_stats, которое поможет понять, обеспечи1

https://oreil.ly/AUZtj

Amazon SQL Server RDS  489
вает ли хранилище Azure Blob достаточную пропускную способность на уровне
обслуживания General Purpose. На уровне Hyperscale метрики, связанные
с серверами страниц, добавляются к планам выполнения, к представлению
sys.dm_exec_requests и представлениям статистики выполнения, а также в несколько расширенных событий.
Расширенные события можно использовать как в базах данных, так и в управляемых экземплярах Azure SQL. Имейте в виду, что для управления расширенными событиями базы данных Azure SQL используют несколько иной синтаксис
T-SQL. Кроме того, из целевых объектов при этом поддерживаются только
ring_buffer, event_counter и event_file. Управляемые экземпляры Azure SQL
поддерживают все целевые объекты, как и обычный SQL Server. Используя целевой объект event_file, файлы нужно размещать в хранилище Azure Blob. Это
может вызвать некоторую задержку, поэтому будьте осторожны, когда нужно
регистрировать события, критичные для производительности.
В управляемых облачных службах нет доступа к PerfMon. Тем не менее метрики
производительности SQL Server можно посмотреть с помощью представления
sys.dm_os_performance_counters.
Наконец, в документации Microsoft1 можно прочесть о некоторых других динамических административных представлениях, специфичных для Azure. В базах
данных Azure SQL представление sys.event_log может заменить журнал ошибок
SQL Server и содержит сведения о подключении и взаимных блокировках.

Amazon SQL Server RDS
Amazon Web Services предлагает несколько технологий в рамках своего семейства управляемых служб реляционных баз данных (RDS, Relational Database
Services). Это семейство поддерживает несколько версий SQL Server, начиная
с устаревшей версии 2012. Можно выбрать любую версию SQL Server, включая
SQL Server Express.
Концептуально SQL Server RDS аналогичен управляемым экземплярам Azure
SQL: вы работаете с виртуализированными экземплярами SQL Server, на которых может размещаться несколько баз данных. Хотя SQL Server RDS поддерживает большинство функций SQL Server, есть определенные ограничения:
например, недоступны FILESTREAM или UNSAFE CLR.
SQL Server RDS допускает развертывание в нескольких зонах для высокой доступности. В этом режиме RDS создает другой экземпляр SQL Server в другой
зоне доступности с помощью либо групп доступности, либо зеркального отображения базы данных с синхронной репликацией. Поскольку зоны доступности
1

https://oreil.ly/ZG09I

490  Глава 16. SQL Server в облаке
полностью изолированы друг от друга, это может привести к дополнительной
задержке фиксации, которая способна вызвать проблемы в системах с большим
количеством транзакций.
В Enterprise Edition можно создавать доступные для чтения реплики в одном
и том же регионе, чтобы масштабировать рабочую нагрузку чтения. Для поддержки этих реплик на внутреннем уровне в RDS применяется асинхронная
репликация групп доступности. К сожалению, мультирегиональные реплики не
поддерживаются, поэтому RDS не подходит, если система должна выдерживать
сбои на уровне региона.
Некоторые административные задачи SQL Server RDS, в том числе перевод
баз данных в оперативный и автономный режим, переименование баз данных
и включение отслеживания измененных данных (CDC, Change Data Capture),
требуют специальных хранимых процедур, которые доступны в базах данных
rdsadmin и msdb. В частности, примечательна процедура rds_read_error_log,
которая заменяет процедуру xp_readerrorlog. В документации AWS1 приведен
полный список задач и соответствующих хранимых процедур.
В консоли AWS есть несколько инструментов для быстрого мониторинга. Мы
рассмотрим инструменты CloudWatch и Performance Insights. Еще один инструмент, Enhanced Monitoring, собирает данные о потреблении памяти и хранилища
в виртуальном экземпляре. Это важный источник информации, но его сведения
довольно просты, поэтому в этом разделе основное внимание уделяется первым
двум инструментам.

CloudWatch
CloudWatch предоставляет данные об общем потреблении ресурсов на уровне
гипервизора, включая нагрузку ЦП, пропускную способность ввода/вывода
и сети, а также использование хранилища (рис. 16.9). Высокий уровень потребления ресурсов указывает на то, что экземпляр перегружен и его следует
расширить или отрегулировать.
Обратите внимание на метрики ввода/вывода: производительность хранилища
часто становится главным узким местом в облаке. Это особенно важно, если
вы оснащаете экземпляр RDS дисками общего назначения (General Purpose).
У этого типа хранилища предусмотрена резервная мощность, которая позволяет
компенсировать всплески рабочей нагрузки ввода/вывода. Однако после исчерпания резервной мощности производительность диска сильно лимитируется,
из-за чего возникают периодические проблемы с производительностью в SQL
Server. Подумайте о том, чтобы перейти на подготовленные диски IOPS, которые
стоят дороже, но обеспечивают предсказуемую производительность.
1

https://oreil.ly/TnlSI

Amazon SQL Server RDS  491

Рис. 16.9. Метрики CloudWatch

Performance Insights
Performance Insights — это очень основательный инструмент мониторинга,
предоставляющий информацию по нескольким направлениям. Прежде всего
он отображает некоторые счетчики производительности ОС и SQL Server
(рис. 16.10), подобно утилите PerfMon.
На втором графике представлена подробная информация о нагрузке базы
данных в зависимости от времени. Данные можно группировать по типу
ожидания, инструкциям SQL, а также по пользователю и узлу, как показано
на рис. 16.11.
Третий график — еще одна проекция нагрузки базы данных — отображает самые
ресурсоемкие запросы к базе (рис. 16.12), самые популярные ожидания и самых
активных пользователей.

492  Глава 16. SQL Server в облаке

Рис. 16.10. Performance Insights: метрики производительности

Рис. 16.11. Performance Insights: информация о нагрузке базы данных
В целом Performance Insights по своим возможностям находится на одном уровне
с утилитами базового мониторинга SQL Server. Этот инструмент полезен для
быстрого устранения неполадок, когда нужно оценить общее состояние сервера
и выявить наименее производительные запросы.
В RDS прекрасно работают стандартные методы устранения неполадок SQL
Server. Можно запрашивать динамические административные представления,

Google Cloud SQL  493
использовать хранилище запросов и применять расширенные события точно
так же, как в обычных экземплярах SQL Server. В качестве целевого объекта
расширенных событий можно указывать event_file; при этом данные будут
сохраняться в папку D:\RDSDATA\LOG.

Рис. 16.12. Performance Insights: самые ресурсоемкие запросы

Google Cloud SQL
Аналогично AWS и Microsoft Azure, Google тоже предлагает службы управляемых баз данных на своей платформе. Google SQL поддерживает SQL Server
примерно так же, как Amazon RDS и управляемые экземпляры Azure SQL,
предоставляя виртуализированный экземпляр SQL Server, на котором можно
создавать несколько баз данных.
Google Cloud Platform поддерживает высокую доступность за счет резервного
экземпляра SQL Server в другой зоне доступности и репликации на уровне
хранилища. При этом не возникает накладных расходов на репликацию групп
доступности AlwaysOn, но хранилище реплицируется синхронно, что может
привести к задержке фиксации, как и у других поставщиков. В Enterprise Edition
также можно подготовить доступные для чтения реплики, в которых на внутреннем уровне используются группы доступности AlwaysOn.
Google официально начал поддерживать SQL Server в своем семействе облачных
служб SQL в 2021 году, и его набор инструментов для мониторинга и устранения
неполадок был еще недостаточно зрелым по состоянию на декабрь 2021 года,
когда писались эти строки. Инструменты мониторинга Google Cloud Platform
довольно просты и ограничены базовыми метриками потребления ресурсов (см.
рис. 16.13). Тем не менее я надеюсь, что со временем мониторинг будет развиваться и «дозреет» до уровня других технологий баз данных, предоставляемых
этой платформой.

494  Глава 16. SQL Server в облаке

Рис. 16.13. Графики потребления ресурсов Google Cloud SQL
В Google Cloud SQL можно использовать сторонние инструменты мониторинга,
а можно устранять неполадки с помощью собственных функций SQL Server, таких как динамические административные представления и хранилище запросов.
Однако у этой службы есть критическое ограничение: у учетной записи администратора, предоставленной платформой, нет прав на создание расширенных
событий или трассировок SQL.

Резюме
Все ведущие поставщики облачных услуг позволяют запускать SQL Server на
облачных виртуальных машинах и предлагают управляемые службы баз данных,
которые обеспечивают высокую доступность, резервное копирование и обслуживание SQL Server и ОС. На внутреннем уровне все эти системы используют
ядро SQL Server и позволяют применять те же методы настройки и устранения
неполадок, что и на обычных локальных экземплярах SQL Server.
Каждая служба предоставляет доступ к облачной консоли, чтобы отслеживать
потребление ресурсов виртуальной машины SQL Server или управляемых экземпляров базы данных. Интенсивное потребление ресурсов приведет к лимити-

Резюме  495
рованию производительности, которое можно преодолеть, если отрегулировать
или нарастить систему.
Обращайте особое внимание на производительность подсистемы хранения.
Для рабочих нагрузок баз данных используйте хранилище с гарантированной
производительностью. Если вы запускаете SQL Server в облачной виртуальной
машине, попробуйте создать в ОС массивы RAID-0, что может обеспечить лучшее соотношение цены и производительности, чем отдельные диски.
Независимо от поставщика убедитесь, что приложения устойчивы и способны
обрабатывать случайные ошибки. Сбои обязательно будут происходить, и приложения должны уметь восстанавливаться после них.
В системах, распределенных по нескольким регионам, учитывайте задержку
межрегиональной связи. Не используйте синхронную репликацию между регионами. Запускайте пакетные операции T-SQL, чтобы уменьшить количество
межрегиональных вызовов, и не используйте доступные для чтения вторичные
реплики, если вам нужны актуальные данные.

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

Еще раз благодарю вас за то, что прочли эту книгу! Надеюсь, она вам понравилась. SQL Server — отличная технология, а устранение неполадок и тонкая
настройка — один из самых увлекательных и полезных аспектов нашей работы.
Для меня было удовольствием и честью писать эту книгу для вас.

ПРИЛОЖЕНИЕ

Типы ожиданий

В этом приложении перечислены наиболее распространенные типы ожиданий,
которые встретятся вам при устранении неполадок. Я опишу условия, при которых могут произойти ожидания, а также дам общие советы по устранению
неполадок.
Это приложение не заменяет документацию Microsoft1 или библиотеку типов
ожиданий SQLSkills2. Но думаю, что этот справочный материал хорошо дополняет книгу.

ASYNC_IO_COMPLETION
Ожидание ASYNC_IO_COMPLETION происходит во время асинхронных операций
ввода/вывода, не связанных с буферным пулом. Вот его распространенные
причины:
Обычные контрольные точки.
Внутренние контрольные точки, которые возникают, когда вы запускаете
резервное копирование базы данных или DBCC CHECKDB.
Чтение страниц глобальной карты распределения (GAM) из файлов данных.
Чтение страниц данных из базы данных во время ее резервного копирования (к сожалению, информация о среднем времени ожидания при этом искажается, что затрудняет анализ).
Заметный процент ожиданий ASYNC_IO_COMPLETION может быть признаком перегруженной подсистемы ввода/вывода, особенно если он проявляется вместе
с другими ожиданиями, связанными с вводом/выводом. Устраните неисправности системы ввода/вывода (см. ожидания PAGEIOLATCH далее в этом приложении).
Дополнительная информация приведена в главе 3.
1

https://oreil.ly/6oto3

2

https://oreil.ly/Wt5Mx

Приложение. Типы ожиданий  497

ASYNC_NETWORK_IO
Ожидание ASYNC_NETWORK_IO возникает, когда SQL Server ждет, пока клиент
примет данные. Три самые распространенные причины этого ожидания таковы:
Медленная сеть — например, когда клиенты подключены к серверу баз данных, размещенному в другом дата-центре или в облаке.
Клиентские приложения, работающие на недостаточно мощных виртуальных машинах или на перегруженных серверах.
Неправильно спроектированное приложение, когда клиент обрабатывает
данные построчно.
Это ожидание не всегда означает, что проблема на стороне сервера. Тем не менее
неэффективное взаимодействие с клиентом потребляет рабочие потоки и другие
ресурсы SQL Server. Когда наблюдается значительный процент таких ожиданий,
стоит выполнить устранение неполадок.
Этапы устранения неполадок:
Проверьте производительность сети на сервере, просматривая счетчики
производительности сети и другие доступные ключевые индикаторы производительности (KPI).
Найдите клиентские приложения, которые генерируют ожидания, с помощью представлений sys.dm_os_waiting_tasks, sys.dm_exec_requests, sys.
dm_exec_sessions и sys.dm_exec_connections (можно использовать листинг 2.3). Для краткосрочного профилирования используйте расширенное
событие wait_completed с захватом действий sqlserver.client_host_name
и sqlserver.client_app_name.
Проанализируйте код приложения и производительность серверов приложений.
Дополнительная информация приведена в главах 13 и 16.

BACKUPBUFFER
См. тип ожидания BACKUPIO.

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

498  Приложение. Типы ожиданий
Если наблюдаются ожидания, связанные с резервным копированием, устраняйте
неполадки производительности процесса резервного копирования. Проанализируйте производительность диска и сетевой подсистемы. Попробуйте внедрить
сжатие резервных копий и/или резервное копирование с чередованием, а также
отрегулировать параметры резервного копирования.
Дополнительная информация приведена в главе 13.

BTREE_INSERT_FLOW_CONTROL
Ожидание BTREE_INSERT_FLOW_CONTROL указывает на существование индексов
с постоянно увеличивающимися ключами, которые создают «горячие точки».
Это ожидание заменяет некоторые ожидания PAGELATCH, когда вы включаете
параметр индекса OPTIMIZE_FOR_SEQUENTIAL_KEY.
Ознакомьтесь с примечаниями по устранению неполадок для ожиданий
PAGELATCH, а также с дополнительной информацией в главе 10.

CXCONSUMER
Ожидания CXPACKET, CXCONSUMER и EXCHANGE возникают во время выполнения
запросов с параллельными планами выполнения. См. тип ожидания CXPACKET
и подробные объяснения в главе 6.

CXPACKET
Ожидания CXPACKET, CXCONSUMER и EXCHANGE возникают во время выполнения
запросов с параллельными планами выполнения. Эти ожидания совершенно
нормальны и должны присутствовать, особенно в системах с рабочими нагрузками хранения данных и/или отчетов. Однако в системах OLTP чрезмерное
количество ожиданий параллелизма может создавать неприятности.
Ожидания параллелизма сами по себе не являются проблемой: они всего
лишь признак ресурсоемких запросов, которые используют планы параллельного выполнения. В системах OLTP такие запросы нужно обнаружить (см.
главу 4) и оптимизировать (см. главу 5). Настройте параметры параллелизма.
Не устанавливайте параметр MAXDOP=1, потому что это просто замаскирует
проблему.
Дополнительная информация приведена в главе 6.

DIRTY_PAGE_TABLE_LOCK
Ожидания DIRTY_PAGE_TABLE_LOCK, DPT_ENTRY_LOCK, PARALLEL_REDO_FLOW_CONTROL
и PARALLEL_REDO_TRAN_TURN могут возникать на доступных для чтения вторичных
узлах в группах доступности AlwaysOn.

Приложение. Типы ожиданий  499
Большое количество таких ожиданий может сигнализировать о проблемах с процессом параллельного повтора. Когда это происходит, ожидания наблюдаются
в представлениях sys.dm_os_waiting_tasks и sys.dm_exec_requests и сопровождаются повышенной нагрузкой ЦП и ростом очереди повтора. Единственный
способ борьбы с этой проблемой, известный мне на данный момент, — применить
исправления SQL Server и/или отключить параллельный повтор с помощью
флага трассировки T3459.
Дополнительная информация приведена в главе 12.

DPT_ENTRY_LOCK
См. тип ожидания DIRTY_PAGE_TABLE_LOCK.

EXCHANGE
Ожидания CXPACKET, CXCONSUMER и EXCHANGE возникают во время выполнения
запросов с параллельными планами выполнения. См. тип ожидания CXPACKET
и подробные объяснения в главе 6.

HADR_GROUP_COMMIT
Ожидание HADR_GROUP_COMMIT происходит, когда первичный узел группы доступности AlwaysOn пытается оптимизировать производительность репликации,
группируя вместе несколько фиксационных записей журнала перед тем, как
отправить их на вторичные узлы.
Это ожидание редко создает проблемы. Если высокий процент этого типа ожидания начинает влиять на пропускную способность системных транзакций,
нужно устранить неполадки с производительностью репликации AlwaysOn (см.
ожидание HADR_SYNC_COMMIT). Можно также отключить групповую фиксацию
с помощью флага трассировки T9546.
Дополнительная информация приведена в главе 12.

HADR_SYNC_COMMIT
Ожидание HADR_SYNC_COMMIT происходит, когда первичный узел группы доступности AlwaysOn ждет, пока синхронные вторичные узлы закрепят записи
журнала транзакций. Эти ожидания всегда присутствуют в группах доступности,
использующих синхронную репликацию.
Проанализируйте среднюю продолжительность этих ожиданий. Она должна
быть как можно меньше: не более нескольких миллисекунд, если синхронные
реплики размещены в том же дата-центре, что и первичный узел. Если наблюдаются большие значения, проанализируйте производительность репликации,

500  Приложение. Типы ожиданий
обращая внимание на задержку и пропускную способность сети, производительность диска и общую нагрузку на синхронные реплики.
Транзакции остаются активными, и на них удерживаются все блокировки до тех
пор, пока первичный узел не получит подтверждение того, что запись журнала
транзакций COMMIT закреплена на синхронных вторичных узлах. Таким образом,
большая продолжительность ожидания HADR_SYNC_COMMIT может усугублять
блокирование в системе.
Дополнительная информация приведена в главе 12.

HTBUILD
Ожидания HTBUILD, HTDELETE, HTMEMO, HTREINIT и HTREPARTITION возникают во
время управления внутренними хеш-таблицами в пакетном режиме выполнения. В средах с индексами columnstore эти ожидания могут говорить о плохом
обслуживании индексов columnstore, когда образуются большие разностные
хранилища и/или группы строк неравномерного размера. Проанализируйте
состояние групп строк с помощью представления sys.column_store_row_group
и при необходимости перестройте индексы.
Дополнительная информация приведена в главе 13.

HTDELETE, HTMEMO, HTREINIT и HTREPARTITION
См. тип ожидания HTBUILD.

IO_COMPLETION
Тип ожидания IO_COMPLETION возникает во время синхронного чтения и записи
в файлы данных, а также во время некоторых операций чтения в журнале транз­
акций. Вот несколько примеров:
Чтение страниц карты распределения из базы данных.
Чтение журнала транзакций во время восстановления базы данных.
Запись данных в tempdb во время переноса при сортировке.
Заметный процент ожиданий IO_COMPLETION может указывать на перегруженную
подсистему ввода/вывода, особенно если при этом наблюдаются и другие ожидания, связанные с вводом/выводом. Устраните неисправности системы ввода/
вывода (см. ожидание PAGEIOLATCH). Обратите особое внимание на задержку
и пропускную способность tempdb. Плохая производительность tempdb — одна
из наиболее частых причин этого ожидания.
Дополнительная информация приведена в главах 3 и 9.

Приложение. Типы ожиданий  501

LATCH_*
Ожидания, имена которых начинаются с LATCH_, вызываются кратковременными блокировками, не связанными с буферным пулом. SQL Server генерирует
разные ожидания LATCH в зависимости от типа кратковременных блокировок
(совмещаемые, монопольные и т. д.).
Чтобы получить статистику и подробности о кратковременных блокировках,
а также анализировать узкие места, можно использовать представление sys.
dm_os_latch_stats.
Дополнительная информация приведена в главе 10.

LCK_M_*
Ожидания, имена которых начинаются с LCK_M_, происходят во время блокирования. Каждому типу блокировки в SQL Server соответствует тип ожидания.
Проанализируйте, когда используется тот или иной тип блокировки, и определите и устраните основную причину блокирования.
В этом приложении подробно описаны ожидания LCK_M_I*, LCK_M_R*, LCK_M_S,
LCK_M_SCH_M, LCK_M_SCH_S, LCK_M_U и LCK_M_X.
Дополнительная информация приведена в главе 8.

LCK_M_I*
SQL Server устанавливает интентные блокировки на уровне объекта (таблицы)
и страницы. На уровне таблицы ожидания LCK_M_I* обычно происходят в двух
случаях:
Несовместимость с блокировками модификации схемы. В этом случае
обычно также наблюдаются ожидания блокировки схемы (LCK_M_SCH_S
и LCK_M_SCH_M). Подробнее см. ожидание LCK_M_SCH_M.
Полная несовместимая блокировка на уровне таблицы, удерживаемая другим сеансом. Обычно это происходит из-за укрупнения блокировки или изза кода, использующего указания TABLOCK и TABLOCKX. Для устранения неполадок с укрупнением блокировки можно использовать Blocking Monitoring
Framework и представление sys.dm_index_operational_stats (см. главу 14).
Ожидания интентных блокировок, возникающие из-за блокирования на уровне
страницы, обычно говорят о неоптимизированных запросах и просмотрах. Анализируйте отдельные случаи блокирования, чтобы найти их основную причину.
Дополнительная информация приведена в главе 8.

502  Приложение. Типы ожиданий

LCK_M_R*
Типы ожидания LCK_M_R* указывают на ожидание блокировки диапазона. SQL
Server устанавливает такие блокировки на уровне изоляции SERIALIZABLE. Еще
одна возможная причина ожиданий — некластеризованные индексы, для которых установлен параметр IGNORE_DUP_KEY=ON.
Увидев эти ожидания, определите и устраните основную причину блокиро­
вания. Избегайте параметра IGNORE_DUP_KEY=ON в некластеризованных индексах. Не используйте уровень изоляции SERIALIZABLE без крайней необходимости.
Дополнительная информация приведена в главе 8.

LCK_M_S
Тип ожидания LCK_M_S указывает на ожидание совмещаемых блокировок (S).
Этот тип блокировки устанавливается запросами SELECT на уровнях изоляции
READ COMMITTED, REPEATABLE READ и SERIALIZABLE. Неоптимизированные запросы
SELECT — самый распространенный источник этих ожиданий.
Если наблюдается большое количество таких ожиданий, сосредоточьтесь на оптимизации запросов (глава 5). Когда запросы выполняются на уровне изоляции
READ COMMITTED, попробуйте включить параметр базы данных READ_COMMITTED_
SNAPSHOT, чтобы устранить блокирование между операциями чтения и записи.
(Правда, это лишь скроет проблему с неоптимизированными запросами, а не
решит ее.)
Дополнительная информация приведена в главе 8.

LCK_M_SCH_M
Тип ожидания LCK_M_SCH_M указывает на ожидание блокировок модификации
схемы (Sch-М), когда сеансы не могут получить монопольные блокировки для
объектов.
Если встречаются эти ожидания, проанализируйте свою стратегию развертывания для изменений базы данных. Также обратите внимание на стратегии
обслуживания индексов и разделов; эти операции устанавливают блокировки
таблиц. Во время перестроения индекса и переключения разделов попробуйте
использовать низкоприоритетные блокировки, если эта функция поддерживается. Наконец, проанализируйте отдельные случаи блокирования, чтобы
определить основную причину проблемы.
Посмотрите, как часто возникают эти ожидания. Они могут быть вызваны однократными событиями и ошибками во время развертывания.
Дополнительная информация приведена в главе 8.

Приложение. Типы ожиданий  503

LCK_M_SCH_S
Тип ожидания LCK_M_SCH_S указывает на ожидание блокировки стабильности
схемы (Sch-S). Этот тип блокирования возникает во время модификации схемы,
когда другие сеансы удерживают блокировки модификации схемы (Sch-M) на
объектах.
См. стратегию устранения неполадок типа ожидания LCK_M_SCH_M и подробные
объяснения в главе 8.

LCK_M_U
Тип ожидания LCK_M_U указывает на ожидание блокировок обновления (U). SQL
Server устанавливает их во время просмотра обновлений, обычно вызванного
неоптимизированными запросами записи (UPDATE, DELETE, MERGE). Во многих
случаях это сопровождается типами ожидания PAGEIOLATCH* и CXPACKET.
Если наблюдается много таких ожиданий, сосредоточьтесь на оптимизации
запросов (глава 5).
Дополнительная информация приведена в главе 8.

LCK_M_X
Тип ожидания LCK_M_X указывает на ожидание монопольных блокировок (X).
Распространенные причины таких ожиданий — искусственные точки сериализации (таблицы счетчиков), злоупотребление уровнями изоляции REPEATABLE READ
и SERIALIZABLE, неэффективное управление транзакциями и длительные транз­
акции, а также указания блокировки на уровне таблицы, такие как (TABLOCKX).
Если наблюдается много таких ожиданий, проанализируйте отдельные случаи
блокирования, чтобы определить основную причину проблемы.
Дополнительная информация приведена в главе 8.

LOGBUFFER
Ожидание LOGBUFFER возникает, когда SQL Server ожидает доступный буфер
журнала, чтобы внести записи журнала. Обычно это ожидание идет в паре
с ожиданием WRITELOG и указывает на недостаточную пропускную способность
журнала транзакций. См. ожидание WRITELOG.
Дополнительная информация приведена в главах 3 и 11.

OLEDB
Ожидание OLEDB происходит, когда SQL Server ожидает данных от поставщика
OLEDB. Чаще всего оно наблюдается в таких случаях:

504  Приложение. Типы ожиданий
Вызовы на связанные серверы.
Выполнение некоторых пакетов SQL Server Integration Services (SSIS).
Операции во время выполнения DBCC CHECKDB.
Запросы к динамическим административным представлениям.
Эти ожидания не всегда являются проблемой, но нужно понять, чем они вызваны
и насколько сильно влияют на систему.
Дополнительная информация приведена в главе 13.

PAGEIOLATCH*
Ожидания PAGEIOLATCH* возникают, когда SQL Server считывает страницы
данных с диска. Эти ожидания очень распространены и наблюдаются в большинстве систем. Существует шесть типов ожидания PAGEIOLATCH, которые
относятся к разным видам операций со страницами буферного пула. Анализируйте ожидания всех типов PAGEIOLATCH совместно, чтобы оценить их
совокупное влияние.
Чрезмерное количество ожиданий PAGEIOLATCH показывает, что SQL Server
постоянно считывает данные с диска. Обычно это происходит в двух случаях:
недостаточное аппаратное обеспечение SQL Server, когда активные данные
не помещаются в памяти;
неоптимизированные запросы, которые просматривают ненужные данные,
сбрасывая содержимое буферного пула на диск.
Можно выполнить перекрестную проверку данных, изучив счетчик производительности Page Life Expectancy, который показывает, как долго страницы
данных остаются в буферном пуле.
Значительный процент ожиданий PAGEIOLATCH всегда требует устранения неполадок. Эти ожидания не обязательно создают проблемы для клиентов, особенно
при использовании флеш-накопителей с малой задержкой, однако рост данных
может привести к тому, что дисковая подсистема перестанет справляться, и тогда
проблема очень быстро затронет всю систему.
Как устранить проблему:
Проверьте производительность и задержку дисковой подсистемы с помощью представления sys.dm_io_virtual_file_stats (листинг 3.1).
Проверьте, не вызвана ли высокая задержка всплесками активности ввода/
вывода, проанализировав метрики производительности SQL Server и ОС.
При необходимости отрегулируйте процесс контрольной точки.

Приложение. Типы ожиданий  505
Проанализируйте весь стек ввода/вывода и найдите узкие места.
Определите и отрегулируйте запросы с интенсивным вводом/выводом
(главы 4 и 5).
Проанализируйте метрики индекса (глава 14), если нужно обнаружить индексы, которые порождают большую часть этих ожиданий, и/или определите самых активных потребителей буферного пула.
Дополнительная информация приведена в главах 3 и 15.

PAGELATCH
Ожидания PAGELATCH указывают на блокировки, связанные с буферным пулом,
которые происходят, когда потокам необходимо одновременно получить доступ к данным (или страницам карт распределения) или модифицировать их.
Две основные причины таких ожиданий — состязания на системных страницах
tempdb и «горячие точки» в постоянно растущих индексах.
Когда наблюдаются эти ожидания, разберитесь, что их вызывает. Чтобы проверить, связаны ли они с tempdb, проанализируйте столбец wait_resource в представлении sys.dm_os_waiting_tasks. Также можно перехватить расширенное
событие sqlserver.latch_suspend_end. Устраняя проблемы с кратковременными
блокировками в tempdb, убедитесь, что tempdb настроена правильно, уменьшите
нагрузку на нее и попробуйте включить метаданные tempdb, оптимизированные
для памяти, если такая функция поддерживается.
Индексы с «горячими точками» можно найти с помощью функции sys.dm_db_
index_operational_stats. В SQL Server 2019 и более поздних версиях можно
уменьшить состязания, включив настройку индекса OPTIMIZE_FOR_SEQUENTIAL_
KEY. Также можно провести рефакторинг схемы, внедрить секционирование
хешей или использовать OLTP в памяти.
Дополнительная информация приведена в главах 9 и 10.

PARALLEL_REDO_FLOW_CONTROL
См. тип ожидания DIRTY_PAGE_TABLE_LOCK.

PARALLEL_REDO_TRAN_TURN
См. тип ожидания DIRTY_PAGE_TABLE_LOCK.

PREEMPTIVE_OS_ACCEPTSCURITYCONTEXT
См. типы ожидания PREEMPTIVE_OS_AUTH*.

506  Приложение. Типы ожиданий

PREEMPTIVE_OS_AUTH*
Ожидания с именами, начинающимися с PREEMPTIVE_OS_AUTH, PREEMPTIVE_OS_
LOOKUPACCOUNTSID и PREEMPTIVE_OS_ACCEPTSCURITYCONTEXT, происходят во время
аутентификации пользователей.
Если наблюдается значительный процент таких ожиданий, проверьте производительность контроллеров Active Directory. Убедитесь, что SQL Server не
выполняет аутентификацию на контроллерах Active Directory в удаленных
дата-центрах.
Также проверьте, не используется ли в коде контекст EXECUTE AS OWNER или
EXECUTE AS USER, который инициирует вызовы аутентификации.
Дополнительная информация приведена в главе 13.

PREEMPTIVE_OS_LOOKUPACCOUNTSID
См. типы ожидания PREEMPTIVE_OS_AUTH*.

PREEMPTIVE_OS_WRITEFILE
Ожидание PREEMPTIVE_OS_WRITEFILE может быть признаком узкого места во
время синхронной записи в файлы. Обнаружив эти ожидания, проверьте, не
выполняет ли сервер несколько трассировок или аудитов SQL, используя
файлы как целевые объекты для сохранения данных. Также проверьте наличие
моментальных снимков базы данных — это другая причина таких ожиданий.
Дополнительная информация приведена в главе 13.

PREEMPTIVE_OS_WRITEFILEGATHER
Ожидание PREEMPTIVE_OS_WRITEFILEGATHER происходит во время процесса инициализации нулями. Когда в системе возникает это ожидание, проверьте и включите мгновенную инициализацию файла, предоставив учетной записи SQL
Server разрешение Perform Volume Management Tasks (SE_MANAGE_VOLUME_NAME).
Изучите параметры автоувеличения журнала транзакций и убедитесь, что нет
процессов, которые регулярно сокращают журнал.
Дополнительная информация приведена в главе 13.

QDS*
Семейство типов ожидания, имена которых начинаются с QDS, связано с хранилищем запросов. Они могут быть признаком накладных расходов из-за сбора
данных хранилища запросов. Ожидания QDS_PERSIST_TASK_MAIN_LOOP_SLEEP
и QDS_ASYNC_QUEUE можно игнорировать — они безвредные.

Приложение. Типы ожиданий  507
Если наблюдаются другие ожидания QDS, проверьте настройки хранилища запросов. Не используйте режим сбора данных QUERY_CAPTURE_MODE=ALL. Уменьшите
размер хранилища запросов, если оно слишком велико. Также включите флаги
трассировки T7745 и T7752.
Дополнительная информация приведена в главе 4.

RESOURCE_SEMAPHORE
Ожидание RESOURCE_SEMAPHORE происходит, когда запросы ожидают предоставления памяти. Этот тип ожидания всегда стоит изучать, если он становится
заметен в системе.
Обнаружив это ожидание, проанализируйте использование памяти в SQL
Server. Проверьте, как клерки используют память и нет ли признаков внешней
и внутренней нехватки памяти. В виртуализированных средах проверьте поведение драйвера накачки. Убедитесь, что сервер адекватно оснащен, а SQL Server
правильно настроен под рабочую нагрузку.
Ожидания RESOURCE_SEMAPHORE также могут вызываться отдельными запросами. Проанализируйте объемы предоставленной памяти с помощью представления sys.dm_exec_query_memory_grants и при необходимости оптимизируйте
запросы.
Дополнительная информация приведена в главе 7.

RESOURCE_SEMAPHORE_QUERY_COMPILE
Ожидание RESOURCE_SEMAPHORE_QUERY_COMPILE происходит, когда SQL Server не
хватает памяти для компиляции запросов. Как и в случае с RESOURCE_SEMAPHORE,
необходимо изучить проблему.
Проверьте, не выполняет ли SQL Server слишком много компиляций. Для
этого просмотрите счетчики производительности SQL Compilations/sec и SQL
Recompilations/sec. Сократите количество компиляций за счет параметризации
запросов (глава 6). Выполните общее устранение неполадок, связанных с памятью, как описано в статье об ожидании RESOURCE_SEMAPHORE.
Я также замечал, что ожидание RESOURCE_SEMAPHORE_QUERY_COMPILE возникает,
когда очень активная таблица удерживает блокировку (Sch-M) во время длительного процесса автономного перестроения индекса. Одновременно с этим SQL
Server пытается перекомпилировать большое количество запросов, обращающихся к этой таблице. Компиляции блокируются и в конце концов потребляют
огромное количество памяти, из-за чего остальным заявкам на компиляцию
приходится ждать с этим типом ожидания.
Дополнительная информация приведена в главе 7.

508  Приложение. Типы ожиданий

THREADPOOL
Ожидание THREADPOOL возникает, когда у SQL Server нет доступных рабочих
процессов для обслуживания запросов пользователей. Это опасное ожидание,
которое необходимо исследовать.
Чаще всего оно возникает в следующих случаях:
неправильная настройка параметра max worker threads;
недостаточный объем памяти SQL Server (проверьте конфигурацию ОС
и SQL Server);
длинные цепочки блокирования;
чрезмерная нехватка памяти;
большое количество подключенных клиентов;
нагрузка с чрезмерным количеством запросов с параллельными планами
выполнения.
В облачных средах эти ожидания могут указывать на то, что база данных работает на недостаточно оснащенном узле, который не справляется с нагрузкой.
Дополнительная информация приведена в главах 13 и 16.

WRITE_COMPLETION
Ожидание WRITE_COMPLETION возникает во время операций синхронной записи
в файлы данных и журналов. По моему опыту, чаще всего это происходит при
создании снимков базы данных.
Когда в системе возникает это ожидание, проверьте, не создаются ли снимки
базы. Помните, что некоторые внутренние процессы, такие как DBCC CHECKDB,
создают внутренние снимки базы.
Когда ожидание WRITE_COMPLETION наблюдается вместе с другими ожиданиями,
связанными с вводом/выводом, следует устранить неполадки системы ввода/
вывода (см. ожидания PAGEIOLATCH).
Дополнительная информация приведена в главе 3.

WRITELOG
Ожидание WRITELOG возникает, когда SQL Server вносит записи журнала в журнал транзакций. Это ожидание нормально в любой системе, но его значительный
процент и/или большое среднее время ожидания могут быть признаком узкого
места в журнале транзакций. Ожидание WRITELOG часто наблюдается в паре
с ожиданием LOGBUFFER, а оно тоже является признаком узкого места.

Приложение. Типы ожиданий  509
Чтобы устранить проблему, проанализируйте среднее время ожидания и задержку записи журнала транзакций с помощью представления sys.dm_io_virtual_
file_stats (листинг 3.1). Высокие значения этих метрик могут повлиять на
пропускную способность системы. Устраните неполадки с производительностью журнала транзакций (глава 11) и системы ввода/вывода (см. ожидания
PAGEIOLATCH).
Дополнительная информация приведена в главах 3 и 11.

Об авторе

Дмитрий Короткевич — Microsoft Data Platform MVP и Microsoft Certified Master
(MCM) с многолетним опытом работы в сфере IT. Он имел дело с Microsoft SQL
Server в качестве разработчика приложений и баз данных, администратора
и архитектора баз данных. Дмитрий специализируется на проектировании, разработке и наладке сложных систем OLTP, которые круглосуточно обрабатывают
тысячи транзакций в секунду. Сейчас он возглавляет группу по обслуживанию
баз данных проекта Chewy.com, дает консультации по SQL Server и обучает
работе с ним клиентов по всему миру.
Дмитрий регулярно выступает на различных мероприятиях по SQL Server.
Он ведет блог на aboutsqlserver.com, иногда пишет в Twitter @aboutsqlserver,
и с ним можно связаться по адресу dk@aboutsqlserver.com.

Иллюстрация на обложке

На обложке изображен кроличий сыч (Athene cunicularia). Название рода
Athene — это имя древнегреческой богини Афины, одним из традиционных
атрибутов которой является сова. Кроличий сыч невелик, но его ноги длиннее,
чем у большинства сов. Эти птицы встречаются в Северной и Южной Америке
и живут в норах на открытых площадках, а не на деревьях, как большинство сов.
Существует 16 подвидов кроличьего сыча. Внешне они немного различаются,
однако у большинства из них коричневые головы и крылья с белыми пятнами,
характерные белые брови и белый «нагрудник». Грудь и брюшко совы тоже белые
с различными коричневыми узорами в зависимости от подвида. Молодые особи
выглядят примерно так же, но без белых пятен. Самцы и самки похожи внешне,
но самцы светлее, потому что они проводят больше времени на открытом воздухе в течение дня и оперение выгорает на солнце.
Кроличьи сычи часто гнездятся в норах, выкопанных сусликами. Если найти
нору не удается, но почва достаточно мягкая, сыч выкапывает нору сам. Эти сычи
обычно создают пары на всю жизнь. Самка откладывает от 4 до 12 яиц в кладке
и высиживает их от 3 до 4 недель. После того как птенцы вылупятся, их кормят
оба родителя. Сычи питаются в основном мелкимигрызунами и крупными насекомыми. В отличие от других сов, они также едят фрукты и семена.
Кроличий сыч считается вымирающим видом в Канаде и находится под угрозой
исчезновения в Мексике и некоторых штатах США. Однако он широко распространен в открытых районах Южной Америки, где сохранность этого вида
не вызывает опасений. Многие животные, изображенные на обложках изданий
O’Reilly, находятся под угрозой исчезновения, и все они важны для мира.
Иллюстрацию на обложке выполнила Карен Монтгомери на основе старинной
ксилографии из «Жизни животных» Брема.

Дмитрий Короткевич
SQL Server. Наладка и оптимизация для профессионалов
Перевел с английского Д. Павлов

Руководитель дивизиона
Ю. Сергиенко
Руководитель проекта
А. Питиримов
Ведущий редактор
Е. Строганова
Научный редактор
Р. Чебыкин
Художественный редактор
В. Мостипан
Корректоры
С. Беляева, Т. Никифорова
Верстка
Л. Егорова

Изготовлено в России. Изготовитель: ООО «Прогресс книга».
Место нахождения и фактический адрес: 194044, Россия, г. Санкт-Петербург,
Б. Сампсониевский пр., д. 29А, пом. 52. Тел.: +78127037373.
Дата изготовления: 05.2023. Наименование: книжная продукция. Срок годности: не ограничен.
Налоговая льгота — общероссийский классификатор продукции ОК 034-2014, 58.11.12 — Книги печатные
профессиональные, технические и научные.
Импортер в Беларусь: ООО «ПИТЕР М», 220020, РБ, г. Минск, ул. Тимирязева, д. 121/3, к. 214, тел./факс: 208 80 01.
Подписано в печать 28.03.23. Формат 70×100/16. Бумага офсетная. Усл. п. л. 41,280. Тираж 700. Заказ 0000.