ในเว็บแอปพลิเคชัน การแบ่งหน้าชุดผลลัพธ์ฐานข้อมูลขนาดใหญ่เป็นปัญหาที่รู้จักกันดี พูดง่ายๆ ก็คือ คุณไม่ต้องการให้ข้อมูลการสืบค้นทั้งหมดแสดงบนหน้าเดียว ดังนั้น การแสดงแบบแบ่งหน้าจึงเหมาะสมกว่า แม้ว่านี่จะไม่ใช่งานง่ายใน ASP แบบดั้งเดิม แต่ใน ASP.NET ตัวควบคุม DataGrid จะทำให้กระบวนการนี้ง่ายขึ้นโดยใช้โค้ดเพียงไม่กี่บรรทัด ดังนั้นใน asp.net การเพจจึงง่ายมาก แต่เหตุการณ์การเพจ DataGrid เริ่มต้นจะอ่านบันทึกทั้งหมดจากฐานข้อมูลและใส่ลงในแอปพลิเคชันเว็บ asp.net เมื่อคุณมีข้อมูลมากกว่าหนึ่งล้านข้อมูล สิ่งนี้จะทำให้เกิดปัญหาด้านประสิทธิภาพการทำงานที่ร้ายแรง (หากคุณไม่เชื่อสิ่งนี้ คุณสามารถดำเนินการค้นหาในแอปพลิเคชันของคุณและดูปริมาณการใช้หน่วยความจำของ aspnet_wp.exe ในสถานการณ์ตัวจัดการงาน) นี่คือสาเหตุ จำเป็นต้องปรับแต่งลักษณะการทำงานของเพจ เพื่อให้แน่ใจว่าจะได้รับเฉพาะบันทึกข้อมูลที่จำเป็นสำหรับเพจปัจจุบันเท่านั้น
มีบทความและโพสต์มากมายเกี่ยวกับปัญหานี้บนอินเทอร์เน็ตตลอดจนวิธีแก้ปัญหาที่ครบถ้วน จุดประสงค์ของฉันในการเขียนบทความนี้ไม่ใช่เพื่อแสดงขั้นตอนการจัดเก็บที่จะแก้ปัญหาทั้งหมดของคุณ แต่เพื่อเพิ่มประสิทธิภาพวิธีการที่มีอยู่และจัดเตรียมแอปพลิเคชันให้คุณทดสอบเพื่อให้คุณสามารถดำเนินการได้ตามความต้องการ
แต่ฉันไม่พอใจกับวิธีการต่างๆ ที่นำเสนอทางออนไลน์ในปัจจุบันมากนัก ขั้นแรก มีการใช้ ADO แบบดั้งเดิม ซึ่งเขียนขึ้นอย่างชัดเจนสำหรับ ASP "โบราณ" วิธีที่เหลือคือขั้นตอนการจัดเก็บ SQL Server และบางวิธีไม่สามารถใช้งานได้เนื่องจากเวลาตอบสนองช้าเกินไป ดังที่คุณเห็นจากผลลัพธ์ประสิทธิภาพในตอนท้ายของบทความ แต่มีบางส่วนที่ดึงดูดความสนใจของฉัน
ลักษณะทั่วไป
ฉันต้องการวิเคราะห์สามวิธีที่ใช้กันทั่วไปในปัจจุบันอย่างรอบคอบ ซึ่งได้แก่ ตารางชั่วคราว (TempTable), SQL แบบไดนามิก (DynamicSQL) และจำนวนแถว (จำนวนแถว) ต่อไปนี้ ฉันชอบเรียกวิธีที่สองว่าวิธี Asc-Desc (จากน้อยไปมาก) ฉันไม่คิดว่า Dynamic SQL เป็นชื่อที่ดีเพราะคุณสามารถใช้ตรรกะ SQL แบบไดนามิกในวิธีอื่นได้ ปัญหาทั่วไปของกระบวนงานที่เก็บไว้เหล่านี้คือ คุณต้องประมาณว่าคอลัมน์ใดที่คุณจะเรียงลำดับ ไม่ใช่แค่คอลัมน์คีย์หลัก (คอลัมน์ PK) ซึ่งอาจนำไปสู่ปัญหาต่างๆ มากมาย - สำหรับการสืบค้นแต่ละครั้ง คุณต้อง แสดงผ่านการเพจ ซึ่งหมายความว่าสำหรับแต่ละคอลัมน์การเรียงลำดับที่แตกต่างกัน คุณต้องมีคิวรีการเพจที่แตกต่างกันจำนวนมาก ซึ่งหมายความว่าคุณต้องใช้ขั้นตอนการจัดเก็บที่แตกต่างกันสำหรับคอลัมน์การเรียงลำดับแต่ละคอลัมน์ (ไม่ว่าจะใช้วิธีการเพจใด) หรือคุณต้อง ใส่ฟังก์ชันนี้ไว้ในขั้นตอนการจัดเก็บด้วยความช่วยเหลือของไดนามิก SQL ทั้งสองวิธีนี้มีผลกระทบเล็กน้อยต่อประสิทธิภาพการทำงาน แต่จะช่วยเพิ่มการบำรุงรักษา โดยเฉพาะอย่างยิ่งถ้าคุณจำเป็นต้องใช้วิธีนี้เพื่อแสดงแบบสอบถามที่แตกต่างกัน ดังนั้นในบทความนี้ ฉันจะพยายามใช้ Dynamic SQL เพื่อสรุปขั้นตอนการจัดเก็บทั้งหมด แต่ด้วยเหตุผลบางประการ เราสามารถบรรลุความเป็นสากลเพียงบางส่วนเท่านั้น ดังนั้นคุณยังคงต้องเขียนขั้นตอนการจัดเก็บที่เป็นอิสระสำหรับการสืบค้นที่ซับซ้อน
ปัญหาที่สองในการอนุญาตฟิลด์การเรียงลำดับทั้งหมด รวมถึงคอลัมน์คีย์หลัก คือ ถ้าคอลัมน์เหล่านั้นไม่ได้รับการจัดทำดัชนีอย่างเหมาะสม วิธีการเหล่านี้ก็ไม่สามารถช่วยได้ ในวิธีการเหล่านี้ทั้งหมด จะต้องเรียงลำดับแหล่งที่มาของเพจก่อน สำหรับตารางข้อมูลขนาดใหญ่ ค่าใช้จ่ายในการเรียงลำดับโดยใช้คอลัมน์ที่ไม่ใช่ดัชนีนั้นมีน้อยมาก ในกรณีนี้ กระบวนการจัดเก็บทั้งหมดไม่สามารถนำมาใช้ในสถานการณ์จริงได้ เนื่องจากเวลาตอบสนองที่ยาวนาน (เวลาที่สอดคล้องกันจะแตกต่างกันไปตั้งแต่ไม่กี่วินาทีไปจนถึงสองสามนาที ขึ้นอยู่กับขนาดของตารางและบันทึกแรกที่ได้รับ) ดัชนีในคอลัมน์อื่นๆ อาจทำให้เกิดปัญหาด้านประสิทธิภาพที่ไม่พึงประสงค์เพิ่มเติมได้ เช่น อาจช้ามากหากคุณนำเข้าข้อมูลจำนวนมากทุกวัน
ตารางชั่วคราว
ก่อนอื่น ฉันจะพูดถึงวิธีตารางชั่วคราว นี่เป็นวิธีแก้ปัญหาที่แนะนำอย่างกว้างขวางซึ่งฉันพบหลายครั้งในโปรเจ็กต์ของฉัน มาดูสาระสำคัญของวิธีนี้กันดีกว่า:
CREATE TABLE #Temp(
ID int รหัสประจำตัวหลัก
PK /*นี่คือประเภท PK*/
)
ใส่ลงใน #Temp SELECT PK FROM Table ORDER BY SortColumn
SELECT FROM Table JOIN # Temp temp ON Table.PK = temp .PK ORDER BY temp .ID WHERE ID > @StartRow AND ID< @EndRow
โดยการคัดลอกแถวทั้งหมดไปไว้ที่ temporary ใน ตาราง เราสามารถเพิ่มประสิทธิภาพการสืบค้นเพิ่มเติมได้ (SELECT TOP EndRow...) แต่กุญแจสำคัญคือสถานการณ์กรณีที่เลวร้ายที่สุด - ตารางที่มี 1 ล้านบันทึกจะสร้างตารางชั่วคราวที่มี 1 ล้านบันทึก
เมื่อพิจารณาถึงสถานการณ์นี้และดูผลลัพธ์ของบทความข้างต้น ฉันจึงตัดสินใจละทิ้งวิธีจากน้อยไปมาก
ในการทดสอบ
วิธีนี้ใช้การเรียงลำดับเริ่มต้นในแบบสอบถามย่อยและการเรียงลำดับแบบย้อนกลับในแบบสอบถามหลัก
ประกาศ @temp TABLE(
PK /* ประเภท PK */
ไม่ใช่ประถมศึกษาที่เป็นโมฆะ
)
ใส่ @temp เลือก TOP @PageSize PK จาก
-
เลือกด้านบน(@StartRow + @PageSize)
พีเค
SortColumn /* หากคอลัมน์การเรียงลำดับแตกต่างจาก PK SortColumn จะต้อง
ดึงมาได้เช่นกัน ไม่งั้นต้องใช้ PK เท่านั้น
*/
เรียงลำดับตามคอลัมน์
-
ลำดับเริ่มต้น - โดยทั่วไป ASC
-
)
เรียงตาม SortColumn
-
กลับคำสั่งเริ่มต้น – โดยทั่วไปคือ DESC
*/
เลือกจาก Table JOIN @Temp temp ON Table .PK= temp .PK
เรียงลำดับตามคอลัมน์
-
คำสั่งเริ่มต้น
-
การนับแถว
นี้อาศัยนิพจน์ SET ROWCOUNT ใน SQL เพื่อให้สามารถข้ามแถวที่ไม่จำเป็นและรับบันทึกแถวที่ต้องการได้:
DECLARE @Sort /* ประเภทของคอลัมน์การเรียงลำดับ */
SET ROWCOUNT @ StartRow
SELECT @Sort=SortColumn จากตารางเรียงลำดับตาม SortColumn
SET ROWCOUNT @PageSize
SELECT FROM Table WHERE SortColumn >= @Sort ORDER BY SortColumn
มีอีกสองวิธีในการสืบค้นย่อย
ที่ฉันพิจารณาแล้ว และแหล่งที่มาต่างกัน วิธีแรกคือ Triple Query หรือวิธีการสืบค้นด้วยตนเองที่รู้จักกันดี ในบทความนี้ ฉันยังใช้ตรรกะทั่วไปที่คล้ายกันซึ่งครอบคลุมขั้นตอนการจัดเก็บอื่นๆ ทั้งหมด แนวคิดที่นี่คือเพื่อเชื่อมต่อกับกระบวนการทั้งหมด ฉันลดโค้ดต้นฉบับลงบางส่วนเนื่องจากไม่จำเป็นต้องใช้จำนวนบันทึกในการทดสอบ)
SELECT FROM Table WHERE PK IN(
เลือก TOP @PageSize PK จากตารางโดยที่ PK ไม่อยู่ใน
-
เลือก TOP @StartRow PK จากตาราง เรียงลำดับตาม SortColumn)
เรียงลำดับตามคอลัมน์)
เรียงลำดับตาม
เคอร์เซอร์
SortColumnขณะที่ดูกลุ่มสนทนาของ Google ฉันพบวิธีสุดท้าย วิธีการนี้ใช้เคอร์เซอร์แบบไดนามิกฝั่งเซิร์ฟเวอร์ หลายๆ คนพยายามหลีกเลี่ยงการใช้เคอร์เซอร์เนื่องจากไม่เกี่ยวข้องและไม่มีประสิทธิภาพเนื่องจากความเป็นระเบียบ แต่เมื่อมองย้อนกลับไป จริงๆ แล้วเพจเป็นงานที่เป็นระเบียบ ไม่ว่าคุณจะใช้วิธีใดก็ตาม คุณต้องส่งคืนเคอร์เซอร์นั้น ในวิธีการก่อนหน้านี้ ขั้นแรกให้คุณเลือกแถวทั้งหมดก่อนเริ่มการบันทึก เพิ่มแถวที่จำเป็นในการบันทึก จากนั้นจึงลบแถวก่อนหน้าทั้งหมด เคอร์เซอร์แบบไดนามิกมีตัวเลือก FETCH RELATIVE ที่จะทำการกระโดดอย่างมหัศจรรย์ ตรรกะพื้นฐานมีดังนี้:
DECLARE @PK /* PKType */
DECLARE @tblPK
TABLE(
PK /*PKType*/ ไม่ใช่คีย์หลักที่เป็นโมฆะ
)
ประกาศ PagingCursor CURSOR DYNAMICREAD_ONLY FOR
เลือก @PK จากตารางเรียงลำดับตาม SortColumn
OPEN PagingCursor
ดึงข้อมูลที่เกี่ยวข้อง @StartRow จาก PagingCursor เข้าสู่ @PK
ในขณะที่ @PageSize>0 และ @@FETCH_STATUS =0
เริ่ม
แทรก @tblPK(PK) ค่า(@PK)
ดึงข้อมูลถัดไปจาก PagingCursor เข้าสู่ @PK
SET @PageSize = @PageSize - 1
สิ้นสุด
การปิด
เคอร์เซอร์เพจ
จัดสรร
PagingCursor
เลือกจากตารางเข้าร่วม @tblPK temp บนตาราง .PK= temp .PK
ลักษณะทั่วไปของการสืบค้นที่ซับซ้อน
ใน ORDER BY SortColumn
ฉันได้ชี้ให้เห็นก่อนหน้านี้ว่ากระบวนงานที่เก็บไว้ทั้งหมดใช้ SQL แบบไดนามิกเพื่อให้เกิดลักษณะทั่วไป ดังนั้นในทางทฤษฎีแล้ว พวกเขาสามารถใช้การสืบค้นที่ซับซ้อนประเภทใดก็ได้ ด้านล่างนี้เป็นตัวอย่างของการสืบค้นที่ซับซ้อนโดยอิงตามฐานข้อมูล Northwind
เลือกลูกค้า ContactName AS ลูกค้า ลูกค้าที่อยู่ + ' , ' + ลูกค้าเมือง + ', '+ ลูกค้าประเทศ
ที่อยู่ AS, SUM([รายละเอียดการสั่งซื้อ].ราคาต่อหน่วย*[รายละเอียดการสั่งซื้อ] .ปริมาณ)
AS [Totalmoneyspent]
จากลูกค้า
INNER JOIN คำสั่งซื้อ ON Customers.CustomerID = orders.CustomerID
INNER JOIN [ OrderDetails ] ON orders.OrderID = [ OrderDetails].OrderID
WHERE Customers.Country <> 'USA' AND Customers.Country <> 'Mexico '
จัดกลุ่มตามลูกค้า ชื่อผู้ติดต่อ ลูกค้า ที่อยู่ ลูกค้า เมือง ลูกค้า ประเทศ
HAVING(SUM([OrderDetails].UnitPrice * [ OrderDetails ] .Quantity)) > 1000
ORDER BY Customer DESC ,Address DESC
ส่งคืนการเรียกหน่วยเก็บเพจของเพจที่สองดังนี้:
EXEC ProcedureName
/*Tables */
'
ลูกค้า
คำสั่งซื้อ INNER JOIN บน Customers.CustomerID=Orders.CustomerID
INNER JOIN [OrderDetails] บน orders.OrderID=[OrderDetails].OrderID
-
-
/* PK */
'
ลูกค้า.รหัสลูกค้า
-
-
/* เรียงลำดับตาม */
'
ลูกค้าชื่อผู้ติดต่อ DESC ลูกค้าที่อยู่ DESC
-
,
/*หมายเลขหน้า */
2
,
/*ขนาดหน้า */
10
,
/*ฟิลด์ */
-
ลูกค้า ชื่อผู้ติดต่อ AS ลูกค้า
ลูกค้าที่อยู่+'' , '' +ลูกค้าเมือง+ '' , '' +ลูกค้าประเทศ ASAddress, SUM([รายละเอียดการสั่งซื้อ].ราคาต่อหน่วย*[รายละเอียดการสั่งซื้อ].ปริมาณ)AS[เงินทั้งหมดที่ใช้จ่าย]
-
,
/*กรอง */
'
ลูกค้าประเทศ<>'' สหรัฐอเมริกา '' และลูกค้าประเทศ<> '' เม็กซิโก ''' ,
/*GroupBy */
-
ลูกค้า ลูกค้า ID ลูกค้า ชื่อผู้ติดต่อ ลูกค้า ที่อยู่
ลูกค้า เมือง ลูกค้า ประเทศ
มี(SUM([รายละเอียดการสั่งซื้อ].ราคาต่อหน่วย*[รายละเอียดการสั่งซื้อ].ปริมาณ))>1000
'
เป็นที่น่าสังเกตว่าคุณใช้นามแฝงในคำสั่ง ORDER BY ในการสืบค้นต้นฉบับ แต่คุณไม่ควรทำเช่นนี้ในขั้นตอนการจัดเก็บแบบเพจ เนื่องจากการข้ามแถวก่อนเริ่มบันทึกจะใช้เวลานาน ในความเป็นจริง มีวิธีการมากมายสำหรับการนำไปใช้ แต่หลักการไม่ได้รวมฟิลด์ทั้งหมดไว้ที่จุดเริ่มต้น แต่รวมเฉพาะคอลัมน์คีย์หลักเท่านั้น (เทียบเท่ากับคอลัมน์การเรียงลำดับในวิธี RowCount) ซึ่งสามารถเร่งให้การดำเนินการเสร็จสมบูรณ์เร็วขึ้น งาน. เฉพาะในหน้าคำขอเท่านั้นที่จะได้รับช่องที่ต้องกรอกทั้งหมด นอกจากนี้ ไม่มีนามแฝงของฟิลด์ในการสืบค้นขั้นสุดท้าย และในการสืบค้นแบบข้ามแถว ต้องใช้คอลัมน์ดัชนีล่วงหน้า
มีปัญหาอื่นกับขั้นตอนการจัดเก็บ RowCount เพื่อให้เกิดลักษณะทั่วไป อนุญาตให้มีเพียงหนึ่งคอลัมน์ในคำสั่ง ORDER BY นี่เป็นปัญหากับวิธีการจากน้อยไปมากและวิธีการเคอร์เซอร์ แม้ว่าจะสามารถเรียงลำดับได้หลายคอลัมน์ก็ตาม ต้องแน่ใจว่ามีเพียงฟิลด์เดียวในคีย์หลัก ฉันเดาว่าสิ่งนี้สามารถแก้ไขได้ด้วย SQL แบบไดนามิกมากกว่านี้ แต่ในความคิดของฉันมันไม่คุ้มค่า แม้ว่าสถานการณ์ดังกล่าวจะเป็นไปได้ แต่ก็ไม่ได้เกิดขึ้นบ่อยนัก โดยปกติแล้ว คุณสามารถใช้หลักการข้างต้นเพื่อจัดเพจกระบวนงานที่เก็บไว้ได้อย่างอิสระ
การทดสอบประสิทธิภาพ
ในการทดสอบ ผมใช้สี่วิธี หากคุณมีวิธีที่ดีกว่า ผมก็อยากจะรู้ อย่างไรก็ตาม ฉันต้องเปรียบเทียบวิธีการเหล่านี้และประเมินประสิทธิภาพ ก่อนอื่น แนวคิดแรกของฉันคือการเขียนแอปพลิเคชันทดสอบ asp.net ที่มีเพจ DataGrid จากนั้นทดสอบผลลัพธ์ของเพจ แน่นอนว่า นี่ไม่ได้สะท้อนถึงเวลาตอบสนองที่แท้จริงของขั้นตอนการจัดเก็บ ดังนั้นแอปพลิเคชันคอนโซลจึงเหมาะสมกว่า ฉันยังได้รวมเว็บแอปพลิเคชันไว้ด้วย แต่ไม่ใช่สำหรับการทดสอบประสิทธิภาพ แต่เป็นตัวอย่างของการแบ่งหน้าแบบกำหนดเองของ DataGrid และขั้นตอนการจัดเก็บที่ทำงานร่วมกัน
ในการทดสอบ ฉันใช้ตารางข้อมูลขนาดใหญ่ที่สร้างขึ้นโดยอัตโนมัติและแทรกข้อมูลประมาณ 500,000 ชิ้น หากคุณไม่มีตารางดังกล่าวให้ทดลอง คุณสามารถคลิกที่นี่เพื่อดาวน์โหลดการออกแบบตารางและสคริปต์ขั้นตอนการจัดเก็บสำหรับการสร้างข้อมูล แทนที่จะใช้คอลัมน์คีย์หลักที่เพิ่มขึ้นอัตโนมัติ ฉันใช้ตัวระบุที่ไม่ซ้ำกันเพื่อระบุเรกคอร์ด ถ้าฉันใช้สคริปต์ที่ฉันกล่าวถึงข้างต้น คุณอาจพิจารณาเพิ่มคอลัมน์เพิ่มอัตโนมัติหลังจากสร้างตารางแล้ว ข้อมูลที่เพิ่มขึ้นอัตโนมัติจะถูกจัดเรียงตามคีย์หลัก ซึ่งหมายความว่าคุณต้องการใช้ขั้นตอนการจัดเก็บแบบแบ่งหน้าด้วย ด้วยการเรียงลำดับคีย์หลักเพื่อรับข้อมูลของหน้าปัจจุบัน
เพื่อดำเนินการทดสอบประสิทธิภาพ ฉันเรียกขั้นตอนที่เก็บไว้เฉพาะหลายครั้งผ่านการวนซ้ำ จากนั้นคำนวณเวลาตอบสนองโดยเฉลี่ย เมื่อพิจารณาถึงเหตุผลในการแคช เพื่อให้จำลองสถานการณ์จริงได้แม่นยำมากขึ้น - เวลาที่ใช้สำหรับเพจเดียวกันในการรับข้อมูลสำหรับการเรียกขั้นตอนที่เก็บไว้หลายครั้งมักจะไม่เหมาะสำหรับการประเมิน ดังนั้น เมื่อเราเรียกขั้นตอนที่เก็บไว้เดียวกัน , หมายเลขหน้าที่ร้องขอสำหรับการโทรแต่ละครั้งควรเป็นแบบสุ่ม แน่นอนว่าเราต้องถือว่าจำนวนหน้าคงที่ 10-20 หน้า และข้อมูลที่มีหมายเลขหน้าต่างกันอาจได้รับหลายครั้งแต่สุ่ม
สิ่งหนึ่งที่เราสังเกตได้ง่ายคือเวลาตอบสนองถูกกำหนดโดยระยะห่างของข้อมูลเพจที่จะได้รับโดยสัมพันธ์กับตำแหน่งเริ่มต้นของชุดผลลัพธ์ ยิ่งอยู่ห่างจากตำแหน่งเริ่มต้นของชุดผลลัพธ์มากเท่าใด บันทึกก็จะยิ่งมากขึ้นเท่านั้น ข้ามไป นี่เป็นเหตุผลที่ฉันไม่รวม 20 อันดับแรกในลำดับการสุ่มของฉัน อีกทางเลือกหนึ่ง ฉันจะใช้ 2^n หน้า และขนาดของการวนซ้ำคือจำนวนหน้าที่แตกต่างกันที่ต้องการ * 1,000 ดังนั้นแต่ละหน้าจึงถูกดึงเกือบ 1,000 ครั้ง (จะมีการเบี่ยงเบนอย่างแน่นอนเนื่องจากเหตุผลแบบสุ่ม)
ผลลัพธ์
ที่นี่ ผลการทดสอบของฉันคือ:
บทสรุป
การทดสอบดำเนินการตามลำดับจากประสิทธิภาพที่ดีที่สุดไปแย่ที่สุด - การนับแถว เคอร์เซอร์ จากน้อยไปหามาก จากมากไปน้อย และแบบสอบถามย่อย สิ่งที่น่าสนใจอย่างหนึ่งคือโดยปกติแล้วผู้คนไม่ค่อยเข้าชมหน้าเว็บหลังจากห้าหน้าแรก ดังนั้นวิธีการสืบค้นย่อยอาจเหมาะกับความต้องการของคุณในกรณีนี้ ขึ้นอยู่กับขนาดของชุดผลลัพธ์ของคุณและระยะห่างจากชุดผลลัพธ์เพื่อคาดการณ์ความถี่ของการเกิดหน้าเว็บ คุณมีแนวโน้มที่จะใช้วิธีการเหล่านี้ผสมผสานกัน ถ้าเป็นฉัน ฉันชอบวิธีนับแถวมากกว่า ในกรณีใดๆ มันใช้งานได้ค่อนข้างดี แม้แต่ในหน้าแรก "กรณีใดๆ" ที่นี่แสดงถึงบางกรณีที่การสรุปข้อมูลทั่วไปเป็นเรื่องยาก ในกรณีนี้ ฉันจะใช้ เคอร์เซอร์ (ฉันอาจจะใช้วิธีสืบค้นย่อยสำหรับสองวิธีแรก และวิธีการเคอร์เซอร์หลังจากนั้น)