เริ่มต้นปูพื้นฐานการเขียน PL/pgSQL จากศูนย์ด้วย Anonymous Block (บล็อกคำสั่งไร้นาม) พร้อมเจาะลึกคอขวดสถาปัตยกรรมระดับองค์กรว่าทำไมการวนลูปยิงคำสั่งยิบย่อยข้ามเน็ตเวิร์กจึงทำให้ระบบล่ม และทำไมแนวคิด "Data Stays, Logic Moves" จึงชนะอย่างเด็ดขาด
วันนี้เราเริ่มจากต้นตอที่ทำให้ระบบช้า ทั้งที่ CPU ของฐานข้อมูลยังว่างอยู่ — ไม่ใช่เพราะ PostgreSQL คิดช้า แต่เพราะแอปเดินข้ามเครือข่ายซ้ำๆ เพื่องานชิ้นเดียว
พฤติกรรมของแอป (C#, Java, Node.js) ที่ยิง SQL ยิบย่อยทีละคำสั่ง นับร้อยนับพันรอบ เพื่อให้งานชิ้นเดียวเสร็จ แทนที่จะรวบเป็นคำสั่งเดียวแล้วจบ
เวลาที่ข้อมูลวิ่งจากแอปไปฐานข้อมูลแล้ววิ่งกลับ รอบละแค่ 1–2 มิลลิวินาที แต่ถ้ายิง 1,000 รอบ ก็เสียเวลาเดินทางฟรีๆ ไปแล้ว 1–2 วินาที
โดยรวมวันนี้คือการชี้ให้เห็นว่าระบบที่ช้า มักไม่ได้เกิดจากฐานข้อมูลประมวลผลช้า แต่เกิดจากแอปที่คุยกับฐานข้อมูลเป็นชิ้นเล็กชิ้นน้อย จน RTT สะสมกินเวลาไปหมด
นี่คือจุดเริ่มของทั้งหลักสูตร เราต้องย้ายตรรกะมาประมวลผลในฐานข้อมูล เพื่อรวบงานให้จบใน single round-trip ไม่ใช่เดินข้ามเครือข่ายเป็นพันรอบ
คำว่า "Chatty" แปลตรงตัวว่า "พูดมาก / คุยไม่หยุด" ในทางสถาปัตยกรรมซอฟต์แวร์ มันคืออาการที่ Application ขยันยิงคำถาม-คำตอบยิบย่อยกับ Database ข้าม Network ไป-กลับนับพันนับหมื่นรอบ การนำเสนอหัวข้อ In-Database Engine ต่อบอร์ดหรือ Senior Architects จะไม่มีน้ำหนักเลย หากเราไม่เริ่มจากการชี้ให้เห็นความพังทลายของสถาปัตยกรรมเดิมข้อนี้ก่อน
ในคืนเทศกาล 11.11 ระบบ E-Commerce ต้องปรับราคาสินค้าค้างชำระ 50,000 รายการ เพื่อมอบส่วนลดพิเศษ ทีมพัฒนาใช้วิธีเขียนสคริปต์บน App Server (Node.js/Python) วนลูปดึงข้อมูลสินค้าทีละแถว (SELECT) มาคำนวณส่วนลดบน App Memory แล้วยิงคำสั่ง UPDATE กลับไปทีละแถวครบ 50,000 รอบ:
"ทุกท่านครับ คอขวดของระบบเราไม่ได้เกิดจาก Database ประมวลผล SQL ช้า แต่เกิดจาก The Chatty Application Anti-Pattern ที่แอปพลิเคชันยิง Query วนลูปไป-กลับนับหมื่นรอบ เสียเวลา RTT ข้าม Network และเกิด Serialization CPU Spike
ทางออกเชิงสถาปัตยกรรมของเราคือการยึดหลัก 'Data Stays, Logic Moves' โดยใช้ PL/pgSQL In-Database Block ยุบกระบวนการทั้งหมดให้ทำงานติดชิด Buffer Pool ภายใน 1 Single Round-Trip ซึ่งตัด Network Latency ออกได้ 99% และบีบ Lock Duration ให้เหลือเพียงมิลลิวินาทีครับ"
ก่อนจะเริ่มเขียน Function หรือ Procedure เรามาเริ่มต้นจาก Anonymous Block (บล็อกไร้นาม) ซึ่งขึ้นต้นด้วย DO $$ ... $$; ข้อดีคือเป็นสคริปต์ที่รันครั้งเดียวทิ้ง ไม่ต้องตั้งชื่อ ไม่ต้องสร้างเป็น Object ค้างไว้ในฐานข้อมูล เหมาะสำหรับงาน Migrate หรือประมวลผลงานพิเศษ
จำง่ายๆ ว่าโครงสร้างบล็อกมี 3 ส่วน แต่ส่วนที่บังคับต้องมีเสมอคือ BEGIN ... END; เท่านั้น
DO $$
DECLARE
-- [ส่วนที่ 1: DECLARE Section] (Optional - มีหรือไม่มีก็ได้)
-- ประกาศตัวแปร และกำหนดค่าเริ่มต้นด้วยสัญลักษณ์ :=
v_emp_name VARCHAR(50) := 'สมชาย';
v_salary NUMERIC := 45000.00;
BEGIN
-- [ส่วนที่ 2: Executable Section] (Mandatory - บังคับต้องมีเสมอ!)
-- ส่วนสั่งประมวลผล ใช้ RAISE NOTICE พิมพ์ข้อความออก Log (% คือตัวแทรกค่าตัวแปร)
RAISE NOTICE 'พนักงาน: % | เงินเดือน: % บาท', v_emp_name, v_salary;
EXCEPTION
-- [ส่วนที่ 3: Exception Section] (Optional - มีหรือไม่มีก็ได้)
-- ส่วนดักจับข้อผิดพลาดหากเกิด Error ขึ้นภายใน Executable Section
WHEN OTHERS THEN
RAISE NOTICE 'เกิดข้อผิดพลาดในการประมวลผล';
END $$;
:= ใช้สำหรับกำหนดค่าใส่ตัวแปร (Assignment) ส่วนเครื่องหมาย = ใช้สำหรับเปรียบเทียบเงื่อนไข (Comparison) ในประโยค IF หรือ WHERE
DO $$
DECLARE
v_price NUMERIC := 1200.00; -- [1] ประกาศตัวแปรเก็บราคาสินค้า = 1200
v_vat NUMERIC; -- [2] ประกาศตัวแปรเก็บภาษี VAT (ยังไม่ใส่ค่า เริ่มต้นเป็น NULL)
v_total NUMERIC; -- [3] ประกาศตัวแปรเก็บราคารวมสุทธิ
BEGIN
v_vat := v_price * 0.07; -- [4] คำนวณ VAT 7% แล้วเอาค่าไปใส่ใน v_vat ด้วยสัญลักษณ์ :=
v_total := v_price + v_vat; -- [5] เอา ราคา + VAT ไปใส่ใน v_total
-- [6] พิมพ์ผลลัพธ์ออกทางหน้าจอ Log (สัญลักษณ์ %% ใช้สำหรับพิมพ์เครื่องหมาย % จริง)
RAISE NOTICE 'ราคา: % | VAT 7%%: % | รวมสุทธิ: %', v_price, v_vat, v_total;
END $$;
-- 📤 ผลลัพธ์ใน Log:
-- NOTICE: ราคา: 1200.00 | VAT 7%: 84.0000 | รวมสุทธิ: 1284.0000
DO $$
DECLARE
v_emp_id INT := 101; -- [1] กำหนดรหัสพนักงานที่ต้องการค้นหา
v_name VARCHAR(50); -- [2] ตัวแปรสำหรับรับชื่อพนักงาน
v_salary NUMERIC; -- [3] ตัวแปรสำหรับรับยอดเงินเดือน
BEGIN
-- [4] SELECT คอลัมน์ last_name, salary แล้วใช้ INTO ยัดค่าใส่ตัวแปร v_name, v_salary ตามลำดับ
SELECT last_name, salary
INTO v_name, v_salary
FROM employees
WHERE employee_id = v_emp_id;
-- [5] ตรวจสอบเงื่อนไขด้วย IF-THEN-ELSE
IF v_salary > 50000 THEN
RAISE NOTICE 'คุณ % (เงินเดือน %) -> High Tier', v_name, v_salary;
ELSE
RAISE NOTICE 'คุณ % (เงินเดือน %) -> Standard Tier', v_name, v_salary;
END IF;
EXCEPTION
-- [6] ดักจับกรณีที่ไม่พบพนักงานรหัสนี้ในตาราง (System Exception: NO_DATA_FOUND)
WHEN NO_DATA_FOUND THEN
RAISE NOTICE 'ไม่พบข้อมูลพนักงานรหัส % ในระบบ', v_emp_id;
END $$;
SELECT คอลัมน์ INTO ตัวแปร FROM ตาราง เป็นวิธีมาตรฐานในการย้ายข้อมูลจากตารางเข้าสู่หน่วยความจำของ PL/pgSQL
เปรียบเทียบโค้ดเดิมที่วนลูปใน Application กับโค้ดใหม่ที่เขียนด้วย Anonymous Block พร้อมฟีเจอร์เด็ด RETURNING ของ PostgreSQL:
# โค้ดฝั่ง Application Server
# วนลูปยิง Query ข้าม Network 50,000 รอบ!
for order in orders:
db.execute(
"UPDATE orders SET discount = %s, status = 'PROCESSED' WHERE id = %s",
(order.total_amount * 0.10, order.id)
)
DO $$
DECLARE
-- [บรรทัดที่ 1] ประกาศตัวแปร v_updated_count ชนิด INT เพื่อรับผลรวมจำนวนแถวที่อัปเดตสำเร็จ
v_updated_count INT;
BEGIN
-- [บรรทัดที่ 2] WITH updated_rows AS (
-- เริ่มต้นสร้าง CTE (Common Table Expression) ชื่อ updated_rows เพื่อทำหน้าที่เป็นตารางเสมือนชั่วคราวบน RAM
WITH updated_rows AS (
-- [บรรทัดที่ 3-6] คำสั่ง UPDATE orders SET ... WHERE ...
-- สั่งประมวลผลอัปเดตข้อมูลตาราง orders แบบ Set-based ทั้งหมดในคำสั่งเดียวบน DB Engine
UPDATE orders
SET discount = total_amount * 0.10, -- คำนวณส่วนลด 10% ยัดใส่คอลัมน์ discount
status = 'PROCESSED' -- เปลี่ยนสถานะในคอลัมน์ status เป็น 'PROCESSED'
WHERE status = 'PENDING' AND total_amount >= 1000 -- กรองเฉพาะแถวที่สถานะ 'PENDING' และยอด >= 1,000
-- [บรรทัดที่ 7] RETURNING id
-- พ่นรายการ id ของทุกแถวที่อัปเดตสำเร็จสดๆ ร้อนๆ ส่งต่อไปเทเก็บไว้ในตารางเสมือน updated_rows บน RAM
RETURNING id
)
-- [บรรทัดที่ 8] SELECT COUNT(*) INTO v_updated_count FROM updated_rows;
-- สั่งนับจำนวน id ทั้งหมดในตารางเสมือน updated_rows แล้วใช้คีย์เวิร์ด INTO ยัดผลลัพธ์ตัวเลขใส่ตัวแปร v_updated_count
SELECT COUNT(*) INTO v_updated_count FROM updated_rows;
-- [บรรทัดที่ 9] RAISE NOTICE '...'
-- พิมพ์แจ้งจำนวนรายการที่ประมวลผลสำเร็จออกทางหน้าจอ Log
RAISE NOTICE 'อัปเดตสำเร็จทั้งหมด % รายการ (ใน 1 Single Round-Trip)', v_updated_count;
END $$;
RETURNING นับผลลัพธ์สรุปกลับมาได้ทันที
แบบทดสอบวัดระดับความเข้าใจสถาปัตยกรรมและไวยากรณ์ Anonymous Block (DO $$) ประจำ Day 1 ครอบคลุมเนื้อหาครบทั้ง 18 ข้อ คลิกเลือกคำตอบ ก, ข, ค, หรือ ง เพื่อดูผลลัพธ์และคำอธิบายอย่างละเอียดทันที
บทเรียน Day 1: โครงสร้างบล็อกพื้นฐาน (DO $$) & ถอดรหัสปัญหา The Chatty Application Anti-Pattern ชี้ให้เห็นว่าคอขวดที่แท้จริงของระบบไม่ได้เกิดจากความเร็วของคำสั่ง SQL แต่เกิดจากสถาปัตยกรรมการสื่อสารข้าม Layer การเปลี่ยนมาใช้ PL/pgSQL Block ร่วมกับหลักการ "Data Stays, Logic Moves" ช่วยตัด Network Latency และ Serialization Overhead ออกได้อย่างหมดจด
Storage Engine & Buffer Pool Mechanics: ในวันถัดไป เราจะเจาะลึกลงไปในระดับ Storage Engine ว่าทำไม Logic ที่รันติดชิด Shared Buffer Pool (RAM) ของ PostgreSQL ถึงประมวลผลข้อมูลระดับล้านแถวได้เร็วในระดับ Nanoseconds ครับ!