私はこれに何度も遭遇しましたが、それには十分な理由があると確信していますが、どうすればそれを回避できますか?
isnumeric
の癖と関係があると思います。英語では、isnumeric(somefield) = 1
でフィルタリングするビューがあります。次に、int
句でwhere
を使用してクエリを実行しようとすると、テーブルの他のフィールドに文字値があるため、全体が失敗します。
エラーを示す SQLフィドル を作成しました。
cast/convert
ビューの選択で、クエリエンジンはそれを無視するようです。
だから-なぜこれが起こるのですか、そしてそれに対処するためのきれいな方法はありますか?
元のフィドルは非常によく似たエラーを起こしました。これは、挿入部分に引用符がないためです。それは私が強調しようとしていた問題ではありません。挿入されるすべての値に引用符を追加して、ビューを修正しました。
ビューはクエリオプティマイザーによって「最適化」されているため、使用されていません。これは、クエリプランを見るとわかります。
インデックス付きマテリアライズドビューを作成してから、ターゲットクエリで 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║ ╚══════════╩══════════ ═╝
クエリプラン:
上記のクエリプランの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
を使用しているため、暗黙的な型変換は行われません。
他の回答で説明されているように、問題は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()
を使用することをお勧めします。
わかりました-私の質問では明確ではなかったかもしれませんが、私がやりたかった主なことは、引用符を使用せずにビューをクエリすることでした。
新しいバージョンのSQLとtry_convert
関数のおかげで、これを実現できました。それを問題のフィールドのビューに追加した場合、引用符を使用せずに正常にクエリを実行できます。
奇妙なことに、フィドルは結果を返しません-しかし、私の実際のコードはこのように機能しているので、私はそれを勝利と呼びます。
ビューとは関係ありません。それは組合と関係があります。 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';
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を返します。もちろん、その時点で、ビューを廃止することができます。
そう:
WHERE
句がクエリのWHERE
句の前に評価されないように再配置できます。次のような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
でのテストも機能します。
基本的なものを取得しましょう。
典型的な初心者の罠。 (一般的に)フィールドに対する関数は、最適化がないことを意味します(計算フィールドに対するインデックスなどの機能を使用しない限り)。このシナリオでの変換は、テーブルスキャンとすべての行の変換を強制しました。悪いテーブルとデータ設計があなたに噛み付くように戻ってきます。
RDFozzが言う のように、最善の方法は文字列比較を強制することです(LIKE '1000'
)これはデータにネイティブです。