[英]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.