Zusammengesetzter Primärschlüssel in einer mandantenfähigen SQL Server-Datenbank

15

Ich erstelle eine mandantenfähige App (einzelne Datenbank, einzelnes Schema) mit ASP-Web-API, Entity Framework und SQL Server / Azure-Datenbank. Diese App wird von 1000-5000 Kunden genutzt. Alle Tabellen haben ein TenantId(Guid / UNIQUEIDENTIFIER) Feld. Im Moment benutze ich ein einzelnes Feld Primärschlüssel, das Id (Guid) ist. Aber indem ich nur das ID-Feld benutze, muss ich überprüfen, ob die vom Benutzer gelieferten Daten von / für den richtigen Mieter sind. Zum Beispiel habe ich eine SalesOrderTabelle, die ein CustomerIdFeld hat. Jedes Mal, wenn Benutzer einen Kundenauftrag veröffentlichen / aktualisieren, muss ich überprüfen, ob der Auftrag CustomerIdvom selben Mieter stammt. Es wird schlimmer, weil jeder Mieter mehrere Filialen haben könnte. Dann muss ich nachsehen TenantIdund OutletId. Es ist wirklich ein Wartungsalptraum und schlecht für die Leistung.

Ich denke, um TenantIdden Primärschlüssel zusammen mit hinzuzufügen Id. Und möglicherweise auch hinzufügen OutletId. So ist der Primärschlüssel in der SalesOrderwird Tabelle sein: Id, TenantId, und OutletId. Was ist der Nachteil dieses Ansatzes? Würde die Leistung bei Verwendung eines zusammengesetzten Schlüssels stark beeinträchtigt? Ist die Reihenfolge der zusammengesetzten Schlüssel wichtig? Gibt es bessere Lösungen für mein Problem?

Reynaldi
quelle

Antworten:

33

Nachdem ich an einem großen System mit mehreren Mandanten gearbeitet habe (Verbundansatz mit Kunden, die auf mehr als 18 Servern verteilt sind, wobei jeder Server das gleiche Schema, nur verschiedene Kunden und Tausende von Transaktionen pro Sekunde und Server aufweist), kann ich Folgendes sagen:

  1. Es gibt einige Leute (zumindest ein paar), die sich auf Ihre Wahl der GUID als ID sowohl für "TenantID" als auch für die "ID" der Entität einigen. Aber nein, keine gute Wahl. Abgesehen von allen anderen Überlegungen schadet diese Auswahl in gewisser Weise: Zunächst einmal Fragmentierung, große Menge an verschwendetem Speicherplatz (sagen Sie nicht, dass die Festplatte billig ist, wenn Sie an Unternehmensspeicher denken - SAN - oder Abfragen, die aufgrund jeder Datenseite länger dauern) Halten Sie weniger Zeilen als es könnte mit entweder INToder BIGINTsogar), schwierigerer Unterstützung und Wartung usw. GUIDs sind ideal für die Portabilität. Werden die Daten in einem System generiert und dann in ein anderes übertragen? Wenn nicht, schaltet dann auf eine kompaktere Datentyp ( zum Beispiel TINYINT, SMALLINT, INT, oder sogar BIGINT) und Inkrement sequentiell über IDENTITYoderSEQUENCE.

  2. Wenn Element 1 nicht im Weg ist, muss das TenantID-Feld in JEDER Tabelle mit Benutzerdaten vorhanden sein. Auf diese Weise können Sie alles filtern, ohne einen zusätzlichen JOIN zu benötigen. Dies bedeutet auch, dass ALLE Abfragen für Client-Datentabellen TenantIDdie Bedingung JOIN und / oder die WHERE-Klausel enthalten müssen. Dies hilft auch zu gewährleisten, dass Sie nicht versehentlich Daten von verschiedenen Kunden mischen oder Tenant A-Daten von Tenant B anzeigen.

  3. Ich denke daran, TenantId als Primärschlüssel zusammen mit der ID hinzuzufügen. Und möglicherweise auch OutletId hinzufügen. Der Primärschlüssel in der Kundenauftragstabelle lautet also ID, TenantId, OutletId.

    Ja, Ihre Clustered-Indizes für die Client-Datentabellen sollten zusammengesetzte Schlüssel sein, einschließlich TenantIDund ID ** . Dies stellt auch sicher, dass TenantIDin jedem NonClustered-Index (da diese den oder die Clustered-Index-Schlüssel enthalten), die Sie sowieso benötigen würden, 98,45% der Abfragen für Client-Datentabellen den benötigen TenantID(die Hauptausnahme ist beim Garbage-Sammeln alter datenbasierter Daten) an CreatedDateund kümmert sich nicht darum TenantID).

    Nein, Sie würden FKs wie OutletIDdie der PK nicht einbeziehen . Die PK muss die Zeile eindeutig identifizieren, und das Hinzufügen von FKs würde dabei nicht helfen. Tatsächlich würde dies die Wahrscheinlichkeit für doppelte Daten erhöhen, vorausgesetzt, dass OrderID für jeden eindeutig ist und nicht für TenantIDjeden OutletIDin jedem TenantID.

    Es ist auch nicht erforderlich, OutletIDdie PK zu ergänzen , um sicherzustellen, dass die Outlets von Tenant A nicht mit Tenant B verwechselt werden. Da alle Benutzerdatentabellen TenantIDin der PK enthalten sind, ist dies TenantIDauch in den FKs der Fall . Beispielsweise hat die OutletTabelle eine PK von (TenantID, OutletID)und die OrderTabelle eine PK von (TenantID, OrderID) und eine FK von, (TenantID, OutletID)die auf die PK in der OutletTabelle verweist . Ordnungsgemäß definierte FKs verhindern, dass Tenant-Daten vermischt werden.

  4. Ist die Reihenfolge der zusammengesetzten Schlüssel wichtig?

    Hier macht es Spaß. Es gibt einige Debatten darüber, welches Feld zuerst kommen sollte. Die "typische" Regel für das Entwerfen guter Indizes lautet, das am besten ausgewählte Feld als führendes Feld auszuwählen. TenantIDEs wird von Natur aus nicht das selektivste Gebiet sein; Das IDFeld ist das selektivste Feld. Hier sind einige Gedanken:

    • ID first: Dies ist das selektivste (dh eindeutigste) Feld. Da es sich jedoch um ein Auto-Inkrement-Feld handelt (oder um ein zufälliges Feld, wenn weiterhin GUIDs verwendet werden), werden die Kundendaten auf alle Tabellen verteilt. Dies bedeutet, dass ein Kunde manchmal 100 Zeilen benötigt und dass fast 100 Datenseiten von der Festplatte (nicht schnell) in den Pufferpool eingelesen werden müssen (was mehr Platz als 10 Datenseiten beansprucht). Dies erhöht auch die Konkurrenz auf den Datenseiten, da es häufiger vorkommt, dass mehrere Kunden dieselbe Datenseite aktualisieren müssen.

      In der Regel treten jedoch nicht so viele Parameter-Sniffing- / Bad-Cached-Plan-Probleme auf, da die Statistiken für die verschiedenen ID-Werte ziemlich konsistent sind. Sie erhalten möglicherweise nicht die optimalen Pläne, aber Sie werden weniger wahrscheinlich schreckliche bekommen. Diese Methode beeinträchtigt im Wesentlichen die Leistung (geringfügig) aller Kunden, um die Vorteile weniger häufiger Probleme zu nutzen.

    • MieterID zuerst:Dies ist überhaupt nicht selektiv. Wenn Sie nur 100 TenantIDs haben, kann es zu sehr geringen Abweichungen zwischen 1 Million Zeilen kommen. Die Statistik für diese Abfragen ist jedoch genauer, da SQL Server weiß, dass eine Abfrage für Mandant A 500.000 Zeilen zurückzieht, dieselbe Abfrage für Mandant B jedoch nur 50 Zeilen enthält. Hier liegt der Hauptschmerzpunkt. Diese Methode erhöht die Wahrscheinlichkeit von Parameternachforschungsproblemen erheblich, wenn die erste Ausführung einer gespeicherten Prozedur für Mandant A ausgeführt wird. Sie basiert auf dem Abfrageoptimierer, der diese Statistiken anzeigt und weiß, dass 500.000 Zeilen effizient abgerufen werden müssen. Wenn jedoch Mandant B mit nur 50 Zeilen ausgeführt wird, ist dieser Ausführungsplan nicht mehr angemessen und in der Tat völlig unangemessen. UND, da die Daten nicht in der Reihenfolge des führenden Feldes eingefügt werden,

      Für die erste TenantID, die eine gespeicherte Prozedur ausführt, sollte die Leistung jedoch besser sein als bei der anderen Methode, da die Daten (zumindest nach der Indexpflege) physisch und logisch so organisiert sind, dass weit weniger Datenseiten erforderlich sind, um die Anforderungen zu erfüllen Abfragen. Dies bedeutet weniger physische E / A, weniger logische Lesevorgänge, weniger Konflikte zwischen Mandanten für dieselben Datenseiten, weniger verschwendeten Speicherplatz im Pufferpool (daher verbesserte Lebenserwartung der Seite) usw.

      Es gibt zwei Hauptkosten, um diese verbesserte Leistung zu erhalten. Das erste ist nicht so schwierig: Sie müssen regelmäßige Indexpflege durchführen, um der zunehmenden Fragmentierung entgegenzuwirken. Die zweite ist ein bisschen weniger Spaß.

      Um den gestiegenen Problemen mit dem Parameter-Sniffing entgegenzuwirken, müssen Sie die Ausführungspläne zwischen den Mandanten trennen. Der vereinfachte Ansatz besteht darin, WITH RECOMPILEauf procs oder den Abfragehinweis zurückzugreifen. Dies OPTION (RECOMPILE)ist jedoch ein Leistungstreffer, der alle Gewinne zunichte machen könnte, die durch das Voranstellen erzielt werden TenantID. Die Methode, die am besten funktioniert hat, ist die Verwendung von parametrisiertem Dynamic SQL über sp_executesql. Der Grund für die Verwendung von Dynamic SQL besteht darin, dass die TenantID in den Text der Abfrage eingebunden werden kann, während alle anderen Prädikate, die normalerweise Parameter sind, weiterhin Parameter sind. Wenn Sie beispielsweise nach einem bestimmten Auftrag suchen, tun Sie Folgendes:

      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;

      Dies bewirkt, dass nur für diese TenantID ein wiederverwendbarer Abfrageplan erstellt wird, der dem Datenvolumen dieses bestimmten Tenants entspricht. Wenn derselbe Mandant A die gespeicherte Prozedur für einen anderen erneut ausführt @OrderID, wird dieser zwischengespeicherte Abfrageplan erneut verwendet. Ein anderer Mandant, der dieselbe gespeicherte Prozedur ausführt, generiert einen Abfragetext, der sich nur im Wert der Mandanten-ID unterscheidet. Jeder Unterschied im Abfragetext reicht jedoch aus, um einen anderen Plan zu generieren. Der für Mandant B generierte Plan entspricht nicht nur dem Datenvolumen für Mandant B, sondern kann auch für Mandant B für verschiedene Werte von wiederverwendet werden @OrderID(da dieses Prädikat noch parametrisiert ist).

      Die Nachteile dieses Ansatzes sind:

      • Es ist etwas mehr Arbeit als nur das Eingeben einer einfachen Abfrage (aber nicht alle Abfragen müssen Dynamic SQL sein, sondern nur diejenigen, bei denen das Problem des Parameter-Sniffs auftritt).
      • Je nachdem, wie viele Mandanten sich in einem System befinden, wird der Plan-Cache vergrößert, da für jede Abfrage jetzt 1 Plan pro Mandanten-ID erforderlich ist, die ihn aufruft. Dies ist möglicherweise kein Problem, aber es ist zumindest etwas zu beachten.
      • Dynamic SQL unterbricht die Besitzerkette, sodass Lese- / Schreibzugriff auf Tabellen nicht mit der EXECUTEBerechtigung für die gespeicherte Prozedur angenommen werden kann. Die einfache, aber weniger sichere Lösung besteht darin, dem Benutzer direkten Zugriff auf die Tabellen zu gewähren. Dies ist sicherlich nicht ideal, aber in der Regel ist dies der Kompromiss zwischen schnell und einfach. Der sicherere Ansatz ist die Verwendung der zertifikatbasierten Sicherheit. Das heißt, erstellen Sie ein Zertifikat und anschließend einen Benutzer aus diesem Zertifikat, gewähren Sie diesem Benutzer die gewünschten Berechtigungen (ein zertifikatbasierter Benutzer oder eine Anmeldung kann keine eigene Verbindung zu SQL Server herstellen) und signieren Sie dann die gespeicherten Prozeduren, die Dynamic SQL verwenden gleiches Zertifikat über ADD SIGNATURE .

        Weitere Informationen zur Modulsignatur und zu Zertifikaten finden Sie unter: ModuleSigning.Info
         

    Weitere Themen im Zusammenhang mit der Behebung der sich aus dieser Entscheidung ergebenden Probleme mit der Statistik finden Sie im Abschnitt UPDATE gegen Ende.


** Persönlich mag ich es nicht, nur "ID" für den PK-Feldnamen in jeder Tabelle zu verwenden, da dies nicht aussagekräftig ist und nicht für alle FKs konsistent ist, da die PK immer "ID" ist und das Feld in der untergeordneten Tabelle muss Geben Sie den Namen der übergeordneten Tabelle an. Zum Beispiel: Orders.ID-> OrderItems.OrderID. Ich finde es viel einfacher, mit einem Datenmodell umzugehen, das hat: Orders.OrderID-> OrderItems.OrderID. Es ist besser lesbar und verringert die Häufigkeit, mit der der Fehler "Mehrdeutige Spaltenreferenz" angezeigt wird :-).


AKTUALISIEREN

  • Würde der OPTIMIZE FOR UNKNOWN Abfragehinweis Hilfe bei beide Bestellung des Verbund PK (in SQL Server 2008 eingeführt)?

    Nicht wirklich. Diese Option umgeht Parameter-Sniffing-Probleme, ersetzt jedoch lediglich ein Problem durch ein anderes. In diesem Fall wird anstelle der statistischen Informationen für die Parameterwerte des ersten Durchlaufs der gespeicherten Prozedur oder der parametrisierten Abfrage (die für einige auf jeden Fall gut, für einige jedoch möglicherweise mittelmäßig und für einige möglicherweise schrecklich sind) ein allgemeiner Wert verwendet Statistik der Datenverteilung zur Schätzung der Zeilenanzahl. Dies ist eine Frage, wie viele (und in welchem ​​Ausmaß) Anfragen positiv, negativ oder gar nicht betroffen sind. Zumindest beim Parameter-Sniffing waren einige Abfragen garantiert von Vorteil. Wenn auf Ihrem System Tenants mit sehr unterschiedlichen Datenmengen vorhanden sind, kann dies die Leistung aller Abfragen beeinträchtigen.

    Diese Option bewirkt dasselbe wie das Kopieren von Eingabeparametern in lokale Variablen und das anschließende Verwenden der lokalen Variablen in der Abfrage (ich habe dies getestet, aber hier keinen Platz dafür). Weitere Informationen finden Sie in diesem Blogbeitrag: http://www.brentozar.com/archive/2013/06/optimize-for-unknown-sql-server-parameter-sniffing/ . Beim Lesen der Kommentare gelangte Daniel Pepermans zu einem ähnlichen Ergebnis wie ich in Bezug auf die Verwendung von Dynamic SQL, das nur begrenzte Variationen aufweist.

  • Wenn ID das führende Feld im Clustered-Index ist, würde es helfen / ausreichen, einen Non-Clustered-Index für (TenantID, ID) oder nur (TenantID) zu haben, um genaue Statistiken für Abfragen zu erhalten, die viele Zeilen eines einzelnen Tenants verarbeiten?

    Ja, das würde helfen. Das große System, von dem ich erwähnte, dass es jahrelang arbeitete, basierte auf einem Indexdesign, bei dem das IDENTITYFeld das führende Feld war, weil es selektiver war und Probleme mit dem Parameter-Sniffing reduzierte. Wenn wir jedoch einen Großteil der Daten eines bestimmten Mandanten verarbeiten mussten, hielt die Leistung nicht an. Tatsächlich musste ein Projekt zur Migration aller Daten in neue Datenbanken ausgesetzt werden, da die SAN-Controller hinsichtlich des Durchsatzes maximal ausgelastet waren. Der Fix bestand darin, nicht gruppierte Indizes zu allen Mandantendatentabellen hinzuzufügen, um nur (TenantID) zu sein. Dies ist nicht erforderlich (TenantID, ID), da ID bereits im Clustered-Index enthalten ist. Daher war die interne Struktur des Non-Clustered-Index natürlich (TenantID, ID).

    Dies löste zwar das unmittelbare Problem, dass auf TenantID basierende Abfragen wesentlich effizienter ausgeführt werden konnten, war jedoch immer noch nicht so effizient, wie es hätte sein können, wenn der Clustered Index in derselben Reihenfolge wäre. Und jetzt hatten wir noch einen Index für jeden Tisch. Dies erhöhte die Menge des von uns verwendeten SAN-Speicherplatzes, vergrößerte die Größe unserer Sicherungen, verlängerte die Dauer der Sicherungen, erhöhte das Potenzial für Blockierungen und Deadlocks, verringerte die Leistung INSERTund den DELETEBetrieb usw.

    UND wir hatten immer noch die allgemeine Ineffizienz, dass die Daten eines Mandanten auf viele Datenseiten verteilt und mit den Daten vieler anderer Mandanten vermischt wurden. Wie oben erwähnt, erhöht dies die Anzahl der Konflikte auf diesen Seiten und füllt den Pufferpool mit vielen Datenseiten mit 1 oder 2 nützlichen Zeilen waren inaktiv, hatten aber noch keinen Müll gesammelt. Bei diesem Ansatz besteht ein viel geringeres Potenzial für die Wiederverwendung der Datenseiten im Pufferpool, sodass unsere Lebenserwartung für Seiten ziemlich niedrig war. Das bedeutet, dass Sie mehr Zeit auf die Festplatte zurücklegen müssen, um weitere Seiten zu laden.

Solomon Rutzky
quelle
2
Haben Sie OPTIMIZE FOR UNKNOWN in diesem Problembereich in Betracht gezogen oder getestet? Nur neugierig.
RLF
1
@RLF Ja, wir haben diese Option untersucht, und sie sollte zumindest nicht besser und möglicherweise schlechter sein als die nicht optimale Leistung, die wir erhalten, wenn wir zuerst das IDENTITY-Feld haben. Ich erinnere mich nicht, wo ich das gelesen habe, aber es gibt angeblich die gleichen "Durchschnittswerte" wie das Zuweisen eines Eingabeparameters zu einer lokalen Variablen. In diesem Artikel wird jedoch erläutert, warum diese Option das Problem nicht wirklich löst: brentozar.com/archive/2013/06/… Daniel Pepermans gelangte beim Lesen der Kommentare zu einer ähnlichen Schlussfolgerung in Bezug auf : Dynamisches SQL mit begrenzten Variationen :)
Solomon Rutzky
3
Was ist, wenn der Clustered-Index aktiviert ist (ID, TenantID)und Sie auch einen Nicht-Clustered-Index erstellen (TenantID, ID), oder einfach (TenantID), um genaue Statistiken für Abfragen zu erhalten, die die meisten Zeilen eines einzelnen Mandanten verarbeiten?
Vladimir Baranov
1
@VladimirBaranov Ausgezeichnete Frage. Ich habe es in einem neuen UPDATE- Bereich gegen Ende der Antwort angesprochen :-).
Solomon Rutzky,
4
schöner punkt über die dynamische sql, um pläne für jeden kunden zu generieren .
Max Vernon