Çok kiracılı SQL Server veritabanında Bileşik Birincil Anahtar


16

ASP Web API, Entity Framework ve SQL Server / Azure veritabanını kullanarak çok kiracılı bir uygulama (tek veritabanı, tek şema) oluşturuyorum. Bu uygulama 1000-5000 müşteriler tarafından kullanılacak. Tüm tablolar TenantId(Guid / UNIQUEIDENTIFIER) alanına sahip olacaktır . Şu anda, Id (Guid) olan tek alanlı Birincil Anahtar kullanıyorum. Ancak, yalnızca Kimlik alanını kullanarak, kullanıcı tarafından sağlanan verilerin doğru kiracıdan / kiracı için olup olmadığını kontrol etmeliyim. Örneğin SalesOrder, bir CustomerIdalanı olan bir tablo var . Kullanıcılar bir müşteri siparişi gönderdiğinde / güncellediğinde CustomerId, bunun aynı kiracıdan olup olmadığını kontrol etmeliyim . Daha da kötüleşir, çünkü her kiracının birkaç prizi olabilir. Sonra kontrol etmeliyim TenantIdve OutletId. Gerçekten bir bakım kabusu ve performans için kötü.

TenantIdİle birlikte birincil anahtara eklemeyi düşünüyorum Id. Ve muhtemelen de ekleyin OutletId. Birincil anahtar Yani SalesOrdermasanın olacaktır: Id, TenantId, ve OutletId. Bu yaklaşımın dezavantajı nedir? Bileşik bir anahtar kullanarak performans kötü incinir mi? Bileşik anahtar sırası önemli mi? Sorunum için daha iyi çözümler var mı?

Yanıtlar:


34

Büyük ölçekli, çok kiracılı bir sistemde çalışarak (18+ sunucuya yayılmış müşterilerle federal yaklaşım, her sunucu aynı şemaya, sadece farklı müşterilere ve her sunucu başına saniyede binlerce işleme sahip) söyleyebilirim:

  1. GUID seçiminizi hem "Kiracı Kimliği" hem de herhangi bir varlık "kimliği" olarak tanımlayan bazı kişiler (en azından birkaç tane) vardır. Ama hayır, iyi bir seçim değil. Diğer tüm düşünceler bir yana, bu seçim tek başına birkaç şekilde acıtacak: başlangıçta parçalanma, büyük miktarda boşa harcanan alan ( kurumsal depolama - SAN - veya her veri sayfası nedeniyle daha uzun süren sorgular için diskin ucuz olduğunu söyleme ya INTda BIGINTçiftle olabileceğinden daha az sayıda satır tutmak ), daha zor destek ve bakım, vb. GUID'ler taşınabilirlik için mükemmeldir. Veriler bir sistemde üretilip başka bir sisteme aktarılıyor mu? Eğer değilse, o zaman, daha kompakt bir veri türü (örneğin, geçiş TINYINT, SMALLINT, INTya da hatta BIGINTile sırayla) ve artış IDENTITYya daSEQUENCE.

  2. Öğe 1 yoldan çıktığında, HER tablodaki kullanıcı verilerine sahip TenantID alanına sahip olmanız gerekir. Bu şekilde, fazladan birleştirme işlemine gerek kalmadan her şeyi filtreleyebilirsiniz. Bu ayrıca, istemci-veri tablolarına karşı TÜM sorguların TenantIDJOIN koşulu ve / veya WHERE yan tümcesine sahip olması gerektiği anlamına gelir . Bu ayrıca, farklı müşterilerden yanlışlıkla veri karıştırmamanıza veya Kiracı B'den Kiracı A verilerini göstermemenize yardımcı olur.

  3. Id ile birlikte birincil anahtar olarak TenantId eklemeyi düşünüyorum. Ve muhtemelen OutletId'i de ekleyin. Dolayısıyla, müşteri siparişi tablosundaki birincil anahtar (lar) Id, TenantId, OutletId olacaktır.

    Evet, istemci-veri tablolarındaki kümelenmiş dizinlerinizin TenantIDve ID ** dahil bileşik anahtarlar olması gerekir . Bu aynı zamanda TenantID, istemci veri tablolarına karşı sorguların% 98,45'inin TenantID(ana istisna eski veri toplama çöpü olduğunda) hakkında CreatedDateve umursamaz TenantID).

    Hayır, OutletIDPK gibi FK'leri dahil etmeyeceksiniz . PK'nın satırı benzersiz bir şekilde tanımlaması gerekiyor ve FK'leri eklemek bu konuda yardımcı olmaz. Aslında, OrderID her biri için eşsiz olduğunu varsayarak, yinelenen veriler için şansını artıracak TenantIDher başına benzersiz aksine, OutletIDher dahilinde TenantID.

    Ayrıca, OutletIDKiracı A'dan Satış Noktalarının Kiracı B ile karışmamasını sağlamak için PK'ya eklemek gerekli değildir.Tüm kullanıcı veri tabloları PK'da olacağından TenantID, bu TenantIDda FK'lerde olacaktır. . Örneğin, Outlettablonun bir PK değeri vardır (TenantID, OutletID)ve Ordertablonun bir PK değeri (TenantID, OrderID) ve bir FK değeri (TenantID, OutletID)vardır Outlet. Düzgün tanımlanmış FK'ler, Kiracı verilerinin karışmasını önleyecektir.

  4. Bileşik anahtar sırası önemli mi?

    İşte burada eğlenceli oluyor. Hangi alanın önce gelmesi gerektiği konusunda bazı tartışmalar var. İyi endeksler tasarlamak için "tipik" kural, önde gelen alan olarak en seçici alanı seçmektir. TenantID, Doğası gereği, edecek olup en seçici cismi; IDalan en seçici bir alandır. İşte bazı düşünceler:

    • Önce kimlik: Bu en seçici (yani en benzersiz) alandır. Ancak bir otomatik artış alanı (veya hala GUID'ler kullanılıyorsa rasgele) olarak, her müşterinin verileri her tabloya yayılır. Bu, bir müşterinin 100 satıra ihtiyaç duyduğu zamanlar olduğu anlamına gelir ve bu, diskten Tampon Havuzuna (10 veri sayfasından daha fazla yer kaplar) okunan (hızlı değil) neredeyse 100 veri sayfası gerektirir. Ayrıca, birden çok müşterinin aynı veri sayfasını güncellemesi gerekeceğinden daha sık olacağı için veri sayfalarındaki çekişmeyi artırır.

      Ancak, farklı kimlik değerlerindeki istatistikler oldukça tutarlı olduğundan, genellikle çok sayıda parametre koklama / kötü önbelleğe alınmış plan sorunuyla karşılaşmazsınız. En uygun planları alamayabilirsiniz, ancak korkunç planları alma olasılığınız daha düşük olacaktır. Bu yöntem, daha az sık karşılaşılan sorunlardan yararlanmak için tüm müşterilerdeki performansı (biraz) feda eder.

    • Önce Kiracı Kimliği:Bu hiç de seçici değil. Yalnızca 100 Kiracı Kimliğiniz varsa, 1 milyon satır arasında çok az değişiklik olabilir. SQL Server Kiracı A için bir sorgu 500.000 satır geri alacak ama bu Kiracı B için aynı sorgu sadece 50 satır olduğunu bilecektir, ancak bu sorguların istatistikleri daha doğrudur. Ana acı noktası burasıdır. Bu yöntem, Saklı Yordamın ilk çalıştırmasının Kiracı A için olduğu durumlarda parametre koklama sorunlarına sahip olma olasılığını büyük ölçüde artırır ve Sorgu Optimize Edici'nin bu istatistikleri görebilmesi ve 500 bin satır almanın verimli olması gerektiğini bilerek uygun şekilde hareket eder. Ancak, Kiracı B, sadece 50 sıra ile çalıştığında, bu icra planı artık uygun değildir ve aslında oldukça uygunsuzdur. VE, veriler öncü alan sırasına göre eklenmediğinden,

      Bununla birlikte, ilk Kiracı Kimliği'nin Saklı Yordam çalıştırması için, veriler (en azından dizin bakımını yaptıktan sonra) fiziksel ve mantıksal olarak organize edilecek, böylece veriyi karşılamak için çok daha az veri sayfası gerekli olacaktır. sorguları. Bu, daha az fiziksel G / Ç, daha az mantıksal okuma, aynı veri sayfaları için Kiracılar arasında daha az çekişme, Arabellek Havuzunda daha az boşa harcanan alan (dolayısıyla Sayfa Yaşam Beklentisi) vb. Anlamına gelir.

      Bu gelişmiş performansı elde etmenin iki ana maliyeti vardır. Birincisi o kadar zor değil: Artan parçalanmaya karşı koymak için düzenli indeks bakımı yapmanız gerekir . İkincisi biraz daha az eğlencelidir.

      Artan parametre koklama sorunlarına karşı koymak için, yürütme planlarını Kiracılar arasında ayırmanız gerekir. Basit yaklaşım, WITH RECOMPILEprocs veya OPTION (RECOMPILE)sorgu ipucunda kullanmaktır, ancak bu, TenantIDönce koyarak elde edilen tüm kazançları silebilecek performansta bir hit . En iyi çalıştığını bulduğum yöntem, parametreli Dinamik SQL'i kullanmaktır sp_executesql. Dinamik SQL'e ihtiyaç duyulmasının nedeni, TenantID'yi sorgu metnine birleştirmeye izin vermektir, normalde parametreler olacağı diğer tüm tahminler hala parametrelerdir. Örneğin, belirli bir Sipariş arıyorsanız, şöyle bir şey yaparsınız:

      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;

      Bunun etkisi, yalnızca söz konusu Kiracı Kimliği için söz konusu Kiracı'nın veri hacmiyle eşleşecek yeniden kullanılabilir bir sorgu planı oluşturmaktır. Aynı Kiracı A, saklı yordamı bir başkası için yeniden yürütürse, @OrderIDönbelleğe alınan sorgu planını yeniden kullanır. Aynı Saklı Yordamı çalıştıran farklı bir Kiracı, yalnızca Kiracı Kimliği değerinde farklı bir sorgu metni oluşturur, ancak sorgu metnindeki herhangi bir fark farklı bir plan oluşturmak için yeterlidir. Ve Kiracı B için oluşturulan plan sadece Kiracı B için veri hacmiyle eşleşmeyecek, aynı zamanda Kiracı B için farklı değerleri için tekrar kullanılabilecektir @OrderID(bu yüklem hala parametrelendirildiği için).

      Bu yaklaşımın dezavantajları şunlardır:

      • Sadece basit bir sorgu yazmaktan biraz daha fazla iştir (ancak tüm sorguların Dinamik SQL olması gerekmez, sadece parametre koklama problemine sahip olanlar).
      • Bir sistemde kaç Kiracı bulunduğuna bağlı olarak, her sorgu artık onu çağıran her Kiracı Kimliği için 1 plan gerektirdiğinden, plan önbelleğinin boyutunu artırır. Bu bir sorun olmayabilir, ancak en azından farkında olunması gereken bir şeydir.
      • Dinamik SQL, sahiplik zincirini kırar; bu EXECUTE, Kayıtlı Yordamda izin alınarak tablolara okuma / yazma erişiminin kabul edilemeyeceği anlamına gelir . Kolay ancak daha az güvenli düzeltme, Kullanıcıya tablolara doğrudan erişim sağlamaktır. Bu kesinlikle ideal değil, ama bu genellikle hızlı ve kolay bir şekilde takas. Daha güvenli yaklaşım, Sertifika tabanlı güvenliği kullanmaktır. Sertifika oluşturmak, Anlam, ardından Belgesi, hibe o bir kullanıcı oluşturup bu kullanıcı istenen izinleri (Sertifika tabanlı kullanıcı veya Giriş kendi başına SQL Server bağlanamıyor), ve daha sonra bu Dinamik SQL kullanmak Saklı yordamları imzalamak SIGNATURE EKLE aynı Sertifika .

        Modül imzalama ve Sertifikalar hakkında daha fazla bilgi için lütfen bkz: ModuleSigning.Info
         

    Bu karardan kaynaklanan istatistik sorunlarının azaltılmasıyla ilgili ek konular için lütfen sonuna doğru GÜNCELLEME bölümüne bakın .


** Şahsen, anlamlı olmadığı için her tablodaki PK alan adı için sadece "ID" kullanmaktan hoşlanmıyorum ve PK her zaman "ID" olduğundan ve alt tablodaki alanın üst tablo adını ekleyin. Örneğin: Orders.ID-> OrderItems.OrderID. Ben bir veri modeli ile başa çıkmak çok daha kolay buluyorum: Orders.OrderID-> OrderItems.OrderID. Daha okunabilir ve "belirsiz sütun başvurusu" hatası :-) sayısını kaç kez azaltır.


GÜNCELLEME

  • Misiniz OPTIMIZE FOR UNKNOWN Sorgu İpucu kompozit PK ya sipariş yardım (SQL Server 2008 tanıtıldı)?

    Pek sayılmaz. Bu seçenek parametre koklama sorunlarını çözer, ancak yalnızca bir sorunu diğeriyle değiştirir. Bu durumda, saklı yordamın veya parametreli sorgunun ilk çalıştırmasının parametre değerlerini (bazıları için kesinlikle harika, bazıları için muhtemelen vasat ve bazıları için muhtemelen korkunç) parametre bilgilerini hatırlamak yerine, genel bir satır sayılarını tahmin etmek için veri dağıtım istatistiği. Bu, kaç (ve hangi dereceye kadar) sorgunun olumlu, olumsuz veya hiç etkilenmeyeceği konusunda isabetsizdir. En azından parametre koklama ile bazı sorguların faydalanacağı garanti edildi. Sisteminizde çok çeşitli veri hacimlerine sahip Kiracılar varsa, bu durum tüm sorguların performansına zarar verebilir.

    Bu seçenek, giriş parametrelerini yerel değişkenlere kopyalama ve ardından sorgudaki yerel değişkenleri kullanma ile aynı şeyi gerçekleştirir (Bunu test ettim ancak burada yer yok). Bu blog yayınında daha fazla bilgi bulunabilir: http://www.brentozar.com/archive/2013/06/optimize-for-unknown-sql-server-parameter-sniffing/ . Yorumları okuyan Daniel Pepermans, varyasyona sahip Dinamik SQL kullanımı konusunda benimkine benzer bir sonuca vardı.

  • Kimlik, Kümelenmiş Dizin'in önde gelen alanı ise, (Kiracı Kimliği, Kimlik) üzerinde Kümelenmemiş Bir Dizin'in mi yoksa tek bir kiracıdaki birçok satırı işleyen sorgular için doğru istatistiklerin bulunması için yeterli / yeterli mi?

    Evet, yardımcı olur. Yıllarca üzerinde çalıştığım büyük sistem IDENTITY, daha seçici ve azaltılmış parametre koklama sorunlarından ötürü , alanın öncü alan olması için bir dizin tasarımına dayanıyordu . Bununla birlikte, belirli bir Kiracı verilerinin büyük bir kısmına karşı operasyonlara ihtiyaç duyduğumuzda, performans devam etmedi. Aslında, SAN denetleyicileri verim açısından maksimum seviyeye ulaştığından, tüm verileri yeni veritabanlarına geçirme projesi beklemeye alındı. Düzeltme, yalnızca kiracı veri tablolarına Kümelenmemiş Dizinler eklemekti (TenantID). Kimlik zaten Kümelenmiş Dizin'de olduğu için (TenantID, ID) gerek yok, bu nedenle Kümelenmemiş Dizin'in iç yapısı doğal olarak (TenantID, ID).

    Bu, TenantID tabanlı sorguları çok daha verimli bir şekilde yapabilmenin acil sorununu çözmüş olsa da, aynı sıradaki Kümelenmiş Endeks olsaydı, olabilecekleri kadar verimli değildi. Ve şimdi her tabloda bir tane daha endeksimiz vardı . Bu, kullandığımız SAN alanı miktarını artırdı, yedeklerimizin boyutunu artırdı, yedeklemelerin tamamlanmasını daha uzun sürdü, engelleme ve kilitlenme potansiyelini artırdı, performansı INSERTve DELETEişlemleri azalttı .

    VE biz hala bir Kiracı verisinin birçok veri sayfasına yayılması ve diğer birçok Kiracı verisi ile karıştırılması konusunda genel verimsiz kaldık. Yukarıda bahsettiğim gibi, bu sayfalardaki çekişme miktarını arttırır ve Tampon Havuzu'nu, özellikle bu sayfalardaki satırların bir kısmı müşteriler için etkin değil ancak henüz çöp toplanmamıştı. Bu yaklaşımda Arabellek Havuzu'ndaki veri sayfalarının yeniden kullanım potansiyeli çok daha düşük olduğundan, Sayfa Yaşam Beklentimiz oldukça düşüktü. Bu, daha fazla sayfa yüklemek için diske geri dönmek için daha fazla zaman anlamına gelir.


2
Bu sorun alanında BİLİNMEYEN OPTİMİZE ETMEDİĞİNİZ Mİ Test Ettiniz mi? Sadece merak.
RLF

1
@RLF Evet, bu seçeneği araştırdık ve öncelikle IDENTITY alanına sahip olmaktan elde ettiğimiz en iyi performanstan daha az iyi ve muhtemelen daha kötü olmamalı. Bunu nerede okuduğumu hatırlamıyorum, ancak yerel bir değişkene bir girdi parametresini yeniden atamakla aynı "ortalama" istatistikleri veriyor. Ancak bu makale, bu seçeneğin sorunu gerçekten çözmediği konuya giriyor : brentozar.com/archive/2013/06/… Yorumları okuyan Daniel Pepermans da benzer bir sonuca vardı: Sınırlı varyasyona sahip dinamik SQL :)
Solomon Rutzky

3
Kümelenmiş dizin açıksa (ID, TenantID)ve aynı zamanda tek bir kiracının çoğu satırını işleyen sorgular için doğru istatistiklere sahip olmak (TenantID, ID)veya kümelenmemiş bir dizin oluşturursanız ne olur (TenantID)?
Vladimir Baranov

1
@VladimirBaranov Mükemmel bir soru. Cevabın sonuna doğru yeni bir GÜNCELLEME bölümünde ele aldım :-).
Solomon Rutzky

4
her müşteri için planlar oluşturmak için dinamik sql hakkında güzel bir nokta .
Max Vernon
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.