Cập nhật chậm trên bảng lớn với truy vấn con


16

Với SourceTablecác bản ghi> 15MM và Bad_Phrasecó các bản ghi > 3K, truy vấn sau mất gần 10 giờ để chạy trên SQL Server 2005 SP4.

UPDATE [SourceTable] 
SET 
    Bad_Count=
             (
               SELECT 
                  COUNT(*) 
               FROM Bad_Phrase 
               WHERE 
                  [SourceTable].Name like '%'+Bad_Phrase.PHRASE+'%'
             )

Trong tiếng Anh, truy vấn này đang đếm số lượng cụm từ riêng biệt được liệt kê trong Bad_Phrase là một chuỗi con của trường Nametrong SourceTablevà sau đó đặt kết quả đó vào trường Bad_Count.

Tôi muốn một số gợi ý về cách để truy vấn này chạy nhanh hơn đáng kể.


3
Vì vậy, bạn đang quét bảng 3K lần và có khả năng cập nhật tất cả các hàng 15MM tất cả 3K lần và bạn có mong đợi nó sẽ nhanh không?
Aaron Bertrand

1
Độ dài của cột tên là gì? Bạn có thể đăng một tập lệnh hoặc SQL fiddle tạo dữ liệu thử nghiệm và tái tạo truy vấn rất chậm này theo cách mà bất kỳ ai trong chúng ta có thể chơi với? Có lẽ tôi chỉ là một người lạc quan, nhưng tôi cảm thấy như chúng ta có thể làm tốt hơn 10 giờ. Tôi đồng ý với các nhà bình luận khác rằng đây là một vấn đề đắt đỏ về mặt tính toán, nhưng tôi không hiểu tại sao chúng ta vẫn không thể làm cho nó "nhanh hơn đáng kể".
Geoff Patterson

3
Matthew, bạn đã xem xét lập chỉ mục toàn văn? Bạn có thể sử dụng những thứ như CONTAIN và vẫn nhận được lợi ích của việc lập chỉ mục cho tìm kiếm đó.
swasheck

Trong trường hợp này, tôi khuyên bạn nên thử logic dựa trên hàng (tức là thay vì 1 bản cập nhật của 15MM hàng, hãy thực hiện cập nhật 15MM mỗi hàng trong SourceTable hoặc cập nhật một số đoạn tương đối nhỏ). Tổng thời gian sẽ không nhanh hơn (mặc dù có thể trong trường hợp cụ thể này), nhưng cách tiếp cận như vậy cho phép phần còn lại của hệ thống tiếp tục hoạt động mà không bị gián đoạn, cho phép bạn kiểm soát kích thước nhật ký giao dịch (giả sử cứ sau 10k cập nhật) cập nhật bất cứ lúc nào mà không mất tất cả các cập nhật trước đó ...
a1ex07

2
@swasheck Toàn văn là một ý kiến ​​hay để xem xét (tôi tin rằng nó mới vào năm 2005, vì vậy có thể áp dụng ở đây), nhưng không thể cung cấp chức năng tương tự mà người đăng yêu cầu vì các từ chỉ toàn văn bản không phải là từ chất nền tùy ý. Nói một cách khác, toàn văn bản sẽ không tìm thấy sự phù hợp cho "con kiến" trong từ "tuyệt vời". Nhưng có thể các yêu cầu kinh doanh có thể được sửa đổi để toàn văn có thể áp dụng được.
Geoff Patterson

Câu trả lời:


21

Mặc dù tôi đồng ý với các nhà bình luận khác rằng đây là một vấn đề tốn kém về mặt tính toán, tôi nghĩ rằng có rất nhiều chỗ để cải thiện bằng cách điều chỉnh SQL mà bạn đang sử dụng. Để minh họa, tôi tạo một bộ dữ liệu giả với 15MM tên và 3K cụm từ, chạy cách tiếp cận cũ và chạy một cách tiếp cận mới.

Tập lệnh đầy đủ để tạo tập dữ liệu giả và thử phương pháp mới

TL; DR

Trên máy của tôi và bộ dữ liệu giả mạo này, cách tiếp cận ban đầu mất khoảng 4 giờ để chạy. Phương pháp mới được đề xuất mất khoảng 10 phút , một sự cải thiện đáng kể. Dưới đây là một bản tóm tắt ngắn về phương pháp đề xuất:

  • Đối với mỗi tên, tạo chuỗi con bắt đầu ở mỗi ký tự bù (và được giới hạn ở độ dài của cụm từ xấu dài nhất, dưới dạng tối ưu hóa)
  • Tạo một chỉ mục cụm trên các chuỗi con
  • Đối với mỗi cụm từ xấu, hãy thực hiện tìm kiếm trong các chuỗi con này để xác định bất kỳ kết quả khớp nào
  • Đối với mỗi chuỗi ban đầu, hãy tính số lượng cụm từ xấu riêng biệt phù hợp với một hoặc nhiều chuỗi con của chuỗi đó


Cách tiếp cận ban đầu: phân tích thuật toán

Từ kế hoạch của UPDATEtuyên bố ban đầu , chúng ta có thể thấy rằng số lượng công việc tỷ lệ tuyến tính với cả số lượng tên (15MM) và số lượng cụm từ (3K). Vì vậy, nếu chúng ta nhân cả số lượng tên và cụm từ với 10, thì thời gian chạy tổng thể sẽ chậm hơn ~ 100 lần.

Các truy vấn thực sự tỷ lệ thuận với chiều dài namelà tốt; trong khi đây là một chút ẩn trong kế hoạch truy vấn, nó xuất hiện trong "số lần thực thi" để tìm kiếm trong bộ đệm bảng. Trong kế hoạch thực tế, chúng ta có thể thấy rằng điều này xảy ra không chỉ một lần cho mỗi lần name, mà thực sự là một lần cho mỗi ký tự bù vào name. Vì vậy, cách tiếp cận này là O ( # names* # phrases* name length) trong độ phức tạp thời gian chạy.

nhập mô tả hình ảnh ở đây


Cách tiếp cận mới: mã

Mã này cũng có sẵn trong pastebin đầy đủ nhưng tôi đã sao chép nó ở đây để thuận tiện. Pastebin cũng có định nghĩa thủ tục đầy đủ, bao gồm @minId@maxIdcác biến mà bạn thấy bên dưới để xác định ranh giới của lô hiện tại.

-- For each name, generate the string at each offset
DECLARE @maxBadPhraseLen INT = (SELECT MAX(LEN(phrase)) FROM Bad_Phrase)
SELECT s.id, sub.sub_name
INTO #SubNames
FROM (SELECT * FROM SourceTable WHERE id BETWEEN @minId AND @maxId) s
CROSS APPLY (
    -- Create a row for each substring of the name, starting at each character
    -- offset within that string.  For example, if the name is "abcd", this CROSS APPLY
    -- will generate 4 rows, with values ("abcd"), ("bcd"), ("cd"), and ("d"). In order
    -- for the name to be LIKE the bad phrase, the bad phrase must match the leading X
    -- characters (where X is the length of the bad phrase) of at least one of these
    -- substrings. This can be efficiently computed after indexing the substrings.
    -- As an optimization, we only store @maxBadPhraseLen characters rather than
    -- storing the full remainder of the name from each offset; all other characters are
    -- simply extra space that isn't needed to determine whether a bad phrase matches.
    SELECT TOP(LEN(s.name)) SUBSTRING(s.name, n.n, @maxBadPhraseLen) AS sub_name 
    FROM Numbers n
    ORDER BY n.n
) sub
-- Create an index so that bad phrases can be quickly compared for a match
CREATE CLUSTERED INDEX IX_SubNames ON #SubNames (sub_name)

-- For each name, compute the number of distinct bad phrases that match
-- By "match", we mean that the a substring starting from one or more 
-- character offsets of the overall name starts with the bad phrase
SELECT s.id, COUNT(DISTINCT b.phrase) AS bad_count
INTO #tempBadCounts
FROM dbo.Bad_Phrase b
JOIN #SubNames s
    ON s.sub_name LIKE b.phrase + '%'
GROUP BY s.id

-- Perform the actual update into a "bad_count_new" field
-- For validation, we'll compare bad_count_new with the originally computed bad_count
UPDATE s
SET s.bad_count_new = COALESCE(b.bad_count, 0)
FROM dbo.SourceTable s
LEFT JOIN #tempBadCounts b
    ON b.id = s.id
WHERE s.id BETWEEN @minId AND @maxId


Cách tiếp cận mới: kế hoạch truy vấn

Đầu tiên, chúng tôi tạo chuỗi con bắt đầu ở mỗi ký tự offset

nhập mô tả hình ảnh ở đây

Sau đó tạo một chỉ mục cụm trên các chuỗi con này

nhập mô tả hình ảnh ở đây

Bây giờ, đối với mỗi cụm từ xấu, chúng tôi tìm kiếm các chuỗi con này để xác định bất kỳ kết quả khớp nào. Sau đó, chúng tôi tính toán số lượng cụm từ xấu riêng biệt phù hợp với một hoặc nhiều chuỗi con của chuỗi đó. Đây thực sự là bước quan trọng; do cách chúng tôi lập chỉ mục các chuỗi con, chúng tôi không còn phải kiểm tra toàn bộ sản phẩm chéo của các cụm từ và tên xấu. Bước này, tính toán thực tế, chỉ chiếm khoảng 10% thời gian chạy thực tế (phần còn lại là tiền xử lý các chuỗi con).

nhập mô tả hình ảnh ở đây

Cuối cùng, thực hiện câu lệnh cập nhật thực tế, sử dụng a LEFT OUTER JOINđể gán tổng số 0 cho bất kỳ tên nào mà chúng tôi không tìm thấy cụm từ xấu.

nhập mô tả hình ảnh ở đây


Cách tiếp cận mới: phân tích thuật toán

Cách tiếp cận mới có thể được chia thành hai giai đoạn, tiền xử lý và kết hợp. Hãy xác định các biến sau:

  • N = # tên
  • B = # cụm từ xấu
  • L = chiều dài tên trung bình, tính bằng ký tự

Giai đoạn tiền xử lý là O(N*L * LOG(N*L))để tạo ra các N*Lchuỗi con và sau đó sắp xếp chúng.

Sự phù hợp thực tế là O(B * LOG(N*L))để tìm kiếm các chuỗi con cho mỗi cụm từ xấu.

Bằng cách này, chúng tôi đã tạo ra một thuật toán không chia tỷ lệ tuyến tính với số lượng cụm từ xấu, hiệu suất chính mở khóa khi chúng tôi mở rộng thành 3K cụm từ và hơn thế nữa. Nói một cách khác, việc triển khai ban đầu mất khoảng 10 lần miễn là chúng ta chuyển từ 300 cụm từ xấu thành 3K cụm từ xấu. Tương tự như vậy, sẽ mất thêm 10 lần nữa nếu chúng ta chuyển từ 3K cụm từ xấu thành 30K. Tuy nhiên, việc triển khai mới sẽ mở rộng quy mô tuyến tính và trên thực tế chỉ mất ít hơn 2 lần thời gian được đo trên 3K cụm từ xấu khi thu nhỏ tới 30K cụm từ xấu.


Giả định / Hãy cẩn thận

  • Tôi đang chia công việc tổng thể thành các lô có kích thước khiêm tốn. Đây có lẽ là một ý tưởng tốt cho một trong hai cách tiếp cận, nhưng nó đặc biệt quan trọng đối với cách tiếp cận mới để SORTcác chuỗi con độc lập với từng lô và dễ dàng phù hợp với bộ nhớ. Bạn có thể thao tác kích thước lô khi cần, nhưng sẽ không khôn ngoan khi thử tất cả các hàng 15MM trong một lô.
  • Tôi đang dùng SQL 2014, không phải SQL 2005, vì tôi không có quyền truy cập vào máy SQL 2005. Tôi đã cẩn thận không sử dụng bất kỳ cú pháp nào không có sẵn trong SQL 2005, nhưng tôi vẫn có thể nhận được lợi ích từ tính năng ghi lười biếng tempdb trong SQL 2012+ và tính năng SELECT INTO song song trong SQL 2014.
  • Độ dài của cả tên và cụm từ khá quan trọng đối với cách tiếp cận mới. Tôi cho rằng các cụm từ xấu thường khá ngắn vì có thể phù hợp với các trường hợp sử dụng trong thế giới thực. Các tên dài hơn một chút so với các cụm từ xấu, nhưng được giả định không phải là hàng ngàn ký tự. Tôi nghĩ rằng đây là một giả định hợp lý và các chuỗi tên dài hơn cũng sẽ làm chậm cách tiếp cận ban đầu của bạn.
  • Một phần của cải tiến (nhưng không ở đâu gần với tất cả) là do cách tiếp cận mới có thể thúc đẩy sự song song hiệu quả hơn so với cách tiếp cận cũ (chạy đơn luồng). Tôi đang sử dụng máy tính xách tay lõi tứ, vì vậy thật tuyệt khi có cách tiếp cận có thể đưa các lõi này vào sử dụng.


Bài đăng trên blog liên quan

Aaron Bertrand khám phá loại giải pháp này chi tiết hơn trong bài đăng trên blog của mình Một cách để có được một chỉ mục tìm kiếm% ký tự đại diện hàng đầu .


6

Chúng ta hãy tạm gác vấn đề rõ ràng được đưa ra bởi Aaron Bertrand trong các bình luận trong một giây:

Vì vậy, bạn đang quét bảng 3K lần và có khả năng cập nhật tất cả các hàng 15MM tất cả 3K lần và bạn có mong đợi nó sẽ nhanh không?

Thực tế là truy vấn phụ của bạn sử dụng các thẻ hoang dã ở cả hai bên ảnh hưởng đáng kể đến khả năng mở rộng . Để lấy một trích dẫn từ bài viết trên blog đó:

Điều đó có nghĩa là SQL Server phải đọc mọi hàng trong bảng Sản phẩm, kiểm tra xem liệu nó có được nut nut hay không ở bất kỳ đâu trong tên và sau đó trả về kết quả của chúng tôi.

Trao đổi từ "nut" cho mỗi "từ xấu" và "Sản phẩm" SourceTable, sau đó kết hợp từ đó với nhận xét của Aaron và bạn sẽ bắt đầu thấy tại sao nó cực kỳ khó (không thể đọc) để làm cho nó chạy nhanh bằng thuật toán hiện tại của bạn.

Tôi thấy một vài lựa chọn:

  1. Thuyết phục doanh nghiệp mua một máy chủ quái vật có sức mạnh lớn đến mức nó vượt qua truy vấn bằng cách cắt vũ lực. (Điều đó sẽ không xảy ra vì vậy hãy vượt qua các ngón tay của bạn, các tùy chọn khác sẽ tốt hơn)
  2. Sử dụng thuật toán hiện tại của bạn, chấp nhận nỗi đau một lần và sau đó trải rộng nó ra. Điều này sẽ liên quan đến việc tính toán các từ xấu khi chèn, nó sẽ làm chậm các phần chèn và chỉ cập nhật toàn bộ bảng khi một từ xấu mới được nhập / phát hiện.
  3. Ôm câu trả lời của Geoff . Đây là một thuật toán tuyệt vời, và tốt hơn nhiều so với bất cứ điều gì tôi sẽ nghĩ ra.
  4. Thực hiện tùy chọn 2 nhưng thay thế thuật toán của bạn bằng Geoff.

Tùy thuộc vào yêu cầu của bạn, tôi muốn giới thiệu tùy chọn 3 hoặc 4.


0

đầu tiên đó chỉ là một bản cập nhật lạ

Update [SourceTable]  
   Set [SourceTable].[Bad_Count] = [fix].[count]
  from [SourceTable] 
  join ( Select count(*) 
           from [Bad_Phrase]  
          where [SourceTable].Name like '%' + [Bad_Phrase].[PHRASE] + '%')

Giống như '%' + [Bad_Phrase]. [PHRASE] đang giết bạn Bạn
không thể sử dụng chỉ mục

Thiết kế dữ liệu không tối ưu cho tốc độ
Bạn có thể chia [Bad_Phrase]. [PHRASE] thành một cụm (s) / từ không?
Nếu cùng một cụm từ / từ xuất hiện nhiều hơn một từ, bạn có thể nhập nó nhiều lần nếu bạn muốn nó có số lượng cao hơn
Vì vậy, số lượng hàng trong pharase xấu sẽ tăng lên
Nếu bạn có thể thì điều này sẽ nhanh hơn nhiều

Update [SourceTable]  
   Set [SourceTable].[Bad_Count] = [fix].[count]
  from [SourceTable] 
  join ( select [PHRASE], count(*) as count 
           from [Bad_Phrase] 
          group by [PHRASE] 
       ) as [fix]
    on [fix].[PHRASE] = [SourceTable].[name]  
 where [SourceTable].[Bad_Count] <> [fix].[count]

Không chắc chắn nếu năm 2005 hỗ trợ nó nhưng Chỉ mục toàn văn bản và sử dụng Chứa


1
Tôi không nghĩ OP muốn đếm các trường hợp của từ xấu trong bảng từ xấu Tôi nghĩ rằng họ muốn đếm số lượng từ xấu ẩn trong bảng nguồn. Ví dụ, mã ban đầu có thể sẽ cho số đếm là 2 cho tên của "shitass" nhưng mã của bạn sẽ cho số đếm là 0.
Erik

1
@Erik "bạn có thể chia [Bad_Ph cụm từ]. [PHRASE] thành một cụm từ duy nhất không?" Thực sự bạn không nghĩ rằng một thiết kế dữ liệu có thể là sửa chữa? Nếu mục đích là tìm những thứ xấu thì "eriK" với số lượng một hoặc nhiều là đủ.
paparazzo
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.