結合された仮想テーブルのNEWID()により、意図しないクロス適用動作が発生する


9

私の実際の作業クエリは内部結合でしたが、クロス結合を使用したこの単純な例では、ほとんど常に問題が再現されているようです。

SELECT *
FROM (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )
CROSS JOIN (
    SELECT NEWID() TEST_ID
) BB ( B )

私の内部結合では、NEWID()関数を使用して各行にGUIDを追加した多くの行があり、そのような行の約9に対して、2行の仮想テーブルとの乗算により期待される結果が生成されました。同じGUID。10のうち1つは異なる結果を生成します。これは控えめに言っても予想外であり、テストデータ生成スクリプトでこのバグを見つけるのに本当に苦労しました。

非決定的なgetdate関数とsysdatetime関数を使用して次のクエリを見ると、これは表示されません。私はとにかく、両方の最終結果行に常に同じdatetime値を表示しています。

SELECT *
FROM (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )
CROSS JOIN (
    SELECT GETDATE() TEST_ID
) BB ( B )

SELECT *
FROM (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )
CROSS JOIN (
    SELECT SYSDATETIME() TEST_ID
) BB ( B )

私は現在SQL Server 2008を使用していますが、今の私の回避策は、ランダムデータ生成スクリプトを完了する前に、GUIDを含む行をテーブル変数に読み込むことです。仮想テーブルではなくテーブルの値としてそれらを取得すると、問題はなくなります。

回避策はありますが、実際のテーブルやテーブル変数なしで回避策を探しています。

これを書いている間、私は成功せずにこれらの可能性を試しました:1)ネストされた仮想テーブルにnewid()を配置します:

SELECT *
FROM (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )
CROSS JOIN (
    SELECT TEST_ID
    FROM (
        SELECT NEWID() TEST_ID
    ) TT
) BB ( B )

2)次のようなキャスト式内でnewid()をラップします。

SELECT CAST(NEWID() AS VARCHAR(100)) TEST_ID

3)結合式内の仮想テーブルの出現順序を逆にする

SELECT *
FROM (
    SELECT NEWID() TEST_ID
) BB ( B )
CROSS JOIN (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )

4)相関のない相互適用の使用

SELECT *
FROM (
    SELECT NEWID() TEST_ID
) BB ( B )
CROSS APPLY (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )

ついにこの質問を投稿する直前に、今私はこれを試して成功したようです、相関のあるクロスが適用されます:

SELECT *
FROM (
    SELECT NEWID() TEST_ID
) BB ( B )
CROSS APPLY (
    SELECT A
    FROM (
        SELECT 1 UNION ALL
        SELECT 2
    ) TT ( A )
    WHERE BB.B IS NOT NULL
) AA ( A )

誰か他のよりエレガントでシンプルな回避策がありますか?必要がない場合は、単純な行の乗算に相互適用または相関を使用したくありません。

回答:


20

この動作は仕様です。詳細は、このConnectバグレポートで説明されています。最も適切なMicrosoftの返信は、便宜上(そしてリンクがいつか死ぬ場合のために)以下に再現されています。

マイクロソフトが2008/7/7午前9:27に投稿

ループを閉じる。。。この質問については、Devチームと話し合いました。そして、最終的には、次の理由により、現在の動作を変更しないことを決定しました。

  1. オプティマイザは、スカラー関数の実行のタイミングまたは数を保証しません。これは長い間確立された信条です。これは、オプティマイザがクエリプランの実行を大幅に改善するのに十分な自由を可能にする基本的な「余裕」です。

  2. この「行ごとに1回の動作」は、あまり議論されていませんが、新しい問題ではありません。Yukonリリースでは、その動作を微調整し始めました。しかし、正確に、すべての場合において、それが意味することを正確に特定することは非常に困難です!たとえば、最終結果に「途中」で計算された中間行に適用されますか?-その場合、それは明らかに選択した計画に依存します。または、最終的に完了した結果に表示される行にのみ適用されますか?-あなたは同意するだろうと私は確信しているので、ここで起こっている厄介な再帰があります!

  3. 先に述べたように、デフォルトでは「パフォーマンスを最適化する」-これは99%のケースに適しています。結果が変わる可能性のあるケースの1%は、かなり簡単に特定できます-NEWIDなどの副作用の「関数」-簡単に「修正」できます(結果として、パフォーマンスのトレーディング)。この「パフォーマンスを最適化する」というデフォルトは、長い間確立され、受け入れられています。(はい、それは従来のプログラミング言語のコンパイラによって選択されたスタンスではありませんが、そうです)。

したがって、推奨事項は次のとおりです。

  1. 非保証のタイミングと実行回数のセマンティクスへの依存を避けます。
  2. NEWID()をテーブル式の奥深くで使用しないでください。
  3. OPTIONを使用して特定の動作を強制する(トレーディングパフォーマンス)

この説明が、このバグを「修正しない」としてクローズした理由を明確にするのに役立つことを願っています。

機能は確かに非決定論的であるが、これらは次のように扱われ、実行時定数特定のクエリのために。大まかに言うと、これはクエリの実行が開始れると関数の値がキャッシュされ、その結果がクエリ内のすべての参照に再利用されることを意味します。GETDATESYSDATETIME

問題の「回避策」はどれも安全ではありません。次回のプランのコンパイル時、次回のサービスパックまたは累積的な更新の適用時などに、動作が変更されないという保証はありません。

唯一の安全な解決策は、ある種の一時オブジェクト(変数、テーブル、マルチステートメント関数など)を使用することです。観察に基づいて今日動作すると思われる回避策を使用することは、通常、日曜日の午前3時にページングアラートの形式で、将来予期しない動作を経験する優れた方法です。


「問題の「回避策」はどれも安全ではありません。」同上。それらの1つを実際に機能しているクエリに適用しようとしても、まったく役に立ちませんでした。
JM Hicks
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.