SQL Serverの予測不能な選択結果(dbmsエラー?)


37

以下に簡単な例を示します。これは奇妙な結果を返しますが、それは予測不可能であり、チームで説明することはできません。私たちは何か間違ったことをしていますか、それともSQL Serverエラーですか?

調査の結果、検索領域をsubqueryのunion句に減らしました。これにより、「men」テーブルから1つのレコードが選択されます。

SQL Server 2000では正常に機能しますが(12行を返します)、2008および2012では1行のみを返します。

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 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 isnull(men.wife_id、null)」(!!!)に変更すると、12が返されます(予想どおり)。
  • 奇妙な動作は、列「wife_id」のデータ型とは無関係のようです。はるかに多くのデータセットを持つ開発システムでそれを観察しました。
  • 「where wife_id> 0」は6行を返します
  • また、この種のステートメントを使用してビューの奇妙な動作を観察します。SELECT *は行のサブセットを返し、SELECT TOP 1000はすべてを返します

回答:


35

私たちは何か間違ったことをしていますか、それともSQL Serverエラーですか?

これは間違った結果のバグであり、通常のサポートチャネルから報告する必要があります。サポート契約を結んでいない場合は、Microsoftがバグとして動作を確認した場合に、有料のインシデントが通常払い戻されることを知ることが役立つ場合があります。

このバグには3つの要素が必要です。

  1. 外部参照を持つネストされたループ(適用)
  2. 外部参照をシークする内側の遅延インデックススプール
  3. 内側の連結演算子

たとえば、質問のクエリは次のような計画を作成します。

注釈付き計画

これらの要素の1つを削除する方法は多数あるため、バグは再現されなくなりました。

たとえば、オプティマイザがレイジーインデックススプールを使用しないことを選択することを意味するインデックスまたは統計を作成できます。または、ヒントを使用して、連結を使用する代わりに、ハッシュまたはマージ結合を強制することができます。クエリを書き換えて同じセマンティクスを表現することもできますが、その場合、1つ以上の必要な要素が欠落している異なるプラン形状になります。

詳細

遅延インデックススプールは、外部参照(相関パラメーター)値でインデックス付けされた作業テーブルに、内部結果行を遅延キャッシュします。レイジーインデックススプールが以前に見た外部参照を要求された場合、ワークテーブルからキャッシュされた結果行をフェッチします(「巻き戻し」)。スプールがこれまでに見たことのない外部参照値を要求された場合、現在の外部参照値でサブツリーを実行し、結果をキャッシュします(「再バインド」)。レイジーインデックススプールのシーク述語は、その作業テーブルのキーを示します。

この外部の参照が以前に見たものと同じであるかどうかをスプールがチェックするときに、この特定の計画形状で問題が発生します。Nested Loops Joinは、外部参照を正しく更新し、PrepRecomputeインターフェイスメソッドを介して内部入力のオペレーターに通知します。このチェックの開始時に、内部演算子はCParamBounds:FNeedToReloadプロパティを読み取り、外部参照が前回から変更されたかどうかを確認します。スタックトレースの例を以下に示します。

CParamBounds:FNeedToReload

上記のサブツリーが存在する場合、具体的には連結が使用される場合CParamBounds:FNeedToReload、外部参照が実際に変更されたかどうかに関係なく、常にfalseを返すようなバインディングで何らかの問題(おそらくByVal / ByRef / Copyの問題)が発生します。

同じサブツリーが存在するが、マージユニオンまたはハッシュユニオンが使用される場合、この必須プロパティは各反復で正しく設定され、レイジーインデックススプールは必要に応じて毎回巻き戻しまたは再バインドします。ちなみに、Distinct SortとStream Aggregateには問題はありません。私の疑いは、MergeとHash Unionが以前の値のコピーを作成するのに対して、連結は参照を使用することです。残念ながら、SQL Serverのソースコードにアクセスせずにこれを検証することはほぼ不可能です。

最終的な結果として、問題のあるプラン形状のレイジーインデックススプールは、常に現在の外部参照を既に見ていると考え、作業テーブルをシークすることで巻き戻し、通常は何も検出しないため、その外部参照に対して行は返されません。デバッガーで実行をステップ実行すると、スプールはそのRewindHelperメソッドのみを実行し、そのメソッドは実行しませんReloadHelper(このコンテキストではreload = rebind)。これは、実行計画で明らかです。スプールの下の演算子はすべて「実行数= 1」であるためです。

RewindHelper

もちろん、例外は、Lazy Index Spoolが指定される最初の外部参照です。これにより、常にサブツリーが実行され、結果テーブルが作業テーブルにキャッシュされます。後続のすべての反復では巻き戻しが行われ、現在の反復の外部参照の値が最初と同じ場合にのみ行(単一のキャッシュされた行)が生成されます。

したがって、ネストされたループ結合の外側に設定された任意の入力に対して、クエリは、処理された最初の行の複製と同じ数の行を返します(もちろん最初の行自体に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);

次の(簡単な)クエリは、Merge Unionを使用して、各行(合計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);

実行計画には問題のある形状があります。

連結計画

そして、結果は今では不正確で、たった3行です。

3行の結果

この動作は保証されていませんが、クラスター化インデックススキャンの最初の行のc1値は1です。この値を持つ行は他に2つあるため、合計で3つの行が生成されます。

次に、データテーブルを切り捨てて、「最初の」行の重複を追加してロードします。

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
はい、これは動作しますが、私のバージョンも動作するはずです。上記の抽象的な例は、本番クエリのはるかに単純化されたバージョンであり、これは非常に理にかなっています。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.