Prachayawut Sirisuth
EN — Read in English
บทความทั้งหมด

คำนวณหน่วยฐานแทนการตั้ง flag

เผยแพร่
ระบบ
Klang Ya Bhaesaj
เวลาอ่าน
6 นาที

ร้านขายยาซื้อพาราเซตามอลเป็นเม็ด เป็นแผง เป็นกล่อง เป็นลัง ยาตัวเดียวกัน ของสี่อย่างที่ใส่ตะกร้าได้ ราคาสี่ราคา สต็อกกองเดียวอยู่ข้างหลัง

คำถามคือหน่วยควรอยู่ตรงไหน ทำผิดง่าย แก้ทีหลังน่ารำคาญ และผมทำผิดก่อน

สามชั้น ไม่ใช่สองชั้น

แคตตาล็อกแยกตัวตนสินค้า ออกจากหน่วยที่ขายได้ ออกจากข้อมูลระดับสาขา

products                    SKU กลาง: ชื่อ บาร์โค้ด ผู้ผลิต
  └── product_units         หน่วยที่ขายได้: ชื่อหน่วย conversion_factor
        └── branch_product_units    ราคา สต็อก แต้ม การมองเห็น

conversion_factor คือจำนวนชิ้นฐานในหน่วยนั้น เม็ด 1 แผง 10 กล่อง 100 ราคากับสต็อกอยู่ชั้นล่างสุด แยกตามสาขา เพราะกล่องเดียวกันคนละจังหวัดราคาไม่เท่ากัน และสต็อกไม่ได้แชร์กันแน่ ๆ

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

boolean ที่ดูเหมือนคำตอบ

สินค้าทุกตัวต้องมีหน่วยฐาน ตัวหารที่ใช้นับสต็อก โมเดลที่นึกออกทันทีคือ

ALTER TABLE product_units ADD COLUMN is_base_unit boolean;

อ่านก็เข้าใจ query ก็เร็ว และมันก็คือแหล่งความจริงที่สองของสิ่งที่ข้อมูลบอกได้อยู่แล้ว แปลว่ามันขัดกับข้อมูลนั้นได้ ให้เวลามันหน่อยเดี๋ยวก็ขัด

ทุกกรณีนี้ทำได้จริงผ่านหน้าแอดมิน

  • สองหน่วยของสินค้าเดียวกันถูกตั้งเป็นหน่วยฐานทั้งคู่
  • ไม่มีหน่วยไหนถูกตั้งเลย เพราะหน่วยแรกโดนลบ
  • flag อยู่ที่แผง ทั้งที่มีหน่วยเม็ดที่ factor เล็กกว่า

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

คำนวณเอาก็จบ

หน่วยฐานไม่ใช่ข้อมูลเพิ่ม conversion factor กำหนดมันไว้ครบแล้ว คือหน่วยที่ factor น้อยที่สุด เท่ากันก็ตัดด้วย sort order แล้วต่อด้วย id

Migration 0040 ลบคอลัมน์ทิ้งแล้วแทนด้วย correlated subquery ตัวตัดสินตอนเสมอสำคัญกว่าที่คิด เพราะสองหน่วยที่ factor เท่ากับ 1 เท่ากันมันทำได้ และจะมีคนทำ คำว่า "น้อยที่สุด" อย่างเดียวไม่ใช่ลำดับที่สมบูรณ์ ถ้าไม่มีตัวตัดสิน หน่วยฐานเปลี่ยนไปมาระหว่างรัน query เดิมสองครั้งได้ ซึ่งเป็นบ่ายที่สับสนกว่าการได้คำตอบผิดเฉย ๆ เยอะ

กฎรวม ๆ คือ ถ้าคำนวณค่านึงจากข้อมูลที่เก็บอยู่แล้วได้ การเก็บมันซ้ำแปลว่าต้องเขียนโค้ดที่มีหน้าที่เดียวคือทำให้ของสองชิ้นตรงกัน โค้ดแบบนั้นไม่มีวันเสร็จ

ต้นทุนคือ subquery ตอนอ่าน จ่ายด้วย index

CREATE INDEX ON product_units (product_id, conversion_factor);

ที่ยังเป็น correlated subquery ไม่เปลี่ยนเป็น LATERAL join เพราะเหตุผลที่ไม่หรูเลย ตัวกรองช่วงราคาของแคตตาล็อกผูกกับ inner join ของราคา ถ้าใช้ lateral ต้องรื้อ query จนตัวกรองนั้นเขียนยาก เวอร์ชันที่สวยกว่าไปสู้กับฟีเจอร์ที่หาเงินได้ เวอร์ชันสวยเลยแพ้

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

เติมกริดให้เต็ม อย่าให้มีรู

พอเปลี่ยนมาคำนวณ ความไม่สอดคล้องกลุ่มนึงหายไป แล้วเปิดอีกกลุ่มขึ้นมา สาขา × หน่วย คือกริด และกริดที่มีรูทำให้ join พัง

เพิ่มหน่วย ทุกสาขาต้องมีแถวใหม่ เพิ่มสาขา ทุกหน่วยที่มีอยู่ต้องมีแถวใหม่ จัดการตรง ๆ ทั้งสองทางตอนเขียน

  • สร้าง product unit → เพิ่มแถวให้ทุกสาขา
  • สร้างสาขา → เพิ่มแถวให้ทุกหน่วย

ใส่ด้วย ON CONFLICT DO NOTHING ค่าเริ่มต้นตั้งให้ใช้ไม่ได้โดยตั้งใจ ราคา 0 จำนวน 0 is_active false is_public false

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

เชิงอรรถที่กินเวลาไปครึ่งบ่าย การ upsert แถว branch product ต้องตั้ง updatedAt เอง เพราะ $onUpdate ของ ORM ไม่ทำงานบนเส้นทาง conflict update แถวเลยเก็บ timestamp เดิมไว้เงียบ ๆ ซึ่งก็ไม่เป็นไร จนกว่าจะมีอะไรปลายทาง sync ตาม updatedAt แล้วข้ามแถวพวกนั้นไปตลอดกาล

ได้อะไรกลับมา

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

สิ่งที่แลกไปมีจริง ฝั่งอ่านแบก subquery และคนที่เปิด schema มาอ่านครั้งแรกจะไม่เห็นทันทีว่าหน่วยไหนคือหน่วยฐาน ผมยอมแลกอีกรอบ เพราะกฎถูกบังคับด้วยรูปร่างของข้อมูล ไม่ใช่ด้วยการที่ทุกคนต้องจำได้