[英]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“連接”進行表掃描 。
按原樣選擇,比它需要的工作多幾十(甚至幾百)。 由於桌子在那里,並且它沒有移動,實現它的另一個版本是一件非常愚蠢的事情。 所以,真正的問題是:
。
如果你不確定,這意味着消除六個物化表並用純連接替換它們到主表。
如果你能接受分手,那就去做吧。 創建並加載此查詢將使用的臨時表FIRST(這意味着僅有3個臨時表用於聚合)。 確保將索引放在正確的列上。
因此,6個物化表將被3個連接替換為主表,3個連接到臨時聚合表。
在某個地方,您已經確定您擁有笛卡兒產品和重復產品; 而不是修復原因(開發產生你需要的集合的代碼)你已經避免了所有這些,留下了充滿欺騙,並拉出了DISTINCT行。 這會導致額外的工作表。 修復它。 您必須先獲取每個臨時表(工作表,物化表,等等),然后才能合理地預期使用它們的選擇是正確的。
然后嘗試選擇。
我認為這都是在WebData中運行的。 如果沒有,請將IsGreaterThan()放在此數據庫中。
請為UDF IsGreaterThan提供DDL。 如果是使用表格,我們需要了解它。
請使用CREATE TABLE語句提供所指控的索引。 它們可能不正確或更糟,加倍而不是必需的。
忘記身份或強制值,這個工作表堆的實際,真實,自然,邏輯PK是什么?
確保連接列上沒有數據類型不匹配
就個人而言,我會羞於發布你所擁有的代碼。 這是完全不可能的。 為了找出這里的問題,我所做的只是格式化,並使其可讀。 使代碼可讀的原因有很多,例如,它可以讓您快速發現問題。 使用什么格式無關緊要,但您必須格式化,並且必須始終如一地進行格式化。 請在再次發布之前清理它,以及所有相關的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.