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) и тд.
Поделиться с друзьями
-->

Комментарии (36)


  1. ggrnd0
    05.04.2017 14:53
    +2

    Oracle очень легко пережёвывает запрос с 20 join и 5 подзапросами с группировкой как в подзапросах так и на верхнем уровне на бизнес данных за 15лет (в некоторых таблицах до 30+ млн срок)...


    Если БД захлебывается, то либо ей не хватает оперативки, либо индексов.


    А по приведенному запросу, если он и проседает, то из-за огромного количества OR в блоке where…
    Так например, MySQL не будет использовать индексы при обработке фильтра where вообще.


    1. Dm1tr1ch
      05.04.2017 15:08

      Я лично не сравнивал оба оптимизатора Oracle и SQL Server, но не раз слышал от коллег по Oracle негативные отзывы о их оптимизаторе. Опыт работы таких специалистов — интегратор и более 10 лет с Oracle, что не должно вызывать вопросы в их компетенции.

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

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


      1. bormotov
        05.04.2017 16:59
        +1

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

        После чтения такой книги, очень странно слышать слова «негативные отзывы от специалиста с 10 лет работы».


        1. Dm1tr1ch
          05.04.2017 17:06

          По SQL Server то есть есть книги на тему оптимизатора, с примерами и рекомендациями, но это же не значит что оптимизатор будет всегда прав, это просто невозможно (можно просто банально плохо написать запрос и никакой оптимизатор не поможет).


          1. bormotov
            05.04.2017 17:13
            +2

            Планировщик запроса — это просто алгоритм. Он может быть плохим, конечно. Книжка которую я читал про оракловый, охватывала версии оракла 8, 9, 10, и автор прям наглядно показывал, как планировщику от версии к версии добавляют мозгов, и ситуации, когда эти «более умные» мозги принимают решения хуже, чем было.

            Меня удивляет отношение человека, с опытом 10 лет.

            В моём понимании, «планировщик запроса» — это такая данность, как восход Солнца. И довольно глупо ругать Солнце, что оно утром светит в окно и мешает спать. Самое простое — добавить к «окно на восток», хорошие светонепроницаемые шторы.

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




            Кстати, отдельный вопрос — когда вообще нужно собирать результаты из 20-ти таблиц? Можно хотя0бы намекнуть на прикладную область, и что за данные такого рода запросом вытаскивали?


            1. Dm1tr1ch
              05.04.2017 17:18

              Согласен по поводу данности оптимизатора.

              Что касается прикладной области, то в моём случае приложением организовано как 1С, из тучи «динамических» таблиц, с похожим названием, но проблема была даже не в этом. Проблема была в том, что генератор запросов на каждую галочку не добавлял параметров к уже существующим соединениям, а писал ещё 1 JOIN. То есть я мог наставить галочек и получить 10 JOIN с одной и той же таблицей и в каждом JOIN я делал ограничение по своему столбцу.


              1. knagaev
                07.04.2017 16:51

                «Динамические» таблицы — это боль для RDBMS.
                Как говорил мой знакомый, «по такой структуре БД хорошо диссертацию писать, но работать оно не будет».


            1. varagian
              05.04.2017 17:48
              +3

              Сколько интриги и метафор о книге про оптимизатор, а где же название-то? Общественность интересуется.


              1. Dm1tr1ch
                05.04.2017 18:05
                +3

                Одну из них, бесплатную, можно скачать вот тут — http://www.red-gate.com/products/sql-development/sql-prompt/entrypage/sql-query-optimizer-ebook

                У bormotov, то же можете поинтересоваться про книгу по Oracle, если необходимо.


                1. knagaev
                  07.04.2017 16:47
                  +1

                  Ещё про оптимизатор Оракла полезно почитать книги мегагуру Тома Кайта — в разделе Библиография.
                  Он, кстати, очень не любит выкручивать руки оптимизатору хинтами и убеждён, что во многом проблемы производительности решаются правильной архитектурой БД.
                  И документацию Oracle Database 12c Release 2 Performance однозначно стоит почитать.

                  P.S. для меня тоже было странно услышать о таком мнении специалиста 10+ об оптимизаторе Oracle…


              1. bormotov
                05.04.2017 19:03
                +3

                про оракловый оптимизатор вообще никаких интриг:

                Дж. Льюис, ORACLE: Основы стоимостной оптимизации.

                На русском издавали, например «Питер», аккурат десять лет назад.


              1. knagaev
                07.04.2017 16:48

                Немножко промахнулся с ответом — хотел сюда ответить, пожалуйста, читайте мой комментарий


            1. Mistrall
              07.04.2017 09:58

              Я могу не намекнуть, я могу пальцем показать: оборотно-сальдовая ведомость в 1С Бухгалтерия.
              При «правильных» настройках планировщик Oracle разворачивает запрос её формирования более, чем в 1000 join из 50+ таблиц.


    1. VolCh
      05.04.2017 18:15

      Возможно, группировки сильно ограничивают количество вариантов.


  1. darthunix
    05.04.2017 16:17
    +1

    Я так и не понял из статьи, как именно MS SQL находит баланс между стоимостью плана и лимитом по времени перебора. N! вариантов — это хорошо для объяснения на пальцах, но вряд ли именно так все работает. У того же PostgreSQL помимо стандартного планировщика запросов есть ещё модуль генетической оптимизации. На последнем pgconf показывали модуль адаптивной оптимизации, который учитывал предыдущий опыт построения планов по схожим запросам. Наверняка в MS SQL все не менее наворечено и можно тоже что-то понастраивать с вычислением стоимости планов на большом количестве соединений. А просто сказать, что когда много таблиц, то планировщик начинает работать не очень… не информативно.


    1. Dm1tr1ch
      05.04.2017 16:30
      -1

      Как именно работает оптимизатор — это тема отдельной статьи, возможно я продолжу эту тему в новой статье.


      1. darthunix
        05.04.2017 17:52
        +2

        Понимаете, просто само название статьи и начала абзацев вроде

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

        настраивают на то, что на примере запроса из начала статьи будет разобран алгоритм построения плана и дана общая теория по специфике работы планировщика MS SQL. А закончится статья тем, что автор победил проблему с учётом вышеописанных знаний. По итогу же имеем «Вот запрос, нагенерированный 1С на 20 таблиц с повторами. Он строится иногда долго. Почему? Много таблиц в соединении плохо, так как эн-факториал. Вывод — не делайте много таблиц и уважайте планировщик». Ну такому уровню статей не место на Хабре, простите.


        1. Dm1tr1ch
          05.04.2017 18:06
          -2

          А так же я написал

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


          1. darthunix
            05.04.2017 18:18
            +2

            Хорошо, спрошу максимально прямо — о чем эта статья и что из нееследует вынести после прочтения?


            1. Dm1tr1ch
              05.04.2017 22:23
              -1

              В первую очередь то, что выделил в статье курсивом

              В данном вопросе очень важно понять — возможное количество соединений таблиц растёт по экспоненте, а не линейно


              И вынес в раздел «вывод»

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


              Для других — что оптимизатор это более сложная часть SQL Server и что о нём надо как минимум задумываться

              Возможно кто-то даже решит заняться изучением оптимизатора основательно. Если хоть 1 человек примет такое решение — статья написана не зря.


  1. alexhott
    05.04.2017 18:52

    да уж такой запрос на базе больше 10 строк это смерть.
    если не секрет что за приложение такое нарисовало?

    конструкция с in() обычно отрабатывается как loop что не способствует производительности

    ну и план запроса неплохо бы в студию

    а вообще любой планировщик бессилен против кривых рук


    1. kxl
      05.04.2017 19:36

      точно не 1С, там нет таких id… но очень похоже…


  1. mnv
    06.04.2017 00:38
    +3

    "Запросы с JOIN выполняются долго" — это наверное такая мантра, я часто это слышал. Но это далеко не всегда так. Даже если джойнятся большие таблицы. Даже если в запросе используется много джойнов.


    Проблемы как правило либо в отсутствии нужных индексов, либо в наличии таких препятствий как OR и IN в условиях запроса, либо попытка обработки слишком большого объема данных в запросе. Или если понятно, что запрос будет отдавать одинаковые данные как с LEFT JOIN так и с INNER JOIN, то последний вариант может оказаться радикально эффективнее. Да что тут говорить, у каждого запроса в каждой СУБД могут быть свои особенности и этих особенностей не мало.


    В приведенном запросе оптимизатор может нормально зацепиться разве что за условия


    AND "pr"."area" IN (700) 
    AND "pr"."deleted_by_user"=0 
    AND "pr"."temporary" = 0

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


  1. musicriffstudio
    06.04.2017 08:42
    +3

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


    неправильный вывод.

    В запросе много IN и ORб данные многократно повторяются, а потом обрезаются DISTINCT.

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

    Попросите специалиста создать нормальную (BCNF) структуру таблиц в новой базе и сливайте туда данные для анализа.

    Количество JOIN в запросах, вероятно, увеличится в разы, а скорость выполнения (что естественно) уменьшится в сотни раз.

    Это стандартный подход.

    Да, в очередной раз порекомендую https://habrahabr.ru/company/mailru/blog/266811/


    1. Dm1tr1ch
      06.04.2017 09:43
      -2

      Сам запрос, приведён как яркий пример из моей жизни, чтобы можно было на основе реальных данных показать что бывает. На самом деле проблемы у этого запроса не в количестве JOIN, но он был показателен, в каких ситуациях возможны проблемы. В любой СУБД бывают моменты, когда разбиение запроса на части даёт существенный прирост в производительности. Так вот, суть вывода в том, что следует взять на вооружение, что если не помогает обычная оптимизация, то, возможно, поможет разбиение запроса. Я не предлагаю думать о проблемах количества JOIN ранее, чем с этим есть проблемы.


      1. musicriffstudio
        06.04.2017 09:46
        +2

        в примере явно нет никакой оптимизации и данные не нормализованы. Вывод неверный.


        1. Dm1tr1ch
          06.04.2017 09:47
          -1

          Ок, у меня нет цели вас переубеждать.

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


          1. musicriffstudio
            06.04.2017 09:53
            +2

            там буквально каждая строка в примере запроса приведёт к проблемам


  1. Vlad_fox
    06.04.2017 12:18

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


  1. OlegZH
    06.04.2017 14:27

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

    И… что это за точки в тексте запроса:

    "pr"."id" = "ufref3758_i16"."request"
    Я немного отстал от жизни и мне требуется пояснить синтаксис.


    1. vlivyur
      07.04.2017 16:18

      «Псевдоним таблицы».«Имя её колонки»


      1. knagaev
        07.04.2017 16:40

        Наверно лучше всё-таки «Псевдоним таблицы».«Имя её поля».
        Колонки — это в экселе :)


        1. VolCh
          07.04.2017 17:54

          "Псевдоним отношения"."Имя его атрибута" :) Таблицы — это в экселе, в реляционных СУБД — отношения :)


          1. knagaev
            07.04.2017 17:59

            Это уже в реляционной алгебре, а не в живых RDBMS :)


        1. vlivyur
          10.04.2017 09:59

          Ничего не знаю. В SSMS есть Tables и Columns.


  1. AlexBin
    07.04.2017 17:50
    +3

    Позволите мне рассказать анекдот про вашу статью?

    Чапаев: А чего вся квартира в тараканах?
    Петька: А это я диссертацию пишу. О тараканах.
    Чапаев: Ну, расскажи.
    Петька: Берем таракана, ставим его на стол и кричим: Беги! И он бежит! Отрываем ему ноги и кричим: Беги! Но он не бежит. Делаем вывод: таракан без ног не слышит!