Mengubah kunci utama dari IDENTITY menjadi persisten kolom dihitung menggunakan COALESCE

10

Dalam upaya untuk memisahkan aplikasi dari database monolitik kami, kami telah mencoba untuk mengubah kolom INT IDENTITY dari berbagai tabel menjadi kolom dihitung PERSISTED yang menggunakan COALESCE. Pada dasarnya, kita memerlukan aplikasi yang dipisahkan kemampuan untuk masih memperbarui database untuk data umum yang dibagikan di banyak aplikasi sambil tetap memungkinkan aplikasi yang ada untuk membuat data dalam tabel ini tanpa perlu modifikasi kode atau prosedur.

Jadi pada dasarnya, kami telah pindah dari definisi kolom tentang;

PkId INT IDENTITY(1,1) PRIMARY KEY

untuk;

PkId AS AS COALESCE(old_id, external_id, new_id) PERSISTED NOT NULL,
old_id INT NULL, -- Values here are from existing records of PkId before table change
external_id INT NULL,
new_id INT IDENTITY(2000000,1) NOT NULL

Dalam semua kasus PkId juga merupakan KUNCI UTAMA dan dalam semua kasus kecuali satu, itu CLUSTERED. Semua tabel memiliki kunci asing dan indeks yang sama seperti sebelumnya. Intinya, format baru memungkinkan PkId disediakan oleh aplikasi yang dipisahkan (sebagai external_id), tetapi juga memungkinkan PkId menjadi nilai kolom IDENTITY sehingga memungkinkan kode yang ada yang bergantung pada kolom IDENTITY melalui penggunaan SCOPE_IDENTITY dan @@ IDENTITY untuk bekerja seperti dulu.

Masalah yang kami miliki adalah bahwa kami telah menemukan beberapa pertanyaan yang biasanya berjalan dalam waktu yang dapat diterima untuk sekarang benar-benar meledak. Paket kueri yang dihasilkan yang digunakan oleh kueri ini tidak seperti dulu.

Mengingat kolom baru adalah KUNCI UTAMA, tipe data yang sama seperti sebelumnya, dan TERUS, saya akan mengharapkan permintaan dan rencana kueri untuk berperilaku sama seperti sebelumnya. Haruskah KOMPUTED INT PkId pada dasarnya berperilaku dengan cara yang sama seperti definisi INT eksplisit dalam hal bagaimana SQL Server akan menghasilkan rencana eksekusi? Adakah kemungkinan masalah lain dengan pendekatan ini yang bisa Anda lihat?

Tujuan dari perubahan ini seharusnya memungkinkan kami untuk mengubah definisi tabel tanpa perlu memodifikasi prosedur dan kode yang ada. Mengingat masalah ini, saya merasa kita tidak bisa menggunakan pendekatan ini.

Tuan Moose
sumber
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Paul White 9

Jawaban:

4

PERTAMA

Anda mungkin tidak perlu semua tiga kolom: old_id, external_id, new_id. The new_idkolom, menjadi IDENTITY, akan memiliki nilai baru yang dihasilkan untuk setiap baris, bahkan ketika Anda masukkan ke dalam external_id. Tetapi, antara old_iddan external_id, itu cukup banyak saling eksklusif: apakah sudah ada old_idnilai atau kolom itu, dalam konsepsi saat ini, hanya akan NULLjika menggunakan external_idatau new_id. Karena Anda tidak akan menambahkan id "eksternal" baru ke baris yang sudah ada (yaitu yang memiliki old_idnilai), dan tidak akan ada nilai baru yang masuk old_id, maka mungkin ada satu kolom yang digunakan untuk kedua tujuan.

Jadi, singkirkan external_idkolom dan ganti nama old_idmenjadi sesuatu seperti old_or_external_idatau apa pun. Ini seharusnya tidak memerlukan perubahan nyata untuk apa pun, namun mengurangi beberapa komplikasi. Paling-paling Anda mungkin perlu memanggil kolom external_id, bahkan jika itu berisi nilai-nilai "lama", jika kode aplikasi sudah ditulis untuk dimasukkan external_id.

Itu mengurangi struktur baru menjadi adil:

PkId AS AS COALESCE(old_or_external_id, new_id, -1) PERSISTED NOT NULL,
old_or_external_id INT NULL, -- values from existing record OR passed in from app
new_id INT IDENTITY(2000000, 1) NOT NULL

Sekarang Anda hanya menambahkan 8 byte per baris, bukan 12 byte (dengan asumsi Anda tidak menggunakan SPARSEopsi atau Kompresi Data). Dan Anda tidak perlu mengubah kode apa pun, T-SQL atau kode Aplikasi.

KEDUA

Melanjutkan jalan penyederhanaan ini, mari kita lihat apa yang tersisa:

  • The old_or_external_idKolom baik memiliki nilai-nilai yang sudah, atau akan diberikan nilai baru dari aplikasi, atau akan ditinggalkan sebagai NULL.
  • The new_idakan selalu memiliki nilai baru yang dihasilkan, tetapi nilai yang hanya akan digunakan jika old_or_external_idkolom NULL.

Tidak pernah ada saat ketika Anda membutuhkan nilai di keduanya old_or_external_iddan new_id. Ya, akan ada saat-saat ketika kedua kolom memiliki nilai karena new_idmenjadi IDENTITY, tetapi new_idnilai - nilai itu diabaikan. Sekali lagi, kedua bidang ini saling eksklusif. Jadi bagaimana sekarang?

Sekarang kita bisa melihat mengapa kita membutuhkannya external_id. Menimbang bahwa dimungkinkan untuk memasukkan ke dalam IDENTITYkolom menggunakan SET IDENTITY_INSERT {table_name} ON;, Anda bisa lolos tanpa membuat perubahan skema sama sekali, dan hanya memodifikasi kode aplikasi Anda untuk membungkus INSERTpernyataan / operasi SET IDENTITY_INSERT {table_name} ON;dan SET IDENTITY_INSERT {table_name} OFF;pernyataan. Anda kemudian perlu menentukan rentang awal untuk mereset IDENTITYkolom ke (untuk nilai yang baru dihasilkan) karena harus jauh di atas nilai-nilai yang akan disisipkan kode Aplikasi karena memasukkan nilai yang lebih tinggi akan menyebabkan nilai yang dihasilkan secara otomatis berikutnya menjadi lebih besar dari nilai MAX saat ini. Tetapi Anda selalu dapat memasukkan nilai yang di bawah nilai IDENT_CURRENT .

Menggabungkan old_or_external_iddan new_idkolom juga tidak meningkatkan peluang berlari ke situasi nilai yang tumpang tindih antara nilai yang dibuat secara otomatis dan nilai yang dihasilkan aplikasi karena maksud memiliki kolom 2, atau bahkan 3, adalah untuk menggabungkannya menjadi nilai Kunci Utama, dan itu selalu merupakan nilai unik.

Dalam pendekatan ini, Anda hanya perlu:

  • Tinggalkan tabel sebagai:

    PkId INT IDENTITY(1,1) PRIMARY KEY

    Ini menambahkan 0 byte ke setiap baris, bukannya 8, atau bahkan 12.

  • Tentukan rentang awal untuk nilai yang dihasilkan aplikasi. Ini akan lebih besar dari nilai MAX saat ini di setiap tabel, tetapi kurang dari apa yang akan menjadi nilai minimum untuk nilai yang dihasilkan secara otomatis.
  • Tentukan nilai berapa rentang yang dihasilkan secara otomatis harus dimulai. Seharusnya ada banyak ruang antara nilai MAX saat ini dan banyak ruang untuk tumbuh, mengetahui pada batas atas hanya lebih dari 2,14 miliar. Anda kemudian dapat menetapkan nilai seed minimum baru ini melalui DBCC CHECKIDENT .
  • Bungkus kode aplikasi Sisipkan SET IDENTITY_INSERT {table_name} ON;dan SET IDENTITY_INSERT {table_name} OFF;pernyataan.

KEDUA, Bagian B

Variasi pada pendekatan yang dicatat secara langsung di atas adalah memiliki nilai-nilai penyisipan kode aplikasi yang dimulai dengan -1 dan turun dari sana. Ini meninggalkan IDENTITYnilai - nilai sebagai satu-satunya yang naik . Manfaatnya di sini adalah Anda tidak hanya tidak memperumit skema, Anda juga tidak perlu khawatir akan mengalami ID yang tumpang tindih (jika nilai yang dihasilkan aplikasi masuk ke rentang baru yang dibuat secara otomatis). Ini hanya opsi jika Anda belum menggunakan nilai ID negatif (dan tampaknya sangat jarang bagi orang untuk menggunakan nilai negatif pada kolom yang dibuat secara otomatis sehingga ini kemungkinan kemungkinan dalam sebagian besar situasi).

Dalam pendekatan ini, Anda hanya perlu:

  • Tinggalkan tabel sebagai:

    PkId INT IDENTITY(1,1) PRIMARY KEY

    Ini menambahkan 0 byte ke setiap baris, bukannya 8, atau bahkan 12.

  • Rentang awal untuk nilai yang dihasilkan aplikasi akan menjadi -1.
  • Bungkus kode aplikasi Sisipkan SET IDENTITY_INSERT {table_name} ON;dan SET IDENTITY_INSERT {table_name} OFF;pernyataan.

Di sini Anda masih perlu melakukan IDENTITY_INSERT, tetapi: Anda tidak menambahkan kolom baru, tidak perlu "memasang kembali" IDENTITYkolom apa pun , dan tidak memiliki risiko tumpang tindih di masa depan.

KEDUA, Bagian 3

Satu variasi terakhir dari pendekatan ini adalah dengan menukar IDENTITYkolom dan menggunakan Sekuens . Alasan untuk mengambil pendekatan ini adalah untuk dapat memiliki nilai-nilai penyisipan kode aplikasi yang: positif, di atas rentang yang dibuat secara otomatis (tidak di bawah), dan tidak perlu SET IDENTITY_INSERT ON / OFF.

Dalam pendekatan ini, Anda hanya perlu:

  • Buat Urutan menggunakan CREATE SEQUENCE
  • Salin IDENTITYkolom ke kolom baru yang tidak memiliki IDENTITYproperti, tetapi memiliki DEFAULTkendala menggunakan fungsi VALUE FOR NEXT FOR :

    PkId INT PRIMARY KEY CONSTRAINT [DF_TableName_NextID] DEFAULT (NEXT VALUE FOR...)

    Ini menambahkan 0 byte ke setiap baris, bukannya 8, atau bahkan 12.

  • Rentang awal untuk nilai-nilai yang dihasilkan aplikasi akan jauh di atas apa yang Anda pikir akan mendekati nilai-nilai yang dihasilkan secara otomatis.
  • Bungkus kode aplikasi Sisipkan SET IDENTITY_INSERT {table_name} ON;dan SET IDENTITY_INSERT {table_name} OFF;pernyataan.

NAMUN , karena persyaratan bahwa kode dengan salah satu SCOPE_IDENTITY()atau @@IDENTITYmasih berfungsi dengan benar, beralih ke Sequences saat ini tidak menjadi pilihan karena tampaknya tidak ada yang setara dengan fungsi-fungsi untuk Sequences :-(. Sedih!

Solomon Rutzky
sumber
Terima kasih banyak atas jawaban Anda. Anda mengangkat beberapa poin yang dibahas di sini secara internal. Sayangnya, beberapa di antaranya tidak bekerja untuk kami karena beberapa alasan. Basis data kami cukup lama dan agak rapuh dan berjalan di bawah mode kompatibilitas 2005 sehingga SEQUENCES keluar. Dorongan data aplikasi kami terjadi melalui alat pemuatan data yang memperoleh catatan baru dari antrian broker layanan dan mendorongnya melalui beberapa utas. IDENTITY_INSERT hanya dapat digunakan untuk satu tabel per sesi, dan pemikiran saat ini adalah arsitektur kami tidak dapat memenuhi kebutuhan itu tanpa perubahan signifikan. Saya sedang menguji saran kepalan tangan Anda sekarang.
Mr Moose
@ Mulailah Ya, saya memperbarui jawaban saya untuk memasukkan lebih banyak info tentang Urutan di akhir. Lagipula itu tidak akan berhasil dalam situasi Anda. Dan saya bertanya-tanya tentang potensi masalah konkurensi dengan IDENTITY_INSERT, tetapi belum mengujinya. Tidak yakin opsi # 1 akan menyelesaikan masalah Anda secara keseluruhan, itu hanya pengamatan untuk mengurangi kerumitan yang tidak perlu. Namun, jika Anda memiliki beberapa utas yang memasukkan ID "eksternal" baru, bagaimana Anda menjamin bahwa mereka unik?
Solomon Rutzky
@MrMoose Sebenarnya, mengenai " IDENTITY_INSERT hanya dapat digunakan untuk satu tabel per sesi ", apa sebenarnya masalahnya di sini? 1) Anda hanya dapat memasukkan ke dalam satu tabel pada satu waktu, sehingga Anda mematikannya untuk TableA sebelum memasukkan ke TableB, dan 2) Saya baru saja menguji dan bertentangan dengan apa yang saya pikirkan, tidak ada masalah konkurensi - saya bisa miliki IDENTITY_INSERT ONuntuk tabel yang sama dalam dua sesi dan memasukkan keduanya tanpa masalah.
Solomon Rutzky
1
Seperti yang Anda sarankan, perubahan 1 membuat sedikit perbedaan. ID yang akan kami gunakan akan dialokasikan di luar basis data saat ini dan digunakan untuk menghubungkan catatan. Mungkin pemahaman saya tentang sesi tidak tepat sehingga IDENTITY_INSERT mungkin berfungsi. Akan butuh sedikit waktu bagiku untuk menyelidiki itu, jadi aku tidak akan bisa melaporkan kembali untuk sementara waktu. Sekali lagi terima kasih atas masukannya. Ini sangat dihargai.
Mr Moose
1
Saya pikir saran Anda untuk menggunakan IDENTITY_INSERT (dengan nilai seed tinggi untuk aplikasi yang ada) akan bekerja dengan baik. Aaron Bertrand memberikan jawaban di sini dengan contoh kecil yang bagus untuk mengujinya dengan konkurensi. Kami telah memodifikasi alat pemuatan data kami untuk dapat menangani tabel yang perlu menentukan nilai identitas dan kami akan melakukan beberapa pengujian lebih lanjut dalam beberapa minggu mendatang.
Tuan Moose