SQL Server不可预测的选择结果(dbms错误?)


37

下面是一个简单的示例,它返回的结果很奇怪,这些结果是无法预测的,我们无法在团队中进行解释。我们是在做错什么还是SQL Server错误?

经过一番调查,我们将搜索范围缩小为子查询中的union子句,该子查询从“ men”表中选择一条记录

它可以在SQL Server 2000中正常工作(返回12行),但是在2008年和2012年,它仅返回一行。

create table dual (dummy int)

insert into dual values (0)

create table men (
man_id int,
wife_id int )

-- there are 12 men, 6 married 
insert into men values (1, 1)
insert into men values (2, 2)
insert into men values (3, null)
insert into men values (4, null)
insert into men values (5, null)
insert into men values (6, 3)
insert into men values (7, 5)
insert into men values (8, 7)
insert into men values (9, null)
insert into men values (10, null)
insert into men values (11, null)
insert into men values (12, 9)

仅返回一行:1 1 2

select 
man_id,
wife_id,
(select count( * ) from 
    (select dummy from dual
     union select men.wife_id  ) family_members
) as family_size
from men
--where wife_id = 2 -- uncomment me and try again

取消注释最后一行,它给出:2 2 2

有很多奇怪的行为:

  • 在一系列放置,创建,截断和插入“ men”表之后,它有时仍然起作用(返回12行)
  • 当将“ union select men.wife_id”更改为“ union all select men.wife_id”或“ union select isull(men.wife_id,null)”(!!!)时,它将返回12行(按预期)。
  • 奇怪的行为似乎与“ wife_id”列的数据类型无关。我们在具有更大数据集的开发系统上观察到了它。
  • “凡ifeid> 0”返回6行
  • 我们还通过这种陈述观察到了视图的奇怪行为。SELECT *返回行的子集,SELECT TOP 1000返回全部

Answers:


35

我们是在做错什么还是SQL Server错误?

这是一个错误结果错误,您应该通过通常的支持渠道进行报告。如果您没有支持协议,则可能会有所帮助,如果Microsoft将行为确认为错误,则通常可以退还已付费的事件

该错误需要三个要素:

  1. 带有外部引用的嵌套循环(适用)
  2. 寻求外部参考的内侧懒惰索引线轴
  3. 内部串联运算符

例如,问题中的查询产生如下的计划:

带注释的计划

有很多方法可以删除这些元素之一,因此该错误不再重现。

例如,可能创建索引或统计信息,这恰好意味着优化器选择不使用惰性索引假脱机。或者,可以使用提示来强制执行哈希或合并并集,而不是使用串联。也可以重写查询以表达相同的语义,但是这会导致缺少一个或多个所需元素的不同计划形状。

更多细节

惰性索引假脱机将内部结果行惰性缓存在由外部引用(相关参数)值索引的工作表中。如果向惰性索引假脱机请求了以前见过的外部引用,它将从其工作表中获取缓存的结果行(“倒带”)。如果要求线轴提供以前从未见过的外部参考值,则它将使用当前外部参考值运行其子树并缓存结果(“重新绑定”)。惰性索引假脱机上的查找谓词指示其工作表的键。

当线轴检查新的外部参照是否与以前看到的相同时,问题就出现在该特定的平面形状中。嵌套循环连接正确更新了其外部引用,并通过其PrepRecompute接口方法在其内部输入上通知了运算符。在此检查开始时,内部运算符会读取该CParamBounds:FNeedToReload属性,以查看外部引用是否自上次更改以来。示例堆栈跟踪如下所示:

CParamBounds:FNeedToReload

当上面显示的子树存在时,特别是在使用“连接”的地方,绑定发生了问题(可能是ByVal / ByRef / Copy问题)CParamBounds:FNeedToReload,无论外部引用是否实际更改,该绑定总是返回false。

如果存在相同的子树,但使用了合并联合或哈希联合,则此基本属性在每次迭代中都正确设置,并且懒惰索引假脱机每次都适当地倒回或重新绑定。顺便说一句,非重复排序和流聚合是无可指摘的。我的怀疑是Merge和Hash Union复制了先前的值,而Concatenation使用了引用。不幸的是,在没有访问SQL Server源代码的情况下几乎不可能进行验证。

最终结果是,有问题的计划形状中的惰性索引假脱机始终认为它已经看到了当前的外部参考,通过查找其工作表来回退,通常什么也找不到,因此没有为该外部参考返回任何行。在调试器中逐步执行该过程,假脱机程序只会执行其RewindHelper方法,而不会执行其ReloadHelper方法(在这种情况下,reload = rebind)。这在执行计划中很明显,因为线轴下的操作员都具有“执行次数= 1”。

倒带助手

当然,例外是,对于第一个外部参考,给出了惰性索引假脱机。这总是执行子树并将结果行缓存在工作表中。所有后续迭代都会导致倒回,当当前迭代的外部引用与第一次迭代具有相同的值时,它将仅生成一行(单个缓存的行)。

因此,对于嵌套循环联接外侧的任何给定输入集,查询将返回的行数与已处理的第一行的重复项一样多(当然,第一行本身也有一个重复项)。

演示版

表和样本数据:

CREATE TABLE #T1 
(
    pk integer IDENTITY NOT NULL,
    c1 integer NOT NULL,

    CONSTRAINT PK_T1
    PRIMARY KEY CLUSTERED (pk)
);
GO
INSERT #T1 (c1)
VALUES
    (1), (2), (3), (4), (5), (6),
    (1), (2), (3), (4), (5), (6),
    (1), (2), (3), (4), (5), (6);

以下(简单)查询使用合并联合为每行(总18个)产生正确的计数:2

SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY 
(
    SELECT COUNT_BIG(*) AS c1
    FROM
    (
        SELECT T1.c1
        UNION
        SELECT NULL
    ) AS U
) AS C;

合并联盟计划

如果现在添加查询提示以强制进行串联:

SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY 
(
    SELECT COUNT_BIG(*) AS c1
    FROM
    (
        SELECT T1.c1
        UNION
        SELECT NULL
    ) AS U
) AS C
OPTION (CONCAT UNION);

执行计划的形状有问题:

串联计划

现在结果不正确,只有三行:

三排结果

尽管不能保证此行为,但是“聚簇索引扫描”的第一行的c1值为1。其他两行都具有该值,因此总共产生了三行。

现在截断数据表,并在其中添加“第一”行的更多重复项:

TRUNCATE TABLE #T1;

INSERT #T1 (c1)
VALUES
    (1), (2), (3), (4), (5), (6),
    (1), (2), (3), (4), (5), (6),
    (1), (1), (1), (1), (1), (1);

现在,连接计划为:

8行串联计划

并且,如所示,将产生8行,所有行c1 = 1当然是:

8行结果

我注意到您已经为此错误打开了一个Connect项,但实际上,它不是报告对生产有影响的问题的地方。如果真是这样,您确实应该联系Microsoft支持。


此错误结果错误已在某个阶段修复。从2012年开始,它不再为我在任何版本的SQL Server上复制。它确实在SQL Server 2008 R2 SP3-GDR内部版本10.50.6560.0(X64)上进行复制。


-3

为什么使用不带from语句的子查询?我认为这可能会导致2005和2008服务器的差异。也许您可以使用显式联接?

select 
m1.man_id,
m1.wife_id,
(select count( * ) from 
    (select dummy from dual
     union
     select m2.wife_id
     from men m2
     where m2.man_id = m1.man_id) family_members
) as family_size
from men m1

3
是的,这可行,但是我的版本也应该可行。上面的抽象示例是我们的生产查询的简化版本,这更有意义。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.