Hapus kinerja untuk data LOB di SQL Server

16

Pertanyaan ini terkait dengan utas forum ini .

Menjalankan SQL Server 2008 Developer Edition di workstation saya dan klaster mesin virtual dua simpul Enterprise Edition di mana saya merujuk ke "alpha cluster".

Waktu yang diperlukan untuk menghapus baris dengan kolom varbinary (max) secara langsung terkait dengan panjang data dalam kolom itu. Itu mungkin terdengar intuitif pada awalnya, tetapi setelah penyelidikan, bertentangan dengan pemahaman saya tentang bagaimana SQL Server sebenarnya menghapus baris secara umum dan berurusan dengan data jenis ini.

Masalahnya berasal dari masalah penghapusan batas waktu (> 30 detik) yang kita lihat di aplikasi web .NET kami, tetapi saya telah menyederhanakannya untuk diskusi ini.

Ketika catatan dihapus, SQL Server menandainya sebagai hantu yang akan dibersihkan oleh Tugas Pembersihan Ghost di lain waktu setelah transaksi dilakukan (lihat blog Paul Randal ). Dalam tes menghapus tiga baris dengan 16 KB, 4 MB, dan 50 MB data dalam kolom varbinary (maks), masing-masing, saya melihat ini terjadi pada halaman dengan bagian in-baris data, serta dalam transaksi catatan.

Apa yang tampak aneh bagi saya adalah bahwa kunci X ditempatkan pada semua halaman data LOB selama penghapusan, dan halaman-halaman tersebut di-deallocated di PFS. Saya melihat ini di log transaksi, serta dengan sp_lockdan hasil dm_db_index_operational_statsDMV ( page_lock_count).

Ini menciptakan bottleneck I / O pada workstation saya dan alpha cluster kami jika halaman-halaman itu belum ada di cache buffer. Bahkan, page_io_latch_wait_in_msdari DMV yang sama praktis seluruh durasi penghapusan, dan page_io_latch_wait_countsesuai dengan jumlah halaman yang dikunci. Untuk file 50 MB di workstation saya, ini diterjemahkan menjadi lebih dari 3 detik ketika dimulai dengan cache buffer kosong ( checkpoint/ dbcc dropcleanbuffers), dan saya tidak ragu itu akan lebih lama untuk fragmentasi berat dan di bawah beban.

Saya mencoba memastikan bahwa itu tidak hanya mengalokasikan ruang dalam cache saat itu. Saya membaca 2 GB data dari baris lain sebelum mengeksekusi hapus alih-alih checkpointmetode, yang lebih dari yang dialokasikan untuk proses SQL Server. Tidak yakin apakah itu tes yang valid atau tidak, karena saya tidak tahu bagaimana SQL Server mengocok data sekitar. Saya berasumsi itu akan selalu mendorong yang lama demi yang baru.

Lebih lanjut, itu bahkan tidak mengubah halaman. Ini bisa saya lihat dm_os_buffer_descriptors. Halaman-halaman tersebut bersih setelah dihapus, sementara jumlah halaman yang dimodifikasi kurang dari 20 untuk ketiga penghapusan kecil, sedang, dan besar. Saya juga membandingkan output DBCC PAGEuntuk pengambilan sampel halaman yang dicari, dan tidak ada perubahan (hanya ALLOCATEDbit yang dihapus dari PFS). Itu hanya memindahkan mereka.

Untuk lebih membuktikan bahwa pencarian halaman / deallocations menyebabkan masalah, saya mencoba tes yang sama menggunakan kolom filestream bukan vanilla varbinary (maks). Penghapusan adalah waktu yang konstan, terlepas dari ukuran LOB.

Jadi, pertama pertanyaan akademis saya:

  1. Mengapa SQL Server perlu mencari semua halaman data LOB untuk menguncinya? Apakah itu hanya detail bagaimana kunci direpresentasikan dalam memori (entah bagaimana disimpan dengan halaman)? Ini membuat dampak I / O sangat bergantung pada ukuran data jika tidak sepenuhnya di-cache.
  2. Mengapa X terkunci sama sekali, hanya untuk membatalkan alokasi mereka? Bukankah itu cukup untuk mengunci hanya daun indeks dengan bagian in-row, karena deallokasi tidak perlu memodifikasi halaman itu sendiri? Apakah ada cara lain untuk mendapatkan data LOB yang dilindungi oleh kunci?
  3. Mengapa tidak mengalokasikan halaman di depan sama sekali, mengingat sudah ada tugas latar belakang yang didedikasikan untuk jenis pekerjaan ini?

Dan mungkin yang lebih penting, pertanyaan praktis saya:

  • Apakah ada cara agar penghapusan beroperasi secara berbeda? Tujuan saya adalah penghapusan waktu secara konstan terlepas dari ukuran, mirip dengan filestream, di mana setiap pembersihan terjadi di latar belakang setelah fakta. Apakah ini konfigurasi? Apakah saya menyimpan barang-barang aneh?

Berikut adalah cara mereproduksi tes yang dijelaskan (dijalankan melalui jendela permintaan SSMS):

CREATE TABLE [T] (
    [ID] [uniqueidentifier] NOT NULL PRIMARY KEY,
    [Data] [varbinary](max) NULL
)

DECLARE @SmallID uniqueidentifier
DECLARE @MediumID uniqueidentifier
DECLARE @LargeID uniqueidentifier

SELECT @SmallID = NEWID(), @MediumID = NEWID(), @LargeID = NEWID()
-- May want to keep these IDs somewhere so you can use them in the deletes without var declaration

INSERT INTO [T] VALUES (@SmallID, CAST(REPLICATE(CAST('a' AS varchar(max)), 16 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@MediumID, CAST(REPLICATE(CAST('a' AS varchar(max)), 4 * 1024 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@LargeID, CAST(REPLICATE(CAST('a' AS varchar(max)), 50 * 1024 * 1024) AS varbinary(max)))

-- Do this before test
CHECKPOINT
DBCC DROPCLEANBUFFERS
BEGIN TRAN

-- Do one of these deletes to measure results or profile
DELETE FROM [T] WHERE ID = @SmallID
DELETE FROM [T] WHERE ID = @MediumID
DELETE FROM [T] WHERE ID = @LargeID

-- Do this after test
ROLLBACK

Berikut adalah beberapa hasil dari pembuatan profil penghapusan pada workstation saya:

| Jenis Kolom | Hapus Ukuran | Durasi (ms) | Baca | Menulis | CPU |
-------------------------------------------------- ------------------
| VarBinary | 16 KB | 40 | 13 | 2 | 0 |
| VarBinary | 4 MB | 952 | 2318 | 2 | 0 |
| VarBinary | 50 MB | 2976 | 28594 | 1 | 62 |
-------------------------------------------------- ------------------
| FileStream | 16 KB | 1 | 12 | 1 | 0 |
| FileStream | 4 MB | 0 | 9 | 0 | 0 |
| FileStream | 50 MB | 1 | 9 | 0 | 0 |

Kami tidak dapat hanya menggunakan filestream saja karena:

  1. Distribusi ukuran data kami tidak menjaminnya.
  2. Dalam praktiknya, kami menambahkan data dalam banyak potongan, dan filestream tidak mendukung pembaruan sebagian. Kita perlu mendesain ini.

Perbarui 1

Menguji teori bahwa data sedang ditulis ke log transaksi sebagai bagian dari penghapusan, dan ini tampaknya tidak terjadi. Apakah saya salah menguji? Lihat di bawah.

SELECT MAX([Current LSN]) FROM fn_dblog(NULL, NULL)
--0000002f:000001d9:0001

BEGIN TRAN
DELETE FROM [T] WHERE ID = @ID

SELECT
    SUM(
        DATALENGTH([RowLog Contents 0]) +
        DATALENGTH([RowLog Contents 1]) +
        DATALENGTH([RowLog Contents 3]) +
        DATALENGTH([RowLog Contents 4])
    ) [RowLog Contents Total],
    SUM(
        DATALENGTH([Log Record])
    ) [Log Record Total]
FROM fn_dblog(NULL, NULL)
WHERE [Current LSN] > '0000002f:000001d9:0001'

Untuk file dengan ukuran lebih dari 5 MB, ini dikembalikan 1651 | 171860.

Selanjutnya, saya berharap halaman itu sendiri menjadi kotor jika data ditulis ke log. Hanya deallocations yang tampaknya dicatat, yang cocok dengan apa yang kotor setelah penghapusan.

Perbarui 2

Saya memang mendapat respons balik dari Paul Randal. Dia menegaskan fakta bahwa itu harus membaca semua halaman untuk melintasi pohon dan menemukan halaman mana yang harus dialokasikan, dan menyatakan bahwa tidak ada cara lain untuk mencari halaman mana. Ini adalah setengah jawaban untuk 1 & 2 (meskipun tidak menjelaskan perlunya kunci pada data out-of-row, tapi itu adalah kentang kecil).

Pertanyaan 3 masih terbuka: Mengapa membatalkan alokasi halaman di depan jika sudah ada tugas latar belakang untuk melakukan pembersihan untuk dihapus?

Dan tentu saja, semua pertanyaan penting: Apakah ada cara untuk secara langsung mengurangi (yaitu tidak memperbaiki) perilaku penghapusan yang tergantung pada ukuran ini? Saya akan berpikir ini akan menjadi masalah yang lebih umum, kecuali kita benar-benar satu-satunya yang menyimpan dan menghapus 50 MB baris dalam SQL Server? Apakah semua orang di luar sana mengatasi ini dengan beberapa bentuk pekerjaan pengumpulan sampah?

Jeremy Rosenberg
sumber
Saya berharap ada solusi yang lebih baik, tetapi belum menemukannya. Saya memiliki situasi logging volume besar dari berbagai baris ukuran, hingga 1MB +, dan saya memiliki proses "pembersihan" untuk menghapus catatan lama. Karena penghapusannya sangat lambat, saya harus membaginya menjadi dua langkah - pertama-tama hapus referensi di antara tabel (yang sangat cepat), kemudian hapus baris yatim. Pekerjaan hapus rata-rata ~ 2,2 detik / MB untuk menghapus data. Jadi tentu saja saya harus mengurangi pertikaian, jadi saya memiliki prosedur tersimpan dengan "DELETE TOP (250)" di dalam satu lingkaran sampai tidak ada baris yang dihapus lagi.
Abacus

Jawaban:

5

Saya tidak bisa mengatakan mengapa tepatnya akan jauh lebih tidak efisien untuk menghapus VARBINARY (MAX) dibandingkan dengan aliran file tetapi satu ide yang dapat Anda pertimbangkan jika Anda hanya mencoba untuk menghindari waktu jeda dari aplikasi web Anda saat menghapus LOBS ini. Anda bisa menyimpan nilai-nilai VARBINARY (MAX) dalam tabel terpisah (sebut saja tblLOB) yang direferensikan oleh tabel asli (mari kita sebut tblParent ini).

Dari sini ketika Anda menghapus catatan, Anda bisa menghapusnya dari catatan induk dan kemudian memiliki proses pengumpulan sampah sesekali untuk masuk dan membersihkan catatan dalam tabel LOB. Mungkin ada aktivitas hard drive tambahan selama proses pengumpulan sampah ini tetapi setidaknya akan terpisah dari web ujung depan dan dapat dilakukan selama waktu non-puncak.

Ian Chamberland
sumber
Terima kasih. Itulah salah satu opsi kami di papan tulis. Tabel adalah sistem file, dan kami saat ini sedang dalam proses memisahkan data biner ke database yang sepenuhnya terpisah dari meta hirarki. Kami dapat melakukan apa yang Anda katakan dan menghapus baris hierarki, dan meminta proses GC membersihkan baris LOB yatim. Atau minta timestamp hapus dengan data untuk mencapai tujuan yang sama. Ini adalah rute yang dapat kita ambil jika tidak ada jawaban yang memuaskan untuk masalah tersebut.
Jeremy Rosenberg
1
Saya akan berhati-hati karena hanya memiliki cap waktu untuk menunjukkan itu dihapus. Itu akan bekerja tetapi kemudian Anda akhirnya akan memiliki banyak ruang yang digunakan ditempati dalam baris aktif. Anda perlu memiliki beberapa jenis proses gc di beberapa titik, tergantung pada berapa banyak yang dihapus, dan itu akan berdampak kurang untuk menghapus lebih sedikit secara teratur daripada banyak pada basis sesekali.
Ian Chamberland