Local-first Data Engineering ด้วย DuckDB
3.2 Local-first Data Engineering ด้วย DuckDB
แนวคิด Local-first Data Engineering คือการลดต้นทุนระบบคลาวด์โดยประมวลผลและทดสอบโค้ดบนเครื่องส่วนตัว ซึ่งในปัจจุบันประสิทธิภาพ CPU และหน่วยความจำมีขีดจำกัดความเร็วสูงมากพอในการประมวลผลข้อมูลขนาดกิกะไบต์
Technical Architecture Diagram
1. โครงสร้างข้อมูลรายคอลัมน์ vs. ระบบจัดเก็บแบบรายแถว
Learning Progression
- [BASIC] ปูพื้นฐานภาษาเข้าใจง่าย - เข้าใจคอนเซปต์ภาพรวมและการแก้ปัญหาเบื้องต้น
- [INTERMEDIATE] โค้ด/คอนฟิกไวยากรณ์จริง - การเขียนโค้ดเพื่อใช้งานจริงในระบบ
- [PROFESSIONAL] Under-the-hood & Performance/FinOps - กลไกเบื้องลึกและการรีดประสิทธิภาพ
Real-World Enterprise Scenario
เคสระบบการเงิน/Big Tech: การรองรับ Transaction จำนวนมหาศาลต่อวินาทีพร้อมประกัน Data Integrity สูงสุด โดยใช้สถาปัตยกรรมที่ยืดหยุ่นและการมอนิเตอร์ระดับสูง
ทฤษฎีและกลไกการทำงาน (How it works): ฐานข้อมูลแบบรายแถว (Row-oriented - RDBMS) บันทึกค่าทุกคอลัมน์ในแถวติดกันบนแผ่นดิสก์ เหมาะกับงานเขียนเป็นธุรกรรมรายตัว (OLTP) ในขณะที่ฐานข้อมูลรายคอลัมน์ (Columnar database เช่น DuckDB, ClickHouse) จัดเก็บค่าข้อมูลในแต่ละคอลัมน์แยกกันออกจากกันอย่างสิ้นเชิง เหมาะอย่างยิ่งสำหรับงานวิเคราะห์สรุปผล (OLAP) เนื่องจากคิวรีจะอ่านเฉพาะข้อมูลคอลัมน์ที่ระบุในเงื่อนไขการคำนวณเท่านั้น และข้ามคอลัมน์อื่นได้โดยตรง ส่งผลให้ประหยัดพื้นที่ดิสก์ I/O มหาศาล
# Concept of row storage vs column storage
Row-oriented (PostgreSQL):
[Row 1: ID, Name, Age] -> [Row 2: ID, Name, Age]
Column-oriented (DuckDB):
[Column ID: 1, 2] -> [Column Name: 'A', 'B'] -> [Column Age: 25, 30]
Use Case ในชีวิตจริง (Real-world Scenario): การรันหาค่าเฉลี่ยของราคาสินค้าจากตารางประวัติธุรกรรมขนาด 10 ล้านแถว DuckDB จะสแกนเฉพาะค่าในคอลัมน์ `price` เท่านั้น ใช้เวลาประมวลผลเสี้ยววินาที ในขณะที่ฐานข้อมูลรายแถวแบบเดิมต้องโหลดระเบียนทุกคอลัมน์ขึ้นมาทั้งหมด ทำให้รันได้ช้ากว่ามาก
ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations): ฐานข้อมูลแบบคอลัมน์ไม่เหมาะกับงานเขียนแทรกทีละแถวถี่ๆ (High-frequency single-row insert) เพราะมีภาระในการจัดเก็บข้อมูลแยกคอลัมน์สูงมาก แก้ไขโดยสะสมข้อมูลเป็นชุด (Micro-batch) แล้วเขียนลงดิสก์พร้อมกันทีเดียว
2. Vectorized Execution Engine และระบบคำนวณความเร็วสูง SIMD
ทฤษฎีและกลไกการทำงาน (How it works): ระบบประมวลผลทั่วไปจะทำการวนลูปประมวลผลทีละแถวข้อมูลซึ่งเกิดความล่าช้าจาก Instruction loop overhead แต่ DuckDB ใช้เครื่องยนต์รันมติแบบ **Vectorized Execution** โดยจัดเก็บข้อมูลแยกเป็นกลุ่มอาร์เรย์ (Vectors ขนาด 1024 ค่า) และประมวลผลพร้อมกันในหน่วยความจำ L1/L2 Cache ของ CPU ซึ่งประสานร่วมกับคำสั่งระดับฮาร์ดแวร์ **SIMD (Single Instruction Multiple Data)** ทำให้ CPU คำนวณขนานกันในระดับสัญญาณนาฬิกา
# Python demonstration comparing loop vs vectorized calculations
import numpy as np
# Simulate 1 million order amounts
amounts = np.random.rand(1000000)
# 1. Slow loop computation
# total = [x * 1.07 for x in amounts]
# 2. Fast Vectorized SIMD computation (DuckDB model)
total_vectorized = amounts * 1.07
Use Case ในชีวิตจริง (Real-world Scenario): วิศวกรข้อมูลรันคำนวณภาษีมูลค่าเพิ่มย้อนหลังบนประวัติธุรกรรมสิบปีในแล็ปท็อปส่วนตัว Vectorized engine ช่วยให้คอมพิวเตอร์รันผ่าน CPU Cache โดยตรงและประมวลผลเสร็จสิ้นได้ในเวลาไม่ถึงวินาที
ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations): ประสิทธิภาพ Vectorized จะลดลงหากประมวลผลคอลัมน์ประเภท String ขนาดใหญ่ที่มีสถิติผันแปรสูง แก้ไขโดยแปลงประเภทข้อมูลให้เป็นหมวดหมู่ (Enum/Dictionary encoding) เพื่อลดขนาดบิตของอาร์เรย์ในแรม
3. DuckDB HTTPFS Extension และกลไกคิวรีไฟล์ Parquet จาก AWS S3
ทฤษฎีและกลไกการทำงาน (How it works): ส่วนต่อขยาย **httpfs** ของ DuckDB ช่วยให้ผู้พัฒนาสามารถรันคำสั่งคิวรีข้อมูล SQL บนไฟล์ Parquet/CSV ที่เก็บอยู่บนคลาวด์ภายนอกได้โดยตรง ผ่านโปรโตคอล HTTP range requests กลไกนี้จะส่งคำขออ่านพิกัดเฉพาะฟิลด์ Metadata ของไฟล์ Parquet เพื่อหาพิกัดระดับบิตของคอลัมน์ที่ต้องการ จากนั้นยิงขอดึงเฉพาะส่วนต่างข้อมูลระดับบล็อกบิตมาประมวลผลข้ามท่อเครือข่าย โดยข้ามการดาวน์โหลดไฟล์ขนาดใหญ่ทั้งไฟล์มาเก็บไว้ในเครื่อง
-- DuckDB setup script to query remote S3 parquet files directly
INSTALL httpfs;
LOAD httpfs;
-- Configure cloud storage access parameters
SET s3_region = 'ap-southeast-1';
SET s3_access_key_id = 'AWS_ACCESS_KEY';
SET s3_secret_access_key = 'AWS_SECRET_KEY';
-- Query only 2 target columns from a 5GB remote parquet file
SELECT customer_id, SUM(purchase_amount)
FROM read_parquet('s3://my-prod-datalake/gold/sales/**/*.parquet')
GROUP BY customer_id;
Use Case ในชีวิตจริง (Real-world Scenario): วิศวกรข้อมูลวิเคราะห์แนวโน้มยอดขายสินค้าผ่านไฟล์บันทึกประวัติขนาด 50GB บน Amazon S3 โดยรันสคริปต์ DuckDB คอนฟิก HTTPFS บนแล็ปท็อป ดึงข้อมูลผ่านเน็ตเวิร์กเพียง 2MB ประมวลผลและตอบคำถามคิวรีได้ทันควันในเวลาครึ่งวินาที
ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations): การรันคิวรีลักษณะนี้บ่อยครั้งอาจมีค่าใช้จ่ายแฝงจากการส่งคำขอเรียกเก็บไฟล์จาก Object Store (API Get/List request charges) แก้ไขโดยดาวน์โหลดส่วนต่างที่กรองพาร์ทิชันแล้วลงฮาร์ดดิสก์เพื่อรันวิเคราะห์แบบ Offline
4. การผสานระบบส่งข้อมูลแบบ Zero-Copy ร่วมกับ Pandas, Polars และ PyArrow
ทฤษฎีและกลไกการทำงาน (How it works): อุปสรรคหนึ่งของการแลกเปลี่ยนข้อมูลระหว่างฐานข้อมูลและหน่วยความจำของโค้ดโปรแกรมคือขั้นตอนการแปลงรูปแบบตัวแปล (Serialization/Deserialization) DuckDB แก้ปัญหานี้โดยสนับสนุนการทำงานบนมาตรฐานความปลอดภัยหน่วยความจำ **Apache Arrow** ทำให้เราสามารถแลกเปลี่ยนตารางข้อมูลขนาดใหญ่ระหว่าง DuckDB, Pandas, Polars และ PyArrow ใน RAM วงเดียวกันได้ทันทีแบบ **Zero-Copy** โดยไม่มีการก๊อปปี้บล็อกข้อมูลหรือเขียนทับในหน่วยความจำ
# Zero-Copy integration demo using DuckDB and Polars
import polars as pl
import duckdb
# 1. Create Polars DataFrame in RAM
df_polars = pl.DataFrame({"id": [1, 2, 3], "score": [95.5, 88.0, 92.4]})
# 2. DuckDB queries Polars DataFrame directly in memory without copy
result_df = duckdb.query("SELECT id, score * 1.1 AS new_score FROM df_polars").pl()
print(result_df)
Use Case ในชีวิตจริง (Real-world Scenario): การนำข้อมูลประวัติการทำรายการมาผ่านกระบวนการทำความสะอาดฟิลด์ด้วย Pandas ล้างเสร็จแล้วยิงวิเคราะห์ข้ามไปสรุป SQL ใน DuckDB จากนั้นส่งต่อขากลับให้โมเดล AI รันทำนายผล โดยขั้นตอนทั้งหมดนี้ไม่มีการเขียนข้อมูลลงไฟล์ดิสก์เลย ช่วยเพิ่มความเร็วได้สูงสุด
ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations): ความเข้ากันไม่ได้ของบางสเปคฟิลด์วันเวลา (Timestamp timezone offset) ระหว่าง Pandas และ DuckDB อาจทำให้ประเภทข้อมูลเคลื่อนแก้ไขดักจับยาก แก้ไขโดยการระบุการจัดรูปแบบประเภทให้อยู่ในมาตรฐานกลาง UTC (Timezone-naive) ก่อนประมวลผล
5. สถาปัตยกรรมแบบฝังตัว (Embedded DB) และกลไก Out-of-Core Processing
ทฤษฎีและกลไกการทำงาน (How it works): DuckDB เป็นฐานข้อมูลแบบ **Embedded** ซึ่งรันอยู่ในโปรเซสหน่วยความจำเดียวกันกับแอปพลิเคชันหลัก เมื่อต้องประมวลผลข้อมูลที่มีขนาดใหญ่กว่า RAM (Larger-than-RAM) DuckDB จะใช้กลไก **Out-of-Core Processing** โดยโหลดข้อมูลเข้ามาทำใน RAM ทีละบล็อก (Chunking) และหาก RAM ใกล้เต็ม จะทำการพักข้อมูล (Spill) ลงบนฮาร์ดดิสก์อัตโนมัติ ทำให้รันคำนวณผ่านไฟล์ใหญ่ได้โดยไม่เกิด Out-of-Memory (OOM)
-- Configuration variables to cap resource footprint and enable out-of-core spilling
SET memory_limit = '4GB'; -- Cap RAM usage to 4 gigabytes
SET threads = 4; -- Limit execution threads to 4 cores
SET temp_directory = '/tmp/duckdb_temp/'; -- Spill excess data to specific disk directory
Use Case ในชีวิตจริง (Real-world Scenario): การรันสคริปต์ทำ JOIN ตารางข้อมูลขนาด 50GB บนแล็ปท็อปที่มีแรมเพียง 16GB กลไก Out-of-Core จะสลับการอ่านเขียนลงดิสก์ NVMe ทำให้งานคำนวณเสร็จสิ้นได้โดยเครื่องไม่แฮงก์
ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations): การพักข้อมูล (Spill to disk) บนฮาร์ดดิสก์แบบเก่า (HDD) จะทำให้ระบบทำงานช้าลงมหาศาล แก้ไขโดยรันงานบนดิสก์ SSD/NVMe และขยาย memory_limit ให้มากที่สุดเท่าที่จะเป็นไปได้
6. Polars Lazy Frame, Streaming Engine และกลไกบีบอัด Parquet
ทฤษฎีและกลไกการทำงาน (How it works): เครื่องมือทางเลือกอย่าง Polars ใช้ Lazy Frame เพื่อจัดเตรียม Query Plan ก่อนเริ่มประมวลผลจริง ทำให้สามารถตัดคอลัมน์และกรองแถวล่วงหน้า (Filter Pushdown) อีกทั้งยังมี Streaming Engine ที่ประมวลผลเป็นชุดๆ (Batches) ช่วยให้จัดการไฟล์ใหญ่กว่า RAM ได้ เมื่อทำงานร่วมกับ Parquet ที่มีการบีบอัดแบบ Dictionary Encoding (แทนที่คำซ้ำด้วยตัวเลข) และ ZSTD ยิ่งทำให้การอ่านข้อมูลมหาศาลผ่าน I/O ทำได้รวดเร็วระดับวินาที
# Python Polars: Lazy execution with streaming and Parquet ZSTD
import polars as pl
# scan_parquet creates a LazyFrame (no data loaded yet)
q = (
pl.scan_parquet("large_sales_data.parquet")
.filter(pl.col("region") == "APAC")
.group_by("product_category")
.agg(pl.col("revenue").sum())
)
# collect(streaming=True) executes processing in chunks to save RAM
result = q.collect(streaming=True)
Use Case ในชีวิตจริง (Real-world Scenario): วิศวกรข้อมูลต้องการหาผลรวมรายได้จากไฟล์ Parquet ขนาด 100GB ระบบ Polars ร่วมกับ Parquet จะอ่านเฉพาะ Dictionary Block ของคอลัมน์ที่ใช้งาน และสตรีมมิ่งคำนวณโดยใช้แรมเพียง 2GB
ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations): คำสั่งบางประเภทใน Polars (เช่น การเรียงลำดับ หรือ Group By ที่คีย์เยอะมากๆ) อาจยังไม่รองรับ Streaming 100% และทำให้เกิด Out-of-Memory ได้ แก้ไขโดยพิจารณาทำพาร์ทิชันไฟล์ล่วงหน้า หรือใช้ DuckDB ช่วยประมวลผลแทนในท่าที่ซับซ้อน
Weekend Sandbox Challenge: Remote query with DuckDB
โจทย์ปฏิบัติการ: จงเขียนสคริปต์ DuckDB เพื่อวิเคราะห์และดึงสรุปไฟล์ข้อมูลดิบ Parquet ขนาดใหญ่จากเซิร์ฟเวอร์จำลองระยะไกล (HTTP) โดยกรองฟิลด์วิเคราะห์เฉพาะเจาะจง
# local_duckdb_analysis.py
import duckdb
# Connect to in-memory DuckDB database
con = duckdb.connect(database=':memory:')
# Query directly from a remote public parquet file
con.execute("INSTALL httpfs; LOAD httpfs;")
query = """
SELECT
passenger_count,
AVG(trip_distance) AS avg_distance
FROM read_parquet('https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-01.parquet')
GROUP BY passenger_count
LIMIT 5;
"""
print(con.execute(query).fetchall())
Senior Technical Interview Q&A
Q1: เหตุใดโครงสร้างข้อมูลเชิงคอลัมน์ (Columnar Layout) จึงได้รับความเร็วสูงกว่าระบบเชิงแถวในการวิเคราะห์ข้อมูล (OLAP)?
A1: เนื่องจากงานวิเคราะห์ OLAP สนใจเฉพาะตัวเลขผลรวมของคอลัมน์เฉพาะเจาะจง (เช่น หายอดรวมรายได้) การเก็บแบบคอลัมน์ช่วยให้เอนจิ้นคิวรี่กวาดสายตาเฉพาะฟิลด์คอลัมน์นั้นบนแผ่นดิสก์ได้ทันที โดยไม่ต้องโหลดรายละเอียดคอลัมน์ที่ไม่จำเป็นอื่นขึ้นมาเป็นภาระในแรม
Q2: การประมวลผลแบบ Vectorized ใน DuckDB ช่วยให้ระบบคำนวณเอาชนะ PostgreSQL สำหรับงาน OLAP ได้อย่างไร?
A2: PostgreSQL ใช้วิธีวนลูปประมวลผลข้อมูลทีละแถว (Volcano model) ซึ่งเกิด Instruction Loop Overhead สูงมาก ในขณะที่ DuckDB จัดกลุ่มเก็บข้อมูลแยกเป็นอาร์เรย์ (Vectors ขนาด 1024 ค่า) และส่งไปคำนวณใน L1/L2 Cache ของ CPU พร้อมกันร่วมกับคำสั่งระดับฮาร์ดแวร์ SIMD ทำให้ประมวลผลเร็วกว่าหลายเท่าตัว
ข้อดี / จุดเด่น & ข้อเสีย / ข้อควรระวัง
ข้อดี / จุดเด่น
- คิวรีประมวลผลไฟล์ Parquet ในเครื่องได้เร็วอย่างมหาศาลโดยไม่มีค่าใช้จ่าย
- ไม่ต้องตั้งค่าเซิร์ฟเวอร์หรือโครงสร้างพื้นฐานใดๆ เพียงแค่รันภายในตัวสคริปต์
ข้อเสีย / ข้อควรระวัง
- ความสามารถในการจัดเก็บและแรมจำกัดอยู่ที่ทรัพยากรของเครื่องเครื่องเดียว
- ไม่รองรับการทำงานคิวรีพร้อมกันปริมาณมากๆ จากผู้ใช้งานที่แตกต่างกันได้ดี
ปฏิบัติการจริง (Lab Practice)
แล็บ: ติดตั้งและเขียนคำสั่ง SQL วิเคราะห์ไฟล์ระยะไกลผ่าน DuckDB
วิธีรันแล็บปฏิบัติการบนเครื่องจริง (Local Terminal Execution Blueprint)
เนื่องจากปฏิบัติการ Data Engineering / DevOps ระดับสูงต้องรันบน Environment จริง ขั้นตอนด้านล่างนี้คือคำสั่งสำหรับนำไปรันบน Terminal / Docker ในเครื่องของคุณ:
mkdir -p pipecraft-lab && cd pipecraft-lab python3 -m venv venv && source venv/bin/activate pip install --upgrade pip pandas polars pytest requests
# Run execution pipeline test
python3 -c "
import polars as pl
print(' [PipeCraft Lab] Running local pipeline engine...')
df = pl.DataFrame({'id': [1, 2, 3], 'status': ['SUCCESS', 'SUCCESS', 'AUDITED']})
print(df)
"
# Verify clean execution exit status echo " PipeCraft Local Lab Execution Completed Successfully!"
💡 คำแนะนำ & ทริกเด็ด
คุณสามารถแปลงไฟล์ CSV ขนาดใหญ่เป็น Parquet ได้ในพริบตาผ่าน DuckDB ด้วยคำสั่ง: COPY (SELECT * FROM 'data.csv') TO 'data.parquet' (FORMAT 'parquet')