sql >> データベース >  >> RDS >> Sqlserver

悪い習慣:SQLServerでのNULLの回避

    ずっと前に、私はStack ExchangeのNULLについて、「なぜNULLを許可すべきではないのですか?」というタイトルの質問に答えました。私はペットのおしっこや情熱を共有しており、NULLの恐れは私のリストのかなり上位にあります。同僚が最近、NULLを許可する代わりに空の文字列を強制する設定を表明した後、私に言いました:

    「コードでnullを処理するのは好きではありません。」

    申し訳ありませんが、それは正当な理由ではありません。プレゼンテーション層が空の文字列またはNULLを処理する方法は、テーブルデザインとデータモデルの推進力になるべきではありません。また、一部の列で「値の不足」を許可している場合、論理的な観点から、「値の不足」が長さゼロの文字列で表されているか、NULLで表されているかは重要ですか。さらに悪いことに、整数の場合は0または-1、日付の場合は1900-01-01のようなトークン値ですか?

    Itzik Ben-Ganは最近、NULLに関するシリーズ全体を作成しました。すべてを確認することを強くお勧めします:

    • NULLの複雑さ–パート1
    • NULLの複雑さ–パート2
    • NULLの複雑さ–パート3、欠落している標準機能とT-SQLの代替
    • NULLの複雑さ–パート4、標準の一意性制約がありません

    しかし、ここでの私の目的は、別のStack Exchangeの質問でトピックが取り上げられた後、それよりも少し複雑ではありません。「既存のテーブルにautonowフィールドを追加する」。そこで、ユーザーは、現在の日付/時刻を自動入力することを目的として、既存のテーブルに新しい列を追加していました。彼らは、既存のすべての行のその列にNULLを残すか、デフォルト値を設定する必要があるのか​​疑問に思いました(おそらく、明示的ではありませんが、1900-01-01など)。

    知っている人にとっては、トークン値に基づいて古い行を除外するのは簡単かもしれません。結局のところ、ある種のBluetooth doodadが1900-01-01に製造または購入されたと誰が信じることができるでしょうか?これは、ビューで任意の日付を使用して魔法のフィルターとして機能し、値が信頼できる行のみを表示する現在のシステムで見られました。実際、これまで見てきたすべてのケースで、WHERE句の日付は、列(またはそのデフォルトの制約)が追加された日時です。これはすべて問題ありません。問題を解決する最善の方法ではないかもしれませんが、 a 方法。

    ただし、ビューからテーブルにアクセスしていない場合は、既知のこの意味 値は、論理的な問題と結果に関連する問題の両方を引き起こす可能性があります。論理的な問題は、テーブルを操作する誰かが1900-01-01が「不明」または「関連性がない」を表す偽のトークン値であることを知っている必要があるということです。実際の例では、1970年代にプレイしたクォーターバックの、そのようなものを測定または追跡する前の平均リリース速度(秒単位)はどれくらいでしたか? 0は「不明」の適切なトークン値ですか? -1はどうですか?または100?日付に戻ると、IDのない患者が病院に入院し、意識がない場合、生年月日として何を入力する必要がありますか? 1900-01-01は良い考えではないと思います。それが本当の誕生日である可能性が高かった頃は、確かに良い考えではありませんでした。

    トークン値のパフォーマンスへの影響

    パフォーマンスの観点から、1900-01-01や9999-21-31のような偽の値または「トークン」値は問題を引き起こす可能性があります。上記の最近の質問に大まかに基づいた例で、これらのいくつかを見てみましょう。ウィジェットテーブルがあり、保証が戻った後、新しい行の現在の日付/時刻を入力するEnteredService列を追加することにしました。 1つのケースでは、既存のすべての行をNULLのままにし、もう1つのケースでは、値を魔法の1900-01-01の日付に更新します。 (ここでは、あらゆる種類の圧縮を会話から除外します。)

      CREATE TABLE dbo.Widgets_NULL
      (
        WidgetID     int IDENTITY(1,1) NOT NULL,
        SerialNumber uniqueidentifier NOT NULL DEFAULT NEWID(),
        Description  nvarchar(500),
        CONSTRAINT   PK_WNULL PRIMARY KEY (WidgetID)
      );
     
      CREATE TABLE dbo.Widgets_Token
      (
        WidgetID     int IDENTITY(1,1) NOT NULL,
        SerialNumber uniqueidentifier NOT NULL DEFAULT NEWID(),
        Description  nvarchar(500),
        CONSTRAINT   PK_WToken PRIMARY KEY (WidgetID)
      );

    次に、同じ100,000行を各テーブルに挿入します。

      INSERT dbo.Widgets_NULL(Description) 
      OUTPUT inserted.Description INTO dbo.Widgets_Token(Description)
      SELECT TOP (100000) LEFT(OBJECT_DEFINITION(o.object_id), 250)
        FROM master.sys.all_objects AS o 
        CROSS JOIN (SELECT TOP (50) * FROM master.sys.all_objects) AS o2
        WHERE o.[type] IN (N'P',N'FN',N'V')
          AND OBJECT_DEFINITION(o.object_id) IS NOT NULL;
    >

    次に、新しい列を追加し、既存の値の10%を現在の日付の分布で更新し、残りの90%をいずれかのテーブルのトークン日付に更新します。

      ALTER TABLE dbo.Widgets_NULL  ADD EnteredService datetime;
      ALTER TABLE dbo.Widgets_Token ADD EnteredService datetime;
      GO
     
      UPDATE dbo.Widgets_NULL  
        SET EnteredService = DATEADD(DAY, WidgetID/250, '20200101') 
        WHERE WidgetID > 90000;
     
      UPDATE dbo.Widgets_Token 
        SET EnteredService = DATEADD(DAY, WidgetID/250, '20200101') 
        WHERE WidgetID > 90000;
     
      UPDATE dbo.Widgets_Token 
        SET EnteredService = '19000101'
        WHERE WidgetID <= 90000;

    最後に、インデックスを追加できます:

      CREATE INDEX IX_EnteredService ON dbo.Widgets_NULL (EnteredService);
      CREATE INDEX IX_EnteredService ON dbo.Widgets_Token(EnteredService);

    使用スペース

    データ型の選択、断片化、トークン値とNULLについて話すとき、私はいつも「ディスクスペースは安い」と聞きます。私の懸念は、これらの余分な無意味な値が占めるディスク容量についてはそれほど重要ではありません。さらに、テーブルが照会されると、メモリが無駄になります。ここで、列とインデックスが追加される前後にトークン値が消費するスペースの量を簡単に把握できます。

    列を追加し、インデックスを追加した後のテーブルの予約スペース。スペースはトークン値でほぼ2倍になります。

    クエリの実行

    必然的に、誰かがテーブル内のデータについて推測し、EnteredService列に対して、そこにあるすべての値が正当であるかのようにクエリを実行します。例:

      SELECT COUNT(*) FROM dbo.Widgets_Token  
        WHERE EnteredService <= '20210101';
     
      SELECT COUNT(*) FROM dbo.Widgets_NULL 
        WHERE EnteredService <= '20210101';

    トークン値は、場合によっては見積もりを混乱させる可能性がありますが、さらに重要なことに、誤った(または少なくとも予期しない)結果が生成されます。トークン値を持つテーブルに対するクエリの実行プランは次のとおりです。

    トークンテーブルの実行プラン。コストが高いことに注意してください。

    そして、NULLを含むテーブルに対するクエリの実行プランは次のとおりです。

    NULLテーブルの実行プラン。見積もりは間違っていますが、コストははるかに低くなっています。

    クエリが>={some date}を要求し、9999-12-31が不明を表す魔法の値として使用された場合、同じことが逆に起こります。

    繰り返しになりますが、特にトークン値を使用したために結果が間違っていることを知っている人にとっては、これは問題ではありません。しかし、それを知らない他のすべての人(将来の同僚、コードの他の継承者や保守者、さらにはメモリの問題を抱えている将来のあなたさえも)はおそらくつまずくでしょう。

    結論

    列でNULLを許可する(またはNULLを完全に回避する)という選択は、イデオロギー的または恐怖に基づく決定に限定されるべきではありません。値がNULLにならないようにデータモデルを設計したり、まったく保存されなかった可能性のあるものを表すために無意味な値を使用したりすることには、実際の具体的な欠点があります。モデルのすべての列でNULLを許可する必要があることを示唆しているわけではありません。 アイデアに反対しないというだけです NULLの。


    1. 日常生活でデータベースをどのように使用するか

    2. メモリ/ストレージテクノロジ階層とSQLServer

    3. MySQLテーブルの列のサイズを変更するにはどうすればよいですか?

    4. 既存のスキーマからテーブル関係図を生成する(SQL Server)