Почему много JOIN в запросе это плохо или не мешайте оптимизатору

в 11:30, , рубрики: dba, developer, MS Sql Server, mssql, sql server, t-sql, Администрирование баз данных
image

Недавно столкнулся с одним приложением, которое генерировало запросы к БД. Я понимаю, что этим никого не удивишь, но когда приложение стало тормозить и мне пришло задание разобраться в чём причина, я был сильно удивлён, обнаружив эти запросы. Вот с чем иногда приходится иметь дело SQL Server:

SELECT COUNT(DISTINCT "pr"."id") FROM  ((((((((((((((((("SomeTable" "pr"
LEFT OUTER JOIN "SomeTable1698" "uf_pr_id_698" ON "uf_pr_id_698"."request" = "pr"."id") 
LEFT OUTER JOIN "SomeTable1700" "ufref3737_i2" ON "ufref3737_i2"."request" = "pr"."id") 
LEFT OUTER JOIN "SomeTable1666" "x0" ON "x0"."request" = "ufref3737_i2"."f6_callerper")
LEFT OUTER JOIN "SomeTable1666" "uf_ufref4646_i3_f58__666" ON "uf_ufref4646_i3_f58__666"."request" = "ufref3737_i2"."f58_")
LEFT OUTER JOIN "SomeTable1694" "x1" ON "x1"."request" = "ufref3737_i2"."f38_servicep")
LEFT OUTER JOIN "SomeTable3754" "ufref3754_i12" ON "pr"."id" = "ufref3754_i12"."request")
LEFT OUTER JOIN "SomeTable1698" "uf_ufref3754_i12_reference_698" ON "uf_ufref3754_i12_reference_698"."request" = "ufref3754_i12"."reference")
LEFT OUTER JOIN "SomeTable1698" "x2" ON "x2"."request" = "ufref3737_i2"."f34_parentse")
LEFT OUTER JOIN "SomeTable4128" "ufref3779_4128_i14" ON "ufref3737_i2"."f34_parentse" = "ufref3779_4128_i14"."request")
LEFT OUTER JOIN "SomeTable1859" "x3" ON "x3"."request" = "ufref3779_4128_i14"."reference")
LEFT OUTER JOIN "SomeTable3758" "ufref3758_i15" ON "pr"."id" = "ufref3758_i15"."request")
LEFT OUTER JOIN "SomeTable1698" "uf_ufref3758_i15_reference_698" ON "uf_ufref3758_i15_reference_698"."request" = "ufref3758_i15"."reference")
LEFT OUTER JOIN "SomeTable3758" "ufref3758_i16" ON "pr"."id" = "ufref3758_i16"."request")
LEFT OUTER JOIN "SomeTable4128" "ufref3758_4128_i16" ON "ufref3758_i16"."reference" = "ufref3758_4128_i16"."request")
LEFT OUTER JOIN "SomeTable1859" "x4" ON "x4"."request" = "ufref3758_4128_i16"."reference")
LEFT OUTER JOIN "SomeTable4128" "ufref4128_i17" ON "pr"."id" = "ufref4128_i17"."request")
LEFT OUTER JOIN "SomeTable1859" "uf_ufref4128_i17_reference_859" ON "uf_ufref4128_i17_reference_859"."request" = "ufref4128_i17"."reference")
LEFT OUTER JOIN "SomeTable1666" "uf_ufref4667_i25_f69__666" ON "uf_ufref4667_i25_f69__666"."request" = "uf_pr_id_698"."f69_"
WHERE ("uf_pr_id_698"."f1_applicant" IN (248,169,180,201,203,205,209,215,223,357,371,379,3502,3503,3506,3514,3517,3531,3740,3741)
OR "x0"."f24_useracco" IN (578872,564618,565084,566420,566422,566936,567032,567260,567689,579571,580813,594452,611522,611523,615836,621430,628371,633044,634132,634136)
OR "uf_ufref4646_i3_f58__666"."f24_useracco" IN (578872,564618,565084,566420,566422,566936,567032,567260,567689,579571,580813,594452,611522,611523,615836,621430,628371,633044,634132,634136)
OR "uf_ufref4667_i25_f69__666"."f24_useracco" IN (578872,564618,565084,566420,566422,566936,567032,567260,567689,579571,580813,594452,611522,611523,615836,621430,628371,633044,634132,634136)
OR ("uf_pr_id_698"."f10_status" Is Null OR "uf_pr_id_698"."f10_status" <> 111)  AND "ufref3737_i2"."f96_" = 0   AND (("ufref3737_i2"."f17_source"  Is Null OR "ufref3737_i2"."f17_source"  <> 566425)
AND ("ufref3737_i2"."f17_source"  Is Null OR "ufref3737_i2"."f17_source"  <> 566424)  OR ("uf_pr_id_698"."f10_status" Is Null OR "uf_pr_id_698"."f10_status" <> 56) )
AND ("uf_pr_id_698"."f12_responsi" IN (578872,564618,565084,566420,566422,566936,567032,567260,567689,579571,580813,594452,611522,611523,615836,621430,628371,633044,634132,634136)
OR "x1"."f19_restrict" IN (578872,564618,565084,566420,566422,566936,567032,567260,567689,579571,580813,594452,611522,611523,615836,621430,628371,633044,634132,634136)
OR "uf_ufref3754_i12_reference_698"."f12_responsi" IN (578872,564618,565084,566420,566422,566936,567032,567260,567689,579571,580813,594452,611522,611523,615836,621430,628371,633044,634132,634136)
OR "x2"."f12_responsi" IN (578872,564618,565084,566420,566422,566936,567032,567260,567689,579571,580813,594452,611522,611523,615836,621430,628371,633044,634132,634136)
OR "x3"."f5_responsib" IN (578872,564618,565084,566420,566422,566936,567032,567260,567689,579571,580813,594452,611522,611523,615836,621430,628371,633044,634132,634136) 
OR "uf_ufref3758_i15_reference_698"."f12_responsi" IN (578872,564618,565084,566420,566422,566936,567032,567260,567689,579571,580813,594452,611522,611523,615836,621430,628371,633044,634132,634136) 
OR "x4"."f5_responsib" IN (578872,564618,565084,566420,566422,566936,567032,567260,567689,579571,580813,594452,611522,611523,615836,621430,628371,633044,634132,634136)
OR "uf_ufref4128_i17_reference_859"."f5_responsib" IN (578872,564618,565084,566420,566422,566936,567032,567260,567689,579571,580813,594452,611522,611523,615836,621430,628371,633044,634132,634136))
AND ("uf_pr_id_698"."f12_responsi"  Is Null OR "uf_pr_id_698"."f12_responsi"  <> 579420)  ) AND "pr"."area" IN (700) AND "pr"."area" IN (700) AND "pr"."deleted_by_user"=0 AND "pr"."temporary" = 0

Название объектов было изменено.

Больше всего бросается в глаза то, что одна и та же таблица используется множество раз, а количество скобок сводит с ума. Но не только мне не понравился такой код, SQL Server то же не в восторге и тратит много ресурсов на создание плана для него. Запрос может выполняться от 50 до 150 мс, а построение плана может занимать до 2,5 секунд. Сегодня я не буду рассматривать варианты исправления ситуации, скажу только что в моём случае исправить генерацию запроса в приложении было невозможно.

Я бы хотел рассмотреть почему же SQL Server так долго строит план запроса. В любой СУБД, SQL Server не исключение, главным вопросом оптимизации является способ соединения таблиц друг с другом. Кроме непосредственно самого способа соединения, очень важен порядок соединения таблиц.

Давайте поговорим о порядке соединения таблиц подробнее. В данном вопросе очень важно понять — возможное количество соединений таблиц растёт по экспоненте, а не линейно. Например, для 2-х таблиц число возможных вариантов соединения всего 2, для 3-х это число может доходить до 12-и. Разный порядок соединения, может иметь разную стоимость запроса и оптимизатор SQL Server должен выбрать «оптимальный» способ, но при большем количестве таблиц, это становится ресурсоёмкой задачей. В случае если SQL Server начнёт перебирать все возможные варианты, то такой запрос может никогда не выполнится, по этой причине SQL Server этого никогда не делает и всегда ищет «достаточно хороший план», а не «наилучший». SQL Server всегда пытается найти компромисс между временем выполнения и качеством плана.

Вот наглядный пример роста количества вариантов соединения по экспоненте. SQL Server может выбирать разные способы соединения (left-deep, right-deep, bushy trees).

Визуально это выглядит следующим образом:

image

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

image

Вы можете самостоятельно получить эти значения:

Для left-deep: 5! = 5 x 4 x 3 x 2 x 1 = 120

Для bushy tree: (2n–2)!/(n–1)!

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

P.S. Конечно, мы должны понимать, что кроме определения порядка соединения таблиц, оптимизатор запросов должен так же выбрать какой тип соединения использовать, выбрать способ доступа к данным (Scan, Seek) и тд.

Автор: Dm1tr1ch

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js