หนึ่งในสถานการณ์ทั่วไปในแอปพลิเคชันทั้งหมดที่เราคุ้นเคยคือการค้นหาข้อมูลตามเกณฑ์ที่กำหนดและแสดงในรูปแบบที่อ่านง่าย อาจมีตัวเลือกเพิ่มเติมสำหรับการเรียงลำดับ การจัดกลุ่ม และการแบ่งหน้า ตามทฤษฎีแล้วงานนี้เป็นเรื่องเล็กน้อย แต่เมื่อแก้ไขแล้ว นักพัฒนาหลายคนทำผิดพลาดหลายครั้ง ซึ่งต่อมาทำให้ประสิทธิภาพการทำงานต้องทนทุกข์ทรมานในภายหลัง ลองพิจารณาตัวเลือกต่าง ๆ ในการแก้ปัญหานี้และกำหนดคำแนะนำในการเลือกการใช้งานที่มีประสิทธิภาพสูงสุด
ตัวเลือกเพจ #1
ตัวเลือกที่ง่ายที่สุดที่นึกถึงคือการแสดงผลการค้นหาแบบหน้าต่อหน้าในรูปแบบคลาสสิกที่สุด
สมมติว่าแอปพลิเคชันของคุณใช้ฐานข้อมูลเชิงสัมพันธ์ ในกรณีนี้ หากต้องการแสดงข้อมูลในแบบฟอร์มนี้ คุณจะต้องเรียกใช้คำสั่ง SQL สองตัว:
- รับแถวสำหรับหน้าปัจจุบัน
- คำนวณจำนวนบรรทัดทั้งหมดที่สอดคล้องกับเกณฑ์การค้นหาซึ่งจำเป็นสำหรับการแสดงหน้า
ลองดูที่แบบสอบถามแรกโดยใช้ฐานข้อมูล MS SQL ทดสอบเป็นตัวอย่าง
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)" เมื่อกำหนดคีย์ผสมที่มีสองฟิลด์นี้ การสืบค้นเช่นนี้น่าจะค่อนข้างง่าย
แนวทางทางเลือกที่สองสามารถพบได้ เช่น ใน
การกรองที่ซับซ้อน
มาทำให้งานซับซ้อนยิ่งขึ้น สมมติว่ามีข้อกำหนดในการใช้สิ่งที่เรียกว่าการค้นหาแบบ 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 และอื่นๆ ทั้งหมดนี้ได้รับการออกแบบมาเพื่อสร้าง "แง่มุม" และทำสิ่งนี้ได้อย่างมีประสิทธิภาพเนื่องจากดัชนีกลับหัว วิธีการทำงานของเสิร์ชเอ็นจิ้นทำไมในกรณีเช่นนี้จึงมีประสิทธิภาพมากกว่าฐานข้อมูลทั่วไปมีแนวทางปฏิบัติและข้อผิดพลาดอะไรบ้าง - นี่คือหัวข้อสำหรับบทความแยกต่างหาก ที่นี่ฉันอยากจะดึงความสนใจของคุณไปที่ความจริงที่ว่าเครื่องมือค้นหาไม่สามารถทดแทนการจัดเก็บข้อมูลหลักได้ มันถูกใช้เป็นส่วนเพิ่มเติม: การเปลี่ยนแปลงใด ๆ ในฐานข้อมูลหลักที่เกี่ยวข้องกับการค้นหาจะถูกซิงโครไนซ์กับดัชนีการค้นหา เครื่องมือค้นหามักจะโต้ตอบกับเครื่องมือค้นหาเท่านั้นและไม่สามารถเข้าถึงฐานข้อมูลหลักได้ จุดที่สำคัญที่สุดประการหนึ่งคือวิธีจัดระเบียบการซิงโครไนซ์นี้อย่างน่าเชื่อถือ ทุกอย่างขึ้นอยู่กับข้อกำหนด "เวลาปฏิกิริยา" หากเวลาระหว่างการเปลี่ยนแปลงในฐานข้อมูลหลักและ "การปรากฏ" ในการค้นหาไม่สำคัญ คุณสามารถสร้างบริการที่ค้นหาบันทึกที่เปลี่ยนแปลงล่าสุดทุกๆ สองสามนาทีและจัดทำดัชนีบันทึกเหล่านั้น หากคุณต้องการเวลาตอบสนองที่สั้นที่สุดเท่าที่จะเป็นไปได้ คุณสามารถดำเนินการบางอย่างเช่น
กล่องขาออกของธุรกรรม เพื่อส่งการอัปเดตไปยังบริการค้นหา
ผลการวิจัย
- การใช้เพจจิ้งฝั่งเซิร์ฟเวอร์เป็นความยุ่งยากที่สำคัญ และเหมาะสมสำหรับชุดข้อมูลที่เติบโตอย่างรวดเร็วหรือเพียงชุดข้อมูลขนาดใหญ่เท่านั้น ไม่มีสูตรตายตัวที่แน่นอนในการประเมินว่า "ใหญ่" หรือ "โตเร็ว" แต่ฉันจะปฏิบัติตามแนวทางนี้:
- หากได้รับข้อมูลที่รวบรวมไว้ครบถ้วน โดยคำนึงถึงเวลาของเซิร์ฟเวอร์และการส่งผ่านเครือข่าย ซึ่งเป็นไปตามข้อกำหนดด้านประสิทธิภาพตามปกติ ก็ไม่มีประโยชน์ที่จะนำเพจจิ้งไปใช้งานบนฝั่งเซิร์ฟเวอร์
- อาจมีสถานการณ์ที่คาดว่าจะไม่มีปัญหาด้านประสิทธิภาพในอนาคตอันใกล้ เนื่องจากมีข้อมูลน้อย แต่การรวบรวมข้อมูลมีการเติบโตอย่างต่อเนื่อง หากชุดข้อมูลบางชุดในอนาคตอาจไม่เป็นไปตามจุดก่อนหน้าอีกต่อไป ควรเริ่มเพจทันที
- หากไม่มีข้อกำหนดที่เข้มงวดในส่วนของธุรกิจในการแสดงจำนวนผลลัพธ์ทั้งหมดหรือแสดงหมายเลขหน้า และระบบของคุณไม่มีเครื่องมือค้นหา จะเป็นการดีกว่าที่จะไม่นำประเด็นเหล่านี้ไปใช้และพิจารณาตัวเลือก #2
- หากมีข้อกำหนดที่ชัดเจนสำหรับการค้นหาแบบประกอบ คุณมีสองตัวเลือกโดยไม่ต้องเสียสละประสิทธิภาพ:
- อย่าคำนวณปริมาณทั้งหมดใหม่ทุกครั้งที่เกณฑ์การค้นหาเปลี่ยนแปลง
- ใช้เครื่องมือค้นหาเช่น Solr, ElasticSearch, Sphinx และอื่นๆ แต่ควรเข้าใจว่าไม่สามารถทดแทนฐานข้อมูลหลักได้ และควรใช้เป็นส่วนเสริมจากที่เก็บข้อมูลหลักในการแก้ปัญหาการค้นหา
- นอกจากนี้ ในกรณีของการค้นหาแบบแยกส่วน ก็สมเหตุสมผลที่จะแบ่งการดึงข้อมูลหน้าผลการค้นหาและการนับเป็นสองคำขอคู่ขนาน การนับปริมาณอาจใช้เวลานานกว่าการได้ผลลัพธ์ ในขณะที่ผลลัพธ์มีความสำคัญต่อผู้ใช้มากกว่า
- หากคุณใช้ฐานข้อมูล SQL ในการค้นหา การเปลี่ยนแปลงโค้ดใดๆ ที่เกี่ยวข้องกับส่วนนี้ควรได้รับการทดสอบอย่างดีเกี่ยวกับประสิทธิภาพของข้อมูลในปริมาณที่เหมาะสม (เกินปริมาณในฐานข้อมูลสด) ขอแนะนำให้ใช้การตรวจสอบเวลาดำเนินการแบบสอบถามในทุกอินสแตนซ์ของฐานข้อมูล และโดยเฉพาะอย่างยิ่งในอินสแตนซ์ "สด" แม้ว่าทุกอย่างจะเรียบร้อยดีกับแผนการสืบค้นในขั้นตอนการพัฒนา แต่เมื่อปริมาณข้อมูลเพิ่มขึ้น สถานการณ์ก็อาจเปลี่ยนแปลงอย่างเห็นได้ชัด
ที่มา: will.com