Złożony klucz podstawowy w bazie danych SQL Server dla wielu dzierżawców

16

Buduję aplikację dla wielu dzierżawców (pojedyncza baza danych, pojedynczy schemat) przy użyciu interfejsu API sieci Web ASP, Entity Framework i bazy danych SQL Server / Azure. Ta aplikacja będzie używana przez 1000-5000 klientów. Wszystkie tabele będą miały pole TenantId(Guid / UNIQUEIDENTIFIER). Obecnie używam klucza podstawowego z jednym polem, którym jest Id (Guid). Ale używając tylko pola Id, muszę sprawdzić, czy dane dostarczone przez użytkownika pochodzą od / dla właściwego najemcy. Na przykład mam SalesOrdertabelę z CustomerIdpolem. Za każdym razem, gdy użytkownicy publikują / aktualizują zamówienie sprzedaży, muszę sprawdzić, czy CustomerIdpochodzi ono od tego samego najemcy. Gorzej, ponieważ każdy najemca może mieć kilka punktów sprzedaży. Potem muszę sprawdzić TenantIdi OutletId. To naprawdę koszmar konserwacji i zły na wydajność.

Zastanawiam się nad dodaniem TenantIddo klucza podstawowego wraz z Id. I ewentualnie OutletIdteż. Więc klucz podstawowy w SalesOrdertabeli będą: Id, TenantId, i OutletId. Jakie są wady tego podejścia? Czy wydajność mocno zaszkodziłaby przy użyciu klucza złożonego? Czy złożona kolejność kluczy ma znaczenie? Czy są lepsze rozwiązania mojego problemu?

Reynaldi
źródło

Odpowiedzi:

34

Pracując na wielkoskalowym systemie z wieloma dzierżawcami (podejście federacyjne z klientami rozproszonymi na ponad 18 serwerach, każdy serwer ma identyczny schemat, tylko różni klienci i tysiące transakcji na sekundę na każdy serwer), mogę powiedzieć:

  1. Są ludzie (przynajmniej kilku), którzy zgodzą się na wybór GUID jako identyfikatorów zarówno dla „TenantID”, jak i dla dowolnego identyfikatora „ID”. Ale nie, niezbyt dobry wybór. Pomijając wszystkie inne rozważania, sam ten wybór zaszkodzi na kilka sposobów: fragmentacja na początku, ogromne ilości zmarnowanego miejsca (nie mów, że dysk jest tani, gdy myślisz o pamięci masowej dla przedsiębiorstw - SAN - lub zapytania trwające dłużej z powodu każdej strony danych utrzymywanie mniejszej liczby rzędów niż mogłoby to mieć nawet INTlub BIGINTnawet), trudniejsze wsparcie i konserwacja itp. GUID-y świetnie nadają się do przenoszenia. Czy dane są generowane w jakimś systemie, a następnie przenoszone do innego? Jeśli nie, to przełączyć się na bardziej kompaktowej typu danych (np TINYINT, SMALLINT, INTlub nawet BIGINT), a przyrost kolejno przez IDENTITYlubSEQUENCE.

  2. Jeśli element 1 jest na uboczu, naprawdę musisz mieć pole TenantID w KAŻDEJ tabeli, która zawiera dane użytkownika. W ten sposób możesz filtrować wszystko bez potrzeby dodatkowego DOŁĄCZENIA. Oznacza to również, że WSZYSTKIE zapytania dotyczące tabel danych klienta muszą mieć TenantIDwarunek JOIN i / lub klauzula WHERE. Pomaga to również zagwarantować, że przypadkowo nie wymieszasz danych od różnych klientów lub nie pokazujesz danych dzierżawcy A od dzierżawcy B.

  3. Zastanawiam się, czy dodać TenantId jako klucz podstawowy wraz z Id. I ewentualnie dodaj też OutletId. Tak więc kluczami podstawowymi w tabeli zamówień sprzedaży będą Id, TenantId, OutletId.

    Tak, indeksy klastrowe w tabelach danych klienta powinny mieć klucze złożone, w tym TenantIDi ID ** . Zapewnia to również, że TenantIDznajduje się w każdym indeksie nieklastrowanym (ponieważ obejmują one Klucz (y) indeksów klastrowanych), które i tak byłyby potrzebne, ponieważ 98,45% zapytań do tabel danych klient będzie potrzebowało TenantID(głównym wyjątkiem jest zbieranie śmieci na podstawie starych danych na podstawie na CreatedDatei nie dbając o TenantID).

    Nie, nie uwzględniłbyś FK takich jak OutletIDPK. PK musi jednoznacznie zidentyfikować wiersz, a dodanie FK nie pomogłoby w tym. W rzeczywistości zwiększyłoby to szanse na zduplikowane dane, zakładając, że OrderID był unikalny dla każdego z nich TenantID, a nie niepowtarzalny dla każdego z OutletIDnich TenantID.

    Ponadto nie jest konieczne dodawanie OutletIDdo PK, aby upewnić się, że punkty sprzedaży z najemcy A nie zostaną pomieszane z najemcą B. Ponieważ wszystkie tabele danych użytkownika będą mieć TenantIDw PK, to znaczy, że TenantIDbędą również w FK . Na przykład Outlettabela ma PK (TenantID, OutletID), a Ordertabela ma PK (TenantID, OrderID) i FK, (TenantID, OutletID)które odwołują się do PK na Outletstole. Właściwie zdefiniowane FK zapobiegną mieszaniu danych Najemcy.

  4. Czy złożona kolejność kluczy ma znaczenie?

    Cóż, tutaj jest fajnie. Trwa debata, która dziedzina powinna być na pierwszym miejscu. „Typową” zasadą projektowania dobrych indeksów jest wybranie najbardziej selektywnego pola, które będzie polem wiodącym. TenantIDze swej natury nie będzie najbardziej selektywnym polem; IDpole to pole najbardziej selektywny. Oto kilka myśli:

    • Najpierw identyfikator: jest to najbardziej selektywne (tj. Najbardziej unikalne) pole. Ale ponieważ jest to pole automatycznego przyrostu (lub losowe, jeśli nadal używa GUID), dane każdego klienta są rozłożone w każdej tabeli. Oznacza to, że zdarza się, że klient potrzebuje 100 wierszy, a to wymaga prawie 100 stron danych odczytanych z dysku (nie szybko) do puli buforów (zajmuje więcej miejsca niż 10 stron danych). Zwiększa także rywalizację na stronach danych, ponieważ częściej klienci będą musieli aktualizować tę samą stronę danych.

      Zazwyczaj jednak nie występuje tak wiele problemów z wąchaniem parametrów / złym buforowaniem planu, ponieważ statystyki dla różnych wartości identyfikatora są dość spójne. Możesz nie uzyskać najbardziej optymalnych planów, ale rzadziej będziesz mieć okropne. Ta metoda zasadniczo poświęca wydajność (nieznacznie) wszystkim klientom, aby czerpać korzyści z rzadszych problemów.

    • TenantID najpierw:Nie jest to wcale wybiórcze. Różnice między milionami wierszy mogą być bardzo małe, jeśli masz tylko 100 TenantID. Ale statystyki dla tych zapytań są dokładniejsze, ponieważ SQL Server będzie wiedział, że zapytanie dla Najemcy A spowoduje wycofanie 500 000 wierszy, ale to samo zapytanie dla Najemcy B ma tylko 50 wierszy. To jest główny punkt bólu. Ta metoda znacznie zwiększa szanse na problemy z wąchaniem parametrów, gdy pierwszy przebieg procedury składowanej jest dla dzierżawcy A i działa odpowiednio w oparciu o Optymalizator zapytań, który widzi te statystyki i wie, że musi być wydajny, uzyskując 500 tys. Wierszy. Ale gdy działa Dzierżawa B, która ma tylko 50 wierszy, ten plan wykonania nie jest już odpowiedni, a wręcz niewłaściwy. ORAZ, ponieważ dane nie są wstawiane w kolejności pola wiodącego,

      Jednak, aby pierwszy TenantID uruchomił procedurę przechowywaną, wydajność powinna być lepsza niż w drugim podejściu, ponieważ dane (przynajmniej po przeprowadzeniu konserwacji indeksu) będą zorganizowane fizycznie i logicznie, tak że potrzeba będzie znacznie mniej stron danych, aby spełnić zapytania. Oznacza to mniej fizycznych operacji we / wy, mniej logicznych odczytów, mniej rywalizacji między dzierżawcami o te same strony danych, mniej zmarnowanego miejsca zajmowanego w puli buforów (a zatem poprawiona oczekiwana długość życia strony) itp.

      Ulepszenie wydajności wiąże się z dwoma głównymi kosztami. Pierwszy nie jest tak trudne: Państwo musi zrobić regularnej konserwacji indeksu, aby przeciwdziałać zwiększoną fragmentację. Drugi jest nieco mniej zabawny.

      Aby przeciwdziałać zwiększonym problemom z wąchaniem parametrów, musisz rozdzielić plany wykonania między najemcami. Uproszczone podejście polega na użyciu WITH RECOMPILEprocs lub OPTION (RECOMPILE)podpowiedzi do zapytania, ale jest to uderzenie w wydajność, które może zniszczyć wszystkie zyski osiągnięte przez stawianie na TenantIDpierwszym miejscu. Metodą, która według mnie działała najlepiej, jest użycie sparametryzowanego Dynamicznego SQL poprzez sp_executesql. Powodem potrzeby użycia dynamicznego SQL jest umożliwienie konkatenacji TenantID z tekstem zapytania, podczas gdy wszystkie inne predykaty, które normalnie byłyby parametrami, są nadal parametrami. Na przykład, jeśli szukasz konkretnego Zakonu, zrobiłbyś coś takiego:

      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;

      Efektem tego jest utworzenie planu zapytań wielokrotnego użytku tylko dla tego TenantID, który będzie pasował do ilości danych tego konkretnego Najemcy. Jeśli ten sam dzierżawca A ponownie uruchomi procedurę przechowywaną dla innej, @OrderIDwówczas ponownie użyje buforowanego planu zapytań. Inny dzierżawca uruchamiający tę samą procedurę składowaną wygenerowałby tekst zapytania, który byłby inny tylko pod względem wartości TenantID, ale każda różnica w tekście zapytania wystarczy, aby wygenerować inny plan. A plan wygenerowany dla dzierżawcy B będzie nie tylko pasował do wielkości danych dla dzierżawcy B, ale będzie również można go użyć ponownie dla dzierżawcy B dla różnych wartości @OrderID(ponieważ ten predykat jest nadal sparametryzowany).

      Wadami tego podejścia są:

      • To trochę więcej pracy niż pisanie prostego zapytania (ale nie wszystkie zapytania muszą być dynamiczne SQL, tylko te, które mają problem z wąchaniem parametrów).
      • W zależności od liczby dzierżawców w systemie, zwiększa to rozmiar pamięci podręcznej planu, ponieważ każde zapytanie wymaga teraz 1 planu na identyfikator TenantID, który go wywołuje. To może nie być problem, ale przynajmniej należy o tym pamiętać.
      • Dynamiczny SQL przerywa łańcuch własności, co oznacza, że ​​nie można zakładać, że dostęp do tabel będzie możliwy dzięki EXECUTEzezwoleniu na procedurę przechowywaną. Łatwym, ale mniej bezpiecznym rozwiązaniem jest zapewnienie Użytkownikowi bezpośredniego dostępu do tabel. To z pewnością nie jest idealne, ale zwykle jest to kompromis dla szybkiego i łatwego. Bardziej bezpiecznym podejściem jest korzystanie z zabezpieczeń opartych na certyfikatach. Oznacza to, że utwórz certyfikat, a następnie utwórz użytkownika na podstawie tego certyfikatu, nadaj temu użytkownikowi żądane uprawnienia (użytkownik oparty na certyfikacie lub login nie może połączyć się z programem SQL Server samodzielnie), a następnie podpisuj za pomocą niego procedury składowane korzystające z dynamicznego SQL ten sam certyfikat przez DODAJ PODPIS .

        Aby uzyskać więcej informacji na temat podpisywania modułów i certyfikatów, zobacz: ModuleSigning.Info
         

    Zapoznaj się z sekcją AKTUALIZACJA pod koniec, aby uzyskać dodatkowe tematy związane z problemem radzenia sobie z problemami statystycznymi wynikającymi z tej decyzji.


** Osobiście bardzo nie lubię używać tylko „ID” dla nazwy pola PK w każdej tabeli, ponieważ nie ma to znaczenia i nie jest spójne w FK, ponieważ PK jest zawsze „ID”, a pole w tabeli potomnej musi dołącz nazwę tabeli nadrzędnej. Na przykład: Orders.ID-> OrderItems.OrderID. O wiele łatwiej mi radzić sobie z modelem danych, który ma: Orders.OrderID-> OrderItems.OrderID. Jest bardziej czytelny i zmniejsza liczbę przypadków, w których pojawia się błąd „niejednoznaczne odniesienie do kolumny” :-).


AKTUALIZACJA

  • Czy OPTIMIZE FOR UNKNOWN wskazówka dotycząca zapytania (wprowadzona w SQL Server 2008) pomogłaby w zamówieniu złożonego PK?

    Nie całkiem. Ta opcja pozwala obejść problemy z wąchaniem parametrów, ale po prostu zastępuje jeden problem z drugim. W tym przypadku, zamiast zapamiętywać dane statystyczne dla wartości parametrów dla początkowego przebiegu procedury składowanej lub sparametryzowanego zapytania (co jest zdecydowanie świetne dla niektórych, ale dla niektórych potencjalnie przeciętne, a dla niektórych potencjalnie okropne), wykorzystuje ogólne Statystyka rozkładu danych w celu oszacowania liczby wierszy. Chodzi o trafienie w to, ile (i do jakiego stopnia) zapytań zostanie pozytywnie, negatywnie lub wcale. Przynajmniej dzięki wąchaniu parametrów pewne zapytania były korzystne. Jeśli w systemie są dzierżawcy z bardzo zróżnicowanymi ilościami danych, może to potencjalnie obniżyć wydajność wszystkich zapytań.

    Ta opcja realizuje to samo, co kopiowanie parametrów wejściowych do zmiennych lokalnych, a następnie używanie zmiennych lokalnych w zapytaniu (przetestowałem to, ale nie ma na to miejsca). Dodatkowe informacje można znaleźć w tym poście na blogu: http://www.brentozar.com/archive/2013/06/optimize-for-unknown-sql-server-parameter-sniffing/ . Czytając komentarze, Daniel Pepermans doszedł do podobnego do mojego wniosku dotyczącego użycia dynamicznego SQL, który ma ograniczoną zmienność.

  • Jeśli identyfikator jest wiodącym polem w indeksie klastrowym, czy pomoc / wystarczałoby posiadanie indeksu nieklastrowanego na (TenantID, ID), czy po prostu (TenantID), aby mieć dokładne statystyki dla zapytań przetwarzających wiele wierszy pojedynczego najemcy?

    Tak, to by pomogło. Duży system, o którym wspominałem pracując od lat, opierał się na projekcie indeksu mającego IDENTITYpole jako pole wiodące, ponieważ było ono bardziej selektywne i ograniczało problemy z węszeniem parametrów. Jednak gdy potrzebowaliśmy operacji na dużej części danych konkretnego Najemcy, wydajność się nie utrzymała. W rzeczywistości projekt migracji wszystkich danych do nowych baz danych musiał zostać zawieszony, ponieważ kontrolery SAN osiągnęły maksymalny poziom przepustowości. Rozwiązaniem było dodanie indeksów nieklastrowanych do wszystkich tabel danych dzierżawy, aby były tylko (TenantID). Nie trzeba tego robić (TenantID, ID), ponieważ ID jest już w indeksie klastrowanym, więc wewnętrzna struktura indeksu nieklastrowanego była naturalnie (TenantID, ID).

    Chociaż rozwiązało to natychmiastową kwestię możliwości wydajniejszego wykonywania zapytań opartych na TenantID, nadal nie były one tak wydajne, jak mogłyby być, gdyby Indeks klastrowany był w tej samej kolejności. A teraz mieliśmy jeszcze jeden indeks na każdym stole. Zwiększyło to ilość używanej przez nas przestrzeni SAN, zwiększyło rozmiar naszych kopii zapasowych, wydłużyło czas ich tworzenia, zwiększyło możliwości blokowania i zakleszczenia, zmniejszyło wydajność INSERTi DELETEoperacje itp.

    ORAZ wciąż pozostawała nam ogólna nieefektywność, że dane Najemcy są rozmieszczone na wielu stronach danych, zmieszane z wieloma innymi danymi Najemcy. Jak wspomniałem powyżej, zwiększa to poziom rywalizacji na tych stronach i wypełnia pulę buforów dużą ilością stron danych, które zawierają 1 lub 2 użyteczne wiersze, szczególnie gdy niektóre wiersze na tych stronach były przeznaczone dla klientów, którzy były nieaktywne, ale jeszcze nie zostały zebrane. W tym podejściu istnieje znacznie mniejsze możliwości ponownego wykorzystania stron danych w puli buforów, więc nasza oczekiwana długość życia strony była dość niska. A to oznacza więcej czasu na powrót na dysk, aby załadować więcej stron.

Solomon Rutzky
źródło
2
Czy rozważałeś lub testowałeś OPTYMALIZUJ DLA NIEZNANE w tym obszarze problemów? Po prostu ciekawy.
RLF
1
@RLF Tak, zbadaliśmy tę opcję i powinna ona być co najmniej nie lepsza, a być może gorsza od mniej niż optymalnej wydajności, jaką otrzymywaliśmy z posiadania pola TOŻSAMOŚĆ jako pierwsza. Nie pamiętam, gdzie to przeczytałem, ale podobno daje to te same „średnie” statystyki, co ponowne przypisanie parametru wejściowego do zmiennej lokalnej. Ale ten artykuł wyjaśnia, dlaczego ta opcja tak naprawdę nie rozwiązuje problemu: brentozar.com/archive/2013/06/... Czytając komentarze, Daniel Pepermans doszedł do podobnego wniosku dotyczącego: Dynamicznego SQL z ograniczoną odmianą :)
Solomon Rutzky
3
Co się stanie, jeśli indeks klastrowy jest włączony, (ID, TenantID)a Ty również utworzysz indeks nieklastrowany (TenantID, ID)lub po prostu będziesz (TenantID)mieć dokładne statystyki dla zapytań przetwarzających większość wierszy jednego dzierżawcy?
Vladimir Baranov
1
@VladimirBaranov Doskonałe pytanie. Zajęłam się tym w nowej sekcji UPDATE pod koniec odpowiedzi :-).
Solomon Rutzky
4
fajna uwaga na temat dynamicznego sql do generowania planów dla każdego klienta.
Max Vernon