DATA ARCHITECTURE · POSTGRESQL

طراحی پایگاه‌داده پروژه (Database Schema)

این سند اسکیمای جدول‌های PostgreSQL پروژه را مطابق وضعیت فعلی کد مستند می‌کند: مدل Django در Application Registry و فایل‌های SQL زیرسیستم Storage Consumer (storage_consumer/persistence/sql/). ستون reviews.sentiment توسط زیرسیستم Sentiment نوشته می‌شود.


نمای کلی روابط

erDiagram APPS_REGISTRY_APPLICATION ||--o{ APP_STATS : "has many" APPS_REGISTRY_APPLICATION ||--o{ REVIEWS : "has many" APPS_REGISTRY_APPLICATION ||--o{ NETWORK_METRICS : "has many" APPS_REGISTRY_APPLICATION { bigint id PK string package_name UK boolean is_messaging_app boolean is_iranian_app boolean is_active } APP_STATS { bigint id PK bigint application_id FK timestamptz crawled_at } REVIEWS { bigint id PK bigint application_id FK string review_id UK string sentiment } NETWORK_METRICS { bigint id PK bigint application_id FK uuid analysis_id UK } DEAD_LETTER_EVENTS { bigint id PK string topic int partition bigint kafka_offset }

۱. جدول Applications

نام واقعی در Postgres: apps_registry_application (مدل Django Application در اپ apps_registry). مالک DDL: app_api از طریق مهاجرت‌های Django.

جدول مرکزی سیستم است که فهرست اپلیکیشن‌های تحت پایش را نگه می‌دارد. فقط وضعیت فعلی لیست را نشان می‌دهد و دادهٔ آماری/تاریخی ندارد؛ آن‌ها در app_stats، reviews و network_metrics می‌مانند تا هر کراول تاریخچه بسازد بدون دستکاری لیست.

دیاگرام (ER)

erDiagram APPS_REGISTRY_APPLICATION { bigint id PK string package_name UK "unique app identifier" string display_name "optional" string category "Play Store category" boolean is_messaging_app "flags network analysis need" boolean is_iranian_app "selects crawl region/locale" boolean is_active "soft delete flag" datetime created_at datetime updated_at }

مشخصات فیلدها

فیلدنوع دادهویژگی‌هاتوضیح و دلیل طراحی
idBigAutoField / BIGINTPK, Auto Incrementکلید داخلی برای FKهای جدول‌های Storage Consumer. Django پیش‌فرض BigAutoField است؛ به همین دلیل FKها در SQL از BIGINT استفاده می‌کنند.
package_nameCharField(255)Unique, Not Null, Indexedشناسهٔ یکتای اپ در Google Play. کلید ارجاع بین‌سیستمی (Crawler، Network Analyzer، Kafka key) — نه id داخلی.
display_nameCharField(255)Nullableنام نمایشی. Create می‌تواند فقط با package_name باشد. شکاف شناخته‌شده: پر شدن خودکار از Play Store توسط کراولر هنوز پیاده نشده؛ فعلاً همان مقدار ورودی API می‌ماند.
categoryCharField(100)Nullableدستهٔ رسمی Play Store برای گزارش‌گیری. همان شکاف display_name برای تکمیل خودکار صدق می‌کند.
is_messaging_appBooleanDefault: Falseفلگ عملیاتی جدا از category: آیا اپ نیاز به جمع‌آوری pcap دارد؟ منطق شبکه با یک Boolean ساده کار می‌کند.
is_iranian_appBooleanDefault: Falseبازار هدف ایران. کراولر بر اساس این فلگ Region/Locale را انتخاب می‌کند (ir/fa در برابر us/en) چون نتایج Play Store (به‌خصوص ریویوها) به کشور/زبان درخواست وابسته‌اند.
is_activeBooleanDefault: TrueSoft Delete. کراولر فقط is_active=True را می‌خواند؛ تاریخچهٔ اپ غیرفعال حفظ می‌شود.
created_atDateTimeauto_now_addزمان ثبت اولیه.
updated_atDateTimeauto_nowزمان آخرین ویرایش رکورد لیست (نه آمار).

تصمیمات طراحی

  • جداسازی وضعیت از تاریخچه: هیچ فیلد آماری در این جدول نیست تا Update لیست، روند زمانی را پاک نکند.
  • Soft Delete: عملیات API «غیرفعال کردن» است، نه حذف فیزیکی.
  • package_name به‌عنوان کلید بین‌سیستمی: پایدار و معنادار برای producer/consumerهای مستقل.
  • is_iranian_app: بدون این فلگ، کراول پیش‌فرض US عمدتاً نقدهای غیرایرانی برای اپ‌های ایرانی برمی‌گرداند.

۲. جدول App Stats

مالک: Storage Consumer — فایل 0001_app_stats.sql. ماهیت Append-Only: هر چرخهٔ کراول یک ردیف جدید (نه Update).

دیاگرام (ER)

erDiagram APPS_REGISTRY_APPLICATION ||--o{ APP_STATS : "has many" APPS_REGISTRY_APPLICATION { bigint id PK string package_name UK } APP_STATS { bigint id PK bigint application_id FK bigint min_installs float score bigint ratings bigint reviews_count string version boolean ad_supported timestamptz app_updated_at "last update on Play Store" timestamptz crawled_at "our crawl timestamp" }

مشخصات فیلدها

فیلدنوع دادهویژگی‌هاتوضیح و دلیل طراحی
idBIGSERIALPKکلید داخلی؛ جدول با نرخ (تعداد اپ × هر ساعت) رشد می‌کند.
application_idBIGINT FKNot Null, ON DELETE CASCADEارجاع به apps_registry_application(id). آمار بدون اپ مادر بی‌معنی است.
min_installsBIGINTNullableحداقل نصب اعلام‌شده. Nullable چون Play Store گاهی مقدار ندارد؛ نبود یک سنجه نباید کل رکورد را دور بیندازد.
scoreDOUBLE PRECISIONNullableامتیاز کلی (مثلاً 4.3).
ratingsBIGINTNullableتعداد کل رتبه‌بندی‌ها.
reviews_countBIGINTNullableتعداد کل نقدهای گزارش‌شده توسط Play Store (متفاوت از تعداد ردیف‌های جدول reviews ما).
versionVARCHAR(50)Nullableنسخهٔ فعلی اپ.
ad_supportedBOOLEANNullableپشتیبانی از تبلیغات.
app_updated_atTIMESTAMPTZNullableآخرین به‌روزرسانی خودِ اپ در Play Store — جدا از زمان کراول ما.
crawled_atTIMESTAMPTZNot Nullزمان کراول ما. همراه با application_id کلید idempotency است.

ایندکس‌ها و قیدها

  • UNIQUE (application_id, crawled_at) — کلید idempotency در برابر at-least-once بودن Kafka و retryهای producer. همان btree عملاً ایندکس مرکب گزارش‌گیری per-app را هم پوشش می‌دهد.
  • INDEX (crawled_at) — برای برش‌های زمانی cross-app در Metabase که ایندکس مرکب با leading column روی application_id سرویس نمی‌دهد.

تصمیمات طراحی

  • Append-Only: پیش‌نیاز تحلیل روند نصب/امتیاز در زمان.
  • جداسازی app_updated_at از crawled_at: «زمان تغییر اپ» در برابر «زمان مشاهدهٔ ما».
  • CASCADE: در عمل Soft Delete در سطح اپ است؛ CASCADE برای یکپارچگی در پاک‌سازی دستی/توسعه است.

۳. جدول Reviews

مالک ساختار: Storage Consumer — 0002_reviews.sql. نویسندگان: Storage Consumer برای همهٔ ستون‌ها به‌جز sentiment؛ زیرسیستم Sentiment فقط همان ستون را UPDATE می‌کند. ماهیت Upsert روی review_id.

دیاگرام (ER)

erDiagram APPS_REGISTRY_APPLICATION ||--o{ REVIEWS : "has many" APPS_REGISTRY_APPLICATION { bigint id PK string package_name UK } REVIEWS { bigint id PK bigint application_id FK string review_id UK string user_name int thumbs_up_count smallint score text content timestamptz at string sentiment "POSITIVE NEUTRAL NEGATIVE" timestamptz first_seen_at timestamptz last_synced_at }

مشخصات فیلدها

فیلدنوع دادهویژگی‌هاتوضیح و دلیل طراحی
idBIGSERIALPKکلید داخلی جدا از review_id معنادار.
application_idBIGINT FKNot Null, CASCADEارجاع به اپ.
review_idVARCHAR(255)UNIQUE, Not Nullشناسهٔ یکتای نقد در Play Store؛ ستون کلیدی Upsert.
user_nameVARCHAR(255)Nullableگاهی در Play Store مخفی است.
thumbs_up_countINTEGERDefault 0در طول زمان عوض می‌شود؛ دلیل اصلی نیاز به Upsert.
scoreSMALLINTNullableامتیاز ۱–۵. Nullable چون scraper گاهی score ندارد و نبود سنجه نباید متن نقد را دور بیندازد.
contentTEXTNullableمتن نقد؛ طول نامحدود.
atTIMESTAMPTZNot Nullزمان ثبت نقد در Play Store؛ مبنای روند زمانی در Metabase.
sentimentVARCHAR(20)Nullable, CHECK ∈ {POSITIVE, NEUTRAL, NEGATIVE}نتیجهٔ Job تحلیل احساس. Storage Consumer در upsert هرگز این ستون را لمس نمی‌کند تا re-sync کراولر تحلیل را پاک نکند. نوشته می‌شود توسط Sentiment.
first_seen_atTIMESTAMPTZNot Null, DEFAULT now()اولین دیده‌شدن؛ در DO UPDATE لیست نیست و پایدار می‌ماند.
last_synced_atTIMESTAMPTZNot Null، از crawled_at پیامزمان همگام‌سازی از Play Store (مهر پیام)، نه auto_now زمان نوشتن ردیف. پیش‌نیاز نگهبان یکنوا.

ایندکس‌ها

  • UNIQUE (review_id)
  • INDEX (application_id, at) — کوئری «روند امتیاز یک اپ»
  • INDEX (at) — برش زمانی cross-app

تصمیمات طراحی

  • Upsert نه Append-Only: هدف آخرین نسخهٔ هر نقد است، نه تاریخچهٔ ویرایش‌ها.
  • نگهبان یکنوا: ON CONFLICT (review_id) DO UPDATE … WHERE EXCLUDED.last_synced_at > reviews.last_synced_at — replay کهنه و ترتیب به‌هم‌خورده بین پارتیشن‌ها نمی‌توانند دادهٔ تازه‌تر را بازنویسی کنند.
  • sentiment زودهنگام در DDL: ستون از مهاجرت reviews ساخته شد تا بعداً ALTER روی جدول بزرگ لازم نباشد؛ مالک نوشتن جداست.

۴. جدول Network Metrics

مالک: Storage Consumer — 0003_network_metrics.sql. Append-Only با کلید idempotency روی analysis_id. ورودی از تحلیل دستی/آفلاین pcap (معمولاً اپ‌های messaging).

دیاگرام (ER)

erDiagram APPS_REGISTRY_APPLICATION ||--o{ NETWORK_METRICS : "has many" APPS_REGISTRY_APPLICATION { bigint id PK string package_name UK } NETWORK_METRICS { bigint id PK bigint application_id FK uuid analysis_id UK string scenario "UPLOAD or DOWNLOAD" float rtt_handshake int retransmission_count int out_of_order_count int spurious_retransmission_count int zero_window_count int tcp_reset_count bigint bytes_transferred_total bigint bytes_payload_total float overhead_ratio string source_pcap_filename timestamptz analyzed_at }

مشخصات فیلدها

فیلدنوع دادهویژگی‌هاتوضیح و دلیل طراحی
idBIGSERIALPKکلید داخلی.
application_idBIGINT FKNot Null, CASCADEمعمولاً اپ‌های is_messaging_app=True.
analysis_idUUIDUNIQUE, Not Nullیک UUID به‌ازای هر فایل تحلیل‌شده (تولید Network Analyzer). کلید idempotency: ON CONFLICT DO NOTHING برای redelivery؛ اجرای واقعی مجدد تست UUID تازه می‌سازد و ردیف جدید می‌دهد.
scenarioVARCHAR(20)Not Null, CHECK ∈ {UPLOAD, DOWNLOAD}سناریوی ضبط.
rtt_handshakeDOUBLE PRECISIONNullableمیانگین زمان SYN→SYN-ACK (ms). ممکن است handshake تازه در pcap نباشد.
retransmission_countINTEGERDefault 0بازفرستادن واقعی داده (بدون spurious).
out_of_order_countINTEGERDefault 0سگمنت‌های out-of-order که حفرهٔ sequence ساخته‌اند.
spurious_retransmission_countINTEGERDefault 0بازفرستادن وقتی peer قبلاً ACK کرده؛ جدا از retransmission_count.
zero_window_countINTEGERDefault 0رخدادهای TCP Zero Window.
tcp_reset_countINTEGERDefault 0بسته‌های RST.
bytes_transferred_totalBIGINTNot Nullحجم کل بسته‌ها.
bytes_payload_totalBIGINTNot Nullحجم خالص payload بدون هدر.
overhead_ratioDOUBLE PRECISIONNot Nullنسبت سربار هدر؛ ذخیرهٔ جدا برای سادگی Metabase.
source_pcap_filenameVARCHAR(255)NullableTraceability به فایل خام.
analyzed_atTIMESTAMPTZNot Null، از پیام Analyzerزمان اجرای تحلیل — نه زمان خواندن پیام توسط consumer و نه auto_now_add.

ایندکس‌ها

  • UNIQUE (analysis_id)
  • INDEX (application_id, analyzed_at) — FK ایندکس خودکار در Postgres ندارد؛ این ایندکس صریح هم FK را پوشش می‌دهد هم کوئری per-app زمانی را.

تصمیمات طراحی

  • تکرار تست ≠ redelivery: بدون Unique روی analysis_id، تحویل مجدد Kafka ردیف تکراری می‌ساخت. با UUID، تست واقعیِ مجدد مجاز می‌ماند.
  • بدون محدودیت روی (application_id, scenario): تکرار آزمایش در شرایط مختلف باید مجاز باشد.

۵. جدول Dead Letter Events

مالک: Storage Consumer — 0004_dead_letter_events.sql. در طرح اولیه نبود؛ همراه با consumer اضافه شد.

پیام‌هایی را نگه می‌دارد که consumer نتوانسته و هرگز نخواهد توانست ذخیره کند (JSON نامعتبر، فیلد هویتی غایب، نوع نادرست، timestamp بد، scenario ناشناخته، package_name ثبت‌نشده، …). بدون DLQ، retry بی‌پایان یک پیام معیوب پارتیشن را قفل می‌کند؛ دور انداختن خاموش هم داده را بی‌صدا از دست می‌دهد. راه سوم: ثبت اینجا، جلو رفتن offset، بررسی بعدی توسط اپراتور.

فیلدنوع دادهویژگی‌هاتوضیح
idBIGSERIALPKکلید داخلی.
topicVARCHAR(255)Not Null، عضو Uniqueتاپیک مبدأ.
partitionINTEGERNot Null، عضو Unique (ستون quoted در SQL)شمارهٔ پارتیشن.
kafka_offsetBIGINTNot Null، عضو Uniqueپیشوند kafka_ چون offset در Postgres رزرو است.
kafka_keyTEXTNullableمعمولاً package_name.
raw_payloadBYTEANullableبدنهٔ خام؛ BYTEA چون ممکن است UTF-8 معتبر نباشد.
error_reasonTEXTNot Nullدلیل خوانا برای اپراتور.
occurred_atTIMESTAMPTZDefault now(), INDEX DESCکوئری عملیاتی: «اخیراً چه رد شده؟»

تصمیمات طراحی

  • در Postgres نه Kafka DLQ: همان تراکنش بچ که offset را جلو می‌برد؛ و قابل کوئری از Metabase/psql.
  • UNIQUE (topic, partition, kafka_offset): هویت رکورد Kafka؛ reset offset ردیف تکراری نمی‌سازد.
  • بدون FK به applications: شایع‌ترین علت DLQ خودِ ناشناخته بودن package است.
  • خطای موقت ≠ دائمی: قطع DB / deadlock هرگز اینجا نمی‌روند؛ با backoff retry می‌شوند.

۶. مالکیت اسکیما و مهاجرت‌ها

  • app_api (Django): تنها مالک apps_registry_application.
  • storage_consumer: مالک app_stats، reviews، network_metrics، dead_letter_events از طریق SQL شماره‌دار در persistence/sql/؛ وضعیت اعمال در جدول storage_consumer_migrations. جدول اپ را فقط می‌خواند (resolve کردن package_name → id).
  • sentiment: مدل/مهاجرت جدا برای reviews ندارد؛ فقط UPDATE ستون sentiment را انجام می‌دهد.

هرگز نباید برای چهار جدول Storage Consumer مدل Django managed ساخته شود — دو ابزار روی یک DDL می‌جنگند. اگر روزی از API خوانده شدند، راه درست managed = False است.

شکاف‌های شناخته‌شده

  • display_name / category: تکمیل خودکار از کراول هنوز نیست؛ payload تاپیک app-stats این فیلدها را حمل نمی‌کند.
  • sentiment: تا وقتی Job Sentiment اجرا نشود NULL می‌ماند؛ upsert کراولر آن را پاک نمی‌کند.

۷. منابع حقیقت در مخزن

منبعپوشش
app_api/apps_registry/models.py + migrationsجدول اپ
storage_consumer/persistence/sql/0001_*.sql … 0004_*.sqlچهار جدول دادهٔ ماندگار + DLQ
storage_consumer/persistence/migrator.pyاعمال SQL و جدول storage_consumer_migrations
مستند Storage Consumerتضمین‌های نوشتن، idempotency، DLQ
مستند Network Analyzerتولید پیام network-metrics و تعریف متریک‌ها
مستند Sentimentنوشتن و overwrite ستون sentiment