← Back to PostgreSQL course
PHASE 2: DEEP DIVE INTO THE 4 PILLARS PILLAR 1: UDFs ⚡

Day 5: Pillar 1 - User-Defined Functions (UDFs) & Inlining Mechanics (กลไกการสร้าง Stored Functions, ความลับของ Function Volatility Categories: IMMUTABLE, STABLE, VOLATILE และกลไก Function Inlining ของ Query Planner)

เริ่มต้นเสาหลักที่ 1 ของ In-Database Automation Engine: เรียนรู้วิธีสร้าง Stored Function ระดับอะตอมที่ฝังใน Query ได้โดยตรง พร้อมไขความลับกลไก Volatility Categories (IMMUTABLE, STABLE, VOLATILE), การทำ Constant Folding, และ Function Inlining ของ Query Planner เพื่อตัด Function Call Overhead และเร่งความเร็ว Query สูงสุด 300 เท่า!

📍 ตำแหน่งปัจจุบันของคุณบน Roadmap (18-Day Masterclass) เริ่มต้น Phase 2: เจาะลึก 4 เสาหลัก
Phase 1: Architecture Synthesis (Days 1–4) ✓
➔
Day 5: Pillar 1 - UDFs & Inlining Mechanics
➔
Day 6: Pillar 2 - Stored Procedures
➔
Day 7: Pillar 3 - Event Triggers
➔
Day 8: Pillar 4 - Pre-computation
เปิดบท

ถอดรหัสคำสำคัญของวันนี้

Phase 2 เริ่มที่เสาหลักต้นแรก — การย้ายสูตรคำนวณธุรกิจเข้าฐานข้อมูล โดยไม่ทำให้ Query ช้าลง

UDFs

ฟังก์ชันที่เราเขียนขึ้นเองในฐานข้อมูล รับค่าตัวแปร นำไปคำนวณตามสูตรธุรกิจ แล้วคืนผลลัพธ์กลับมา ฝังใน SELECT, WHERE หรือ JOIN ได้

Inlining

เทคนิคของ Query Planner ที่นำเนื้อหาข้างในฟังก์ชันไปยุบรวมเข้ากับ SQL หลักโดยตรง เพื่อไม่ต้องเสียเวลาสลับโหมดทำงาน (context switching)

โดยรวมวันนี้คือการเขียน UDF แล้วติดแท็กบอกฐานข้อมูล เช่น IMMUTABLE เพื่อให้เอนจินนำโค้ดไปทำ Inlining รวมกับ SQL หลัก คำนวณได้เร็วระดับ native โดยไม่เสีย overhead

เปรียบกับชีวิตจริง

เหมือนสูตรคูณแม่ 2 ที่ท่องจนจำขึ้นใจ พอเห็น 2 × 4 ในหัวยุบเป็น 8 ทันที (Inlining) ไม่ต้องตั้งบวก 2+2+2+2 ใหม่ทุกรอบ

นี่คือจุดเริ่มของการสร้างโค้ดในฐานข้อมูล ช่วยรวบสูตรคำนวณธุรกิจมาไว้ที่ DB โดยไม่ทำให้ Query กลายเป็นกล่องดำที่ช้าลง

1

🏛️ มโนทัศน์ สถาปัตยกรรมองค์กร และสถานการณ์จริง (Concept & Architecture)

เมื่อก้าวเข้าสู่ Phase 2 เสาหลักต้นแรกของ In-Database Automation Engine คือ User-Defined Functions (UDFs) ซึ่งทำหน้าที่เป็นรูทีนคำนวณและแปลงค่าที่สามารถฝังอยู่ในประโยค SELECT, WHERE หรือ JOIN ได้โดยตรง แต่หากผู้พัฒนาสร้างฟังก์ชันโดยไม่เข้าใจกลไกของ Query Planner และระดับความผันผวน (Volatility) ฟังก์ชันนั้นจะกลายเป็น "Black Box" ที่บังคับให้เอนจินต้องสลับ Context Switching เรียกคำนวณซ้ำนับล้านรอบจนเซิร์ฟเวอร์ค้าง!

🛒 วิกฤตรายงานคำนวณราคาสุทธิ 1,000,000 ออเดอร์: เมื่อ UDF ถูกรันซ้ำ 2,000,000 ครั้ง!

ระบบรายงานยอดขายของแพลตฟอร์ม E-Commerce มีการคำนวณราคาสุทธิหลังหักส่วนลดสำหรับ 1,000,000 ออเดอร์ ทีมพัฒนาสร้าง UDF ชื่อ fn_calculate_net_price(amount, discount_rate) ด้วยภาษา PL/pgSQL แต่ลืมระบุ Volatility Category ทำให้ PostgreSQL กำหนดเป็น VOLATILE โดยอัตโนมัติ:

2M ครั้ง
เรียกซ้ำ 2 รอบต่อแถว (ใน WHERE 1M + ใน SELECT 1M)
45 วินาที
Query ช้าจนเกิน SLA รายงาน Dashboard ค้าง
100% CPU
Context Switching ระหว่าง SQL Core กับ PL/pgSQL Sandbox

ทีมงานเข้าใจผิดคิดว่า "การเขียนฟังก์ชันในฐานข้อมูลทำให้ระบบช้า" ทั้งที่ต้นเหตุที่แท้จริงเกิดจากการละเลยการติดแท็ก IMMUTABLE / STABLE และการไม่เปิดทางให้ Query Planner ทำ Function Inlining นั่นเอง!

⚖️ กฎเหล็ก 3 ระดับความผันผวนของฟังก์ชัน (Volatility Categories Triad)

1. IMMUTABLE เร็วที่สุด / Pure Math

คำมั่นสัญญา: รับ Input เดิม จะได้ผลลัพธ์เดิมตลอดไป ห้ามอ่านตาราง ห้ามเรียกเวลา NOW() ห้ามมี Random

  • Planner ทำ Constant Folding คำนวณค่าล่วงหน้าตั้งแต่ตอน Compile
  • เปิดทางให้ทำ Function Inlining และนำไปสร้าง Functional Index บนตารางได้!
2. STABLE คงที่ระดับ Query Scan

คำมั่นสัญญา: รับ Input เดิม จะได้ผลลัพธ์เหมือนเดิมตลอดภายใน Single Table Scan / Transaction เดียวกัน

  • อ่านข้อมูลจากตารางได้ (SELECT) แต่ห้ามแก้ไข (ห้าม DML)
  • Optimizer สามารถ Cache ผลลัพธ์ซ้ำใน Query เดียวกันได้ ไม่ต้องคำนวณใหม่ทุกแถว
3. VOLATILE Default / Black Box

คำมั่นสัญญา: ผลลัพธ์พร้อมเปลี่ยนตลอดเวลา (เช่น มี random(), clock_timestamp() หรือมีคำสั่ง UPDATE/INSERT)

  • Optimizer มองเป็น Black Box บังคับให้รันใหม่ทุกแถวทุกจุด (Row-by-Row)
  • ห้ามทำ Inlining, ห้ามทำ Constant Folding, และห้ามสร้าง Index Expression
สถาปัตยกรรมระดับลึก: การทำงานของ Query Planner (Function Inlining vs Context Switching) Query Execution Pipeline
❌ แย่: Default VOLATILE (Black Box & Double Evaluation) ✅ ปัง: IMMUTABLE + LANGUAGE sql (Function Inlining 100%) 1. SQL Executor Engine (Sequential Scan) • อ่านแถวข้อมูลจาก Data Page 8KB ทีละแถว (1 ถึง 1,000,000) • พบฟังก์ชัน VOLATILE: Planner ไม่กล้า Inlining เพราะกลัวผลลัพธ์เปลี่ยน ⚠️ บังคับทำ Context Switch สลับข้ามเอนจินทีละแถว! 2. PL/pgSQL Interpreter Sandbox (Black Box Call) • จัดเตรียม Function Call Frame และ Stack Memory ทีละรอบ • คำนวณใน WHERE 1 ล้านครั้ง + คำนวณซ้ำใน SELECT อีก 1 ล้านครั้ง • รวมการเรียกฟังก์ชันทะลุ 2,000,000 รอบใน Query เดียว! 💥 CPU บน DB Server พุ่งแตะ 100% สิ้นเปลืองรอบสัญญาณนาฬิกา ผลกระทบ: Double Evaluation & Slow Query • ใช้เวลาประมวลผลนานถึง 45 วินาที • ปิดกั้นการทำ Parallel Worker Scans ทีมงานเข้าใจผิดคิดว่า "PostgreSQL UDF ช้า" ทั้งที่ผิดที่การตั้งค่า 1. Query Planner: Function Inlining & Constant Folding • Planner รู้ว่าฟังก์ชันเป็น IMMUTABLE: ผลลัพธ์แน่นอนตาม Input • สลายขอบเขตฟังก์ชัน ยุบเนื้อคำสั่งมาแปะเป็น Native SQL Expression • ตัด PL/pgSQL Sandbox ทิ้ง! (Zero Context Switching Overhead) ⚡ รันสูตรคำนวณติดชิดระดับ CPU Register ในเสี้ยววินาที 2. CTE Single Evaluation Pattern (Eval-Once) • เขียน Query ด้วย CTE ให้คำนวณผลลัพธ์ครั้งเดียวใน Virtual View • ประโยค WHERE และ SELECT อ้างอิงผลลัพธ์เดิม ไม่ต้องรันซ้ำ 2 รอบ • ตัดจำนวนรอบการคำนวณจาก 2,000,000 ครั้ง เหลือ 1,000,000 ครั้ง • ปลดล็อก Parallel Query Scan กระจายลงทุก CPU Cores ได้เต็มสูบ เวลารวม: จาก 45 วินาที เหลือเพียง 15 มิลลิวินาที (เร็วขึ้น 300x!) CPU ทำงานเต็มประสิทธิภาพ คืน Connection ทันที ไม่ติดค้าง SLA สามารถนำผลลัพธ์ไปสร้าง Functional Index บนตารางได้โดยตรง
🎙️ สคริปต์คำพูดสำหรับพรีเซนท์บนเวที (Presentation Pitch Script)

"ทุกท่านครับ การที่ 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 มิลลิวินาทีครับ"

🧮 วิดเจ็ตทดลอง: ตัวจำลองผลกระทบของ Function Inlining & Double Evaluation

ลองปรับจำนวนแถวข้อมูลและเลือกกลยุทธ์การเขียนฟังก์ชัน เพื่อสังเกตจำนวนครั้งที่ฟังก์ชันถูกรันจริง และความแตกต่างของเวลา Execution Time

ผลลัพธ์การประเมินประสิทธิภาพ (Optimizer Outcome):
Function Inlining สำเร็จ: ยุบสูตรเข้ากับ Execution Plan หลัก ไร้ Context Switching และทำ Constant Folding ได้ 100%
0.015 วินาที (เร็วขึ้น 300 เท่า!)
2

💻 ปูพื้นฐานไวยากรณ์ & แบบฝึกหัดไต่ระดับ (UDFs Syntax & Hands-on)

การสร้าง User-Defined Function (UDF) ใน PostgreSQL ต้องเข้าใจโครงสร้างคำสั่ง CREATE FUNCTION, การระบุชนิดข้อมูลคืนค่า (RETURNS), การเลือกภาษา (LANGUAGE sql vs LANGUAGE plpgsql), และการติดแท็ก Volatility อย่างแม่นยำ:

🧱 แกะโครงสร้างไวยากรณ์ UDF: แม่แบบฟังก์ชันที่สนับสนุน Function Inlining

ไวยากรณ์หลัก: High-Performance Stored Function Blueprint Pillar 1 UDF Architecture
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 โดยสมบูรณ์

🏋️‍♂️ แบบฝึกหัดไต่ระดับการเขียนจริง (จากระดับเริ่มต้น ➔ Refactoring ระดับ Production)

🟢 Level 1: การสร้าง Pure Math Function พร้อมติดแท็ก IMMUTABLE ระดับเริ่มต้น
-- [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!
🟡 Level 2: การสร้าง Data Lookup Function พร้อมติดแท็ก STABLE ระดับปานกลาง
-- [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

🔴 Level 3: Refactoring โค้ดคำนวณราคาสุทธิระดับ Production (Inlining + Single Evaluation)

เปรียบเทียบโค้ดเดิมที่เป็น PL/pgSQL Default VOLATILE และเรียกซ้ำ 2 รอบ กับโค้ดใหม่ที่ใช้ LANGUAGE sql + IMMUTABLE ร่วมกับ CTE Single Evaluation Pattern:

❌ โค้ดเดิมแบบ Flawed: VOLATILE + Double Evaluation รัน 2,000,000 ครั้ง (45s)
-- ❌ ฟังก์ชันแบบ 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;
วิเคราะห์ข้อเสีย: สลับ Context ข้ามไปมาระหว่าง SQL Core กับ PL/pgSQL Sandbox 2 ล้านครั้ง CPU พุ่ง 100% และ Query รันนานถึง 45 วินาที
✅ โค้ดใหม่ระดับ Production: Inlining + CTE Single Evaluation 15 ms (เร็วขึ้น 300x)
-- [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 ไม่ต้องเรียกฟังก์ชันซ้ำอีกรอบ!
วิเคราะห์ข้อได้เปรียบ: Query Planner ยุบสูตรเข้าไปใน Execution Plan หลัก (Function Inlining) ตัด Call Frame ทิ้ง และคำนวณเพียงรอบเดียวต่อแถว ใช้เวลาเพียง 15 มิลลิวินาที!
3

📝 คลังข้อสอบและแบบประเมินความรู้ (18 ข้อ Multiple Choices พร้อมเฉลยยิบ)

แบบทดสอบวัดระดับความเข้าใจ Pillar 1: User-Defined Functions, Volatility Categories (IMMUTABLE, STABLE, VOLATILE), Function Inlining, STRICT behavior, และข้อสอบสะสมทบทวนครบทั้ง Day 1–4 คลิกเลือกคำตอบ ก, ข, ค, หรือ ง เพื่อดูผลลัพธ์และคำอธิบายอย่างละเอียดทันที

📊 สรุปผลการประเมิน Pillar 1: UDFs (Day 5) 0 / 18
ความคืบหน้า ตอบแล้ว 0 จาก 18 ข้อ

🎯 สรุปหมุดเชื่อมโยง & ก้าวต่อไปสู่ Day 6

บทเรียน 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 ได้อย่างมหาศาล

🚩 ก้าวต่อไปสู่ Day 6:

Pillar 2 - Stored Procedures & Transaction Control (COMMIT / ROLLBACK): ในวันถัดไป เราจะไปเจาะลึกเสาหลักต้นที่ 2 นั่นคือ Stored Procedures (CREATE PROCEDURE) และกลไกการควบคุม Transaction ภายในตัวโปรแกรมด้วยคำสั่ง COMMIT และ ROLLBACK เพื่อรองรับงาน Batch Processing ขนาดใหญ่ครับ!