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

Day 2: Storage Engine & Buffer Pool Mechanics (กลไกหน่วยความจำ Shared Buffers, Disk I/O Bottleneck และการประมวลผลแบบ Buffer Pool Proximity)

เจาะลึกกลไกภายในของ PostgreSQL Storage Engine ว่าทำไมข้อมูล Data Pages ขนาด 8KB ที่พักอยู่ใน Shared Buffer Pool บน RAM ถึงประมวลผลเร็วกว่าการอ่านจาก Disk นับแสนเท่า และการเขียน PL/pgSQL ด้วยแนวคิด "Buffer Pool Proximity" ช่วยตัดภาระ Disk I/O Bottleneck ได้อย่างไร

📍 ตำแหน่งปัจจุบันของคุณบน Roadmap (18-Day Masterclass) Phase 1: ก้าวสู่ระดับ Memory Architecture
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
เปิดบท

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

เมื่อเราอยากประมวลผลในฐานข้อมูล คำถามถัดไปคือฐานข้อมูลเก็บและดึงข้อมูลอย่างไร — วันนี้เราดูสองชั้นที่ทำงานคู่กัน: ความจำบน RAM กับไฟล์บนดิสก์

Buffer Pool

พื้นที่ RAM ส่วนกลางของฐานข้อมูล ใน PostgreSQL คือ shared_buffers ถูกตัดเป็นบล็อก 8KB ทำหน้าที่แคชพักข้อมูล เพื่อให้อ่านและแก้บน RAM ได้ทันทีโดยไม่ต้องแตะฮาร์ดดิสก์

Storage Engine Mechanics

กลไกที่จัดการพื้นที่จัดเก็บบนดิสก์ จับบล็อก 8KB ไว้ในไฟล์ แล้วคอยดึงขึ้นมาที่ RAM หรือเขียนบล็อกที่ถูกแก้กลับลงดิสก์

โดยรวมวันนี้คือเรื่องที่ RAM กับดิสก์ส่งไม้ต่อกันอย่างไร เวลาฐานข้อมูลจะค้นหาหรือแก้ไขข้อมูลขนาด 8KB — ถ้าบล็อกนั้นอยู่บน RAM อยู่แล้ว งานจะจบเร็วมาก ถ้าไม่มี ต้องเดินไปเปิดไฟล์บนดิสก์ก่อน

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

เหมือนโต๊ะทำงานในออฟฟิศ (Buffer Pool) กับห้องเก็บเอกสารชั้นล่าง (Storage Engine) ถ้าเอกสารถูกหยิบมาวางบนโต๊ะแล้ว อ่านและแก้ได้ในไม่กี่วินาที แต่ถ้าไม่มีบนโต๊ะ ต้องเดินลงไปเปิดตู้เอกสารชั้นล่างขึ้นมาก่อน

จาก Day 1 ที่บอกว่าต้องย้ายงานเข้าฐานข้อมูล วันนี้บอกต่อว่าให้เขียน Query ให้เจอข้อมูลบน RAM (Buffer Hit) แทนที่จะบังคับให้ฐานข้อมูลเดินไปอ่านดิสก์ (Disk I/O) ที่ช้ากว่าเป็นพันเท่า

1

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

การสร้าง In-Database Automation Engine บน PostgreSQL ให้มีประสิทธิภาพระดับ Nanoseconds เราไม่สามารถมองฐานข้อมูลเป็นเพียงตารางลอยๆ ได้ แต่ต้องเข้าใจกลไกภายในของ Storage Engine โดยเฉพาะ Shared Buffer Pool (shared_buffers) ซึ่งเป็นพื้นที่ RAM หลักที่ PostgreSQL ใช้พักบล็อกข้อมูล (Data Pages ขนาด 8KB)

🏦 วิกฤตประมวลผลยอดสิ้นวัน 10 ล้านบัญชี: เมื่อ Disk I/O Wait พุ่งแตะ 98%

ระบบคำนวณดอกเบี้ยและยอดคงเหลือสิ้นวัน (End-of-Day Balance Processing) ของสถาบันการเงินแห่งหนึ่ง มีข้อมูลบัญชีลูกค้า 10,000,000 แถว ทีมพัฒนาเขียนสคริปต์บน App Server ดึงข้อมูลทีละ Batch ข้าม Network มาคำนวณใน App Memory ผลปรากฏว่า:

98% I/O Wait
เซิร์ฟเวอร์ติดคอขวด IO:DataFileRead จนแทบค้าง
60% Hit Ratio
Cache Hit ร่วงกราว เกิด Buffer Cache Thrashing
8 ชั่วโมงรวด
รันข้ามคืน 22:00 - 06:00 น. ชนเวลาเปิดระบบสาขา

📚 เปรียบเทียบกับโลกจริง: การค้นหาหนังสือ 10 ล้านเล่มในห้องสมุด

❌ วิธีการเดิม: เดินลงไปค้นโกดังใต้ดินทีละเล่ม (Buffer Read / Disk I/O)

คุณสั่งพนักงานให้นำหนังสือทีละเล่มจากโกดังใต้ดิน (Disk NVMe/SSD) แบกขึ้นลิฟต์มาให้เชฟที่นั่งอยู่อีกตึกหนึ่ง (App Server)

  • การเดินลงโกดังใต้ดินใช้เวลา 100,000 นาโนวินาที (ช้ากว่าเปิดอ่านบนโต๊ะ 1,000 เท่า)
  • โต๊ะทำงานข้างบนมีหนังสือใหม่มาเบียดหนังสือเก่าตกโต๊ะตลอดเวลา (Cache Thrashing)
  • ลิฟต์ขนส่งติดค้างจนระบบอื่นใช้งานไม่ได้ (Disk I/O Bottleneck)
✅ วิธีการแบบ Buffer Pool Proximity (ประมวลผลติดชิด RAM)

คุณส่ง "สคริปต์คำนวณสั้นๆ (PL/pgSQL Block)" เดินเข้าไปหาโต๊ะอ่านหนังสือขนาดใหญ่ (Shared Buffers) ที่หนังสือถูกวางกางรอไว้อยู่แล้ว

  • กวาดสายตาอ่านและขีดเขียนบนโต๊ะ RAM ได้ทันทีในเวลา ~100 นาโนวินาที
  • ไม่ต้องเดินลงโกดังใต้ดินซ้ำซ้อน (Disk Read = 0, Cache Hit Ratio > 99%)
  • คำนวณและปรับยอด 10 ล้านแถวเสร็จรวดเดียวภายใน ไม่ถึง 3 นาที

⚡ บันไดความเร็วของคอมพิวเตอร์ (Latency Numbers Every Architect Must Know)

ทำไมการรันบน RAM (shared_buffers) ถึงเร็วกว่าการปล่อยให้หลุดไปอ่าน Disk? ดูความแตกต่างทางฟิสิกส์ของหน่วยเวลา:

Shared Buffers RAM (In-DB)
~100 Nanoseconds
NVMe SSD Disk Read
~100 Microseconds (1,000x ช้ากว่า)
Local Network RTT
~1 - 2 Milliseconds (20,000x ช้ากว่า)
Magnetic Hard Drive (HDD)
~10 Milliseconds (100,000x ช้ากว่า)
สถาปัตยกรรมระดับลึก: การเดินทางของหน่วยความจำ (Storage Engine Memory Flow) Shared Buffers Mechanics
❌ แย่: App Loop ดึงข้าม Network (Cache Thrashing + Disk I/O) ✅ ดี: Buffer Pool Proximity (ประมวลผลบน RAM 100%) Storage Disk Data Files (NVMe) I/O Wait 98% หัวอ่านทำงานหนัก shared_buffers RAM 8KB Pages Hit Ratio 60% Cache โดนเตะทิ้ง App Server (Python / Node) RAM บวม 4x 10M Objects Read Network ⚠️ วงจรหายนะ: Buffer Cache Thrashing 1. แอปขอข้อมูลชุดใหม่ ➔ บังคับอ่าน Data Page 8KB จาก Disk 2. Data Page ใหม่เบียดเตะ Data Page เดิมออกจาก shared_buffers 3. เมื่อแถวถัดไปต้องการใช้ Page เดิม ➔ ต้องวิ่งลงไปอ่าน Disk ซ้ำ! ผลลัพธ์: Disk I/O ติดเพดาน 98%, งานสิ้นวันลากยาว 8 ชั่วโมง PostgreSQL Server (shared_buffers RAM Engine) 8KB Data Pages โหลดขึ้น RAM พักไว้ Clean / Dirty State PL/pgSQL Block รันติดชิด Memory Direct Pointer < 100ns ⚡ Write-Ahead Log (WAL): Sequential Write รับประกัน ACID ปลอดภัย ผลลัพธ์: Cache Hit Ratio > 99% (Disk Read = 0) ประมวลผลคำนวณและปรับยอด 10 ล้านบัญชีจบใน < 3 นาที ไร้การคัดลอก Memory ข้าม Layer, ระบบหลักเปิดบริการได้ตรงเวลา
🎙️ สคริปต์คำพูดสำหรับพรีเซนท์บนเวที (Presentation Pitch Script)

"ทุกท่านครับ คอขวดการประมวลผลสิ้นวันไม่ได้เกิดจาก CPU คำนวณช้า แต่เกิดจาก Disk I/O Bottleneck จากการกวาดอ่าน Data Pages ขนาด 8KB จาก Disk ขึ้นๆ ลงๆ จนค่า Cache Hit Ratio ตกเหลือเพียง 60%

ด้วยการใช้ PL/pgSQL In-Database Processing ร่วมกับกลไก Buffer Pool Proximity เราจะประมวลผลข้อมูลที่โหลดอยู่ใน shared_buffers บน RAM โดยตรง ยึดค่า Cache Hit Ratio ให้สูงกว่า 99% ลด Disk Read จากกิกะไบต์เหลือศูนย์ และตัดเวลาประมวลผลสิ้นวันจาก 8 ชั่วโมงเหลือเพียงไม่ถึง 3 นาทีครับ"

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

🧠 Shared Buffer Pool (shared_buffers)
พื้นที่ RAM กลางของ PostgreSQL สำหรับแคชบล็อกข้อมูล หาก Query หาข้อมูลเจอในนี้จะเรียกว่า Buffer Hit (เข้าถึงเร็วระดับ Nanoseconds) แต่ถ้าไม่เจอต้องลงไปอ่าน Disk เรียกว่า Buffer Read (ช้ากว่าหลายพันเท่า)
📦 Data Page (8KB Block)
หน่วยจัดเก็บพื้นฐานที่สุดใน PostgreSQL ไม่ว่าเราจะขอข้อมูลเพียงคอลัมน์เดียวหรือแถวเดียว PostgreSQL จะต้องโหลด Data Page ขนาด 8KB ทั้งหน้า ขึ้นมาพักไว้ใน shared_buffers เสมอ
🌪️ Buffer Cache Thrashing
ปรากฏการณ์ที่เกิดจาก Access Pattern แย่ เช่น การวนลูปอ่านข้อมูลขนาดใหญ่จนเตะ Data Pages ดีๆ ที่ถูกใช้บ่อยหลุดออกจาก RAM บังคับให้ฐานข้อมูลต้องวิ่งลงไปอ่านจาก Disk ซ้ำซากจนเกิด Disk I/O Bottleneck
📝 Clean Page vs Dirty Page
Clean Page: หน้าใน RAM ที่ข้อมูลตรงกับบน Disk
Dirty Page: หน้าใน RAM ที่ถูกคำสั่ง UPDATE/INSERT แก้ไขแล้ว แต่ยังไม่ได้เขียน (Flush) ลงดิสก์จริง รอ Checkpointer มาเคลียร์
🛡️ Write-Ahead Log (WAL)
กลไกรับประกันความปลอดภัย (ACID): ทุกการเปลี่ยนแปลงจะถูกบันทึกลงไฟล์ WAL บน Disk แบบ Sequential Write รวดเร็วก่อนที่จะไปแตะต้อง Data Page ใน Memory ช่วยป้องกันข้อมูลสูญหายเมื่อเครื่องดับ
📊 Cache Hit Ratio (> 99%)
สัดส่วนการค้นพบข้อมูลใน RAM เทียบกับการอ่านทั้งหมด: $\frac{\text{Hits}}{\text{Hits} + \text{Reads}} \times 100\%$ ในระบบ Production ที่ดีต้องรักษาระดับให้สูงกว่า 99% เสมอ
🧮 วิดเจ็ตทดลองคำนวณ: ตัวจำลองผลกระทบของ Cache Hit Ratio ต่อ Disk I/O (Latency Simulator)

ลองปรับเปอร์เซ็นต์ Cache Hit Ratio และจำนวนคำขอข้อมูล เพื่อดูว่าเมื่อข้อมูลหลุดไปอ่านจาก Disk (NVMe/SSD) เวลารวมจะพุ่งสูงขึ้นกี่พันเท่าตัว

ผลรวมเวลาประมวลผลการอ่านข้อมูล (Estimated I/O Time):
(RAM Hit = 0.0001 ms | Disk Read = 0.1 ms ต่อ Page)
0.11 วินาที (สถานะ: ยอดเยี่ยม RAM Proximity)
2

💻 ปูพื้นฐานไวยากรณ์ & แบบฝึกหัดไต่ระดับ (Dynamic Anchoring: %TYPE & %ROWTYPE)

ในการเขียน PL/pgSQL ร่วมกับ Storage Engine การฮาร์ดโค้ดชนิดตัวแปร (เช่น v_name VARCHAR(50);) มีความเสี่ยงสูงหากอนาคตตารางเปลี่ยน Schema การใช้ Dynamic Data Anchoring (%TYPE และ %ROWTYPE) ช่วยผูกชนิดข้อมูลกับตารางโดยตรงในระดับ RAM ป้องกัน Data Alignment Mismatch ได้อย่างสมบูรณ์

🧱 แกะโครงสร้างไวยากรณ์: %TYPE vs %ROWTYPE

จำง่ายๆ: %TYPE ผูกกับ "คอลัมน์เดียว" ส่วน %ROWTYPE ผูกกับ "ทั้งแถว (ทุกคอลัมน์)"

ไวยากรณ์มาตรฐาน: Dynamic Data Anchoring PL/pgSQL Memory Anchoring
DO $$
DECLARE
    -- [Statement 1: %TYPE] ผูกชนิดข้อมูลของตัวแปรตามคอลัมน์เฉพาะในตาราง
    -- ไวยากรณ์: ชื่อตาราง.ชื่อคอลัมน์%TYPE;
    v_emp_id   employees.employee_id%TYPE := 101;
    v_salary   employees.salary%TYPE;

    -- [Statement 2: %ROWTYPE] สร้างตัวแปรเรกคอร์ดรับโครงสร้างทั้งแถวของตาราง
    -- ไวยากรณ์: ชื่อตาราง%ROWTYPE;
    v_emp_rec  employees%ROWTYPE;

BEGIN
    -- [Statement 3: SELECT ... INTO Record] ดึงข้อมูลทั้งแถวจาก Buffer มาพักใน RAM Record
    SELECT * INTO v_emp_rec
    FROM employees
    WHERE employee_id = v_emp_id;

    -- [Statement 4: Accessing Record Fields] อ้างอิงฟิลด์ผ่านเครื่องหมายจุด (.field_name)
    RAISE NOTICE 'พนักงาน: % % | เงินเดือนปัจจุบัน: %',
                 v_emp_rec.first_name, v_emp_rec.last_name, v_emp_rec.salary;
END $$;
💡 ประโยชน์สูงสุด: หากวันข้างหน้า DBA ทำการขยายคอลัมน์ salary จาก NUMERIC(8,2) เป็น NUMERIC(14,2) โค้ด PL/pgSQL นี้จะปรับตามอัตโนมัติ ไม่ต้องแก้โค้ดและไม่เกิด Runtime Overflow!

🏋️️‍♂️ แบบฝึกหัดไต่ระดับการเขียนจริง (จากระดับเริ่มต้น ➔ แก้ปัญหา Disk I/O Bottleneck)

🟢 Level 1: การใช้ %TYPE เพื่อความปลอดภัยของชนิดข้อมูล (Data Type Safety) ระดับเริ่มต้น
DO $$
DECLARE
    -- [1] ประกาศตัวแปรโดยอ้างอิงชนิดข้อมูลจากตาราง accounts โดยตรง
    v_acc_id  accounts.account_id%TYPE := 5001; -- ถ้านานไป account_id เปลี่ยนเป็น BIGINT โค้ดนี้จะไม่พัง
    v_balance accounts.balance%TYPE;            -- อ้างอิงชนิดข้อมูล balance จากตาราง accounts
BEGIN
    -- [2] ดึงค่า balance มาใส่ตัวแปร v_balance
    SELECT balance INTO v_balance
    FROM accounts
    WHERE account_id = v_acc_id;

    -- [3] แสดงผลลัพธ์ออก Log
    RAISE NOTICE 'รหัสบัญชี: % | ยอดเงินคงเหลือ: % บาท', v_acc_id, v_balance;
END $$;

-- 📤 ผลลัพธ์ใน Log:
-- NOTICE: รหัสบัญชี: 5001 | ยอดเงินคงเหลือ: 45000.00 บาท
จุดที่ต้องสังเกต: ตัวแปร v_balance จะมีชนิดข้อมูลเหมือนกับคอลัมน์ balance ในตารางเป๊ะๆ โดยไม่ต้องเปิดดูโครงสร้างตารางล่วงหน้า
🟡 Level 2: การใช้ %ROWTYPE จัดเก็บโครงสร้างทั้งแถวบน Memory Buffer ระดับปานกลาง
DO $$
DECLARE
    v_target_id accounts.account_id%TYPE := 5001;
    v_acc_row   accounts%ROWTYPE; -- [1] สร้างตัวแปรเรกคอร์ดรองรับทุกคอลัมน์ของตาราง accounts
BEGIN
    -- [2] ดึงข้อมูลทั้งแถว (Data Page ใน shared_buffers) มายัดใส่ v_acc_row
    SELECT * INTO v_acc_row
    FROM accounts
    WHERE account_id = v_target_id;

    -- [3] แสดงผลข้อมูลจากฟิลด์ต่างๆ ของ %ROWTYPE โดยใช้เครื่องหมายจุด (.)
    IF v_acc_row.status = 'ACTIVE' THEN
        RAISE NOTICE 'บัญชี % ของคุณ % % มีสถานะใช้งานปกติ ยอดเงิน: % บาท',
                     v_acc_row.account_id, v_acc_row.first_name, v_acc_row.last_name, v_acc_row.balance;
    ELSE
        RAISE NOTICE 'บัญชี % มีสถานะ: %', v_acc_row.account_id, v_acc_row.status;
    END IF;
END $$;
จุดที่ต้องสังเกต: ไม่ต้องประกาศตัวแปรแยกทีละตัวสำหรับ first_name, last_name, balance อีกต่อไป ใช้ v_acc_row.ชื่อคอลัมน์ ได้ทันที

🔴 Level 3: Refactoring โค้ดแก้ปัญหา Disk I/O Bottleneck (Buffer Pool Proximity)

เปรียบเทียบโค้ดเดิมที่กวาดอ่านข้อมูลข้าม Network จนล้าง Cache ใน RAM ทิ้ง กับโค้ดใหม่ที่เขียนด้วย PL/pgSQL ประมวลผลติดชิด shared_buffers:

❌ โค้ดเดิมฝั่ง App: กวาดอ่านล้าง Cache (ช้าและ I/O พุ่ง) Cache Hit < 60%
# โค้ดฝั่ง Application Server
# วนลูปดึงข้อมูลทีละ 1,000 แถวมาคำนวณดอกเบี้ย
# เกิด Buffer Read จาก Disk ซ้ำซ้อน และ Cache Thrashing!
accounts = db.execute(
    "SELECT account_id, balance FROM accounts WHERE status = 'ACTIVE'"
)
for acc in accounts:
    new_balance = acc.balance + (acc.balance * 0.05) # คำนวณดอกเบี้ย 5%
    db.execute(
        "UPDATE accounts SET balance = %s WHERE account_id = %s",
        (new_balance, acc.account_id)
    )
วิเคราะห์ข้อเสีย: สแกน Data Pages จาก Disk ซ้ำซาก เตะแคชดีๆ ทิ้งจนเกิด Disk I/O Wait 98% และกินเวลาประมวลผลนานถึง 8 ชั่วโมง
✅ โค้ดใหม่: PL/pgSQL In-Database บน shared_buffers Cache Hit > 99%
DO $$
DECLARE
    -- [บรรทัดที่ 1] ประกาศตัวแปรอัตราดอกเบี้ย โดยใช้อ้างอิง %TYPE จากคอลัมน์ balance
    v_interest_rate accounts.balance%TYPE := 0.05;

    -- [บรรทัดที่ 2] ประกาศตัวแปรเก็บจำนวนแถวที่ประมวลผลสำเร็จ
    v_processed_cnt INT;
BEGIN
    -- [บรรทัดที่ 3] เริ่มต้น CTE เพื่อประมวลผลคำนวณและอัปเดตข้อมูลบน shared_buffers ใน RAM โดยตรง
    WITH updated_accounts AS (

        -- [บรรทัดที่ 4-7] คำสั่ง UPDATE ประมวลผลแบบ Set-based ติดชิด Buffer Pool
        -- ทำการอ่านและแก้ไข Data Pages ขนาด 8KB ที่อยู่ใน shared_buffers รวดเดียว ไม่ต้องดึงออกนอก DB Engine
        UPDATE accounts
        SET balance = balance + (balance * v_interest_rate),
            last_interest_applied = CURRENT_TIMESTAMP
        WHERE status = 'ACTIVE'

        -- [บรรทัดที่ 8] พ่น account_id ของแถวที่อัปเดตสำเร็จบน RAM ส่งต่อไปเก็บใน CTE updated_accounts
        RETURNING account_id
    )
    -- [บรรทัดที่ 9] สั่งนับจำนวนรายการที่ถูกประมวลผลทั้งหมด ยัดใส่ v_processed_cnt
    SELECT COUNT(*) INTO v_processed_cnt FROM updated_accounts;

    -- [บรรทัดที่ 10] แสดงผลการประมวลผลบน RAM
    RAISE NOTICE 'ประมวลผลคิดดอกเบี้ยสำเร็จ % รายการ (Buffer Cache Hit Rate > 99%%)', v_processed_cnt;
END $$;
วิเคราะห์ข้อได้เปรียบ: ไร้การแปลง Serialization ข้อมูลถูกอ่านและแก้ไขบน Data Page 8KB ใน Shared Buffer Pool โดยตรง รวดเร็วระดับ Nanoseconds และใช้เวลาเพียงไม่ถึง 3 นาที
3

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

แบบทดสอบวัดระดับความเข้าใจสถาปัตยกรรม Storage Engine, Shared Buffers, Data Pages 8KB, Dynamic Anchoring (%TYPE / %ROWTYPE) และข้อสอบทบทวน Day 1 คลิกเลือกคำตอบ ก, ข, ค, หรือ ง เพื่อดูผลลัพธ์และคำอธิบายอย่างละเอียดทันที

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

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

บทเรียน Day 2: Storage Engine & Buffer Pool Mechanics ชี้ให้เห็นถึงความสำคัญของการประมวลผลติดชิดหน่วยความจำ Shared Buffer Pool (shared_buffers) โดยใช้หลักการ Buffer Pool Proximity ร่วมกับการประกาศตัวแปรที่ปลอดภัยแบบ %TYPE และ %ROWTYPE ซึ่งช่วยลด Disk I/O Bottleneck และรักษาค่า Cache Hit Ratio ให้สูงกว่า 99%

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

Transaction Lock Contention & Concurrency Control: ในวันถัดไป เราจะไปเจาะลึกเรื่องการบริหารจัดการ Row-level Locks (FOR UPDATE, SKIP LOCKED) และการตั้งค่า lock_timeout เพื่อป้องกันปัญหาตารางติด Lock ค้างระหว่างการประมวลผลระดับ Concurrency สูงครับ!