คู่มือปรับแต่งประสิทธิภาพ Go + Postgres สำหรับ API ที่สร้างด้วย AI: ตั้ง pool การเชื่อมต่อ, อ่านแผนคิวรี, ทำดัชนีอย่างชาญฉลาด, แบ่งหน้าอย่างปลอดภัย และจัดรูปแบบ JSON ให้เร็วขึ้น

API ที่สร้างจาก AI มักรู้สึกเร็วในการทดสอบแรก ๆ คุณเรียก endpoint สักสองสามครั้ง ข้อมูลยังน้อย และคำขอเข้ามาทีละเรื่อง พอมีทราฟฟิกจริง: endpoints หลายแบบ โหลดเป็นช่วง ๆ แคชเย็นกว่าเดิม และแถวข้อมูลมากกว่าที่คาดไว้ โค้ดเดิมก็เริ่มรู้สึกช้าบ้างเป็นบางครั้ง ถึงแม้จะไม่มีอะไรพังจริง ๆ
ความช้ามักแสดงออกไม่กี่แบบ: latency พุ่ง (request ส่วนใหญ่โอเค แต่บางอันใช้เวลา 5x ถึง 50x มากกว่าเดิม), เกิด timeout (ส่วนน้อยล้มเหลว), หรือ CPU รันหนัก (CPU ของ Postgres จากงานคิวรี หรือ CPU ของ Go จากการจัดการ JSON, goroutine, logging และ retries).
สถานการณ์ปกติคือ endpoint แบบรายการที่มีฟิลเตอร์ยืดหยุ่นและคืน JSON ขนาดใหญ่ ในฐานข้อมูลทดสอบมันสแกนไม่กี่พันแถวและจบเร็ว แต่ใน production มันสแกนเป็นล้านแถว จัดเรียง แล้วค่อย apply LIMIT API ยัง "ทำงาน" แต่ p95 latency ระเบิดและบางคำขอ timeout ในช่วงพีค
เพื่อแยกความช้าจากฐานข้อมูลกับความช้าจากแอป ให้เก็บโมเดลความคิดให้เรียบง่าย
ถ้าฐานข้อมูลช้า handler ใน Go ส่วนใหญ่รอกระบวนการคิวรี คุณอาจเห็นคำขอหลายรายการค้าง "in flight" ในขณะที่ Go CPU ดูปกติ
ถ้าแอปช้า คิวรีกลับมารวดเร็ว แต่เวลาเสียหลังคิวรี: สร้างออบเจ็กต์ขนาดใหญ่, marshal JSON, รันคิวรีต่อแถวหลายตัว, หรือทำงานมากเกินไปต่อคำขอ Go CPU เพิ่มขึ้น หน่วยความจำเพิ่ม และ latency โตตามขนาด response
"พอใช้" ก่อนปล่อยไม่ต้องสมบูรณ์แบบ สำหรับหลาย ๆ CRUD endpoints ตั้งเป้า p95 ที่นิ่ง (ไม่ใช่แค่อัตราเฉลี่ย), พฤติกรรมที่คาดเดาได้ตอนเกิด burst, และไม่มี timeout ในพีคที่คาดไว้ เป้าหมายชัดเจน: ไม่มีคำขอช้าที่น่าตกใจเมื่อข้อมูลและทราฟฟิกเพิ่ม และมีสัญญาณชัดเจนเมื่อมีบางอย่างเปลี่ยนไป
ก่อนจะปรับแต่งอะไร ให้ตัดสินใจว่า "ดี" คืออะไรสำหรับ API ของคุณ ถ้าไม่มี baseline ง่ายที่จะใช้เวลาหลายชั่วโมงเปลี่ยนค่าต่าง ๆ แล้วไม่รู้ว่าได้ปรับปรุงจริงหรือแค่ย้ายคอขวด
ตัวเลขสามตัวมักบอกเรื่องได้มากที่สุด:
p95 คือเมตริกาของ "วันที่แย่" ถ้า p95 สูงแต่ค่าเฉลี่ยโอเค แสดงว่าชุดคำขอเล็ก ๆ กำลังทำงานหนักเกิน, ถูกบล็อกโดยล็อก หรือทำให้เกิดแผนช้า
ทำให้คิวรีช้าเห็นได้ตั้งแต่ต้น ใน Postgres ให้เปิด slow query logging ด้วย threshold ต่ำสำหรับการทดสอบก่อนปล่อย (เช่น 100–200 ms) และล็อก statement เต็มทั้งก้อนเพื่อคุณจะคัดลอกไปยัง client SQL ได้ เก็บการตั้งค่านี้ไว้ชั่วคราว เพราะการล็อกทุก slow query ใน production จะเสียงดังเร็ว
ถัดมา ทดสอบด้วยคำขอที่เหมือนของจริง ไม่ใช่แค่ route "hello world" หนึ่งตัว ชุดเล็ก ๆ ก็พอถ้ามันสอดคล้องกับสิ่งที่ผู้ใช้จะทำ: การเรียกรายการพร้อมฟิลเตอร์และการเรียง, หน้ารายละเอียดมี join สองสามตัว, create/update ที่มีการตรวจสอบความถูกต้อง, และคิวรีแบบ search ที่ใช้ partial match
ถ้าคุณสร้าง endpoints จากสเปค (เช่น ด้วยเครื่องมือสร้างโค้ดอย่าง Koder.ai) ให้รันคำขอชุดเดียวกันซ้ำ ๆ ด้วยอินพุตคงที่ นั่นทำให้การเปลี่ยนแปลงเช่นดัชนี การปรับ pagination และการเขียนคิวรีวัดผลได้ง่ายขึ้น
สุดท้าย เลือกเป้าหมายที่คุณบอกออกมาเป็นคำพูดได้ ตัวอย่าง: "คำขอส่วนใหญ่อยู่ใต้ 200 ms p95 ที่ 50 concurrent users และ error ต่ำกว่า 0.5%" ตัวเลขจริงขึ้นอยู่กับผลิตภัณฑ์ แต่มีเป้าหมายชัดเจนช่วยหยุดการปรับจูนที่ไร้ที่สิ้นสุด
Connection pool เก็บจำนวนการเชื่อมต่อฐานข้อมูลที่เปิดจำกัดและใช้ซ้ำ เมื่อไม่มี pool แต่ละคำขออาจเปิดการเชื่อมต่อใหม่ และ Postgres จะเสียเวลาและหน่วยความจำไปกับการจัดการ session แทนที่จะรันคิวรี
เป้าหมายคือให้ Postgres ทำงานที่มีประโยชน์ ไม่ใช่สลับบริบทระหว่างการเชื่อมต่อจำนวนมาก นี่มักเป็นชัยชนะที่จับต้องได้เป็นครั้งแรก โดยเฉพาะกับ API ที่สร้างจาก AI ซึ่งเงียบ ๆ กลายเป็น endpoints ที่คุยกันเยอะ
ใน Go ปกติคุณจะปรับ max open connections, max idle connections, และ connection lifetime ค่าเริ่มต้นที่ปลอดภัยสำหรับ API ขนาดเล็กหลายตัวคือหลายเท่าของจำนวนคอร์ CPU (มัก 5 ถึง 20 การเชื่อมต่อทั้งหมด), เก็บ idle ใกล้เคียงกัน, และรีไซเคิลการเชื่อมต่อเป็นระยะ (เช่น ทุก 30–60 นาที)
ถ้าคุณรันหลาย instance ของ API จำไว้ว่าพูลจะทวีคูณ พูล 20 connection ข้าม 10 instance กลายเป็น 200 connection ที่กระหน่ำ Postgres นี่คือสาเหตุที่ทีมมักเจอขีดจำกัด connection โดยไม่คาดคิด
ปัญหาพูลให้ความรู้สึกต่างจาก SQL ช้า
ถ้าพูลเล็กเกินไป คำขอจะรอก่อนจะเข้า Postgres Latency พุ่ง แต่ CPU และเวลา query ของ DB อาจดูปกติ
ถ้าพูลใหญ่เกินไป Postgres ดูโอเวอร์โหลด: session จำนวนมากใช้งาน หน่วยความจำกดดัน และ latency ไม่สม่ำเสมอระหว่าง endpoints
วิธีง่าย ๆ ในการแยกคือจับเวลาการเรียก DB เป็นสองส่วน: เวลาที่รอสายการเชื่อมต่อ vs เวลาที่ใช้รันคิวรี ถ้าส่วนใหญ่อยู่ใน "การรอ" พูลคือคอขวด ถ้าส่วนใหญ่อยู่ใน "รันคิวรี" ให้โฟกัสที่ SQL และดัชนี
เช็ครวดเร็วที่ใช้ได้:
max_connections แค่ไหนถ้าใช้ pgxpool คุณได้พูลที่ออกแบบมาสำหรับ Postgres มีสถิติชัดเจนและค่าดีฟอลต์ที่เหมาะกับพฤติกรรมของ Postgres ถ้าใช้ database/sql คุณได้อินเทอร์เฟซมาตรฐานที่ทำงานกับหลายฐานข้อมูล แต่ต้องตั้งค่าพูลและพฤติกรรมของไดรเวอร์ให้ชัดเจน
กฎปฏิบัติ: ถ้าคุณใช้ Postgres เต็มตัวและต้องการควบคุมโดยตรง pgxpool มักง่ายกว่า ถ้าคุณพึ่งไลบรารีที่คาดว่าใช้ database/sql ก็ใช้มัน แต่ตั้งพูลให้ชัดและวัดการรอ
ตัวอย่าง: endpoint ที่แสดง orders อาจวิ่ง 20 ms แต่ภายใต้ 100 concurrent users กระโดดเป็น 2 s ถ้าล็อกแสดงว่า 1.9 s ใช้ไปกับการรอ connection การปรับคิวรีจะไม่ช่วยจนกว่าจะปรับพูลและจำนวน connection ทั้งหมดให้ถูกต้อง
เมื่อ endpoint รู้สึกช้า ให้เช็กว่า Postgres ทำอะไรอยู่จริง ๆ การอ่าน EXPLAIN อย่างเร็วมักชี้จุดแก้ได้ในไม่กี่นาที
รันบน SQL ที่ API ส่งจริง:
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, status, created_at
FROM orders
WHERE user_id = $1 AND status = $2
ORDER BY created_at DESC
LIMIT 50;
บรรทัดไม่กี่บรรทัดมีความหมายที่สุด ดูโหนดบนสุด (ที่ Postgres เลือก) และตัวเลขรวมด้านล่าง (ใช้เวลาเท่าไร) แล้วเทียบ estimated vs actual rows ช่องว่างใหญ่ ๆ มักหมายถึง planner ทายผิด
ถ้าเห็น Index Scan หรือ Index Only Scan แปลว่า Postgres ใช้ดัชนี ซึ่งมักดี Bitmap Heap Scan พอใช้ได้กับการแมตช์ขนาดกลาง Seq Scan หมายถึงอ่านทั้งตาราง ซึ่งใช้ได้เมื่อโต๊ะเล็กหรือเกือบทุกแถวตรงเงื่อนไข
สัญญาณเตือนทั่วไป:
ORDER BY)แผนช้าส่วนใหญ่มาจากรูปแบบไม่กี่อย่าง:
WHERE + ORDER BY ของคุณ (เช่น (user_id, status, created_at))WHERE (เช่น WHERE lower(email) = $1) ซึ่งอาจบังคับให้สแกนเว้นแต่จะเพิ่ม expression index ที่ตรงกันถ้าแผนดูแปลกและ estimate ผิดมาก สถิติอาจล้าสมัย รัน ANALYZE (หรือรอ autovacuum) ให้ Postgres เรียนรู้จำนวนแถวและการแจกแจงค่าปัจจุบัน สิ่งนี้สำคัญหลัง import ขนาดใหญ่หรือเมื่อ endpoint ใหม่เริ่มเขียนข้อมูลจำนวนมากอย่างรวดเร็ว
ดัชนีช่วยได้เมื่อมันตรงกับวิธีที่ API ของคุณเรียกข้อมูล ถ้าคุณสร้างจากการเดา คุณจะได้เขียนช้าลง พื้นที่เก็บเพิ่ม และได้ประสิทธิภาพไม่คุ้มค่า
คิดง่าย ๆ ว่า: ดัชนีคือทางลัดสำหรับคำถามเฉพาะ ถ้า API ของคุณถามคำถามอื่น Postgres ก็จะไม่ใช้ทางลัดนั้น
ถ้า endpoint กรองด้วย account_id และเรียงด้วย created_at DESC ดัชนีเชิงประกอบเดียวมักชนะสองดัชนีแยก มันช่วยให้ Postgres หาแถวที่ถูกต้องและคืนผลในลำดับที่ต้องการด้วยงานน้อยกว่า
กฎง่าย ๆ ที่มักใช้ได้:
ตัวอย่าง: ถ้า API ของคุณมี GET /orders?status=paid และแสดงล่าสุดก่อน ดัชนี (status, created_at DESC) จะเหมาะ หากการคิวรีส่วนใหญ่กรองด้วย customer ด้วย (customer_id, status, created_at) อาจดีกว่า แต่เฉพาะถ้านั่นคือรูปแบบการรันจริงใน production
ถ้าทราฟฟิกส่วนใหญ่เข้าถึงชิ้นเล็ก ๆ ของแถว ดัชนีแบบ partial อาจถูกกว่าและเร็วกว่า ตัวอย่าง: ถ้าแอปอ่านเฉพาะเรคอร์ดที่ active = true การทำดัชนีเฉพาะ WHERE active = true จะทำให้ดัชนีเล็กกว่าและมีโอกาสอยู่ในหน่วยความจำมากขึ้น
เพื่อยืนยันว่าดัชนีช่วย ให้ทำการเช็กง่าย ๆ:
EXPLAIN (หรือ EXPLAIN ANALYZE ในสภาพแวดล้อมที่ปลอดภัย) และมองหาการใช้ index scan ที่ตรงกับคิวรีของคุณลบดัชนีที่ไม่ได้ใช้ด้วยความระมัดระวัง ตรวจสอบสถิติการใช้งาน (เช่น ดัชนีถูกสแกนหรือไม่) ลบทีละตัวในช่วงเวลาความเสี่ยงต่ำ และมีแผนย้อนกลับ ดัชนีที่ไม่ได้ใช้ไม่ไร้พิษภัย พวกมันชะลอการ insert และ update ทุกครั้งที่เขียน
การแบ่งหน้าเป็นจุดที่ API ที่เร็วเริ่มรู้สึกช้า ถึงแม้ว่าฐานข้อมูลจะยังแข็งแรง ให้มองการแบ่งหน้าเป็นปัญหาการออกแบบคิวรี ไม่ใช่รายละเอียด UI
LIMIT/OFFSET ดูเรียบง่าย แต่หน้าลึกมักมีค่าใช้จ่ายมากขึ้น Postgres ยังต้องเดินผ่าน (และมักจะจัดเรียง) แถวที่คุณข้าม หน้า 1 อาจแตะไม่กี่สิบแถว หน้า 500 อาจบังคับให้ดาต้าเบสสแกนและทิ้งหลักหมื่นแถวเพื่อคืน 20 ผลลัพธ์
มันยังสร้างผลลัพธ์ไม่เสถียรเมื่อแถวถูกเพิ่มหรือลบระหว่างคำขอ ผู้ใช้เห็นผลซ้ำหรือข้ามรายการได้ เพราะความหมายของ "แถวที่ 10,000" เปลี่ยนแปลงเมื่อโต๊ะเปลี่ยน
Keyset pagination ถามคำถามต่างออกไป: "ให้ฉัน 20 แถวถัดไปหลังจากแถวสุดท้ายที่ฉันเห็น" นั่นทำให้ฐานข้อมูลทำงานบนสไลซ์เล็กและคงที่
เวอร์ชันง่ายใช้ id เพิ่มขึ้น:
SELECT id, created_at, title
FROM posts
WHERE id > $1
ORDER BY id
LIMIT 20;
API ของคุณคืน next_cursor เท่ากับ id สุดท้ายในหน้า คำขอต่อมาจะใช้ค่านั้นเป็น $1
สำหรับการเรียงตามเวลา ให้ใช้ลำดับที่เสถียรและแก้ปัญหา tie-breaker created_at อย่างเดียวไม่พอถ้าสองแถวมี timestamp เดียวกัน ให้ใช้ compound cursor:
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 20;
กฎไม่กี่ข้อป้องกันการซ้ำหรือหายไปของรายการ:
ORDER BY เสมอ (มักเป็น id)created_at และ id รวมกัน)เหตุผลที่คาดไม่ถึงที่ทำให้ API รู้สึกช้าบ่อยครั้งไม่ใช่ฐานข้อมูล แต่เป็น response JSON ขนาดใหญ่ การสร้าง JSON ใหญ่ใช้เวลามากขึ้น ส่งช้าลง และ client ต้อง parse นานขึ้น ชัยชนะที่เร็วที่สุดมักคือการส่งน้อยลง
เริ่มที่ SELECT ถ้า endpoint ต้องการแค่ id, name, และ status ให้เลือกเฉพาะคอลัมน์เหล่านั้น SELECT * จะหนักขึ้นเงียบ ๆ เมื่อโต๊ะเพิ่ม text ยาว, JSON blob, และคอลัมน์ audit
อีกจุดชะงักบ่อยคือการสร้างตอบแบบ N+1: คุณดึงรายการ 50 รายการ แล้วรันคิวรี 50 ครั้งเพื่อแนบข้อมูลที่เกี่ยวข้อง มันอาจผ่านการทดสอบ แต่ล่มในทราฟฟิกจริง ชอบคิวรีเดียวที่คืนสิ่งที่ต้องการ (join อย่างระมัดระวัง) หรือสองคิวรีที่ batch ตาม IDs
วิธีลด payload โดยไม่ทำลาย client:
include= flag (หรือ fields= mask) ให้ list responses เบา และ detail responses รับส่วนเสริมทั้งสองแบบอาจเร็ว เลือกตามสิ่งที่คุณต้องการเพิ่มประสิทธิภาพ
ฟังก์ชัน JSON ของ Postgres (jsonb_build_object, json_agg) มีประโยชน์เมื่อคุณต้องการรอบการร้องขอน้อยลงและรูปร่างที่คาดเดาได้จากคิวรีเดียว การปั้นใน Go เหมาะเมื่อคุณต้องการเงื่อนไขเชิงตรรกะ ตัวอย่างซ้ำโครงสร้าง struct หรือต้องการให้ SQL อ่านง่ายขึ้น ถ้า SQL ที่สร้าง JSON ยากอ่าน มันยากที่จะปรับจูน
กฎดี ๆ คือ: ให้ Postgres กรอง เรียง และรวมข้อมูล แล้วให้ Go จัดการการนำเสนอขั้นสุดท้าย
ถ้าคุณสร้าง API อย่างรวดเร็ว (เช่น กับ Koder.ai) การใส่ include flags ตั้งแต่ต้นช่วยหลีกเลี่ยง endpoints ที่บวมเมื่อเวลาผ่านไป และให้วิธีปลอดภัยในการเพิ่มฟิลด์โดยไม่ทำให้ทุก response หนักขึ้น
คุณไม่ต้องมีแล็บทดสอบใหญ่เพื่อจับปัญหาส่วนใหญ่ การผ่านแบบสั้น ๆ และทำซ้ำได้จะเผยปัญหาที่ทำให้เกิด outage เมื่อทราฟฟิกมา โดยเฉพาะเมื่อจุดเริ่มต้นคือโค้ดที่สร้างอัตโนมัติและคุณตั้งใจจะปล่อย
ก่อนเปลี่ยนอะไร จด baseline เล็ก ๆ:
เริ่มจากเล็ก ๆ เปลี่ยนทีละอย่าง แล้วทดสอบซ้ำหลังแต่ละการเปลี่ยน
รัน load test 10–15 นาทีที่เหมือนการใช้งานจริง กระทบบริการที่ผู้ใช้แรกจะใช้ (login, list pages, search, create) แล้วจัดเรียง route ตาม p95 latency และเวลารวม
ตรวจเช็กแรงกดดัน connection ก่อนปรับ SQL พูลใหญ่เกินไปทำให้ Postgres โอเวอร์โหลด พูลเล็กเกินไปทำให้รอนาน มองหาเวลาการรอรับ connection ที่เพิ่มขึ้นและจำนวนการเชื่อมต่อที่พุ่งช่วง burst ปรับพูลและ idle limits ก่อน แล้วรันโหลดซ้ำ
EXPLAIN คิวรีที่ช้าที่สุดและแก้ปัญหา red flag ที่ใหญ่ที่สุด ผู้ร้ายส่วนใหญ่อาจเป็น full table scans บนตารางใหญ่, sort บนชุดผลขนาดใหญ่, และ joins ที่เพิ่มแถวอย่างรวดเร็ว เลือกคิวรีที่แย่ที่สุดแล้วทำให้มันเรียบง่าย
เพิ่มหรือปรับดัชนีทีละอัน แล้วทดสอบใหม่ ดัชนีช่วยเมื่อมันตรงกับ WHERE และ ORDER BY อย่าเพิ่มทีละหลายตัว ถ้า endpoint ช้าเป็น "list orders by user_id ordered by created_at" ดัชนีประกอบ (user_id, created_at) อาจต่างระหว่างเร็วทันทีกับช้ามาก
เบียดการตอบและการแบ่งหน้าให้เข้มขึ้น แล้วทดสอบอีกครั้ง ถ้า endpoint คืน 50 แถวพร้อม JSON ใหญ่ ฐานข้อมูล เครือข่าย และ client จะจ่ายทั้งหมด คืนเฉพาะฟิลด์ที่ UI ต้องการ และเลือก pagination ที่ไม่ช้าลงตามการเติบโตของตาราง
เก็บ change log ง่าย ๆ: เปลี่ยนอะไร ทำไม และอะไรที่เคลื่อนไหวใน p95 ถ้าการเปลี่ยนไม่มีผลดีให้ย้อนกลับและไปต่อ
ปัญหาประสิทธิภาพใน Go APIs บน Postgres ส่วนใหญ่เกิดจากการกระทำของตัวเอง ข้อดีคือการเช็กไม่กี่อย่างจับปัญหาได้ก่อนทราฟฟิกจริง
กับดักคลาสสิกคือการมองขนาดพูลเป็นปุ่มเพิ่มความเร็ว การตั้งเป็น "มากที่สุดเท่าที่จะทำได้" มักทำให้ช้าลง Postgres เสียเวลาไปกับการจัดการ session หน่วยความจำ และล็อก และแอปเริ่ม timeout เป็นลูกคลื่น พูลขนาดเล็กและคงที่ที่มีความขนานที่คาดเดาได้มักชนะ
ความผิดพลาดอีกอย่างคือ "ทำดัชนีทุกอย่าง" ดัชนีพิเศษอาจช่วยอ่าน แต่ก็ชะลอการเขียนและอาจเปลี่ยนแผนคิวรีในทางที่ไม่คาดคิด ถ้า API ของคุณ insert/update บ่อย ดัชนีเพิ่มงานทุกครั้งที่เขียน วัดผลก่อนและหลัง แล้วเช็กแผนคิวรีหลังเพิ่มดัชนี
หนี้จาก pagination แทรกซึมเงียบ ๆ OFFSET ดูโอเคในตอนแรก แต่ p95 เพิ่มขึ้นตามเวลาเพราะฐานข้อมูลต้องเดินผ่านแถวเพิ่มขึ้นเรื่อย ๆ
ขนาด payload JSON เป็นภาษีที่ซ่อนอยู่อีกอย่าง การบีบอัดช่วยแถบแบนด์วิดท์แต่ไม่ลดต้นทุนการสร้าง การจัดสรร และการแปลงวัตถุขนาดใหญ่ ตัดฟิลด์ ลดการซ้อนลึก และคืนเฉพาะสิ่งที่หน้าต้องการ
ถ้าคุณดูแค่ค่าเฉลี่ยของ response time คุณจะพลาดจุดที่ผู้ใช้จริงเจ็บปวด p95 (และบางครั้ง p99) คือที่ที่ pool saturation, lock waits, และแผนช้าจะปรากฏก่อน
เช็ครวดเร็วก่อนปล่อย:
EXPLAIN อีกครั้งหลังเพิ่มดัชนีหรือเปลี่ยนฟิลเตอร์ก่อนผู้ใช้จริงมาถึง คุณต้องมีหลักฐานว่า API ของคุณคงพฤติกรรมได้ภายใต้ความกดดัน เป้าหมายไม่ใช่ตัวเลขสมบูรณ์แบบ แต่จับปัญหาสำคัญที่จะทำให้เกิด timeout, spike, หรือฐานข้อมูลหยุดรับงาน
รันเช็กในสเตจิงที่ใกล้เคียง production (ขนาด DB ใกล้เคียง ดัชนีเหมือนกัน การตั้งพูลเหมือนกัน): วัด p95 ต่อ endpoint สำคัญภายใต้โหลด จับ 5 คิวรีที่ช้าที่สุดตามเวลารวม ดูเวลา pool wait, EXPLAIN (ANALYZE, BUFFERS) สำหรับคิวรีแย่ที่สุดเพื่อยืนยันว่ามันใช้ดัชนีตามที่คาด และตรวจสอบขนาด payload บน route ที่มีโหลดมากที่สุด
แล้วลองสลับ worst-case: ขอหน้าลึก, ใส่ฟิลเตอร์กว้างสุด, และลองกับ cold start (restart API แล้วเรียกคำขอเดิมเป็นครั้งแรก) ถ้าการแบ่งหน้าเชิงลึกช้าลงทุกหน้า ให้เปลี่ยนเป็น cursor-based pagination ก่อนปล่อย
จดค่าเริ่มต้นไว้เพื่อทีมจะตัดสินใจสอดคล้องต่อไป: ขีดจำกัดพูลและ timeout, กฎการแบ่งหน้า (ขนาดหน้า max, อนุญาต offset หรือไม่, รูปแบบ cursor), กฎคิวรี (select เฉพาะคอลัมน์ที่ต้องการ, หลีกเลี่ยง SELECT *, จำกัดฟิลเตอร์ที่แพง), และกฎการล็อก (threshold slow query, เก็บตัวอย่างนานแค่ไหน, วิธีติดป้าย endpoints)
ถ้าคุณสร้างและส่งออกบริการ Go + Postgres ด้วย Koder.ai การผ่านการวางแผนสั้น ๆ ก่อน deploy จะช่วยให้ฟิลเตอร์ การแบ่งหน้า และรูปแบบตอบกลับมีเจตนา เมื่อคุณเริ่มปรับดัชนีและรูปแบบคิวรี snapshots และ rollback จะทำให้ง่ายต่อการย้อนการ "แก้" ที่ช่วย endpoint หนึ่งแต่ทำร้ายอื่น ๆ ถ้าคุณต้องการที่เดียวสำหรับวน workflow นั้น Koder.ai บน koder.ai ออกแบบมาเพื่อสร้างและปรับบริการเหล่านี้ผ่านแชท แล้วส่งออกซอร์สเมื่อคุณพร้อม
Start by separating DB wait time from app work time.
Add simple timing around “wait for connection” and “query execution” to see which side dominates.
Use a small baseline you can repeat:
Pick a clear target like “p95 under 200 ms at 50 concurrent users, errors under 0.5%.” Then only change one thing at a time and re-test the same request mix.
Enable slow query logging with a low threshold in pre-launch testing (for example 100–200 ms) and log the full statement so you can copy it into a SQL client.
Keep it temporary:
Once you’ve found the worst offenders, switch to sampling or raise the threshold.
A practical default is a small multiple of CPU cores per API instance, often 5–20 max open connections, with similar max idle connections, and recycle connections every 30–60 minutes.
Two common failure modes:
Remember pools multiply across instances (20 connections × 10 instances = 200 connections).
Time DB calls in two parts:
If most time is pool wait, adjust pool sizing, timeouts, and instance counts. If most time is query execution, focus on EXPLAIN and indexes.
Also confirm you always close rows promptly so connections return to the pool.
Run EXPLAIN (ANALYZE, BUFFERS) on the exact SQL your API sends and look for:
Indexes should match what the endpoint actually does: filters + sort order.
Good default approach:
WHERE + ORDER BY pattern.Use a partial index when most traffic hits a predictable subset of rows.
Example pattern:
active = trueA partial index like ... WHERE active = true stays smaller, is more likely to fit in memory, and reduces write overhead versus indexing everything.
Confirm with that Postgres actually uses it for your high-traffic queries.
LIMIT/OFFSET gets slower on deep pages because Postgres still has to walk past (and often sort) the skipped rows. Page 500 can be dramatically more expensive than page 1.
Prefer keyset (cursor) pagination:
Usually yes for list endpoints. The fastest response is the one you don’t send.
Practical wins:
SELECT *).include= or fields= so clients opt into heavy fields.ORDER BYFix the biggest red flag first; don’t tune everything at once.
Example: if you filter by user_id and sort by newest, an index like (user_id, created_at DESC) is often the difference between stable p95 and spikes.
EXPLAINid).ORDER BY identical across requests.(created_at, id) or similar into a cursor.This keeps each page cost roughly constant as tables grow.
You’ll often reduce Go CPU, memory pressure, and tail latency just by shrinking payloads.