OLTP در برابر OLAP: داستانی از دو دنیای متفاوت داده

مقدمه: یک تجربه واقعی وقتی در Dr. Link روی سیستم کلینیک‌های روان‌شناسی کار می‌کردم، یک الگوی جالب دیدم که بعداً در PaymentHub هم دقیقاً همون رو تجربه کردم.…

مقدمه: یک تجربه واقعی

وقتی در Dr. Link روی سیستم کلینیک‌های روان‌شناسی کار می‌کردم، یک الگوی جالب دیدم که بعداً در PaymentHub هم دقیقاً همون رو تجربه کردم. این الگو ریشه در دو نوع کاملاً متفاوت از نیاز به داده داره — چیزی که کتاب Software Architecture: The Hard Parts بهش می‌پردازه وقتی صحبت از تجزیه دیتا (Data Decomposition) و مرزهای معماری میشه.

بذار با یک سوال ساده شروع کنیم: چرا یک سیستم واحد نمی‌تونه هم به خوبی تراکنش ثبت کنه، هم گزارش تحلیلی بده؟


بخش اول: OLTP — دنیای «الان»

OLTP یعنی Online Transaction Processing. تصور کن در Dr. Link یک بیمار وقت رزرو می‌کنه:

1. چک موجودی ظرفیت دکتر در اون ساعت
2. ثبت رزرو در جدول Appointments
3. کم کردن یک اسلات از ظرفیت دکتر
4. ارسال نوتیفیکیشن

یا در PaymentHub، لحظه‌ای که یک تراکنش پرداخت اتفاق می‌افته:

1. چک موجودی کیف‌پول
2. ثبت تراکنش (Pending)
3. صدا زدن PSP
4. آپدیت وضعیت (Success/Failed)
5. ثبت Outbox Event برای اطلاع‌رسانی به سرویس‌های دیگه

ویژگی‌های کلیدی این دنیا:

ویژگی توضیح
حجم هر عملیات کوچک — یک رکورد، یک کاربر
تعداد عملیات بسیار زیاد در ثانیه
اولویت Consistency و Latency پایین
مدل داده Normalized (برای جلوگیری از تناقض)
الگوهای رایج ACID Transaction، Outbox Pattern، Saga

اینجا دقیقاً جایی‌ست که تخصص تو در Outbox Pattern و Saga معنا پیدا می‌کنه — چون یک تراکنش پرداخت ممکنه چند سرویس رو درگیر کنه (کیف پول، PSP، حسابداری) و باید همه یا هیچ‌کدوم (all-or-nothing) اجرا بشن.


بخش دوم: OLAP — دنیای «چرا و چقدر»

حالا یک سوال کاملاً متفاوت رو تصور کن. مدیر Dr. Link می‌پرسه:

«در سه ماه گذشته، کدوم دکترها بیشترین نرخ لغو جلسه رو داشتن؟ آیا الگوی خاصی بین ساعت جلسه و نرخ عدم‌حضور هست؟»

یا در PaymentHub:

«نرخ موفقیت تراکنش‌ها بر اساس هر PSP و هر بانک مقصد در بازه ۶ ماهه چطور بوده؟ کدوم PSP در ساعات پیک کندتر جواب می‌ده؟»

این سوالات هیچ‌ربطی به «یک رکورد» ندارن. اینجا OLAP (Online Analytical Processing) وارد می‌شه:

ویژگی توضیح
حجم هر عملیات میلیون‌ها ردیف در یک کوئری
تعداد عملیات کم — چند کوئری سنگین در روز/ساعت
اولویت Throughput تحلیلی، نه Latency لحظه‌ای
مدل داده Denormalized / Columnar (برای اسکن سریع)
ابزارهای رایج ClickHouse، Data Warehouse، Star Schema

بخش سوم: چرا این دو نباید در یک جا زندگی کنن؟

اینجا دقیقاً همون درسی‌ست که Hard Parts روی تجزیه دیتا تاکید می‌کنه. اگه بخوای کوئری تحلیلی رو مستقیم روی همون دیتابیس OLTP بزنی:

-- این کوئری روی دیتابیس زنده PaymentHub اجرا میشه
SELECT psp_name, bank_code, 
       COUNT(*) as total, 
       SUM(CASE WHEN status='Success' THEN 1 ELSE 0 END) as success
FROM Transactions
WHERE created_at > '2026-01-01'
GROUP BY psp_name, bank_code

این کوئری روی جدولی با میلیون‌ها ردیف، table lock یا row lock سنگین ایجاد می‌کنه. نتیجه؟ تراکنش‌های زنده‌ی کاربرا (که همین الان دارن پرداخت می‌کنن) کند یا حتی timeout می‌شن. یعنی یک گزارش‌گیری داخلی، تجربه‌ی کاربر نهایی رو خراب می‌کنه.

راه‌حل: جداسازی مسیر Write از Read/Analytics

        [Payment Service - OLTP]
         SQL Server / Postgres
         (Normalized Schema)
                  │
                  │ Outbox Pattern
                  ▼
         [Message Broker]
          Kafka / RabbitMQ
                  │
                  ▼
        [Stream Processor / ETL]
                  │
                  ▼
       [Analytics Store - OLAP]
        ClickHouse / Warehouse
        (Denormalized, Columnar)
                  │
                  ▼
         [BI Dashboard / Reports]

این معماری همون CQRS رو در سطح دیتا پیاده می‌کنه — چیزی که احتمالاً در سطح اپلیکیشن باهاش کار کردی، ولی حالا در سطح Storage:

// Write Model - OLTP side
public class PaymentTransaction
{
    public Guid Id { get; set; }
    public decimal Amount { get; set; }
    public string PspName { get; set; }
    public TransactionStatus Status { get; set; }
    
    // بعد از commit موفق:
    // یک Domain Event در Outbox ثبت می‌شه
}

public class TransactionCompletedEvent
{
    public Guid TransactionId { get; set; }
    public string PspName { get; set; }
    public string BankCode { get; set; }
    public decimal Amount { get; set; }
    public DateTime CompletedAt { get; set; }
}
// این Event، async توسط یک Consumer خونده می‌شه
// و به شکل denormalized در ClickHouse نوشته می‌شه:
// جدولی flat که هر ردیفش شامل تمام context لازم برای تحلیل هست
// بدون نیاز به JOIN زدن بین چند جدول

در Dr. Link، جدول اصلی جلسات (Sessions) برای ثبت سریع رزرو بهینه بود — normalized، با foreign key به Doctor و Patient. ولی وقتی نیاز به گزارش «نرخ retention بیماران بر اساس دکتر» پیدا شد، مجبور شدیم یک Read Model جدا بسازیم که هر بار با یک Job شبانه (یا با event) sync می‌شد. این باعث شد:

  • کوئری‌های سنگین گزارش‌گیری هیچ اثری روی سرعت رزرو نداشته باشن
  • Schema گزارش‌گیری بتونه کاملاً مستقل از Schema عملیاتی تکامل پیدا کنه

جمع‌بندی

OLTP OLAP
سوال «الان چه اتفاقی افتاد؟» «در طول زمان چه الگویی بوده؟»
مثال PaymentHub ثبت یک تراکنش گزارش نرخ موفقیت PSP در ۶ ماه
مثال Dr. Link رزرو یک جلسه نرخ لغو جلسات بر اساس دکتر
الگوی معماری Outbox، Saga، ACID ETL، Star Schema، Columnar Store

نکته‌ی کلیدی که Hard Parts بهش تاکید می‌کنه اینه که این جداسازی، یک تصمیم معماری آگاهانه در مرز بین Bounded Context‌هاست، نه صرفاً یک بهینه‌سازی فنی — و باید از همون ابتدای طراحی PaymentHub بهش فکر کرد، نه بعد از اینکه دیتابیس زیر بار گزارش‌گیری از پا در اومد.

1790329786404

نوشته‌شده توسط محمد نظری من اینجا هستم تا دانشم رو با شما به اشتراک بذارم.

دیدگاه‌ها 0

برای ارسال دیدگاه یا پسند، وارد شوید.

ورود / ثبت‌نام

هنوز دیدگاهی نیست — اولین نفر باشید.