web-dev-qa-db-ja.com

ビューでNOT NULL計算列がNULL可能と見なされるのはなぜですか?

テーブルがあります:

CREATE TABLE [dbo].[Realty](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [RankingBonus] [int] NOT NULL,
    [Ranking]  AS ([Id]+[RankingBonus]) PERSISTED NOT NULL
    ....
)

そしてビュー:

CREATE View  [dbo].[FilteredRealty] AS
 SELECT 
realty.Id as realtyId,
...
COALESCE(realty.Wgs84X, ruian_cobce.Wgs84X, ruian_obec.Wgs84X) as Wgs84X,
COALESCE(realty.Wgs84Y, ruian_cobce.Wgs84Y, ruian_obec.Wgs84Y) as Wgs84Y,
realty.Ranking,
...
FROM realty
JOIN Category ON realty.CategoryId = Category.Id
LEFT JOIN ruian_cobce ON realty.cobceId = ruian_cobce.cobce_kod
LEFT JOIN ruian_obec ON realty.obecId = ruian_obec.obec_kod
LEFT JOIN okres ON realty.okresId = okres.okres_kod
LEFT JOIN ExternFile ON realty.Id = ExternFile.ForeignId AND ExternFile.IsMain = 1
                     AND ExternFile.ForeignTable = 5
INNER JOIN Person ON realty.OwnerId = Person.Id
WHERE Person.ConfirmStatus = 1

FilteredRealtyビューを含むC#(LinqToSQL)のdbmlモデルがあります。 [Ranking]フィールドはnull許容のintとして認識されるため、データベースで何かを変更するたびに、生成されたコードの型を修正する必要があります。これは私と多くの手作業にとって非常にイライラします。

FilteredRealtyで使用される集計はありません( この関連する質問 に関して)。

Realty.Rankingがnull可能でない場合、ビューのRanking列がnull可能と見なされるのはなぜですか?

15
Tomas Kubes

[Ranking]フィールドは、計算列であるため「Nullable」と表示されています。はい、それはNOT NULLとして宣言されていますが、MSDNページ 計算列 の状態として、データベースエンジンはクエリ時にその決定を変更できます。

データベースエンジンは、使用された式に基づいて、計算列のNULL可能性を自動的に決定します。 null可能でない列のみが存在する場合でも、ほとんどの式の結果はnull可能と見なされます。これは、アンダーフローまたはオーバーフローによってnullの結果も生成されるためです。 AllowsNullプロパティを指定してCOLUMNPROPERTY関数を使用し、テーブル内の計算された列のnull可能性を調査します。 NULL可能である式は、ISNULL(check_expressionconstant)を指定することにより、null不可の式に変換できます。ここで、constantは、nullの結果の代わりにnull以外の値です。

それで、これが本当かどうか見てみましょう:

CREATE TABLE [dbo].[Realty](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [RankingBonus] [int] NOT NULL,
    [Ranking]  AS ([Id]+[RankingBonus]) PERSISTED NOT NULL
);
GO

EXEC sp_help 'dbo.Realty';
-- Ranking: Nullable = "no"

SELECT COLUMNPROPERTY(OBJECT_ID(N'dbo.Realty'), N'Ranking', 'AllowsNull') AS [AllowsNull?];
-- 0

SELECT * FROM sys.dm_exec_describe_first_result_set(N'SELECT * FROM dbo.Realty', '', NULL);
-- Ranking: is_nullable = 1  ==  :-(

次に、ISNULLに関するアドバイスが機能するかどうかを確認します。

SELECT * FROM sys.dm_exec_describe_first_result_set(
   N'SELECT Id, RankingBonus, ISNULL(Ranking, -99) AS [RealRanking] FROM dbo.Realty;',
   '',
   NULL);
-- RealRanking: is_nullable = 0

彼らのアドバイスは正確に思えるので、計算された列の定義にそれを適用してみましょう:

ALTER TABLE dbo.Realty
  ADD [RankingFixed] AS (ISNULL(([Id]+[RankingBonus]), -99))
  PERSISTED NOT NULL;
GO

次に、もう一度プロパティをチェックしますが、新しいフィールドについては次のようになります。

EXEC sp_help 'dbo.Realty';
-- RankingFixed: Nullable = "no"

SELECT COLUMNPROPERTY(OBJECT_ID(N'dbo.Realty'),
                      N'RankingFixed',
                      'AllowsNull') AS [AllowsNullsNow?];
-- 0

これは今のところポジティブに見えますが、元の定義でさえこれらの2つのチェックから「NOT NULL」と報告されました。それでは、実際のテストを試してみましょう。データベースエンジンが実行時にnull可能性をどのように決定するかです。

SELECT * FROM sys.dm_exec_describe_first_result_set(N'SELECT * FROM dbo.Realty', '', NULL);
-- RankingFixed: is_nullable = 0  ==  :-) WOO HOO!
20
Solomon Rutzky

Ranking計算列式がどのような状況でもNULLを返さないことを保証するには、適切なデフォルト値でISNULLにラップする必要があります。例えば:

Ranking AS ISNULL(Id + RankingBonus, 0) PERSISTED NOT NULL

NOT NULL制約は、テーブルが変更されたときに有効なテーブルおよびセッションレベルの設定のコンテキストで、永続化された値がnullでないことを保証します。

ただし、クエリがその式を参照する場合、SQL Serverは永続的な値を使用する(設定が一致する場合)か、式を新たに計算するかを選択できます。

たとえば、一部のセッション設定ではオーバーフローがNULLを返す可能性があるため、SQL Serverはこの可能性を考慮する必要があります。ビューを介してアクセスすると、SQL Serverは列をNULLを返す可能性があるものとして正しくマークします。

式で最も外側のISNULLを使用することが、目的を達成するためにサポートされている唯一の方法です。たとえば、COALESCEを使用しても機能しません。

デモ:

CREATE TABLE dbo.T1
(
    c1 integer NOT NULL,
    c2 integer NOT NULL,
    c3 AS c1 + c2 PERSISTED NOT NULL
);
GO
CREATE VIEW dbo.V1
AS
SELECT T.c1,
       T.c2,
       T.c3
FROM dbo.T1 AS T;
GO
SELECT AllowsNull = COLUMNPROPERTY(OBJECT_ID(N'dbo.V1', N'V'), N'c3', 'AllowsNull');
GO
ALTER TABLE dbo.T1
DROP COLUMN c3;
GO
ALTER TABLE dbo.T1
ADD c3 AS ISNULL(c1 + c2, 0) PERSISTED NOT NULL;
GO
EXECUTE sys.sp_refreshsqlmodule
    @name = N'dbo.V1';
GO
SELECT AllowsNull = COLUMNPROPERTY(OBJECT_ID(N'dbo.V1', N'V'), N'c3', 'AllowsNull');
GO
DROP VIEW dbo.V1;
DROP TABLE dbo.T1;
GO

ビューはスキーマにバインドされていないため、sys.sp_refreshsqlmoduleの使用に注意してください。

13
Paul White 9