【问题标题】:SQL non-duplicate join to table which uses nulls as wildcardsSQL 非重复连接到使用空值作为通配符的表
【发布时间】:2021-10-04 22:40:28
【问题描述】:

我正在查询使用空值作为通配符的表,如果另一行包含非通配符值,则可以覆盖它。例如:

表 1:客户 - 包含客户和相关产品:

create table #cust (
    id int not null,
    product varchar(3) not null
)

insert into #cust
values (1,'ABC'),(1,'DEF'),(1,'GHI')

============
id | product
------------
1  | ABC
1  | DEF
1  | GHI
============

表 2:回扣 - 包含客户从每种产品中获得的回扣。一个空产品字段指定了一个默认折扣,适用于所有产品,但明确指定的产品除外:

create table #rebate (
    cust_id int not null,
    product varchar(3) NULL,
    rebate numeric(5,2) not null
)

insert into #rebate
values (1,null,0.25),(1,'ABC',0.05)

==========================
cust_id | product | rebate
--------------------------
1       | null    | 0.25
1       | ABC     | 0.05
==========================

因此,这位客户 a 获得了所有产品的 25% 回扣,但他们获得 5% 的“ABC”除外。

我试图用这样一种简单的方式编写代码:

select * 
from #cust c
left join #rebate r
    on c.id = r.cust_id
        and c.product = isnull(r.product, c.product)

但是,这样做的结果是 ABC 产品上的重复(匹配 r.product 为 null,并且 r.product = c.product 部分的连接):

======================================
id  product cust_id  product    rebate
--------------------------------------
1   ABC     1        NULL       0.25    -- duplication
1   DEF     1        NULL       0.25
1   GHI     1        NULL       0.25
1   ABC     1        ABC        0.05    -- duplication. This is the row needed
=======================================

有什么建议吗?

【问题讨论】:

  • 那么你想要的逻辑是什么? 2 行不是重复的,因为它们明显不同; productrebate 的值不同。 JOIN 也按预期工作,好像 JOIN 在另一个表中有 2 行匹配,你得到 2 行,而不是 1。
  • 你想达到什么目的?特定客户的价目表?
  • 谢谢大家,我同意上述观点 - SQL 正在按照逻辑运行,但所需的输出是每个客户、每个产品的单个回扣 - 指定 产品回扣应覆盖默认回扣。以下 Marc Guillot 和 Thorsten Kettner 的建议都是正确的。现在正在测试它们。再次感谢!
  • 旁注:在现实世界中,您当然不应该调用表#cust,因为这不是客户表(每个客户一行)。例如,您可以称之为customer_product。并且列id 不应称为id,因为如果它是表的ID,则表中每个ID 将只有一行。你应该叫它cust_id 之类的。
  • 如果特定产品的返利数量往往明显低于每位客户的产品数量,我会使用双左连接方法,但将返利保留在两个表中(客户级别返利,和客户产品级别的回扣),而不是试图将它们合并为一个。我希望它更容易理解、更容易维护、更容易适应等等。

标签: sql sql-server tsql join


【解决方案1】:

您可以有条件地加入加入返利。您先加入特定返利,然后对未找到特定返利的产品加入默认返利。

select c.*, coalesce(specific_r.rebate, default_r.rebate) as rebate
from #cust c
     left join #rebate specific_r on 
               c.id = specific_r.cust_id and c.product = specific_r.product
     left join #rebate default_r on 
               c.id = default_r.cust_id and default_r.product is null and 
               specific_r.cust_id is null  -- No specific rebate found

这会给你想要的结果:

==================
id  product rebate
------------------
1   ABC     0.05
1   DEF     0.25
1   GHI     0.25
==================

【讨论】:

  • 谢谢马克。我非常喜欢这个,但在我看来,下面的 Thorsten 解决方案只是稍微优雅一点。你怎么看?
  • @PeterJohnson 尝试两者并测试性能,它们都是此类问题的常见解决方案
  • @PeterJohnson Thorsten 解决方案是一个相关子查询,所以我的建议应该会给你更好的性能。他的解决方案非常优雅,但我会将其用作排名 CTE,就像这里建议的那样:stackoverflow.com/questions/3800551/…
【解决方案2】:

你想要排名。有一般回扣和特定回扣,如果存在的话,您更喜欢特殊回扣而不是一般回扣。有几种方法可以解决这个问题,它们主要归结为选择候选人(每个客户一到两行),然后选择更好的候选人。这是一个横向连接的解决方案(在 SQL Server 中称为 OUTER APPLY):

select * 
from #cust c
outer apply
(
  select top(1) * -- TOP(1) picks the row brought up first by ORDER BY
  from #rebate r
  where r.cust_id = c.id
  and (r.product = c.product or r.product is null)
  order by r.product desc -- DESC orders NULLs last in SQL Server
) rr;

SQL Server 缺少用于 ORDER BYNULLS LAST 子句,但您通过降序排序最后得到空值。

【讨论】:

  • Wunderbar Thorsten。非常感谢这个不错的解决方案
猜你喜欢
  • 2015-10-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-29
  • 1970-01-01
  • 2020-09-13
  • 2021-05-08
  • 1970-01-01
相关资源
最近更新 更多