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 UPDATE
tuyê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 name
là 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.
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
và @maxId
cá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
Sau đó tạo một chỉ mục cụm trên các chuỗi con nà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).
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.
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*L
chuỗ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 để
SORT
cá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 .