นักพัฒนา Magento คนหนึ่งเปลี่ยนการเรียกใช้ ProductRepositoryInterface::getById() แยกกัน 300 ครั้ง มาเป็นการใช้ collection query เพียงครั้งเดียว ทำให้จำนวนการเรียกฐานข้อมูล (database hits) ลดลงจาก 300 เหลือเพียง 1 และหน้าหมวดหมู่สินค้า (category page) ก็โหลดเร็วขึ้นอย่างเห็นได้ชัด แต่สิ่งที่ต้องแลกมาคืออะไรน่ะหรือ? สินค้าบางรายการหายไปจากหน้าร้านค้า สร้างความสับสนให้กับทั้งลูกค้าและเจ้าของร้าน

ปัญหา N+1 บนหน้าหมวดหมู่สินค้าของ Magento

หน้านี้แสดงรายการสินค้า 300 รายการ โดยในแต่ละรายการ โค้ดจะดึง custom attributes สองตัวผ่านเมธอด getById() ของ repository ซึ่งทำให้เกิดหนึ่ง query ต่อหนึ่งสินค้า—นี่คือรูปแบบ N+1 แบบคลาสสิก การเปลี่ยนจากการโหลดทีละสินค้ามาเป็นการใช้ collection ที่ดึงข้อมูลทุกแถวที่ต้องการใน query เดียว ช่วยลดจำนวน query ลงได้อย่างมหาศาล แม้ความหน่วง (latency) ของหน้าบ้านจะดีขึ้น แต่รายการสินค้ากลับขาดรายการที่เคยแสดงผลก่อนหน้านี้ไป

ทำไม collection ถึงซ่อนสินค้าที่หมดสต็อก

โมดูล CatalogInventory ของ Magento จะเพิ่มฟิลเตอร์ "in-stock" ให้กับ product collection ใดๆ ที่สร้างขึ้นสำหรับหน้าบ้านโดยอัตโนมัติ ฟิลเตอร์นี้จะทำงานเมื่อการตั้งค่าร้านค้า Display Out of Stock Products ถูกปิดไว้ ซึ่งเป็นตัวเลือกปกติสำหรับร้านค้าที่ไม่ต้องการแสดงสินค้าที่ไม่มีจำหน่าย

ในทางตรงกันข้าม repository จะไม่ใช้ฟิลเตอร์สต็อกเลย มันเพียงแค่ตรวจสอบว่ามีสินค้านั้นอยู่จริงหรือไม่แล้วจึงส่งคืนข้อมูลกลับมา โดยไม่สนใจระดับสินค้าคงคลัง เมื่อโค้ดเปลี่ยนจากการเรียก repository มาเป็น collection ฟิลเตอร์สต็อกจึงแอบลบสินค้าทุกรายการที่มีจำนวนเป็นศูนย์ออกไปอย่างเงียบๆ โดยไม่มีการบันทึก error ใดๆ เพียงแต่ collection ส่งคืนจำนวนแถวข้อมูลน้อยกว่าที่นักพัฒนาคาดไว้

สิ่งนี้หมายถึงอะไรสำหรับผู้ประกอบการ

หน้าร้านค้าที่ตัด SKU ที่หมดสต็อกออกไปอย่างเงียบๆ ก่อให้เกิดปัญหาหลายประการ:

  • คอมโพเนนต์หน้าบ้านได้รับข้อมูลที่ไม่ครบถ้วน ทำให้เกิดป้ายกำกับที่ว่างเปล่าหรือการแสดงราคาที่ผิดพลาด
  • การดีบั๊ก (debugging) ทำได้ยากขึ้น เพราะอาการที่เกิดขึ้น—สินค้าหายไป—ไม่ได้สร้างคำเตือน (warnings) ใดๆ ออกมา

เนื่องจากชุดข้อมูลทดสอบ (automated tests) ส่วนใหญ่มักใช้ข้อมูลจำลองที่มีสินค้าในสต็อก บั๊กนี้จึงมักจะถูกซ่อนไว้จนกว่าเว็บไซต์จะทำงานด้วยข้อมูลสินค้าจริง

วิธีรักษาความเร็วโดยไม่ซ่อนสินค้าในสต็อก

  1. ข้ามฟิลเตอร์สต็อกเริ่มต้น – ลบฟิลเตอร์ออกจาก collection อย่างชัดเจนหากคุณต้องการสินค้าทุกรายการโดยไม่คำนึงถึงสถานะการมีอยู่ Magento มีเมธอดสำหรับปิดการใช้งานหรือแทนที่ stock plugin ในแต่ละ query
  2. ใช้ repository เมื่อความถูกต้องสำคัญที่สุด – repository รับประกันว่า product ID ทุกตัวที่ร้องขอจะถูกส่งคืน แม้ว่าสินค้านั้นจะหมดสต็อกก็ตาม คุณสามารถใช้การทำ cache ผลลัพธ์ หรือโหลดเฉพาะ ID ที่จำเป็นเพื่อควบคุมจำนวน query ที่เพิ่มขึ้น
  3. ใช้ resource model หรือ raw SQL – คิวรี (query) ไปที่ตารางสินค้าโดยตรงเพื่อดึง attributes ที่ต้องการ วิธีนี้จะข้าม collection plugins ทั้งหมด และช่วยให้คุณควบคุมได้อย่างเต็มที่ว่าต้องการรวมแถวข้อมูลใดบ้าง

ความเร็ว กับ ความถูกต้อง

การเปลี่ยนจากลูป N+1 ที่ทำงานหนักมาเป็นการใช้ collection query เพียงครั้งเดียวดูเป็นเรื่องที่สมเหตุสมผล เพราะความเร็วที่เพิ่มขึ้นนั้นเห็นได้ชัดเจน แต่ถึงอย่างนั้น การคิวรีที่เร็วขึ้นแต่กลับส่งคืนชุดสินค้าที่ไม่ถูกต้องก็ถือเป็นข้อบกพร่อง (defect) ไม่ใช่การเพิ่มประสิทธิภาพ (optimization) นักพัฒนาต้องชั่งน้ำหนักระหว่างความเร็วเพียวๆ กับความเสี่ยงในการแสดงแคตตาล็อกสินค้าที่ไม่ครบถ้วน

สิ่งที่ควรระวังต่อไป

  • ความคลาดเคลื่อนของการตั้งค่า (Configuration drift) – ตรวจสอบให้แน่ใจว่า flag Display Out of Stock Products สอดคล้องกับพฤติกรรมที่ต้องการของโค้ด collection ที่เขียนขึ้นเอง
  • ผลกระทบข้างเคียงจาก Plugin – โมดูลอื่นๆ อาจมีการเพิ่มฟิลเตอร์ของตัวเองลงใน collection ควรตรวจสอบ plugin stack ทุกครั้งที่คุณเปลี่ยนกลยุทธ์การคิวรี

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