繁体   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