Diğer yorumcular ile bu hesaplamalı olarak pahalı bir sorun olduğunu kabul ederken, kullandığınız SQL tweaking ile iyileştirilmesi için çok yer olduğunu düşünüyorum. Göstermek için, 15MM adları ve 3K ifadeleriyle sahte bir veri kümesi oluşturdum, eski yaklaşımı ve yeni bir yaklaşımı çalıştırdım.
Sahte veri kümesi oluşturmak ve yeni yaklaşımı denemek için tam komut dosyası
TL; DR
Makinemde ve bu sahte veri kümesinde, orijinal yaklaşımın çalışması yaklaşık 4 saat sürer . Önerilen yeni yaklaşım yaklaşık 10 dakika sürüyor ve bu önemli bir gelişme. İşte önerilen yaklaşımın kısa bir özeti:
- Her ad için, alt karakter dizisini her karakter ofsetinden başlayarak (ve en iyileştirme olarak en uzun bozuk tümcecik uzunluğunda sınırlanmış olarak) oluşturun
- Bu alt dizelerde kümelenmiş bir dizin oluşturun
- Her kötü ifade için, eşleşmeleri tanımlamak üzere bu alt dizelere bir arama yapın
- Her orijinal dize için, o dizenin bir veya daha fazla alt dizesiyle eşleşen farklı hatalı kelime öbeklerinin sayısını hesaplayın
Orijinal yaklaşım: algoritmik analiz
Orijinalin planından UPDATE
ifadenin iş miktarının hem ad sayısı (15MM) hem de ifade sayısı (3K) ile doğrusal olarak orantılı olduğunu görebiliriz. Dolayısıyla, hem adların hem de kelime öbeklerinin sayısını 10 ile çarparsak, toplam çalışma süresi ~ 100 kat daha yavaş olacaktır.
Sorgu aslında uzunluğuyla da orantılıdır name
; Bu sorgu planında biraz gizli olsa da, tablo biriktirme aramak için "yürütme sayısı" gelir. Gerçek planda, bunun sadece başına bir kez değil name
, aslında name
. Yani bu yaklaşım çalışma zamanı karmaşıklığında O ( # names
* # phrases
* name length
) 'dur.
Yeni yaklaşım: kod
Bu kod tam macunta da mevcuttur, ancak kolaylık sağlamak için burada kopyaladım. Ayrıca, geçerli toplu işin sınırlarını tanımlamak için aşağıda gördüğünüz @minId
ve @maxId
değişkenleri içeren tam yordam tanımına da sahiptir .
-- 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
Yeni yaklaşım: sorgu planları
İlk olarak, her karakter ofsetinden başlayarak alt dizeyi oluşturuyoruz
Ardından bu alt dizelerde kümelenmiş bir dizin oluşturun
Şimdi, her kötü ifade için herhangi bir eşleşmeyi tanımlamak için bu alt dizeleri araştırıyoruz. Daha sonra, bu dizenin bir veya daha fazla alt dizesiyle eşleşen farklı hatalı ifadelerin sayısını hesaplıyoruz. Bu gerçekten önemli bir adım; alt dizeleri dizine eklediğimizden dolayı, artık kötü ifadelerin ve adların tam bir çapraz ürününü kontrol etmemiz gerekmiyor. Gerçek hesaplamayı yapan bu adım, gerçek çalışma süresinin yalnızca% 10'unu oluşturur (geri kalanı alt dizelerin ön işlemidir).
Son olarak, gerçek ifade ifadesini kullanarak, LEFT OUTER JOIN
hatalı kelime öbekleri bulamadığımız adlara 0 sayısı atamak için kullanın.
Yeni yaklaşım: algoritmik analiz
Yeni yaklaşım, ön işleme ve eşleştirme olmak üzere iki aşamaya ayrılabilir. Aşağıdaki değişkenleri tanımlayalım:
N
= ad sayısı
B
= hatalı kelime öbeği sayısı
L
= karakter cinsinden ortalama ad uzunluğu
Ön işleme aşaması alt dizeler O(N*L * LOG(N*L))
oluşturmak N*L
ve bunları sıralamak içindir.
Gerçek eşleme, O(B * LOG(N*L))
her kötü ifade için alt dizeleri aramak içindir.
Bu şekilde, kötü ifadelerin sayısı ile doğrusal olarak ölçeklenmeyen bir algoritma oluşturduk, 3K ifadelerine ve ötesine ölçeklendiğimizde kilit performans kilidi. Başka bir deyişle, 300 kötü ifadeden 3K kötü ifadeye gittiğimiz sürece orijinal uygulama yaklaşık 10 kat alır. Benzer şekilde, 3K kötü ifadelerden 30K'ya geçersek 10 kat daha sürecekti. Bununla birlikte, yeni uygulama alt doğrusal olarak ölçeklenecek ve aslında 30K'ya kadar kötü ifadelere ölçeklendiğinde 3K kötü ifadelerde ölçülen sürenin 2 katından daha az zaman alacaktır.
Varsayımlar / Uyarılar
- Genel çalışmayı mütevazı boyutlarda toplu işlere bölüyorum. Bu muhtemelen her iki yaklaşım için de iyi bir fikirdir, ancak yeni yaklaşım için özellikle önemlidir, böylece
SORT
alt dizelerdeki her grup için bağımsızdır ve kolayca belleğe sığar. Toplu iş boyutunu gerektiği gibi işleyebilirsiniz, ancak 15MM satırların tümünü bir toplu işte denemek akıllıca olmaz.
- Bir SQL 2005 makinesine erişimim olmadığından SQL 2005 değil, SQL 2014 kullanıyorum. SQL 2005'te bulunmayan herhangi bir sözdizimini kullanmamaya dikkat ettim, ancak yine de SQL 2012 + 'daki tempdb tembel yazma özelliğinden ve SQL 2014'teki paralel SELECT INTO özelliğinden faydalanıyor olabilirim .
- Hem adların hem de kelime öbeklerinin uzunlukları yeni yaklaşım için oldukça önemlidir. Kötü ifadelerin tipik olarak oldukça kısa olduğunu varsayıyorum, çünkü bu gerçek dünyadaki kullanım durumlarıyla eşleşebilir. İsimler kötü ifadelerden biraz daha uzundur, ancak binlerce karakter olmadığı varsayılmaktadır. Bunun adil bir varsayım olduğunu düşünüyorum ve daha uzun isim dizeleri orijinal yaklaşımınızı da yavaşlatacaktır.
- İyileştirmenin bir kısmı (ancak hiçbir yere yakın değil), yeni yaklaşımın eski yaklaşımdan (tek iş parçacıklı çalışan) paralellikten daha etkili bir şekilde yararlanabilmesinden kaynaklanmaktadır. Dört çekirdekli bir dizüstü bilgisayardayım, bu yüzden bu çekirdekleri kullanmak için iyi bir yaklaşım var.
İlgili blog yazısı
Aaron Bertrand blog yazısında bu tür bir çözümü daha ayrıntılı bir şekilde araştırıyor Bir dizin% joker karakteri aramak için bir yol .