Limit เถอะพ่อคุณ
ไม่รู้จะเขียนเรื่องอะไร จะเขียนเทคโนโลยีก็ตามคนอื่นเขาไม่ทันละ คิดไปคิดมาก็นึกได้ว่าเขียนเรื่องที่ไม่เคยเจอตอนเรียนแล้วมาเจอตอนทำงานดีกว่า น่าจะเป็นประโยชน์กับคนอื่นบ้าง อันนี้ปัญหาแรกๆที่เจอตอนเริ่มทำงานเลยก็คือ “Limit เถอะพ่อคุณ”
ตอนเริ่มฝึกงานใหม่ๆได้รับงานให้เขียน Sql Query หาข้อมูล ด้วยความที่ร้อนวิชาจากการที่ได้เรียนวิชา Database มาใหม่ๆ กับความภูมิใจที่ทำการเคลีย “โจทย์ SQL เกี่ยวกับประธานาธิปดี” มาได้หมด ทำเสร็จเรียบร้อยส่ง Code ไปให้เขาอย่างสบายๆ หลังจากพี่เขาเอาไปลอง Test ก็บอกว่า “เจ๊ง” Server test เกือบดับ
ทำไมต้อง Limit
ความคิดในหัวตอนแรกคือคำว่า “Code เราก็ถูกนี่หว่า” มองยังไงก็ไม่ผิด Logic การ join อะไรก็ถูกหมด จนไปถามว่ามันเกิดอะไรขึ้นทำไมมันเจ๊ง จนได้คำตอบมาว่า “ทำไมไม่ใส่ Limit” ตอนนั้น Stun เลยแบบ เอ้าจะ Limit ทำไมก็มันต้องดึงมาโชว์หมดอยู่แล้วและอื่นๆ พี่ที่ฝึกงานก็เลยสอนว่ามันเป็นแบบนี้
ข้อมูลในงานจริงมีมากกว่าที่เครื่อง Dev หลายเท่า
อันนี้คือเรื่องจริงที่เด็กที่ไม่เคยเจองานจริงมักมองข้ามไปคือตอนเราเริ่ม Dev เราก็แค่สมมุติ Data ขึ้นมาจำนวนหนึ่ง Data พวกนั้นมีแค่ 10 - 20 ตัวซึ่งเรามาใช้เพื่อทดสอบ Logic การทำงานของเราว่าถูกไหม เราลืมมองเรื่อง Performance ไป ลองนึกภาพตามว่าข้อมูลที่ตรงกับเงื่อนไขที่เราต้องการหามี 20 ล้านตัว แต่ละตัวกิน size ประมาณ 30KB รวมข้อมูลที่ต้องดึงมาจาก Database 20 KB * (20 * (10^6)) = 400 GB คุณพระ เครื่อง Server ต้องดึงข้อมูลจาก Database 400 GB ออกมาเข้า RAM ส่งไปต่อไปที่เครื่อง APP โอ้มันจะเหลืออะไรล่ะครับ ถ้าสมมุติว่า DBMS (ตัว Software จัดการข้อมูล) มันมีวิธีดึงข้อมูลห่วยมากคือดึงข้อมูลทั้งหมดขึ้น RAM มันจะเกิดอะไรขึ้น “พังสิครับ” แต่ DBMS ส่วนใหญ่ เขา manage วิธีดึงข้อมูลดีมากคือค่อยๆ ดึง ค่อยๆ ส่ง (แน่นอนว่าทุกเจ้าคงทำแบบนี้) ต่อให้ทำแบบนี้ก็ต้องเสียเวลาส่งข้อมูลผ่าน Link อีกทั้งไอเจ้า APP อะมีจัดการวิธีจัดการ Data ยังไง ถ้าเอาขึ้น RAM หมดก็ “พังสิครับ” นี่เป็นเหตุผลแรกที่ฟังแล้วก็เข้าใจ แต่ปัญหาคือไม่ดึงทีละทั้งหมดต้องดึงแบบไหน จนได้คำตอบข้อต่อมาคือ
1. ค่อยดึงค่อยทำสิ
อันนี้เป็นคำตอบง่ายๆที่คิดไม่ถึงเลย ในเมื่อดึงทีละทั้งหมดไม่ได้ ก็ค่อยๆดึงเอาสิ ดึงเอาเป็นรอบๆไปตามจำนวนที่ต้องการ ดึงไปเรื่อยๆจนครบ เช่น
1 | // ส่วน import ก็ไม่ต้องสนใจมาก |
อย่างตัวอย่างนี้อยากดึงข้อมูลขึ้นมาทีละ 10 ตัวทำแล้วเอาไปทำไรสักอย่างจนเสร็จแล้วค่อยดึงตัวใหม่ขึ้นมา ข้อสำคัญของการทำแบบนี้คืออย่าลืม Order แล้วก็ระวังเรื่อง concurrentcy กล่าวคือถ้าเรา Order ด้วย Field นึงแล้วบังเอิญมีคน Insert หรือ Delete หรือ ทำอะไรก็แล้วแต่ที่ทำให้ผล Order มันเปลี่ยน ข้อมูลเข้ามาระหว่างกำลังทำ Process นี้ลำดับจะเลื่อนแล้วอาจทำให้เกิดการทำงานซ้ำ หรือ ข้ามไปได้ ตัวอย่างเช่น

กรณีนี้คือดึงข้อมูลขึ้นมาทีละ 4 ตัว ดังนั้นรอบแรกจะดึงข้อมูลตัวที่ 1 -4 และ รอบที่ 2 ดึงข้อมูล 5 - 8 ขึ้นมาตามภาพ ตอนนี้ก็ไม่มีอะไร ต่อมาเมื่อระบบมีความซับซ้อน (ซ่อนเงื่อน เพื่อนทรยศ) มากขึ้นมีการทำงานเกี่ยวกับการแก้ไขข้อมูลในขณะเดียวกับการประมวลผลข้อมูลเป้นชุดแบบนี้ก็จะเกิดเหตุการณ์นี้ขึ้น

จากภาพจะเห็นเลยว่าระหว่างการ select รอบ 1 และ 2 มีอีกงานนึงเข้ามาลบค่า 2 ไปทำให้ลำดับการเรียงผิดไป ทำให้การ Select รอบที่ 2 นั้น 5 ไม่ได้ถูกดึงไปทำ อันนี้เป็นปัญหา Classic ที่คนไม่ค่อยคำนึงถึง หรือเจอไม่ค่อยบ่อย แต่ถ้าเกิดขึ้นแล้วรับรองว่างงแน่นอนเพราะ โปรแกรมทำงานถูกต้องทุกอย่างตามปกติ มันผิดตรง concurrent ที่เข้ามา ในประสบการณ์ทำงานของผมยังไม่เคยเจอนะ หรืออาจจะเจอแต่แค่ไม่มีคนแจ้งมาหรือเขาลองกดใหม่ ปัญหานี้หาอ่านได้ในเรื่อง ACID ซึ่งเป็นมหากาพย์เรื่องยาวเลยทีเดียว ตอนเรียนผมจำได้ว่าเรียนไปเกือบ 2 คาบมั้ง
ถามว่ามีวิธีอื่นไหมที่จะแก้ไอปัญหาพวกนี้ จริงๆมีนะแต่ไม่ได้แก้ได้สมบูรณ์แบบอะไรมากแค่พอขัดในระดับ Dev แก้การเลื่อนจากการลบแต่ไม่แก้ปัญหาเรื่อง Insert ก่อนหน้า คือ
WHERE ด้วยตัวสุดท้ายของรอบนั้น
อันนี้แนะนำกรณีที่ใช้กับการ sort ที่จัดลำดับด้วยอะไรที่เป็น unique วิธีไม่ยากครับ แทนที่จะใช้ skip เราเปลี่ยนมาใช้ตัวสุดท้ายเป็นเงื่อนไขในการค้นหาแทน จากอันนี้ผมทราบแล้วว่า id เป็น unique ผมก็เปลี่ยนฟังก์ชัน getCities แทนที่จะรับ skip ผมเปลี่ยนมารับ id ที่ต้องมากกว่าเข้ามาแทน
1 | // ตรงนี้ส่วนที่ Limit ที่บอก |
แล้วก็ไปเปลี่ยน main function ให้เรียกแบบใหม่เป็นแบบนี้
1 | func main() { |
จาก Code จะเห็นได้ว่าเราเปลี่ยนจากใช้ SKIP มาเป็น greaterThanId มาเป็นเงื่อนไขดังนั้นถ้าเราเก็บตัวสุดท้ายไปเป็นเงื่อนไขค้นหาก็รับรองได้่ว่ามันจะไม่มีทางเอาตัวก่อนหน้าและดีกว่า SKIP ตรงที่มันจะเริ่มหาจากตัวที่มีค่ามากกว่า greaterThanId แต่อันนี้ก็มีปัญหาเหมือนกันนะ คือถ้ามีการ insert ตัวนี้ที่มีค่าน้อยกว่าหรือเท่ากับ greaterThanId เข้าไปตัวนั้นก็จะไม่ถูกทำ ซึ่งปกติเราคงให้ค่า Id มันมีค่ามากขึ้นเรื่อยๆแบบ Auto increment ดังนั้นมันไม่มีทางที่จะทำให้ id มาแทรกอยู่ด้านหน้าได้
2. Pagination

นี่เป็นคำศัพท์ใหม่ที่รู้ตอนเริ่มทำงานใหม่ๆซึ่งมันก็คือการแบ่งหน้านั่นเอง คือในความเป็นจริงเราไม่ต้องส่งข้อมูลทั้งหมดมาให้ User ดูหรอก ลองนึกภาพตามคุณเข้ามาหน้า Tracking การส่งพัสดุ คำถามคือคุณอยากเห็นการส่งพัสดุของคุณทั้งหมดตั้งแต่เริ่่มเลยไหม หรือ เข้าไปหน้าดูประวัติการฝากถอนเงินของธนาคาร ถามว่าเข้ามาหน้านั้นมันจะต้องเรียงตั้งแต่วันแรกที่เปิดบัญชีจนถึงวันปัจจุบันในหน้าเดียวไหม ใช่ครับไม่มีที่ไหนเขาทำ ปกติเขาก็แสดงเป็นหน้าๆ หน้าละ 10 - 25 รายการ คือถ้ามากกว่านี้มันก็ต้องเริ่ม scroll หน้าละ อีกทั้งไม่มีใครมานั่งไล่หาทีละหน้าหรอก เขาก็ต้องกด search เป็นช่วงเวลา และ เงื่อนไขต่างๆ ให้ได้ผลลัพธ์น้อยๆแล้วค่อยมาหา ลองถามคุณดูว่าใช้ App ใช้อะไรมันเป็นแบบนั้นไหม ซึ่งดังนั้นเราก็ดึงตามเงื่อนไขของผู้ใช้เช่น เอาหน้าที่ 5 ขนาดหน้าละ 20 ก็ไปแปลงเป็น skip limit ได้ไม่ยากใช่ไหมครับ
ก็จบไปสำหรับประสบการณ์ที่ได้มาตอนทำงาน เป็นประสบการณ์ง่อยๆที่เจอมา ไม่รู้ว่าจะมีประโยชน์หรือคุ้มกับเวลาที่อ่านหรือเปล่า
"สิ่งที่ผมเขียนขึ้นเป็นเพียงความรู้และความเข้าใจของบุคคลเพียงบุคคลเดียว ดังนั้นอย่าเพิ่งเชื่อในสิ่งที่ผมเขียนและอธิบาย ลองทำความเข้าใจว่ามันเป็นจริงอย่างนั้นไหมและลองหาแหล่งอ้างอิงอื่นๆว่าเขามีแนวคิดอย่างไร เรื่องการ Design และวิธีการใช้งานไม่มีถูกไม่มีผิดมีแต่เหมาะสมกับงานนั้นไหม"