Разделение SQL-запроса со многими объединениями на более мелкие помогает?

18

Нам нужно каждый вечер составлять отчеты на нашем SQL Server 2008 R2. Расчет отчетов занимает несколько часов. Чтобы сократить время, мы пересчитываем таблицу. Эта таблица создана на основе JOINining 12 довольно больших (десятки миллионов строк) таблиц.

Расчет этой таблицы агрегации занял несколько дней назад около 4 часов. Наш администратор баз данных разделил это большое объединение на 3 меньших объединения (каждое объединяет 4 таблицы). Временный результат каждый раз сохраняется во временной таблице, которая используется при следующем соединении.

Результатом улучшения DBA является то, что таблица агрегации рассчитывается за 15 минут. Я задавался вопросом, как это возможно. DBA сказал мне, что это потому, что количество данных, которые сервер должен обработать, меньше. Другими словами, что при большом оригинальном объединении сервер должен работать с большим количеством данных, чем при суммированных меньших объединениях. Однако я бы предположил, что оптимизатор позаботится о том, чтобы сделать это эффективно с оригинальным большим соединением, разделив объединения самостоятельно и отправив только количество столбцов, необходимое для следующих объединений.

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

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

Итак, вопросы:

  1. Что может вызвать большое улучшение?

  2. Это стандартная процедура для разделения больших объединений на более мелкие?

  3. Действительно ли объем данных, который должен обрабатывать сервер, будет меньше в случае нескольких меньших объединений?

Вот оригинальный запрос:

    Insert Into FinalResult_Base
SELECT       
    TC.TestCampaignContainerId,
    TC.CategoryId As TestCampaignCategoryId,
    TC.Grade,
    TC.TestCampaignId,    
    T.TestSetId
    ,TL.TestId
    ,TSK.CategoryId
    ,TT.[TestletId]
    ,TL.SectionNo
    ,TL.Difficulty
    ,TestletName = Char(65+TL.SectionNo) + CONVERT(varchar(4),6 - TL.Difficulty) 
    ,TQ.[QuestionId]
    ,TS.StudentId
    ,TS.ClassId
    ,RA.SubjectId
    ,TQ.[QuestionPoints] 
    ,GoodAnswer  = Case When TQ.[QuestionPoints] Is null Then 0
                      When TQ.[QuestionPoints] > 0 Then 1 
                      Else 0 End
    ,WrongAnswer = Case When TQ.[QuestionPoints] = 0 Then 1 
                      When TQ.[QuestionPoints] Is null Then 1
                     Else 0 End
    ,NoAnswer    = Case When TQ.[QuestionPoints] Is null Then 1 Else 0 End
    ,TS.Redizo
    ,TT.ViewCount
    ,TT.SpentTime
    ,TQ.[Position]  
    ,RA.SpecialNeeds        
    ,[Version] = 1 
    ,TestAdaptationId = TA.Id
    ,TaskId = TSK.TaskId
    ,TaskPosition = TT.Position
    ,QuestionRate = Q.Rate
    ,TestQuestionId = TQ.Guid
    ,AnswerType = TT.TestletAnswerTypeId
FROM 
    [TestQuestion] TQ WITH (NOLOCK)
    Join [TestTask] TT WITH (NOLOCK)            On TT.Guid = TQ.TestTaskId
    Join [Question] Q WITH (NOLOCK)         On TQ.QuestionId =  Q.QuestionId
    Join [Testlet] TL WITH (NOLOCK)         On TT.TestletId  = TL.Guid 
    Join [Test]     T WITH (NOLOCK)         On TL.TestId     =  T.Guid
    Join [TestSet] TS WITH (NOLOCK)         On T.TestSetId   = TS.Guid 
    Join [RoleAssignment] RA WITH (NOLOCK)  On TS.StudentId  = RA.PersonId And RA.RoleId = 1
    Join [Task] TSK WITH (NOLOCK)       On TSK.TaskId = TT.TaskId
    Join [Category] C WITH (NOLOCK)     On C.CategoryId = TSK.CategoryId
    Join [TimeWindow] TW WITH (NOLOCK)      On TW.Id = TS.TimeWindowId 
    Join [TestAdaptation] TA WITH (NOLOCK)  On TA.Id = TW.TestAdaptationId
    Join [TestCampaign] TC WITH (NOLOCK)        On TC.TestCampaignId = TA.TestCampaignId 
WHERE
    T.TestTypeId = 1    -- eliminuji ankety 
    And t.ProcessedOn is not null -- ne vsechny, jen dokoncene
    And TL.ShownOn is not null
    And TS.Redizo not in (999999999, 111111119)
END;

Новые разделенные соединения после отличной работы DBA:

    SELECT       
    TC.TestCampaignContainerId,
    TC.CategoryId As TestCampaignCategoryId,
    TC.Grade,
    TC.TestCampaignId,    
    T.TestSetId
    ,TL.TestId
    ,TL.SectionNo
    ,TL.Difficulty
    ,TestletName = Char(65+TL.SectionNo) + CONVERT(varchar(4),6 - TL.Difficulty) -- prevod na A5, B4, B5 ...
    ,TS.StudentId
    ,TS.ClassId
    ,TS.Redizo
    ,[Version] = 1 -- ? 
    ,TestAdaptationId = TA.Id
    ,TL.Guid AS TLGuid
    ,TS.TimeWindowId
INTO
    [#FinalResult_Base_1]
FROM 
    [TestSet] [TS] WITH (NOLOCK)
    JOIN [Test] [T] WITH (NOLOCK) 
        ON [T].[TestSetId] = [TS].[Guid] AND [TS].[Redizo] NOT IN (999999999, 111111119) AND [T].[TestTypeId] = 1 AND [T].[ProcessedOn] IS NOT NULL
    JOIN [Testlet] [TL] WITH (NOLOCK)
        ON [TL].[TestId] = [T].[Guid] AND [TL].[ShownOn] IS NOT NULL
    JOIN [TimeWindow] [TW] WITH (NOLOCK)
        ON [TW].[Id] = [TS].[TimeWindowId] AND [TW].[IsActive] = 1
    JOIN [TestAdaptation] [TA] WITH (NOLOCK)
        ON [TA].[Id] = [TW].[TestAdaptationId] AND [TA].[IsActive] = 1
    JOIN [TestCampaign] [TC] WITH (NOLOCK)
        ON [TC].[TestCampaignId] = [TA].[TestCampaignId] AND [TC].[IsActive] = 1
    JOIN [TestCampaignContainer] [TCC] WITH (NOLOCK)
        ON [TCC].[TestCampaignContainerId] = [TC].[TestCampaignContainerId] AND [TCC].[IsActive] = 1
    ;

 SELECT       
    FR1.TestCampaignContainerId,
    FR1.TestCampaignCategoryId,
    FR1.Grade,
    FR1.TestCampaignId,    
    FR1.TestSetId
    ,FR1.TestId
    ,TSK.CategoryId AS [TaskCategoryId]
    ,TT.[TestletId]
    ,FR1.SectionNo
    ,FR1.Difficulty
    ,TestletName = Char(65+FR1.SectionNo) + CONVERT(varchar(4),6 - FR1.Difficulty) -- prevod na A5, B4, B5 ...
    ,FR1.StudentId
    ,FR1.ClassId
    ,FR1.Redizo
    ,TT.ViewCount
    ,TT.SpentTime
    ,[Version] = 1 -- ? 
    ,FR1.TestAdaptationId
    ,TaskId = TSK.TaskId
    ,TaskPosition = TT.Position
    ,AnswerType = TT.TestletAnswerTypeId
    ,TT.Guid AS TTGuid

INTO
    [#FinalResult_Base_2]
FROM 
    #FinalResult_Base_1 FR1
    JOIN [TestTask] [TT] WITH (NOLOCK)
        ON [TT].[TestletId] = [FR1].[TLGuid] 
    JOIN [Task] [TSK] WITH (NOLOCK)
        ON [TSK].[TaskId] = [TT].[TaskId] AND [TSK].[IsActive] = 1
    JOIN [Category] [C] WITH (NOLOCK)
        ON [C].[CategoryId] = [TSK].[CategoryId]AND [C].[IsActive] = 1
    ;    

DROP TABLE [#FinalResult_Base_1]

CREATE NONCLUSTERED INDEX [#IX_FR_Student_Class]
ON [dbo].[#FinalResult_Base_2] ([StudentId],[ClassId])
INCLUDE ([TTGuid])

SELECT       
    FR2.TestCampaignContainerId,
    FR2.TestCampaignCategoryId,
    FR2.Grade,
    FR2.TestCampaignId,    
    FR2.TestSetId
    ,FR2.TestId
    ,FR2.[TaskCategoryId]
    ,FR2.[TestletId]
    ,FR2.SectionNo
    ,FR2.Difficulty
    ,FR2.TestletName
    ,TQ.[QuestionId]
    ,FR2.StudentId
    ,FR2.ClassId
    ,RA.SubjectId
    ,TQ.[QuestionPoints] -- 1+ good, 0 wrong, null no answer
    ,GoodAnswer  = Case When TQ.[QuestionPoints] Is null Then 0
                      When TQ.[QuestionPoints] > 0 Then 1 -- cookie
                      Else 0 End
    ,WrongAnswer = Case When TQ.[QuestionPoints] = 0 Then 1 
                      When TQ.[QuestionPoints] Is null Then 1
                     Else 0 End
    ,NoAnswer    = Case When TQ.[QuestionPoints] Is null Then 1 Else 0 End
    ,FR2.Redizo
    ,FR2.ViewCount
    ,FR2.SpentTime
    ,TQ.[Position] AS [QuestionPosition]  
    ,RA.SpecialNeeds -- identifikace SVP        
    ,[Version] = 1 -- ? 
    ,FR2.TestAdaptationId
    ,FR2.TaskId
    ,FR2.TaskPosition
    ,QuestionRate = Q.Rate
    ,TestQuestionId = TQ.Guid
    ,FR2.AnswerType
INTO
    [#FinalResult_Base]
FROM 
    [#FinalResult_Base_2] FR2
    JOIN [TestQuestion] [TQ] WITH (NOLOCK)
        ON [TQ].[TestTaskId] = [FR2].[TTGuid]
    JOIN [Question] [Q] WITH (NOLOCK)
        ON [Q].[QuestionId] = [TQ].[QuestionId] AND [Q].[IsActive] = 1

    JOIN [RoleAssignment] [RA] WITH (NOLOCK)
        ON [RA].[PersonId] = [FR2].[StudentId]
        AND [RA].[ClassId] = [FR2].[ClassId] AND [RA].[IsActive] = 1 AND [RA].[RoleId] = 1

    drop table #FinalResult_Base_2;

    truncate table [dbo].[FinalResult_Base];
    insert into [dbo].[FinalResult_Base] select * from #FinalResult_Base;

    drop table #FinalResult_Base;
Ондрей Петерка
источник
3
Слово предупреждения - WITH (NOLOCK) является злом - может привести к возвращению неверных данных. Я предлагаю попробовать С (ROWCOMMITTED).
TomTom
1
@TomTom Вы имели в виду READCOMMITTED? Я никогда не видел ROWCOMMITTED раньше.
ypercubeᵀᴹ
4
С (NOLOCK) не зло. Это просто не та волшебная пуля, о которой люди думают. Как и большинство вещей в SQL Server и разработке программного обеспечения в целом, оно имеет свое место.
Зейн
2
Да, но учитывая, что NOLOCK может выдавать предупреждения в журнале и - что более важно - возвращать НЕПРАВИЛЬНЫЕ ДАННЫЕ, я считаю это злом. Это в значительной степени применимо только к таблицам, ГАРАНТИРУЕМЫМ, чтобы не изменять первичный ключ и выбранные ключи во время выполнения запроса. И да, я и READCOMMMITED, извините.
TomTom

Ответы:

11

1 Сокращение «пространства поиска» в сочетании с улучшенной статистикой для промежуточных / поздних объединений.

Мне приходилось иметь дело с объединениями из 90 таблиц (дизайн микки-мауса), когда обработчик запросов отказывался даже создавать план. Разбиение такого объединения на 10 подсоединений по 9 таблиц в каждой значительно снижает сложность каждого объединения, которое растет экспоненциально с каждой дополнительной таблицей. Плюс Оптимизатор запросов теперь рассматривает их как 10 планов, тратя (потенциально) больше времени в целом (у Пола Уайта даже могут быть метрики!).

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

Кроме того, вы можете вначале принудительно выполнять самые выборочные объединения, сокращая объемы данных, перемещаясь вверх по дереву. Если вы можете оценить селективность своих предикатов намного лучше, чем Оптимизатор, почему бы не форсировать порядок соединения. Возможно, стоит поискать «Кустовые планы».

2 На мой взгляд, следует учитывать, важны ли эффективность и производительность

3 Не обязательно, но это может произойти, если самые селективные объединения выполняются рано

Джон Алан
источник
3
+1 Спасибо. Специально для описания вашего опыта. Совершенно верно, говоря это: «Если вы можете оценить селективность своих предикатов намного лучше, чем Оптимизатор, почему бы не форсировать порядок объединения».
Ондрей Петерка
2
Это очень актуальный вопрос на самом деле. Соединение из 90 таблиц может быть принудительно создано для создания плана с помощью параметра «Принудительный порядок». Неважно, что порядок был, вероятно, случайным и неоптимальным, просто сокращения пространства поиска было достаточно, чтобы помочь оптимизатору создать план за пару секунд (без намека на то, что он истечет через 20 секунд).
Джон Алан
6
  1. Оптимизатор SQLServer обычно делает хорошую работу. Однако его цель не в том, чтобы создать наилучший из возможных планов, а в том, чтобы найти достаточно хороший план. Для определенного запроса со многими объединениями это может привести к очень низкой производительности. Хорошим признаком такого случая является большая разница между оценочным и фактическим количеством строк в фактическом плане выполнения. Кроме того, я почти уверен, что план выполнения для начального запроса покажет много «объединение вложенных циклов», которое медленнее, чем «объединение слиянием». Последнее требует сортировки обоих входов с использованием одного и того же ключа, что является дорогостоящим, и обычно оптимизатор отбрасывает такую ​​опцию. Сохранение результатов во временной таблице и добавление правильных индексов, как вы уже сделали - я думаю, - при выборе лучшего алгоритма для дальнейших объединений (примечание: вы следуете рекомендациям, сначала заполнив временную таблицу, и добавление индексов после). Кроме того, SQLServer генерирует и хранит статистику для временных таблиц, что также помогает в выборе правильного индекса.
  2. Я не могу сказать, что существует стандарт использования временных таблиц, когда число объединений больше некоторого фиксированного числа, но это определенно вариант, который может повысить производительность. Это случается не часто, но у меня были похожие проблемы (и похожее решение) пару раз. В качестве альтернативы, вы можете попытаться определить лучший план выполнения самостоятельно, сохранить и принудительно использовать его повторно, но это займет огромное количество времени (100% гарантии, что вы добьетесь успеха). Еще одно замечание - в случае, если результирующий набор, который хранится во временной таблице, относительно мал (например, около 10 тыс. Записей), табличная переменная работает лучше, чем временная таблица.
  3. Я ненавижу говорить «это зависит», но это, вероятно, мой ответ на ваш третий вопрос. Оптимизатор должен давать результаты быстро; вы не хотите, чтобы он часами пытался выяснить лучший план; каждое соединение добавляет дополнительную работу, и иногда оптимизатор «запутывается».
a1ex07
источник
3
+1 спасибо за подтверждение и объяснение. То, что вы написали, имеет смысл.
Ондрей Петерка
4

Хорошо, позвольте мне начать с того, что вы работаете с небольшими данными - 10 нс миллионов невелики. В последнем проекте DWH, который у меня был, 400 миллионов строк были добавлены в таблицу фактов. В ДЕНЬ. Хранение 5 лет.

Проблема аппаратная, частично. Поскольку большие объединения могут использовать много временного пространства, а оперативной памяти очень много, в тот момент, когда вы переполняете диск, все становится намного медленнее. Таким образом, может иметь смысл разделить работу на более мелкие части просто потому, что, хотя SQL живет в мире наборов и не заботится о размере, сервер, на котором вы работаете, не бесконечен. Я довольно привык выходить из-за ошибок пространства в 64 ГБ tempdb во время некоторых операций.

В противном случае, если все в порядке, оптимизатор запросов не перегружен. На самом деле неважно, насколько велика таблица - она ​​работает по статистике, которая на самом деле не растет. ЭТО СКАЗАЛ: Если у вас действительно есть БОЛЬШАЯ таблица (двузначное число миллиардов строк), то они могут быть немного грубыми.

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

В конце мы работаем с ограниченным оборудованием.

TomTom
источник
1
+1 спасибо за ваш ответ. Есть смысл сказать, что это зависит от HW. У нас всего 32 ГБ оперативной памяти, что, вероятно, недостаточно.
Ондрей Петерка
2
Я немного расстраиваюсь каждый раз, когда читаю такие ответы - даже несколько десятков миллионов строк создают нагрузку на процессор на нашем сервере баз данных в течение нескольких часов. Может быть, число измерений велико, но 30 измерений, кажется, не слишком большое число. Я думаю, что очень большое количество строк, которые вы можете обработать, взяты из простой модели. Еще хуже: все данные помещаются в оперативную память. И все еще занимает часы.
flaschenpost
1
30 измерений - это много, вы уверены, что модель должным образом оптимизирована в звезду? Некоторые ошибки, например, то, что стоит ЦП - при запросе OP использует GUID в качестве первичных ключей (uniqueidentifier). Я тоже их люблю - поскольку уникальный индекс, первичный ключ - это поле идентификатора, делает все сравнение быстрее и индекс больше nawwox (4 или 8 байт, а не 18). Такие трюки позволяют сэкономить тонну процессора.
TomTom