เริ่มต้นเสาหลักที่ 1 ของ In-Database Automation Engine: เรียนรู้วิธีสร้าง Stored Function ระดับอะตอมที่ฝังใน Query ได้โดยตรง พร้อมไขความลับกลไก Volatility Categories (IMMUTABLE, STABLE, VOLATILE), การทำ Constant Folding, และ Function Inlining ของ Query Planner เพื่อตัด Function Call Overhead และเร่งความเร็ว Query สูงสุด 300 เท่า!
Phase 2 เริ่มที่เสาหลักต้นแรก — การย้ายสูตรคำนวณธุรกิจเข้าฐานข้อมูล โดยไม่ทำให้ Query ช้าลง
ฟังก์ชันที่เราเขียนขึ้นเองในฐานข้อมูล รับค่าตัวแปร นำไปคำนวณตามสูตรธุรกิจ แล้วคืนผลลัพธ์กลับมา ฝังใน SELECT, WHERE หรือ JOIN ได้
เทคนิคของ Query Planner ที่นำเนื้อหาข้างในฟังก์ชันไปยุบรวมเข้ากับ SQL หลักโดยตรง เพื่อไม่ต้องเสียเวลาสลับโหมดทำงาน (context switching)
โดยรวมวันนี้คือการเขียน UDF แล้วติดแท็กบอกฐานข้อมูล เช่น IMMUTABLE เพื่อให้เอนจินนำโค้ดไปทำ Inlining รวมกับ SQL หลัก คำนวณได้เร็วระดับ native โดยไม่เสีย overhead
นี่คือจุดเริ่มของการสร้างโค้ดในฐานข้อมูล ช่วยรวบสูตรคำนวณธุรกิจมาไว้ที่ DB โดยไม่ทำให้ Query กลายเป็นกล่องดำที่ช้าลง
เมื่อก้าวเข้าสู่ Phase 2 เสาหลักต้นแรกของ In-Database Automation Engine คือ User-Defined Functions (UDFs) ซึ่งทำหน้าที่เป็นรูทีนคำนวณและแปลงค่าที่สามารถฝังอยู่ในประโยค SELECT, WHERE หรือ JOIN ได้โดยตรง แต่หากผู้พัฒนาสร้างฟังก์ชันโดยไม่เข้าใจกลไกของ Query Planner และระดับความผันผวน (Volatility) ฟังก์ชันนั้นจะกลายเป็น "Black Box" ที่บังคับให้เอนจินต้องสลับ Context Switching เรียกคำนวณซ้ำนับล้านรอบจนเซิร์ฟเวอร์ค้าง!
ระบบรายงานยอดขายของแพลตฟอร์ม E-Commerce มีการคำนวณราคาสุทธิหลังหักส่วนลดสำหรับ 1,000,000 ออเดอร์ ทีมพัฒนาสร้าง UDF ชื่อ fn_calculate_net_price(amount, discount_rate) ด้วยภาษา PL/pgSQL แต่ลืมระบุ Volatility Category ทำให้ PostgreSQL กำหนดเป็น VOLATILE โดยอัตโนมัติ:
ทีมงานเข้าใจผิดคิดว่า "การเขียนฟังก์ชันในฐานข้อมูลทำให้ระบบช้า" ทั้งที่ต้นเหตุที่แท้จริงเกิดจากการละเลยการติดแท็ก IMMUTABLE / STABLE และการไม่เปิดทางให้ Query Planner ทำ Function Inlining นั่นเอง!
"ทุกท่านครับ การที่ Query เรียก UDF แล้วช้าจน CPU พุ่ง 100% ไม่ได้แปลว่า UDF ช้า แต่เกิดจากการปล่อยให้ Volatility เป็น VOLATILE โดยไม่จำเป็น ทำให้ Query Planner ไม่สามารถ Optimise หรือ Cache ผลลัพธ์ได้ จนต้องวิ่งรันฟังก์ชันซ้ำซ้อนนับล้านรอบ
ด้วย Pillar 1 Architectural Pattern เราจะติดแท็ก IMMUTABLE / STABLE และเลือกใช้ LANGUAGE sql เพื่อเปิดทางให้ Optimizer ทำ Function Inlining ยุบโค้ดฟังก์ชันลงไปรันติดชิด Execution Plan ของ Main Query ซึ่งตัด Function Call Overhead ออกทั้งหมด ดันความเร็วขึ้น 300 เท่า จาก 45 วินาที เหลือเพียง 15 มิลลิวินาทีครับ"
ลองปรับจำนวนแถวข้อมูลและเลือกกลยุทธ์การเขียนฟังก์ชัน เพื่อสังเกตจำนวนครั้งที่ฟังก์ชันถูกรันจริง และความแตกต่างของเวลา Execution Time
การสร้าง User-Defined Function (UDF) ใน PostgreSQL ต้องเข้าใจโครงสร้างคำสั่ง CREATE FUNCTION, การระบุชนิดข้อมูลคืนค่า (RETURNS), การเลือกภาษา (LANGUAGE sql vs LANGUAGE plpgsql), และการติดแท็ก Volatility อย่างแม่นยำ:
CREATE OR REPLACE FUNCTION fn_calculate_vat (
p_amount NUMERIC -- [1] พารามิเตอร์ขาเข้า (Input Parameter)
)
RETURNS NUMERIC -- [2] บังคับระบุชนิดข้อมูลผลลัพธ์ (Mandatory Return Type)
LANGUAGE sql -- [3] ใช้ภาษา SQL เพื่อเปิดทางให้ Query Planner ทำ Function Inlining ได้ 100%
IMMUTABLE -- [4] 🎯 ติดแท็ก Volatility: รับ Input เดิม ได้ผลลัพธ์เดิมตลอดไป ทำ Constant Folding ได้
STRICT -- [5] 🎯 หาก Parameter เป็น NULL ให้คืน NULL ทันที ไม่ต้องเสียเวลาคำนวณโค้ดข้างใน
AS $$
-- [6] เนื้อหาคำสั่งคำนวณสั้นๆ ที่ Query Planner สามารถยกไปแปะใน Main Execution Plan ได้
SELECT ROUND(p_amount * 0.07, 2);
$$;
LANGUAGE sql ร่วมกับ IMMUTABLE หรือ STABLE และไม่มีฟังก์ชันข้างเคียงซับซ้อน จะได้รับอภิสิทธิ์การทำ Function Inlining จาก Optimizer โดยสมบูรณ์
-- [1] สร้างฟังก์ชันคำนวณดัชนีมวลกาย (BMI) เป็น Pure Math Function
CREATE OR REPLACE FUNCTION fn_calculate_bmi (
p_weight_kg NUMERIC,
p_height_m NUMERIC
)
RETURNS NUMERIC
LANGUAGE plpgsql
IMMUTABLE -- 🎯 ระบุ IMMUTABLE เพราะผลลัพธ์ขึ้นอยู่กับ p_weight_kg และ p_height_m เท่านั้น ไร้การแตะตาราง
AS $$
BEGIN
-- ตรวจสอบเงื่อนไขป้องกันการหารด้วยศูนย์
IF p_height_m <= 0 THEN
RETURN 0;
END IF;
-- คำนวณสูตร BMI: น้ำหนัก (กก.) / ส่วนสูง (ม.) ยกกำลังสอง
RETURN ROUND(p_weight_kg / (p_height_m * p_height_m), 2);
END $$;
-- 📤 ทดสอบเรียกใช้งาน (Optimizer สามารถคำนวณค่าล่วงหน้าแบบ Constant Folding ได้ทันที):
SELECT fn_calculate_bmi(70, 1.75); -- คืนค่า 22.86
IMMUTABLE หากคำสั่งมีค่าคงที่ เช่น WHERE bmi > fn_calculate_bmi(70, 1.75) ตัว Planner จะแปลงเป็น WHERE bmi > 22.86 ตั้งแต่ช่วง Compile Plan!
-- [1] สร้างฟังก์ชันดึงชื่อแผนกจากรหัสพนักงาน (มีการอ่านตาราง แต่ไม่มีการ INSERT/UPDATE)
CREATE OR REPLACE FUNCTION fn_get_dept_name (
p_emp_id INT
)
RETURNS VARCHAR
LANGUAGE plpgsql
STABLE -- 🎯 ระบุ STABLE เพราะอ่านข้อมูลจากตาราง ผลลัพธ์ไม่เปลี่ยนแปลงภายใน Transaction เดียวกัน
AS $$
DECLARE
v_dept_name VARCHAR(50);
BEGIN
-- ดึงชื่อแผนกจากตาราง departments
SELECT d.department_name INTO v_dept_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE e.employee_id = p_emp_id;
RETURN COALESCE(v_dept_name, 'N/A');
END $$;
-- 📤 ทดสอบเรียกใช้งานใน Query:
SELECT employee_id, first_name, fn_get_dept_name(employee_id) AS dept
FROM employees
LIMIT 10;
IMMUTABLE ให้กับฟังก์ชันที่อ่านข้อมูลตาราง DB เพราะถ้าวันข้างหน้ามีคนเปลี่ยนชื่อแผนก ผลลัพธ์ที่แคชไว้อาจผิดพลาดได้ จึงต้องใช้ STABLE
เปรียบเทียบโค้ดเดิมที่เป็น PL/pgSQL Default VOLATILE และเรียกซ้ำ 2 รอบ กับโค้ดใหม่ที่ใช้ LANGUAGE sql + IMMUTABLE ร่วมกับ CTE Single Evaluation Pattern:
-- ❌ ฟังก์ชันแบบ VOLATILE (ไม่ได้ใส่คีย์เวิร์ด Volatility)
-- ปิดกั้น Optimizer ไม่ให้ทำ Inlining หรือ Constant Folding
CREATE OR REPLACE FUNCTION fn_net_price_flawed(
p_amount NUMERIC,
p_discount NUMERIC
)
RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
BEGIN
RETURN p_amount - (p_amount * p_discount);
END $$;
-- ❌ Query ที่เรียกใช้: รันฟังก์ชัน 2,000,000 รอบสำหรับ 1,000,000 แถว
-- (รอบแรกใน WHERE และรอบสองใน SELECT)
SELECT id, fn_net_price_flawed(amount, 0.10) AS net_amount
FROM orders
WHERE fn_net_price_flawed(amount, 0.10) > 500;
-- [Statement 1] สร้าง UDF ใหม่ที่สนับสนุน Function Inlining 100%
CREATE OR REPLACE FUNCTION fn_net_price_optimized(
p_amount NUMERIC,
p_discount NUMERIC
)
RETURNS NUMERIC
LANGUAGE sql -- 🎯 [บรรทัดที่ 1] ใช้ LANGUAGE sql เพื่อให้ Query Planner สลายฟังก์ชันเข้า Execution Plan ได้
IMMUTABLE -- 🎯 [บรรทัดที่ 2] ติดแท็ก IMMUTABLE บอก Planner ว่าเป็น Pure Math ทำ Constant Folding ได้
STRICT -- 🎯 [บรรทัดที่ 3] ถ้า Input เป็น NULL ให้คืนค่า NULL ทันที
AS $$
SELECT p_amount - (p_amount * p_discount);
$$;
-- [Statement 2] เขียน Query แบบ CTE Single Evaluation ร่วมกับ Inlined UDF
WITH evaluated_orders AS (
SELECT
id,
amount,
-- 🎯 [บรรทัดที่ 4] ประมวลผลฟังก์ชันเพียงครั้งเดียวต่อแถวใน CTE
fn_net_price_optimized(amount, 0.10) AS net_amount
FROM orders
)
SELECT id, net_amount
FROM evaluated_orders
WHERE net_amount > 500; -- 🎯 [บรรทัดที่ 5] อ้างอิงจากคอลัมน์ใน CTE ไม่ต้องเรียกฟังก์ชันซ้ำอีกรอบ!
แบบทดสอบวัดระดับความเข้าใจ Pillar 1: User-Defined Functions, Volatility Categories (IMMUTABLE, STABLE, VOLATILE), Function Inlining, STRICT behavior, และข้อสอบสะสมทบทวนครบทั้ง Day 1–4 คลิกเลือกคำตอบ ก, ข, ค, หรือ ง เพื่อดูผลลัพธ์และคำอธิบายอย่างละเอียดทันที
บทเรียน Day 5: Pillar 1 - User-Defined Functions (UDFs) & Inlining Mechanics ชี้ให้เห็นถึงความลับของการออกแบบ Stored Functions ให้ทรงพลัง โดยเน้นการกำหนด Volatility Categories (IMMUTABLE, STABLE, VOLATILE) ที่ถูกต้อง และการเลือกใช้ LANGUAGE sql เพื่อเปิดทางให้ Query Planner ทำ Function Inlining ขจัด Function Call Overhead ได้อย่างมหาศาล
Pillar 2 - Stored Procedures & Transaction Control (COMMIT / ROLLBACK): ในวันถัดไป เราจะไปเจาะลึกเสาหลักต้นที่ 2 นั่นคือ Stored Procedures (CREATE PROCEDURE) และกลไกการควบคุม Transaction ภายในตัวโปรแกรมด้วยคำสั่ง COMMIT และ ROLLBACK เพื่อรองรับงาน Batch Processing ขนาดใหญ่ครับ!