SQL Server'da LOB verileri için performansı silme


16

Bu soru bu forum konusu ile ilgilidir .

SQL Server 2008 Developer Edition'ı iş istasyonumda ve "alfa kümesi" olarak adlandırdığım iki düğümlü bir Enterprise Edition sanal makine kümesinde çalıştırıyorum.

Bir varbinary (max) sütunu olan satırları silmek için geçen süre, doğrudan o sütundaki verilerin uzunluğuyla ilişkilidir. İlk başta sezgisel gelebilir, ancak araştırmadan sonra SQL Server'ın genel olarak satırları nasıl sildiğini ve bu tür verilerle nasıl başa çıktığını anladığımla çelişir.

Sorun, .NET web uygulamasında gördüğümüz silme zaman aşımı (> 30 saniye) sorunundan kaynaklanıyor, ancak bu tartışma uğruna basitleştirdim.

Bir kayıt silindiğinde, SQL Server, işlem tamamlandıktan sonra bir Hayalet Temizleme Görevi tarafından temizlenecek bir hayalet olarak işaretler ( Paul Randal'ın bloguna bakın ). Sırasıyla bir varbinary (maks) sütununda 16 KB, 4 MB ve 50 MB veri içeren üç satırı silme testinde, bunun verilerinde satır içi kısmı ve işlemde olduğunu görüyorum log.

Bana garip gelen şey, X kilitlerinin silme işlemi sırasında tüm LOB veri sayfalarına yerleştirilmesi ve sayfaların PFS'ye yerleştirilmesidir. Bunu işlem günlüğünde, DMV ( ) ile sp_lockve sonuçlarında görüyorum . dm_db_index_operational_statspage_lock_count

Bu, bu sayfalar arabellek önbelleğinde değilse, iş istasyonumda ve alfa kümemizde bir G / Ç darboğazı oluşturur. Aslında, page_io_latch_wait_in_msaynı DMV'den neredeyse tüm silme süresi ve page_io_latch_wait_countkilitli sayfaların sayısına karşılık gelir. İş istasyonumdaki 50 MB dosya için bu, boş bir arabellek önbelleği ( checkpoint/ dbcc dropcleanbuffers) ile başlarken 3 saniyeden fazla bir süreye dönüşür ve şüphesiz ki ağır parçalanma ve yük altında daha uzun olur.

Sadece o zaman alan önbellekte yer ayırmak değildi emin olmak için çalıştı. checkpointSQL Server işlemine ayrılan yöntem yerine silme yürütülmeden önce diğer satırlardan 2 GB veri okumak . SQL Server'ın verileri nasıl karıştırdığını bilmediğimden, geçerli bir test olup olmadığından emin değilim. Her zaman eskiyi yeninin lehine çıkaracağını varsaydım.

Ayrıca, sayfaları bile değiştirmez. Bunu görebiliyorum dm_os_buffer_descriptors. Silme işleminden sonra sayfalar temizken, değiştirilen sayfa sayısı üç küçük, orta ve büyük silme işleminin tümü için 20'den azdır. Ayrıca DBCC PAGE, aranan sayfaların örneklemesinin çıktısını da karşılaştırdım ve değişiklik olmadı (sadece ALLOCATEDbit PFS'den kaldırıldı). Sadece onları yeniden konumlandırır.

Sayfa arama / deallocations soruna neden olduğunu kanıtlamak için, vanilya varbinary (max) yerine bir filestream sütun kullanarak aynı testi denedim. Silmeler LOB boyutundan bağımsız olarak sabit bir süreydi.

İlk olarak akademik sorularım:

  1. SQL Server'ın X'i kilitlemek için neden tüm LOB veri sayfalarını araması gerekiyor? Bu, kilitlerin bellekte nasıl temsil edildiğinin sadece bir detayı mı (sayfa ile bir şekilde saklanıyor)? Bu, G / Ç etkisini tamamen önbelleğe alınmazsa veri boyutuna büyük ölçüde bağımlı hale getirir.
  2. Neden X sadece onları yeniden konumlandırmak için kilitleniyor? Yeniden yerleştirme sayfaların kendilerinin değiştirilmesine gerek olmadığından, yalnızca dizin yapısını satır içi bölümle kilitlemek yeterli değil mi? Kilidin korunduğu LOB verilerine ulaşmanın başka bir yolu var mı?
  3. Zaten bu tür bir çalışmaya adanmış bir arka plan görevi olduğu için, sayfaları neden önceden dağıtmalıyız?

Ve belki daha da önemlisi, pratik sorum:

  • Silme işlemlerinin farklı çalışmasını sağlamanın bir yolu var mı? Amacım, gerçekliğin ardından arka planda herhangi bir temizlemenin gerçekleştiği, filestream'e benzer boyuttan bağımsız olarak sabit sürelerin silinmesidir. Bu bir yapılandırma işi mi? Bir şeyleri tuhaf mı saklıyorum?

Açıklanan sınamanın nasıl yeniden oluşturulacağı (SSMS sorgu penceresinden yürütülür):

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

İşte benim iş istasyonumdaki silme işlemlerinin bazı sonuçları:

| Sütun Türü | Boyutu Sil | Süresi (ms) | Okur | Yazarlar | CPU |
-------------------------------------------------- ------------------
| VarBinary | 16 KB | 40 | 13 | 2 | 0 |
| VarBinary | 4 MB | 952 | 2318 | 2 | 0 |
| VarBinary | 50 MB | 2976 | 28594 | 1 | 62 |
-------------------------------------------------- ------------------
| Dosya Akışı | 16 KB | 1 | 12 | 1 | 0 |
| Dosya Akışı | 4 MB | 0 | 9 | 0 | 0 |
| Dosya Akışı | 50 MB | 1 | 9 | 0 | 0 |

Bunun yerine sadece filestream'i kullanamayız çünkü:

  1. Veri boyutu dağıtımımız bunu garanti etmez.
  2. Pratikte, birçok parçaya veri ekliyoruz ve filestream kısmi güncellemeleri desteklemiyor. Bunun etrafında tasarım yapmamız gerekecekti.

Güncelleme 1

Verilerin silme işleminin bir parçası olarak işlem günlüğüne yazıldığına dair bir teori test edildi ve durum böyle görünmüyor. Bunu yanlış mı test ediyorum? Aşağıya bakınız.

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'

5 MB'tan büyük bir dosya için bu geri döndü 1651 | 171860.

Ayrıca, veri günlüğe yazıldıysa sayfaların kendilerinin kirli olmasını beklerim. Silme işleminden sonra kirli olanla eşleşen yalnızca serbest bırakmalar günlüğe kaydedilir.

Güncelleme 2

Paul Randal'dan bir cevap aldım. Ağacın üzerinden geçmek ve hangi sayfaların dağıtılacağını bulmak için tüm sayfaları okuması gerektiğini doğruladı ve hangi sayfalara bakmanın başka bir yolu olmadığını belirtti. Bu, 1 ve 2'ye yarım yanıttır (ancak sıra dışı verilerin kilitlenmesi ihtiyacını açıklamaz, ancak bu küçük patateslerdir).

Soru 3 hala açık: Silme işlemleri için temizleme işlemi yapmak üzere zaten bir arka plan görevi varsa, neden sayfaları neden yeniden dağıtmalısınız?

Ve elbette, tüm önemli soru: Bu boyuta bağlı silme davranışını doğrudan azaltmanın (yani, geçici olarak çalışmanın) bir yolu var mı? SQL Server'da 50 MB satırlarını depolayan ve silen tek kişi olmadıkça, bunun daha yaygın bir sorun olacağını düşünürdüm? Dışarıdaki herkes bunun etrafında bir çeşit çöp toplama işi ile çalışıyor mu?


Keşke daha iyi bir çözüm olsaydı ama bulamadım. 1MB + 'ya kadar değişen boyut satırları büyük hacimli günlük kaydı için bir durum var ve eski kayıtları silmek için bir "temizleme" işlemi var. Silme işlemleri çok yavaş olduğundan, iki adıma ayırmak zorunda kaldım - önce tablolar arasındaki referansları kaldırın (çok hızlı), sonra artık satırları silin. Verileri silmek için silme işinin ortalaması ~ 2.2 saniye / MB'dir. Tabii ki çekişmeyi azaltmak zorunda kaldım, bu yüzden artık hiçbir satır silinene kadar bir döngü içinde "TOP SİL (250)" ile saklı bir prosedür var.
Abaküs

Yanıtlar:


5

Neden dosya akışına kıyasla bir VARBINARY (MAX) silmek çok daha verimsiz olduğunu söyleyemem ama bir fikir sadece bu LOBS silerken web uygulamanızdan zaman aşımlarından kaçınmaya çalışıyorsanız düşünebilirsiniz. VARBINARY (MAX) değerlerini, özgün tablo tarafından başvurulan ayrı bir tabloda (diyelim tblLOB olarak adlandırılır) depolayabilirsiniz (bu tblParent diyelim).

Buradan bir kaydı sildiğinizde, üst kayıttan silebilir ve daha sonra LOB tablosundaki kayıtları temizlemek ve temizlemek için ara sıra çöp toplama işlemine sahip olabilirsiniz. Bu çöp toplama işlemi sırasında ilave sabit sürücü etkinliği olabilir, ancak en azından ön uç ağdan ayrı olacaktır ve yoğun olmayan zamanlarda gerçekleştirilebilir.


Teşekkürler. Bu, tahtadaki seçeneklerimizden biri. Tablo bir dosya sistemidir ve şu anda ikili verileri hiyerarşi metadan tamamen ayrı bir veritabanına ayırma sürecindeyiz. Söylediğiniz gibi yapabilir ve hiyerarşi satırını silebiliriz ve bir GC işleminin artık LOB satırlarını temizlemesini sağlayabiliriz. Ya da aynı hedefi gerçekleştirmek için verilerle birlikte bir zaman damgası ekleyin. Soruna tatmin edici bir cevap yoksa, izleyebileceğimiz rota budur.
Jeremy Rosenberg

1
Silindiğini göstermek için sadece bir zaman damgasına sahip olduğum konusunda dikkatli olurdum. Bu işe yarayacaktır, ancak sonunda aktif satırlarda çok fazla kullanılan alana sahip olacaksınız. Ne kadar silindiğine bağlı olarak bir noktada bir tür gc sürecine sahip olmanız gerekecek ve ara sıra yerine lotlardan ziyade düzenli olarak daha az silmek daha az etkili olacaktır.
Ian Chamberland
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.