Open BlackWrangler opened 6 years ago
Искал решение проблемы долгих запросов, если в выдаче имеется поле LineNumbers.ProtocolId
:
https://github.com/GolosMovement/StatElectGenerator/issues/86#issuecomment-440567953
Проблема была в том, что СУБД сканировала всю таблицу LineNumbers
(использовался clustered index scan, т.е. проход по таблице, как она располагается на носителе - по PK) не используя созданный специально для этого nonclustered индекс на ProtocolId
. Поэтому запросы хоть как-то связанные с ProtocolId
были медленные. Чтобы такого не было при joined запросах, нужно дополнительно создавать nonclustered index
на поле join + include на ProtocolId
:
CREATE NONCLUSTERED INDEX [IX_LineNumbers_LineDescriptionId_ProtocolIdValue]
ON [dbo].[LineNumbers]([LineDescriptionId] ASC)
INCLUDE ([ProtocolId],[Value])
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF,
ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
Value
добавил для оптимизации запросов вычисления динамических пресетов.
В итоге, провел тесты на dev машине и на staging. Входные данные такие:
staging:
Protocols: 1 291 308
LineNumbers: 23 127 818
dev:
Protocols: 2 094 299
LineNumbers: 42 389 640
Тестировал на Президент РФ 2018
, QA#3. Вычисляемая формула взята с "потолка", вычисляет что-то с большим количеством LineDescription
. Можно поправить на реальную, но сути это не изменит. Выборка по всем протоколам выборов.
SELECT
valuesQuery.*,
(valuesQuery.[529] +
valuesQuery.[530] -
valuesQuery.[531] +
valuesQuery.[532] +
valuesQuery.[533] -
valuesQuery.[534] +
valuesQuery.[535] -
valuesQuery.[536]) / (NULLIF(valuesQuery.[526], 0.0) * 1.33) AS Result
FROM (
SELECT
baseQuery.ProtocolId,
MAX(CASE WHEN baseQuery.LineDescriptionId = 526 THEN baseQuery.Value END) AS [526],
MAX(CASE WHEN baseQuery.LineDescriptionId = 529 THEN baseQuery.Value END) AS [529],
MAX(CASE WHEN baseQuery.LineDescriptionId = 530 THEN baseQuery.Value END) AS [530],
MAX(CASE WHEN baseQuery.LineDescriptionId = 531 THEN baseQuery.Value END) AS [531],
MAX(CASE WHEN baseQuery.LineDescriptionId = 532 THEN baseQuery.Value END) AS [532],
MAX(CASE WHEN baseQuery.LineDescriptionId = 533 THEN baseQuery.Value END) AS [533],
MAX(CASE WHEN baseQuery.LineDescriptionId = 534 THEN baseQuery.Value END) AS [534],
MAX(CASE WHEN baseQuery.LineDescriptionId = 535 THEN baseQuery.Value END) AS [535],
MAX(CASE WHEN baseQuery.LineDescriptionId = 536 THEN baseQuery.Value END) AS [536]
FROM (
SELECT
LineNumbers.LineDescriptionId,
LineNumbers.ProtocolId,
LineNumbers.Value
FROM LineNumbers
WHERE LineNumbers.LineDescriptionId in (526, 529, 530, 531, 532, 533, 534, 535, 536)
) AS baseQuery
GROUP BY baseQuery.ProtocolId
) AS valuesQuery
ORDER BY valuesQuery.ProtocolId
-- Elapsed time: 00:00:01.3906250
SELECT
baseQuery.*,
(baseQuery.[529] +
baseQuery.[530] -
baseQuery.[531] +
baseQuery.[532] +
baseQuery.[533] -
baseQuery.[534] +
baseQuery.[535] -
baseQuery.[536]) / (NULLIF(baseQuery.[526], 0.0) * 1.33) AS Result
FROM (
SELECT ProtocolId,
[526], [529], [530], [531], [532], [533], [534], [535], [536]
FROM
(
SELECT LineNumbers.Value,
LineNumbers.LineDescriptionId,
LineNumbers.ProtocolId
FROM LineNumbers
WHERE LineNumbers.LineDescriptionId in (526, 529, 530, 531, 532, 533, 534, 535, 536)
) AS sourceQuery
PIVOT
(
MAX(Value) FOR LineDescriptionId IN ([526], [529], [530], [531], [532], [533], [534], [535], [536])
) AS pivotQuery
) AS baseQuery
ORDER BY baseQuery.ProtocolId
-- Elapsed time: 00:00:01.3437500
SELECT
valuesQuery.*,
(valuesQuery.[219] +
valuesQuery.[220] -
valuesQuery.[221] +
valuesQuery.[222] +
valuesQuery.[223] -
valuesQuery.[224] +
valuesQuery.[225] -
valuesQuery.[226]) / (NULLIF(valuesQuery.[216], 0.0) * 1.33) AS Result
FROM (
SELECT
baseQuery.ProtocolId,
MAX(CASE WHEN baseQuery.LineDescriptionId = 216 THEN baseQuery.Value END) AS [216],
MAX(CASE WHEN baseQuery.LineDescriptionId = 219 THEN baseQuery.Value END) AS [219],
MAX(CASE WHEN baseQuery.LineDescriptionId = 220 THEN baseQuery.Value END) AS [220],
MAX(CASE WHEN baseQuery.LineDescriptionId = 221 THEN baseQuery.Value END) AS [221],
MAX(CASE WHEN baseQuery.LineDescriptionId = 222 THEN baseQuery.Value END) AS [222],
MAX(CASE WHEN baseQuery.LineDescriptionId = 223 THEN baseQuery.Value END) AS [223],
MAX(CASE WHEN baseQuery.LineDescriptionId = 224 THEN baseQuery.Value END) AS [224],
MAX(CASE WHEN baseQuery.LineDescriptionId = 225 THEN baseQuery.Value END) AS [225],
MAX(CASE WHEN baseQuery.LineDescriptionId = 226 THEN baseQuery.Value END) AS [226]
FROM (
SELECT
LineNumbers.LineDescriptionId,
LineNumbers.ProtocolId,
LineNumbers.Value
FROM LineNumbers
WHERE LineNumbers.LineDescriptionId in (216, 219, 220, 221, 222, 223, 224, 225, 226)
) AS baseQuery
GROUP BY baseQuery.ProtocolId
) AS valuesQuery
ORDER BY valuesQuery.ProtocolId
-- Elapsed time: 00:00:18.3564453
SELECT
baseQuery.*,
(baseQuery.[219] +
baseQuery.[220] -
baseQuery.[221] +
baseQuery.[222] +
baseQuery.[223] -
baseQuery.[224] +
baseQuery.[225] -
baseQuery.[226]) / (NULLIF(baseQuery.[216], 0.0) * 1.33) AS Result
FROM (
SELECT ProtocolId,
[216], [219], [220], [221], [222], [223], [224], [225], [226]
FROM
(
SELECT LineNumbers.Value,
LineNumbers.LineDescriptionId,
LineNumbers.ProtocolId
FROM LineNumbers
WHERE LineNumbers.LineDescriptionId in (216, 219, 220, 221, 222, 223, 224, 225, 226)
) AS sourceQuery
PIVOT
(
MAX(Value) FOR LineDescriptionId IN ([216], [219], [220], [221], [222], [223], [224], [225], [226])
) AS pivotQuery
) AS baseQuery
ORDER BY ProtocolId
-- Elapsed time: 00:00:15.8750000
Запросы по иерархии с 97699 записей с динамическими пресетами работают 1-2 секунды на dev машине (40 миллионов записей в LineNumbers
). На staging (а значит и production) это длится 15-20 секунд (23 миллиона записей в LineNumbers
). Нужно использовать метод PIVOT, т.к. на staging он быстрее, когда как на dev разницы нет.
Возможно, на реальном приложении запросы будут еще быстрее, т.к. время на непосредственную передачу данных между приложением и БД будет меньше.
В любом случае, azure db сейчас работает на порядок медленнее чем dev машины на которых идет разработка. Без оптимизационного индекса для ProtocolId
запросы MAX-CASE/PIVOT занимают 10-11 секунд на dev машине, на staging их невозможно будет дождаться.
Реализация пресетов несложна, если мы обуславливаемся использовать в формуле NULLIF
функцию для выражения в знаменателе, чтобы отсеять ошибку деления на 0. Так нам не придется писать парсер формул, достаточно будет просто подменять идентификаторы LineDescription
как это происходит сейчас.
В итоге формулы станут, например, выглядеть так: ([555] + [999]) / NULLIF([123] + [321], 0) * 100.0
.
Прекрасно. Теперь у нас есть ещё один путь к динамическим пресетам. Изящнее чем в моих экспериментах. Скаттер-плот по скорости не хуже.
Скорость Пара комментариев по скорости. Гистограмма у меня строилась на 10 секунд дольше. Интересно, как это будет в методах Артёма.
Какое-то время занимает построение иерархии (когда нужно выбрать не все данные, а нескольких регионов). У меня это входило в эксперименты.
Без выбора иерархии, т.е. по всем данным ProtocolSet, если создана расчётная таблица, как в моих экспериментах запрос из метода Артёма длится 5 секунд на Azure
Сравнение идей по динамическим пресетам Предложение Артёма + Изящно, не создается много постоянных таблиц - Какое-то место займут дополнительные индексы (м.б. они и так нужны) - Довольно сложные запросы
Расчётные обычные таблицы - Неизящно, создаются таблицы на каждый Preset. + Простая работа в дальнейшем (простые запросы, скорость)
Расчётные глобальные временные таблицы - Надо ещё понять, как долго она может жить + Плюсы обоих методов
Каким путём идти
Это главный вопрос. 2 ноября я протестировал хранимые пресеты. Функционал не сработал. https://github.com/GolosMovement/StatElectGenerator/issues/86 Поэтому я стал ковыряться сам и предлагать считать налету. Не потому, что я хотел заставить выполнить двойную работу, а потому что мне кажется сложный участок можно обойти. По крайней мере, запросы, на первый взгляд, корректно выполняют этот функционал. Я помню, сколько было работы с ошибками импорта, и опасался того же при хранимых пресетах (это очень похоже на импорт).
Поэтому я и начал предлагать считать пресеты налету и пробовал запросы. Чтобы было меньше работы, а не больше.
Если вы решите, что проще заставить работать хранимые - ок. Так изначально ставилась задача. Если вы решите, что проще опираться на запросы (или сделать то же своими методами) - ок. Я понимаю, что это дополнительная работа, и предлагал сократить функционал следующих модулей до минимума. Т.е. взять старые скаттер-плот и гистограмм-генератор и переключить их на пресеты. Как с GAB. Мы уже сможем показать практически полезный функционал, хотя и старый.
Поэтому я и спрашивал каким путём вам проще идти
Как вам лучше, так давайте и двигаться. Но двигаться надо. С 2 ноября прошло 20 дней, а пресеты никак пока не работают. 9 декабря выборы в Армении, где просят показать работу на практике. GAB у меня просят каждый день уже неделю. Скоро 3 месяца с российских выборов, когда мы обещали показать какую-то работу, а мы до сих пор не можем показать ничего (LAB держит глюк по Швеции).
Давайте двигаться, все пути открыты.
Нужно довести скорость работы prod/staging до уровня скорости работы dev машины. Это большой тормоз из-за которого мы не функционалом занимаемся, а делаем преждевременную оптимизацию. Я об этом уже говорил и приводил примеры из других СУБД (PostgreSQL, синтетический тест, а также реальный проект). Те проблемы, которые мы имеем сейчас на данных в 20 млн на prod, мы на dev получим на данных 200 млн, а то и больше.
На dev, судя по execution plan
, используется параллелизация:
https://www.brentozar.com/pastetheplan/?id=SkbVX4tCQ
Тот же самый запрос на staging выглядит так (распараллеливания нет): https://www.brentozar.com/pastetheplan/?id=Sy-uQ4tRQ
Единственное, что приходит на ум, судя по плану.
Также, MSSQL/Azure имеет некую особенность в работе (index + include, IX_LineNumbers_LineDescriptionId_ProtocolIdValue
) по сравнению с другими СУБД, об этом написал выше в тесте. За темами обсуждения можно следить тут:
Если вы решите, что проще заставить работать хранимые - ок. Так изначально ставилась задача. Если вы решите, что проще опираться на запросы (или сделать то же своими методами) - ок. Я понимаю, что это дополнительная работа, и предлагал сократить функционал следующих модулей до минимума.
Мы это усно обсудили и без динамических пресетов дальнейшие модули работать не будут вообще (агрегация, count(1) и т.п.). Соответственно, нужно отказаться от рассчитанных заранее пресетов.
Т.е. взять старые скаттер-плот и гистограмм-генератор и переключить их на пресеты. Как с GAB. Мы уже сможем показать практически полезный функционал, хотя и старый.
Поэтому я и спрашивал каким путём вам проще идти
- Исправить ошибки функционала на хранимых величинах
Была ошибка связанная с обработкой NULL значений, о которой я предупреждал тут 09.11
(зачем делать двойную работу, если мы на тот момент решали вопрос о динамических пресетах?):
https://github.com/GolosMovement/StatElectGenerator/issues/27#issuecomment-437416286
Сделано это было из-за вот этой задачи: https://github.com/GolosMovement/StatElectGenerator/issues/87
Также была ошибка, которая связана с повторяющимися идентификаторами LineDescription
в формулах, которые не были предусмотрены мной заранее: https://github.com/GolosMovement/StatElectGenerator/issues/95
Обе ошибки исправлены.
Сам функционал (модуль GBDV) доступен с 01.11
:
https://github.com/GolosMovement/StatElectGenerator/issues/80#ref-commit-685aec0
Расчет пресетов сделали 30.10
:
https://github.com/GolosMovement/StatElectGenerator/issues/50#event-1934284932
- Сделать на запросах или как-нибудь ещё.
Как вам лучше, так давайте и двигаться. Но двигаться надо. С 2 ноября прошло 20 дней, а пресеты никак пока не работают. 9 декабря выборы в Армении, где просят показать работу на практике. GAB у меня просят каждый день уже неделю. Скоро 3 месяца с российских выборов, когда мы обещали показать какую-то работу, а мы до сих пор не можем показать ничего (LAB держит глюк по Швеции).
Давайте двигаться, все пути открыты.
Да, давайте 🚀
С нуля сделал работающий прототип по самому пока сложному: histogram generator.
https://egsandbox.azurewebsites.net/histogramgenerator.aspx
Работает на временных глобальных таблицах.
Временная таблица, если её нет, начинает подгружаться при задании выборов асинхронным запросом.
При анализе также проверяется на её наличие, и подгружается в случае отсутствия (где-то 20-30 сек.)
Формулы введены по Польше, Франции, России.
Работает шустро.
Время засекает с момента процедуры подготовки и отправки запроса до получения данных от sql сервера.
2 тысячи бинов - 3 сек (вся Россия, Президентские РФ 2018) На 10 тысячах бинов - 14 сек Франция, 13 сек или 36 сек (если временая отвалилась) по всей России, 20 тысяч - 35 сек (вся Россия)
На 100 тысячах бинов были 84, 88 сек (Россия, 11 регионов), по всей России - вылетает после простоя (таймаут?).
На миллион бинов строит 13 сек (вся Франция).
На миллион бинов - вылетает по всей России.
Нужно учитывать простоту решения для будущих наворотов (посторить 2 гистограммы одновременно. Гистограммы на 10 тысяч бинов. Это обычная практика в анализе).
Иногда вылетает (таймаут?). Нужно тут же перезагрузить. Не было времени настроить.
Само кодирование работы с данными заняло не более 2 часов. От формирования запроса до вывода.
Часов 8 - чтение учебников по программированию и интерфейс (всё забыл :( )
Узкие места: Асинхронный запрос. Что будет, если пользователь начнёт анализ, пока таблица не сформировалась? (она мгновенно показывается как существующая, если запущено её формирование). Стабильность временных таблиц. Но остается возможность реальных хранимых расчётных таблиц.
Нужно ещё посмотреть производительность запросов Артёма, но понять их мне не хватает знаний.
Функция PIVOT строит простую и удобную таблицу за 50 секунд для 100 000 записей. Таблица может быть global temporary или обычной. Разница только в "##" ('CalcTbl' + convert(nvarchar,@PrSetID) или ('##CalcTbl' + convert(nvarchar,@PrSetID)
Если global temporary будет быстро умирать, можно пользоваться постоянной. Размер - 10 Мб для самых больших выборов. Индексов, кажется, нет.
С помощью этой расчетной таблицы, как временной, так и реальной scatter-plot рассчитывается за 15 секунд, histogram - 24 секунды на 100 000 записей. Большая часть времени уходит на построение иерархии.
Это примитивными SQL query кусками найдеными в Интернете. Наверно, RDM сможет лучше. Формулы, кажется, абсолютно аналогичны задаваемыми при настройке пресетов.
Построение расчетной таблицы можно сделать гибким, не зависящим от конкретных выборов. Был бы только ProtocolSetID (в скрипте - @PrSetID)
Скрипт для построения global temporary:
Для постоянной: убрать ## и использовать
IF OBJECT_ID('CalcTbl' + convert(nvarchar,@PrSetID), 'U') IS NULL
Получается табличка типа:
Скаттер-плот (15 секунд)
Гистограмма (20-24 секунды)
Всё это исследование заняло 3 часа.