Оптимизировать выбор подзапроса с помощью COALESCE (…)


8

У меня есть большое представление, которое я использую из приложения. Я думаю, что сузил свою проблему с производительностью, но я не уверен, как ее исправить. Упрощенная версия представления выглядит так:

SELECT ISNULL(SEId + '-' + PEId, '0-0') AS Id,
   *,
   DATEADD(minute, Duration, EventTime) AS EventEndTime
FROM (
    SELECT se.SEId, pe.PEId,
        COALESCE(pe.StaffName, se.StaffName) AS StaffName, -- << Problem!
        COALESCE(pe.EventTime, se.EventTime) AS EventTime,
        COALESCE(pe.EventType, se.EventType) AS EventType,
        COALESCE(pe.Duration, se.Duration) AS Duration,
        COALESCE(pe.Data, se.Data) AS Data,
        COALESCE(pe.Field, se.Field) AS Field,
        pe.ThisThing, se.OtherThing
    FROM PE pe FULL OUTER JOIN SE se 
      ON pe.StaffName = se.StaffName
     AND pe.Duration = se.Duration
     AND pe.EventTime = se.EventTime
    WHERE NOT(pe.ThisThing = 1 AND se.OtherThing = 0)
) Z

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

Итак, так как это представление используется из приложения, при попытке оптимизации я обертываю его в другой SELECT, например так:

SELECT * FROM (
    -- … above code …
) Q
WHERE StaffName = 'SMITH, JOHN Q'

потому что приложение ищет конкретных сотрудников в результате.

Проблема, кажется, в COALESCE(pe.StaffName, se.StaffName) AS StaffNameразделе, и что я выбираю из представления StaffName. Если я изменю это на pe.StaffName AS StaffNameили se.StaffName AS StaffName, проблемы с производительностью исчезнут (но см. Обновление 2 ниже) . Но это не сработает, потому что одна или другая сторона FULL OUTER JOINмогут отсутствовать, поэтому одно или другое поле может иметь значение NULL.

Могу ли я выполнить рефакторинг, заменив его COALESCE(…)чем-то еще, что будет переписано в подзапрос?

Другие заметки:

  • Я уже добавил несколько индексов, чтобы исправить проблемы с производительностью в остальной части запроса - без COALESCEнего это очень быстро.
  • К моему удивлению, просмотр плана выполнения не поднимает никаких флагов, даже если WHEREвключены подзапрос и оператор переноса . Моя общая стоимость подзапроса в анализаторе равна 0.0065736. Хммм. Выполнение занимает четыре секунды.
  • Изменение приложения для запроса по-другому (например, возврат pe.StaffName AS PEStaffName, se.StaffName AS SEStaffNameи выполнение WHERE PEStaffName = 'X' OR SEStaffName = 'X') может работать, но в крайнем случае - я действительно надеюсь, что смогу оптимизировать представление, не прибегая к прикосновению к приложению.
  • Хранимая процедура, вероятно, имела бы для этого больше смысла, но приложение построено с использованием Entity Framework, и я не мог понять, как заставить его хорошо работать с SP, который возвращает тип таблицы (еще одна тема).

Индексы

Индексы, которые я добавил до сих пор, выглядят примерно так:

CREATE NONCLUSTERED INDEX [IX_PE_EventTime]
ON [dbo].[PE] ([EventTime])
INCLUDE ([StaffName],[Duration],[EventType],[Data],[Field],[ThisThing])

CREATE NONCLUSTERED INDEX [IX_SE_EventTime]
ON [dbo].[SE] ([EventTime])
INCLUDE ([StaffName],[Duration],[EventType],[Data],[Field],[OtherThing])

Обновить

Хм ... Я попытался смоделировать пораженные изменения выше, и это не помогло. Т.е. до того как ) Zя добавил AND (pe.StaffName = 'SMITH, JOHN Q' OR se.StaffName = 'SMITH, JOHN Q'), но производительность такая же. Теперь я действительно не знаю, с чего начать.

Обновление 2

Комментарий @ypercube о необходимости полного объединения заставил меня осознать, что мой синтезированный запрос исключил, вероятно, важный компонент. Хотя да, мне нужно полное объединение, тест, который я провел выше, отбрасывая COALESCEи проверяя только одну сторону соединения на ненулевое значение, сделал бы другую сторону полного соединения неактуальной , и оптимизатор, вероятно, использовал это факт, чтобы ускорить запрос. Кроме того, я обновил пример, чтобы показать, что StaffNameэто на самом деле один из ключей соединения - что, вероятно, имеет большое значение для вопроса. Я также сейчас склоняюсь к его предположению, что разделение этого на трехсторонний союз вместо полного объединения может быть ответом и упростит изобилие тех, COALESCEчто я делаю в любом случае. Пробую сейчас.


Какие индексы вы добавили? Включаете ли вы StaffName в индекс?
Марк Синкинсон

@MarkSinkinson У меня есть некластеризованный индекс на каждой таблице на KeyField, оба индекса INCLUDEв StaffNameполе и некоторых других областях. Я могу опубликовать определения индекса в вопросе. Я работаю над этим на тестовом сервере, поэтому могу добавить любые индексы, которые, по вашему мнению, могут быть полезны для проб!
S'pht'Kr

1
У вас есть WHERE pe.ThisThing = 1 AND se.OtherThing = 0условие, которое отменяет FULL OUTERсоединение и делает запрос эквивалентным внутреннему соединению. Вы уверены, что вам нужно ПОЛНОЕ присоединение?
ypercubeᵀᴹ

@ypercube Я извиняюсь, это было плохое кодирование с моей стороны, дело в том, что у меня есть условия в обеих таблицах, но да, я учитываю нулевые значения с обеих сторон в реальном запросе. Я объединяю две таблицы и ищу совпадения, но мне нужны доступные данные из любой таблицы, когда нет соответствующей записи слева или справа - так что да, мне нужно полное объединение.
S'pht'Kr

1
А мысли: это рискованное , но вы можете попытаться сломать внутренний запрос на три части ( INNER JOIN, LEFT JOINс WHERE IS NULLчеком, RIGHT JOIN с IS NULL) , а затем UNION ALLтри части. Таким образом, вам не нужно будет его использовать, COALESCE()и это может (просто может) помочь оптимизатору разобраться в переписывании.
ypercubeᵀᴹ

Ответы:


4

Это было довольно давно, но поскольку ОП говорит, что это сработало, я добавляю его как ответ (не стесняйтесь исправлять его, если вы обнаружите, что что-то не так).

Попробуйте сломать внутренний запрос на три части ( INNER JOIN, LEFT JOINс WHERE IS NULLпроверкой, RIGHT JOINс IS NULLпроверкой) , а затем из UNION ALLтрех частей. Это имеет следующие преимущества:

  • В оптимизаторе доступно меньше вариантов преобразования для FULLобъединений, чем для (более распространенных) INNERи LEFTобъединений.

  • Производная Zтаблица может быть удалена (вы можете сделать это в любом случае) из определения представления.

  • NOT(pe.ThisThing = 1 AND se.OtherThing = 0)Будет нужен только на INNERвступление части.

  • Незначительное улучшение, использование COALESCE()будет минимальным , если таковые вообще (я предположил , что se.SEIdи pe.PEIdне обнуляемые. Если больше столбцов не обнуляемые, вы будете в состоянии удалить больше COALESCE()вызовов.)
    Более важным, оптимизатор может оттолкнуть какое - либо условие ваши запросы, которые включают эти столбцы (теперь это COALESCE()не блокирует толчок.)

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

В целом, представление может быть записано как:

SELECT 
    se.SEId + '-' + pe.PEId AS Id,
    se.SEId, pe.PEId,
    pe.StaffName, 
    pe.EventTime,
    COALESCE(pe.EventType, se.EventType) AS EventType,
    pe.Duration,
    COALESCE(pe.Data, se.Data) AS Data,
    COALESCE(pe.Field, se.Field) AS Field,
    pe.ThisThing, se.OtherThing,
    DATEADD(minute, pe.Duration, pe.EventTime) AS EventEndTime
FROM PE pe INNER JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (pe.ThisThing = 1 AND se.OtherThing = 0) 

UNION ALL

SELECT 
    '0-0',
    NULL, pe.PEId,
    pe.StaffName, 
    pe.EventTime,
    pe.EventType,
    pe.Duration,
    pe.Data,
    pe.Field,
    pe.ThisThing, NULL,
    DATEADD(minute, pe.Duration, pe.EventTime) AS EventEndTime
FROM PE pe LEFT JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (pe.ThisThing = 1)
  AND se.StaffName IS NULL

UNION ALL

SELECT 
    '0-0',
    se.SEId, NULL,
    se.StaffName, 
    se.EventTime,
    se.EventType,
    se.Duration,
    se.Data,
    se.Field,
    NULL, se.OtherThing, 
    DATEADD(minute, se.Duration, se.EventTime) AS EventEndTime
FROM PE pe RIGHT JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (se.OtherThing = 0)
  AND pe.StaffName IS NULL ;

0

Моя интуиция заключалась бы в том, что это не должно быть проблемой, поскольку к тому времени, когда COALESCE(pe.StaffName, se.StaffName) AS StaffNameвсе что-либо будет сделано, все строки из двух источников уже должны быть извлечены и сопоставлены, так что вызов функции представляет собой простое сравнение в памяти в памяти и -выбирать. Очевидно, что это не так, поэтому, возможно, что-то в одном из источников (если они являются представлениями или встроенными производными таблицами) или в базовых таблицах (т.е. отсутствие индексов) заставляет планировщика запросов думать, что ему нужно сканировать эти столбцы по отдельности.

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

Чтобы попытаться сделать сравнение в конце концов, вы можете просто выбрать оба значения в deribed table ( pe.StaffName AS pe.StaffName, se.StaffName AS seStaffName), а затем выполнить выборку во внешнем запросе ( COALESCE(peStaffName, seStaffName) AS StaffName), или вы даже можете поместить данные из внутреннего запроса в затем временная таблица выполняет внешний запрос, выбирая из этого (но для этого потребуется хранимая процедура, и в зависимости от количества строк этот дамп в базу данных tempdb может быть дорогостоящим и, следовательно, проблематичным по своей сути).


Благодаря Дэвиду, я ошибался на стороне паранойи в отношении того, сколько я должен раскрыть по этому вопросу, даже если речь идет о структуре (pe => PatientEvent, так что ...), но я знаю, что это усложняет ситуацию. Я думаю, что на самом деле выполняется соединение на основе индексов, а затем выполняется «простое сравнение в памяти» для фильтрации… но нефильтрованная производная таблица в Zнастоящее время возвращается с ~ 1,5 млн строк. Я хочу, чтобы он переписал этот предикат в запрос, Zчтобы он использовал индексы ... но теперь я также растерялся, потому что когда я помещаю туда предикат вручную, он по-прежнему не использует индекс ... так что теперь Я не уверен.
S'pht'Kr
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.