用于确定可以合并哪些行的SQL查询

时间:2018-04-10 21:46:44

标签: sql sql-server sql-server-2008

我们有正在注册活动的客户。有两种类型的注册“基本包”和“完整包”。基本套餐仅包括注册人。每位额外的客人均需支付额外费用。完整套餐包括2名成人(一名是注册人)和2名儿童。然后每位额外的客人需支付额外费用。每个客户都可以根据自己的意愿添加尽可能多的基本和完整注册。

我的任务是编写一个查询,评估每个客户的所有注册,如果他们有任何注册可以合并以降低总成本,则返回客户的ID。我们有两个表来维护这些数据 - 每个注册一个,每个注册的额外客人有一个第二个表。请参阅DB-Fiddle示例,了解数据的结构。

在示例数据中,有3个客户'00001','00002'和'00003'。对于示例数据,我的查询应返回“00001”和“00002”但不是“00003”的客户ID。

客户'00001'有3个注册(一个基本注册和两个完整注册)。吉姆史密斯的第一次完整注册已经包含2名成人和2名儿童。但是,第2次完整注册(Pam Jones)只有1名成人和2名儿童。因此,John Doe的基本注册是没有必要的,因为John Doe可以作为Pam Jone注册的第二位成年人加入。

客户'00002'有两个完整的注册Walter Mann和Paula Wilson。 Walter的注册包含两个成人和两个孩子。但还包括额外的成人和额外的儿童 - 每个儿童都需支付额外费用。但是,Paula Wilson的注册仅包括一名成人和一名儿童。因此,沃尔特的额外成人和额外的孩子可以转移到宝拉的注册。 *注意 - 我的数据不反映这一点。但是,如果沃尔特的注册中包含比Paula注册更多的额外成人和/或孩子,它仍然会被标记,因为它仍会通过填充尽可能多的可用空位来降低客户'00002的总成本。

客户'00003'有两个注册 - 一个是基本的,一个是完整的。 Albert Palmer酒店的基本注册包括额外收费的额外成人和额外儿童。然而,Dalton Evans的第二次注册已经包含两名成人和两名子女,因此无法降低客户的整体成本。

因此,如前所述,我需要一个查询来评估所有客户的所有注册,并返回客户ID,如果他们的任何注册可以优化。这个级别的SQL超出了我的想象。所以,对此的任何帮助将不胜感激。

2 个答案:

答案 0 :(得分:2)

至少您需要为每对(或部分对)成人(儿童)进行注册。如果您的注册数量超过了这两个数字所要求的数量,那么它们可以合并。

select r.customerid
from Registrations r left outer join RegistrationAdditionalGuestDetails g
    on g.registrationid = r.registrationid
group by r.customerid
having
    count(distinct r.registrationid) >
      ceiling(
        (
          count(distinct r.registrationid) /* non-guest adults */ +
          count(case when guesttype like '%Adult%' then 1 end)
        ) / 2.0
      )
    or
      ceiling(count(case when guesttype like '%Child%' then 1 end) / 2.0)
        between 1 and /* children are optional */
          count(distinct case when r.registrationtype = 'Full' then r.registrationid end);

如果包装定义发生变化,则可以根据需要轻松调整计算。我还假设只有两个成年人完全注册比两个基本要好。如果这不正确,我仍然很想知道所需合并规则的更多参数。

答案 1 :(得分:1)

有趣的问题。为了使这更容易理解,我将使用临时表来说明可能的解决方案。如果它适合您并且您不想在那里使用该表,那么您应该能够稍微折叠查询。以下是我想了解的每项预订:

declare @RegistrationDetail table
(
    RegistrationId int,
    CustomerId varchar(5),
    AdultsIncluded int,
    ChildrenIncluded int,
    AdultsRemaining int,
    ChildrenRemaining int
);

前两个字段将直接来自您的dbo.Registrations表。 AdultsIncludedChildrenIncluded分别会显示已在每次预订中出现的成人和儿童总数。 AdultsRemainingChildrenRemaining分别会提供可能仍会添加到预订中的成人和儿童的数量,而不会产生额外费用。因此,对于完整注册,剩余的计数将是两个减去已经使用的预订数量,而对于基本预订,剩余的计数将始终为零。

with GuestCountCTE as
(
    select
        Reg.RegistrationId,
        AdultsIncluded = 1 + sum(case when Guest.GuestType in ('Adult Included', 'Additional Adult') then 1 else 0 end),
        ChildrenIncluded = sum(case when Guest.GuestType in ('Child Included', 'Additional Child') then 1 else 0 end)
    from
        dbo.Registrations Reg
        left join dbo.RegistrationAdditionalGuestDetails Guest on Reg.RegistrationId = Guest.RegistrationId
    group by
        Reg.RegistrationId
)
insert @RegistrationDetail
select
    Reg.RegistrationId,
    Reg.customerId,
    GCount.AdultsIncluded,
    GCount.ChildrenIncluded,
    AdultsRemaining = case when Reg.RegistrationType = 'Full' and GCount.AdultsIncluded < 2 then 2 - GCount.AdultsIncluded else 0 end,
    ChildrenRemaining = case when Reg.RegistrationType = 'Full' and GCount.ChildrenIncluded < 2 then 2 - GCount.ChildrenIncluded else 0 end
from
    dbo.Registrations Reg
    inner join GuestCountCTE GCount on Reg.RegistrationId = GCount.RegistrationId;

CTE构建已使用的预留数量,随后的INSERT语句直接从CTE获取该数据,然后推断剩余的预留数量。这里有几点需要注意:

  1. 你可以在没有CTE的情况下做到这一点。我已经使用了一个只是为了进一步打破这种方法,并避免必须每次两次写AdultsIncludedChildrenIncluded的表达式。

  2. 我在这里假设你永远不会有一个有&#34;额外成人&#34;尚未使用其成人包含的成人,&#34;或者有一个&#34;额外的孩子&#34;没有使用过它的“儿童包含”和#34; s。如果情况并非如此,那么虽然整体方法仍然有效,但您需要更加明智地了解如何确定每个预订中剩余的可用空间。

  3. 请注意1表达式中添加的AdultsIncluded;代表首先进行注册的成年人。

  4. 这是我现在在@RegistrationDetail中的数据:

    RegistrationId   CustomerId   AdultsIncluded   ChildrenIncluded   AdultsRemaining   ChildrenRemaining
    1                00001        1                0                  0                 0
    2                00001        2                2                  0                 0
    3                00001        1                2                  1                 0
    4                00002        3                3                  0                 0
    5                00002        1                1                  1                 1
    6                00003        2                1                  0                 0
    7                00003        2                2                  0                 0
    

    所以现在我需要的是任何至少有一个预订的客户的标识符,其中出现在该预订中的成人和儿童的数量小于或等于所有未使用的成人和儿童空间的总数。该客户其他预订。这实际上并不那么难:

    with RemaindersByCustomerCTE as
    (
        select
            Detail.CustomerId,
            AdultsRemaining = sum(Detail.AdultsRemaining),
            ChildrenRemaining = sum(Detail.ChildrenRemaining)
        from
            @RegistrationDetail Detail
        group by
            Detail.CustomerId
    )
    select
        Rem.CustomerId
    from
        RemaindersByCustomerCTE Rem
    where
        exists
        (
            select 1
            from
                @RegistrationDetail Detail
            where
                Detail.AdultsIncluded <= (Rem.AdultsRemaining - Detail.AdultsRemaining) and
                Detail.ChildrenIncluded <= (Rem.ChildrenRemaining - Detail.ChildrenRemaining)
        );
    

    在这里,我首先使用CTE生成一个结果集,每个客户有一条记录,以及每个客户在所有预订时未使用的成人和子空间总数。 CTE产生:

    CustomerId   AdultsRemaining   ChildrenRemaining
    00001        1                 0
    00002        1                 1
    00003        0                 0
    

    最后,CTE之后的位使用半连接(即EXISTS)仅返回CustomerId值的集合,其中注册符合上面给出的条件。请特别注意此查询中的WHERE子句:我不能简单地写Detail.AdultsIncluded <= Rem.AdultsRemaining,因为Rem.AdultsRemaining所有上可用的成人空间数客户的预订,包括Detail记录所代表的预订,客户无法通过将客人从一个注册移动到同一注册的空白区域来节省资金。最终结果集是:

    CustomerId
    00001
    00002
    

    这看起来是否适合你?