Jika Anda menggunakan partisi tabel dengan satu atau beberapa partisi yang disimpan di grup file baca-saja, pernyataan pemutakhiran dan penghapusan SQL mungkin gagal dengan kesalahan. Tentu saja, ini adalah perilaku yang diharapkan jika ada modifikasi yang memerlukan penulisan ke filegroup read-only; namun juga memungkinkan untuk menemukan kondisi kesalahan ini di mana perubahan dibatasi pada grup file yang ditandai sebagai baca-tulis.
Contoh Basis Data
Untuk mendemonstrasikan masalah ini, kami akan membuat database sederhana dengan satu grup file khusus yang nantinya akan kami tandai sebagai hanya-baca. Perhatikan bahwa Anda perlu menambahkan jalur nama file agar sesuai dengan contoh pengujian Anda.
USE master; GO CREATE DATABASE Test; GO -- This filegroup will be marked read-only later ALTER DATABASE Test ADD FILEGROUP ReadOnlyFileGroup; GO -- Add a file to the new filegroup ALTER DATABASE Test ADD FILE ( NAME = 'Test_RO', FILENAME = '<...your path...>\MSSQL\DATA\Test_ReadOnly.ndf' ) TO FILEGROUP ReadOnlyFileGroup;
Fungsi dan skema partisi
Sekarang kita akan membuat fungsi dan skema partisi dasar yang akan mengarahkan baris dengan data sebelum 1 Januari 2000 ke partisi read-only. Nanti data akan disimpan di filegroup utama baca-tulis:
USE Test; GO CREATE PARTITION FUNCTION PF (datetime) AS RANGE RIGHT FOR VALUES ({D '2000-01-01'}); GO CREATE PARTITION SCHEME PS AS PARTITION PF TO (ReadOnlyFileGroup, [PRIMARY]);
Spesifikasi kanan rentang berarti baris dengan nilai batas 1 Januari 2000 akan berada di partisi baca-tulis.
Tabel dan indeks yang dipartisi
Sekarang kita dapat membuat tabel pengujian kita:
CREATE TABLE dbo.Test ( dt datetime NOT NULL, c1 integer NOT NULL, c2 integer NOT NULL, CONSTRAINT PK_dbo_Test__c1_dt PRIMARY KEY CLUSTERED (dt) ON PS (dt) ) ON PS (dt); GO CREATE NONCLUSTERED INDEX IX_dbo_Test_c1 ON dbo.Test (c1) ON PS (dt); GO CREATE NONCLUSTERED INDEX IX_dbo_Test_c2 ON dbo.Test (c2) ON PS (dt);
Tabel memiliki kunci utama berkerumun pada kolom datetime, dan juga dipartisi pada kolom itu. Ada indeks nonclustered pada dua kolom bilangan bulat lainnya, yang dipartisi dengan cara yang sama (indeks disejajarkan dengan tabel dasar).
Contoh data
Terakhir, kami menambahkan beberapa baris contoh data, dan membuat partisi data pra-2000 hanya dapat dibaca:
INSERT dbo.Test WITH (TABLOCKX) (dt, c1, c2) VALUES ({D '1999-12-31'}, 1, 1), -- Read only ({D '2000-01-01'}, 2, 2); -- Writable GO ALTER DATABASE Test MODIFY FILEGROUP ReadOnlyFileGroup READ_ONLY;
Anda dapat menggunakan pernyataan pembaruan pengujian berikut untuk mengonfirmasi bahwa data di partisi hanya-baca tidak dapat diubah, sedangkan data dengan dt
nilai pada atau setelah 1 Januari 2000 dapat ditulis ke:
-- Will fail, as expected UPDATE dbo.Test SET c2 = 1 WHERE dt = {D '1999-12-31'}; -- Will succeed, as expected UPDATE dbo.Test SET c2 = 999 WHERE dt = {D '2000-01-01'}; -- Reset the value of c2 UPDATE dbo.Test SET c2 = 2 WHERE dt = {D '2000-01-01'};
Kegagalan Tak Terduga
Kami memiliki dua baris:satu hanya-baca (1999-12-31); dan satu baca-tulis (2000-01-01):
Sekarang coba kueri berikut. Ini mengidentifikasi baris "2000-01-01" yang dapat ditulis yang sama yang baru saja berhasil kami perbarui, tetapi menggunakan predikat klausa where yang berbeda:
UPDATE dbo.Test SET c2 = 2 WHERE c1 = 2;
Perkiraan rencana (pra-eksekusi) adalah:
Empat (!) Hitung Skalar tidak penting untuk diskusi ini. Mereka digunakan untuk menentukan apakah indeks nonclustered perlu dipertahankan untuk setiap baris yang tiba di operator Clustered Index Update.
Yang lebih menarik adalah pernyataan pembaruan ini gagal dengan kesalahan yang mirip dengan:
Msg 652, Level 16, Status 1Indeks "PK_dbo_Test__c1_dt" untuk tabel "dbo.Test" (RowsetId 72057594039042048) berada di filegroup read-only ("ReadOnlyFileGroup"), yang tidak dapat dimodifikasi.
Bukan Penghapusan Partisi
Jika Anda pernah bekerja dengan partisi sebelumnya, Anda mungkin berpikir bahwa 'penghapusan partisi' mungkin menjadi alasannya. Logikanya akan seperti ini:
Dalam pernyataan sebelumnya, nilai literal untuk kolom partisi disediakan di klausa where, sehingga SQL Server dapat segera menentukan partisi mana yang akan diakses. Dengan mengubah klausa where menjadi tidak lagi mereferensikan kolom partisi, kami telah memaksa SQL Server untuk mengakses setiap partisi menggunakan Clustered Index Scan.
Itu semua benar, secara umum, tetapi itu bukan alasan mengapa pernyataan pembaruan gagal di sini.
Perilaku yang diharapkan adalah bahwa SQL Server harus dapat membaca dari setiap dan semua partisi selama eksekusi kueri. Operasi modifikasi data seharusnya hanya gagal jika mesin eksekusi benar-benar mencoba untuk memodifikasi baris yang disimpan pada filegroup hanya-baca.
Sebagai ilustrasi, mari kita buat sedikit perubahan pada kueri sebelumnya:
UPDATE dbo.Test SET c2 = 2, dt = dt WHERE c1 = 2;
Klausa where sama persis seperti sebelumnya. Satu-satunya perbedaan adalah bahwa kita sekarang (sengaja) mengatur kolom partisi sama dengan dirinya sendiri. Ini tidak akan mengubah nilai yang disimpan di kolom itu, tetapi itu mempengaruhi hasilnya. Pembaruan sekarang berhasil (walaupun dengan rencana eksekusi yang lebih kompleks):
Pengoptimal telah memperkenalkan operator Split, Sort, dan Collapse baru, dan menambahkan mesin yang diperlukan untuk mempertahankan setiap indeks nonclustered yang berpotensi terpengaruh secara terpisah (menggunakan strategi lebar, atau per indeks).
Properti Clustered Index Scan menunjukkan bahwa kedua partisi tabel diakses saat membaca:
Sebaliknya, Pembaruan Indeks Clustered menunjukkan bahwa hanya partisi baca-tulis yang diakses untuk menulis:
Setiap operator Pembaruan Indeks Nonclustered menunjukkan informasi yang serupa:hanya partisi yang dapat ditulis (#2) yang dimodifikasi pada saat dijalankan, jadi tidak ada kesalahan yang terjadi.
Alasan Terungkap
Rencana baru berhasil tidak karena indeks nonclustered dipertahankan secara terpisah; atau apakah ini (langsung) karena kombinasi Split-Sort-Collapse yang diperlukan untuk menghindari kesalahan kunci duplikat sementara dalam indeks unik.
Alasan sebenarnya adalah sesuatu yang saya sebutkan secara singkat di artikel saya sebelumnya, "Mengoptimalkan Kueri Pembaruan" – pengoptimalan internal yang dikenal sebagai Berbagi Rowset . Saat ini digunakan, Pembaruan Indeks Clustered berbagi baris mesin penyimpanan dasar yang sama sebagai Clustered Index Scan, Seek, atau Key Lookup di sisi membaca paket.
Dengan optimasi Rowset Sharing, SQL Server memeriksa offline atau filegroup read-only saat membaca. Dalam paket di mana Pembaruan Indeks Tergugus menggunakan kumpulan baris terpisah, pemeriksaan offline/hanya-baca hanya dilakukan untuk setiap baris di iterator pembaruan (atau hapus).
Solusi Tidak Terdokumentasi
Mari kita singkirkan hal-hal yang menyenangkan, culun, tetapi tidak praktis terlebih dahulu.
Pengoptimalan rowset bersama hanya dapat diterapkan bila rute dari pencarian indeks berkerumun, pemindaian, atau pencarian kunci adalah pipa . Operator pemblokiran atau semi pemblokiran tidak diizinkan. Dengan kata lain, setiap baris harus bisa dari sumber baca ke tujuan tulis sebelum baris berikutnya dibaca.
Sebagai pengingat, berikut adalah contoh data, pernyataan, dan rencana eksekusi untuk gagal perbarui lagi:
--Change the read-write row UPDATE dbo.Test SET c2 = 2 WHERE c1 = 2;
Perlindungan Halloween
Salah satu cara untuk memperkenalkan operator pemblokiran ke rencana tersebut adalah dengan meminta Perlindungan Halloween (HP) eksplisit untuk pembaruan ini. Memisahkan pembacaan dari penulisan dengan operator pemblokiran akan mencegah pengoptimalan berbagi baris agar tidak digunakan (tanpa saluran pipa). Bendera jejak tidak terdokumentasi dan tidak didukung (hanya sistem pengujian!) 8692 menambahkan Eager Table Spool untuk HP eksplisit:
-- Works (explicit HP) UPDATE dbo.Test SET c2 = 2 WHERE c1 = 2 OPTION (QUERYTRACEON 8692);
Rencana eksekusi yang sebenarnya (tersedia karena kesalahan tidak lagi terjadi) adalah:
Kombinasi Sort in the Split-Sort-Collapse yang terlihat pada pembaruan sebelumnya yang berhasil memberikan pemblokiran yang diperlukan untuk menonaktifkan berbagi rowset dalam instance tersebut.
Tanda Jejak Berbagi Anti-Rowset
Ada tanda jejak tidak berdokumen lain yang menonaktifkan pengoptimalan berbagi rowset. Ini memiliki keuntungan karena tidak memperkenalkan operator pemblokiran yang berpotensi mahal. Itu tidak dapat digunakan dalam praktik tentu saja (kecuali jika Anda menghubungi Dukungan Microsoft dan mendapatkan sesuatu secara tertulis yang merekomendasikan Anda untuk mengaktifkannya, saya kira). Namun demikian, untuk tujuan hiburan, berikut adalah jejak bendera 8746 beraksi:
-- Works (no rowset sharing) UPDATE dbo.Test SET c2 = 2 WHERE c1 = 2 OPTION (QUERYTRACEON 8746);
Rencana eksekusi sebenarnya untuk pernyataan itu adalah:
Jangan ragu untuk bereksperimen dengan nilai yang berbeda (nilai yang benar-benar mengubah nilai yang disimpan jika Anda mau) untuk meyakinkan diri Anda tentang perbedaannya di sini. Seperti yang disebutkan dalam posting saya sebelumnya, Anda juga dapat menggunakan tanda jejak tidak berdokumen 8666 untuk mengekspos properti berbagi baris dalam rencana eksekusi.
Jika Anda ingin melihat kesalahan berbagi rowset dengan pernyataan hapus, cukup ganti klausa update dan setel dengan delete, sambil menggunakan klausa where yang sama.
Solusi yang Didukung
Ada sejumlah cara potensial untuk memastikan bahwa berbagi baris tidak diterapkan dalam kueri dunia nyata tanpa menggunakan tanda pelacakan. Sekarang setelah Anda mengetahui bahwa masalah inti memerlukan rencana baca dan tulis indeks berkerumun yang dibagikan dan disalurkan, Anda mungkin dapat membuat rencana Anda sendiri. Meski begitu, ada beberapa contoh yang sangat layak untuk dilihat di sini.
Indeks Paksa / Indeks Penutup
Satu ide alami adalah memaksa sisi membaca dari rencana untuk menggunakan indeks nonclustered alih-alih indeks clustered. Kami tidak dapat menambahkan petunjuk indeks secara langsung ke kueri pengujian seperti yang tertulis, tetapi membuat alias tabel memungkinkan hal ini:
UPDATE T SET c2 = 2 FROM dbo.Test AS T WITH (INDEX(IX_dbo_Test_c1)) WHERE c1 = 2;
Ini mungkin tampak seperti solusi yang seharusnya dipilih oleh pengoptimal kueri sejak awal, karena kami memiliki indeks nonclustered pada kolom predikat klausa where c1. Rencana eksekusi menunjukkan mengapa pengoptimal memilih seperti itu:
Biaya Pencarian Kunci cukup untuk meyakinkan pengoptimal menggunakan indeks berkerumun untuk membaca. Pencarian diperlukan untuk mengambil nilai kolom c2 saat ini, sehingga Skalar Hitung dapat memutuskan apakah indeks nonclustered perlu dipertahankan.
Menambahkan kolom c2 ke indeks nonclustered (kunci atau termasuk) akan menghindari masalah. Pengoptimal akan memilih indeks yang mencakup sekarang daripada indeks berkerumun.
Yang mengatakan, tidak selalu mungkin untuk mengantisipasi kolom mana yang akan dibutuhkan, atau untuk memasukkan semuanya bahkan jika himpunan diketahui. Ingat, kolom diperlukan karena c2 ada di klausa set dari pernyataan pembaruan. Jika kueri bersifat ad-hoc (mis. diajukan oleh pengguna atau dihasilkan oleh alat), setiap indeks yang tidak dikelompokkan harus menyertakan semua kolom untuk menjadikannya opsi yang kuat.
Satu hal yang menarik tentang rencana dengan Pencarian Kunci di atas adalah bahwa hal itu tidak menghasilkan kesalahan. Ini terlepas dari Pencarian Kunci dan Pembaruan Indeks Clustered menggunakan Baris Bersama. Alasannya adalah bahwa Pencarian Indeks yang tidak berkerumun menempatkan baris dengan c1 =2 sebelum Pencarian Kunci menyentuh indeks berkerumun. Pemeriksaan rowset bersama untuk grup file offline / read-only masih dilakukan pada pencarian, tetapi tidak menyentuh partisi read-only, jadi tidak ada kesalahan yang terjadi. Sebagai tujuan akhir (terkait), perhatikan bahwa Pencarian Indeks menyentuh kedua partisi, tetapi Pencarian Kunci hanya mengenai satu.
Tidak termasuk partisi read-only
Solusi sepele adalah mengandalkan eliminasi partisi sehingga sisi pembacaan rencana tidak pernah menyentuh partisi hanya-baca. Ini dapat dilakukan dengan predikat eksplisit, misalnya salah satu dari ini:
UPDATE dbo.Test SET c2 = 2 WHERE c1 = 2 AND dt >= {D '2000-01-01'}; UPDATE dbo.Test SET c2 = 2 WHERE c1 = 2 AND $PARTITION.PF(dt) > 1; -- Not partition #1
Jika tidak mungkin, atau tidak nyaman, untuk mengubah setiap kueri untuk menambahkan predikat eliminasi partisi, solusi lain seperti memperbarui melalui tampilan mungkin cocok. Misalnya:
CREATE VIEW dbo.TestWritablePartitions WITH SCHEMABINDING AS -- Only the writable portion of the table SELECT T.dt, T.c1, T.c2 FROM dbo.Test AS T WHERE $PARTITION.PF(dt) > 1; GO -- Succeeds UPDATE dbo.TestWritablePartitions SET c2 = 2 WHERE c1 = 2;
Salah satu kelemahan menggunakan tampilan adalah bahwa pembaruan atau penghapusan yang menargetkan bagian baca-saja dari tabel dasar akan berhasil tanpa mempengaruhi baris, daripada gagal dengan kesalahan. Alih-alih pemicu di tabel atau tampilan mungkin menjadi solusi untuk itu dalam beberapa situasi, tetapi juga dapat menimbulkan lebih banyak masalah…tapi saya ngelantur.
Seperti disebutkan sebelumnya, ada banyak solusi potensial yang didukung. Inti dari artikel ini adalah untuk menunjukkan bagaimana berbagi baris menyebabkan kesalahan pembaruan yang tidak terduga.