Alt sorgu ile büyük tabloda yavaş güncelleme


16

İle SourceTable> sahip 15MM kayıtları ve Bad_Phrase> 3K kayıtları sahip aşağıdaki sorgu SQL Server 2005 SP4 çalıştırmak için neredeyse 10 saat sürer.

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

İngilizce, bu sorgu bir alanın substring edilir Bad_Phrase listelenen farklı cümle sayısının sayılması olup Nameiçinde SourceTableve daha sonra alanda bu sonucu yerleştirme Bad_Count.

Bu sorguyu oldukça hızlı çalıştırmak nasıl bazı öneriler istiyorum.


3
Yani tabloyu 3K kez tarıyor ve 15MM satırların tümünü 3K kez güncelliyorsunuz ve hızlı olmasını mı bekliyorsunuz?
Aaron Bertrand

1
İsim sütununun uzunluğu nedir? Test verileri üreten ve bu çok yavaş sorguyu herhangi birimizin oynayabileceği şekilde yeniden üreten bir komut dosyası veya SQL kemanı gönderebilir misiniz? Belki sadece iyimserim ama 10 saatten daha iyisini yapabiliriz gibi hissediyorum. Diğer yorumcularla bunun hesaplama açısından pahalı bir sorun olduğunu kabul ediyorum, ama neden hala "çok daha hızlı" yapmayı hedefleyemediğimizi anlamıyorum.
Geoff Patterson

3
Matthew, tam metin endekslemeyi düşündün mü? CONTAINS gibi şeyleri kullanabilir ve yine de bu arama için dizin oluşturmanın avantajlarından yararlanabilirsiniz.
eylül

Bu durumda, satır tabanlı mantığı denemenizi öneririm (yani 15MM satırların 1 güncellemesi yerine SourceTable'daki her satırı 15MM güncellemeleri yapar veya bazı nispeten küçük parçaları günceller). Toplam süre daha hızlı olmayacak (bu özel durumda mümkün olmasına rağmen), ancak böyle bir yaklaşım sistemin geri kalanının kesintisiz olarak çalışmaya devam etmesini sağlar, işlem günlüğü boyutu üzerinde kontrol sağlar (her 10k güncellemede kesin), kesinti önceki tüm güncellemeleri kaybetmeden herhangi bir zamanda güncelleme ...
a1ex07

2
@swasheck Tam metin düşünmek için iyi bir fikirdir (2005'te yeni olduğuna inanıyorum, bu yüzden burada uygulanabilir), ancak posterin tam metin dizinleri sözcüklerinden bu yana istediği işlevselliği sağlamak mümkün olmayacaktı. gelişigüzel alt dizeler. Başka bir deyişle, tam metin "fantastik" kelimesi içinde "karınca" için bir eşleşme bulamaz. Ancak, iş gereksinimlerinin tam metin uygulanabilmesi için değiştirilmesi mümkün olabilir.
Geoff Patterson

Yanıtlar:


21

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.

resim açıklamasını buraya girin


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 @minIdve @maxIddeğ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

resim açıklamasını buraya girin

Ardından bu alt dizelerde kümelenmiş bir dizin oluşturun

resim açıklamasını buraya girin

Ş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).

resim açıklamasını buraya girin

Son olarak, gerçek ifade ifadesini kullanarak, LEFT OUTER JOINhatalı kelime öbekleri bulamadığımız adlara 0 sayısı atamak için kullanın.

resim açıklamasını buraya girin


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*Lve 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 SORTalt 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 .


6

Bir süredir yorumlarda Aaron Bertrand'ın gündeme getirdiği bariz sorunu rafalayalım :

Yani tabloyu 3K kez tarıyor ve 15MM satırların tümünü 3K kez güncelliyorsunuz ve hızlı olmasını mı bekliyorsunuz?

Alt sorgunuzun her iki tarafta da joker kart kullanması, anlaşılabilirliği önemli ölçüde etkiler . Bu blog gönderisinden fiyat almak için:

Bu, SQL Server'ın Ürün tablosundaki her satırı okuması, adında herhangi bir yerde “somun” olup olmadığını kontrol etmesi ve sonuçlarımızı döndürmesi gerektiği anlamına gelir.

Her "kötü kelime" ve "Ürün" için "fındık" kelimesini değiştirin SourceTable, ardından Aaron'un yorumuyla birleştirin ve mevcut algoritmanızı kullanarak hızlı bir şekilde çalışmasının neden son derece zor olduğunu (imkansız okuyun) görmeye başlamalısınız .

Birkaç seçenek görüyorum:

  1. İşletmeyi o kadar fazla güce sahip bir canavar sunucu satın almaya ikna edin ki, kesme kuvveti ile sorguyu aşar. (Bu olmayacak, bu yüzden parmaklarınızı çaprazlayın, diğer seçenekler daha iyi)
  2. Mevcut algoritmanızı kullanarak ağrıyı bir kez kabul edin ve sonra yayın. Bu, insert üzerindeki yavaş kelimelerin hesaplanmasını içerir ve bu, insertleri yavaşlatır ve sadece yeni bir kötü kelime girildiğinde / keşfedildiğinde tüm tabloyu günceller.
  3. Geoff'un cevabını kucakla . Bu harika bir algoritma ve ortaya çıkardığım her şeyden çok daha iyi.
  4. Seçenek 2'yi yapın, ancak algoritmanızı Geoff's ile değiştirin.

Gereksinimlerinize bağlı olarak seçenek 3 veya 4'ü öneririm.


0

ilk olarak bu sadece garip bir güncelleme

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

'%' + [Bad_Phrase] gibi. [PHRASE] sizi öldürüyor
Dizin kullanamaz

Veri tasarımı hız için uygun değil
[Bad_Phrase] 'i [PHRASE] tek bir cümleye / kelimeye bölebilir misiniz?
Aynı cümle / kelime birden fazla görünüyorsa, daha yüksek bir sayıya sahip olmasını istiyorsanız bir kereden fazla girebilirsiniz
. Kötü firavundaki satırların sayısı artardı
Eğer eğer yapabiliyorsanız bu çok daha hızlı olacaktır

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]

2005'in destekleyip desteklemediğinden emin değil, Tam Metin Dizini ve İçerir


1
OP'nin kötü kelime tablosundaki kötü sözcüğün örneklerini saymak istediğini sanmıyorum, kaynak tabloda gizlenen kötü sözcüklerin sayısını saymak istediklerini düşünüyorum. Örneğin, orijinal kod muhtemelen "shitass" adı için 2 sayısını verir, ancak kodunuz 0 sayısını verir.
Erik

1
@Erik "[Bad_Phrase] 'i [PHRASE] tek kelime öbeğine bölebilir misiniz?" Gerçekten bir veri tasarımının düzeltme olabileceğini düşünmüyor musunuz? Amaç kötü şeyler bulmak ise o zaman bir veya daha fazla sayıda "eriK" yeterlidir.
paparazzo
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.