簡體   English   中英

使用三重自聯接執行緩慢的SQL查詢

[英]Slow performing SQL query with triple self-join

我有一個帶有下表的遺留數據庫(注意:沒有主鍵)

它為每個住宿“單位”和日期以及該日期的價格定義了每條記錄。

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條記錄。

但我需要以完全不同的格式將數據相乘,例如:

  • 住所
  • 日期
  • 持續時間
  • 總價

每個到達日期的最長持續時間。

我正在使用以下查詢來實現此目的:

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秒; 並且在所有情況下,它在執行更大的連接時最大化CPU。

沒有其他進程同時訪問該表,可以假設。

我真的不介意查詢的長度,就像對CPU的需求一樣,這可能會導致其他查詢同時嘗試訪問其他數據庫/表時出現問題。

我已通過查詢優化器運行查詢,並遵循索引和統計信息的所有建議。

任何幫助使這個查詢更快或至少減少CPU密集的幫助將非常感激。 如果需要將其分解為不同的階段,那是可以接受的。

說實話,速度並不是那么重要,因為它是在沒有被其他進程觸及的表上執行的批量操作。

我並不是特別關注這個結構有多糟糕和不規范化的評論......我已經知道了:-)

這個網站是專業程序員的權利。

在沒有主鍵的情況下嘗試操作“表”是很麻煩的。 很好,它是一個工作區,而不是一個真正的表(但它很大,你試圖在它上面執行關系表操作)。 好吧,你知道它是非標准化的。 實際上數據庫是非標准化的,這個“表”是它的產物:指數非標准化產品。

這有效,給了我大約413,000條記錄。 這個查詢的結果我用來更新另一個表。

那更加瘋狂。 所有這些(a)臨時工作表和(b)臨時工作台業務的臨時工作表是非規范化數據庫的典型症狀。 或無法理解數據,如何獲取數據,以及創建不必要的工作表以滿足您的需求。 我不是試圖讓你改變它,這將是第一個選擇 ,並且將消除對這整個混亂的需要。

第二個選項是,看看你是否可以從原始表中產生最終結果:
- 不使用工作台
- 使用一個工作台
而不是兩個工作表(16,500和413,000“記錄”;這是指數非正常化的兩個級別)

第三種選擇是,改善你所擁有的混亂......但首先你需要了解表現豬的位置......

但是查詢執行起來非常糟糕,正如您可能期望的那樣有很多自聯接

無意義,連接和自連接都沒有任何成本。 問題是,成本是:

  • 你在堆上操作

  • 沒有PK

    • 僅這兩個項目的平均表現尚未被考慮且無法預期
  • 在連接中使用運算符和函數(而不是純“=”)意味着服務器無法對搜索值做出合理的決定,因此您始終在進行表掃描

  • 表格大小(Dev / Test / Prod可能不同)

  • 有效的,可用的指數(或不是)

  • 成本在這四個項目中,各個方面的堆都非常慢,而且運營商沒有找到任何可以縮小搜索范圍的內容; 不是有或沒有連接操作的事實。

下一系列問題是你的方式。

  • 你沒有意識到“連接”是物化表; 你沒有“加入”你正在實現表格??? 沒有什么是免費的:物化有巨大的成本。 您如此專注於實現而不知道成本,您認為連接是問題所在。 這是為什么 ?

  • 在做出任何合理的編碼決定之前,您需要設置SHOWPLAN和STATISTICS IO ON。 在你開發的過程中這樣做(它還沒有准備好進行“測試”)。 這會讓你了解表格; 連接(你所期望的與它所決定的,從混亂中); 工作表(物化)。 高CPU使用率是沒有的,等到你看到你的代碼使用瘋狂的I / O. 如果你想爭論實時成本,請成為我的客人,但首先發布SHOWPLAN。

  • 請注意, 實體化表沒有索引,因此每次都會對um“連接”進行表掃描

  • 按原樣選擇,比它需要的工作多幾十(甚至幾百)。 由於桌子在那里,並且它沒有移動,實現它的另一個版本是一件非常愚蠢的事情。 所以,真正的問題是:

為什么我的SQL查詢與一個表和六個物化版本本身很慢?


如果你不確定,這意味着消除六個物化表並用純連接替換它們到主表。

  • 如果你能接受分手,那就去做吧。 創建並加載此查詢將使用的臨時表FIRST(這意味着僅有3個臨時表用於聚合)。 確保將索引放在正確的列上。

  • 因此,6個物化表將被3個連接替換為主表,3個連接到臨時聚合表。

  • 在某個地方,您已經確定您擁有笛卡兒產品和重復產品; 而不是修復原因(開發產生你需要的集合的代碼)你已經避免了所有這些,留下了充滿欺騙,並拉出了DISTINCT行。 這會導致額外的工作表。 修復它。 您必須先獲取每個臨時表(工作表,物化表,等等),然后才能合理地預期使用它們的選擇是正確的。

  • 然后嘗試選擇。

  • 我認為這都是在WebData中運行的。 如果沒有,請將IsGreaterThan()放在此數據庫中。


  1. 請為UDF IsGreaterThan提供DDL。 如果是使用表格,我們需要了解它。

  2. 請使用CREATE TABLE語句提供所指控的索引。 它們可能不正確或更糟,加倍而不是必需的。

  3. 忘記身份或強制值,這個工作表堆的實際,真實,自然,邏輯PK是什么?

  4. 確保連接列上沒有數據類型不匹配

  5. 就個人而言,我會羞於發布你所擁有的代碼。 這是完全不可能的。 為了找出這里的問題,我所做的只是格式化,並使其可讀。 使代碼可讀的原因有很多,例如,它可以讓您快速發現問題。 使用什么格式無關緊要,但您必須格式化,並且必須始終如一地進行格式化。 請在再次發布之前清理它,以及所有相關的DDL。

難怪你沒有得到答案。 您需要先做一些基本的工作(showplan等)並准備好代碼,以便人類可以閱讀它,以便他們可以提供答案。

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)

這很可能無法解決您的所有問題,但請嘗試切換

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不喜歡=符號左側的函數where where子句

既然你說你已經運行了查詢優化器,那么我只能假設你的所有索引都是正確的。 我的下一個方法是在應用程序中進行連接。 那是什么意思? 而不是讓DB做10萬行的連接。 在您的應用程序中獲取所有這些,然后循環和邏輯來執行您在sql中所做的事情。

原因是許多fe應用程序,如facebook,yahoo,aol皺眉加入。 加入並不是最好的事情,除非你知道它會很快。 在這種情況下,您可能希望在應用程序中加入,然后將其緩存以備將來使用。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM