Здесь, в Treatwell, мы не столько любим реляционные базы данных, сколько уважаем и восхищаемся ими за то, что они последовательно (атомарно, изолированно и надолго) справляются с нескончаемым потоком сложных вопросов, которые мы им задаем. Хотя мы предпочитаем PostgreSQL для новых сервисов и находимся в процессе перехода на него для более старых, по историческим причинам у нас все еще есть много MSSQL, и, вероятно, еще какое-то время так будет. Это не означает, что мы незнакомы с нереляционными базами данных - с точки зрения ЦП и ОЗУ мы используем гораздо больше Elasticsearch и Redis, чем реляционные базы данных, - но мы твердо уверены, что как проверенные временем и боевыми решениями решения проблем с сохраняемостью , они заслуживают важное место в нашей архитектуре.

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

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

appointment_event - это настоящая таблица, содержащая более полумиллиарда строк. Подзапрос возвращает 11 строк, что похоже на «счастливый путь» на протяжении жизненного цикла встречи. Он также быстро возвращается - ‹1 мс. Почему же тогда весь запрос занял так много времени?

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

Приблизительные строки 4814410 - WTF !? С некоторым отрывом от фактического числа 11. Подробно изучив план выполнения, мы увидели, что оценки по ряду операций были весьма далекими. Сосредоточившись на appointment_event, мы увидели, что оценка для запросов, подобных этому, тоже была неверной:

Предполагаемое количество строк: 776, фактических - 11.

Зная, что SQL Server (и большинство СУБД) использует статистику для определения наиболее эффективного способа выполнения запроса, казалось, что статистика была «отключена» каким-то (основным) образом.

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

AVG_RANGE_ROWS должно быть средним числом строк на значение ключа, в данном случае средним числом appointment_event строк на встречу. Очевидно, именно отсюда планировщик запросов получил оценку в 776 строк, но определенно это не могло быть правильным. Мы выполнили несколько запросов, чтобы узнать, каково реальное распределение значений в этом диапазоне, и ответили примерно 12. Так откуда же взялось 776?

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

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

Продолжая аналогию с телефонной книгой, просматривая больше страниц, вы получите все более точное представление о реальном распределении имен. То же самое и с нашей статистикой событий встреч - оказывается, что коэффициент выборки 0,17%, который MSSQL считал разумным, недостаточно для наших целей. Решение нашей проблемы (в некоторой степени разочаровывающее) заключалось в простом увеличении коэффициента выборки до 1% для этого индекса, что по-прежнему приводит к неточной статистике (оценка ~ 45 для AVG_RANGE_ROWS), но этого достаточно для наших целей и не слишком вычислительно дорого.