อัปเดตช้าในตารางขนาดใหญ่พร้อมแบบสอบถามย่อย


16

เมื่อSourceTableมีระเบียน> 15MM และBad_Phraseมีระเบียน> 3K แบบสอบถามต่อไปนี้จะใช้เวลาเกือบ 10 ชั่วโมงในการเรียกใช้บน SQL Server 2005 SP4

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

ในภาษาอังกฤษคำนี้คือการนับจำนวนของวลีที่แตกต่างกันการระบุไว้ใน Bad_Phrase ที่มี substring ของสนามNameในแล้ววางผลว่าในสนามSourceTableBad_Count

ฉันต้องการคำแนะนำเกี่ยวกับวิธีทำให้แบบสอบถามนี้ทำงานได้เร็วขึ้นอย่างมาก


3
ดังนั้นคุณกำลังสแกนตาราง 3K ครั้งและอาจอัพเดตแถว 15MM ทั้งหมด 3K ครั้งและคุณคาดหวังว่ามันจะเร็วหรือไม่
Aaron Bertrand

1
ความยาวของคอลัมน์ชื่อคืออะไร? คุณสามารถโพสต์สคริปต์หรือซอซอร์ต SQL ที่สร้างข้อมูลทดสอบและสร้างคิวรีที่ช้ามากในแบบที่พวกเราทุกคนสามารถเล่นได้หรือไม่? บางทีฉันแค่เป็นคนมองโลกในแง่ดี แต่ฉันรู้สึกว่าเราสามารถทำได้ดีกว่า 10 ชั่วโมง ฉันเห็นด้วยกับผู้แสดงความคิดเห็นคนอื่นว่านี่เป็นปัญหาที่มีราคาแพง แต่ฉันไม่เห็นว่าทำไมเรายังไม่สามารถตั้งเป้าให้มัน "เร็วขึ้นมาก"
Geoff Patterson

3
Matthew คุณเคยคิดที่จะจัดทำดัชนีข้อความหรือไม่? คุณสามารถใช้สิ่งต่าง ๆ เช่น CONTAINS และยังได้รับประโยชน์จากการจัดทำดัชนีสำหรับการค้นหานั้น
swasheck

ในกรณีนี้ฉันขอแนะนำให้ลองใช้ตรรกะแถว (เช่นแทนที่จะอัปเดต 1 แถว 15MM ทำ 15MM อัปเดตแต่ละแถวใน SourceTable หรืออัปเดตชิ้นที่ค่อนข้างเล็ก) เวลาทั้งหมดจะไม่เร็วขึ้น (แม้ว่าจะเป็นไปได้ในกรณีนี้) แต่วิธีการดังกล่าวช่วยให้ระบบที่เหลือทำงานต่อไปได้โดยไม่หยุดชะงักช่วยให้คุณสามารถควบคุมขนาดของบันทึกธุรกรรม (พูดการอัพเดททุก 10k) อัปเดตได้ตลอดเวลาโดยไม่สูญเสียการอัปเดตก่อนหน้าทั้งหมด ...
a1ex07

2
@swasheck Full-text เป็นความคิดที่ดีที่จะพิจารณา (เป็นสิ่งใหม่ในปี 2005 ฉันเชื่อว่าดังนั้นจึงสามารถใช้งานได้ที่นี่) แต่มันเป็นไปไม่ได้ที่จะให้ฟังก์ชั่นแบบเดียวกันกับที่โปสเตอร์ขอมาตั้งแต่ดัชนีข้อความแบบเต็ม สตริงย่อยโดยพลการ กล่าวอีกวิธีหนึ่งข้อความแบบเต็มจะไม่พบการจับคู่สำหรับ "มด" ภายในคำว่า "มหัศจรรย์" แต่อาจเป็นไปได้ว่าข้อกำหนดทางธุรกิจสามารถปรับเปลี่ยนได้เพื่อให้สามารถใช้ข้อความแบบเต็มได้
Geoff Patterson

คำตอบ:


21

ในขณะที่ฉันเห็นด้วยกับผู้แสดงความคิดเห็นคนอื่นว่านี่เป็นปัญหาที่มีราคาแพง แต่ฉันคิดว่ามีพื้นที่สำหรับการปรับปรุงมากมายโดยการปรับแต่ง SQL ที่คุณใช้อยู่ เพื่อแสดงให้เห็นว่าฉันสร้างชุดข้อมูลปลอมที่มีชื่อ 15 มม. และวลี 3K วิ่งตามแนวทางเก่าและวิ่งเข้าหาแนวทางใหม่

สคริปต์แบบเต็มเพื่อสร้างชุดข้อมูลปลอมและลองใช้วิธีการใหม่

TL; DR

บนเครื่องของฉันและชุดข้อมูลปลอมนี้วิธีดั้งเดิมใช้เวลาประมาณ 4 ชั่วโมงในการทำงาน วิธีการใหม่ที่นำเสนอนี้ใช้เวลาประมาณ 10 นาทีซึ่งเป็นการปรับปรุงที่สำคัญ นี่เป็นบทสรุปโดยย่อของวิธีการที่นำเสนอ:

  • สำหรับแต่ละชื่อสร้างซับสตริงเริ่มต้นที่แต่ละอักขระออฟเซ็ต (และต่อยอดที่ความยาวของวลีที่ไม่ดีที่สุดที่ยาวที่สุดเป็นการปรับให้เหมาะสม)
  • สร้างดัชนีคลัสเตอร์บนสตริงย่อยเหล่านี้
  • สำหรับวลีที่ไม่ดีแต่ละข้อให้ค้นหาในสตริงย่อยเหล่านี้เพื่อระบุการจับคู่ใด ๆ
  • สำหรับแต่ละสตริงต้นฉบับให้คำนวณจำนวนวลีที่ไม่เหมาะสมที่ตรงกับสตริงย่อยหนึ่งรายการหรือมากกว่านั้น


วิธีการดั้งเดิม: การวิเคราะห์อัลกอริทึม

จากแผนของUPDATEคำแถลงเดิมเราจะเห็นได้ว่าปริมาณงานเป็นสัดส่วนเชิงเส้นตรงทั้งจำนวนชื่อ (15 มม.) และจำนวนวลี (3K) ดังนั้นถ้าเราคูณทั้งจำนวนชื่อและวลีด้วย 10 เวลารันไทม์โดยรวมจะช้ากว่า ~ 100 เท่า

แบบสอบถามเป็นสัดส่วนจริงกับความยาวของnameเช่นกัน ในขณะที่นี่เป็นบิตที่ซ่อนอยู่ในแผนแบบสอบถามมันมาใน "จำนวนการประหารชีวิต" สำหรับการค้นหาลงในตารางเก็บพัก ในแผนจริงเราจะเห็นว่านี้เกิดขึ้นไม่ได้เป็นเพียงหนึ่งครั้งต่อหนึ่งแต่จริงๆแล้วหนึ่งครั้งต่อตัวละครชดเชยภายในname nameดังนั้นวิธีนี้คือ O ( # names* # phrases* name length) ในความซับซ้อนของเวลาทำงาน

ป้อนคำอธิบายรูปภาพที่นี่


วิธีการใหม่: รหัส

รหัสนี้ยังมีอยู่ในpastebinแบบเต็มแต่ฉันได้คัดลอกไว้ที่นี่เพื่อความสะดวก Pastebin ยังมีคำจำกัดความขั้นตอนแบบเต็มซึ่งรวมถึง@minIdและ@maxIdตัวแปรที่คุณเห็นด้านล่างเพื่อกำหนดขอบเขตของชุดปัจจุบัน

-- 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


วิธีการใหม่: แผนแบบสอบถาม

ก่อนอื่นเราสร้างซับสตริงเริ่มต้นที่ตัวละครแต่ละตัว

ป้อนคำอธิบายรูปภาพที่นี่

จากนั้นสร้างดัชนีคลัสเตอร์บนสตริงย่อยเหล่านี้

ป้อนคำอธิบายรูปภาพที่นี่

ทีนี้สำหรับวลีที่ไม่ดีแต่ละครั้งที่เราค้นหาในสตริงย่อยเหล่านี้เพื่อระบุการแข่งขันใด ๆ จากนั้นเราจะคำนวณจำนวนวลีที่ไม่เหมาะสมที่ตรงกับสตริงย่อยหนึ่งรายการหรือมากกว่านั้น นี่เป็นขั้นตอนสำคัญจริงๆ เนื่องจากวิธีการที่เราจัดทำดัชนีสารตั้งต้นเราไม่จำเป็นต้องตรวจสอบผลิตภัณฑ์ที่มีวลีและชื่อที่ไม่เหมาะสมทั้งหมด ขั้นตอนนี้ซึ่งทำการคำนวณจริงคิดเป็นเพียงประมาณ 10% ของเวลาทำงานจริง (ที่เหลือคือการประมวลผลล่วงหน้าของสตริงย่อย)

ป้อนคำอธิบายรูปภาพที่นี่

ท้ายสุดให้ดำเนินการตามคำสั่งการอัพเดทจริงโดยใช้ a LEFT OUTER JOINเพื่อกำหนดจำนวน 0 ให้กับชื่อใด ๆ ที่เราไม่พบวลีที่ไม่ดี

ป้อนคำอธิบายรูปภาพที่นี่


วิธีการใหม่: การวิเคราะห์อัลกอริทึม

วิธีการใหม่สามารถแบ่งออกเป็นสองขั้นตอนก่อนการประมวลผลและการจับคู่ ลองกำหนดตัวแปรต่อไปนี้:

  • N = # ของชื่อ
  • B = # ของวลีที่ไม่ดี
  • L = ความยาวชื่อเฉลี่ยเป็นตัวอักษร

ขั้นตอนการประมวลผลล่วงหน้าคือO(N*L * LOG(N*L))การสร้างN*Lสตริงย่อยจากนั้นเรียงลำดับ

การจับคู่ที่แท้จริงคือO(B * LOG(N*L))การค้นหาวัสดุพิมพ์สำหรับแต่ละวลีที่ไม่ดี

ด้วยวิธีนี้เราได้สร้างอัลกอริทึมที่ไม่ได้ปรับขนาดเชิงเส้นตามจำนวนวลีที่ไม่ดีปลดล็อคประสิทธิภาพที่สำคัญเมื่อเราปรับขนาดเป็น 3K วลีและอื่น ๆ กล่าวอีกวิธีหนึ่งการใช้งานดั้งเดิมนั้นจะใช้เวลาประมาณ 10 เท่าหากเราเปลี่ยนจากวลีที่ไม่ดี 300 วลีเป็นวลีไม่ดี 3K ในทำนองเดียวกันมันจะใช้เวลา 10 เท่าอีกต่อไปหากเราต้องเปลี่ยนจากวลี 3K ไปเป็น 30K อย่างไรก็ตามการใช้งานใหม่จะเพิ่มขนาดย่อยเชิงเส้นและในความเป็นจริงนั้นใช้เวลาน้อยกว่า 2x ในการวัดวลีที่ไม่ดี 3K เมื่อปรับขนาดวลีที่ไม่ดี 30K


สมมติฐาน / Caveats

  • ฉันแบ่งงานโดยรวมออกเป็นแบทช์ที่มีขนาดพอประมาณ นี่อาจเป็นความคิดที่ดีสำหรับแนวทางใดวิธีหนึ่ง แต่เป็นสิ่งสำคัญอย่างยิ่งสำหรับวิธีการใหม่เพื่อให้การตั้งค่าSORTบนวัสดุพิมพ์แยกกันสำหรับแต่ละชุดและง่ายในหน่วยความจำ คุณสามารถจัดการขนาดแบทช์ได้ตามต้องการ แต่ไม่ควรลองแถว 15 มม. ทั้งหมดในแบทช์เดียว
  • ฉันใช้ SQL 2014 ไม่ใช่ SQL 2005 เนื่องจากฉันไม่มีสิทธิ์เข้าถึงเครื่อง SQL 2005 ฉันระมัดระวังที่จะไม่ใช้ไวยากรณ์ใด ๆ ที่ไม่พร้อมใช้งานใน SQL 2005 แต่ฉันยังอาจได้รับประโยชน์จากคุณลักษณะการเขียนแบบช้าของ tempdbใน SQL 2012+ และคุณลักษณะSELECT INTO แบบขนานใน SQL 2014
  • ความยาวของทั้งชื่อและวลีนั้นมีความสำคัญต่อวิธีการใหม่ ฉันสมมติว่าวลีที่ไม่ดีมักจะค่อนข้างสั้นเนื่องจากน่าจะตรงกับกรณีการใช้งานจริง ชื่อมีความยาวมากกว่าวลีที่ไม่ดี แต่ค่อนข้างจะสันนิษฐานว่าไม่ใช่ตัวละครนับพัน ฉันคิดว่านี่เป็นสมมติฐานที่ยุติธรรมและการตั้งชื่อให้นานขึ้นจะทำให้วิธีการเดิมของคุณช้าลงเช่นกัน
  • การปรับปรุงบางส่วน (แต่ไม่มีที่ใกล้เคียงทั้งหมด) เนื่องจากความจริงที่ว่าวิธีการใหม่สามารถยกระดับความขนานได้อย่างมีประสิทธิภาพมากกว่าวิธีการแบบเก่า (ซึ่งทำงานแบบเธรดเดียว) ฉันใช้แล็ปท็อปแบบ quad core ดังนั้นจึงเป็นเรื่องดีที่มีวิธีการที่สามารถทำให้แกนประมวลผลเหล่านี้ใช้งานได้


โพสต์บล็อกที่เกี่ยวข้อง

แอรอนเบอร์ทรานด์สำรวจประเภทของการแก้ปัญหานี้ในรายละเอียดมากขึ้นในการโพสต์บล็อกของเขาวิธีการหนึ่งที่จะได้รับดัชนีแสวงหาตัวแทนชั้นนำ%


6

ลองระงับปัญหาที่ชัดเจนที่Aaron Bertrand นำเสนอในความคิดเห็นเป็นครั้งที่สอง:

ดังนั้นคุณกำลังสแกนตาราง 3K ครั้งและอาจอัพเดตแถว 15MM ทั้งหมด 3K ครั้งและคุณคาดหวังว่ามันจะเร็วหรือไม่

ความจริงที่ว่าแบบสอบถามย่อยของคุณใช้บัตรเสริมทั้งสองด้านอย่างมากส่งผลกระทบต่อ sargability วิธีอ้างจากบล็อกโพสต์:

นั่นหมายความว่า SQL Server จะต้องอ่านทุกแถวออกจากตารางผลิตภัณฑ์ตรวจสอบเพื่อดูว่ามันมี "nut" ที่ใดก็ได้ในชื่อแล้วส่งกลับผลลัพธ์ของเรา

สลับคำว่า "nut" สำหรับ "bad word" และ "Product" สำหรับแต่ละคำSourceTableจากนั้นรวมเข้ากับความคิดเห็นของ Aaron และคุณควรเริ่มดูว่าทำไมมันยากมาก (อ่านไม่ได้) เพื่อให้ทำงานได้อย่างรวดเร็วโดยใช้อัลกอริทึมปัจจุบันของคุณ

ฉันเห็นตัวเลือกน้อย:

  1. โน้มน้าวใจทางธุรกิจให้ซื้อเซิร์ฟเวอร์มอนสเตอร์ที่มีพลังมากพอที่จะเอาชนะการสืบค้นด้วยการใช้กำลังอันโหดร้าย (นั่นจะไม่เกิดขึ้นเพื่อให้ตัวเลือกอื่น ๆ ดีกว่า)
  2. ใช้อัลกอริทึมที่มีอยู่ของคุณยอมรับความเจ็บปวดหนึ่งครั้งแล้วกระจายออกไป สิ่งนี้จะเกี่ยวข้องกับการคำนวณคำที่ไม่ดีในส่วนแทรกซึ่งจะทำให้เม็ดมีดช้าลงและอัปเดตทั้งตารางเมื่อมีการป้อน / ค้นคำค้นใหม่
  3. กอดคำตอบของเจฟฟ์ นี่เป็นอัลกอริธึมที่ยอดเยี่ยมและดีกว่าทุกอย่างที่ฉันคิด
  4. ทำตัวเลือก 2 แต่แทนที่อัลกอริทึมของคุณด้วย Geoff's

ขึ้นอยู่กับความต้องการของคุณฉันจะแนะนำตัวเลือก 3 หรือ 4


0

ครั้งแรกที่เป็นเพียงการปรับปรุงที่แปลก

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] [PHRASE] กำลังฆ่าคุณ
ที่ไม่สามารถใช้ดัชนีได้

การออกแบบข้อมูลไม่เหมาะสำหรับความเร็ว
คุณสามารถแบ่ง [Bad_Phrase] [PHRASE] เป็นวลี / คำเดียวได้หรือไม่
หากวลี / คำเดียวกันปรากฏมากกว่าหนึ่งคุณสามารถป้อนได้มากกว่าหนึ่งครั้งถ้าคุณต้องการให้มีจำนวนที่สูงกว่า
ดังนั้นจำนวนแถวใน pharase ที่ไม่ดีจะเพิ่มขึ้น
หากคุณสามารถทำได้สิ่งนี้จะเร็วขึ้นมาก

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 รองรับหรือไม่ แต่เป็นดัชนีข้อความแบบเต็มและใช้ประกอบด้วย


1
ฉันไม่คิดว่า OP ต้องการนับอินสแตนซ์ของคำที่ไม่ดีในตารางคำที่ไม่ดีฉันคิดว่าพวกเขาต้องการนับจำนวนคำที่ซ่อนอยู่ในตารางต้นฉบับ ตัวอย่างเช่นรหัสต้นฉบับอาจให้นับ 2 สำหรับชื่อ "shitass" แต่รหัสของคุณจะนับเป็น 0
Erik

1
@Erik "คุณแยก [Bad_Phrase] ออกได้ [PHRASE] เป็นวลีเดียวหรือไม่?" จริง ๆ แล้วคุณไม่คิดว่าการออกแบบข้อมูลอาจเป็นการแก้ไขหรือไม่ หากวัตถุประสงค์คือการค้นหาสิ่งที่ไม่ดีแล้ว "eriK" ที่มีการนับหนึ่งหรือมากกว่านั้นก็เพียงพอแล้ว
paparazzo
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.