Chia tách truy vấn SQL với nhiều tham gia thành các tham gia nhỏ hơn giúp?


18

Chúng tôi cần thực hiện một số báo cáo mỗi đêm trên SQL Server 2008 R2. Tính toán các báo cáo mất vài giờ. Để rút ngắn thời gian chúng tôi tính toán trước một bảng. Bảng này được tạo dựa trên THAM GIA 12 bảng khá lớn (hàng chục triệu hàng).

Việc tính toán bảng tổng hợp này mất đến vài ngày trước cca 4 giờ. DBA của chúng tôi hơn là chia sự tham gia lớn này thành 3 lần tham gia nhỏ hơn (mỗi lần tham gia 4 bảng). Kết quả tạm thời được lưu vào một bảng tạm thời mỗi lần, được sử dụng trong lần nối tiếp theo.

Kết quả của việc tăng cường DBA là, bảng tổng hợp được tính trong 15 phút. Tôi tự hỏi làm thế nào là có thể. DBA nói với tôi rằng đó là vì số lượng dữ liệu mà máy chủ phải xử lý nhỏ hơn. Nói cách khác, trong bản gốc lớn, máy chủ phải làm việc với nhiều dữ liệu hơn so với các phép nối nhỏ hơn. Tuy nhiên, tôi cho rằng trình tối ưu hóa sẽ đảm nhiệm việc thực hiện nó một cách hiệu quả với phép nối lớn ban đầu, tự tách các phép nối và chỉ gửi số lượng cột cần thiết cho các phép nối tiếp theo.

Một điều khác anh ta đã làm là anh ta tạo một chỉ mục trên một trong những bảng tạm thời. Tuy nhiên, một lần nữa tôi sẽ nghĩ rằng trình tối ưu hóa sẽ tạo ra các bảng băm phù hợp nếu cần và hoàn toàn tối ưu hóa tính toán.

Tôi đã nói về điều này với DBA của chúng tôi, nhưng bản thân anh ta không chắc chắn về những gì tạo ra sự cải thiện trong thời gian xử lý. Anh ta chỉ đề cập rằng anh ta sẽ không đổ lỗi cho máy chủ vì nó có thể quá sức để tính toán dữ liệu lớn như vậy và có thể là trình tối ưu hóa khó có thể dự đoán kế hoạch thực hiện tốt nhất .... Điều này tôi hiểu, nhưng tôi muốn có câu trả lời rõ ràng hơn là chính xác tại sao.

Vì vậy, các câu hỏi là:

  1. Điều gì có thể gây ra sự cải thiện lớn?

  2. Đây có phải là một quy trình chuẩn để chia các liên kết lớn thành nhỏ hơn?

  3. Là lượng dữ liệu mà máy chủ phải xử lý thực sự nhỏ hơn trong trường hợp có nhiều kết nối nhỏ hơn?

Đây là truy vấn ban đầu:

    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;

Sự chia tách mới tham gia sau khi DBA hoạt động tốt:

    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
Một lời cảnh báo - VỚI (NOLOCK) Là xấu xa - có thể dẫn đến dữ liệu xấu quay trở lại. Tôi khuyên bạn nên thử VỚI (ROWCOMMITTED).
TomTom

1
@TomTom Ý bạn là READCOMMITTEDsao? Tôi chưa bao giờ thấy ROWCOMMITTED trước đây.
ypercubeᵀᴹ

4
VỚI (NOLOCK) không phải là xấu xa. Nó chỉ không phải là viên đạn ma thuật mà mọi người dường như nghĩ rằng nó là. Giống như hầu hết mọi thứ trong SQL Server và phát triển phần mềm nói chung, nó có vị trí của nó.
Zane

2
Có, nhưng cho rằng NOLOCK có thể tạo ra các cảnh báo trong nhật ký và - quan trọng hơn - trả lại SAU DỮ LIỆU, tôi coi đó là điều xấu. Nó chỉ có thể sử dụng được trên các bảng được ĐẢM BẢO không thay đổi trong khóa chính và các khóa được chọn trong khi truy vấn chạy. Và vâng, tôi xin lỗi READCOMMMITED, xin lỗi.
TomTom

Câu trả lời:


11

1 Giảm 'không gian tìm kiếm', cùng với số liệu thống kê tốt hơn cho các kết nối trung gian / trễ.

Tôi đã phải đối phó với các phép nối 90 bảng (thiết kế chuột mickey) trong đó Bộ xử lý truy vấn từ chối thậm chí không tạo kế hoạch. Việc chia một liên kết như vậy thành 10 liên kết của 9 bảng mỗi bảng, làm giảm đáng kể độ phức tạp của mỗi liên kết, tăng theo cấp số nhân với mỗi bảng bổ sung. Cộng với Trình tối ưu hóa truy vấn hiện coi chúng là 10 kế hoạch, dành tổng thời gian (có khả năng) nhiều hơn (Paul White thậm chí có thể có số liệu!).

Các bảng kết quả trung gian bây giờ sẽ có số liệu thống kê mới của riêng họ, do đó kết hợp tốt hơn nhiều so với thống kê của một cây sâu bị lệch sớm và kết thúc là Khoa học viễn tưởng ngay sau đó.

Ngoài ra, bạn có thể buộc các tham gia chọn lọc nhất trước tiên, cắt giảm khối lượng dữ liệu di chuyển lên cây. Nếu bạn có thể ước tính độ chọn lọc của các vị từ tốt hơn nhiều so với Trình tối ưu hóa, tại sao không bắt buộc thứ tự tham gia. Có thể đáng để tìm kiếm "Kế hoạch Bushy".

2nên được xem xét trong quan điểm của tôi, nếu tính hiệu quả và hiệu suất rất quan trọng

3 Không nhất thiết, nhưng có thể là nếu các phép nối được chọn nhiều nhất được thực hiện sớm vào


3
+1 Cảm ơn. Đặc biệt là cho mô tả kinh nghiệm của bạn. Rất đúng khi nói điều này "Nếu bạn có thể ước tính độ chọn lọc của các vị từ của bạn tốt hơn nhiều so với Trình tối ưu hóa, tại sao không bắt buộc thứ tự tham gia."
Ondrej Peterka

2
Đó là một câu hỏi rất hợp lệ thực sự. Việc tham gia 90 bảng có thể bị ép buộc để tạo ra một kế hoạch chỉ bằng cách sử dụng tùy chọn 'Lệnh bắt buộc'. Không có vấn đề gì khi thứ tự có thể là ngẫu nhiên và không tối ưu, chỉ cần giảm không gian tìm kiếm là đủ để giúp Optimiser tạo ra một kế hoạch trong vài giây (không có gợi ý sẽ hết sau 20 giây).
John Alan

6
  1. Trình tối ưu hóa SQLServer thường làm tốt công việc. Tuy nhiên, mục tiêu của nó không phải là tạo ra kế hoạch tốt nhất có thể, mà là tìm ra kế hoạch đủ tốt một cách nhanh chóng. Đối với một truy vấn cụ thể có nhiều tham gia, nó có thể gây ra hiệu suất rất kém. Dấu hiệu tốt của trường hợp này là sự khác biệt lớn giữa số lượng hàng ước tính và thực tế trong kế hoạch thực hiện thực tế. Ngoài ra, tôi khá chắc chắn rằng kế hoạch thực hiện cho truy vấn ban đầu sẽ hiển thị nhiều 'vòng lặp lồng nhau', chậm hơn 'hợp nhất tham gia'. Cái sau đòi hỏi cả hai đầu vào phải được sắp xếp bằng cùng một khóa, đắt tiền và thường tối ưu hóa loại bỏ tùy chọn như vậy. Lưu trữ kết quả trong bảng tạm thời và thêm các chỉ mục thích hợp như bạn đã làm kết quả - tôi đoán - trong việc chọn thuật toán tốt hơn cho các phép nối tiếp (lưu ý phụ - bạn làm theo các thực tiễn tốt nhất bằng cách điền vào bảng tạm thời trước, và thêm chỉ mục sau). Ngoài ra, SQLServer tạo và giữ số liệu thống kê cho các bảng tạm thời cũng giúp chọn chỉ mục thích hợp.
  2. Tôi không thể nói có một tiêu chuẩn về việc sử dụng các bảng tạm thời khi số lượng tham gia lớn hơn một số số cố định, nhưng đó chắc chắn là một tùy chọn có thể cải thiện hiệu suất. Điều đó không xảy ra thường xuyên, nhưng tôi đã gặp vấn đề tương tự (và giải pháp tương tự) vài lần. Ngoài ra, bạn có thể thử tự mình tìm ra kế hoạch thực hiện tốt nhất, lưu trữ và buộc sử dụng lại, nhưng sẽ mất rất nhiều thời gian (không đảm bảo 100% bạn sẽ thành công). Một lưu ý phụ khác - trong trường hợp nếu resultset được lưu trữ trong bảng tạm thời tương đối nhỏ (khoảng 10k bản ghi) thì biến bảng hoạt động tốt hơn bảng tạm thời.
  3. Tôi ghét nói 'điều đó phụ thuộc', nhưng có lẽ đó là câu trả lời của tôi cho câu hỏi thứ ba của bạn. Trình tối ưu hóa phải cho kết quả nhanh; bạn không muốn nó mất hàng giờ để cố gắng vạch ra kế hoạch tốt nhất; mỗi lần tham gia sẽ thêm công việc và đôi khi trình tối ưu hóa 'bị lẫn lộn'.

3
+1 cảm ơn đã xác nhận và giải thích. Những gì bạn đã viết có ý nghĩa.
Ondrej Peterka

4

Chà, hãy để tôi bắt đầu bằng cách nói rằng bạn làm việc trên dữ liệu nhỏ - 10 triệu trong số hàng triệu không lớn. Projet DWH cuối cùng tôi đã có 400 triệu hàng được thêm vào bảng thực tế. M DAYI NGÀY. Lưu trữ trong 5 năm.

Vấn đề là phần cứng, một phần. Vì các phép nối lớn có thể sử dụng RẤT NHIỀU dung lượng tạm thời và chỉ có rất nhiều RAM, thời điểm bạn tràn vào đĩa, mọi thứ trở nên chậm hơn rất nhiều. Như vậy, có thể hợp lý khi chia công việc thành các phần nhỏ hơn đơn giản vì trong khi SQL sống trong một thế giới tập hợp và không quan tâm đến kích thước, thì máy chủ bạn chạy không phải là vô hạn. Tôi khá quen với việc thoát khỏi lỗi không gian trong tempdb 64gb trong một số thao tác.

Mặt khác, miễn là các staitsics theo thứ tự, trình tối ưu hóa truy vấn không bị quá tải. Nó không thực sự quan tâm bảng lớn đến mức nào - nó hoạt động theo số liệu thống kê thực sự không tăng trưởng. R SANG NÓI: Nếu bạn thực sự có một bảng LỚN (hai chữ số tỷ số hàng) thì chúng có thể hơi thô.

Ngoài ra còn có vấn đề về khóa - trừ khi bạn lập trình một cách độc đáo, phép nối lớn có thể khóa bảng trong nhiều giờ. Tôi đang thực hiện các hoạt động sao chép 200gb vào lúc này và tôi đang chia chúng thành smllerparty bằng một khóa doanh nghiệp (vòng lặp hiệu quả) giúp khóa ngắn hơn rất nhiều.

Cuối cùng, chúng tôi làm việc với phần cứng hạn chế.


1
+1 cảm ơn câu trả lời của bạn. Có một điểm tốt khi nói nó phụ thuộc vào CTNH. Chúng tôi chỉ có 32 GB RAM, có lẽ không đủ.
Ondrej Peterka

2
Tôi hơi nản lòng mỗi khi đọc câu trả lời như vậy - thậm chí vài chục triệu hàng tạo ra tải CPU trên máy chủ cơ sở dữ liệu của chúng tôi trong nhiều giờ. Có thể số lượng kích thước cao, nhưng 30 kích thước dường như không phải là một con số quá lớn. Tôi nghĩ rằng số lượng hàng rất cao bạn có thể xử lý là từ một mô hình đơn giản. Thậm chí tệ hơn: Toàn bộ dữ liệu phù hợp với RAM. Và vẫn mất hàng giờ.
flaschenpost

1
30 kích thước là RẤT NHIỀU - bạn có chắc mô hình được tối ưu hóa đúng cách thành một ngôi sao không? Một số sai lầm, ví dụ như CPU ​​có giá - trên truy vấn OP đang sử dụng GUID làm khóa chính (định danh duy nhất). Tôi cũng yêu chúng - vì chỉ mục duy nhất, khóa chính là trường ID, làm cho toàn bộ so sánh nhanh hơn và chỉ mục trở nên gọn gàng hơn (4 hoặc 8 byte, không phải 18). Thủ thuật như vậy tiết kiệm một TẤN CPU.
TomTom
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.