マルチテナントSQL Serverデータベースの複合主キー


16

ASP Web API、Entity Framework、およびSQL Server / Azureデータベースを使用して、マルチテナントアプリ(単一データベース、単一スキーマ)を構築しています。このアプリは、1000〜5000人の顧客が使用します。すべてのテーブルにはTenantId(Guid / UNIQUEIDENTIFIER)フィールドがあります。現在、私はId(Guid)という単一フィールドの主キーを使用しています。しかし、Idフィールドのみを使用することで、ユーザーから提供されたデータが正しいテナントからのものであるかどうかを確認する必要があります。たとえばSalesOrderCustomerIdフィールドを持つテーブルがあります。ユーザーが販売注文を投稿/更新するたびにCustomerId、同じテナントからのものかどうかを確認する必要があります。各テナントに複数のコンセントがあるため、状況はさらに悪化します。次に、確認する必要がTenantIdありOutletIdます。これは本当にメンテナンスの悪夢であり、パフォーマンスに悪影響を及ぼします。

TenantIdとともに主キーに追加することを考えていIdます。また、おそらく追加OutletIdします。だから、主キーSalesOrder:テーブルになりますIdTenantIdOutletId。このアプローチの欠点は何ですか?複合キーを使用すると、パフォーマンスが大幅に低下しますか?複合キーの順序は重要ですか?私の問題に対するより良い解決策はありますか?

回答:


34

大規模なマルチテナントシステム(18以上のサーバーに顧客が分散し、各サーバーが同一のスキーマ、異なる顧客、およびサーバーごとに1秒間に数千のトランザクションを持つフェデレーションアプローチ)に取り組んでいると言えます。

  1. "TenantID"とエンティティ "ID"の両方のIDとしてGUIDを選択することに同意する人々(少なくとも少数)がいます。しかし、いいえ、良い選択ではありません。他のすべての考慮事項は別として、その選択だけがいくつかの方法で傷つきます:断片化、膨大な無駄なスペース(エンタープライズストレージ、SAN、または各データページにより時間がかかるクエリについて考えるとディスクが安いとは言わないでください)保持する行の数が少ない場合INTBIGINT偶数の場合、サポートやメンテナンスが難しい場合などです。GUIDは移植性に優れています。データは何らかのシステムで生成され、別のシステムに転送されますか?そうでない場合には、(例えば、よりコンパクトなデータ・タイプに切り替えてTINYINTSMALLINTINT、またはであってもBIGINT)を介して順次インクリメントIDENTITYまたはSEQUENCE

  2. 項目1が邪魔にならないので、ユーザーデータがあるすべてのテーブルにTenantIDフィールドが必要です。そうすれば、追加のJOINを必要とせずに何でもフィルタリングできます。これは、クライアントデータテーブルに対するすべてのクエリTenantIDが、JOIN条件またはWHERE句、あるいはその両方を持つ必要があることも意味します。これは、異なる顧客からのデータを誤って混合したり、テナントBからテナントAのデータを表示したりしないことを保証するのにも役立ちます。

  3. IdとともにTenantIdを主キーとして追加することを考えています。また、OutletIdも追加する可能性があります。したがって、販売注文テーブルの主キーは、Id、TenantId、OutletIdになります。

    はい、クライアントデータテーブルのクラスター化インデックスは、TenantIDおよびID **を含む複合キーである必要があります。これにより、TenantIDクライアントデータテーブルに対するクエリの98.45%が必要となるため、とにかく必要となるすべての非クラスター化インデックス(クラスター化インデックスキーが含まれているため)にあることが保証されます(TenantID主な例外は、古いデータベースのガベージコレクション時です)でCreatedDate、気にしないTenantID)。

    いいえ、OutletIDPK などのFKは含めません。PKは行を一意に識別する必要があり、FKを追加してもそれは役に立ちません。実際には、TenantIDOutletID内で一意であるのではなく、各でOrderIDが一意であると仮定すると、データが重複する可能性が高くなりますTenantID

    また、OutletIDテナントAからのアウトレットがテナントBと混同されないようにするためにPKに追加する必要はありません。すべてのユーザーデータテーブルはTenantIDPKにあるため、その手段TenantIDはFKにもあります。 。たとえば、OutletテーブルのPKはで(TenantID, OutletID)OrderテーブルにはPKが(TenantID, OrderID) あり、FKはテーブル(TenantID, OutletID)上のPKを参照しOutletます。適切に定義されたFKは、テナントデータが混在するのを防ぎます。

  4. 複合キーの順序は重要ですか?

    さて、ここからが楽しみです。どのフィールドが最初に来るべきかについて、いくつかの議論があります。適切なインデックスを設計するための「典型的な」ルールは、最も選択的なフィールドを先頭フィールドとして選択することです。TenantID、その性質上、最も選択的なフィールドではありませんIDフィールドには、最も選択フィールドです。ここにいくつかの考えがあります:

    • 最初のID:これは最も選択的な(つまり、最も一意な)フィールドです。ただし、自動インクリメントフィールド(またはGUIDを使用している場合はランダム)にすることで、各顧客のデータが各テーブルに分散されます。これは、顧客が100行を必要とし、ディスクから(高速ではない)ほぼ100データページをバッファープールに読み込む必要がある場合があることを意味します(10データページより多くのスペースを占有します)。また、複数の顧客が同じデータページを更新する必要がより頻繁に発生するため、データページでの競合も増加します。

      ただし、異なるID値の統計はかなり一貫しているため、通常はパラメータースニッフィング/キャッシュキャッシュプランの問題はそれほど多く発生しません。最適なプランを取得できない可能性がありますが、恐ろしいプランを取得する可能性は低くなります。この方法は、本質的にすべての顧客のパフォーマンスを(わずかに)犠牲にして、問題の発生頻度を減らすという利点を得ることができます。

    • 最初にTenantID:これはまったく選択的ではありません。100個のTenantIDしかない場合、100万行に渡ってほとんど変動がない可能性があります。ただし、テナントAのクエリは500,000行をプルバックするが、テナントBの同じクエリは50行のみであることがSQL Serverに認識されるため、これらのクエリの統計はより正確です。これが主な問題点です。この方法は、ストアドプロシージャの最初の実行がテナントAである場合にパラメータースニッフィングの問題が発生する可能性を大幅に高め、クエリオプティマイザーがこれらの統計を確認し、50万行を効率的に取得する必要があることを認識して適切に動作します。しかし、50行しかないテナントBが実行されると、その実行計画は適切ではなくなり、実際、まったく不適切です。AND、データは先行フィールドの順序で挿入されていないため、

      ただし、ストアドプロシージャを実行する最初のTenantIDの場合、データは(少なくともインデックスメンテナンスを実行した後)物理的および論理的に編成されるため、他のアプローチよりもパフォーマンスが向上するはずです。クエリ。つまり、物理I / Oが少なくなり、論理読み取りが少なくなり、同じデータページのテナント間の競合が少なくなり、バッファプールで消費される無駄なスペースが少なくなります(したがって、ページの寿命が長くなります)。

      このパフォーマンスの向上には、主に2つのコストがあります。1つ目はそれほど難しくありません。断片化の増加に対処するには、定期的なインデックスメンテナンスを行う必要あります。2番目は少し面白くありません。

      増加したパラメータスニッフィングの問題に対処するには、実行プランをテナント間で分離する必要があります。単純なアプローチはWITH RECOMPILE、プロシージャまたはOPTION (RECOMPILE)クエリヒントで使用することですが、それはパフォーマンスへの打撃であり、TenantID最初に置くことによって得られるすべての利益を一掃する可能性があります。私が最もうまくいくとわかった方法は、を介してパラメータ化された動的SQLを使用することsp_executesqlです。ダイナミックSQLが必要な理由は、TenantIDをクエリのテキストに連結できるようにするためですが、通常はパラメーターとなる他のすべての述語はまだパラメーターです。たとえば、特定の注文を探している場合は、次のようにします。

      DECLARE @GetOrderSQL NVARCHAR(MAX);
      SET @GetOrderSQL = N'
        SELECT ord.field1, ord.field2, etc.
        FROM   dbo.Orders ord
        WHERE  ord.TenantID = ' + CONVERT(NVARCHAR(10), @TenantID) + N'
        AND    ord.OrderID = @OrderID_dyn;
      ';
      
      EXEC sp_executesql
         @GetOrderSQL,
         N'@OrderID_dyn INT',
         @OrderID_dyn = @OrderID;

      これにより、特定のテナントのデータボリュームに一致する、そのテナントIDのみの再利用可能なクエリプランが作成されます。同じテナントAが別のストアドプロシージャを再度実行すると、@OrderIDキャッシュされたクエリプランが再利用されます。同じストアドプロシージャを実行している別のテナントは、TenantIDの値のみが異なるクエリテキストを生成しますがクエリテキストの違いは、異なるプランを生成するのに十分です。また、テナントBに対して生成されたプランは、テナントBのデータボリュームと一致するだけでなく、異なる値のテナントBに対しても再利用可能になります@OrderID(述部はまだパラメーター化されているため)。

      このアプローチの欠点は次のとおりです。

      • 単純なクエリを入力するだけではありません(ただし、すべてのクエリが動的SQLである必要はなく、パラメータスニッフィングの問題が発生するクエリだけである必要があります)。
      • システム上のテナントの数に応じて、各クエリはそれを呼び出しているTenantIDごとに1つのプランを必要とするため、プランキャッシュのサイズが増加します。これは問題ではないかもしれませんが、少なくとも注意する必要があります。
      • 動的SQLは所有権の連鎖を破ります。つまり、テーブルへの読み取り/書き込みアクセスEXECUTEは、ストアドプロシージャに対する権限を持つことで想定できません。簡単だが安全性の低い修正方法は、ユーザーがテーブルに直接アクセスできるようにすることです。これは確かに理想的ではありませんが、通常は迅速で簡単なトレードオフです。より安全なアプローチは、証明書ベースのセキュリティを使用することです。つまり、証明書を作成し、その証明書からユーザーを作成し、そのユーザーに必要なアクセス許可を付与(証明書ベースのユーザーまたはログインはそれ自体ではSQL Serverに接続できません)、それで動的SQLを使用するストアドプロシージャに署名しますADD SIGNATUREを介した同じ証明書。

        モジュールの署名と証明書の詳細については、ModuleSigning.Infoをご覧ください。
         

    この決定に起因する統計問題の緩和に対処する問題に関連する追加のトピックについては、最後に向かって更新セクションを参照してください。


**個人的には、すべてのテーブルのPKフィールド名に「ID」だけを使用するのは本当に意味がありません。また、PKは常に「ID」であり、子テーブルのフィールドは親テーブル名を含めます。例:Orders.ID-> OrderItems.OrderID。次のようなデータモデルを扱う方がはるかに簡単ですOrders.OrderID-> OrderItems.OrderID。より読みやすく、「あいまいな列参照」エラーが発生する回数を減らします:-)。


更新

  • OPTIMIZE FOR UNKNOWN クエリヒント複合PKのいずれかの順序で(SQL Server 2008で導入された)ヘルプ?

    あんまり。このオプションは、パラメータスニッフィングの問題を回避しますが、ある問題を別の問題に置き換えるだけです。この場合、ストアドプロシージャまたはパラメーター化されたクエリの最初の実行のパラメーター値の統計情報を覚えるのではなく(一部のユーザーには間違いなく素晴らしいですが、一部のユーザーには平凡で、一部のユーザーには恐ろしい)、一般的な行数を推定するためのデータ分布の統計。これは、肯定的、否定的、またはまったく影響を及ぼさないクエリの数(およびその程度)について、ヒットまたはミスです。少なくともパラメータスニッフィングでは、一部のクエリのメリットが保証されました。システムにさまざまなデータ量のテナントがある場合、すべてのクエリのパフォーマンスが低下する可能性があります。

    このオプションは、入力パラメーターをローカル変数にコピーし、クエリでローカル変数を使用するのと同じことを実現します(ここではこれをテストしましたが、その余地はありません)。追加情報は、このブログの記事に記載されています:http://www.brentozar.com/archive/2013/06/optimize-for-unknown-sql-server-parameter-sniffing/。コメントを読んで、Daniel Pepermansは、バリエーションが限られている動的SQLの使用に関して、私のものと同様の結論に達しました。

  • IDがクラスター化インデックスの先頭のフィールドである場合、単一のテナントの多くの行を処理するクエリの正確な統計を得るには、非クラスター化インデックスを(TenantID、ID)に、または単に(TenantID)にすると役立ちますか?

    はい、役立ちます。私が長年取り組んでいる大規模なシステムIDENTITYは、より選択的でパラメータスニッフィングの問題が少ないため、フィールドを先行フィールドとするインデックス設計に基づいていました。ただし、特定のテナントのデータのかなりの部分を操作する必要がある場合、パフォーマンスは維持されませんでした。実際、SANコントローラーのスループットが最大になったため、すべてのデータを新しいデータベースに移行するプロジェクトを保留する必要がありました。修正されたのは、すべてのテナントデータテーブルに非クラスター化インデックスを追加して(TenantID)だけにすることでした。IDはすでにクラスター化インデックスにあるため、実行する必要はありません(TenantID、ID)。したがって、非クラスター化インデックスの内部構造は当然(TenantID、ID)でした。

    これにより、TenantIDベースのクエリをはるかに効率的に実行できるという当面の問題は解決しましたが、それでも同じ順序でクラスター化インデックスを作成した場合ほど効率的ではありませんでした。そして、すべてのテーブルにもう1つのインデックスがありました。これにより、使用しているSANスペースの量が増加し、バックアップのサイズが増加し、バックアップの完了までの時間が長くなり、ブロックおよびデッドロックの可能性が増加し、パフォーマンスINSERTDELETE操作が低下しました。

    そして、テナントのデータを他の多くのテナントのデータと混合して、多くのデータページに分散させるという一般的な非効率性が残っていました。上で述べたように、これはこれらのページの競合の量を増やし、特にこれらのページの行の一部がクライアント用である場合、1つまたは2つの有用な行を含む多くのデータページでバッファプールをいっぱいにします非アクティブでしたが、まだガベージコレクションされていませんでした。このアプローチでは、バッファプール内のデータページを再利用する可能性がはるかに低いため、ページの平均寿命はかなり低くなりました。そして、それはより多くのページをロードするためにディスクに戻る時間が増えることを意味します。


2
この問題の分野で、最適​​化の最適化を検討またはテストしましたか?ちょっと興味があるんだけど。
RLF

1
@RLFはい、そのオプションを調査しましたが、IDENTITYフィールドを最初に持つことで得られた最適なパフォーマンスよりも、少なくとも良くも悪くもないかもしれません。これをどこで読んだかは思い出せませんが、入力変数をローカル変数に再割り当てするのと同じ「平均的な」統計が得られるはずです。しかし、この記事では、そのオプションが本当に問題が解決しない理由に入る:brentozar.com/archive/2013/06/...は :)限られた変化に動的SQLのコメントを読むと、ダニエルPepermansは同様の結論の再に来た
ソロモンRutzky

3
単一のテナントのほとんどの行を処理するクエリの正確な統計情報を取得するために、クラスター化インデックスがオン(ID, TenantID)で、非クラスター化インデックスもに作成した場合(TenantID, ID)、または単にオンに(TenantID)した場合はどうなりますか?
ウラジミールバラノフ

1
@VladimirBaranovすばらしい質問です。私は答えの終わりに向かって新しい更新セクションでそれを取り上げました:-)。
ソロモンラツキー

4
顧客の計画を生成する動的SQLについての良い点。
マックスヴァーノン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.