← Back to PostgreSQL course
PHASE 1: THE CORE PROBLEM & ARCHITECTURE DAY 1 OF 18 🚀

Day 1: โครงสร้างบล็อกพื้นฐาน (DO $$) & ถอดรหัสปัญหา The Chatty Application Anti-Pattern

เริ่มต้นปูพื้นฐานการเขียน PL/pgSQL จากศูนย์ด้วย Anonymous Block (บล็อกคำสั่งไร้นาม) พร้อมเจาะลึกคอขวดสถาปัตยกรรมระดับองค์กรว่าทำไมการวนลูปยิงคำสั่งยิบย่อยข้ามเน็ตเวิร์กจึงทำให้ระบบล่ม และทำไมแนวคิด "Data Stays, Logic Moves" จึงชนะอย่างเด็ดขาด

📍 ตำแหน่งปัจจุบันของคุณบน Roadmap (18-Day Masterclass) Phase 1: เริ่มต้นก้าวแรก
Day 1: Anonymous Block (DO $$) & Chatty App
➔
Day 2: Storage Engine & Buffer Pool
➔
Day 3: Transaction Lock Contention
➔
Day 4: Architecture Mapping Summary
➔
Day 5: Pillar 1 - UDFs
เปิดบท

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

วันนี้เราเริ่มจากต้นตอที่ทำให้ระบบช้า ทั้งที่ CPU ของฐานข้อมูลยังว่างอยู่ — ไม่ใช่เพราะ PostgreSQL คิดช้า แต่เพราะแอปเดินข้ามเครือข่ายซ้ำๆ เพื่องานชิ้นเดียว

Chatty App Anti-Pattern

พฤติกรรมของแอป (C#, Java, Node.js) ที่ยิง SQL ยิบย่อยทีละคำสั่ง นับร้อยนับพันรอบ เพื่อให้งานชิ้นเดียวเสร็จ แทนที่จะรวบเป็นคำสั่งเดียวแล้วจบ

Network Latency RTT

เวลาที่ข้อมูลวิ่งจากแอปไปฐานข้อมูลแล้ววิ่งกลับ รอบละแค่ 1–2 มิลลิวินาที แต่ถ้ายิง 1,000 รอบ ก็เสียเวลาเดินทางฟรีๆ ไปแล้ว 1–2 วินาที

โดยรวมวันนี้คือการชี้ให้เห็นว่าระบบที่ช้า มักไม่ได้เกิดจากฐานข้อมูลประมวลผลช้า แต่เกิดจากแอปที่คุยกับฐานข้อมูลเป็นชิ้นเล็กชิ้นน้อย จน RTT สะสมกินเวลาไปหมด

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

เหมือนสั่งส้มตำหนึ่งถุง แต่เดินไปบอกแม่ค้าทีละอย่าง — “เอาพริก 3 เม็ด” แล้วเดินกลับโต๊ะ “เอามะนาว” แล้วเดินกลับโต๊ะอีก เสียเวลาเดินข้ามห้องยี่สิบรอบ ทั้งที่เขียนใส่กระดาษแผ่นเดียวส่งทีเดียวก็จบ

นี่คือจุดเริ่มของทั้งหลักสูตร เราต้องย้ายตรรกะมาประมวลผลในฐานข้อมูล เพื่อรวบงานให้จบใน single round-trip ไม่ใช่เดินข้ามเครือข่ายเป็นพันรอบ

1

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

คำว่า "Chatty" แปลตรงตัวว่า "พูดมาก / คุยไม่หยุด" ในทางสถาปัตยกรรมซอฟต์แวร์ มันคืออาการที่ Application ขยันยิงคำถาม-คำตอบยิบย่อยกับ Database ข้าม Network ไป-กลับนับพันนับหมื่นรอบ การนำเสนอหัวข้อ In-Database Engine ต่อบอร์ดหรือ Senior Architects จะไม่มีน้ำหนักเลย หากเราไม่เริ่มจากการชี้ให้เห็นความพังทลายของสถาปัตยกรรมเดิมข้อนี้ก่อน

🔥 วิกฤตคืนโปรโมชัน 11.11: เมื่อระบบ Checkout ล่มสลาย

ในคืนเทศกาล 11.11 ระบบ E-Commerce ต้องปรับราคาสินค้าค้างชำระ 50,000 รายการ เพื่อมอบส่วนลดพิเศษ ทีมพัฒนาใช้วิธีเขียนสคริปต์บน App Server (Node.js/Python) วนลูปดึงข้อมูลสินค้าทีละแถว (SELECT) มาคำนวณส่วนลดบน App Memory แล้วยิงคำสั่ง UPDATE กลับไปทีละแถวครบ 50,000 รอบ:

25 นาที
ระยะเวลารันสคริปต์ที่ยาวนานจนชนเวลาเปิดขาย
100% CPU
App Server ค้างจาก Serialization Overhead
Checkout Failed
ตารางถูกล็อกค้าง ลูกค้าชำระเงินไม่ได้ทั่วระบบ

📦 เปรียบเทียบกับโลกจริง: การขนของ 50,000 กล่องระหว่างสองตึก

❌ วิธีการแบบ Chatty (คนส่งข่าวพูดจายืดยาว)

คุณจ้างพนักงานเดินข้ามตึกจากตึก App ไปตึก Database เพื่อส่งข้อความและยกของทีละกล่อง เดินไป-กลับ 50,000 เที่ยว

  • แม้เดินเที่ยวละ 2 วินาที เวลารวมก็กลายเป็นเกือบ 28 ชั่วโมง!
  • ต้องคอยเปิดกล่องแกะดูของทุกรอบ (Serialization Overhead)
  • ทางเดินเชื่อมตึกติดขัดจนคนอื่นเดินไม่ได้ (Lock Contention)
✅ วิธีการแบบ "Data Stays, Logic Moves"

คุณส่ง "ใบสั่งงานใบเดียว (PL/pgSQL Block)" วิ่งข้ามตึกไปหาเจ้าหน้าที่ประจำโกดัง Database แล้วสั่งให้จัดการของทั้ง 50,000 ชิ้นรวดเดียว

  • คนส่งสารวิ่งข้ามตึกเพียง 1 เที่ยวถ้วน (1 Single Round-Trip)
  • ของถูกปรับสถานะในโกดัง RAM (Buffer Pool) เสร็จใน 0.2 วินาที
  • ปล่อยกุญแจโกดังทันที ไม่ขวางทางลูกค้าชำระเงิน
สถาปัตยกรรมการสื่อสาร: เปรียบเทียบการวิ่งของข้อมูล (Architecture Flow) End-to-End Latency Topology
❌ แย่: The Chatty Loop (วนลูปยิงข้ามเน็ตเวิร์ก 50,000 รอบ) ✅ ดี: In-Database Automation (1 Single Round-Trip) App Server (Node/Python) CPU 100% Serialization 50,000 Objects PostgreSQL Database Row Lock ค้าง idle in transaction Checkout ล่ม! SELECT #1 UPDATE #1 ⋮ (ทำซ้ำ 50,000 รอบ) RTT รวม = 50,000 × 2ms = 100s ผลลัพธ์: เสียเวลา 25 นาที, Connection Pool ล้น, ยอดขายพัง App Server ทำงานชิลๆ CPU < 2% ไม่สร้าง Objects PostgreSQL In-DB Engine รันใน RAM ตรงๆ (shared_buffers) บีบ Lock เหลือ ms ปลดกุญแจทันที 1 Single DO $$ Block คืนผลลัพธ์จบใน 1 เที่ยว ผลลัพธ์: ประมวลผลเสร็จใน < 0.2 วินาที (เร็วขึ้นหลายพันเท่า!)
🎙️ สคริปต์คำพูดสำหรับพรีเซนท์บนเวที (Presentation Pitch Script)

"ทุกท่านครับ คอขวดของระบบเราไม่ได้เกิดจาก 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 ให้เหลือเพียงมิลลิวินาทีครับ"

ถอดรหัสคำศัพท์เชิงลึก (Architecture Vocab & Mental Models)

⏱️ Network Latency & RTT
Round-Trip Time: เวลาเดินทางของข้อมูลไป-กลับข้ามสายเน็ตเวิร์ก หาก RTT = 2ms การยิง 50,000 รอบจะสูญเสียเวลาไปกับค่าเดินทางเปล่าๆ ถึง 100 วินาที โดยที่ DB ยังไม่ได้เริ่มคิดคำนวณเลยแม้แต่นิดเดียว
📦 Serialization Overhead
ภาระ CPU ในการ "แพ็กและแกะกล่องข้อมูล": ต้องแปลง Object ใน Memory ของ App ให้เป็น Binary Stream ส่งออกสาย LAN และฝั่ง DB ต้องแปลงกลับมาเป็น Row ทำแบบนี้หมื่นๆ รอบ CPU ทั้งสองฝั่งจึงพุ่งแตะ 100%
🧠 Data Stays, Logic Moves
แก่นแท้ของระบบประสิทธิภาพสูง: ข้อมูลมีขนาดใหญ่และเคลื่อนย้ายลำบาก ดังนั้น "จงส่งสคริปต์ตรรกะขนาดเล็กเข้าไปทำงานติดชิดกับข้อมูลใน DB RAM แทนที่จะขนข้อมูลดิบมหาศาลออกมาหาโค้ด"
⚡ Single Round-Trip Boundary
การออกแบบสถาปัตยกรรมที่ยุบการสื่อสารระหว่าง App กับ DB จาก $N$ ครั้ง ให้เหลือเพียง 1 ครั้ง ช่วยบีบสโคปการถือ Row Lock ให้เหลือเพียงระดับมิลลิวินาที ป้องกันปัญหา Checkout ล่มได้อย่างเด็ดขาด
2

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

ก่อนจะเริ่มเขียน Function หรือ Procedure เรามาเริ่มต้นจาก Anonymous Block (บล็อกไร้นาม) ซึ่งขึ้นต้นด้วย DO $$ ... $$; ข้อดีคือเป็นสคริปต์ที่รันครั้งเดียวทิ้ง ไม่ต้องตั้งชื่อ ไม่ต้องสร้างเป็น Object ค้างไว้ในฐานข้อมูล เหมาะสำหรับงาน Migrate หรือประมวลผลงานพิเศษ

🧱 แกะโครงสร้าง 3 ส่วนประกอบของ PL/pgSQL Block

จำง่ายๆ ว่าโครงสร้างบล็อกมี 3 ส่วน แต่ส่วนที่บังคับต้องมีเสมอคือ BEGIN ... END; เท่านั้น

ไวยากรณ์มาตรฐาน: Anonymous Block (DO $$) PL/pgSQL Block Structure
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 $$;
💡 กฎเหล็กที่มือใหม่ต้องจำ: ใน PL/pgSQL สัญลักษณ์ := ใช้สำหรับกำหนดค่าใส่ตัวแปร (Assignment) ส่วนเครื่องหมาย = ใช้สำหรับเปรียบเทียบเงื่อนไข (Comparison) ในประโยค IF หรือ WHERE

🏋️‍♂️ แบบฝึกหัดไต่ระดับการเขียนจริง (จากระดับอนุบาล ➔ แก้ปัญหาระบบจริง)

🟢 Level 1: คำนวณเบื้องต้น (Variables & Math Expressions) ระดับเริ่มต้น
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
จุดที่ต้องสังเกต: เราสามารถนำตัวแปรมาคำนวณคณิตศาสตร์บวกลบคูณหารได้ตามปกติ และส่งค่าต่อกันเป็นทอดๆ ได้อย่างตรงไปตรงมา
🟡 Level 2: ดึงข้อมูลตารางมาใส่ตัวแปร (SELECT ... INTO & IF-ELSE) ระดับปานกลาง
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

🔴 Level 3: Refactoring โค้ดแก้ปัญหา Chatty App (Single Round-Trip)

เปรียบเทียบโค้ดเดิมที่วนลูปใน Application กับโค้ดใหม่ที่เขียนด้วย Anonymous Block พร้อมฟีเจอร์เด็ด RETURNING ของ PostgreSQL:

❌ โค้ดเดิมฝั่ง App: Python Loop (พังและช้า) 50,000 Network Trips
# โค้ดฝั่ง 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)
    )
วิเคราะห์ข้อเสีย: สลับโอนข้อมูลข้ามสาย LAN 50,000 ครั้ง และบังคับให้ CPU ทั้งสองฝั่งต้องแปลง JSON/Objects ไปมาจนแตะ 100%
✅ โค้ดใหม่: PL/pgSQL Block (ถอดไวยากรณ์ทีละบรรทัด) 1 Single Round-Trip
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 $$;
วิเคราะห์ข้อได้เปรียบ: สั่งงานครั้งเดียวจบ ข้อมูลถูกอัปเดตบน RAM (shared_buffers) ในเสี้ยววินาที และใช้ RETURNING นับผลลัพธ์สรุปกลับมาได้ทันที
3

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

แบบทดสอบวัดระดับความเข้าใจสถาปัตยกรรมและไวยากรณ์ Anonymous Block (DO $$) ประจำ Day 1 ครอบคลุมเนื้อหาครบทั้ง 18 ข้อ คลิกเลือกคำตอบ ก, ข, ค, หรือ ง เพื่อดูผลลัพธ์และคำอธิบายอย่างละเอียดทันที

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

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

บทเรียน Day 1: โครงสร้างบล็อกพื้นฐาน (DO $$) & ถอดรหัสปัญหา The Chatty Application Anti-Pattern ชี้ให้เห็นว่าคอขวดที่แท้จริงของระบบไม่ได้เกิดจากความเร็วของคำสั่ง SQL แต่เกิดจากสถาปัตยกรรมการสื่อสารข้าม Layer การเปลี่ยนมาใช้ PL/pgSQL Block ร่วมกับหลักการ "Data Stays, Logic Moves" ช่วยตัด Network Latency และ Serialization Overhead ออกได้อย่างหมดจด

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

Storage Engine & Buffer Pool Mechanics: ในวันถัดไป เราจะเจาะลึกลงไปในระดับ Storage Engine ว่าทำไม Logic ที่รันติดชิด Shared Buffer Pool (RAM) ของ PostgreSQL ถึงประมวลผลข้อมูลระดับล้านแถวได้เร็วในระดับ Nanoseconds ครับ!