ERP

วิธีทำให้ Odoo หรือ ERPNext เร็วขึ้นอีกครั้ง: คู่มือแก้ปัญหาประสิทธิภาพเชิงปฏิบัติ

August 24, 2026

ERPNext ทุกระบบเร็วในวันแรกที่ใช้งาน ผ่านไปหกเดือนพร้อมฟิลด์กำหนดเองสี่สิบตัว ทีมขายก็เริ่มบ่นว่าหน้ารายการ Sales Order โหลดนานถึงแปดวินาที และมีใครสักคนเริ่มแอบทำสเปรดชีตสำรอง "ไว้ใช้ชั่วคราวจนกว่าจะแก้ปัญหาได้" นี่คือหนึ่งในทิกเก็ตซัพพอร์ตที่พบบ่อยที่สุด และแทบไม่เคยมีสาเหตุเดียว มักเป็นปัญหาเล็กๆ สามสี่อย่างซ้อนทับกัน

คู่มือนี้ครอบคลุมทั้งวิธีวินิจฉัยและวิธีแก้ไข สำหรับทั้ง Odoo และ ERPNext (Frappe) เพราะทั้งสองระบบมีรูปแบบปัญหาประสิทธิภาพที่คล้ายกันมากกว่าที่สแต็กเทคโนโลยีที่ต่างกันจะบ่งบอก ทั้งคู่เป็น Python ORM ที่วางอยู่บนฐานข้อมูลเชิงสัมพันธ์ ทั้งคู่รันอยู่หลัง worker process จำนวนจำกัด และทั้งคู่ช้าลงในจุดเดียวกันสามจุด นั่นคือ ฐานข้อมูล ชั้น worker และการปรับแต่งที่ซ้อนทับอยู่ด้านบน

1. วินิจฉัยก่อนปรับจูน

สัญชาตญาณแรกมักบอกให้ "เพิ่ม RAM" หรือ "เพิ่มจำนวน worker" แต่ควรอดใจไว้ก่อน การเพิ่มทรัพยากรให้ระบบที่ช้าเพราะขาดอินเด็กซ์ มีแต่จะทำให้คุณไปชนกำแพงเดิมได้เร็วขึ้นเท่านั้น

หาให้เจอก่อนว่าเวลาหายไปไหน:

  • ช้าแค่หน้าเดียวหรือทั้งระบบ? รายงานที่ช้าเพียงตัวเดียวมักชี้ไปที่คิวรีหรือสคริปต์ตัวใดตัวหนึ่ง ส่วนความช้าที่เกิดขึ้นทั้งระบบมักชี้ไปที่โครงสร้างพื้นฐาน — worker, การเชื่อมต่อฐานข้อมูล หรือ disk I/O
  • ช้าเฉพาะผู้ใช้บางคนหรือทุกคน? ผู้ใช้คนเดียวที่มีแดชบอร์ดส่วนตัวขนาดใหญ่ หรือฟิลเตอร์ที่บันทึกไว้ซึ่งดึงข้อมูล 50,000 แถว เป็นปัญหาคนละแบบกับทุกคนช้าพร้อมกันตอน 9 โมงเช้าวันจันทร์
  • Odoo: รันด้วย --log-level=debug_sql (หรือใช้ฟีเจอร์ Performance/Query Count ในโหมด developer) เพื่อดูจำนวนคิวรีและระยะเวลาต่อการเรียกหนึ่งครั้ง หน้าจอที่ยิงคิวรีมากกว่า 200 ครั้งคือปัญหาการปรับแต่ง ไม่ใช่ปัญหาโครงสร้างพื้นฐาน
  • ERPNext/Frappe: ใช้เครื่องมือ Recorder ที่มีมาให้ในตัว (bench --site [site] set-config recorder 1 แล้วเข้าไปที่ Developer Settings → Recorder ในหน้า UI) เพื่อบันทึกจำนวนคิวรี SQL และเวลาที่ใช้ต่อการร้องขอ ส่วน slow query log ของ MariaDB (slow_query_log = 1, long_query_time = 1) จะช่วยจับส่วนที่เหลือ

ถ้าจะทำอย่างเดียวก่อนไปแตะโครงสร้างพื้นฐาน ให้ทำสิ่งนี้: หาคิวรีหรือคำขอที่ช้าที่สุดห้าอันดับแรก แล้วสืบย้อนแต่ละอันกลับไปยัง DocType/model รายงาน หรือสคริปต์ที่เจาะจง

2. ชั้นฐานข้อมูล

ทั้งสองระบบวางน้ำหนักเกือบทั้งหมดไว้ที่ฐานข้อมูล — Odoo ใช้ PostgreSQL ส่วน ERPNext/Frappe ใช้ MariaDB เป็นค่าเริ่มต้น (Frappe รองรับ PostgreSQL ด้วยตั้งแต่ v14 เป็นต้นไป) ฐานข้อมูลจึงเป็นคอขวดตัวจริงบ่อยกว่าที่หลายคนคิด

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

  • Odoo: ตั้งค่า index=True ในนิยามฟิลด์ หรือเพิ่มผ่าน SQL/migration สำหรับข้อมูลที่มีอยู่แล้ว
  • Frappe: การตั้งค่า "in_list_view": 1 เพียงอย่างเดียว ไม่ได้ สร้างอินเด็กซ์ให้ฟิลด์ ต้องตั้งค่า "search_index": 1 ใน DocField หรือเพิ่มอินเด็กซ์ด้วยตนเองผ่าน bench --site [site] add-index

สถิติที่ล้าสมัยและข้อมูลพอกพูน PostgreSQL ต้องการ VACUUM ANALYZE เป็นประจำ (โดยทั่วไป autovacuum จะจัดการให้ แต่ควรตรวจสอบว่ามันทำงานจริงและไม่ตามหลังในตารางที่เขียนบ่อยอย่าง stock_move หรือ account_move_line) ส่วนตาราง MariaDB จะได้ประโยชน์จากการรัน ANALYZE TABLE และ OPTIMIZE TABLE เป็นระยะบนตารางที่อัปเดตบ่อย โดยเฉพาะตาราง Version และ Comment ของ Frappe ที่มักโตขึ้นมหาศาลและแทบไม่เคยถูกล้างข้อมูล

การเชื่อมต่อฐานข้อมูลหมด worker ของ Odoo และ gunicorn worker ของ Frappe แต่ละตัวจะถือการเชื่อมต่อฐานข้อมูลไว้หนึ่งชุด บนฐานข้อมูลระดับกลางๆ การเจอ max_connections เต็มภายใต้ภาระงานสูงเป็นเรื่องง่าย ซึ่งจะแสดงอาการเป็นการ timeout เป็นครั้งคราวมากกว่าความช้าที่สม่ำเสมอ ตัวช่วยพูลการเชื่อมต่อ (เช่น PgBouncer สำหรับ Odoo/PostgreSQL) เป็นแนวปฏิบัติมาตรฐานสำหรับระบบที่มีผู้ใช้พร้อมกันมากกว่าหยิบมือหนึ่ง

รายงานที่อ่านข้อมูลหนักบนฐานข้อมูลธุรกรรม รายงานตามกำหนดเวลาที่สแกนข้อมูล Sales Invoice Item ย้อนหลังหนึ่งปี จะไปแย่งทรัพยากรที่ทีมขายต้องการใช้อยู่ตอนนั้น หากมีรายงานหนักหรือแดชบอร์ด BI ที่รันเป็นประจำ ควรชี้ให้ไปอ่านจาก read replica แทนฐานข้อมูลหลัก

3. ชั้น worker

ทั้ง Odoo และ Frappe เป็นแอปพลิเคชัน WSGI ที่รันอยู่หลัง worker process จำนวนจำกัด — ขนาดของพูลนี้คือตัวแปรโครงสร้างพื้นฐานที่สำคัญที่สุด และมักตั้งค่าผิดอยู่บ่อยครั้ง

Worker ของ Odoo สูตรมาตรฐานคือ workers = (จำนวน CPU core × 2) + 1 โดยสำรอง worker ประมาณ 20% ไว้สำหรับงาน cron (max_cron_threads) แต่ละ worker เป็นโพรเซสแยกที่มีพื้นที่หน่วยความจำของตัวเอง (--limit-memory-soft / --limit-memory-hard ช่วยป้องกันคำขอที่ใช้หน่วยความจำมากเกินไป) หากจัดสรร RAM ต่อ worker ไม่เพียงพอ worker จะถูกฆ่าและรีสตาร์ทกลางคัน ซึ่งดูเหมือนความช้าแบบสุ่มหรือ 502 เป็นครั้งคราว คำขอที่ใช้เวลานาน (การนำเข้าข้อมูลขนาดใหญ่ การสร้าง PDF) ควรผ่านกลไก longpolling/คิวแยกต่างหาก แทนที่จะไปจอง worker ปกติไว้

Worker ของ Frappe/ERPNext bench รันพูล gunicorn สำหรับคำขอเว็บ บวกกับ RQ (Redis Queue) worker แยกต่างหากสำหรับงานเบื้องหลัง ซึ่งแบ่งเป็นคิว short, default และ long รูปแบบความผิดพลาดที่พบบ่อย: มีคนตั้งเวลางานหนัก (ส่งอีเมลจำนวนมาก นำเข้าข้อมูลขนาดใหญ่ สร้าง PDF เป็นชุด) ไปที่คิว default แล้วมันไปกีดขวางงานเบื้องหลังอื่นๆ ที่ต่อคิวอยู่ด้านหลัง ควรจัดงานที่ใช้เวลานานให้ไปที่คิว long โดยตรง และตรวจสอบให้แน่ใจว่ามี worker process เพียงพอต่อปริมาณงานเบื้องหลังของแต่ละคิว — ค่านี้ตั้งใน Procfile/supervisor.conf และไม่ได้ปรับขยายอัตโนมัติ

ชั้น socket/realtime การอัปเดตแบบเรียลไทม์ของ Frappe และ longpolling/bus ของ Odoo ต่างพึ่งพา Redis และโพรเซสเฉพาะ (node สำหรับ socketio ของ Frappe, longpolling worker สำหรับ Odoo) หากโพรเซสนี้ล้ม อาการที่ผู้ใช้มักรายงานคือ "หน้าจอเหมือนค้าง" มากกว่าจะเป็นข้อความ error ที่ชัดเจน

4. การแคชที่แยกส่วนอย่างถูกต้อง

ทั้งสองระบบใช้ Redis สำหรับมากกว่าหนึ่งวัตถุประสงค์ — โดยทั่วไปคือแคช คิวงานเบื้องหลัง และ pub/sub สำหรับเรียลไทม์ — และข้อผิดพลาดในการตั้งค่าที่พบบ่อยคือการชี้ทั้งสามอย่างไปที่ Redis instance/database number เดียวกันโดยไม่จำกัดหน่วยความจำ การที่แคชถูกล้างเมื่อหน่วยความจำตึงตัวอาจทำให้งานเบื้องหลังที่รอคิวอยู่หายไปด้วย ควรใช้ Redis database แยก (หรือ instance แยก) สำหรับแคชกับคิวงาน และตั้งค่า maxmemory พร้อม eviction policy ที่เหมาะสมเฉพาะสำหรับ instance แคช

สำหรับ ERPNext ควรตรวจสอบว่า redis_cache, redis_queue และ redis_socketio ถูกชี้ไปที่ database number ที่แยกจากกันจริงใน site_config.json — ระบบที่ดูแลเองจำนวนมากมักปล่อยไว้ตามค่าเริ่มต้นของ bench ซึ่งใช้ได้ดีสำหรับสภาพแวดล้อมพัฒนาระบบเดียว แต่ไม่เหมาะสำหรับระบบโปรดักชันที่มีภาระงานจริง

5. รูปแบบการปรับแต่งที่ไม่แสดงอาการจนกว่าจะขยายขนาด

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

  • คิวรีแบบ N+1 ในสคริปต์ฝั่งเซิร์ฟเวอร์/สคริปต์กำหนดเอง ลูปที่เรียก frappe.get_doc() หรือ self.env['res.partner'].browse() ทีละแถวแทนที่จะดึงเป็นชุด จะไม่มีปัญหาเมื่อมี 10 แถว แต่จะกลายเป็นหายนะเมื่อมี 10,000 แถว ควรดึงข้อมูลเป็นชุดด้วย frappe.get_all() / search_read() ก่อนวนลูปเสมอ
  • Client script ที่คิวรีเซิร์ฟเวอร์ทุกครั้งที่กดคีย์บอร์ดหรือเปลี่ยนค่าฟิลด์ โดยเฉพาะบนหน้ารายการที่มีแถวแสดงผลจำนวนมาก แต่ละครั้งคือการเดินทางไปกลับ (round trip) หนึ่งรอบ
  • คิวรีของหน้ารายการ/รายงานที่กว้างเกินความจำเป็น: ดึงทุกคอลัมน์ของ DocType/model ที่มีฟิลด์เยอะ ทั้งที่แสดงผลจริงแค่สามคอลัมน์ หรือดึงข้อมูลแถวของ child table ล่วงหน้าทั้งที่แทบไม่มีใครเปิดดู
  • กฎสิทธิ์และการแชร์ที่ต้องประเมินทีละแถว เมื่อข้อมูลมีขนาดใหญ่ — user permission และ record rule เป็นเครื่องมือที่ทรงพลัง แต่กฎที่มี subquery ประเมินทีละแถวบนรายการ 100,000 แถว ก็จะช้าตามที่ฟังดู
  • การสร้างรูปแบบการพิมพ์และ PDF ที่รันแบบ synchronous ในรอบการร้องขอ แทนที่จะเป็นงานเบื้องหลัง — ไม่มีปัญหาถ้าพิมพ์ใบแจ้งหนี้ทีละใบ แต่จะทรมานมากถ้าเป็นการกระทำแบบชุด "พิมพ์ใบแจ้งหนี้ 200 ใบ"

วิธีแก้สำหรับทั้งหมดนี้เหมือนกัน: โปรไฟล์ก่อน (ตามข้อ 1) หาสคริปต์หรือคิวรีที่เจาะจง แล้วแก้ไขมัน ไม่ใช่ "อัดเซิร์ฟเวอร์ให้ใหญ่ขึ้นแล้วหวังว่าจะดีขึ้นเอง"

6. Filestore และไฟล์แนบ

ทั้งสองระบบเก็บไฟล์แนบไว้บนดิสก์เครื่องแอปพลิเคชันโดยเป็นค่าเริ่มต้น ซึ่งใช้ได้ดีจนกว่าจะไม่ได้ผล: ดิสก์บนเครื่องแอปไม่สามารถขยายแนวนอนได้ การสำรองข้อมูลจะช้าลง และบนบางระบบจัดเก็บข้อมูลผ่านเครือข่าย ความหน่วงของ filestore I/O จะทำให้เอกสารทุกฉบับที่มีตัวอย่างไฟล์แนบช้าลงโดยตรง สำหรับการติดตั้งที่เกินกว่าเซิร์ฟเวอร์เดียว ควรย้ายไฟล์แนบไปยัง object storage (แบบเข้ากันได้กับ S3) — ทั้ง Odoo (ผ่าน add-on filestore) และ Frappe (รองรับ S3 backup/attachment โดยตรง) รองรับสิ่งนี้ในตัว

7. การกำหนดขนาดโครงสร้างพื้นฐานและการขยายแนวนอน

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

  • ดิสก์สำคัญกว่าที่หลายคนคิด ประสิทธิภาพฐานข้อมูลบนดิสก์แบบจานหมุนหรือระดับ cloud IOPS ที่จัดสรรไม่เพียงพอ จะเป็นคอขวดของทุกสิ่งที่คุณแก้ไขไปแล้ว สตอเรจแบบ SSD ที่มี IOPS เพียงพอไม่ใช่ทางเลือกสำหรับฐานข้อมูล ERP โปรดักชันที่มีทีมงานเกินขนาดเล็ก
  • การขยายแนวนอนต้องมี shared state การเพิ่มเซิร์ฟเวอร์แอปตัวที่สองหมายความว่า filestore และชั้น session/cache ต้องถูกแชร์ร่วมกัน (object storage + Redis ที่ใช้ร่วมกัน) ไม่เช่นนั้นผู้ใช้จะเห็นพฤติกรรมไม่สม่ำเสมอขึ้นอยู่กับว่าไปตกที่โหนดไหน
  • Static asset ควรอยู่หลัง CDN หรืออย่างน้อยที่สุดคือ reverse proxy ที่แคชอย่างจริงจัง (ให้ nginx เสิร์ฟ /assets โดยตรงแทนที่จะ proxy ทุกคำขอ JS/CSS ผ่าน worker ของแอป)

รายการตรวจสอบเพื่อการวินิจฉัย

  1. จำลองอาการช้าให้เกิดซ้ำ แล้วระบุว่า: ช้าแค่หน้าเดียวหรือทุกหน้า, ผู้ใช้คนเดียวหรือทุกคน
  2. เปิดการโปรไฟล์คิวรี (Odoo debug_sql, Frappe Recorder) แล้วหาคำขอที่ช้าที่สุดห้าอันดับแรก
  3. ตรวจสอบ slow query log ของ MariaDB/PostgreSQL ในช่วงเวลาเดียวกัน
  4. สำหรับแต่ละคิวรีที่ช้า: มีฟิลด์กำหนดเองที่ใช้ใน WHERE clause ที่ขาดอินเด็กซ์หรือไม่
  5. ตรวจสอบสถานะ worker/คิว: worker ของ Odoo ถูกฆ่าเพราะหน่วยความจำเต็มหรือไม่ หรือคิวงานเบื้องหลังของ Frappe ค้างอยู่หรือไม่
  6. ตรวจสอบ Redis: แคชกำลังแย่งหน่วยความจำกับคิวงานอยู่หรือไม่
  7. ตรวจหารูปแบบ N+1 ในสคริปต์กำหนดเองที่เกี่ยวข้องกับเส้นทางที่ช้า
  8. หลังจากทำข้อ 1–7 แล้วเท่านั้น: จึงค่อยพิจารณาเพิ่ม CPU/RAM/worker

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


แหล่งที่มา:

Ready to talk about your project?

Share goals and constraints. We'll assemble architects and engineers to move fast with you.

Get in touch