Медленно выполняющийся SQL-запрос с тройным самостоятельным соединением - PullRequest
7 голосов
/ 03 ноября 2010

У меня есть устаревшая база данных со следующей таблицей (примечание: без первичного ключа)

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

CREATE TABLE [single_date_availability](
    [accommodation_id] [int],
    [accommodation_unit_id] [int],
    [arrival_date] [datetime],
    [price] [decimal](18, 0),
    [offer_discount] [decimal](18, 0),
    [num_pax] [int],
    [rooms_remaining] [int],
    [eta_available] [int],
    [date_correct] [datetime],
    [max_occupancy] [int],
    [max_adults] [int],
    [min_stay_nights] [int],
    [max_stay_nights] [int],
    [nights_remaining_count] [numeric](2, 0)
) ON [PRIMARY]

Таблица содержит примерно 16 500 записей.

Но мне нужно умножить данные в совершенно другом формате, например:

  • Размещение
  • Дата
  • Продолжительность
  • Общая стоимость

Максимальная продолжительность для каждой даты прибытия.

I 'используя следующий запрос для достижения этой цели:

SELECT
    MIN(units.MaxAccommodationAvailabilityPax) AS MaxAccommodationAvailabilityPax,
    MIN(units.MaxAccommodationAvailabilityAdults) AS MaxAccommodationAvailabilityAdults,
    StartDate AS DepartureDate,
    EndDate AS ReturnDate,
    DATEDIFF(DAY, StartDate, EndDate) AS Duration,
    MIN(units.accommodation_id) AS AccommodationID, 
    x.accommodation_unit_id AS AccommodationUnitID,
    SUM(Price) AS Price,
    MAX(num_pax) AS Occupancy,
    SUM(offer_discount) AS OfferSaving,
    MIN(date_correct) AS DateTimeCorrect,
    MIN(rooms_remaining) AS RoomsRemaining,
    MIN(CONVERT(int, dbo.IsGreaterThan(ISNULL(eta_available, 0)+ISNULL(nights_remaining_count, 0), 0))) AS EtaAvailable
FROM single_date_availability fp
INNER JOIN (
    /* This gets max availability for the whole accommodation on the arrival date */
    SELECT accommodation_id, arrival_date,
        CASE EtaAvailable WHEN 1 THEN 99 ELSE MaxAccommodationAvailabilityPax END AS MaxAccommodationAvailabilityPax,
        CASE EtaAvailable WHEN 1 THEN 99 ELSE MaxAccommodationAvailabilityAdults END AS MaxAccommodationAvailabilityAdults
    FROM (SELECT accommodation_id, arrival_date, SUM(MaximumOccupancy) MaxAccommodationAvailabilityPax, SUM(MaximumAdults) MaxAccommodationAvailabilityAdults,
            CONVERT(int, WebData.dbo.IsGreaterThan(SUM(EtaAvailable), -1)) AS EtaAvailable                 
            FROM (SELECT accommodation_id, arrival_date, MIN(rooms_remaining*max_occupancy) as MaximumOccupancy,
                    MIN(rooms_remaining*max_adults) as MaximumAdults, MIN(ISNULL(eta_available, 0) + ISNULL(nights_remaining_count, 0) - 1) as EtaAvailable
                    FROM single_date_availability
                    GROUP BY accommodation_id, accommodation_unit_id, arrival_date) a 
            GROUP BY accommodation_id, arrival_date) b
) units ON fp.accommodation_id = units.accommodation_id AND fp.arrival_date = units.arrival_date
INNER JOIN (
    /* This gets every combination of StartDate and EndDate for each Unit/Occupancy */
    SELECT DISTINCT a.accommodation_unit_id, StartDate = a.arrival_date,
        EndDate = b.arrival_date+1, Duration = DATEDIFF(DAY, a.arrival_date, b.arrival_date)+1
        FROM single_date_availability AS a
        INNER JOIN (SELECT accommodation_unit_id, arrival_date FROM single_date_availability) AS b
        ON a.accommodation_unit_id = b.accommodation_unit_id
            AND DATEDIFF(DAY, a.arrival_date, b.arrival_date)+1 >= a.min_stay_nights
            AND DATEDIFF(DAY, a.arrival_date, b.arrival_date)+1 <= (CASE a.max_stay_nights WHEN 0 THEN 28 ELSE a.max_stay_nights END)
) x ON fp.accommodation_unit_id = x.accommodation_unit_id AND fp.arrival_date >= x.StartDate AND fp.arrival_date < x.EndDate
GROUP BY x.accommodation_unit_id, StartDate, EndDate
/* This ensures that all dates between StartDate and EndDate are actually available */
HAVING COUNT(*) = DATEDIFF(DAY, StartDate, EndDate)

Это работает и дает мне около 413 000 записей.Результаты этого запроса я использую для обновления другой таблицы.

Но запрос работает довольно плохо, как вы могли ожидать при таком количестве самостоятельных объединений.Локальная работа занимает около 15 секунд, но на нашем тестовом сервере это занимает более 1:30 минуты, а на нашем живом сервере SQL - более 30 секунд;и во всех случаях он максимизирует процессор, пока он выполняет большее из соединений.

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

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

Я выполнил запрос через запросоптимизатор и следовал всем рекомендациям для индексов и статистики.

Любая помощь по ускорению этого запроса или, по крайней мере, менее интенсивному использованию ЦП, будет принята с благодарностью.Если это необходимо разбить на несколько этапов, это приемлемо.

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

Я не особенно ищу комментарии о том, как ужасна и ненормализована эта структура ... это, я уже знаю: -)

Ответы [ 3 ]

16 голосов
/ 06 ноября 2010

Этот сайт для профессиональных программистов, верно.

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

Это работает и дает мне около 413 000 записей.Результаты этого запроса я использую для обновления другой таблицы.

Это еще более безумно.Все эти (а) временные рабочие таблицы и (б) временные рабочие таблицы для бизнеса временных рабочих столов являются классическими признаками ненормализованной базы данных.ИЛИ неспособность понять данные как они есть, как получить данные, и создание ненужных рабочих столов для удовлетворения ваших потребностей.Я не пытаюсь заставить вас изменить это, что будет первым параметром и которое устранит необходимость во всем этом беспорядке.

второй вариант Посмотрите, можете ли вы получить окончательный результат из исходных таблиц:
- без использования рабочих таблиц
- с использованием одного рабочего стола
вместо двух рабочих таблиц (16 500 и 413 000 «записей»; это двауровни экспоненциальной ненормализации)

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

Но запрос работает довольно плохо, как вы могли ожидать при таком количестве самосоединений

Ерунда, объединения и самосоединения ничего не стоят.Проблемы, стоимость в:

  • вы работаете на куче

  • без PK

    • эти два элемента означают, что производительность не рассматривалась и не может ожидаться
  • использование операторов и функций (а не просто "=") в объединениях означает, что сервер не может сделать разумныйрешения по поисковым значениям, поэтому вы все время сканируете таблицу

  • размер таблицы (может отличаться в Dev / Test / Prod)

  • действительные, пригодные для использования индексы (или нет)

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

следующая серия проблем - это способ, которым вы это делаете.

  • Вы НЕ понимаете, что "соединения" - это материализованные таблицы;вы не «присоединяетесь», вы материализуете TABLES на лету ???Ничто не бесплатно: материализация имеет огромную стоимость.Вы настолько сосредоточены на материализации без какого-либо представления о стоимости, что вы думаете, что объединения - это проблема.Это почему ?

  • Прежде чем вы сможете принимать какие-либо разумные решения по кодированию, вам нужно включить SHOWPLAN и STATISTICS IO ON.Делайте это во время разработки (это далеко не готово к «тестированию»).Это даст вам представление о таблицах;соединения (что вы ожидаете от того, что он определил, из беспорядка);рабочие столы (материализованные).Высокая загрузка ЦП - ничто, подождите, пока вы не увидите безумный ввод-вывод, который использует ваш код.Если вы хотите поспорить о стоимости материализации на лету, будьте моим гостем, но сначала опубликуйте SHOWPLAN.

  • обратите внимание, что материализованные таблицы не имеют индексов, поэтомуТаблица сканирует каждый раз , для ум "соединения".

  • Выбор как есть, выполняет в десятки раз (возможно, в сотни) больше работы, чем нужно.Поскольку таблица есть, и она не сдвинулась, материализовать другую версию очень глупо.Итак, истинный вопрос:

Почему мой SQL-запрос с одной таблицей и шестью материализованными версиями сам по себе медленен?

.
В случае, если вы не уверены, это означает, что исключите шесть материализованных таблиц и замените их чистыми соединениями с основным столом.

  • Если вы можете согласиться разбить его, тогда сделай так.Создайте и загрузите временные таблицы, которые этот запрос будет использовать FIRST (это означает, что 3 временные таблицы только для агрегатов).Убедитесь, что вы размещаете индексы в правильных столбцах.

  • Таким образом, 6 материализованных таблиц должны быть заменены на 3 соединения с основной таблицей и 3 соединения с временными таблицами агрегирования.

  • Где-то вдольлиния, вы определили, что у вас есть декартовы произведения и дубликаты;вместо того, чтобы устранить причину (разрабатывая код, который производит необходимый набор), вы избежали всего этого, оставили его полным дуплей и вытащили строки DISTINCT.Это вызывает дополнительный рабочий стол.Исправь это.Вы должны получить каждую временную таблицу (рабочие таблицы, материализованные таблицы и т. Д.) Правильно ПЕРВЫМ, прежде чем разумно ожидать, что выбор, который их использует, будет правильным.

  • ТО, попробуйте выбрать,

  • Я предполагаю, что все это выполняется в WebData.Если нет, поместите IsGreaterThan () в эту базу данных.


  1. Пожалуйста, предоставьте DDL для UDF IsGreaterThan.Если для этого используются таблицы, нам нужно знать об этом.

  2. Пожалуйста, предоставьте предполагаемые индексы с помощью оператора CREATE TABLE.Они могут быть неправильными или хуже, дублироваться и не требовать.

  3. Забудьте Identity или принудительные значения, каково действительное, реальное, естественное, логическое PK для этой кучи рабочего стола?

  4. Убедитесь, что у вас нет несоответствий типов данных в столбцах соединения

  5. Лично мне было бы слишком стыдно отправлять такой код, как у вас.Это совершенно нечитабельно.Все, что я сделал, чтобы выявить проблемы, это отформатировать и сделать его читаемым.Есть причины сделать код читабельным, например, он позволяет быстро выявлять проблемы.Неважно, какой формат вы используете, но вы должны отформатировать, и вы должны делать это последовательно.Пожалуйста, очистите его, прежде чем отправлять снова, вместе со ВСЕМ связанным DDL.

Неудивительно, что вы не получили ответов.Сначала вам нужно выполнить некоторую базовую работу (showplan и т. Д.) И подготовить код, чтобы люди могли его прочитать, чтобы они могли дать ответы.

<code>SELECT
        MIN(units.MaxAccommodationAvailabilityPax) AS MaxAccommodationAvailabilityPax,
        MIN(units.MaxAccommodationAvailabilityAdults) AS MaxAccommodationAvailabilityAdults,
        StartDate AS DepartureDate,
        EndDate AS ReturnDate,
        DATEDIFF(DAY, StartDate, EndDate) AS Duration,
        MIN(units.accommodation_id) AS AccommodationID, 
        x.accommodation_unit_id AS AccommodationUnitID,
        SUM(Price) AS Price,
        MAX(num_pax) AS Occupancy,
        SUM(offer_discount) AS OfferSaving,
        MIN(date_correct) AS DateTimeCorrect,
        MIN(rooms_remaining) AS RoomsRemaining,
        MIN(CONVERT(int, dbo.IsGreaterThan(ISNULL(eta_available, 0)+ISNULL(nights_remaining_count, 0), 0))) 
            AS EtaAvailable
    FROM single_date_availability fp INNER JOIN (
        -- This gets max availability for the whole accommodation on the arrival date
        SELECT  accommodation_id, arrival_date,
                CASE EtaAvailable 
                    WHEN 1 THEN 99
                    ELSE MaxAccommodationAvailabilityPax 
                    END AS MaxAccommodationAvailabilityPax,
                CASE EtaAvailable
                    WHEN 1 THEN 99
                    ELSE MaxAccommodationAvailabilityAdults
                    END AS MaxAccommodationAvailabilityAdults
            FROM ( 
                SELECT  accommodation_id, arrival_date,
                        SUM(MaximumOccupancy) 
                        MaxAccommodationAvailabilityPax,
                        SUM(MaximumAdults) MaxAccommodationAvailabilityAdults,
                        CONVERT(int, WebData.dbo.IsGreaterThan(SUM(EtaAvailable), -1))
                            AS EtaAvailable                 
                    FROM ( 
                        SELECT  accommodation_id,
                                arrival_date,
                                MIN(rooms_remaining*max_occupancy) as MaximumOccupancy,
                                MIN(rooms_remaining*max_adults) as MaximumAdults, 
                                MIN(ISNULL(eta_available, 0) + ISNULL(nights_remaining_count, 0) - 1)
                                    as EtaAvailable
                            FROM single_date_availability
                            GROUP BY accommodation_id, accommodation_unit_id, arrival_date
                            ) a 
                    GROUP BY accommodation_id, arrival_date
                    ) b
            ) units 
        ON fp.accommodation_id = units.accommodation_id 
        AND fp.arrival_date = units.arrival_date INNER JOIN (
            -- This gets every combination of StartDate and EndDate for each Unit/Occupancy
            SELECT  D.I.S.T.I.N.C.T a.accommodation_unit_id,
                    StartDate = a.arrival_date,
                    EndDate = b.arrival_date+1,
                    Duration = DATEDIFF(DAY, a.arrival_date, b.arrival_date)+1
                FROM single_date_availability AS a INNER JOIN ( 
                    SELECT  accommodation_unit_id,
                            arrival_date 
                        FROM single_date_availability
                        ) AS b
                ON a.accommodation_unit_id = b.accommodation_unit_id
                AND DATEDIFF(DAY, a.arrival_date, b.arrival_date)+1 >= a.min_stay_nights
                AND DATEDIFF(DAY, a.arrival_date, b.arrival_date)+1 <= (
                    CASE a.max_stay_nights 
                        WHEN 0 THEN 28 
                        ELSE a.max_stay_nights 
                        END
                )
        ) x ON fp.accommodation_unit_id = x.accommodation_unit_id 
        AND fp.arrival_date >= x.StartDate 
        AND fp.arrival_date < x.EndDate
    GROUP BY x.accommodation_unit_id, StartDate, EndDate
    -- This ensures that all dates between StartDate and EndDate are actually available
    HAVING COUNT(*) = DATEDIFF(DAY, StartDate, EndDate)
2 голосов
/ 03 ноября 2010

это, скорее всего, не решит все ваши проблемы, но попробуйте переключить

AND DATEDIFF(DAY , a.arrival_date , b.arrival_date) + 1 >= a.min_stay_nights
AND DATEDIFF(DAY , a.arrival_date , b.arrival_date) + 1 <= (CASE a.max_stay_nights WHEN 0 THEN 28 ELSE a.max_stay_nights END)

на

and a.min_stay_nights<=DATEDIFF(DAY , a.arrival_date , b.arrival_date)
and (CASE a.max_stay_nights WHEN 0 THEN 28 ELSE a.max_stay_nights END)>=DATEDIFF(DAY , a.arrival_date , b.arrival_date) + 1

причина в том, что, насколько я помню,SQL Server не нравятся функции в левой части знака =, где предложения

1 голос
/ 03 ноября 2010

Поскольку вы сказали, что уже запустили оптимизатор запросов, я могу только предположить, что все ваши индексы верны.Мой следующий подход - сделать соединение в приложении.Что я имею в виду под этим?Вместо того, чтобы иметь DB, делайте соединения из 100 тысяч строк.Получите все из них один раз в вашем приложении, а затем вы получите циклы и логику, чтобы сделать то, что вы сделали бы в SQL вместо этого.

Причина этого заключается в том, что многие приложения, такие как Facebook, Yahoo, AOL не одобряют присоединения.Присоединения не лучшая вещь, если вы не знаете, что это будет быстро.В этом случае вы захотите присоединиться к приложению, а затем кэшировать его для будущих нужд.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...