ในยุคที่ข้อมูลคือทรัพยากรที่มีค่าที่สุดขององค์กร ระบบ Tardis ซึ่งเป็นแพลตฟอร์มบันทึกประวัติการทำงาน (History Tracking System) กลายเป็นหัวใจสำคัญของหลายธุรกิจ ไม่ว่าจะเป็นระบบ E-commerce ที่ต้องเก็บประวัติการสั่งซื้อและพฤติกรรมลูกค้า ระบบ RAG องค์กรที่ต้องจัดการ Embeddings จำนวนมหาศาล หรือโปรเจกต์ของนักพัฒนาอิสระที่ต้องการระบบ Analytics ในราคาที่เข้าถึงได้

บทความนี้จะพาคุณเจาะลึกการเปรียบเทียบ 3 วิธีการจัดเก็บข้อมูลประวัติที่ได้รับความนิยมสูงสุดในปี 2024-2025 พร้อมตารางเปรียบเทียบที่ครอบคลุม ตัวอย่างโค้ดที่พร้อมใช้งานจริง และบทวิเคราะห์จากประสบการณ์ตรงของทีมพัฒนาระบบ E-commerce ขนาดใหญ่ที่ต้องจัดการข้อมูลมากกว่า 10 ล้าน Event ต่อวัน

Tardis History Data คืออะไร และทำไมต้องสนใจ

ระบบ Tardis (Time-series Application with Rich Data Including Statistics) เป็นรูปแบบการออกแบบระบบที่เน้นการจัดเก็บข้อมูลเหตุการณ์ (Event) ตามลำดับเวลา ตัวอย่างการใช้งานจริงที่พบบ่อย ได้แก่

ความท้าทายหลักคือ ข้อมูลเหล่านี้มีปริมาณเพิ่มขึ้นอย่างมหาศาล ระบบหนึ่งอาจต้องจัดการข้อมูลหลายร้อย GB ต่อวัน และต้องสามารถ Query ได้อย่างรวดเร็ว (sub-second) เพื่อรองรับการใช้งาน Real-time

เปรียบเทียบ 3 วิธีการจัดเก็บข้อมูล Tardis History

1. Apache Parquet — รูปแบบไฟล์คอลัมน์ที่เหมาะกับ Data Lake

Apache Parquet เป็นรูปแบบไฟล์แบบ Column-oriented ที่พัฒนาโดย Twitter และ Apache Software Foundation มีจุดเด่นเรื่องการบีบอัดที่มีประสิทธิภาพสูงและการรองรับ Schema Evolution ทำให้เหมาะกับการจัดเก็บข้อมูลใน Data Lake หรือ Object Storage อย่าง S3, GCS

ข้อดี:

ข้อเสีย:

2. ClickHouse — OLAP Database ที่เร็วที่สุดในตลาด

ClickHouse คือ Column-oriented DBMS ที่พัฒนาโดย Yandex ซึ่งเป็นบริษัท Search Engine ยักษ์ใหญ่ของรัสเซีย ถูกออกแบบมาเพื่อ OLAP (Online Analytical Processing) โดยเฉพาะ สามารถ Query ข้อมูลหลายพันล้าน Rows ได้ในเวลาไม่กี่วินาที

ข้อดี:

ข้อเสีย:

3. DuckDB — In-Process Analytics Database

DuckDB เป็น Embedded OLAP Database ที่ทำงานใน Process เดียวกับ Application ไม่ต้องมี Server แยกต่างหาก ถูกออกแบบมาเพื่อให้เป็น SQLite ของ Analytics ที่รองรับ SQL เต็มรูปแบบ

ข้อดี:

ข้อเสีย:

ตารางเปรียบเทียบระบบจัดเก็บ Tardis History

เกณฑ์การเปรียบเทียบ Apache Parquet ClickHouse DuckDB
รูปแบบ File Format OLAP Database Embedded DB
ความเร็ว Query ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
ความเร็ว Write ⭐⭐ (Batch) ⭐⭐⭐⭐⭐ ⭐⭐⭐
ความง่ายในการติดตั้ง ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐
ค่าใช้จ่าย Infrastructure ต่ำ สูง ต่ำมาก
Concurrent Access จำกัด สูงมาก ไม่รองรับ
รองรับ Data Volume Unlimited (PB) Unlimited จำกัด (RAM)
SQL Support ผ่าน Engine เต็มรูปแบบ เต็มรูปแบบ
Real-time Capability ⚠️ จำกัด

เหมาะกับใคร / ไม่เหมาะกับใคร

Apache Parquet

✅ เหมาะกับ:

❌ ไม่เหมาะกับ:

ClickHouse

✅ เหมาะกับ:

❌ ไม่เหมาะกับ:

DuckDB

✅ เหมาะกับ:

❌ ไม่เหมาะกับ:

ตัวอย่างโค้ด: การใช้งานจริงในแต่ละวิธี

ตัวอย่างที่ 1: การบันทึก E-commerce Event ด้วย Parquet (Python)

import pyarrow as pa
import pyarrow.parquet as pq
from datetime import datetime
import os

class TardisParquetWriter:
    """ตัวอย่างการจัดเก็บ Event ด้วย Parquet - เหมาะกับ Batch Processing"""
    
    def __init__(self, bucket_path: str):
        self.bucket_path = bucket_path
        self.schema = pa.schema([
            ('event_id', pa.string()),
            ('customer_id', pa.string()),
            ('event_type', pa.string()),  # view, add_to_cart, purchase
            ('product_id', pa.string()),
            ('timestamp', pa.timestamp('us')),
            ('metadata', pa.map_(pa.string(), pa.string())),
        ])
    
    def write_daily_events(self, events: list, date_str: str):
        """
        บันทึก Event ของวันเดียวเป็นไฟล์ Parquet
        แนะนำ: สร้าง Partition ตาม date/hour เพื่อ Query เร็วขึ้น
        """
        table = pa.Table.from_pylist(events, schema=self.schema)
        
        # บีบอัดด้วย ZSTD ซึ่งให้ Compression Ratio ดีที่สุด
        writer = pq.ParquetWriter(
            f"{self.bucket_path}/date={date_str}/events.parquet",
            table.schema,
            compression='zstd',
            use_dictionary=True,
        )
        writer.write_table(table)
        writer.close()
        
        print(f"✅ บันทึก {len(events):,} events เรียบร้อย - ขนาด: {os.path.getsize(f'{self.bucket_path}/date={date_str}/events.parquet') / 1024 / 1024:.2f} MB")
    
    def query_with_pyarrow(self, date_range: tuple):
        """Query ข้อมูลด้วย PyArrow - อ่านเฉพาะ Partition ที่ต้องการ"""
        start_date, end_date = date_range
        
        # ใช้ ParquetDataset เพื่ออ่านเฉพาะ Partition ที่ต้องการ
        dataset = pq.ParquetDataset(
            self.bucket_path,
            filters=[('date', 'in', [start_date, end_date])]
        )
        table = dataset.read()
        
        # แปลงเป็น Pandas สำหรับ Analysis
        df = table.to_pandas()
        return df

การใช้งาน

writer = TardisParquetWriter('s3://my-ecommerce-tardis/') sample_events = [ {'event_id': 'evt_001', 'customer_id': 'cust_123', 'event_type': 'view', 'product_id': 'prod_456', 'timestamp': datetime.now(), 'metadata': {}}, # ... events อื่นๆ ] writer.write_daily_events(sample_events, '2025-01-15')

ตัวอย่างที่ 2: การใช้งาน ClickHouse สำหรับ Real-time Analytics

from clickhouse_driver import Client
from datetime import datetime
import json

class TardisClickHouse:
    """ตัวอย่าง ClickHouse สำหรับ Real-time E-commerce Analytics"""
    
    def __init__(self, host: str, port: int = 9000):
        self.client = Client(host=host, port=port, database='tardis')
        self._init_table()
    
    def _init_table(self):
        """สร้าง MergeTree Table พร้อม Partition ตามเดือน"""
        self.client.execute('''
            CREATE TABLE IF NOT EXISTS customer_events (
                event_id String,
                customer_id String,
                event_type Enum8('view' = 1, 'add_to_cart' = 2, 'purchase' = 3, 'refund' = 4),
                product_id String,
                price Decimal(10, 2),
                quantity UInt32,
                timestamp DateTime64(3, 'Asia/Bangkok'),
                metadata JSON
            ) ENGINE = MergeTree()
            PARTITION BY toYYYYMM(timestamp)
            ORDER BY (customer_id, timestamp)
            TTL timestamp + INTERVAL 12 MONTH
            SETTINGS index_granularity = 8192
        ''')
    
    def insert_events(self, events: list):
        """Insert แบบ Asynchronous สำหรับ Throughput สูง"""
        # แปลงเป็น Tuple format ที่ ClickHouse เข้าใจ
        data = [
            (
                e['event_id'],
                e['customer_id'],
                e['event_type'],
                e['product_id'],
                float(e.get('price', 0)),
                int(e.get('quantity', 1)),
                e['timestamp'],
                json.dumps(e.get('metadata', {}))
            )
            for e in events
        ]
        
        self.client.execute(
            'INSERT INTO customer_events VALUES',
            data,
            types_check=True
        )
    
    def get_customer_journey(self, customer_id: str, days: int = 30):
        """ดึง Customer Journey พร้อม Funnel Analysis"""
        result = self.client.query_dataframe('''
            WITH 
                -- คำนวณ Session
                arrayJoin(groupArray((timestamp, event_type, product_id))) AS events,
                date_diff('minute', lag(timestamp) OVER w, timestamp) AS session_gap
            SELECT 
                timestamp,
                event_type,
                product_id,
                price,
                quantity,
                price * quantity AS revenue
            FROM customer_events
            WHERE customer_id = %(customer_id)s
              AND timestamp > now() - INTERVAL %(days)s DAY
            WINDOW w AS (PARTITION BY customer_id ORDER BY timestamp)
            ORDER BY timestamp
        ''', params={'customer_id': customer_id, 'days': days})
        
        return result
    
    def get_daily_funnel(self, date: str):
        """Funnel Analysis รายวัน - ClickHouse ทำได้เร็วมาก"""
        result = self.client.query_dataframe(f'''
            SELECT 
                event_type,
                count() AS count,
                uniqExact(customer_id) AS unique_customers,
                sum(price * quantity) AS revenue
            FROM customer_events
            WHERE date(timestamp) = '{date}'
            GROUP BY event_type
            ORDER BY count() DESC
        ''')
        return result

การใช้งาน

tardis = TardisClickHouse('clickhouse.example.com') tardis.insert_events([{ 'event_id': 'evt_001', 'customer_id': 'cust_123', 'event_type': 'purchase', 'product_id': 'prod_456', 'price': 299.00, 'quantity': 2, 'timestamp': datetime.now(), 'metadata': {'campaign': 'summer_sale'} }]) print(tardis.get_daily_funnel('2025-01-15'))

ตัวอย่างที่ 3: DuckDB สำหรับ Developer Analytics Dashboard

import duckdb
import pandas as pd
from datetime import datetime, timedelta

class TardisDuckDB:
    """DuckDB สำหรับ Developer SaaS - Embedded Analytics"""
    
    def __init__(self, db_path: str = ':memory:'):
        """
        ใช้ :memory: สำหรับ Testing
        หรือระบุ Path เพื่อ Persist ข้อมูล
        """
        self.con = duckdb.connect(db_path)
        self._init_schema()
    
    def _init_schema(self):
        """สร้าง Schema พร้อม Compression"""
        self.con.execute('''
            CREATE SEQUENCE IF NOT EXISTS event_id START 1;
            
            CREATE TABLE IF NOT EXISTS user_events (
                event_id BIGINT PRIMARY KEY DEFAULT nextval('event_id'),
                user_id VARCHAR NOT NULL,
                event_type VARCHAR NOT NULL,
                page_path VARCHAR,
                duration_ms INTEGER,
                metadata JSON,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            );
            
            -- สร้าง Index สำหรับ Query ที่ใช้บ่อย
            CREATE INDEX IF NOT EXISTS idx_user_events_user_id 
            ON user_events(user_id);
            
            CREATE INDEX IF NOT EXISTS idx_user_events_created_at 
            ON user_events(created_at);
        ''')
        
        # กดให้ DuckDB ใช้ Memory เยอะขึ้นเพื่อ Performance
        self.con.execute("SET memory_limit='8GB'")
        self.con.execute("SET threads=4")
    
    def track_event(self, user_id: str, event_type: str, 
                    page_path: str = None, duration_ms: int = None, 
                    metadata: dict = None):
        """บันทึก Event แบบ Real-time"""
        self.con.execute('''
            INSERT INTO user_events (user_id, event_type, page_path, duration_ms, metadata)
            VALUES (?, ?, ?, ?, ?)
        ''', [user_id, event_type, page_path, duration_ms, 
              duckdb.json(metadata) if metadata else None])
    
    def get_user_cohort(self, start_date: datetime, end_date: datetime):
        """Cohort Analysis - ดูว่า User ใช้งานยังไงในแต่ละ Cohort"""
        result = self.con.execute('''
            WITH user_cohort AS (
                SELECT 
                    user_id,
                    DATE(MIN(created_at)) AS cohort_date,
                    COUNT(*) AS total_events,
                    SUM(duration_ms) AS total_duration
                FROM user_events
                WHERE created_at BETWEEN ? AND ?
                GROUP BY user_id
            )
            SELECT 
                cohort_date,
                COUNT(*) AS cohort_size,
                AVG(total_events) AS avg_events_per_user,
                AVG(total_duration) / 1000 AS avg_duration_sec
            FROM user_cohort
            GROUP BY cohort_date
            ORDER BY cohort_date
        ''', [start_date, end_date]).df()
        
        return result
    
    def get_retention_matrix(self, cohort_month: str):
        """Retention Matrix - วิเคราะห์ว่า User กลับมาใช้งานอีกไหม"""
        result = self.con.execute(f'''
            WITH first_activity AS (
                SELECT 
                    user_id,
                    DATE_TRUNC('month', MIN(created_at))::VARCHAR AS first_month
                FROM user_events
                GROUP BY user_id
                HAVING DATE_TRUNC('month', MIN(created_at))::VARCHAR = '{cohort_month}'
            ),
            activity AS (
                SELECT 
                    user_id,
                    DATE_TRUNC('month', created_at)::VARCHAR AS activity_month
                FROM user_events
            )
            SELECT 
                fa.first_month,