Ускорение sql ПРИСОЕДИНЯЙТЕСЬ - PullRequest
2 голосов
/ 07 ноября 2008

Прежде всего, немного фона.

У нас есть система обработки заказов, в которой сотрудники вводят данные о выставлении счетов в приложение, которое хранит их в базе данных sql server 2000. Эта база данных не является реальной биллинговой системой: это просто хранилище, так что записи могут быть запущены в систему мэйнфреймов через ночные пакетные процессы.

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

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

Затем выполняется процесс аудита, чтобы убедиться, что все, что первоначально было введено персоналом, может быть где-то учтено. Этот аудит принимает форму SQL-запроса, который мы запускаем, и выглядит примерно так:

SELECT *
FROM [StaffEntry] s with (nolock)
LEFT JOIN [MainFrame] m with (nolock)
    ON m.ItemNumber = s.ItemNumber 
        AND m.Customer=s.Customer 
        AND m.CustomerPO = s.CustomerPO -- purchase order
        AND m.CustPORev = s.CustPORev  -- PO revision number
LEFT JOIN [Rejected] r with (nolock) ON r.OrderID = s.OrderID
WHERE s.EntryDate BETWEEN @StartDate AND @EndDate
    AND r.OrderID IS NULL AND m.MainFrameOrderID IS NULL

Конечно, это сильно модифицировано, но я считаю, что важные части представлены. Проблема в том, что этот запрос начинает выполняться слишком долго, и я пытаюсь выяснить, как его ускорить.

Я почти уверен, что проблема в соединении таблицы StaffEntry с таблицей MainFrame. Так как оба хранят данные для каждого заказа с начала времени (2003 в этой системе), они имеют тенденцию быть немного большими. Значения OrderID и EntryDate, используемые в таблице StaffEntry, не сохраняются при импорте в мэйнфрейм, поэтому такое объединение немного сложнее. И наконец, так как я ищу записи в таблице MainFrame, которые не существуют, после выполнения JOIN мы имеем этот уродливый IS NULL в предложении where.

Таблица StaffEntry индексируется с помощью EntryDate (кластеризовано) и отдельно для Customer / PO / rev. MainFrame индексируется клиентом и номером платы за мэйнфрейм (кластеризовано, это необходимо для других систем) и отдельно клиентом / PO / Rev. Rejected вообще не индексируется, но он небольшой, и тестирование показывает, что это не проблема.

Итак, мне интересно, есть ли другой (надеюсь, быстрее) способ выразить эти отношения?

Ответы [ 7 ]

5 голосов
/ 07 ноября 2008

Во-первых, вы можете избавиться от второго LEFT JOIN.

Ваш WHERE удалял любые совпадения, во всяком случае ... Например, если S.OrderID был 1 и был R.OrderID со значением 1, принудительное применение IS NULL в WHERE не допустило бы это. Таким образом, он вернет только записи, в которых s.OrderID равен NULL, если я правильно его читаю ...

Во-вторых, если вы имеете дело с большим количеством данных, добавление подсказки таблицы NOLOCK обычно не повредит. Предполагая, что вы не возражаете против возможности грязного чтения здесь или там :-P Обычно стоит риск, хотя.

SELECT *
FROM [StaffEntry] s (nolock)
LEFT JOIN [MainFrame] m (nolock) ON m.ItemNumber = s.ItemNumber 
    AND m.Customer=s.Customer 
    AND m.CustomerPO = s.CustomerPO -- purchase order
    AND m.CustPORev = s.CustPORev  -- PO revision number
WHERE s.EntryDate BETWEEN @StartDate AND @EndDate
    AND s.OrderID IS NULL

Наконец, была часть вашего вопроса, которая была не слишком ясна для меня ...

"так как я ищу записи в таблице MainFrame, которые не существует, после присоединения мы иметь это уродливое значение NULL в где пункт. "

Хорошо ... Но вы пытаетесь ограничить его только тем, где нет записей таблицы MainFrame? Если да, то вы хотите, чтобы это также было выражено в ГДЕ, верно? Так что-то вроде этого ...

SELECT *
FROM [StaffEntry] s (nolock)
LEFT JOIN [MainFrame] m (nolock) ON m.ItemNumber = s.ItemNumber 
    AND m.Customer=s.Customer 
    AND m.CustomerPO = s.CustomerPO -- purchase order
    AND m.CustPORev = s.CustPORev  -- PO revision number
WHERE s.EntryDate BETWEEN @StartDate AND @EndDate
    AND s.OrderID IS NULL AND m.ItemNumber IS NULL

Если это то, что вы намеревались использовать в исходном утверждении, возможно, вы можете избавиться от проверки s.OrderID IS NULL?

1 голос
/ 08 ноября 2008

попробуйте поменять LEFT JOIN [отклонено] r с (nolock) ON r.OrderID = s.OrderID в ПРАВИЛЬНОЕ СЛОВО СОЕДИНЕНИЕ:

SELECT ...
FROM [Rejected] r
     RIGHT MERGE JOIN [StaffEntry] s with (nolock) ON r.OrderID = s.OrderID
     LEFT JOIN [MainFrame] m with (nolock) ON....
1 голос
/ 07 ноября 2008

Индексирование по всем таблицам будет иметь важное значение. Если вы не можете многое сделать с индексированием по столбцам [MainFrame], используемым в объединении, вы также можете предварительно ограничить строки для поиска в [MainFrame] (и [Rejected], хотя это уже выглядит так, как будто PK), указав диапазон дат - если окно даты должно быть примерно одинаковым. Это может привести к сокращению на правой стороне этого соединения.

Я бы также посмотрел план выполнения и выполнил простую оценку «черного ящика», какой из ваших JOIN s действительно самый дорогой - m или r, сопоставив запрос только с одним или Другой. Я подозреваю, что это m из-за нескольких столбцов и отсутствующих полезных индексов.

Вы можете использовать m.EntryDate в течение нескольких дней или месяцев от вашего диапазона. Но если у вас уже есть индексы на мэйнфреймах, вопрос заключается в том, почему они не используются или используются, почему производительность такая низкая.

1 голос
/ 07 ноября 2008

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

Моя последняя мысль: иногда вам просто нужно поэкспериментировать с различными методами объединения запроса. Здесь может быть слишком много переменных, чтобы кто-то мог ответить ... С другой стороны, люди здесь умные, поэтому я могу ошибаться.

Удачи!

С уважением, Frank

PS: я забыл упомянуть, что если вы хотите попробовать этот метод временных таблиц, вам также необходимо поэкспериментировать с различными индексами и первичными ключами временных таблиц. В зависимости от объема данных могут помочь индексы и PK.

1 голос
/ 07 ноября 2008

Это не имеет смысла:

SELECT *
FROM [StaffEntry] s
LEFT JOIN [MainFrame] m ON m.ItemNumber = s.ItemNumber 
    AND m.Customer=s.Customer 
    AND m.CustomerPO = s.CustomerPO -- purchase order
    AND m.CustPORev = s.CustPORev  -- PO revision number
LEFT JOIN [Rejected] r ON r.OrderID = s.OrderID
WHERE s.EntryDate BETWEEN @StartDate AND @EndDate
    AND r.OrderID IS NULL AND s.OrderID IS NULL

если s.OrderID IS NULL, то r.OrderID = s.OrderID никогда не будет истинным, поэтому никакие строки из [Rejected] никогда не будут включены, таким образом, как указано, это эквивалентно:

SELECT *
FROM [StaffEntry] s
LEFT JOIN [MainFrame] m ON m.ItemNumber = s.ItemNumber 
    AND m.Customer=s.Customer 
    AND m.CustomerPO = s.CustomerPO -- purchase order
    AND m.CustPORev = s.CustPORev  -- PO revision number
WHERE s.EntryDate BETWEEN @StartDate AND @EndDate
    AND s.OrderID IS NULL

Вы уверены, что введенный вами код верен?

1 голос
/ 07 ноября 2008

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

0 голосов
/ 07 ноября 2008

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

У меня также есть некоторые обновления статистики: я могу сделать запрос быстрым и приятным, строго ограничив диапазон данных, используемый с StaffEntry.EntryDate. К сожалению, я могу сделать это только потому, что, пройдя долгий путь, я точно знаю, какие даты меня волнуют. Обычно я не знаю этого заранее.

План выполнения из исходного прогона показал 78% стоимости сканирования кластерного индекса в таблице StaffEntry и 11% стоимости при поиске индекса для таблицы MainFrame, а затем 0% стоимости самого соединения , Запуск его с использованием узкого диапазона дат, который изменяется на 1% для поиска индекса StaffEntry, 1% для поиска индекса MainFrame и 93% для сканирования таблицы Rejected. Это «реальные» планы, а не оценочные.

...