Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Serialisasi Penghapusan Dari Indeks Columnstore Clustered

Di Stack Overflow, kami memiliki beberapa tabel yang menggunakan indeks penyimpanan kolom berkerumun, dan ini berfungsi dengan baik untuk sebagian besar beban kerja kami. Tetapi kami baru-baru ini menemukan situasi di mana "badai sempurna" — beberapa proses yang semuanya mencoba untuk menghapus dari CCI yang sama — akan membanjiri CPU karena semuanya berjalan paralel dan berjuang untuk menyelesaikan operasinya. Berikut tampilannya di SolarWinds SQL Sentry:

Dan inilah penantian menarik yang terkait dengan pertanyaan ini:

Kueri yang bersaing semuanya berbentuk ini:

DELETE dbo.LargeColumnstoreTable WHERE col1 = @p1 AND col2 = @p2;

Rencananya terlihat seperti ini:

Dan peringatan pada pemindaian memberi tahu kami tentang beberapa I/O sisa yang cukup ekstrem:

Tabel memiliki 1,9 miliar baris tetapi hanya 32GB (terima kasih, penyimpanan kolom!). Namun, penghapusan satu baris ini akan memakan waktu masing-masing 10 – 15 detik, dengan sebagian besar waktu ini dihabiskan untuk SOS_SCHEDULER_YIELD .

Untungnya, karena dalam skenario ini operasi penghapusan bisa jadi tidak sinkron, kami dapat menyelesaikan masalah dengan dua perubahan (meskipun saya terlalu menyederhanakannya di sini):

  • Kami membatasi MAXDOP di tingkat basis data sehingga penghapusan ini tidak dapat dilakukan secara paralel
  • Kami meningkatkan serialisasi proses yang berasal dari aplikasi (pada dasarnya, kami mengantrekan penghapusan melalui satu operator)

Sebagai DBA, kita dapat dengan mudah mengontrol MAXDOP , kecuali jika diganti di tingkat kueri (lubang kelinci lain untuk hari lain). Kami tidak serta merta dapat mengontrol aplikasi sejauh ini, terutama jika itu didistribusikan atau bukan milik kami. Bagaimana kita bisa membuat serial penulisan dalam kasus ini tanpa mengubah logika aplikasi secara drastis?

Pengaturan Mock

Saya tidak akan mencoba membuat tabel dua miliar baris secara lokal — apalagi tabel persisnya — tetapi kita dapat memperkirakan sesuatu dalam skala yang lebih kecil dan mencoba mereproduksi masalah yang sama.

Anggap saja ini SuggestedEdits tabel (pada kenyataannya, tidak). Tetapi ini adalah contoh yang mudah digunakan karena kami dapat menarik skema dari Stack Exchange Data Explorer. Dengan menggunakan ini sebagai dasar, kita dapat membuat tabel yang setara (dengan beberapa perubahan kecil untuk membuatnya lebih mudah untuk diisi) dan melemparkan indeks penyimpanan kolom berkerumun di atasnya:

CREATE TABLE dbo.FakeSuggestedEdits
(
  Id            int IDENTITY(1,1),
  PostId        int NOT NULL DEFAULT CONVERT(int, ABS(CHECKSUM(NEWID()))) % 200,
  CreationDate  datetime2 NOT NULL DEFAULT sysdatetime(),
  ApprovalDate  datetime2 NOT NULL DEFAULT sysdatetime(),
  RejectionDate datetime2 NULL,
  OwnerUserId   int NOT NULL DEFAULT 7,
  Comment       nvarchar (800)   NOT NULL DEFAULT NEWID(),
  Text          nvarchar (max)   NOT NULL DEFAULT NEWID(),
  Title         nvarchar (250)   NOT NULL DEFAULT NEWID(),
  Tags          nvarchar (250)   NOT NULL DEFAULT NEWID(),
  RevisionGUID  uniqueidentifier NOT NULL DEFAULT NEWSEQUENTIALID(),
  INDEX CCI_FSE CLUSTERED COLUMNSTORE
);

Untuk mengisinya dengan 100 juta baris, kita dapat melakukan cross join sys.all_objects dan sys.all_columns lima kali (di sistem saya, ini akan menghasilkan 2,68 juta baris setiap kali, tetapi YMMV):

-- 2680350 * 5 ~ 3 minutes
 
INSERT dbo.FakeSuggestedEdits(CreationDate)
  SELECT TOP (10) /*(2000000) */ modify_date
  FROM sys.all_objects AS o
  CROSS JOIN sys.columns AS c;
GO 5

Kemudian, kita dapat memeriksa spasi:

EXEC sys.sp_spaceused @objname = N'dbo.FakeSuggestedEdits';

Ini hanya 1,3 GB, tapi ini sudah cukup:

Meniru Penghapusan Clustered Columnstore Kami

Berikut ini kueri sederhana yang kira-kira cocok dengan apa yang dilakukan aplikasi kami dengan tabel:

DECLARE @p1 int = ABS(CHECKSUM(NEWID())) % 10000000, @p2 int = 7;
DELETE dbo.FakeSuggestedEdits WHERE Id = @p1 AND OwnerUserId = @p2;

Namun, rencananya tidak terlalu cocok:

Untuk membuatnya paralel dan menghasilkan pertentangan serupa di laptop saya yang kecil, saya harus sedikit memaksa pengoptimal dengan petunjuk ini:

OPTION (QUERYTRACEON 8649);

Sekarang, sepertinya benar:

Mereproduksi Masalah

Kemudian, kita dapat membuat lonjakan aktivitas penghapusan bersamaan menggunakan SqlStressCmd untuk menghapus 1.000 baris acak menggunakan 16 dan 32 utas:

sqlstresscmd -s docs/ColumnStore.json -t 16
sqlstresscmd -s docs/ColumnStore.json -t 32

Kita dapat mengamati tekanan yang ditimbulkan pada CPU:

Ketegangan pada CPU berlangsung sepanjang batch masing-masing sekitar 64 dan 130 detik:

Catatan:Keluaran dari SQLQueryStress terkadang sedikit kurang baik pada iterasi, tetapi saya telah mengonfirmasi bahwa pekerjaan yang Anda minta diselesaikan dengan tepat.

Penanganan Potensial:Antrian Penghapusan

Awalnya, saya berpikir untuk memperkenalkan tabel antrian di database, yang bisa kita gunakan untuk menghapus aktivitas penghapusan:

CREATE TABLE dbo.SuggestedEditDeleteQueue
(
  QueueID       int IDENTITY(1,1) PRIMARY KEY,
  EnqueuedDate  datetime2 NOT NULL DEFAULT sysdatetime(),
  ProcessedDate datetime2 NULL,
  Id            int NOT NULL,
  OwnerUserId   int NOT NULL
);

Yang kita butuhkan hanyalah pemicu BUKAN untuk mencegat penghapusan jahat ini yang berasal dari aplikasi dan menempatkannya pada antrian untuk pemrosesan latar belakang. Sayangnya, Anda tidak dapat membuat pemicu pada tabel dengan indeks penyimpanan kolom tergugus:

Pesan 35358, Level 16, Status 1
BUAT PEMICU pada tabel 'dbo.FakeSuggestedEdits' gagal karena Anda tidak dapat membuat pemicu pada tabel dengan indeks penyimpanan kolom tergugus. Pertimbangkan untuk menerapkan logika pemicu dengan cara lain, atau jika Anda harus menggunakan pemicu, gunakan heap atau indeks B-tree sebagai gantinya.

Kita memerlukan sedikit perubahan pada kode aplikasi, sehingga memanggil prosedur tersimpan untuk menangani penghapusan:

CREATE PROCEDURE dbo.DeleteSuggestedEdit
  @Id          int,
  @OwnerUserId int
AS
BEGIN
  SET NOCOUNT ON;
 
  DELETE dbo.FakeSuggestedEdits 
    WHERE Id = @Id AND OwnerUserId = @OwnerUserId;
END

Ini bukan keadaan permanen; ini hanya untuk menjaga perilaku tetap sama saat hanya mengubah satu hal di aplikasi. Setelah aplikasi diubah dan berhasil memanggil prosedur tersimpan ini alih-alih mengirimkan kueri penghapusan ad hoc, prosedur tersimpan dapat berubah:

CREATE PROCEDURE dbo.DeleteSuggestedEdit
  @Id          int,
  @OwnerUserId int
AS
BEGIN
  SET NOCOUNT ON;
 
  INSERT dbo.SuggestedEditDeleteQueue(Id, OwnerUserId)
    SELECT @Id, @OwnerUserId;
END

Menguji Dampak Antrian

Sekarang, jika kita mengubah SqlQueryStress untuk memanggil prosedur tersimpan sebagai gantinya:

DECLARE @p1 int = ABS(CHECKSUM(NEWID())) % 10000000, @p2 int = 7;
EXEC dbo.DeleteSuggestedEdit @Id = @p1, @OwnerUserId = @p2;

Dan kirimkan kumpulan yang serupa (menempatkan 16 ribu atau 32 ribu baris pada antrean):

DECLARE @p1 int = ABS(CHECKSUM(NEWID())) % 10000000, @p2 int = 7;
EXEC dbo.@Id = @p1 AND OwnerUserId = @p2;

Dampak CPU sedikit lebih tinggi:

Namun beban kerja selesai jauh lebih cepat — masing-masing 16 dan 23 detik:

Ini adalah pengurangan yang signifikan dalam rasa sakit yang akan dirasakan aplikasi saat mereka memasuki periode konkurensi tinggi.

Kami Masih Harus Melakukan Penghapusan, Meskipun

Kami masih harus memproses penghapusan tersebut di latar belakang, tetapi kami sekarang dapat memperkenalkan batching dan memiliki kontrol penuh atas laju dan penundaan yang ingin kami masukkan di antara operasi. Berikut adalah struktur paling dasar dari prosedur tersimpan untuk memproses antrian (diakui tanpa kontrol transaksional sepenuhnya, penanganan kesalahan, atau pembersihan tabel antrian):

CREATE PROCEDURE dbo.ProcessSuggestedEditQueue
  @JobSize        int = 10000,
  @BatchSize      int = 100,
  @DelayInSeconds int = 2      -- must be between 1 and 59
AS
BEGIN
  SET NOCOUNT ON;
 
  DECLARE @d TABLE(Id int, OwnerUserId int);
  DECLARE @rc int = 1,
          @jc int = 0, 
          @wf nvarchar(100) = N'WAITFOR DELAY ' + CHAR(39) 
              + '00:00:' + RIGHT('0' + CONVERT(varchar(2), 
                @DelayInSeconds), 2) + CHAR(39);
 
  WHILE @rc > 0 AND @jc < @JobSize
  BEGIN 
    DELETE @d; 
 
    UPDATE TOP (@BatchSize) q SET ProcessedDate = sysdatetime() 
      OUTPUT inserted.Id, inserted.OwnerUserId INTO @d 
      FROM dbo.SuggestedEditDeleteQueue AS q WITH (UPDLOCK, READPAST) 
       WHERE ProcessedDate IS NULL; 
 
    SET @rc = @@ROWCOUNT; 
    IF @rc = 0 BREAK; 
 
    DELETE fse 
      FROM dbo.FakeSuggestedEdits AS fse 
      INNER JOIN @d AS d 
        ON fse.Id = d.Id 
       AND fse.OwnerUserId = d.OwnerUserId; 
 
    SET @jc += @rc; 
    IF @jc > @JobSize BREAK;
 
    EXEC sys.sp_executesql @wf;
  END
  RAISERROR('Deleted %d rows.', 0, 1, @jc) WITH NOWAIT;
END

Sekarang, menghapus baris akan memakan waktu lebih lama — rata-rata untuk 10.000 baris adalah 223 detik, ~100 di antaranya adalah penundaan yang disengaja. Tapi tidak ada pengguna yang menunggu, jadi siapa yang peduli? Profil CPU hampir nol, dan aplikasi dapat terus menambahkan item pada antrean secara bersamaan seperti yang diinginkan, dengan hampir nol konflik dengan pekerjaan latar belakang. Saat memproses 10.000 baris, saya menambahkan 16 ribu baris lagi ke antrean, dan menggunakan CPU yang sama seperti sebelumnya — hanya membutuhkan waktu satu detik lebih lama daripada saat pekerjaan tidak berjalan:

Dan rencananya sekarang terlihat seperti ini, dengan perkiraan / baris aktual yang jauh lebih baik:

Saya dapat melihat pendekatan tabel antrian ini sebagai cara yang efektif untuk menangani konkurensi DML yang tinggi, tetapi ini membutuhkan setidaknya sedikit fleksibilitas dengan aplikasi yang mengirimkan DML — ini adalah salah satu alasan saya sangat suka aplikasi memanggil prosedur tersimpan, karena mereka memberi kami lebih banyak kontrol lebih dekat ke data.

Opsi Lain

Jika Anda tidak memiliki kemampuan untuk mengubah kueri penghapusan yang berasal dari aplikasi — atau, jika Anda tidak dapat menunda penghapusan ke proses latar belakang — Anda dapat mempertimbangkan opsi lain untuk mengurangi dampak penghapusan:

  • Indeks nonclustered pada kolom predikat untuk mendukung pencarian titik (kita dapat melakukan ini secara terpisah tanpa mengubah aplikasi)
  • Hanya menggunakan penghapusan lunak (masih memerlukan perubahan pada aplikasi)

Akan menarik untuk melihat apakah opsi ini menawarkan manfaat yang serupa, tetapi saya akan menyimpannya untuk postingan mendatang.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Cara Menghitung Perbedaan Antara Dua Datetime di T-SQL

  2. Cara Menggunakan DISTINCT dalam SQL

  3. Dasar-dasar sys.dm_exec_requests

  4. Tantangannya aktif! Panggilan komunitas untuk membuat generator seri nomor tercepat

  5. Bendera Jejak Baru untuk Memperbaiki Kinerja Variabel Tabel