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 ของแอป)
รายการตรวจสอบเพื่อการวินิจฉัย
- จำลองอาการช้าให้เกิดซ้ำ แล้วระบุว่า: ช้าแค่หน้าเดียวหรือทุกหน้า, ผู้ใช้คนเดียวหรือทุกคน
- เปิดการโปรไฟล์คิวรี (Odoo
debug_sql, Frappe Recorder) แล้วหาคำขอที่ช้าที่สุดห้าอันดับแรก - ตรวจสอบ slow query log ของ MariaDB/PostgreSQL ในช่วงเวลาเดียวกัน
- สำหรับแต่ละคิวรีที่ช้า: มีฟิลด์กำหนดเองที่ใช้ใน
WHEREclause ที่ขาดอินเด็กซ์หรือไม่ - ตรวจสอบสถานะ worker/คิว: worker ของ Odoo ถูกฆ่าเพราะหน่วยความจำเต็มหรือไม่ หรือคิวงานเบื้องหลังของ Frappe ค้างอยู่หรือไม่
- ตรวจสอบ Redis: แคชกำลังแย่งหน่วยความจำกับคิวงานอยู่หรือไม่
- ตรวจหารูปแบบ N+1 ในสคริปต์กำหนดเองที่เกี่ยวข้องกับเส้นทางที่ช้า
- หลังจากทำข้อ 1–7 แล้วเท่านั้น: จึงค่อยพิจารณาเพิ่ม CPU/RAM/worker
อาการ ERP ช้าส่วนใหญ่แก้ได้โดยไม่ต้องอัปเกรดฮาร์ดแวร์ ฐานข้อมูลและชั้นการปรับแต่งเป็นสาเหตุของทิกเก็ตส่วนใหญ่ในโลกจริง — การกำหนดขนาดโครงสร้างพื้นฐานมักเป็นห้าเปอร์เซ็นต์สุดท้าย ไม่ใช่สิ่งแรกที่ควรคว้ามาแก้ปัญหา
แหล่งที่มา:
- Odoo Deployment Documentation — Worker configuration
- Frappe Framework — Background Jobs
- Frappe Framework — Recorder
- PostgreSQL Documentation — Routine Vacuuming
บทความล่าสุด
- การลงทะเบียน Passkey คือช่องโหว่ใหม่ขององค์กรคุณ: บทเรียนจาก Pass-the-Passkey สำหรับผู้ดูแล IdP August 22, 2026
- ผู้โจมตีคุณอาจเป็น AI Agent ไปแล้ว: บทเรียนจากเหตุการณ์ OpenAI–Hugging Face ที่ทีม SOC ต้องรู้ August 17, 2026
- การพัฒนา OCPI 2.2.1: คู่มือสำหรับนักพัฒนาในการทำ Locations, Sessions และ CDR August 7, 2026
- ทำความเข้าใจ OCPI: สิ่งที่ CPO และ eMSP ต้องสร้างจริงเพื่อรองรับการชาร์จข้ามเครือข่าย August 7, 2026
- ปลากะพงกลางแผ่นดิน: สร้างระบบให้อาหารอัตโนมัติสำหรับปลาทะเลที่ไกลจากทะเล July 31, 2026
- โรงงานของคุณพูดได้ห้าภาษา: ทำไม OPC UA เพียงอย่างเดียวแก้ปัญหาโปรโตคอลที่กระจัดกระจายไม่ได้ July 30, 2026