Birçok sorgular ile SQL sorgusu küçük olanlara bölmek yardımcı olur?


18

Her gece SQL Server 2008 R2 üzerinde bazı raporlar yapmamız gerekiyor. Raporların hesaplanması birkaç saat sürer. Süreyi kısaltmak için bir tabloyu önceden hesaplıyoruz. Bu tablo, oldukça büyük 12 (on milyonlarca satır) tabloyu BİRLEŞTİRMEK temel alınarak oluşturulmuştur.

Bu toplama tablosunun hesaplanması birkaç gün öncesine kadar 4 saat civarındaydı. DBA'mız bu büyük birleşmeyi 3 küçük birleşime (her biri 4 tabloya katılıyor) ayırdı. Geçici sonuç, her seferinde bir sonraki birleştirmede kullanılan geçici bir tabloya kaydedilir.

DBA geliştirmesinin sonucu, toplama tablosunun 15 dakika içinde hesaplanmasıdır. Bunun nasıl mümkün olduğunu merak ettim. DBA, bunun sunucunun işlemesi gereken veri sayısının daha az olması nedeniyle olduğunu söyledi. Başka bir deyişle, büyük orijinal birleştirmede sunucunun toplanan daha küçük birleştirmelerden daha fazla veriyle çalışması gerekir. Bununla birlikte, optimize edicinin orijinal büyük birleştirme ile verimli bir şekilde yapmayı, birleştirmeleri kendi başına böldüğünü ve yalnızca bir sonraki birleşmelere gereken sayıda sütun göndereceğini düşünürdüm.

Yaptığı bir diğer şey de geçici tablolardan birinde bir indeks oluşturması. Ancak, bir kez daha optimize edicinin gerekirse uygun karma tabloları oluşturacağını ve tamamen daha iyi hesaplamayı optimize edeceğini düşünürdüm.

Bunu DBA'mızla konuştum, ancak işlem süresindeki iyileşmeyi neyin engellediğinden kendisi belirsizdi. Az önce, bu kadar büyük verileri hesaplamak çok zor olabileceğinden sunucuyu suçlamayacağını ve optimize edicinin en iyi yürütme planını tahmin etmekte zorlanabileceğini söyledi. Bunu anlıyorum, ama tam olarak nedenine ilişkin daha tanımlayıcı bir cevap almak istiyorum.

Yani, sorular:

  1. Büyük iyileşmeye ne sebep olabilir?

  2. Büyük birleştirmeleri daha küçüklere ayırmak standart bir prosedür mü?

  3. Birden çok küçük birleştirme durumunda sunucunun işlemesi gereken veri miktarı gerçekten daha mı düşük?

İşte orijinal sorgu:

    Insert Into FinalResult_Base
SELECT       
    TC.TestCampaignContainerId,
    TC.CategoryId As TestCampaignCategoryId,
    TC.Grade,
    TC.TestCampaignId,    
    T.TestSetId
    ,TL.TestId
    ,TSK.CategoryId
    ,TT.[TestletId]
    ,TL.SectionNo
    ,TL.Difficulty
    ,TestletName = Char(65+TL.SectionNo) + CONVERT(varchar(4),6 - TL.Difficulty) 
    ,TQ.[QuestionId]
    ,TS.StudentId
    ,TS.ClassId
    ,RA.SubjectId
    ,TQ.[QuestionPoints] 
    ,GoodAnswer  = Case When TQ.[QuestionPoints] Is null Then 0
                      When TQ.[QuestionPoints] > 0 Then 1 
                      Else 0 End
    ,WrongAnswer = Case When TQ.[QuestionPoints] = 0 Then 1 
                      When TQ.[QuestionPoints] Is null Then 1
                     Else 0 End
    ,NoAnswer    = Case When TQ.[QuestionPoints] Is null Then 1 Else 0 End
    ,TS.Redizo
    ,TT.ViewCount
    ,TT.SpentTime
    ,TQ.[Position]  
    ,RA.SpecialNeeds        
    ,[Version] = 1 
    ,TestAdaptationId = TA.Id
    ,TaskId = TSK.TaskId
    ,TaskPosition = TT.Position
    ,QuestionRate = Q.Rate
    ,TestQuestionId = TQ.Guid
    ,AnswerType = TT.TestletAnswerTypeId
FROM 
    [TestQuestion] TQ WITH (NOLOCK)
    Join [TestTask] TT WITH (NOLOCK)            On TT.Guid = TQ.TestTaskId
    Join [Question] Q WITH (NOLOCK)         On TQ.QuestionId =  Q.QuestionId
    Join [Testlet] TL WITH (NOLOCK)         On TT.TestletId  = TL.Guid 
    Join [Test]     T WITH (NOLOCK)         On TL.TestId     =  T.Guid
    Join [TestSet] TS WITH (NOLOCK)         On T.TestSetId   = TS.Guid 
    Join [RoleAssignment] RA WITH (NOLOCK)  On TS.StudentId  = RA.PersonId And RA.RoleId = 1
    Join [Task] TSK WITH (NOLOCK)       On TSK.TaskId = TT.TaskId
    Join [Category] C WITH (NOLOCK)     On C.CategoryId = TSK.CategoryId
    Join [TimeWindow] TW WITH (NOLOCK)      On TW.Id = TS.TimeWindowId 
    Join [TestAdaptation] TA WITH (NOLOCK)  On TA.Id = TW.TestAdaptationId
    Join [TestCampaign] TC WITH (NOLOCK)        On TC.TestCampaignId = TA.TestCampaignId 
WHERE
    T.TestTypeId = 1    -- eliminuji ankety 
    And t.ProcessedOn is not null -- ne vsechny, jen dokoncene
    And TL.ShownOn is not null
    And TS.Redizo not in (999999999, 111111119)
END;

Yeni bölünmüş DBA harika çalışmalarından sonra katıldı:

    SELECT       
    TC.TestCampaignContainerId,
    TC.CategoryId As TestCampaignCategoryId,
    TC.Grade,
    TC.TestCampaignId,    
    T.TestSetId
    ,TL.TestId
    ,TL.SectionNo
    ,TL.Difficulty
    ,TestletName = Char(65+TL.SectionNo) + CONVERT(varchar(4),6 - TL.Difficulty) -- prevod na A5, B4, B5 ...
    ,TS.StudentId
    ,TS.ClassId
    ,TS.Redizo
    ,[Version] = 1 -- ? 
    ,TestAdaptationId = TA.Id
    ,TL.Guid AS TLGuid
    ,TS.TimeWindowId
INTO
    [#FinalResult_Base_1]
FROM 
    [TestSet] [TS] WITH (NOLOCK)
    JOIN [Test] [T] WITH (NOLOCK) 
        ON [T].[TestSetId] = [TS].[Guid] AND [TS].[Redizo] NOT IN (999999999, 111111119) AND [T].[TestTypeId] = 1 AND [T].[ProcessedOn] IS NOT NULL
    JOIN [Testlet] [TL] WITH (NOLOCK)
        ON [TL].[TestId] = [T].[Guid] AND [TL].[ShownOn] IS NOT NULL
    JOIN [TimeWindow] [TW] WITH (NOLOCK)
        ON [TW].[Id] = [TS].[TimeWindowId] AND [TW].[IsActive] = 1
    JOIN [TestAdaptation] [TA] WITH (NOLOCK)
        ON [TA].[Id] = [TW].[TestAdaptationId] AND [TA].[IsActive] = 1
    JOIN [TestCampaign] [TC] WITH (NOLOCK)
        ON [TC].[TestCampaignId] = [TA].[TestCampaignId] AND [TC].[IsActive] = 1
    JOIN [TestCampaignContainer] [TCC] WITH (NOLOCK)
        ON [TCC].[TestCampaignContainerId] = [TC].[TestCampaignContainerId] AND [TCC].[IsActive] = 1
    ;

 SELECT       
    FR1.TestCampaignContainerId,
    FR1.TestCampaignCategoryId,
    FR1.Grade,
    FR1.TestCampaignId,    
    FR1.TestSetId
    ,FR1.TestId
    ,TSK.CategoryId AS [TaskCategoryId]
    ,TT.[TestletId]
    ,FR1.SectionNo
    ,FR1.Difficulty
    ,TestletName = Char(65+FR1.SectionNo) + CONVERT(varchar(4),6 - FR1.Difficulty) -- prevod na A5, B4, B5 ...
    ,FR1.StudentId
    ,FR1.ClassId
    ,FR1.Redizo
    ,TT.ViewCount
    ,TT.SpentTime
    ,[Version] = 1 -- ? 
    ,FR1.TestAdaptationId
    ,TaskId = TSK.TaskId
    ,TaskPosition = TT.Position
    ,AnswerType = TT.TestletAnswerTypeId
    ,TT.Guid AS TTGuid

INTO
    [#FinalResult_Base_2]
FROM 
    #FinalResult_Base_1 FR1
    JOIN [TestTask] [TT] WITH (NOLOCK)
        ON [TT].[TestletId] = [FR1].[TLGuid] 
    JOIN [Task] [TSK] WITH (NOLOCK)
        ON [TSK].[TaskId] = [TT].[TaskId] AND [TSK].[IsActive] = 1
    JOIN [Category] [C] WITH (NOLOCK)
        ON [C].[CategoryId] = [TSK].[CategoryId]AND [C].[IsActive] = 1
    ;    

DROP TABLE [#FinalResult_Base_1]

CREATE NONCLUSTERED INDEX [#IX_FR_Student_Class]
ON [dbo].[#FinalResult_Base_2] ([StudentId],[ClassId])
INCLUDE ([TTGuid])

SELECT       
    FR2.TestCampaignContainerId,
    FR2.TestCampaignCategoryId,
    FR2.Grade,
    FR2.TestCampaignId,    
    FR2.TestSetId
    ,FR2.TestId
    ,FR2.[TaskCategoryId]
    ,FR2.[TestletId]
    ,FR2.SectionNo
    ,FR2.Difficulty
    ,FR2.TestletName
    ,TQ.[QuestionId]
    ,FR2.StudentId
    ,FR2.ClassId
    ,RA.SubjectId
    ,TQ.[QuestionPoints] -- 1+ good, 0 wrong, null no answer
    ,GoodAnswer  = Case When TQ.[QuestionPoints] Is null Then 0
                      When TQ.[QuestionPoints] > 0 Then 1 -- cookie
                      Else 0 End
    ,WrongAnswer = Case When TQ.[QuestionPoints] = 0 Then 1 
                      When TQ.[QuestionPoints] Is null Then 1
                     Else 0 End
    ,NoAnswer    = Case When TQ.[QuestionPoints] Is null Then 1 Else 0 End
    ,FR2.Redizo
    ,FR2.ViewCount
    ,FR2.SpentTime
    ,TQ.[Position] AS [QuestionPosition]  
    ,RA.SpecialNeeds -- identifikace SVP        
    ,[Version] = 1 -- ? 
    ,FR2.TestAdaptationId
    ,FR2.TaskId
    ,FR2.TaskPosition
    ,QuestionRate = Q.Rate
    ,TestQuestionId = TQ.Guid
    ,FR2.AnswerType
INTO
    [#FinalResult_Base]
FROM 
    [#FinalResult_Base_2] FR2
    JOIN [TestQuestion] [TQ] WITH (NOLOCK)
        ON [TQ].[TestTaskId] = [FR2].[TTGuid]
    JOIN [Question] [Q] WITH (NOLOCK)
        ON [Q].[QuestionId] = [TQ].[QuestionId] AND [Q].[IsActive] = 1

    JOIN [RoleAssignment] [RA] WITH (NOLOCK)
        ON [RA].[PersonId] = [FR2].[StudentId]
        AND [RA].[ClassId] = [FR2].[ClassId] AND [RA].[IsActive] = 1 AND [RA].[RoleId] = 1

    drop table #FinalResult_Base_2;

    truncate table [dbo].[FinalResult_Base];
    insert into [dbo].[FinalResult_Base] select * from #FinalResult_Base;

    drop table #FinalResult_Base;

3
Uyarı kelimesi - İLE (NOLOCK) kötüdür - kötü verilerin geri gelmesine neden olabilir. ILE (ROWCOMMITTED) denemenizi öneririz.
TomTom

1
Bunu mu demek istediniz READCOMMITTED? Daha önce ROWCOMMITTED'i hiç görmedim.
ypercubeᵀᴹ

4
İLE (NOLOCK) kötü değil. İnsanların düşündükleri sihirli mermi değil. SQL Server'daki çoğu şey ve genel olarak yazılım geliştirme gibi bir yeri vardır.
Zane

2
Evet, ancak NOLOCK'un günlükte uyarılar üretebileceği ve - daha da önemlisi - YANLIŞ VERİ'ye dönebileceği göz önüne alındığında, kötü olduğunu düşünüyorum. Sorgu çalışırken, birincil anahtarda ve seçilen anahtarlarda değişiklik yapılmaması GARANTİLİ tablolarda hemen hemen kullanılabilir. Ve evet, OKUYORUM ve özür dilerim.
TomTom

Yanıtlar:


11

1 Ara / geç birleşimler için daha iyi istatistiklerle birlikte, 'arama alanının' azaltılması.

Sorgu İşlemci'nin bir plan oluşturmayı bile reddettiği 90 tablo birleşimleriyle (mickey mouse tasarımı) uğraşmak zorunda kaldım. Böyle bir birleştirmeyi her biri 9 tablonun 10 alt birleşimine bölmek, her bir birleştirmenin karmaşıklığını önemli ölçüde azalttı, bu da her ek tabloyla katlanarak büyüyor. Ayrıca, Sorgu Optimize Edici şimdi onları 10 plan olarak ele alıyor ve (potansiyel olarak) toplamda daha fazla zaman harcıyor (Paul White'ın metrikleri bile olabilir!).

Ara sonuç tabloları artık kendi istatistiklerine sahip olacak, böylece erken eğilmiş ve daha sonra Bilim Kurgu olarak ortaya çıkan derin bir ağacın istatistiklerine kıyasla çok daha iyi bir araya geliyor.

Ayrıca önce en seçici birleştirmeleri zorlayarak ağacı yukarı doğru hareket ettiren veri birimlerini azaltabilirsiniz. Tahminlerinizin seçiciliğini Optimize Edici'den çok daha iyi tahmin edebiliyorsanız neden katılma sırasını zorlamıyorsunuz? "Bushy Planları" aramaya değer olabilir.

2 O olmalıdır verimlilik ve performans önemliyse, bana göre düşünülmelidir

3 Mutlaka değil, ancak en seçici birleştirmeler erken yapılırsa olabilir


3
+1 Teşekkürler. Özellikle deneyiminizin açıklaması için. Bunu "Tahminlerinizin seçiciliğini Optimize Edici'den çok daha iyi tahmin edebiliyorsanız neden katılma düzenini zorlamıyorsunuz?"
Ondrej Peterka

2
Aslında çok geçerli bir soru. 90 masalı birleştirme, sadece 'Zorunlu Sipariş' seçeneğini kullanarak bir plan oluşturmak için zorlanabilir. Siparişin muhtemelen rastgele ve en alt düzeyde olması önemli değildi, sadece arama alanını azaltmak Optimize Edici'nin birkaç saniye içinde bir plan oluşturmasına yardımcı olmak için yeterliydi (20 saniye sonra zaman aşımına uğrayacak ipucu olmadan).
John Alan

6
  1. SQLServer optimizer genellikle iyi bir iş çıkarır. Ancak hedefi mümkün olan en iyi planı oluşturmak değil, yeterince hızlı olan planı bulmaktır. Birçok birleşim içeren belirli bir sorgu için çok düşük performansa neden olabilir. Bu tür bir durumun iyi bir göstergesi, gerçek yürütme planındaki tahmini ve gerçek satır sayısı arasındaki büyük farktır. Ayrıca, ilk sorgu için yürütme planı 'birleştirmeyi birleştir' den daha yavaş birçok 'iç içe döngüler katılmak' gösterecektir eminim. İkincisi, her iki girişin de aynı anahtar kullanılarak sıralanmasını gerektirir, bu da pahalıdır ve genellikle optimizer böyle bir seçeneği atar. Sonuçları geçici tabloda saklamak ve daha sonraki birleştirmeler için daha iyi algoritma seçiminde sonuçları tahmin ettiğim gibi uygun dizinler eklemek (yan not - önce geçici tabloyu doldurarak en iyi uygulamaları takip edersiniz, ve ardından dizin ekleme). Buna ek olarak, SQLServer geçici tablolar için istatistikler üretir ve saklar, bu da uygun dizinin seçilmesine yardımcı olur.
  2. Birleştirme sayısı sabit sayıdan daha büyük olduğunda geçici tabloları kullanma konusunda bir standart olduğunu söyleyemem, ancak kesinlikle performansı artırabilecek bir seçenek. Bu sık sık olmaz, ancak benzer problemler (ve benzer bir çözüm) birkaç kez yaşadım. Alternatif olarak, en iyi uygulama planını kendiniz bulmayı deneyebilir, yeniden kullanmaya ve depolamaya zorlayabilirsiniz, ancak çok fazla zaman alacaktır (başarılı olacağınız% 100 garanti edilmez). Başka bir yan not - geçici tabloda saklanan sonuç kümesinin nispeten küçük olması durumunda (yaklaşık 10k kayıt) tablo değişkeni geçici tablodan daha iyi performans gösterirse.
  3. 'Bu bağlıdır' demekten nefret ediyorum, ama muhtemelen üçüncü sorunuza cevabım. Optimizer sonuçları hızlı bir şekilde vermek zorundadır; en iyi planı bulmaya çalışmak için saatler harcamak istemezsiniz; her birleştirme fazladan iş ekler ve bazen optimize edici 'kafası karışır'.

3
+1 onay ve açıklama için teşekkürler. Yazdıklarınız mantıklı.
Ondrej Peterka

4

Küçük veriler üzerinde çalıştığınızı söyleyerek başlayayım - 10 milyon kişi büyük değil. Sahip olduğum son DWH projesinde olgu tablosuna 400 milyon sıra eklendi. GÜN BAŞINA. 5 yıl depolama.

Sorun kısmen donanımdır. Büyük birleşimler çok fazla geçici alan kullanabileceğinden ve çok fazla RAM olduğundan, disklere taştığınız an çok daha yavaşlar. Bu nedenle, çalışmayı daha küçük parçalara bölmek mantıklı olabilir, çünkü SQL bir setler dünyasında yaşarken ve boyutu önemsemese de, üzerinde çalıştığınız sunucu sonsuz değildir. Bazı işlemler sırasında 64GB tempdb'de alan hatalarından kurtulmak için oldukça alışkınım.

Aksi takdirde, statikler sırayla olduğu sürece, sorgu optimize edici bunalmaz. Tablonun ne kadar büyük olduğu gerçekten umursamıyor - gerçekten büyümeyen istatistiklerle çalışır. DEDİĞİNİZ: Gerçekten BÜYÜK bir tablonuz (çift haneli milyar satır sayısı) varsa, bunlar biraz kaba olabilir.

Ayrıca, büyük bir birleşimin masayı saatlerce kilitleyebileceğini programlamadığınız sürece bir kilitleme sorunu da vardır. Şu anda 200GB kopya işlemleri yapıyorum ve kilitleri çok daha kısa tutan bir iş anahtarı (etkin döngü) ile smllerparty içine bölüyorum.

Sonunda sınırlı donanımla çalışıyoruz.


1
Cevabınız için +1 teşekkürler. HW'ye bağlı olduğunu söylemenin iyi bir anlamı var. Muhtemelen yeterli olmayan sadece 32 GB RAM'imiz var.
Ondrej Peterka

2
Böyle cevapları her okuduğumda biraz sinirliyim - birkaç düzine milyon satır bile saatlerce veritabanı sunucumuzda CPU yükü yaratıyor. Belki boyut sayısı yüksektir, ancak 30 boyut çok büyük bir sayı değildir. Bence işleyebileceğiniz çok fazla sayıda satır basit bir modelden geliyor. Daha da kötüsü: Tüm veriler RAM'e sığar. Ve hala saatler sürüyor.
flaschenpost

1
30 boyut LOT - modelin bir yıldıza uygun şekilde optimize edildiğinden emin misiniz? OP sorgusunda maliyet CPU'su gibi bazı hatalar GUID'leri birincil anahtarlar (uniqueidentifier) ​​olarak kullanıyor. Onları da seviyorum - benzersiz dizin olarak, birincil anahtar bir kimlik alanıdır, tüm karşılaştırmayı daha hızlı hale getirir ve endeksi daha nawwox yapar (4 veya 8 bayt, 18 değil). Bunun gibi püf noktaları bir TON CPU kazandırır.
TomTom
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.