ผลลัพธ์การค้นหาและปัญหาด้านประสิทธิภาพ

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

ผลลัพธ์การค้นหาและปัญหาด้านประสิทธิภาพ

ตัวเลือกเพจ #1

ตัวเลือกที่ง่ายที่สุดที่นึกถึงคือการแสดงผลการค้นหาแบบหน้าต่อหน้าในรูปแบบคลาสสิกที่สุด

ผลลัพธ์การค้นหาและปัญหาด้านประสิทธิภาพ
สมมติว่าแอปพลิเคชันของคุณใช้ฐานข้อมูลเชิงสัมพันธ์ ในกรณีนี้ หากต้องการแสดงข้อมูลในแบบฟอร์มนี้ คุณจะต้องเรียกใช้คำสั่ง SQL สองตัว:

  • รับแถวสำหรับหน้าปัจจุบัน
  • คำนวณจำนวนบรรทัดทั้งหมดที่สอดคล้องกับเกณฑ์การค้นหาซึ่งจำเป็นสำหรับการแสดงหน้า

ลองดูที่แบบสอบถามแรกโดยใช้ฐานข้อมูล MS SQL ทดสอบเป็นตัวอย่าง แอดเวนเจอร์เวิร์คส์ สำหรับเซิร์ฟเวอร์ปี 2016 เพื่อจุดประสงค์นี้ เราจะใช้ตาราง Sales.SalesOrderHeader:

SELECT * FROM Sales.SalesOrderHeader
ORDER BY OrderDate DESC
OFFSET 0 ROWS
FETCH NEXT 50 ROWS ONLY

ข้อความค้นหาข้างต้นจะส่งกลับคำสั่งซื้อ 50 คำสั่งแรกจากรายการ โดยจัดเรียงตามวันที่เพิ่มจากมากไปหาน้อย หรืออีกนัยหนึ่งคือ 50 คำสั่งซื้อล่าสุด

มันทำงานอย่างรวดเร็วบนฐานการทดสอบ แต่ลองดูแผนการดำเนินการและสถิติ I/O กัน:

ผลลัพธ์การค้นหาและปัญหาด้านประสิทธิภาพ

Table 'SalesOrderHeader'. Scan count 1, logical reads 698, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

คุณสามารถรับสถิติ I/O สำหรับแต่ละเคียวรีได้โดยการรันคำสั่ง SET STATISTICS IO ON ในรันไทม์ของเคียวรี

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

ผลลัพธ์การค้นหาและปัญหาด้านประสิทธิภาพ

Table 'SalesOrderHeader'. Scan count 1, logical reads 165, physical reads 0, read-ahead reads 5, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

แน่นอนว่ามันดีขึ้นมากแล้ว แต่ปัญหาทั้งหมดได้รับการแก้ไขแล้วหรือยัง? มาเปลี่ยนคำค้นหาเพื่อค้นหาคำสั่งซื้อที่ต้นทุนรวมของสินค้าเกิน $100:

SELECT * FROM Sales.SalesOrderHeader
WHERE SubTotal > 100
ORDER BY OrderDate DESC
OFFSET 0 ROWS
FETCH NEXT 50 ROWS ONLY

ผลลัพธ์การค้นหาและปัญหาด้านประสิทธิภาพ

Table 'SalesOrderHeader'. Scan count 1, logical reads 1081, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

เรามีสถานการณ์ที่ตลก: แผนการสืบค้นไม่ได้แย่ไปกว่าแผนก่อนหน้ามากนัก แต่จำนวนการอ่านเชิงตรรกะจริงนั้นมากกว่าการสแกนตารางแบบเต็มเกือบสองเท่า มีทางออก - หากเราสร้างดัชนีคอมโพสิตจากดัชนีที่มีอยู่แล้วและเพิ่มราคารวมของสินค้าเป็นฟิลด์ที่สอง เราจะได้รับการอ่านแบบลอจิคัล 165 รายการอีกครั้ง:

CREATE INDEX IX_SalesOrderHeader_OrderDate_SubTotal on Sales.SalesOrderHeader(OrderDate, SubTotal);

ตัวอย่างชุดนี้สามารถดำเนินต่อไปได้เป็นเวลานาน แต่ความคิดหลักสองประการที่ฉันต้องการแสดงที่นี่คือ:

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

ตอนนี้เรามาดูคำค้นหาที่สองที่กล่าวถึงในตอนต้นซึ่งเป็นคำที่นับจำนวนระเบียนที่ตรงตามเกณฑ์การค้นหา ลองใช้ตัวอย่างเดียวกัน - การค้นหาคำสั่งซื้อที่มีมูลค่ามากกว่า $100:

SELECT COUNT(1) FROM Sales.SalesOrderHeader
WHERE SubTotal > 100

จากดัชนีคอมโพสิตที่ระบุข้างต้น เราได้รับ:

ผลลัพธ์การค้นหาและปัญหาด้านประสิทธิภาพ

Table 'SalesOrderHeader'. Scan count 1, logical reads 698, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

ความจริงที่ว่าการสืบค้นผ่านดัชนีทั้งหมดนั้นไม่ใช่เรื่องน่าแปลกใจ เนื่องจากเขตข้อมูล SubTotal ไม่อยู่ในตำแหน่งแรก ดังนั้นการสืบค้นจึงไม่สามารถใช้ได้ ปัญหาได้รับการแก้ไขโดยการเพิ่มดัชนีอื่นลงในฟิลด์ SubTotal และด้วยเหตุนี้จึงให้การอ่านแบบลอจิคัลเพียง 48 รายการเท่านั้น

คุณสามารถยกตัวอย่างคำขอเพิ่มเติมสำหรับการนับจำนวนได้ แต่สาระสำคัญยังคงเหมือนเดิม: การรับข้อมูลและการนับจำนวนทั้งหมดเป็นคำขอสองคำขอที่แตกต่างกันโดยพื้นฐานและแต่ละรายการต้องมีมาตรการของตนเองเพื่อการเพิ่มประสิทธิภาพ โดยทั่วไป คุณจะไม่สามารถค้นหาชุดดัชนีที่ทำงานได้ดีพอๆ กันสำหรับทั้งสองข้อความค้นหา

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

ตัวเลือกเพจ #2

สมมติว่าผู้ใช้ไม่สนใจที่จะรู้จำนวนอ็อบเจ็กต์ทั้งหมดที่พบ เรามาลองทำให้หน้าค้นหาง่ายขึ้น:

ผลลัพธ์การค้นหาและปัญหาด้านประสิทธิภาพ
ที่จริงแล้ว สิ่งเดียวที่เปลี่ยนแปลงคือไม่มีวิธีนำทางไปยังหมายเลขหน้าใดหน้าหนึ่ง และตอนนี้ตารางนี้ไม่จำเป็นต้องรู้ว่ามีกี่หน้าจึงจะแสดงได้ แต่คำถามเกิดขึ้น - ตารางจะรู้ได้อย่างไรว่ามีข้อมูลสำหรับหน้าถัดไปหรือไม่ (เพื่อแสดงลิงก์ "ถัดไป" อย่างถูกต้อง)

คำตอบนั้นง่ายมาก: คุณสามารถอ่านจากฐานข้อมูลได้มากกว่าที่จำเป็นสำหรับการแสดงผลหนึ่งรายการและการมีอยู่ของบันทึก "เพิ่มเติม" นี้จะแสดงว่ามีส่วนถัดไปหรือไม่ ด้วยวิธีนี้ คุณจะต้องเรียกใช้คำขอเดียวเพื่อรับข้อมูลหนึ่งหน้า ซึ่งช่วยปรับปรุงประสิทธิภาพได้อย่างมาก และทำให้รองรับฟังก์ชันการทำงานดังกล่าวได้ง่ายขึ้น ในทางปฏิบัติของฉัน มีกรณีที่ปฏิเสธที่จะนับจำนวนบันทึกทั้งหมดทำให้การส่งมอบผลลัพธ์เร็วขึ้น 4-5 เท่า

มีตัวเลือกอินเทอร์เฟซผู้ใช้หลายแบบสำหรับแนวทางนี้: คำสั่ง "ย้อนกลับ" และ "ไปข้างหน้า" ดังตัวอย่างด้านบน ปุ่ม "โหลดเพิ่มเติม" ซึ่งเพิ่มส่วนใหม่ให้กับผลลัพธ์ที่แสดง "การเลื่อนแบบไม่มีที่สิ้นสุด" ซึ่งใช้งานได้ บนหลักการ "โหลดเพิ่ม" " แต่สัญญาณที่จะได้รับส่วนถัดไปคือให้ผู้ใช้เลื่อนผลลัพธ์ที่แสดงทั้งหมดไปจนสุด ไม่ว่าโซลูชันด้านภาพจะเป็นอย่างไร หลักการสุ่มตัวอย่างข้อมูลยังคงเหมือนเดิม

ความแตกต่างของการใช้เพจ

ตัวอย่างเคียวรีทั้งหมดที่ให้ไว้ข้างต้นใช้วิธี "offset + count" เมื่อเคียวรีระบุลำดับแถวผลลัพธ์และจำนวนแถวที่ต้องส่งคืน อันดับแรก มาดูวิธีที่ดีที่สุดในการจัดระเบียบการส่งผ่านพารามิเตอร์ในกรณีนี้ ในทางปฏิบัติ ฉันพบหลายวิธี:

  • หมายเลขซีเรียลของหน้าที่ร้องขอ (pageIndex) ขนาดหน้า (pageSize)
  • หมายเลขซีเรียลของเรกคอร์ดแรกที่จะส่งคืน (startIndex) จำนวนเรกคอร์ดสูงสุดในผลลัพธ์ (จำนวน)
  • หมายเลขลำดับของเรกคอร์ดแรกที่จะส่งคืน (startIndex) หมายเลขลำดับของเรกคอร์ดสุดท้ายที่จะส่งคืน (endIndex)

เมื่อมองแวบแรกอาจดูเหมือนว่านี่เป็นเรื่องพื้นฐานจนไม่มีความแตกต่าง แต่ไม่เป็นเช่นนั้น - ตัวเลือกที่สะดวกและเป็นสากลที่สุดคือตัวเลือกที่สอง (startIndex, count) มีหลายสาเหตุนี้:

  • สำหรับวิธีการพิสูจน์อักษรรายการ +1 ที่ระบุข้างต้น ตัวเลือกแรกที่มี pageIndex และ pageSize นั้นไม่สะดวกอย่างยิ่ง ตัวอย่างเช่น เราต้องการแสดง 50 โพสต์ต่อหน้า ตามอัลกอริธึมข้างต้น คุณต้องอ่านหนึ่งบันทึกเกินความจำเป็น หากไม่ได้ใช้ "+1" นี้บนเซิร์ฟเวอร์ปรากฎว่าสำหรับหน้าแรกเราต้องขอบันทึกตั้งแต่ 1 ถึง 51 สำหรับหน้าที่สอง - จาก 51 ถึง 101 เป็นต้น หากคุณระบุขนาดหน้าเป็น 51 และเพิ่ม pageIndex หน้าที่สองจะกลับมาจาก 52 เป็น 102 เป็นต้น ดังนั้น ในตัวเลือกแรก วิธีเดียวที่จะใช้ปุ่มอย่างถูกต้องเพื่อไปยังหน้าถัดไปคือการให้เซิร์ฟเวอร์ตรวจทานบรรทัด "พิเศษ" ซึ่งจะมีความแตกต่างกันโดยนัยอย่างมาก
  • ตัวเลือกที่สามไม่สมเหตุสมผลเลย เนื่องจากในการเรียกใช้คิวรีในฐานข้อมูลส่วนใหญ่ คุณจะต้องผ่านการนับมากกว่าดัชนีของบันทึกล่าสุด การลบ startIndex จาก endIndex อาจเป็นการดำเนินการทางคณิตศาสตร์อย่างง่าย แต่ที่นี่จะไม่จำเป็น

ตอนนี้เราควรอธิบายข้อเสียของการใช้เพจผ่าน "offset + quantity":

  • การเรียกแต่ละหน้าที่ตามมาจะมีราคาแพงและช้ากว่าหน้าก่อนหน้า เนื่องจากฐานข้อมูลยังคงต้องผ่านบันทึกทั้งหมด "ตั้งแต่ต้น" ตามเกณฑ์การค้นหาและการเรียงลำดับแล้วหยุดที่ส่วนที่ต้องการ
  • ไม่ใช่ทุก DBMS ที่สามารถรองรับแนวทางนี้ได้

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

SELECT * FROM Sales.SalesOrderHeader
ORDER BY OrderDate DESC
OFFSET 0 ROWS
FETCH NEXT 50 ROWS ONLY

และในบันทึกล่าสุด เราได้ค่าของวันที่สั่งซื้อ '2014-06-29' จากนั้นเพื่อให้ได้หน้าถัดไป คุณสามารถลองทำสิ่งนี้ได้:

SELECT * FROM Sales.SalesOrderHeader
WHERE OrderDate < '2014-06-29'
ORDER BY OrderDate DESC
OFFSET 0 ROWS
FETCH NEXT 50 ROWS ONLY

ปัญหาคือ OrderDate เป็นฟิลด์ที่ซ้ำกัน และเงื่อนไขที่ระบุข้างต้นมีแนวโน้มที่จะพลาดแถวที่จำเป็นจำนวนมาก หากต้องการเพิ่มความคลุมเครือให้กับแบบสอบถามนี้ คุณต้องเพิ่มฟิลด์เฉพาะให้กับเงื่อนไข (สมมติว่า 75074 เป็นค่าสุดท้ายของคีย์หลักจากส่วนแรก):

SELECT * FROM Sales.SalesOrderHeader
WHERE (OrderDate = '2014-06-29' AND SalesOrderID < 75074)
   OR (OrderDate < '2014-06-29')
ORDER BY OrderDate DESC, SalesOrderID DESC
OFFSET 0 ROWS
FETCH NEXT 50 ROWS ONLY

ตัวเลือกนี้จะทำงานได้อย่างถูกต้อง แต่โดยทั่วไปการปรับให้เหมาะสมจะเป็นเรื่องยากเนื่องจากเงื่อนไขมีตัวดำเนินการ OR หากค่าของคีย์หลักเพิ่มขึ้นเมื่อ OrderDate เพิ่มขึ้น เงื่อนไขจะง่ายขึ้นโดยเหลือเพียงตัวกรองตาม SalesOrderID แต่หากไม่มีความสัมพันธ์ที่เข้มงวดระหว่างค่าของคีย์หลักและฟิลด์ที่ใช้เรียงลำดับผลลัพธ์ OR นี้จะไม่สามารถหลีกเลี่ยงได้ใน DBMS ส่วนใหญ่ ข้อยกเว้นที่ฉันทราบคือ PostgreSQL ซึ่งรองรับการเปรียบเทียบทูเพิลอย่างสมบูรณ์ และเงื่อนไขข้างต้นสามารถเขียนเป็น "WHERE (OrderDate, SalesOrderID) < ('2014-06-29', 75074)" เมื่อกำหนดคีย์ผสมที่มีสองฟิลด์นี้ การสืบค้นเช่นนี้น่าจะค่อนข้างง่าย

แนวทางทางเลือกที่สองสามารถพบได้ เช่น ใน API เลื่อน ElasticSearch หรือ คอสมอส DB — เมื่อมีการร้องขอ นอกเหนือจากข้อมูลแล้ว ยังส่งคืนตัวระบุพิเศษซึ่งคุณสามารถรับข้อมูลส่วนถัดไปได้ หากตัวระบุนี้มีอายุการใช้งานไม่จำกัด (เช่นใน Comsos DB) นี่เป็นวิธีที่ดีเยี่ยมในการใช้เพจพร้อมการเปลี่ยนตามลำดับระหว่างเพจ (ตัวเลือก #2 ที่กล่าวถึงข้างต้น) ข้อเสียที่เป็นไปได้: ไม่รองรับ DBMS ทั้งหมด ตัวระบุส่วนถัดไปที่เป็นผลลัพธ์อาจมีอายุการใช้งานที่จำกัด ซึ่งโดยทั่วไปไม่เหมาะสำหรับการใช้งานการโต้ตอบของผู้ใช้ (เช่น ElasticSearch scroll API)

การกรองที่ซับซ้อน

มาทำให้งานซับซ้อนยิ่งขึ้น สมมติว่ามีข้อกำหนดในการใช้สิ่งที่เรียกว่าการค้นหาแบบ faceted ซึ่งทุกคนจากร้านค้าออนไลน์คุ้นเคยเป็นอย่างดี ตัวอย่างข้างต้นตามตารางคำสั่งซื้อไม่ค่อยมีภาพประกอบในกรณีนี้ ดังนั้น เรามาสลับไปใช้ตารางผลิตภัณฑ์จากฐานข้อมูล AdventureWorks กัน:

ผลลัพธ์การค้นหาและปัญหาด้านประสิทธิภาพ
แนวคิดเบื้องหลังการค้นหาแบบประกอบคืออะไร ความจริงก็คือสำหรับแต่ละองค์ประกอบตัวกรองจะแสดงจำนวนบันทึกที่ตรงตามเกณฑ์นี้ โดยคำนึงถึงตัวกรองที่เลือกในหมวดหมู่อื่นๆ ทั้งหมด.

ตัวอย่างเช่น หากเราเลือกหมวดหมู่จักรยานและสีดำในตัวอย่างนี้ ตารางจะแสดงเฉพาะจักรยานสีดำ แต่:

  • สำหรับแต่ละเกณฑ์ในกลุ่มหมวดหมู่ จำนวนผลิตภัณฑ์จากหมวดหมู่นั้นจะแสดงเป็นสีดำ
  • สำหรับแต่ละเกณฑ์ของกลุ่ม "สี" จะแสดงจำนวนจักรยานที่มีสีนี้

นี่คือตัวอย่างผลลัพธ์ผลลัพธ์สำหรับเงื่อนไขดังกล่าว:

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

ทีนี้ลองจินตนาการว่าสิ่งนี้สามารถนำไปใช้บนพื้นฐานเชิงสัมพันธ์ได้อย่างไร เกณฑ์แต่ละกลุ่ม เช่น หมวดหมู่และสี จะต้องมีการสืบค้นแยกกัน:

SELECT pc.ProductCategoryID, pc.Name, COUNT(1) FROM Production.Product p
  INNER JOIN Production.ProductSubcategory ps ON p.ProductSubcategoryID = ps.ProductSubcategoryID
  INNER JOIN Production.ProductCategory pc ON ps.ProductCategoryID = pc.ProductCategoryID
WHERE p.Color = 'Black'
GROUP BY pc.ProductCategoryID, pc.Name
ORDER BY COUNT(1) DESC

ผลลัพธ์การค้นหาและปัญหาด้านประสิทธิภาพ

SELECT Color, COUNT(1) FROM Production.Product p
  INNER JOIN Production.ProductSubcategory ps ON p.ProductSubcategoryID = ps.ProductSubcategoryID
WHERE ps.ProductCategoryID = 1 --Bikes
GROUP BY Color
ORDER BY COUNT(1) DESC

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

โดยปกติหลังจากข้อความเหล่านี้ ฉันจะได้รับวิธีแก้ปัญหาบางอย่าง กล่าวคือ:

  • รวมจำนวนทั้งหมดไว้ในแบบสอบถามเดียว ในทางเทคนิคแล้ว สิ่งนี้เป็นไปได้โดยใช้คีย์เวิร์ด UNION แต่จะไม่ช่วยประสิทธิภาพมากนัก - ฐานข้อมูลยังคงต้องดำเนินการแต่ละแฟรกเมนต์ตั้งแต่เริ่มต้น
  • ปริมาณแคช สิ่งนี้แนะนำให้ฉันเกือบทุกครั้งที่ฉันอธิบายปัญหา ข้อแม้ก็คือว่าโดยทั่วไปแล้วสิ่งนี้เป็นไปไม่ได้ สมมติว่าเรามี 10 "แง่มุม" ซึ่งแต่ละค่ามี 5 ค่า นี่เป็นสถานการณ์ที่ "เรียบง่าย" มากเมื่อเทียบกับสิ่งที่สามารถเห็นได้ในร้านค้าออนไลน์เดียวกัน การเลือกองค์ประกอบด้านหนึ่งจะส่งผลต่อปริมาณในอีก 9 องค์ประกอบ กล่าวคือ ปริมาณอาจแตกต่างกันสำหรับเกณฑ์แต่ละชุดรวมกัน ในตัวอย่างของเรา มีเกณฑ์ทั้งหมด 50 เกณฑ์ที่ผู้ใช้สามารถเลือกได้ ดังนั้น จะมีชุดค่าผสมที่เป็นไปได้ 250 รายการ มีหน่วยความจำหรือเวลาไม่เพียงพอที่จะเติมอาร์เรย์ของข้อมูลดังกล่าว ที่นี่คุณสามารถคัดค้านและบอกว่าไม่ใช่ว่าชุดค่าผสมทั้งหมดจะเป็นของจริง และผู้ใช้มักเลือกเกณฑ์มากกว่า 5-10 รายการ ใช่ เป็นไปได้ที่จะทำการโหลดแบบ Lazy Loading และแคชในปริมาณเฉพาะที่เคยเลือกไว้ แต่ยิ่งมีตัวเลือกมากขึ้น แคชดังกล่าวก็จะยิ่งมีประสิทธิภาพน้อยลง และปัญหาเวลาตอบสนองก็จะยิ่งสังเกตเห็นได้ชัดเจนมากขึ้น (โดยเฉพาะอย่างยิ่งหาก ชุดข้อมูลมีการเปลี่ยนแปลงสม่ำเสมอ)

โชคดีที่ปัญหาดังกล่าวมีวิธีแก้ปัญหาที่มีประสิทธิภาพมายาวนานซึ่งสามารถคาดเดาได้กับข้อมูลปริมาณมาก สำหรับตัวเลือกใด ๆ เหล่านี้ การแบ่งการคำนวณ facets ใหม่และรับหน้าผลลัพธ์เป็นการเรียกเซิร์ฟเวอร์แบบขนานสองครั้ง และจัดระเบียบอินเทอร์เฟซผู้ใช้ในลักษณะที่การโหลดข้อมูลตาม facets "ไม่รบกวน" กับการแสดงผลของ ผลการค้นหา.

  • เรียกการคำนวณใหม่ของ "แง่มุม" ให้น้อยที่สุดเท่าที่จะทำได้ ตัวอย่างเช่น อย่าคำนวณทุกอย่างใหม่ทุกครั้งที่เกณฑ์การค้นหาเปลี่ยนแปลง แต่ให้ค้นหาจำนวนผลลัพธ์ทั้งหมดที่ตรงกับเงื่อนไขปัจจุบันและแจ้งให้ผู้ใช้แสดง - “พบบันทึก 1425 รายการ แสดงหรือไม่” ผู้ใช้สามารถเปลี่ยนคำค้นหาต่อหรือคลิกปุ่ม "แสดง" เฉพาะในกรณีที่สองเท่านั้นที่คำขอทั้งหมดเพื่อให้ได้ผลลัพธ์และการคำนวณปริมาณใหม่ใน "แง่มุม" ทั้งหมดจะถูกดำเนินการ ในกรณีนี้ อย่างที่คุณเห็นได้ง่าย คุณจะต้องจัดการกับคำขอเพื่อให้ได้ผลลัพธ์ทั้งหมดและการเพิ่มประสิทธิภาพ วิธีนี้สามารถพบได้ในร้านค้าออนไลน์ขนาดเล็กหลายแห่ง แน่นอนว่านี่ไม่ใช่ยาครอบจักรวาลสำหรับปัญหานี้ แต่ในกรณีง่ายๆ ก็สามารถประนีประนอมได้ดี
  • ใช้เครื่องมือค้นหาเพื่อค้นหาผลลัพธ์และนับแง่มุม เช่น Solr, ElasticSearch, Sphinx และอื่นๆ ทั้งหมดนี้ได้รับการออกแบบมาเพื่อสร้าง "แง่มุม" และทำสิ่งนี้ได้อย่างมีประสิทธิภาพเนื่องจากดัชนีกลับหัว วิธีการทำงานของเสิร์ชเอ็นจิ้นทำไมในกรณีเช่นนี้จึงมีประสิทธิภาพมากกว่าฐานข้อมูลทั่วไปมีแนวทางปฏิบัติและข้อผิดพลาดอะไรบ้าง - นี่คือหัวข้อสำหรับบทความแยกต่างหาก ที่นี่ฉันอยากจะดึงความสนใจของคุณไปที่ความจริงที่ว่าเครื่องมือค้นหาไม่สามารถทดแทนการจัดเก็บข้อมูลหลักได้ มันถูกใช้เป็นส่วนเพิ่มเติม: การเปลี่ยนแปลงใด ๆ ในฐานข้อมูลหลักที่เกี่ยวข้องกับการค้นหาจะถูกซิงโครไนซ์กับดัชนีการค้นหา เครื่องมือค้นหามักจะโต้ตอบกับเครื่องมือค้นหาเท่านั้นและไม่สามารถเข้าถึงฐานข้อมูลหลักได้ จุดที่สำคัญที่สุดประการหนึ่งคือวิธีจัดระเบียบการซิงโครไนซ์นี้อย่างน่าเชื่อถือ ทุกอย่างขึ้นอยู่กับข้อกำหนด "เวลาปฏิกิริยา" หากเวลาระหว่างการเปลี่ยนแปลงในฐานข้อมูลหลักและ "การปรากฏ" ในการค้นหาไม่สำคัญ คุณสามารถสร้างบริการที่ค้นหาบันทึกที่เปลี่ยนแปลงล่าสุดทุกๆ สองสามนาทีและจัดทำดัชนีบันทึกเหล่านั้น หากคุณต้องการเวลาตอบสนองที่สั้นที่สุดเท่าที่จะเป็นไปได้ คุณสามารถดำเนินการบางอย่างเช่น กล่องขาออกของธุรกรรม เพื่อส่งการอัปเดตไปยังบริการค้นหา

ผลการวิจัย

  1. การใช้เพจจิ้งฝั่งเซิร์ฟเวอร์เป็นความยุ่งยากที่สำคัญ และเหมาะสมสำหรับชุดข้อมูลที่เติบโตอย่างรวดเร็วหรือเพียงชุดข้อมูลขนาดใหญ่เท่านั้น ไม่มีสูตรตายตัวที่แน่นอนในการประเมินว่า "ใหญ่" หรือ "โตเร็ว" แต่ฉันจะปฏิบัติตามแนวทางนี้:
    • หากได้รับข้อมูลที่รวบรวมไว้ครบถ้วน โดยคำนึงถึงเวลาของเซิร์ฟเวอร์และการส่งผ่านเครือข่าย ซึ่งเป็นไปตามข้อกำหนดด้านประสิทธิภาพตามปกติ ก็ไม่มีประโยชน์ที่จะนำเพจจิ้งไปใช้งานบนฝั่งเซิร์ฟเวอร์
    • อาจมีสถานการณ์ที่คาดว่าจะไม่มีปัญหาด้านประสิทธิภาพในอนาคตอันใกล้ เนื่องจากมีข้อมูลน้อย แต่การรวบรวมข้อมูลมีการเติบโตอย่างต่อเนื่อง หากชุดข้อมูลบางชุดในอนาคตอาจไม่เป็นไปตามจุดก่อนหน้าอีกต่อไป ควรเริ่มเพจทันที
  2. หากไม่มีข้อกำหนดที่เข้มงวดในส่วนของธุรกิจในการแสดงจำนวนผลลัพธ์ทั้งหมดหรือแสดงหมายเลขหน้า และระบบของคุณไม่มีเครื่องมือค้นหา จะเป็นการดีกว่าที่จะไม่นำประเด็นเหล่านี้ไปใช้และพิจารณาตัวเลือก #2
  3. หากมีข้อกำหนดที่ชัดเจนสำหรับการค้นหาแบบประกอบ คุณมีสองตัวเลือกโดยไม่ต้องเสียสละประสิทธิภาพ:
    • อย่าคำนวณปริมาณทั้งหมดใหม่ทุกครั้งที่เกณฑ์การค้นหาเปลี่ยนแปลง
    • ใช้เครื่องมือค้นหาเช่น Solr, ElasticSearch, Sphinx และอื่นๆ แต่ควรเข้าใจว่าไม่สามารถทดแทนฐานข้อมูลหลักได้ และควรใช้เป็นส่วนเสริมจากที่เก็บข้อมูลหลักในการแก้ปัญหาการค้นหา
  4. นอกจากนี้ ในกรณีของการค้นหาแบบแยกส่วน ก็สมเหตุสมผลที่จะแบ่งการดึงข้อมูลหน้าผลการค้นหาและการนับเป็นสองคำขอคู่ขนาน การนับปริมาณอาจใช้เวลานานกว่าการได้ผลลัพธ์ ในขณะที่ผลลัพธ์มีความสำคัญต่อผู้ใช้มากกว่า
  5. หากคุณใช้ฐานข้อมูล SQL ในการค้นหา การเปลี่ยนแปลงโค้ดใดๆ ที่เกี่ยวข้องกับส่วนนี้ควรได้รับการทดสอบอย่างดีเกี่ยวกับประสิทธิภาพของข้อมูลในปริมาณที่เหมาะสม (เกินปริมาณในฐานข้อมูลสด) ขอแนะนำให้ใช้การตรวจสอบเวลาดำเนินการแบบสอบถามในทุกอินสแตนซ์ของฐานข้อมูล และโดยเฉพาะอย่างยิ่งในอินสแตนซ์ "สด" แม้ว่าทุกอย่างจะเรียบร้อยดีกับแผนการสืบค้นในขั้นตอนการพัฒนา แต่เมื่อปริมาณข้อมูลเพิ่มขึ้น สถานการณ์ก็อาจเปลี่ยนแปลงอย่างเห็นได้ชัด

ที่มา: will.com

เพิ่มความคิดเห็น