Composite Primary Key dalam database SQL Server multi-tenant

16

Saya sedang membangun aplikasi multi-penyewa (database tunggal, skema tunggal) menggunakan ASP Web API, Entity Framework, dan database SQL Server / Azure. Aplikasi ini akan digunakan oleh 1000-5000 pelanggan. Semua tabel akan memiliki bidang TenantId(Guid / UNIQUEIDENTIFIER). Saat ini, saya menggunakan Kunci Utama bidang tunggal yang Id (Guid). Tetapi dengan hanya menggunakan bidang Id, saya harus memeriksa apakah data yang diberikan oleh pengguna adalah dari / untuk penyewa yang tepat. Misalnya, saya punya SalesOrdertabel yang memiliki CustomerIdbidang. Setiap kali pengguna memposting / memperbarui pesanan penjualan, saya harus memeriksa apakah CustomerIditu dari penyewa yang sama. Semakin buruk karena setiap penyewa mungkin memiliki beberapa outlet. Maka saya harus memeriksa TenantIddan OutletId. Ini benar-benar mimpi buruk pemeliharaan dan buruk untuk kinerja.

Saya sedang berpikir untuk menambahkan TenantIdKunci Utama bersama Id. Dan mungkin menambahkan OutletIdjuga. Jadi Primary Key di SalesOrdertabel akan: Id, TenantId, dan OutletId. Apa kelemahan dari pendekatan ini? Apakah kinerjanya sangat buruk menggunakan kunci komposit? Apakah urutan kunci komposit penting? Apakah ada solusi yang lebih baik untuk masalah saya?

Reynaldi
sumber

Jawaban:

34

Setelah bekerja pada sistem berskala besar, multi-tenant (pendekatan gabungan dengan pelanggan yang tersebar di 18+ server, setiap server memiliki skema yang identik, hanya pelanggan yang berbeda, dan ribuan transaksi per detik per setiap server), saya dapat mengatakan:

  1. Ada beberapa orang (setidaknya beberapa) yang akan menyetujui pilihan Anda tentang GUID sebagai ID untuk "TenantID" dan "ID" entitas apa pun. Tapi tidak, bukan pilihan yang baik. Selain semua pertimbangan lain, pilihan itu saja akan merugikan dalam beberapa cara: fragmentasi untuk memulai dengan, sejumlah besar ruang yang terbuang (jangan katakan disk murah ketika memikirkan penyimpanan perusahaan - SAN - atau permintaan yang lebih lama karena setiap halaman data) memegang lebih sedikit baris daripada yang bisa dilakukan dengan salah satu INTatau BIGINTbahkan), dukungan dan pemeliharaan yang lebih sulit, dll. GUID sangat bagus untuk portabilitas. Apakah data dihasilkan di beberapa sistem dan kemudian ditransfer ke yang lain? Jika tidak, kemudian beralih ke jenis yang lebih kompak data (misalnya TINYINT, SMALLINT, INT, atau bahkan BIGINT), dan kenaikan secara berurutan melalui IDENTITYatauSEQUENCE.

  2. Dengan item 1 keluar dari jalan, Anda benar-benar perlu memiliki bidang TenantID di tabel SETIAP yang memiliki data pengguna. Dengan begitu Anda dapat memfilter apa pun tanpa perlu GABUNG ekstra. Ini juga berarti bahwa SEMUA kueri terhadap tabel data klien diharuskan memiliki TenantIDklausa JOIN dalam kondisi dan / atau WHERE. Ini juga membantu menjamin bahwa Anda tidak secara tidak sengaja menggabungkan data dari pelanggan yang berbeda, atau menunjukkan data Tenant A dari Tenant B.

  3. Saya sedang berpikir untuk menambahkan TenantId sebagai kunci utama bersama dengan Id. Dan mungkin menambahkan OutletId juga. Jadi kunci utama dalam tabel pesanan penjualan adalah Id, TenantId, OutletId.

    Ya, Anda harus memiliki indeks berkerumun di tabel data klien menjadi kunci komposit, termasuk TenantIDdan ID ** . Ini juga memastikan bahwa TenantIDada di setiap indeks NonClustered (karena itu termasuk Kunci Indeks Clustered (s)) yang akan Anda butuhkan karena 98,45% pertanyaan terhadap tabel data klien akan membutuhkan TenantID(pengecualian utama adalah ketika sampah mengumpulkan data lama berdasarkan pada CreatedDatedan tidak peduli tentang TenantID).

    Tidak, Anda tidak akan memasukkan FK seperti OutletIDke PK. PK perlu mengidentifikasi secara unik pertikaian tersebut, dan menambahkan dalam FK tidak akan membantu hal itu. Bahkan, itu akan meningkatkan peluang untuk data duplikat, dengan anggapan bahwa OrderID unik untuk masing-masing TenantID, berbeda dengan unik per masing OutletID-masing dalam masing-masing TenantID.

    Juga, tidak perlu menambahkan OutletIDke PK untuk memastikan bahwa Outlet dari Tenant A tidak tercampur dengan Tenant B. Karena semua tabel data pengguna akan ada TenantIDdi PK, itu berarti TenantIDjuga akan ada di FK . Misalnya, Outlettabel memiliki PK (TenantID, OutletID), dan Ordertabel memiliki PK (TenantID, OrderID) dan FK (TenantID, OutletID)yang merujuk PK di atas Outletmeja. FK yang didefinisikan dengan benar akan mencegah data Tenant dari bercampur.

  4. Apakah urutan kunci komposit penting?

    Nah, di sinilah tempat itu menjadi menyenangkan. Ada beberapa perdebatan tentang bidang mana yang harus didahulukan. Aturan "khas" untuk mendesain indeks yang baik adalah memilih bidang yang paling selektif untuk menjadi bidang utama. TenantID, pada dasarnya, tidak akan menjadi bidang yang paling selektif; yang IDbidang yang paling lapangan selektif. Berikut ini beberapa pemikiran:

    • ID pertama: Ini adalah bidang yang paling selektif (yaitu paling unik). Tetapi dengan menjadi bidang kenaikan otomatis (atau acak jika masih menggunakan GUID), data masing-masing pelanggan tersebar di setiap tabel. Ini berarti bahwa ada kalanya pelanggan membutuhkan 100 baris, dan itu membutuhkan hampir 100 halaman data yang dibaca dari disk (tidak cepat) ke dalam Buffer Pool (mengambil lebih banyak ruang daripada 10 halaman data). Hal ini juga meningkatkan pertikaian pada halaman data karena akan lebih sering bahwa banyak pelanggan perlu memperbarui halaman data yang sama.

      Namun, Anda biasanya tidak mengalami sebanyak masalah parameter sniffing / rencana cache yang jelek karena statistik lintas nilai ID yang berbeda cukup konsisten. Anda mungkin tidak mendapatkan rencana yang paling optimal, tetapi Anda tidak akan mendapatkan rencana yang mengerikan. Metode ini pada dasarnya mengorbankan kinerja (sedikit) di semua pelanggan untuk mendapatkan manfaat dari masalah yang lebih jarang.

    • TenantID pertama:Ini sangat tidak selektif sama sekali. Mungkin ada sedikit variasi di 1 juta baris jika Anda hanya memiliki 100 TenantID. Tetapi statistik untuk kueri ini lebih akurat karena SQL Server akan tahu bahwa permintaan untuk penyewa A akan menarik kembali 500.000 baris tetapi permintaan yang sama untuk penyewa B hanya 50 baris. Di sinilah titik nyeri utamanya. Metode ini sangat meningkatkan kemungkinan memiliki masalah mengendus parameter di mana proses pertama dari Prosedur Tersimpan adalah untuk Penyewa A dan bertindak dengan tepat berdasarkan Pengoptimal Kueri melihat statistik tersebut dan mengetahui itu perlu efisien mendapatkan 500k baris. Tetapi ketika Tenant B, dengan hanya 50 baris, berjalan, rencana eksekusi tidak lagi sesuai, dan pada kenyataannya, sangat tidak pantas. DAN, karena data tidak dimasukkan dalam urutan bidang terkemuka,

      Namun, untuk TenantID pertama yang menjalankan Prosedur Tersimpan, kinerja harus lebih baik daripada dalam pendekatan lain karena data (setidaknya setelah melakukan pemeliharaan indeks) akan diatur secara fisik dan logis sehingga jauh lebih sedikit halaman data diperlukan untuk memenuhi pertanyaan. Ini berarti I / O fisik lebih sedikit, lebih sedikit bacaan logis, lebih sedikit pertentangan antara Penyewa untuk halaman data yang sama, lebih sedikit ruang terbuang yang digunakan dalam Buffer Pool (karenanya meningkatkan Page Life Expectancy) dll.

      Ada dua biaya utama untuk mendapatkan peningkatan kinerja ini. Yang pertama tidak begitu sulit: Anda harus melakukan pemeliharaan indeks secara teratur untuk mengatasi peningkatan fragmentasi. Yang kedua agak kurang menyenangkan.

      Untuk mengatasi peningkatan masalah sniffing parameter, Anda harus memisahkan rencana eksekusi antara Penyewa. Pendekatan sederhana adalah untuk digunakan WITH RECOMPILEpada procs atau OPTION (RECOMPILE)petunjuk kueri, tetapi itu adalah hit pada kinerja yang bisa menghapus semua keuntungan yang dibuat dengan menempatkan TenantIDpertama. Metode yang saya temukan bekerja paling baik adalah dengan menggunakan Dynamic SQL parameterized via sp_executesql. Alasan untuk membutuhkan SQL Dinamis adalah untuk memungkinkan menggabungkan TenantID ke dalam teks kueri, sementara semua predikat lain yang biasanya menjadi parameter masih merupakan parameter. Misalnya, jika Anda mencari pesanan tertentu, Anda akan melakukan sesuatu seperti:

      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;

      Efek yang dimilikinya adalah membuat rencana permintaan yang dapat digunakan kembali hanya untuk TenantID yang akan cocok dengan volume data Tenant tertentu. Jika penyewa yang sama mengeksekusi prosedur yang tersimpan lagi untuk yang lain @OrderIDmaka itu akan menggunakan kembali rencana permintaan yang di-cache. Penyewa berbeda yang menjalankan Prosedur Disimpan yang sama akan menghasilkan teks kueri yang hanya berbeda dalam nilai TenantID, tetapi setiap perbedaan dalam teks kueri cukup untuk menghasilkan rencana yang berbeda. Dan rencana yang dihasilkan untuk Tenant B tidak hanya akan cocok dengan volume data untuk Tenant B, tetapi juga akan dapat digunakan kembali untuk Tenant B untuk nilai yang berbeda dari @OrderID(karena predikat itu masih parameter).

      Kelemahan dari pendekatan ini adalah:

      • Ini sedikit lebih banyak pekerjaan daripada hanya mengetikkan query sederhana (tetapi tidak semua query harus Dynamic SQL, hanya yang akhirnya memiliki masalah sniffing parameter).
      • Bergantung pada berapa banyak Penyewa pada suatu sistem, ia meningkatkan ukuran cache rencana karena setiap permintaan sekarang membutuhkan 1 paket per TenantID yang memanggilnya. Ini mungkin bukan masalah, tetapi setidaknya sesuatu yang harus diperhatikan.
      • SQL dinamis memecah rantai kepemilikan, yang berarti akses baca / tulis ke tabel tidak dapat diasumsikan dengan memiliki EXECUTEizin pada Prosedur Tersimpan. Perbaikan yang mudah tapi kurang aman hanya untuk memberikan akses langsung ke tabel. Ini tentu saja tidak ideal, tetapi itu biasanya merupakan pertukaran dengan cepat dan mudah. Pendekatan yang lebih aman adalah dengan menggunakan keamanan berbasis Sertifikat. Artinya, membuat sertifikat, kemudian membuat Pengguna dari yang Sertifikat, memberikan bahwa pengguna hak akses yang diinginkan (User berbasis sertifikat atau Login tidak dapat terhubung ke SQL Server sendiri), dan kemudian menandatangani Stored Procedures yang menggunakan Dynamic SQL dengan itu Sertifikat yang sama melalui ADD SIGNATURE .

        Untuk informasi lebih lanjut tentang penandatanganan modul dan Sertifikat, silakan lihat: ModuleSigning.Info
         

    Silakan lihat bagian PEMBARUAN menjelang akhir untuk topik tambahan yang terkait dengan masalah berurusan dengan mitigasi masalah statistik yang dihasilkan dari keputusan ini.


** Secara pribadi, saya benar-benar tidak suka hanya menggunakan "ID" untuk nama bidang PK di setiap tabel karena tidak bermakna, dan tidak konsisten di seluruh FK karena PK selalu "ID" dan bidang di tabel anak harus termasuk nama tabel induk. Misalnya: Orders.ID-> OrderItems.OrderID. Saya merasa jauh lebih mudah untuk berurusan dengan model data yang memiliki: Orders.OrderID-> OrderItems.OrderID. Ini lebih mudah dibaca, dan mengurangi berapa kali Anda akan mendapatkan kesalahan "referensi kolom ambigu" :-).


MEMPERBARUI

  • Apakah OPTIMIZE FOR UNKNOWN Petunjuk Kueri (diperkenalkan dalam SQL Server 2008) membantu dengan pemesanan PK komposit?

    Tidak juga. Opsi ini tidak mengatasi masalah mengendus parameter, tetapi hanya mengganti satu masalah dengan yang lain. Dalam hal ini, alih-alih mengingat info statistik untuk nilai parameter dari proses awal dari prosedur tersimpan atau kueri parameterisasi (yang pasti bagus untuk beberapa, tetapi berpotensi biasa-biasa saja untuk beberapa, dan berpotensi mengerikan untuk beberapa), ia menggunakan umum statistik distribusi data untuk memperkirakan jumlah baris. Hit-atau-miss ini mengenai berapa banyak (dan sampai tingkat) pertanyaan apa yang akan terpengaruh secara positif, negatif, atau tidak sama sekali. Setidaknya dengan parameter sniffing, beberapa permintaan dijamin akan menguntungkan. Jika sistem Anda memiliki Penyewa dengan volume data yang sangat beragam, ini berpotensi merusak kinerja untuk semua kueri.

    Opsi ini menyelesaikan hal yang sama dengan menyalin parameter input ke variabel lokal dan kemudian menggunakan variabel lokal dalam kueri (saya telah menguji ini tetapi tidak ada ruang untuk itu di sini). Info tambahan dapat ditemukan di posting blog ini: http://www.brentozar.com/archive/2013/06/optimize-for-unknown-sql-server-parameter-sniffing/ . Membaca komentar, Daniel Pepermans sampai pada kesimpulan yang mirip dengan saya mengenai penggunaan Dynamic SQL yang memiliki variasi terbatas.

  • Jika ID adalah bidang utama dalam Indeks Clustered, apakah akan membantu / mencukupi untuk memiliki Indeks Non-Clustered di (TenantID, ID), atau just (TenantID) untuk memiliki statistik akurat untuk kueri yang memproses banyak baris penyewa tunggal?

    Ya, itu akan membantu. Sistem besar yang saya sebutkan bekerja selama bertahun-tahun didasarkan pada desain indeks memiliki IDENTITYbidang sebagai bidang terkemuka karena lebih selektif dan mengurangi masalah mengendus parameter. Namun, ketika kami perlu melakukan operasi terhadap sebagian besar data Tenant tertentu, kinerjanya tidak bertahan. Bahkan, sebuah proyek untuk memigrasi semua data ke dalam basis data baru harus ditunda karena pengendali SAN mendapatkan hasil maksimal dalam hal throughput. Cara mengatasinya adalah menambahkan Indeks Non-Clustered ke semua tabel data penyewa menjadi adil (TenantID). Tidak perlu dilakukan (TenantID, ID) karena ID sudah ada dalam Indeks Clustered sehingga struktur internal Indeks Non-Clustered secara alami (TenantID, ID).

    Meskipun hal ini benar-benar menyelesaikan masalah segera untuk dapat melakukan permintaan berdasarkan TenantID jauh lebih efisien, mereka masih tidak seefisien yang seharusnya jika Clustered Index yang berada dalam urutan yang sama. Dan, sekarang kami memiliki satu indeks lagi di setiap tabel. Yang meningkatkan jumlah ruang SAN yang kami gunakan, meningkatkan ukuran cadangan kami, membuat cadangan membutuhkan waktu lebih lama untuk diselesaikan, meningkatkan potensi pemblokiran dan kebuntuan, penurunan kinerja INSERTdan DELETEoperasi, dll.

    DAN kami masih dibiarkan dengan inefisiensi umum memiliki data Tenant tersebar di banyak halaman data, dicampur dengan banyak data Tenant lainnya. Seperti yang saya sebutkan di atas, ini meningkatkan jumlah pertikaian pada halaman-halaman ini, dan mengisi Buffer Pool dengan banyak halaman data yang memiliki 1 atau 2 baris berguna di dalamnya, terutama ketika beberapa baris pada halaman tersebut adalah untuk klien yang tidak aktif tetapi belum mengumpulkan sampah. Ada sedikit potensi untuk menggunakan kembali halaman data dalam Buffer Pool dalam pendekatan ini, sehingga Page Life Expectancy kami cukup rendah. Dan itu berarti lebih banyak waktu kembali ke disk untuk memuat lebih banyak halaman.

Solomon Rutzky
sumber
2
Sudahkah Anda mempertimbangkan atau menguji MENGOPTIMALKAN UNTUK TIDAK DIKETAHUI di ruang masalah ini? Hanya penasaran.
RLF
1
@RLF Ya, kami meneliti opsi itu, dan itu seharusnya setidaknya tidak lebih baik, dan mungkin lebih buruk, daripada kinerja yang kurang optimal yang kami dapatkan dari memiliki bidang IDENTITAS terlebih dahulu. Saya tidak ingat di mana saya membaca ini, tetapi seharusnya memberikan statistik "rata-rata" yang sama dengan menugaskan param input ke variabel lokal. Tapi artikel ini membahas mengapa opsi itu tidak benar-benar menyelesaikan masalah: brentozar.com/archive/2013/06/... Membaca komentar, Daniel Pepermans sampai pada kesimpulan yang sama: Dynamic SQL dengan variasi terbatas :)
Solomon Rutzky
3
Bagaimana jika indeks berkerumun aktif (ID, TenantID)dan Anda juga membuat indeks non-berkerumun aktif (TenantID, ID), atau hanya (TenantID)untuk memiliki statistik yang akurat untuk kueri yang memproses sebagian besar baris penyewa tunggal?
Vladimir Baranov
1
@VladimirBaranov Pertanyaan yang bagus. Saya telah mengatasinya di bagian PEMBARUAN baru menjelang akhir jawaban :-).
Solomon Rutzky
4
poin bagus tentang sql dinamis untuk menghasilkan rencana untuk setiap pelanggan.
Max Vernon