Memisahkan query SQL dengan banyak bergabung menjadi lebih kecil membantu?

18

Kita perlu melakukan beberapa pelaporan setiap malam di SQL Server 2008 R2 kami. Menghitung laporan membutuhkan beberapa jam. Untuk mempersingkat waktu kita menghitung ulang tabel. Tabel ini dibuat berdasarkan tabel JOINining 12 cukup besar (puluhan juta baris).

Perhitungan tabel agregasi ini memakan waktu hingga beberapa hari yang lalu karena 4 jam. DBA kami kemudian membagi gabungan besar ini menjadi 3 gabungan lebih kecil (masing-masing bergabung dengan 4 tabel). Hasil sementara disimpan ke dalam tabel sementara setiap waktu, yang digunakan di gabung berikutnya.

Hasil peningkatan DBA adalah, bahwa tabel agregasi dihitung dalam 15 menit. Saya bertanya-tanya bagaimana itu mungkin. DBA mengatakan kepada saya bahwa itu karena jumlah data yang harus diproses server lebih kecil. Dengan kata lain, bahwa dalam gabung asli yang besar, server harus bekerja dengan lebih banyak data daripada jumlah yang lebih kecil. Namun, saya menganggap pengoptimal akan melakukannya secara efisien dengan gabung besar asli, memisahkan gabung sendiri dan hanya mengirim jumlah kolom yang diperlukan untuk gabung berikutnya.

Hal lain yang dia lakukan adalah dia membuat indeks di salah satu tabel sementara. Namun, sekali lagi saya akan berpikir bahwa pengoptimal akan membuat tabel hash yang sesuai jika diperlukan dan semuanya lebih baik mengoptimalkan perhitungan.

Saya membicarakan hal ini dengan DBA kami, tetapi dia sendiri tidak yakin tentang apa yang menyebabkan peningkatan dalam waktu pemrosesan. Dia hanya menyebutkan, bahwa dia tidak akan menyalahkan server karena dapat berlebihan untuk menghitung data sebesar itu dan bahwa mungkin pengoptimal memiliki waktu yang sulit untuk memprediksi rencana pelaksanaan terbaik .... Ini saya mengerti, tetapi saya ingin memiliki jawaban yang lebih jelas tentang mengapa persisnya.

Jadi, pertanyaannya adalah:

  1. Apa yang mungkin menyebabkan peningkatan besar?

  2. Apakah ini prosedur standar untuk membagi sambungan besar menjadi lebih kecil?

  3. Apakah jumlah data yang server harus proses benar-benar lebih kecil untuk beberapa gabungan yang lebih kecil?

Ini query aslinya:

    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;

Dipecah baru bergabung setelah karya besar DBA:

    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;
Ondrej Peterka
sumber
3
Sebuah kata peringatan - DENGAN (NOLOCK) Adalah jahat - dapat mengakibatkan data buruk kembali. Saya sarankan mencoba WITH (ROWCOMMITTED).
TomTom
1
@ TomTom, Apakah maksud Anda READCOMMITTED? Saya belum pernah melihat ROWCOMMITTED sebelumnya.
ypercubeᵀᴹ
4
WITH (NOLOCK) tidak jahat. Hanya bukan peluru ajaib yang tampaknya dipikirkan orang-orang. Seperti kebanyakan hal dalam SQL Server dan pengembangan perangkat lunak pada umumnya ia memiliki tempatnya.
Zane
2
Ya, tetapi mengingat bahwa NOLOCK dapat menghasilkan peringatan di log dan - lebih penting - mengembalikan DATA SALAH, saya menganggapnya jahat. Cukup banyak hanya dapat digunakan pada tabel DIJAMIN untuk tidak mengubah kunci primer dan kunci yang dipilih saat kueri berjalan. Dan ya, saya bertemu dan DIADAKAN, maaf.
TomTom

Jawaban:

11

1 Pengurangan 'ruang pencarian', ditambah dengan statistik yang lebih baik untuk gabungan antara / akhir.

Saya harus berurusan dengan 90-tabel bergabung (desain mickey mouse) di mana Prosesor Kueri bahkan menolak untuk membuat rencana. Memecah gabungan seperti itu menjadi 10 submoins dari masing-masing 9 tabel, secara dramatis menurunkan kompleksitas setiap sambungan, yang tumbuh secara eksponensial dengan setiap tabel tambahan. Ditambah Pengoptimal Permintaan sekarang menganggapnya sebagai 10 paket, menghabiskan (berpotensi) lebih banyak waktu secara keseluruhan (Paul White bahkan mungkin memiliki metrik!).

Tabel hasil antara sekarang akan memiliki statistik baru sendiri, sehingga bergabung jauh lebih baik dibandingkan dengan statistik pohon yang dalam yang menjadi miring awal dan berakhir sebagai Fiksi Ilmiah segera sesudahnya.

Plus, Anda dapat memaksa gabungan paling selektif terlebih dahulu, mengurangi volume data yang bergerak ke atas pohon. Jika Anda dapat memperkirakan selektivitas predikat Anda jauh lebih baik daripada Pengoptimal, mengapa tidak memaksakan urutan bergabung. Mungkin layak mencari "Rencana Lebat".

2 Ini harus dipertimbangkan dalam pandangan saya, jika efisiensi dan kinerja penting

3 Tidak harus, tetapi bisa juga jika gabungan yang paling selektif dijalankan sejak awal

John Alan
sumber
3
+1 Terima kasih. Khusus untuk deskripsi pengalaman Anda. Sangat benar dalam mengatakan ini, "Jika Anda dapat memperkirakan selektivitas predikat Anda jauh lebih baik daripada Pengoptimal, mengapa tidak memaksakan urutan bergabung."
Ondrej Peterka
2
Ini sebenarnya adalah pertanyaan yang sangat valid. Gabung 90-tabel bisa dipaksa untuk menghasilkan rencana hanya dengan menggunakan opsi 'Force Order'. Tidak masalah bahwa urutannya mungkin acak dan suboptimal, cukup mengurangi ruang pencarian cukup untuk membantu Pengoptimal membuat rencana dalam beberapa detik (tanpa petunjuk itu akan habis setelah 20 detik).
John Alan
6
  1. Pengoptimal SQLServer biasanya melakukan pekerjaan dengan baik. Namun, tujuannya bukan untuk menghasilkan rencana terbaik, tetapi untuk menemukan rencana yang cukup baik dengan cepat. Untuk permintaan tertentu dengan banyak gabungan, ini dapat menyebabkan kinerja yang sangat buruk. Indikasi yang baik untuk kasus tersebut adalah perbedaan besar antara estimasi dan jumlah aktual baris dalam rencana eksekusi aktual. Juga, saya cukup yakin bahwa rencana eksekusi untuk permintaan awal akan menunjukkan banyak 'loop bersarang bergabung' yang lebih lambat daripada 'gabungan bergabung'. Yang terakhir membutuhkan kedua input untuk diurutkan menggunakan kunci yang sama, yang mahal, dan biasanya optimizer membuang opsi seperti itu. Menyimpan hasil dalam tabel sementara dan menambahkan indeks yang tepat seperti yang Anda lakukan hasil - tebakan saya - dalam memilih algoritma yang lebih baik untuk bergabung lebih lanjut (catatan tambahan - Anda mengikuti praktik terbaik dengan mengisi tabel temp pertama, dan menambahkan indeks setelahnya). Selain itu, SQLServer menghasilkan dan menyimpan statistik untuk tabel sementara yang juga membantu memilih indeks yang tepat.
  2. Saya tidak bisa mengatakan ada standar tentang menggunakan tabel sementara ketika jumlah gabungan lebih besar dari beberapa angka tetap, tapi itu jelas merupakan opsi yang dapat meningkatkan kinerja. Itu tidak sering terjadi, tapi saya punya masalah yang sama (dan solusi serupa) beberapa kali. Sebagai alternatif, Anda dapat mencoba mencari tahu rencana eksekusi terbaik sendiri, menyimpan dan memaksa menggunakannya kembali, tetapi itu akan memakan banyak waktu (tidak dijamin 100% Anda akan berhasil). Catatan sisi lain - dalam kasus jika resultset yang disimpan dalam tabel sementara relatif kecil (katakan tentang catatan 10k) variabel tabel berkinerja lebih baik daripada tabel temp.
  3. Saya benci mengatakan 'itu tergantung', tapi mungkin itu jawaban saya untuk pertanyaan ketiga Anda. Pengoptimal harus memberikan hasil dengan cepat; Anda tidak ingin menghabiskan waktu berjam-jam untuk mencari tahu rencana terbaik; setiap bergabung menambah kerja ekstra, dan terkadang pengoptimal 'bingung'.
a1ex07
sumber
3
+1 terima kasih atas konfirmasi dan penjelasannya. Apa yang Anda tulis masuk akal.
Ondrej Peterka
4

Baiklah, izinkan saya mulai dengan mengatakan bahwa Anda mengerjakan data kecil - 10ns juta tidak besar. Projet DWH terakhir yang saya miliki memiliki 400 juta baris ditambahkan ke tabel fakta. PER HARI. Penyimpanan selama 5 tahun.

Masalahnya adalah perangkat keras, sebagian. Karena sambungan besar dapat menggunakan BANYAK ruang sementara dan hanya ada begitu banyak RAM, saat Anda melimpah ke disk, hal itu menjadi jauh lebih lambat. Dengan demikian, mungkin masuk akal untuk membagi pekerjaan menjadi bagian-bagian yang lebih kecil hanya karena sementara SQL hidup di dunia set, dan tidak peduli tentang ukuran, server yang Anda jalankan tidak terbatas. Saya cukup terbiasa untuk keluar dari kesalahan ruang dalam 64 GB tempdb selama beberapa operasi.

Jika tidak, selama staitsics teratur, pengoptimal kueri tidak kewalahan. Tidak peduli seberapa besar tabelnya - ia bekerja dengan statistik yang benar-benar tidak tumbuh. BILANG BAHWA: Jika Anda benar-benar memiliki tabel BESAR (digit dua digit jumlah baris) maka mereka mungkin sedikit kasar.

Ada juga masalah penguncian - kecuali jika Anda memprogram bahwa sambungan besar dapat mengunci meja selama berjam-jam. Saya sedang melakukan operasi penyalinan 200gb saat ini, dan saya membaginya menjadi lebih kecil dengan kunci bisnis (perulangan efektif) yang membuat kunci jauh lebih pendek.

Pada akhirnya, kami bekerja dengan perangkat keras yang terbatas.

TomTom
sumber
1
+1 terima kasih atas jawaban Anda. Ada gunanya mengatakan itu tergantung pada HW. Kami hanya memiliki 32 GB RAM, yang mungkin tidak cukup.
Ondrej Peterka
2
Saya agak frustrasi setiap kali saya membaca jawaban seperti itu - bahkan beberapa lusin juta baris membuat beban CPU di server database kami selama berjam-jam. Mungkin jumlah dimensi tinggi, tetapi 30 dimensi tampaknya bukan angka yang terlalu besar. Saya pikir jumlah baris yang sangat tinggi yang dapat Anda proses berasal dari model sederhana. Lebih buruk lagi: Seluruh data masuk ke dalam RAM. Dan masih butuh waktu berjam-jam.
flaschenpost
1
30 dimensi adalah BANYAK - apakah Anda yakin model ini dioptimalkan dengan benar menjadi bintang? Beberapa kesalahan misalnya biaya CPU - pada permintaan OP menggunakan GUID sebagai kunci utama (uniqueidentifier). Saya juga menyukai mereka - karena indeks unik, kunci utama adalah bidang ID, membuat seluruh perbandingan lebih cepat dan indeks lebih nawwox (4 atau 8 byte, bukan 18). Trik seperti itu menghemat satu TON CPU.
TomTom