SQL - 查找一对行是否不存在的最有效方法

SQL - most efficient way to find if a pair of row does NOT exist

我好像在网上找不到和我类似的情况。我有一个用于 'orders' 的 table,称为订单,还有一个 table 用于这些订单的详细信息,称为 'order detail'。某类订单的定义是,如果它有两对订单明细(Value-Unit 对)中的 1 个。因此,我的订单详细信息 table 可能如下所示:

order_id | detail
---------|-------
1        | X  
1        | Y
1        | Z
2        | X
2        | Z
2        | B
3        | A
3        | Z
3        | B

在一起的两对是 (X & Y) 和 (A & B)。仅检索不包含其中任何一对的 order_ids 的有效方法是什么?例如对于上面的table,我只需要接收order_id 2.

我能想出的唯一解决方案基本上是使用两个查询并执行自连接:

select distinct o.order_id
from orders o
where o.order_id not in (
    select distinct order_id
    from order_detail od1 where od1.detail=X
    join order_detail od2 on od2.order_id = od1.order_id and od2.detail=Y
) 
and o.order_id not in (
    select distinct order_id
    from order_detail od1 where od1.detail=A
    join order_detail od2 on od2.order_id = od1.order_id and od2.detail=B
)

问题是性能问题,我的 order_detail table 是巨大的,而且我对查询语言非常缺乏经验。有没有更快的方法以较低的基数来做到这一点?我对 table 的模式也有零控制,所以我不能在那里改变任何东西。

我会使用聚合和 having:

select order_id
from order_detail od
group by order_id
having sum(case when detail in ('X', 'Y') then 1 else 0 end) < 2 and
       sum(case when detail in ('A', 'B') then 1 else 0 end) < 2;

这假定订单没有具有相同 detail 的重复行。如果可以的话:

select order_id
from order_detail od
group by order_id
having count(distinct case when detail in ('X', 'Y') then detail end) < 2 and
       count(distinct case when detail in ('A', 'B') then detail end) < 2;

首先我想强调的是找到最有效的查询是一个好的查询一个好的组合指数。我经常在这里看到人们希望魔术只在其中发生的问题。

例如在各种解决方案中,当没有索引时,您的解决方案是最慢的(在修复语法错误之后),但是在 (detail, order_id)

上有索引要好得多

另请注意,您拥有实际数据和 table 结构。您需要尝试各种查询和索引的组合,以找到最有效的方法;尤其是因为您没有指明您使用的是什么平台,而且平台之间的结果可能会有所不同。

[/ranf-off]


查询

事不宜迟,戈登·利诺夫 (Gordon Linoff) 提供了一些不错的 。还有另一种选择可能会提供类似的性能。你说你无法控制模式;但您可以使用子查询将数据转换为 'friendlier structure'.

具体来说,如果您:

  • 旋转数据,使每个 order_id
  • 有一行
  • 以及您要检查的每个 detail 的列
  • 交集是有多少订单具有该详细信息...

那么您的查询很简单:where (x=0 or y=0) and (a=0 or b=0)。下面使用SQL服务器的临时tables以示例数据进行演示。无论是否有重复的 id, val 对,下面的查询都有效。

/*Set up sample data*/
declare @t table (
    id int,
    val char(1)
)
insert @t(id, val)
values  (1, 'x'), (1, 'y'), (1, 'z'),
        (2, 'x'), (2, 'z'), (2, 'b'),
        (3, 'a'), (3, 'z'), (3, 'b')

/*Option 1 manual pivoting*/
select  t.id
from    (
        select  o.id,
                sum(case when o.val = 'a' then 1 else 0 end) as a,
                sum(case when o.val = 'b' then 1 else 0 end) as b,
                sum(case when o.val = 'x' then 1 else 0 end) as x,
                sum(case when o.val = 'y' then 1 else 0 end) as y
        from    @t o
        group by o.id
        ) t
where   (x = 0 or y = 0) and (a = 0 or b = 0)

/*Option 2 using Sql Server PIVOT feature*/
select  t.id
from    (
        select  id ,[a],[b],[x],[y]
        from    (select id, val from @t) src
                pivot (count(val) for val in ([a],[b],[x],[y])) pvt
        ) t
where   (x = 0 or y = 0) and (a = 0 or b = 0)

值得注意的是,上面选项 1 和选项 2 的查询计划略有不同。这表明在大型数据集上可能存在不同的性能特征。


索引

请注意,上面的代码可能会处理整个 table。因此,从索引中几乎没有收获。但是,如果 table 有“长行”,则只有您正在使用的 2 列的索引意味着需要从磁盘读取的数据较少。

您提供的查询结构可能受益于 (detail, order_id) 等索引。这是因为服务器可以更有效地检查 NOT IN 子查询条件。受益程度取决于 table.

中的数据分布

作为旁注,我测试了各种查询选项,包括你的和 Gordon 的固定版本。 (虽然数据量很小。)

  • 如果没有上述索引,您的查询是这批查询中最慢的。
  • 使用上述索引,Gordon 的第二个查询是最慢的。

替代查询

您的查询(固定):

select distinct o.id
from @t o
where o.id not in (
    select  od1.id
    from    @t od1 
            inner join @t od2 on 
                od2.id = od1.id
            and od2.val='Y'
    where   od1.val= 'X'
) 
and o.id not in (
    select  od1.id
    from    @t od1 
            inner join @t od2 on 
                od2.id = od1.id
            and od2.val='a'
    where   od1.val= 'b'
)

Gordon 的第一个和第二个查询的混合。修复了第一个中的重复问题和第二个中的性能问题:

select id
from @t od
group by id
having (    sum(case when val in ('X') then 1 else 0 end) = 0
         or sum(case when val in ('Y') then 1 else 0 end) = 0
        )
    and(    sum(case when val in ('A') then 1 else 0 end) = 0
         or sum(case when val in ('B') then 1 else 0 end) = 0
        )

使用 INTERSECT 和 EXCEPT:

select  id
from    @t
except
(
    select  id
    from    @t
    where   val = 'a'
    intersect
    select  id
    from    @t
    where   val = 'b'
)
except
(
    select  id
    from    @t
    where   val = 'x'
    intersect
    select  id
    from    @t
    where   val = 'y'
)