web-dev-qa-db-ja.com

SQLがwhere句で除外されている行を変換しようとするのはなぜですか?

私はこれに何度も遭遇しましたが、それには十分な理由があると確信していますが、どうすればそれを回避できますか?

isnumericの癖と関係があると思います。英語では、isnumeric(somefield) = 1でフィルタリングするビューがあります。次に、int句でwhereを使用してクエリを実行しようとすると、テーブルの他のフィールドに文字値があるため、全体が失敗します。

エラーを示す SQLフィドル を作成しました。

cast/convertビューの選択で、クエリエンジンはそれを無視するようです。

だから-なぜこれが起こるのですか、そしてそれに対処するためのきれいな方法はありますか?

元のフィドルは非常によく似たエラーを起こしました。これは、挿入部分に引用符がないためです。それは私が強調しようとしていた問題ではありません。挿入されるすべての値に引用符を追加して、ビューを修正しました。

7
IronicMuffin

ビューはクエリオプティマイザーによって「最適化」されているため、使用されていません。これは、クエリプランを見るとわかります。

enter image description here

インデックス付きマテリアライズドビューを作成してから、ターゲットクエリで NOEXPAND table hint を使用して、この最適化を防ぐことができます。

例:

USE tempdb;

IF OBJECT_ID(N'dbo.TestView', N'V') IS NOT NULL
BEGIN
    DROP VIEW dbo.TestView;
    DROP TABLE dbo.Test;
END
CREATE TABLE dbo.Test 
(
    TableKey int
    , TestField nvarchar(24)
);
GO

SCHEMABINDINGを使用してビューを作成します。

CREATE VIEW dbo.TestView 
WITH SCHEMABINDING
AS
    SELECT 
        t.TableKey
        , t.TestField
    FROM
       dbo.Test t
    WHERE ISNUMERIC(t.TestField) = 1;
GO

ビューにインデックスを作成します。

CREATE UNIQUE CLUSTERED INDEX PK_TestView
ON dbo.TestView (TableKey);
GO

テストデータを挿入します。

INSERT INTO dbo.Test
select 0,        '0'
union select 1,  'Rejected'
union select 2,  'Unlinked'
union select 0,  '0'
union select 3,  '1'
union select 162,'1000'
union select 16, '10000'
union select 17, '10010'
union select 18, '10011'
union select 19, '10012'
union select 20, '10031'
union select 21, '10041'

ビューをクエリします。

SELECT * 
FROM dbo.TestView WITH (NOEXPAND)
WHERE Testfield = 1000

結果:

╔══════════╦═══════════╗
║TableKey║TestField║
╠══════════ ╬═══════════╣
║162║1000║
╚══════════╩══════════ ═╝

クエリプラン:

enter image description here

上記のクエリプランのSELECTノードにある三角形の感嘆符に注意してください。これは警告です。

式の型変換(CONVERT_IMPLICIT(int、[tempdb]。[dbo]。[TestView]。[TestField]、0))は、クエリプランの選択の "CardinalityEstimate"に影響する可能性があります。式の型変換(CONVERT_IMPLICIT(int、[tempdb] 。[dbo]。[TestView]。[TestField]、0)= CONVERT_IMPLICIT(int、[@ 1]、0))は、クエリプランの選択で「SeekPlan」に影響を与える可能性があります

タイプ変換の警告は、ビューを次のように変更すると削除できます。

CREATE VIEW dbo.TestView 
WITH SCHEMABINDING
AS
    SELECT 
        t.TableKey
        , TestField = TRY_CONVERT(int, t.TestField)
    FROM
       dbo.Test t
    WHERE ISNUMERIC(t.TestField) = 1;

これで、ビューは整数として型指定されたTestField列を返します。永続データと組み合わせてNOEXPANDを使用しているため、暗黙的な型変換は行われません。

enter image description here

7
Max Vernon

他の回答で説明されているように、問題はINSERTステートメントにあり、それが修正された場合はビューが最適化されます。

その結果、クエリの_where Testfield = 1000_は、すべての行、つまり列のすべての値に対してTestFieldを数値にキャストしようとします( データ型優先規則 により)。

オプティマイザはクエリの書き換えを自由に行うことができ、スカラー式の評価のタイミング、順序、または評価数について 保証なし を提供するため、実行順序を強制することは非常に困難です。

これを回避する1つの方法は、CASE式を使用することです。この種の問題を回避することはまだ完全に保証されているわけではありませんが、ほとんどの場合は機能します。 sqlfiddle.com でテスト:

_CREATE VIEW dbo.TestView 
AS
  SELECT  
    TableKey,
    TestField = CASE WHEN IsNumeric(TestField) = 1 THEN TestField END
  FROM
    dbo.Test
  WHERE
    IsNumeric(TestField) = 1 ;
_

詳細な説明については、Aaron Bertrandによるこの回答も参照してください。 CTEエラー(nvarcharから数値) と、十分な最新バージョンを使用している場合は、より堅牢なTRY_CONVERT()を使用することをお勧めします。

7
ypercubeᵀᴹ

わかりました-私の質問では明確ではなかったかもしれませんが、私がやりたかった主なことは、引用符を使用せずにビューをクエリすることでした。

新しいバージョンのSQLとtry_convert関数のおかげで、これを実現できました。それを問題のフィールドのビューに追加した場合、引用符を使用せずに正常にクエリを実行できます。

変化を示すフィドル

奇妙なことに、フィドルは結果を返しません-しかし、私の実際のコードはこのように機能しているので、私はそれを勝利と呼びます。

2
IronicMuffin

ビューとは関係ありません。それは組合と関係があります。 SQL Serverは、ユニオンを(int, int)に入力しています。(int, nvarchar(24))に入力する必要があります。直感に反しますが、共用体の型は常に興味深いものです。あなたの例では、挿入は実際に失敗しています。

これを修正するには、テキストフィールドであるかのようにTestFieldにクエリを実行し(それが実際に行ったため)、SQL Serverを混乱させないように、挿入で値を正しく入力していることを確認してください。

insert into dbo.Test
select 0,   '0'
union select 1, 'Rejected'
union select 2, 'Unlinked'
union select 0, '0'
union select 3, '1'
union select 162,   '1000'
union select 16,    '10000'
union select 17,    '10010'
union select 18,    '10011'
union select 19,    '10012'
union select 20,    '10031'
union select 21,    '10041'
;
select * from dbo.Test
where Testfield = '1000';
1
Evan Carroll

sqlfiddle.comリンク を少し変更したバージョンを作成しました。

データは正しく入力され、ビューと同じステートメントを使用して選択できますが、id = 1000がまだ失敗するビューからの選択は失敗します。

どうして?それはオプティマイザの責任です。

オプティマイザはビュー全体を確認し、基本的に以下を扱います。

_SELECT TableKey, TestField
  FROM dbo.TestView
 WHERE TestField = 1000
;
_

まるで:

_SELECT TableKey, TestField
  FROM dbo.Test
 WHERE IsNumeric(TestField) = 1
   AND TestField = 1000
;
_

最適化を行う場合、返されるデータをより適切に制限するものを決定する必要があります。TestFieldのカーディナリティはIsNumeric(TestField)よりもはるかに優れています。

認めます、私はそこにいると思います。数値のみが一致する可能性があるため関数を無視するか、テーブルをスキャンして関数を実行せずに関数結果のカーディナリティを簡単に決定できないため、2番目に評価するだけです。

それでも、基本的な現実は、オプティマイザが数値以外の行を最初に除外しないことです。

ビューをインデックス付きビューにし、NOEXPANDテーブルヒントを使用した場合、期待どおりの結果が得られる可能性があります。オプティマイザは、テーブルに直接行くことを拒否し、必要なもののためにビューのインデックスを使用する必要があります。

最も単純な解決策は、暗黙的な変換を回避することです。 SQLに変換する側を残さない場合は、何が起こっているかを制御できます。

SQL 2012より前は、入力を文字列に強制する(_'1000'_の代わりに_1000_を使用)がおそらく最も簡単なソリューションです。

SQL 2012以降では、_TRY_CONVERT_を使用できます( sqlfiddle の最後のクエリを参照してください)。これは、可能な場合は変換された値を返し、できない場合はNULLを返します。もちろん、その時点で、ビューを廃止することができます。

そう:

  1. SQL Serverはビューをテーブルのように処理せず、ビューのWHERE句がクエリのWHERE句の前に評価されないように再配置できます。
  2. 暗黙の変換を信頼して、望みどおりの方法で実行しないでください。
1
RDFozz

次のようなcaseを使用して、値ではなくテストを短絡する必要があります。

  SELECT TableKey, TestField
  FROM dbo.Test
 WHERE 
   case when isnumeric(testfield)=1 then testfield else null end = 1000

パッティング

case when isnumeric(testfield)=1 then testfield else null end as newTestField

ビューで、それに対するselectでのテストも機能します。

1
LoztInSpace

基本的なものを取得しましょう。

  • ビューは、実行時に評価された方法で評価されます。
  • フィルターをチェックするには、数値かどうかに関係なく、すべての行を比較する必要があります。ビューに「最適化」はありません。ビューは、数値のチェックのためにすべての行の変換を強制します。
  • クエリと同じです。クエリはビューを冗長にします-したがって、チェックは論理的に排除されます。ただし、数値と比較すると、すべての行を変換する必要があります。

典型的な初心者の罠。 (一般的に)フィールドに対する関数は、最適化がないことを意味します(計算フィールドに対するインデックスなどの機能を使用しない限り)。このシナリオでの変換は、テーブルスキャンとすべての行の変換を強制しました。悪いテーブルとデータ設計があなたに噛み付くように戻ってきます。

RDFozzが言う のように、最善の方法は文字列比較を強制することです(LIKE '1000')これはデータにネイティブです。

1
TomTom