DDIA·مطالعهSTUDY

نقشه راهRoadmap/ فصل ۳Chapter 3

مدل‌های داده و زبان‌های کوئریData models and query languages

هر لایه بخشی از جزئیاتِ لایهٔ زیر را پنهان می‌کند. مدلِ داده زبانِ بیانِ مسئله است؛ شکلِ روی دیسک لزوماً همان شکلِ دیده‌شده در برنامه نیست. A focused distillation: relational vs document models, normalization, graph models, event sourcing and DataFrames — with interactive Archify diagrams, a teacher narration for each, and an interview question pack.

۱۱11 نمودار تعاملیinteractive diagrams· محور: relational · document · graph · eventsTheme: relational · document · graph · events· زمان مطالعه: حدود ۶۰ دقیقهReading time: ~60 min

۰0 داده‌ها لایه‌لایه‌اندData comes in layers

هر لایه بخشی از جزئیاتِ لایهٔ زیر را پنهان می‌کند. مدلِ داده زبانِ بیانِ مسئله است؛ شکلِ روی دیسک لزوماً همان شکلِ دیده‌شده در برنامه نیست.

مثال فنی: یک رزومه در چهار لایه

وضعیت اولیه: کاربر سابقهٔ «مهندس در شرکت ۷» را در فرم می‌بیند.

اجرای مرحله‌به‌مرحله: برنامه آن را شیء می‌کند؛ جدول jobs(person_id, company_id) رابطه را نگه می‌دارد؛ موتور پایگاه داده آن را در صفحه‌های دیسک می‌نویسد.

نتیجه و دلیل: هر لایه مدل خودش را دارد؛ تغییر مدل نمایشی الزاماً به تغییر همانند در چیدمان دیسک نیاز ندارد.

ایدهٔ کلیدی فصلChapter thesis

مدل‌های داده شاید مهم‌ترین بخش توسعهٔ نرم‌افزار باشند، چون فقط شکلِ کدنویسی را تعیین نمی‌کنند؛ شکلِ فکر کردن ما دربارهٔ مسئله را هم تعیین می‌کنند. هر برنامه‌ای از لایه‌بندیِ چند مدل داده ساخته شده:

  1. لایهٔ برنامه: دنیای واقعی (افراد، سازمان‌ها، پول، سنسورها) را با اشیا و APIهای خاصِ خودت مدل می‌کنی.
  2. لایهٔ ذخیره‌سازی: آن ساختارها را با یک مدل عمومی بیان می‌کنی: JSON/XML، جدول‌های رابطه‌ای، یا رأس و یالِ گراف. موضوع این فصل همین لایه است.
  3. لایهٔ موتور ذخیره‌سازی: آن مدل چطور به بایت‌های روی دیسک/حافظه/شبکه تبدیل شود (فصل ۴).
  4. لایهٔ سخت‌افزار: بایت‌ها چطور با جریان الکتریکی و نور و مغناطیس نمایش داده شوند.

هر لایه، پیچیدگیِ لایهٔ زیرین را با یک مدل تمیز پنهان می‌کند و همین، همکاری گروه‌های مختلف را ممکن می‌سازد.

Data models may be the most important part of software development, because they determine not just how we write code; they determine how we think about the problem. Every application is built from layers of data models:

  1. Application layer: you model the real world (people, organizations, money, sensors) with your own objects and APIs.
  2. Storage layer: you express those structures in a general-purpose model: JSON/XML, relational tables, or graph nodes and edges. This chapter is about this layer.
  3. Storage-engine layer: how that model turns into bytes on disk/memory/network (Chapter 4).
  4. Hardware layer: how bytes are represented with electric current, light and magnetism.

Each layer hides the complexity below behind a clean model, and that is what lets different teams collaborate.

لایه‌های داده — هر لایه زیرین را پنهان می‌کندData layers — each layer hides the one below باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: یک رزومه در چهار لایه

وضعیت اولیه: کاربر سابقهٔ «مهندس در شرکت ۷» را در فرم می‌بیند.

اجرای مرحله‌به‌مرحله: برنامه آن را شیء می‌کند؛ جدول jobs(person_id, company_id) رابطه را نگه می‌دارد؛ موتور پایگاه داده آن را در صفحه‌های دیسک می‌نویسد.

نتیجه و دلیل: هر لایه مدل خودش را دارد؛ تغییر مدل نمایشی الزاماً به تغییر همانند در چیدمان دیسک نیاز ندارد.

مدلِ داده مشخص می‌کند اطلاعات را با چه شکلی نگه داریم و چه رابطه‌هایی را آسان‌تر ببینیم. جدول، سند، گراف و رویداد، چهار نگاه متفاوت‌اند؛ انتخاب از روی سؤال‌های برنامه شروع می‌شود.

مثال فنی: انتخاب مدل با سؤال شروع می‌شود

وضعیت اولیه: داده شامل افراد، شرکت‌ها و سابقهٔ کار است.

اجرای مرحله‌به‌مرحله: نمایش کل رزومه یک خواندنِ تو‌در‌تو می‌خواهد؛ جست‌وجوی «همکارِ دوستِ من» پیمایش چند رابطه می‌خواهد.

نتیجه و دلیل: یک سند برای اولی طبیعی‌تر و یک گراف برای دومی مناسب‌تر است؛ شکل پرسش بخشی از انتخاب مدل است.

مثال‌های این فصل سناریوهای آموزشیِ برگرفته از کارهای روزمره‌اند. عددهای انتخاب‌شده برای توضیح‌اند؛ معماریِ داخلیِ یک شرکت مشخص را توصیف نمی‌کنند.

Read the four bars top to bottom: L1 App objects (people and orders in code), L2 Storage model (JSON, tables or graph — this chapter’s subject), L3 Storage engine (bytes on disk and memory — Chapter 4), and L4 Hardware (current, light, magnetism). The three right-hand tags (abstracts / encodes / embodies) say what each layer does to the one below: hide it, encode it, embody it.

زبان کوئری اعلانی (Declarative) در برابر دستوری (Imperative)Declarative vs imperative query languages

زبان اعلانی می‌گوید چه نتیجه‌ای می‌خواهیم؛ زبان دستوری مراحل رسیدن به نتیجه را مشخص می‌کند. در SQL معمولاً انتخاب روش اجرا را به موتور پایگاه داده می‌سپاریم.

مثال فنی: SQL نتیجه را می‌خواهد، نه حلقه را

وضعیت اولیه: هدف مشتریانی است که بیش از سه سفارش دارند.

اجرای مرحله‌به‌مرحله: GROUP BY customer_id HAVING COUNT(*) > 3 شرط نتیجه را می‌گوید؛ optimizer می‌تواند نوع اسکن و تجمیع را انتخاب کند.

نتیجه و دلیل: در حلقهٔ دستوری ترتیب اجرا را خودمان تعیین می‌کنیم؛ اعلانی بودن آزادی انتخاب طرح اجرا را به موتور می‌دهد.

  • اعلانی (مثل SQL، Cypher، SPARQL، Datalog): می‌گویی چه می‌خواهی — چه الگویی از داده، چه شرط‌هایی، چه مرتب‌سازی و تجمیعی — اما نمی‌گویی «چطور».
  • دستوری (مثل Python و Java): باید الگوریتم و ترتیب عملیات را خودت بنویسی.
  • مزیت بزرگ اعلانی: مخفی ماندنِ جزئیات موتور کوئری، پس بهینه‌سازِ پایگاه داده می‌تواند ایندکس و الگوریتم join را عوض کند — حتی کوئری را موازی کند — بدون تغییرِ حتی یک خط از کدِ تو.
  • Declarative (SQL, Cypher, SPARQL, Datalog): you say what you want — which data pattern, which conditions, which sort and aggregation — but never “how”.
  • Imperative (Python, Java): you must write the algorithm and operation order yourself.
  • The big declarative win: the query engine’s internals stay hidden, so the database optimizer can swap indexes and join algorithms — even parallelize the query — without changing a line of your code.

۱1 مدل رابطه‌ای در برابر مدل سندیRelational vs document model

در مدل رابطه‌ای، داده در جدول‌ها و ارتباط میان آن‌ها نگه داشته می‌شود. در مدل سندی، اطلاعات مرتبط می‌تواند داخل یک سندِ تو‌در‌تو قرار بگیرد.

مثال فنی: دو نمایش یک سابقهٔ شغلی

وضعیت اولیه: مریم با person_id=17 در شرکت 7 کار کرده است.

اجرای مرحله‌به‌مرحله: رابطه‌ای: ردیف در people و ردیف در jobs با کلید خارجی؛ سندی: آرایهٔ jobs داخل سند مریم.

نتیجه و دلیل: سند خواندن یکجای رزومه را ساده می‌کند؛ گزارش همهٔ کارمندان شرکت ممکن است ایندکس و پردازش متفاوتی بخواهد.

The relational model (Codd, 1970) keeps data in relations (in SQL: tables) with unordered rows. It dominated by the mid-80s and still rules business analytics. Challengers came and went: network and hierarchical models, object databases, XML databases; each made noise and never stuck. Instead SQL absorbed them (XML, JSON, graph). The 2010s NoSQL wave popularized document models, schema flexibility and scale-out, while NewSQL chased NoSQL scalability with relational transactional guarantees.

ناهماهنگی شیء-رابطه‌ای (Impedance Mismatch) و ORMObject-relational impedance mismatch and ORMs

ORM واسطه‌ای است که اشیای برنامه را به جدول‌ها و ردیف‌ها تبدیل می‌کند. این واسطه نوشتن کد را آسان‌تر می‌کند، اما تفاوتِ شکل اشیا و جدول‌ها را از بین نمی‌برد.

مثال فنی: N+1 در صفحهٔ رزومه‌ها

وضعیت اولیه: ORM ابتدا ۱۰۰ متقاضی را با یک SQL می‌خواند.

اجرای مرحله‌به‌مرحله: دسترسی lazy به person.jobs برای هر متقاضی یک SQL جدا می‌زند: مجموعاً ۱۰۱ پرس‌وجو؛ preload می‌تواند رابطه‌ها را دسته‌ای بخواند.

نتیجه و دلیل: کد کوتاه لزوماً مسیر کم‌هزینه نیست؛ تعداد query و دادهٔ برگردانده‌شده باید دیده شود.

Since most development is object-oriented, storing data in tables needs an awkward translation layer; the gap is called impedance mismatch (a term borrowed from electronics). ORM frameworks (like Hibernate and ActiveRecord) slim this layer down, but the criticisms are serious:

  • پیچیده‌اند و نمی‌توانند تفاوت دو مدل را کامل پنهان کنند.
  • فقط برای OLTP‌اند؛ مهندس داده همچنان با نمایش رابطه‌ایِ زیرین کار دارد، پس طراحی اسکیما مهم می‌ماند.
  • در سیستم‌های متنوع (موتور جست‌وجو، گراف، NoSQL) معمولاً پشتیبانی ندارند.
  • اسکیمای تولیدشان می‌تواند هم برای کاربرِ مستقیمِ داده ناخوشایند باشد و هم ناکارآمد.
  • به‌سادگی کوئری ناکارآمد می‌سازند؛ مثال معروف: مسئلهٔ N+1 — یک کوئری برای N نظر، و بعد N کوئری جدا برای نویسندهٔ هر نظر، در حالی که یک JOIN این کار را یک‌جا می‌کرد.
  • They are complex and cannot fully hide the two models’ differences.
  • They serve OLTP only; the data engineer still works with the underlying relational representation, so schema design stays important.
  • They usually lack support across diverse systems (search engines, graphs, NoSQL).
  • Their generated schemas can be both unpleasant for direct data users and inefficient.
  • They easily generate inefficient queries; the famous case: the N+1 problem — one query for N comments, then N separate queries for each comment’s author, where one JOIN would have done it at once.

مدل سندی: رزومه به‌صورت یک درختThe document model: a resume as a tree

یک سند برای داده‌ای مناسب است که به‌شکل یک مجموعهٔ تو‌در‌تو خوانده می‌شود. این سادگی زمانی کمتر می‌شود که بخش‌های سند میان افراد زیادی مشترک باشند.

مثال فنی: Embedding تا کجا مناسب است؟

وضعیت اولیه: رزومه دو سابقهٔ شغلی دارد و هر دو در آرایهٔ jobs ذخیره‌اند.

اجرای مرحله‌به‌مرحله: خواندن سند همهٔ رزومه را می‌آورد؛ اگر نام شرکت در ۱۰ هزار رزومه کپی شده باشد، تغییر نام به اصلاح همان ۱۰ هزار نسخه نیاز دارد.

نتیجه و دلیل: بخش‌های متعلق به یک رزومه برای embedding مناسب‌اند؛ هویت مشترک شرکت معمولاً مرجع مستقل می‌خواهد.

Not all data is relational. The book’s example: a resume (a career-network profile). Fields like name appear once, but “job history”, “education” and “contact info” each hold several items. Two fixes:

  • Shredding (تکه‌تکه کردن): هر بخش در جدول جدا با کلید خارجی به جدول users.
  • Embedding (توکار کردن): همه در یک سند JSON — یک درختِ یک‌به‌چند که دقیقاً با ساختار شیء در کد یکی است.
  • Shredding: each section in its own table with a foreign key to users.
  • Embedding: everything in one JSON document — a one-to-many tree identical to the in-code object structure.
{
  "user_id": 251,
  "first_name": "Barack",
  "last_name": "Obama",
  "positions": [
    {"job_title": "President", "organization": "United States of America"},
    {"job_title": "US Senator (D-IL)", "organization": "United States Senate"}
  ],
  "education": [
    {"school_name": "Harvard University", "start": 1988, "end": 1991}
  ],
  "contact_info": {"website": "https://barackobama.com"}
}

مزیت سند: locality — هرچه برای نمایش یک پروفایل لازم است یک‌جاست و یک خواندن کافی است؛ در مدل رابطه‌ای یا باید چند کوئری بزنی یا یک join چندطرفهٔ درهم‌وبرهم. اما اگر واقعاً آیتم‌های مرتبط «تعدادشان زیاد» باشد (مثلاً هزاران نظر زیر یک پست)، توکار کردن در سند ناخوشایند می‌شود.

The document’s edge: locality — everything a profile page needs sits together, so one read suffices; in the relational model you either issue several queries or one tangled multi-way join. But when related items truly run “large” (e.g. thousands of comments under a post), embedding in a document turns unpleasant.

مقایسه — تکه‌تکه کردن در برابر توکار کردنComparison — shredding vs embedding باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: Embedding تا کجا مناسب است؟

وضعیت اولیه: رزومه دو سابقهٔ شغلی دارد و هر دو در آرایهٔ jobs ذخیره‌اند.

اجرای مرحله‌به‌مرحله: خواندن سند همهٔ رزومه را می‌آورد؛ اگر نام شرکت در ۱۰ هزار رزومه کپی شده باشد، تغییر نام به اصلاح همان ۱۰ هزار نسخه نیاز دارد.

نتیجه و دلیل: بخش‌های متعلق به یک رزومه برای embedding مناسب‌اند؛ هویت مشترک شرکت معمولاً مرجع مستقل می‌خواهد.

دو کارت را کنار هم بخوانید. چپ (Shredding): سه جدولِ کوچکِ users و positions و education که با کلید خارجی به هم گره خورده‌اند؛ خواندن یعنی JOIN در هر بار، نوشتن یعنی یک ردیف در هر بار. راست (Embedding): یک سند JSON که همه‌چیز را در خود دارد؛ خواندن یعنی یک جست‌وجو، نوشتن یعنی بازنویسی و کپی‌ها. جملهٔ پایین حکم انتخاب است: درختِ یک‌به‌چند را توکار کن، وبِ چند‌به‌چند را ارجاع بده.

Read the two cards side by side. Left (Shredding): three small tables — users, positions, education — tied by foreign keys; reading means a JOIN every time, writing means one row at a time. Right (Embedding): one JSON document holding everything; reading means a single lookup, writing means rewrites and copies. The bottom line is the selection rule: embed a one-to-few tree, reference a many-to-many web.

نرمال‌سازی در برابر نرمال‌زداییNormalization vs denormalization

ایدهٔ کلیدیKey idea

نرمال‌سازی یعنی اطلاعات مشترک را یک‌جا نگه داریم و با شناسه به آن ارجاع بدهیم. نرمال‌زدایی یعنی برای آسان‌تر شدن خواندن، بخشی از داده را تکرار کنیم.

مثال فنی: نشانیِ فعلی و نشانیِ تاریخی

وضعیت اولیه: نشانی فعلی مشتری در جدول customers است؛ سفارش ۴۲ به نشانی زمان خرید نیاز دارد.

اجرای مرحله‌به‌مرحله: سفارش یک snapshot از نشانی ارسال نگه می‌دارد؛ تغییر نشانی فعلی، سفارش تحویل‌شدهٔ قدیمی را بازنویسی نمی‌کند.

نتیجه و دلیل: این تکرار داده عمدی است؛ هر کپی معنای زمانی جدا دارد و نباید بی‌فکر با مرجع فعلی همگام شود.

If you keep region_id (not the text “Washington, DC”), your data is normalized: human-meaningful information is stored exactly once, and everything else references it by ID. Copying the text everywhere is denormalized. An ID means nothing to humans, so it never changes; but anything meaningful may change tomorrow — and then every copy must be updated.

نرمال‌شده (ID + join)دنرمال‌شده (کپی متن)
نوشتنارزان‌تر: فقط یک نسخهگران‌تر: چند نسخه برای به‌روزرسانی
خواندنکندتر: نیاز به joinسریع‌تر: join کمتر
سازگاریخودکار با تراکنش اتمیخطر ناسازگاری اگر فرآیند وسط کار بمیرد
نمونهOLTP (هر دو سریع باشند)سیستم‌های تحلیلی (به‌روزرسانی دسته‌ای)
Normalized (ID + join)Denormalized (copied text)
WritesCheaper: a single copyPricier: several copies to update
ReadsSlower: needs a joinFaster: fewer joins
ConsistencyAutomatic with atomic transactionsInconsistency risk if a process dies mid-way
ExampleOLTP (both stay fast)Analytical systems (batch updates)
نکته‌های ظریفSubtleties
  • نرمال‌زدایی را مثل دادهٔ مشتق‌شده ببین (مثل فصل ۱): یعنی باید فرآیندی برای به‌روزرسانیِ کپی‌های اضافی بسازی.
  • درسِ شبکهٔ اجتماعی (فصل ۲): تایم‌لاینِ متریالایزشده در X فقط ID پست و فرستنده را نگه می‌دارد، نه متن و تعداد لایک را — چون آن‌ها سریع عوض می‌شوند. هنگام خواندن، این IDها را «آبرسانی» می‌کنند (hydrating IDs) که عملاً یک join در کد برنامه است.
  • نتیجهٔ مهم: «join هنگام خواندن» ذاتاً مانع مقیاس‌پذیری نیست؛ آبرسانیِ IDها به‌خوبی موازی می‌شود.
  • تصمیمِ نرمال‌زدایی همیشه بدیهی نیست: بعضی چیزها را دنرمال کن و بعضی را نرمال نگه دار؛ بستگی به تناوبِ تغییر و هزینهٔ خواندن/نوشتن دارد.
  • See denormalization as derived data (like Chapter 1): you must build a process that updates the extra copies.
  • The social-network lesson (Chapter 2): X’s materialized timeline keeps only post and sender IDs, not text or like counts — because those change fast. At read time these IDs get “hydrated” (hydrating IDs), effectively a join in application code.
  • Key result: “join at read time” is not inherently a scalability blocker; hydrating IDs parallelizes well.
  • The denormalization call is rarely obvious: denormalize some things, keep others normalized; it depends on change frequency and read/write cost.

یک‌به‌چند، چند‌به‌یک و چند‌به‌چندOne-to-many, many-to-one and many-to-many

نوع رابطه مشخص می‌کند چند رکورد به چند رکورد دیگر وصل می‌شوند. بهتر است آن را با نیاز واقعی بشناسیم، نه فقط با شکل نمودار.

مثال فنی: کلید خارجی و جدول واسط

وضعیت اولیه: مشتری 17 دو سفارش 42 و 43 دارد؛ هر سفارش چند کالا دارد.

اجرای مرحله‌به‌مرحله: orders.customer_id رابطهٔ چند‌به‌یک را می‌سازد؛ order_items(order_id, product_id, qty) سفارش و کالا را چند‌به‌چند می‌کند.

نتیجه و دلیل: هر قلم می‌تواند qty مخصوص همان رابطه داشته باشد؛ گذاشتن آن فقط روی مشتری یا کالا معنای درستی ندارد.

  • یک‌به‌چند (one-to-few): رزومه چند موقعیت شغلی دارد — درختِ مناسبِ مدل سندی.
  • چند‌به‌یک: افراد زیادی در یک منطقه زندگی می‌کنند → منطقه را با ID ارجاع بده.
  • چند‌به‌چند: یک نفر در چند سازمان کار کرده و هر سازمان چند نفر دارد → در رابطه‌ای با جدول انجمنی (join table)؛ در مدل سندی یا ارجاع به سند دیگر یا نگه‌داشتن ID در هر دو طرف (که خودش دنرمال‌شده و ناسازگاری‌ساز است).
  • راه‌حل نرمال: رابطه را یک‌جا نگه دار و با ایندکس ثانویه روی هر دو ستون، جست‌وجو در هر دو جهت را سریع کن.
  • محدودیت سند: نمی‌توانی به یک آیتمِ توکار مستقیماً ارجاع بدهی (باید بگویی «آیتم دومِ لیستِ positions کاربر ۲۵۱»)؛ و برای لیست‌هایی که کاربر جابه‌جاشان می‌کند (مثل to-do)، آرایهٔ JSON بسیار طبیعی‌تر از ترفندهای رابطه‌ای است.
  • One-to-few: a resume holds a few jobs — a tree fit for the document model.
  • Many-to-one: many people live in one region → reference the region by ID.
  • Many-to-many: one person worked at several organizations and each organization has many people → in relational, an association table (join table); in documents, either reference another document or keep IDs on both sides (itself denormalized and inconsistency-prone).
  • The normalized fix: keep the relationship in one place and speed up both directions with a secondary index on each column.
  • Document limits: you cannot reference an embedded item directly (you must say “second item of user 251’s positions list”); and for lists users reorder (like to-dos), a JSON array is far more natural than relational tricks.

نمودار ER — اسکیمای رابطه‌ای رزومهER diagram — the resume's relational schema

نمودار ER — موجودیت‌ها، کلیدهای خارجی و جدولِ انجمنیER diagram — entities, foreign keys and the association table باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: کلید خارجی و جدول واسط

وضعیت اولیه: مشتری 17 دو سفارش 42 و 43 دارد؛ هر سفارش چند کالا دارد.

اجرای مرحله‌به‌مرحله: orders.customer_id رابطهٔ چند‌به‌یک را می‌سازد؛ order_items(order_id, product_id, qty) سفارش و کالا را چند‌به‌چند می‌کند.

نتیجه و دلیل: هر قلم می‌تواند qty مخصوص همان رابطه داشته باشد؛ گذاشتن آن فقط روی مشتری یا کالا معنای درستی ندارد.

این نمودار ER، همان رزومهٔ نمودار قبلی را در مدل رابطه‌ای نشان می‌دهد. از جعبهٔ وسطِ سمت چپ شروع کنید: «users» با زیرنویسِ «PK user_id, name, FK region_id». سه فلشِ «1..N» از آن بیرون می‌آید: به «positions» بالا، «education» وسط و «contact_info» پایین. یعنی هر کاربر در هرکدام چند ردیف دارد — همان درختِ یک‌به‌چند که در JSON توکار می‌شد.

حالا فلشِ چین‌چینِ «N..1» را از users دنبال کنید که به «regions» می‌رود (برچسبِ «normalized»): بسیاری از افراد در یک منطقه‌اند، اما نامِ منطقه فقط یک‌جا ذخیره می‌شود. به‌جای متنِ تکراری، ID می‌گذاریم و هنگام نمایش، یک lookup یا join اضافه می‌پردازیم.

سمت راست، «positions» با فلشِ «N..1» به «organizations» وصل است (برچسبِ «entity»، زیرنویسِ «PK org_id, name, logo_url»). این همان تصمیمِ نرمال‌سازی است: اگر لوگوی سازمان عوض شود، فقط یک ردیف تغییر می‌کند. و اگر دقیق‌تر نگاه کنید، بین users و organizations عملاً رابطهٔ چند‌به‌چند داریم که همان جدولِ positions آن را می‌سازد: هر ردیفِ positions یک user_id را به یک org_id گره می‌زند.

This ER diagram shows the previous diagram’s resume in the relational model. Start from the mid-left box: “users” (“PK user_id, name, FK region_id”). Three “1..N” arrows leave it: to “positions” above, “education” in the middle and “contact_info” below. Each user owns several rows in each — the same one-to-many tree that JSON would embed.

Now follow the dashed “N..1” arrow from users to “regions” (tagged “normalized”): many people share one region, but the region’s name is stored exactly once. Instead of repeated text we keep an ID, paying one extra lookup or join at display time.

On the right, “positions” connects with “N..1” to “organizations” (“entity”, “PK org_id, name, logo_url”). That is the normalization call: if an organization’s logo changes, only one row changes. And look closer: users and organizations effectively hold a many-to-many relationship, built by that same positions table: each positions row ties one user_id to one org_id.

اسکیمای تحلیلی: Star، Snowflake و OBTAnalytic schemas: star, snowflake and OBT

در مدل تحلیلی، جدول واقعیت رویدادها و مقدارها را نگه می‌دارد و جدول‌های بُعد ویژگی‌های توصیفی را. میزان جدا کردن یا کنار هم گذاشتن این اطلاعات، شکل اسکیما را تعیین می‌کند.

مثال فنی: Fact و dimension روی یک فروش

وضعیت اولیه: قلم فروش شامل product_id=7, store_id=3, amount=200 است.

اجرای مرحله‌به‌مرحله: جدول fact مبلغ و کلیدها را دارد؛ dimension محصول نام و دسته‌بندی را می‌دهد؛ join برای جمع فروش به تفکیک دسته اجرا می‌شود.

نتیجه و دلیل: در OBT نام دسته در ردیف‌های فروش تکرار می‌شود؛ خواندن ساده‌تر می‌شود، ولی تغییر و معنای تاریخچه باید مدیریت شود.

Warehouses are usually relational with well-known conventions. In a star schema, the center holds a fact table (e.g. fact_sales) where each row is one event at a point in time (a purchase). Its columns are either attributes (sale price, cost) or foreign keys to dimension tables: the who/what/where/when/how/why dimensions. E.g. dim_product (SKU, brand, category), dim_store, even dim_date (so public holidays stay analyzable). The “star” name comes from the rays around the central table.

  • Snowflake: بعد‌ها به زیربعد شکسته می‌شوند (برند و دسته جدول جدا) — نرمال‌تر، ولی برای تحلیلگر کار با star ساده‌تر است.
  • OBT (One Big Table): حذف کامل جدول‌های بعد و تا کردن اطلاعاتشان در ستون‌های دنرمال‌شده — فضای بیشتر، گاهی کوئری سریع‌تر.
  • دنرمال‌سازی در تحلیل بی‌دردسر است: داده تاریخچهٔ تغییرناپذیر است و نگرانیِ سازگاریِ نوشتنِ OLTP را ندارد.
  • جدول واقعیه می‌تواند پتابایتی شود؛ چون هر رویداد را جدا نگه می‌دارند، بعداً انعطاف تحلیلی بیشتر است.
  • Snowflake: dimensions split into sub-dimensions (brand and category in separate tables) — more normalized, but analysts find star simpler.
  • OBT (One Big Table): dimension tables dropped entirely, their data folded into denormalized columns — more space, sometimes faster queries.
  • Denormalization is painless in analytics: data is immutable history with none of OLTP’s write-consistency worries.
  • Fact tables can reach petabytes; keeping each event separate preserves future analytical flexibility.

Schema-on-Read در برابر Schema-on-Write + نمودار ۱Schema-on-read vs schema-on-write + Diagram 1

در schema-on-write، داده پیش از ذخیره با ساختار موردانتظار بررسی می‌شود. در schema-on-read، بخش بیشتری از تفسیر و بررسی ساختار هنگام خواندن انجام می‌شود؛ دادهٔ سندی هم الزاماً بی‌ساختار نیست.

مثال فنی: اعتبارسنجی در نوشتن یا خواندن

وضعیت اولیه: منبعی age="unknown" می‌فرستد، اما گزارش میانگین سن عدد می‌خواهد.

اجرای مرحله‌به‌مرحله: schema-on-write می‌تواند رکورد نامعتبر را همان ورود رد کند؛ schema-on-read فایل خام را می‌پذیرد و هنگام تحلیل خطا یا مقدار نامعلوم را مدیریت می‌کند.

نتیجه و دلیل: نبود schema در ذخیره، نیاز به تفسیر و اعتبارسنجی را حذف نمی‌کند؛ فقط محل آن جابه‌جا می‌شود.

  • schema-on-write (روابطه‌ای سنتی): اسکیما صریح است و پایگاه داده هنگام نوشتن تضمین می‌کند داده مطابق آن باشد — مثل type checking ایستا.
  • schema-on-read (سند): هیچ اسکیمایی اجرا نمی‌شود و ساختار فقط هنگام خواندن تفسیر می‌شود — مثل type checking پویا. واژهٔ «schemaless» گمراه‌کننده است چون کدِ خواننده همیشه ساختاری را فرض می‌کند.
  • هنگام تغییر قالب (مثلاً جدا کردن نام کامل به نام و نام خانوادگی): در سند، کدِ برنامه باید با اسناد قدیمی کنار بیاید؛ در رابطه‌ای، مهاجرت (ALTER TABLE + UPDATE) که روی جدول بزرگ کند است.
  • schema-on-read وقتی مفید است که ساختار داده ناهمگون باشد یا سیستم‌های بیرونی آن را تعیین کنند.
  • Schema-on-write (traditional relational): the schema is explicit and the database enforces it at write time — like static type checking.
  • Schema-on-read (documents): no schema is enforced and structure is only interpreted at read time — like dynamic type checking. “Schemaless” misleads, since reader code always assumes some structure.
  • When the format changes (e.g. splitting a full name into first and last): in documents, application code must cope with old documents; in relational, a migration (ALTER TABLE + UPDATE) that runs slowly on big tables.
  • Schema-on-read pays off when data structure is heterogeneous or dictated by outside systems.
نمودار ۱ · Architecture — یک رزومه، دو مدل دادهDiagram 1 · Architecture — one resume, two data models باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودار ۱Teacher narration of Diagram 1

مثال فنی برای خواندنِ نمودار: دو نمایش یک سابقهٔ شغلی

وضعیت اولیه: مریم با person_id=17 در شرکت 7 کار کرده است.

اجرای مرحله‌به‌مرحله: رابطه‌ای: ردیف در people و ردیف در jobs با کلید خارجی؛ سندی: آرایهٔ jobs داخل سند مریم.

نتیجه و دلیل: سند خواندن یکجای رزومه را ساده می‌کند؛ گزارش همهٔ کارمندان شرکت ممکن است ایندکس و پردازش متفاوتی بخواهد.

بیایید این نمودار را با هم، قدم‌به‌قدم، بخوانیم. اول چشمتان به نیمهٔ بالای شکل می‌افتد: جعبهٔ «Profile Page (SQL)» با توضیحِ «fetch full resume». از کنارهٔ راست همین جعبه سه فلش بیرون آمده است.

فلش اول، پررنگ، با برچسب «query + join» به جدول users می‌رسد؛ دو فلش بعدی هر دو برچسب یکسان «join on user_id» دارند و به جدول‌های positions و education می‌روند. یعنی برای ساختنِ یک صفحهٔ رزومه باید سه جدول را کنار هم بیاوریم.

حالا فلش چین‌چینِ «resolve ID» را دنبال کنید: از جدول users به جدولِ regions می‌رود که برچسبِ کنارش normalized است. این همان نرمال‌سازی است: مقدار region_id که برای انسان بی‌معنی است، این‌جا به نامِ قابل‌خواندن تبدیل می‌شود.

به نیمهٔ پایینِ شکل بروید. این‌جا جعبهٔ «Profile Page (JSON)» فقط یک فلش پررنگ دارد: «single read» که مستقیم به «Profile Document» می‌رسد؛ سندی که زیرنویسش «positions + education inside» است و برچسبش «one read». تنها فلش چین‌چینِ این نیمه، «by region_id» است که به regions collection می‌رود.

درسِ شکل در همین تضاد است: بالا چند پرس‌وجو و join برای بازسازی یک درخت، پایین یک خواندن — اما بهای پایین، تکرار داده و مسئلهٔ به‌روزرسانیِ کپی‌هاست.

Let’s read this diagram together, step by step. First your eye lands on the top half: the “Profile Page (SQL)” box (“fetch full resume”). Three arrows leave its right edge.

The first, bold arrow, labelled “query + join”, reaches the users table; the next two share one label, “join on user_id”, and reach the positions and education tables. Building one resume page means joining three tables.

Now follow the dashed “resolve ID” arrow: from the users table to the regions table, tagged normalized. That is normalization: the human-meaningless region_id becomes a readable name here.

Move to the bottom half. Here the “Profile Page (JSON)” box has a single bold arrow: “single read”, straight into the “Profile Document”; a document subtitled “positions + education inside” and tagged “one read”. This half’s only dashed arrow, “by region_id”, goes to the regions collection.

The lesson is in this contrast: above, several queries and joins to rebuild one tree; below, a single read — but the price below is data duplication and the copy-update problem.

نمودار کلاس/ER — یک رزومه، سه شکلClass/ER diagram — one resume, three shapes

تا اینجا هر مدل را جدا دیدیم. حالا هر سه را با زبانِ نمودارِ کلاس/ER کنار هم بگذار تا تفاوتِ شکلِ داده یک‌جا معلوم شود: در مدلِ رابطه‌ای با «موجودیت + کلید خارجی + کاردینالیتی» طرفیم، در مدلِ سندی با «یک درختِ تو در تو»، و در مدلِ گرافی با «رأس + یالِ دارای خاصیت». همان اطلاعات، سه شکل — و هر شکل، دسته‌ای از پرس‌وجوها را ارزان می‌کند.

So far we saw each model alone. Now put all three side by side in class/ER notation so the shape of data shows at once: in relational, “entity + foreign key + cardinality”; in documents, “one nested tree”; in graphs, “node + attributed edge”. Same information, three shapes — and each shape cheapens one family of queries.

نمودار کلاس/ER — یک رزومه، سه شکل: Relational · Document · GraphClass/ER diagram — one resume, three shapes: Relational · Document · Graph باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: دو نمایش یک سابقهٔ شغلی

وضعیت اولیه: مریم با person_id=17 در شرکت 7 کار کرده است.

اجرای مرحله‌به‌مرحله: رابطه‌ای: ردیف در people و ردیف در jobs با کلید خارجی؛ سندی: آرایهٔ jobs داخل سند مریم.

نتیجه و دلیل: سند خواندن یکجای رزومه را ساده می‌کند؛ گزارش همهٔ کارمندان شرکت ممکن است ایندکس و پردازش متفاوتی بخواهد.

شکل سه پنل دارد و عنوانِ هر پنل بالایش آمده. در پنلِ چپ — «A · Relational: tables + foreign keys» — دو جعبهٔ کلاس‌مانند می‌بینی. جعبهٔ بالا «person» است با سه ویژگی: «person_id PK»، «first_name» و «last_name». جعبهٔ پایین «job» است با «job_id PK»، «person_id FK»، «title» و «start_date». بین این دو، خطی عمودی با لوزیِ ترکیب در سرِ آن می‌بینی و دو برچسبِ کاردینالیتی: «1» کنارِ person و «0..*» کنارِ job — یعنی هر شخص صفر یا چند شغل دارد و هر شغل مالِ یک شخص است؛ برچسبِ «person_id» روی خط، همان کلیدِ خارجی است.

پنلِ وسط — «B · Document: one self-contained tree» — یک جعبهٔ بزرگ دارد: «resume (one JSON document)» با ویژگی‌های «_id»، «name» و سپس آرایه‌های تو در تو: «jobs: [ { title, start_date }, … ]»، «education: [ ... ]» و «skills: [ ... ]». زیر آن، جعبهٔ کوچکِ «job (embedded, not a table)» با ویژگی‌های «title» و «start_date» نشسته و لوزیِ ترکیب با برچسبِ «1» و «0..* embedded» آن را به سند وصل می‌کند. یعنی این‌جا job جدولِ جدا نیست؛ داخلِ همان سند جا گرفته و خواندنِ رزومه فقط یک خواندن است.

پنلِ راست — «C · Graph: nodes and relationships are first-class» — سه جعبهٔ رأس داری: «(:Person)» با ویژگی‌های «name» و «born»، «(:Company)» با «name» و «industry»، و «(:City)» با «name» و «country». سه فلشِ سبز بین‌شان هست: از Person به Company با برچسبِ «[:WORKS_AT {since}]»، از Person به City با «[:LIVES_IN]» و از Company به City با «[:LOCATED_IN]». دقت کن برچسبِ اول یک ویژگی هم دارد: «{since}» — یال خودش می‌تواند داده داشته باشد، چیزی که در کلیدِ خارجیِ ساده جایی ندارد.

حالا زیرِ هر پنل، سه خط توضیحِ کوتاه را بخوان. پنلِ A: «هر ردیف یک چیز است؛ ارجاع‌ها باید معتبر بمانند» و «نمایشِ رزومه یعنی JOIN در لحظهٔ خواندن». پنلِ B: «همهٔ فیلدهای یک کاربر کنار هم‌اند — بدونِ join» اما «denormalization یعنی تکرارِ داده در چند جا». پنلِ C: «روابط دادهٔ خودشان را دارند» و «هزینهٔ پرس‌وجو به جست‌وجو بستگی دارد، نه به schema». و خطِ پایینِ کلِ شکل نتیجه را می‌گیرد: همان اطلاعات، سه شکل — مدل تعیین می‌کند کدام دسترسی ارزان است و کدام‌یک به تراکنش یا تکرارِ داده نیاز دارد.

درسِ شکل: «ناهماهنگی شیء-رابطه‌ای» دقیقاً همین شکافِ بین جعبه‌های پنلِ A و شیءِ درونِ کدِ برنامه است. مدلِ سندی آن را با جا دادنِ کلِ درخت در یک سند حل می‌کند — اما بهایش را هنگامِ به‌روزرسانیِ داده‌های تکرارشده می‌پردازی. مدلِ گرافی هم وقتی برنده است که سوالِ تو اساساً دربارهٔ «چه کسی به چه چیزی وصل است» باشد.

The figure has three panels, each titled above. In the left panel — “A · Relational: tables + foreign keys” — you see two class-like boxes. The top box is “person” with three attributes: “person_id PK”, “first_name” and “last_name”. The bottom box is “job” with “job_id PK”, “person_id FK”, “title” and “start_date”. Between them a vertical line with a composition diamond carries two cardinality labels: “1” by person and “0..*” by job — each person has zero or more jobs, each job belongs to one person; the “person_id” label on the line is the foreign key.

The middle panel — “B · Document: one self-contained tree” — holds one big box: “resume (one JSON document)” with “_id”, “name” and nested arrays: “jobs: [ { title, start_date }, … ]”, “education: [ ... ]” and “skills: [ ... ]”. Below sits the small “job (embedded, not a table)” box with “title” and “start_date”, joined to the document by a composition diamond labelled “1” and “0..* embedded”. Here job is no separate table; it lives inside the same document, so reading the resume is a single read.

The right panel — “C · Graph: nodes and relationships are first-class” — has three node boxes: “(:Person)” with “name” and “born”, “(:Company)” with “name” and “industry”, and “(:City)” with “name” and “country”. Three green arrows run between them: Person to Company labelled “[:WORKS_AT {since}]”, Person to City “[:LIVES_IN]”, Company to City “[:LOCATED_IN]”. Note the first label carries an attribute: “{since}” — an edge can hold data itself, something a plain foreign key has no room for.

Now read the three short notes under each panel. Panel A: “each row is one thing; references must stay valid” and “showing the resume means JOIN at read time”. Panel B: “all of one user’s fields sit together — no join” but “denormalization means duplicated data in several places”. Panel C: “relationships carry their own data” and “query cost depends on traversal, not schema”. The bottom line of the whole figure concludes: same information, three shapes — the model decides which access is cheap and which needs transactions or duplicated data.

The lesson: “object-relational mismatch” is exactly this gap between panel A’s boxes and the in-code object. The document model closes it by fitting the whole tree in one document — but pays at duplicated-data update time. And the graph model wins when your question is fundamentally “who connects to what”.

۲2 مدل‌های گرافیGraph models

ایدهٔ کلیدیKey idea

مدل گرافی داده را به گره‌ها و رابطه‌ها تبدیل می‌کند. وقتی سؤال اصلی دربارهٔ مسیر و ارتباط چندمرحله‌ای است، این شکل بیان مسئله طبیعی‌تر می‌شود.

مثال فنی: پیمایشِ دو رابطه

وضعیت اولیه: مریم به علی با رابطهٔ KNOWS وصل است و علی در شرکت C7 کار می‌کند.

اجرای مرحله‌به‌مرحله: پرسش «دوستان مریم در کدام شرکت‌اند؟» ابتدا یال دوستی و سپس یال اشتغال را طی می‌کند.

نتیجه و دلیل: وقتی عمق و نوع رابطه‌ها متغیر است، مدل گراف این مسیر را مستقیم بیان می‌کند؛ برای یک lookup ساده لزوماً لازم نیست.

If your data is a one-to-many tree, the document model is excellent. But when many-to-many relationships multiply and queries must hop several steps, the most natural model is the graph: vertices (nodes) and edges (relationships). Examples: the social graph (people and acquaintances), the web graph (pages and links), road networks (intersections and routes — shortest paths, PageRank).

Property Graph در برابر Triple StoreProperty graphs vs triple stores

در property graph، گره و یال می‌توانند ویژگی داشته باشند. در triple store، اطلاعات با سه‌تاییِ موضوع، رابطه و مقدار یا مقصد بیان می‌شود.

مثال فنی: ویژگی روی گره یا یال

وضعیت اولیه: گره‌های Person:17 و Company:7 داریم و شروع همکاری سال 2020 است.

اجرای مرحله‌به‌مرحله: در property graph، یال WORKS_AT ویژگی since=2020 دارد؛ در triple store رابطه‌ها به subject، predicate و object بیان می‌شوند و مشخصات رابطه مدل‌سازی اضافه می‌خواهد.

نتیجه و دلیل: زمان شروع به رابطه تعلق دارد، نه به همهٔ شغل‌های شخص یا همهٔ کارکنان شرکت.

  • Property graph (Neo4j، Memgraph، KùzuDB): هر رأس = شناسهٔ یکتا + برچسب (نوع شیء) + مجموعه‌ای از ویژگی‌ها + یال‌های ورودی/خروجی. هر یال = شناسه + رأس دُم + رأس سر + برچسب رابطه + ویژگی‌ها. عملاً معادل دو جدول رابطه‌ای (vertices و edges) با ایندکس روی هر دو سمت.
  • هیچ اسکیمایی جلوی اتصالِ رأس‌ها را نمی‌گیرد؛ با برچسب‌های متفاوت می‌توانی چند نوع اطلاعات را در یک گراف نگه داری (مثل گرافِ فیس‌بوک یا knowledge graph موتورهای جست‌وجو).
  • Triple store (Datomic، AllegroGraph، Blazegraph): همه‌چیز به‌صورت سه‌تایی (subject, predicate, object) — مثلاً (lucy, bornIn, idaho) یا (lucy, birthYear, 1989). مدل RDF و فرمت‌هایی مثل Turtle از همین‌جا می‌آیند.
  • محدودیت: یک یال فقط دو رأس را به هم وصل می‌کند؛ رابطهٔ سه‌طرفه را باید با یک رأسِ اضافه یا hypergraph مدل کنی.
  • Property graph (Neo4j, Memgraph, KùzuDB): each vertex = unique id + label (object type) + property set + in/out edges. Each edge = id + tail vertex + head vertex + relationship label + properties. Effectively two relational tables (vertices and edges) with indexes on both ends.
  • No schema blocks vertices from connecting; with different labels you can hold several kinds of information in one graph (like Facebook’s graph or search-engine knowledge graphs).
  • Triple store (Datomic, AllegroGraph, Blazegraph): everything as (subject, predicate, object) triples — e.g. (lucy, bornIn, idaho) or (lucy, birthYear, 1989). The RDF model and formats like Turtle come from here.
  • Limitation: one edge joins only two vertices; a three-way relationship needs an extra vertex or a hypergraph.

نمودار ۲ + روایت مدرسDiagram 2 + narration

نمودار ۲ · Architecture — مدل گرافی: افراد و مکان‌هاDiagram 2 · Architecture — graph model: people and places باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودار ۲Teacher narration of Diagram 2

مثال فنی برای خواندنِ نمودار: پیمایشِ دو رابطه

وضعیت اولیه: مریم به علی با رابطهٔ KNOWS وصل است و علی در شرکت C7 کار می‌کند.

اجرای مرحله‌به‌مرحله: پرسش «دوستان مریم در کدام شرکت‌اند؟» ابتدا یال دوستی و سپس یال اشتغال را طی می‌کند.

نتیجه و دلیل: وقتی عمق و نوع رابطه‌ها متغیر است، مدل گراف این مسیر را مستقیم بیان می‌کند؛ برای یک lookup ساده لزوماً لازم نیست.

این شکل را مثل یک نقشهٔ ساده بخوانید. سمت چپ، سه جعبه یک زنجیرهٔ جغرافیایی ساخته‌اند: پایین «Idaho» ایستاده، بالایش «United States» و بالاتر «North America». دو فلش عمودی با برچسب WITHIN این زنجیره را به هم دوخته‌اند: آیداهو «در» آمریکاست و آمریکا «در» آمریکای شمالی.

حالا جعبهٔ «Lucy» را پیدا کنید که زیرنویسش Person است. از آن دو فلش پررنگ بیرون می‌آید: یکی با برچسب BORN_IN به آیداهو می‌رود، دیگری با برچسب LIVES_IN به «London» که وسط شکل نشسته است.

سمت راست، همان الگو تکرار شده: «Saint-Lo» با WITHIN به «France»، و فرانسه با WITHIN به «Europe». زیر آن‌ها «Alain» را می‌بینید که هم BORN_IN به سن-لو دارد و هم LIVES_IN به لندن. و یک فلش افقی با برچسب MARRIED_TO این دو نفر را به هم وصل می‌کند.

حالا سوال کتاب را روی همین شکل اجرا کنیم: «کسانی که در ایالات متحده متولد شده‌اند و اکنون در اروپا زندگی می‌کنند». یک رأسِ person را بردارید، با BORN_IN به مکان تولدش بروید، بعد با زنجیرهٔ WITHIN*0.. (صفر یا چند پرشِ «درِ») تا جعبهٔ United States بالا بروید؛ همان person باید با LIVES_IN به مکانی برسد که با WITHIN*0.. به Europe می‌رسد. Lucy دقیقاً همین الگو را پر می‌کند.

و نکتهٔ طلاییِ مصاحبه: همین چهار خطِ الگو را در SQL باید با WITH RECURSIVE و حدود ۳۱ خط بنویسی — چون تعداد joinها در گراف از پیش معلوم نیست.

Read this figure like a simple map. On the left, three boxes form a geographic chain: “Idaho” at the bottom, “United States” above it, “North America” higher still. Two vertical arrows labelled WITHIN stitch the chain: Idaho is “within” the US, the US “within” North America.

Now find the “Lucy” box, subtitled Person. Two bold arrows leave it: one labelled BORN_IN to Idaho, another labelled LIVES_IN to “London”, sitting mid-figure.

On the right the same pattern repeats: “Saint-Lo” WITHIN “France”, France WITHIN “Europe”. Below them sits “Alain”, with both BORN_IN to Saint-Lo and LIVES_IN to London. And a horizontal MARRIED_TO arrow joins the two people.

Now run the book’s question on this figure: “people born in the United States who now live in Europe”. Take a person node, follow BORN_IN to their birthplace, then climb the WITHIN*0.. chain (zero or more “within” hops) up to the United States box; that same person must reach, via LIVES_IN, a place that reaches Europe via WITHIN*0... Lucy fills exactly this pattern.

And the golden interview point: the same four-line pattern costs ~31 lines of SQL with WITH RECURSIVE — because the number of joins in a graph is unknown upfront.

نمودار ساختاری — ساختار داخلی مدل گرافStructural diagram — graph model internals

نمودار ساختاری (کلاس‌مانند) — رأس، یال و ایندکس‌هاStructural (class-like) diagram — vertices, edges and indexes باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: ویژگی روی گره یا یال

وضعیت اولیه: گره‌های Person:17 و Company:7 داریم و شروع همکاری سال 2020 است.

اجرای مرحله‌به‌مرحله: در property graph، یال WORKS_AT ویژگی since=2020 دارد؛ در triple store رابطه‌ها به subject، predicate و object بیان می‌شوند و مشخصات رابطه مدل‌سازی اضافه می‌خواهد.

نتیجه و دلیل: زمان شروع به رابطه تعلق دارد، نه به همهٔ شغل‌های شخص یا همهٔ کارکنان شرکت.

این نمودار، برخلاف نمودارهای قبلی، «جریان» نشان نمی‌دهد؛ ساختار نشان می‌دهد — درست مثل یک نمودار کلاس در UML. سمت چپ دو «کلاس» داریم: «Vertex» با فیلدهای «id, label, properties» و برچسبِ «entity»، و پایین‌تر «Edge» با فیلدهای «id, tail, head, label, props» و برچسبِ «relationship». فلشِ چین‌چینِ «tail + head» از Edge به Vertex می‌گوید هر یال به دو رأس اشاره می‌کند.

حالا سمت راست ببینید همین دو «کلاس» چطور در یک پایگاه داده رابطه‌ای پیاده می‌شوند: فلشِ پررنگِ «maps to» از Vertex به «vertices table» می‌رود («PK vertex_id, label, jsonb») و از Edge به «edges table» («PK edge_id, FK tail, FK head»). فلشِ چین‌چینِ «FK references» بین دو جدول، همان کلیدهای خارجی است.

و آخرین جعبه، «Indexes» با برچسبِ «both directions»: ایندکس روی tail_vertex برای یال‌های خروجی و روی head_vertex برای یال‌های ورودی. بدون این دو ایندکس، هر پیمایشِ گراف باید کل جدولِ edges را بگردد؛ با آن‌ها، گام زدن در گراف سریع می‌ماند.

Unlike previous diagrams, this one shows no “flow”; it shows structure — just like a UML class diagram. On the left sit two “classes”: “Vertex” with fields “id, label, properties” tagged “entity”, and below it “Edge” with “id, tail, head, label, props” tagged “relationship”. The dashed “tail + head” arrow from Edge to Vertex says each edge points at two vertices.

Now see how these same two “classes” map onto a relational database on the right: the bold “maps to” arrow runs from Vertex to the “vertices table” (“PK vertex_id, label, jsonb”) and from Edge to the “edges table” (“PK edge_id, FK tail, FK head”). The dashed “FK references” arrow between the tables is the foreign keys.

And the last box, “Indexes”, tagged “both directions”: an index on tail_vertex for outgoing edges and on head_vertex for incoming ones. Without these two indexes every graph traversal must scan the whole edges table; with them, walking the graph stays fast.

زبان‌های کوئری گرافGraph query languages

زبان پرس‌وجوی گراف، الگوی رابطه‌ای را که می‌خواهیم پیدا کنیم توصیف می‌کند. بعضی زبان‌ها الگو را اعلام می‌کنند و بعضی مسیر پیمایش را مرحله‌به‌مرحله می‌سازند.

مثال فنی: همان مسیر با زبان گراف

وضعیت اولیه: گراف شامل مریم، دوستانش و شرکت‌های آن‌هاست.

اجرای مرحله‌به‌مرحله: Cypher می‌تواند الگویی مانند (p)-[:KNOWS]->(f)-[:WORKS_AT]->(c) را match کند و نام شرکت را برگرداند.

نتیجه و دلیل: موتور نحوهٔ پیمایش را انتخاب می‌کند؛ SPARQL روی الگوی triple و Datalog با قواعد رابطه‌ای همین نوع مسئله را صورت‌بندی می‌کنند.

// Cypher (Neo4j / openCypher): the pattern from Diagram 2
MATCH (person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (:Location {name:'United States'}),
      (person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (:Location {name:'Europe'})
RETURN person.name
  • Cypher: تطبیق الگو با فلش‌ها؛ از Neo4j آمد و اکنون استاندارد باز (openCypher) است. نامش از شخصیت فیلم ماتریکس است، نه رمزنگاری!
  • SPARQL: برای triple storeهای RDF؛ الگویش تقریباً مثل Cypher است (?person :bornIn / :within* / :name "United States") و چون RDF فرقِ صفت و یال نمی‌گذارد، همان سینتکس برای صفت‌ها هم کار می‌کند.
  • Datalog: قدیمی‌تر و ریشه‌دار در پژوهش‌های دههٔ ۸۰؛ به‌جای SELECT، قاعده تعریف می‌کنی و جدول‌های مجازی می‌سازی (مثل view)، و قاعده‌ها می‌توانند بازگشتی باشند → پیمایش گراف.
  • GraphQL: متفاوت از بقیه — برای OLTP و رابط کاربری؛ کلاینت شکل دقیق JSON پاسخ را تعیین می‌کند. عمداً محدود است: نه کوئری بازگشتی، نه شرط دل‌خواه (چون از منبع غیرقابل‌اعتماد می‌آید و می‌تواند DoS بسازد). روی هر پایگاه‌داده‌ای قابل پیاده‌سازی است.
  • GQL: استاندارد ISO در ۲۰۲۴ بر پایهٔ Cypher — امید به یکدستیِ بیشتر.
  • Cypher: pattern matching with arrows; from Neo4j, now the openCypher open standard. Named after the Matrix character, not cryptography!
  • SPARQL: for RDF triple stores; its pattern is nearly Cypher-like (?person :bornIn / :within* / :name "United States"), and since RDF doesn’t separate attributes from edges, the same syntax works for attributes.
  • Datalog: older, rooted in 80s research; instead of SELECT you define rules and build virtual tables (like views), and rules can recurse → graph traversal.
  • GraphQL: unlike the rest — for OLTP and UIs; the client dictates the exact JSON response shape. Deliberately limited: no recursive queries, no arbitrary conditions (requests come from untrusted sources and could DoS). Implementable over any database.
  • GQL: the 2024 ISO standard on Cypher foundations — hoping for more uniformity.

۳3 Event Sourcing و CQRSEvent sourcing and CQRS

ایدهٔ کلیدیKey idea

در Event Sourcing، تغییرات را به‌صورت رویداد نگه می‌داریم و وضعیت فعلی را از آن‌ها می‌سازیم. CQRS یعنی مدلِ ثبت تغییر و مدلِ پاسخ به خواندن می‌توانند جدا باشند؛ این دو مفهوم یکی نیستند.

مثال فنی: رویداد و نمای خواندن

وضعیت اولیه: برای سفارش ۴۲ رویدادهای OrderPlaced و سپس OrderCancelled با شمارهٔ ترتیب ثبت می‌شوند.

اجرای مرحله‌به‌مرحله: projector آن‌ها را به‌ترتیب اعمال می‌کند و ردیف خواندنی status=cancelled می‌سازد؛ command مسیر اعتبارسنجی و ثبت رویداد را دارد.

نتیجه و دلیل: Event Sourcing تاریخچه را مرجع می‌گیرد؛ CQRS مسیر ثبت فرمان و نمایش نتیجه را جدا می‌کند و الزاماً نیازمند هم نیستند.

In all previous models, data is read in the shape it was written. But sometimes one shape cannot serve every query. So store data optimized for writing and derive read-optimized views from it. The simplest write shape: an append-only event log of immutable events.

  • Event Sourcing: هر تغییر وضعیت اول به‌صورت رویداد در لاگ ثبت می‌شود؛ رویدادها تغییرناپذیرند و فقط اضافه می‌شوند. رویدادها را به زمان گذشته نام‌گذاری کن (seats were booked) چون گزارشِ واقعه‌ای‌اند که افتاده.
  • CQRS: جداسازی مسئولیتِ فرمان و کوئری — نمایش‌های فقط‌خواندنی (projection / read model / materialized view) از روی لاگ ساخته و به‌روز می‌شوند.
  • Command → اعتبارسنجی → fact: درخواست کاربر اول باید معتبر شود (مثلاً صندلی خالی باشد)؛ بعد رویدادش در لاگ می‌نشیند. پس لاگ فقط رویدادِ معتبر دارد و مصرف‌کنندهٔ نما حق رد کردن رویداد ندارد.
  • Event sourcing: every state change is first recorded as an event in the log; events are immutable and only appended. Name events in the past tense (seats were booked) because they report something that happened.
  • CQRS: separating command and query responsibility — read-only views (projection / read model / materialized view) are built and refreshed from the log.
  • Command → validation → fact: the user’s request must first validate (e.g. the seat is free); then its event lands in the log. So the log holds only valid events, and view consumers may never reject an event.

نمودار ۳ + روایت مدرسDiagram 3 + narration

نمودار ۳ · Dataflow — اول لاگ رویداد، بعد نماهای مشتق‌شدهDiagram 3 · Dataflow — event log first, derived views after باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودار ۳Teacher narration of Diagram 3

مثال فنی برای خواندنِ نمودار: رویداد و نمای خواندن

وضعیت اولیه: برای سفارش ۴۲ رویدادهای OrderPlaced و سپس OrderCancelled با شمارهٔ ترتیب ثبت می‌شوند.

اجرای مرحله‌به‌مرحله: projector آن‌ها را به‌ترتیب اعمال می‌کند و ردیف خواندنی status=cancelled می‌سازد؛ command مسیر اعتبارسنجی و ثبت رویداد را دارد.

نتیجه و دلیل: Event Sourcing تاریخچه را مرجع می‌گیرد؛ CQRS مسیر ثبت فرمان و نمایش نتیجه را جدا می‌کند و الزاماً نیازمند هم نیستند.

جریان این شکل از چپ به راست است و ستون‌های بالای آن مراحل را نام‌گذاری کرده‌اند: Commands ← Validate ← Event Log ← Materialized Views ← Reads.

در ردیف میانی، جعبهٔ «Client App» با زیرنویسِ «book, cancel» شروع می‌کند. فلش پررنگِ «command» به «Command Handler» می‌رسد؛ جعبه‌ای که زیرنویسش گویاست: «validate, then emit» — اول اعتبارسنجی، بعد صدور رویداد.

فلش پررنگِ بعدی، «append event»، به قلبِ سیستم می‌رسد: جعبهٔ «Event Log» با برچسبِ «source of truth» و زیرنویسِ «append-only, immutable». یعنی هیچ‌چیز عوض نمی‌شود؛ فقط نوشته می‌شود.

حالا از راستِ این جعبه سه فلش چین‌چین بیرون می‌آید که هرکدام یک projection هستند: «booking view» بالا می‌رود به «Booking View»، «chart data» راست می‌رود به «Dashboard Charts»، و «badge files» پایین می‌رود به «Badge Files». هر سه برچسبِ projection دارند؛ یعنی قابل بازسازی از روی لاگ‌اند.

در آخرین ستون، هر نما به مصرف‌کنندهٔ خودش وصل است: فلش‌های «query view» به «Support UI» و «Organizer View»، و «render badges» به «Badge Printer». یک نکته را خوب نگاه کنید: هیچ فلشی از راست به چپ برنمی‌گردد؛ تغییر فقط از راهِ افزودنِ رویدادِ تازه اتفاق می‌افتد، و اگر رویدادی اشتباه بود، یک رویدادِ اصلاحیِ تازه می‌نویسی.

This figure flows left to right, its top columns naming the stages: Commands → Validate → Event Log → Materialized Views → Reads.

In the middle row, the “Client App” box (“book, cancel”) starts it off. The bold “command” arrow reaches the “Command Handler”; a box whose subtitle says it all: “validate, then emit” — validate first, then emit the event.

The next bold arrow, “append event”, reaches the system’s heart: the “Event Log” box, tagged “source of truth” and subtitled “append-only, immutable”. Nothing ever changes; things only get written.

Now three dashed arrows leave this box’s right edge, each a projection: “booking view” climbs to “Booking View”, “chart data” runs right to “Dashboard Charts”, and “badge files” drops to “Badge Files”. All three carry the projection tag; all are rebuildable from the log.

In the last column, each view connects to its consumer: “query view” arrows to “Support UI” and “Organizer View”, “render badges” to “Badge Printer”. Note one thing well: no arrow ever points right-to-left; change happens only by appending a fresh event — and a mistaken event is fixed by writing a fresh corrective event.

مزیت‌هاAdvantages

رویدادها امکانِ پیگیریِ تغییر و ساختِ نمایش‌های تازه را می‌دهند، اگر تاریخچه و قواعدِ بازپخش کافی و معتبر باشند.

مثال فنی: بازسازی یک گزارش تازه

وضعیت اولیه: تاریخچهٔ سفارش شامل زمان ثبت، پرداخت و ارسال است.

اجرای مرحله‌به‌مرحله: projector جدید فاصلهٔ پرداخت تا ارسال را محاسبه می‌کند؛ رویدادهای قدیمی بدون تغییر منبع دوباره پردازش می‌شوند.

نتیجه و دلیل: می‌توان نمای تازه ساخت، به‌شرط حفظ داده و معنای رویدادها؛ snapshotِ وضعیت نهایی به‌تنهایی این فاصله‌ها را نمی‌دهد.

  • رویداد، نیت را می‌گوید: «رزرو لغو شد» بهتر از «ستون active ردیف ۴۰۰۱ false شد».
  • نماها قابل بازتولیدند: حذف و پخش دوبارهٔ همان رویدادها با همان ترتیب و کد.
  • هر تعداد نمای بهینه‌شده، حتی فقط در حافظه؛ نمای جدید از روی لاگ قدیمی هم ساخته می‌شود.
  • کارهای بازگشت‌ناپذیر کمتر می‌شود؛ لاگ، حسابرسی (audit) هم هست.
  • نوشتنِ ترتیبی، throughput بالایی می‌دهد و جهشِ رویداد را جذب می‌کند.
  • Events state intent: “booking canceled” beats “row 4001’s active column set to false”.
  • Views are reproducible: drop and replay the same events in the same order with the same code.
  • As many optimized views as you like, even memory-only; new views build from the old log too.
  • Fewer irreversible actions; the log doubles as an audit trail.
  • Sequential writes give high throughput and absorb event bursts.

هشدارهاCautions

رویدادِ قدیمی با تغییرِ کد ناپدید نمی‌شود. باید نسخهٔ رویداد، بازپخش و اثرهای جانبی را آگاهانه مدیریت کنیم.

مثال فنی: Replay نباید پیامک تاریخی بفرستد

وضعیت اولیه: projector گزارش، رویداد OrderPlaced سال گذشته را دوباره می‌خواند.

اجرای مرحله‌به‌مرحله: مسیر بازسازی فقط جدول گزارش را می‌نویسد؛ ارسال پیامک مسیرِ اثر جانبی جدا با شناسهٔ یکتا و سیاست جلوگیری از تکرار دارد.

نتیجه و دلیل: بازپخش محاسبه با تکرارِ اثر بیرونی فرق دارد؛ کپی کردن همهٔ handlerهای زنده در replay خطرناک است.

  • پردازش باید قطعی (deterministic) باشد: نرخ ارز را داخل رویداد بگذار، نه از بیرون بگیر.
  • تغییرناپذیری با «حق حذف» قوانین حریم خصوصی تضاد دارد: راه‌حل‌هایی مثل crypto-shredding.
  • پردازش دوباره نباید اثر جانبی تکرار کند (مثلاً دوباره ایمیل تأیید نرود).
  • الزام فنی: همهٔ نماها باید رویدادها را دقیقاً با همان ترتیبِ لاگ ببینند.
  • Processing must be deterministic: put the exchange rate inside the event, don’t fetch it from outside.
  • Immutability clashes with privacy law’s “right to erasure”: fixes like crypto-shredding.
  • Replays must not repeat side effects (e.g. no second confirmation email).
  • Technical must: every view must see events in exactly the log’s order.

نمودار وضعیت — چرخهٔ عمر رزروState diagram — the booking lifecycle

نمودار وضعیت (State Machine) — رویدادها به‌جای ویرایشState machine — events instead of edits باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: رزرو و رویدادِ دیررس

وضعیت اولیه: رزرو R7 با نسخهٔ ۱ ساخته و با نسخهٔ ۲ لغو شده است.

اجرای مرحله‌به‌مرحله: پیام تأییدِ متعلق به نسخهٔ قدیمی دیر می‌رسد؛ handler باید وضعیت و نسخه را بررسی کند و رزروِ لغوشده را بدون فرمان مجاز به confirmed برنگرداند.

نتیجه و دلیل: هر پیکانِ ماشین وضعیت، شرط و نتیجه دارد؛ تحویلِ یک پیام به‌تنهایی مجوز هر تغییر وضعیت نیست.

این ماشین وضعیت، زندگیِ یک صندلیِ رزرو را در مدلِ event sourcing نشان می‌دهد. از لینِ بالا شروع کنید، «Booking states»: جعبهٔ آغازِ «Open» («registration starts») با فلشِ «capacity set» به «Available» می‌رود. بعد فلشِ پررنگِ SEAT_BOOKED به «Reserved» و فلشِ پررنگِ PAYMENT_RECEIVED به «Confirmed». دقت کنید: نامِ گذارها به‌صورتِ رویدادِ زمان گذشته است، نه دستور.

از «Reserved» یک فلشِ امنیتی با برچسبِ SEAT_CANCELED به لینِ پایین می‌رود: جعبهٔ «Canceled» در «Terminal states». و بعد فلشِ چین‌چینِ «seat released» آن را دوباره به «Available» برمی‌گرداند — صندلی آزاد می‌شود، اما تاریخچه‌اش پاک نمی‌شود.

آموزنده‌ترین بخشِ شکل، گوشهٔ راست است. اگر از «Confirmed» اشتباهی پیدا شود، فلشِ چین‌چینِ «mistake found» به جعبهٔ «Correction Event» در لینِ «Corrections» می‌رود؛ زیرنویسش همه‌چیز را می‌گوید: «new event, never edit». و «compensating event» وضعیت را دوباره به «Available» می‌برد. هیچ‌جا داده ویرایش نمی‌شود؛ فقط رویدادِ تازه اضافه می‌شود — و این، همان قابلیتِ حسابرسی و بازتولیدپذیری است.

This state machine shows one bookable seat’s life under event sourcing. Start from the top lane, “Booking states”: the “Open” start box (“registration starts”) moves via “capacity set” to “Available”. Then the bold SEAT_BOOKED transition to “Reserved” and bold PAYMENT_RECEIVED to “Confirmed”. Note: transition names are past-tense events, not commands.

From “Reserved” a guard arrow labelled SEAT_CANCELED drops to the lower lane: the “Canceled” box in “Terminal states”. Then the dashed “seat released” arrow returns it to “Available” — the seat frees up, but its history is never erased.

The most instructive corner is the right one. If a mistake surfaces from “Confirmed”, the dashed “mistake found” arrow goes to the “Correction Event” box in the “Corrections” lane; its subtitle says everything: “new event, never edit”. And a “compensating event” carries the state back to “Available”. Data is never edited anywhere; only fresh events append — and that is auditability and reproducibility.

۴4 DataFrame، ماتریس و آرایهDataFrames, matrices and arrays

DataFrame داده را شبیه جدول برای تبدیل و تحلیل نمایش می‌دهد. ماتریس و آرایه برای محاسبات عددی و داده‌های چندبُعدی مناسب‌اند؛ شکل داده باید با عملیات هماهنگ باشد.

مثال فنی: جدول در برابر آرایه

وضعیت اولیه: فروش روزانه ستون‌های day, amount دارد؛ تصویر رنگی اندازهٔ 256×256×3 دارد.

اجرای مرحله‌به‌مرحله: روی DataFrame، فیلتر روز و جمع مبلغ اجرا می‌شود؛ روی تصویر، محاسبه با اندیس محورهای ارتفاع، عرض و رنگ انجام می‌شود.

نتیجه و دلیل: هر دو دادهٔ ساخت‌یافته‌اند، اما عملیات و معنای محورهایشان متفاوت است؛ تبدیل کورکورانه مدل را نامفهوم می‌کند.

These models belong more to analytics and data science than to OLTP:

  • DataFrame (R، Pandas، Spark، Dask): در نگاه اول شبیه جدول است و عملگرهای رابطه‌ای (فیلتر، گروه‌بندی، ادغام که معادل join است) دارد، اما به‌جای زبان اعلانی، با سلسله‌مراتبی از دستورها دستکاری می‌شود — دقیقاً متناسبِ کارِ تدریجیِ دانشمند داده روی نسخهٔ خصوصیِ داده.
  • کاربرد مهم: تبدیلِ دادهٔ رابطه‌ای به ماتریسِ تنک (sparse) برای الگوریتم‌های ML — مثلاً ماتریسِ امتیازِ کاربران به فیلم‌ها. ستون‌ها می‌توانند هزاران عدد باشند که در پایگاه داده رابطه‌ای جا نمی‌شوند.
  • داده‌های غیرعددی باید عددی شوند: تاریخ‌ها را مقیاس‌بندی، مقادیر دسته‌ای با one-hot encoding.
  • پایگاه داده آرایه‌ای (مثل TileDB) برای آرایه‌های چندبُعدیِ بزرگ: داده‌های ژئومکانیکی، تصاویر پزشکی، رصدخانه‌ها؛ و در مالی برای سری‌های زمانی قیمت‌ها.
  • DataFrame (R, Pandas, Spark, Dask): looks like a table at first and has relational operators (filter, group-by, merge as join), but is manipulated through a sequence of commands instead of a declarative language — exactly matching a data scientist’s incremental work on a private data copy.
  • Key use: turning relational data into a sparse matrix for ML algorithms — e.g. the user–movie ratings matrix. Columns can run to thousands of numbers that don’t fit a relational database.
  • Non-numeric data must become numeric: scale dates, encode categories with one-hot encoding.
  • Array databases (like TileDB) for large multidimensional arrays: geomechanics data, medical images, observatories; in finance, for price time series.

۵5 کدام مدل را انتخاب کنیم؟Which model to choose?

قاعدهٔ کلیRule of thumb

مدل داده را از روی سؤال‌هایی انتخاب کنیم که برنامه باید پاسخ دهد. وجود چند نوع سؤال ممکن است به چند نمایش از یک داده نیاز داشته باشد.

مثال فنی: مدل تابع الگوی دسترسی است

وضعیت اولیه: محصول هم رزومهٔ کامل، هم ارتباط افراد و هم جمع حقوق ماهانه می‌خواهد.

اجرای مرحله‌به‌مرحله: سند برای نمایش رزومه، گراف برای مسیر ارتباط و جدول تحلیلی برای تجمیع می‌تواند به‌کار رود؛ هر نمای مشتق مسیر به‌روزشدن مشخص می‌خواهد.

نتیجه و دلیل: چند مدل هزینهٔ همگامی دارد؛ وقتی همان مدل رابطه‌ای نیاز را جواب می‌دهد، افزودن سه مخزن الزام نیست.

First pin down the shape of relationships and the access pattern, then pick the model. No model is absolutely better; you can even simulate one model atop another (graphs in SQL!) — but the result turns clumsy.

اگر داده‌ات این‌طور استانتخاب معمولدلیل
درختِ یک‌به‌چند (one-to-few) که یک‌جا بارگذاری می‌شودسند (JSON)locality، نزدیکی به شیء برنامه، انعطاف اسکیما
چند‌به‌چندِ فراوان با پیمایشِ چندپلهگرافالگوی‌محور، پیمایش متغیر، تکامل‌پذیر
ترکیب روابط + join + تحلیلرابطه‌ای / star schemajoin قوی، اسکیمای صریح، ابزار BI
نوشتنِ زیاد + شکل‌های مختلف خواندنلاگ رویداد + CQRSنوشتنِ سریع، نماهای بازسازی‌پذیر
داده‌های عددیِ شبکه‌ای/آرایه‌ای برای MLDataFrame / ماتریسجبر خطی، آرایه‌های تنک
If your data looks likeUsual pickWhy
One-to-few tree loaded all at onceDocument (JSON)Locality, closeness to app objects, schema flexibility
Abundant many-to-many with multi-hop traversalGraphPattern-oriented, variable traversal, evolvable
Mixed relationships + joins + analyticsRelational / star schemaStrong joins, explicit schema, BI tooling
Heavy writes + varied read shapesEvent log + CQRSFast writes, rebuildable views
Grid/array numeric data for MLDataFrame / matrixLinear algebra, sparse arrays
جمع‌بندیِ تریدآف‌هاTrade-off summary
  • اگر کل سند را یک‌جا می‌خواهی ← سند. اگر به آیتمِ توکار باید ارجاع بدهی ← رابطه‌ای.
  • نرمال‌شده: نوشتن ارزان، خواندن با join. دنرمال‌شده: خواندن سریع، نوشتن پرهزینه و حساس به سازگاری.
  • در مقیاس کوچک و متوسط، نرمال‌سازی معمولاً بهتر است؛ در مقیاس بسیار بزرگ، هزینهٔ join مسئله‌ساز می‌شود.
  • روند روز: همگرایی — رابطه‌ای JSON گرفت، سندی join گرفت؛ ترکیبشان قدرتمند است.
  • Want the whole document at once → document. Must reference an embedded item → relational.
  • Normalized: cheap writes, join reads. Denormalized: fast reads, costly writes, consistency-sensitive.
  • At small and medium scale normalization usually wins; at very large scale join cost bites.
  • The current trend: convergence — relational gained JSON, documents gained joins; combined they are powerful.

نمودار ۴ + روایت مدرسDiagram 4 + narration

نمودار ۴ · Workflow — مسیر انتخاب مدل دادهDiagram 4 · Workflow — the data-model selection path باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودار ۴Teacher narration of Diagram 4

مثال فنی برای خواندنِ نمودار: مدل تابع الگوی دسترسی است

وضعیت اولیه: محصول هم رزومهٔ کامل، هم ارتباط افراد و هم جمع حقوق ماهانه می‌خواهد.

اجرای مرحله‌به‌مرحله: سند برای نمایش رزومه، گراف برای مسیر ارتباط و جدول تحلیلی برای تجمیع می‌تواند به‌کار رود؛ هر نمای مشتق مسیر به‌روزشدن مشخص می‌خواهد.

نتیجه و دلیل: چند مدل هزینهٔ همگامی دارد؛ وقتی همان مدل رابطه‌ای نیاز را جواب می‌دهد، افزودن سه مخزن الزام نیست.

این شکل را مثل یک مسیر تصمیم بخوانید. در ردیفِ بالا با عنوانِ «Classify»، دو جعبه کنار هم نشسته‌اند: «Data Shape» با زیرنویسِ «tree, web, or events» و کنارش «Access Pattern» با زیرنویسِ «tree, hops, scans». فلشِ پررنگِ میانِ آن‌ها برچسبِ «classify» دارد: اول شکل داده، بعد الگوی دسترسی.

حالا از «Access Pattern» چهار فلش به سمت گزینه‌ها باز می‌شود. در لینِ «Flexible Shapes» دو گزینه نشسته‌اند: «Document» که برچسبِ «one-to-few» دارد و فلشِ «tree, one-to-few» به سمتش می‌رود، و «Graph» با برچسبِ «many-to-many» و فلشِ کوتاهِ «hops».

در لینِ پایینی، «Structured History»، دو گزینهٔ دیگر جا گرفته‌اند: «Relational / Star» که فلشِ پررنگِ «joins, analytics» به آن می‌رسد — و این پررنگ‌بودن یعنی مسیر اصلیِ تصمیم‌های معمول — و «Event Log + CQRS» که فلشِ چین‌چینِ «writes, many reads» به سمتش می‌رود.

درسِ مسیر همین است: جعبه‌های چپِ شکل را که پر کنید، انتخابِ جعبه‌های راست تقریباً خودکار می‌شود.

Read this figure as a decision path. In the top lane, “Classify”, two boxes sit side by side: “Data Shape” (“tree, web, or events”) and “Access Pattern” (“tree, hops, scans”). The bold arrow between them is labelled “classify”: data shape first, access pattern second.

From “Access Pattern” four arrows fan out to the options. In the “Flexible Shapes” lane sit two: “Document”, tagged “one-to-few” with the “tree, one-to-few” arrow heading to it, and “Graph” tagged “many-to-many” with the short “hops” arrow.

In the lower lane, “Structured History”, sit the other two: “Relational / Star”, reached by the bold “joins, analytics” arrow — its boldness marks the default path of typical decisions — and “Event Log + CQRS”, reached by the dashed “writes, many reads” arrow.

The path’s lesson: once you fill in the left-hand boxes, the right-hand choice is nearly automatic.

نقشهٔ فصل — همهٔ مفاهیم کلیدی در یک نگاهChapter map — all key concepts at a glance باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: مدل تابع الگوی دسترسی است

وضعیت اولیه: محصول هم رزومهٔ کامل، هم ارتباط افراد و هم جمع حقوق ماهانه می‌خواهد.

اجرای مرحله‌به‌مرحله: سند برای نمایش رزومه، گراف برای مسیر ارتباط و جدول تحلیلی برای تجمیع می‌تواند به‌کار رود؛ هر نمای مشتق مسیر به‌روزشدن مشخص می‌خواهد.

نتیجه و دلیل: چند مدل هزینهٔ همگامی دارد؛ وقتی همان مدل رابطه‌ای نیاز را جواب می‌دهد، افزودن سه مخزن الزام نیست.

گرهٔ مرکزی (Data models and query languages) را بخوانید و شش یال را دنبال کنید: normalize به رابطه‌ای با جدول و JOIN، embed به سندی با یک درخت JSON، traverse به گراف با رأس و یال، derive به لاگ رویداد به‌عنوان حقیقتِ فقط‌افزودنی، optimize به اعلانی‌بودن (بگو چه می‌خواهی، چطورش را پنهان کن)، و decide به قاعدهٔ انتخاب: اول شکل رابطه‌ها، بعد الگوی دسترسی.

Read the central node (Data models and query languages) and follow the six edges: normalize to relational with tables and JOIN, embed to documents with one JSON tree, traverse to graphs with nodes and edges, derive to the event log as append-only truth, optimize to declarativeness (say WHAT, hide the how), and decide to the selection rule: relationship shape first, access pattern second.

۶6 بستهٔ مصاحبهInterview pack

۶.۱ همه‌چیز در یک نگاه6.1 Everything at a glance

اصطلاحتعریف یک‌خطیکلیدواژهٔ مصاحبه
Declarativeبگو چه می‌خواهی، نه چطوربهینه‌ساز کوئری آزاد است
Impedance Mismatchفاصلهٔ شیءِ برنامه و جدول‌هالایهٔ ترجمه، ORM، مسئلهٔ N+1
Shredding / Embeddingتکه‌تکه کردن درخت در جدول‌ها / توکار کردن در سندlocality در برابر join
Normalizationهر fact یک‌جا، بقیه با ID ارجاع دهندنوشتن ارزان، خواندن با join
Denormalizationکپیِ اطلاعات برای خواندنِ سریعدادهٔ مشتق‌شده، سازگاری
Hydrating IDsتبدیل ID به اطلاعات قابل‌خواندن هنگام خواندنjoin در کد برنامه
Star / Snowflake / OBTقالب‌های جدول واقعیه + بعد‌ها در انباردادهfact در مرکز، بعد‌ها دور آن
Schema-on-Read/Writeاسکیمای ضمنی هنگام خواندن / صریح هنگام نوشتنتایپ‌چکینگ پویا در برابر ایستا
Property Graphرأس با ویژگی و یال‌های برچسب‌دارمعادل دو جدول vertices/edges
Triple Store / RDFسه‌تایی subject, predicate, objectTurtle، SPARQL
Cypher / SPARQL / Datalogالگوی گراف / سه‌تایی / قاعدهٔ بازگشتی۴ خط در برابر ۳۱ خط SQL
GraphQLکلاینت شکل پاسخ JSON را تعیین می‌کندمحدودِ آگاهانه، بدون بازگشت
Event Sourcingلاگ رویدادِ تغییرناپذیر = منبع حقیقترویداد به زمان گذشته
CQRS / Projectionنماهای خواندنیِ مشتق‌شده از لاگقابل بازتولید
DataFrameجدولِ دستکاری‌شده با دستورها، پلِ به ماتریسML، آرایهٔ تنک
TermOne-line definitionInterview keyword
DeclarativeSay what you want, not howQuery optimizer is free
Impedance MismatchGap between app objects and tablesTranslation layer, ORM, N+1
Shredding / EmbeddingSplitting a tree across tables / nesting it in a documentLocality vs join
NormalizationEach fact once, everything else by IDCheap writes, join reads
DenormalizationCopying data for fast readsDerived data, consistency
Hydrating IDsResolving IDs to readable data at read timeJoin in application code
Star / Snowflake / OBTFact + dimension layouts in warehousesFact at center, dimensions around
Schema-on-Read/WriteImplicit schema at read / explicit at writeDynamic vs static type checking
Property GraphAttributed vertices with labelled edgesEquals two vertices/edges tables
Triple Store / RDFSubject, predicate, object triplesTurtle, SPARQL
Cypher / SPARQL / DatalogGraph pattern / triple / recursive rule4 lines vs 31 lines of SQL
GraphQLClient dictates the JSON response shapeDeliberately limited, no recursion
Event SourcingImmutable event log = source of truthPast-tense events
CQRS / ProjectionRead views derived from the logReproducible
DataFrameCommand-manipulated table, bridge to matricesML, sparse arrays

۶.۲ سوالات پرتکرار مصاحبه (برای دیدن جواب کلیک کن)6.2 Frequent interview questions (click for answers)

۱. چرا مدل داده این‌قدر مهم است؟1. Why does the data model matter so much?

مدلِ داده تعیین می‌کند چه چیزهایی را موجودیت و رابطه بدانیم و چه سؤال‌هایی را آسان‌تر بیان کنیم. این انتخاب بر کد و فکرِ ما دربارهٔ مسئله اثر دارد.

مثال فنی: انتخاب مدل با سؤال شروع می‌شود

وضعیت اولیه: داده شامل افراد، شرکت‌ها و سابقهٔ کار است.

اجرای مرحله‌به‌مرحله: نمایش کل رزومه یک خواندنِ تو‌در‌تو می‌خواهد؛ جست‌وجوی «همکارِ دوستِ من» پیمایش چند رابطه می‌خواهد.

نتیجه و دلیل: یک سند برای اولی طبیعی‌تر و یک گراف برای دومی مناسب‌تر است؛ شکل پرسش بخشی از انتخاب مدل است.

Because it shapes not just code but how we think about the problem. Every application stacks several model layers, each hiding the complexity below.

۲. declarative یعنی چه و چه فایده‌ای دارد؟2. What does declarative mean, and why is it useful?

در زبانِ اعلانی نتیجه را می‌گوییم و روشِ اجرا را به موتور می‌سپاریم. این آزادی به بهینه‌ساز اجازه می‌دهد طرحِ اجرا را با وضعیتِ داده و منابع تغییر دهد.

مثال فنی: SQL نتیجه را می‌خواهد، نه حلقه را

وضعیت اولیه: هدف مشتریانی است که بیش از سه سفارش دارند.

اجرای مرحله‌به‌مرحله: GROUP BY customer_id HAVING COUNT(*) > 3 شرط نتیجه را می‌گوید؛ optimizer می‌تواند نوع اسکن و تجمیع را انتخاب کند.

نتیجه و دلیل: در حلقهٔ دستوری ترتیب اجرا را خودمان تعیین می‌کنیم؛ اعلانی بودن آزادی انتخاب طرح اجرا را به موتور می‌دهد.

It means stating the desired data pattern, not the route to it. Payoff: the database optimizer can swap indexes, algorithms, even parallel execution without touching the query.

۳. ORM چه کمکی می‌کند و مسئلهٔ N+1 چیست؟3. How do ORMs help, and what is the N+1 problem?

ORM ترجمهٔ اشیا و جدول‌ها را آسان می‌کند، اما هزینهٔ پرس‌وجو را حذف نمی‌کند. در N+1، یک پرس‌وجوی اولیه و سپس یک پرس‌وجوی جدا برای هر نتیجه اجرا می‌شود.

مثال فنی: N+1 در صفحهٔ رزومه‌ها

وضعیت اولیه: ORM ابتدا ۱۰۰ متقاضی را با یک SQL می‌خواند.

اجرای مرحله‌به‌مرحله: دسترسی lazy به person.jobs برای هر متقاضی یک SQL جدا می‌زند: مجموعاً ۱۰۱ پرس‌وجو؛ preload می‌تواند رابطه‌ها را دسته‌ای بخواند.

نتیجه و دلیل: کد کوتاه لزوماً مسیر کم‌هزینه نیست؛ تعداد query و دادهٔ برگردانده‌شده باید دیده شود.

ORMs cut object↔table translation boilerplate but never fully hide the differences. N+1: one query for N records, then N separate queries for their dependents; fix: join or eager loading.

۴. تفاوت shredding و embedding؟4. Shredding vs embedding?

Shredding یعنی شکستنِ دادهٔ تو‌در‌تو به رکوردهای مرتبط؛ embedding یعنی نگه داشتنِ بخش‌های مرتبط داخلِ یک سند. خواندنِ یک مجموعه و تغییرِ روابط میان مجموعه‌ها نیازهای متفاوتی دارند.

مثال فنی: Embedding تا کجا مناسب است؟

وضعیت اولیه: رزومه دو سابقهٔ شغلی دارد و هر دو در آرایهٔ jobs ذخیره‌اند.

اجرای مرحله‌به‌مرحله: خواندن سند همهٔ رزومه را می‌آورد؛ اگر نام شرکت در ۱۰ هزار رزومه کپی شده باشد، تغییر نام به اصلاح همان ۱۰ هزار نسخه نیاز دارد.

نتیجه و دلیل: بخش‌های متعلق به یک رزومه برای embedding مناسب‌اند؛ هویت مشترک شرکت معمولاً مرجع مستقل می‌خواهد.

Shredding: splitting a tree across tables with foreign keys (relational flexibility, needs joins). Embedding: all in one document (locality, one read) — good for one-to-few, bad for large counts or referencing embedded items.

۵. کی نرمال‌سازی و کی نرمال‌زدایی؟5. When to normalize, when to denormalize?

نرمال‌سازی تکرار را کم و تغییرِ اطلاعاتِ مشترک را آسان می‌کند؛ نرمال‌زدایی بعضی خواندن‌ها را ساده‌تر می‌کند، ولی هماهنگ نگه داشتنِ نسخه‌های تکراری هزینه دارد.

مثال فنی: نشانیِ فعلی و نشانیِ تاریخی

وضعیت اولیه: نشانی فعلی مشتری در جدول customers است؛ سفارش ۴۲ به نشانی زمان خرید نیاز دارد.

اجرای مرحله‌به‌مرحله: سفارش یک snapshot از نشانی ارسال نگه می‌دارد؛ تغییر نشانی فعلی، سفارش تحویل‌شدهٔ قدیمی را بازنویسی نمی‌کند.

نتیجه و دلیل: این تکرار داده عمدی است؛ هر کپی معنای زمانی جدا دارد و نباید بی‌فکر با مرجع فعلی همگام شود.

Normalized: cheap writes, join reads, easy consistency — for OLTP and medium scale. Denormalized: fast reads, costly writes, inconsistency-prone — for analytics and when joins get expensive. Denormalization = derived data needing an update process.

۶. در مثالِ تایم‌لاینِ فصل، چرا شناسهٔ پست ذخیره می‌شود؟6. Why does X’s timeline store only IDs?

در سناریوی تایم‌لاینِ فصل، شناسهٔ پست در فهرست قرار می‌گیرد و محتوای قابل‌تغییر جدا خوانده می‌شود. این کار حجمِ کپی و هزینهٔ اصلاحِ داده را کم می‌کند.

مثال فنی: چرا timeline شناسه نگه می‌دارد؟

وضعیت اولیه: پست p9 به timeline هزار دنبال‌کننده وارد شده است.

اجرای مرحله‌به‌مرحله: فهرست‌ها فقط post_id دارند؛ متن از مخزن پست خوانده می‌شود، پس ویرایش متن به تغییر هزار کپی متن نیاز ندارد.

نتیجه و دلیل: هنوز باید حذف پست و کش مدیریت شود؛ کاهش تکرار متن همهٔ هزینهٔ نگهداری timeline را حذف نمی‌کند.

Because text, likes and profile pictures change fast and must always show the latest value; denormalizing them costs space. At read time IDs are hydrated; that parallelizes well and blocks no scale.

۷. star schema چیست و fact/dimension یعنی چه؟7. What is a star schema? What are facts and dimensions?

در طرحِ ستاره‌ای، جدولِ واقعیت رویداد و مقدارهای قابل‌محاسبه را نگه می‌دارد؛ جدول‌های بُعد اطلاعاتِ توصیفیِ آن‌ها را دارند.

مثال فنی: Fact و dimension روی یک فروش

وضعیت اولیه: قلم فروش شامل product_id=7, store_id=3, amount=200 است.

اجرای مرحله‌به‌مرحله: جدول fact مبلغ و کلیدها را دارد؛ dimension محصول نام و دسته‌بندی را می‌دهد؛ join برای جمع فروش به تفکیک دسته اجرا می‌شود.

نتیجه و دلیل: در OBT نام دسته در ردیف‌های فروش تکرار می‌شود؛ خواندن ساده‌تر می‌شود، ولی تغییر و معنای تاریخچه باید مدیریت شود.

A fact table at the center, each row one timestamped event (e.g. a purchase); dimension tables explain the who/what/where/when. “Star” comes from the rays around the central table; snowflake splits dimensions into sub-dimensions, OBT denormalizes everything into one big table.

۸. schema-on-read در برابر schema-on-write؟8. Schema-on-read vs schema-on-write?

در schema-on-write، ساختار پیش از ثبت بررسی می‌شود؛ در schema-on-read، خواننده ساختار را هنگامِ استفاده تفسیر می‌کند. روشِ دوم هم به قراردادِ داده نیاز دارد.

مثال فنی: اعتبارسنجی در نوشتن یا خواندن

وضعیت اولیه: منبعی age="unknown" می‌فرستد، اما گزارش میانگین سن عدد می‌خواهد.

اجرای مرحله‌به‌مرحله: schema-on-write می‌تواند رکورد نامعتبر را همان ورود رد کند؛ schema-on-read فایل خام را می‌پذیرد و هنگام تحلیل خطا یا مقدار نامعلوم را مدیریت می‌کند.

نتیجه و دلیل: نبود schema در ذخیره، نیاز به تفسیر و اعتبارسنجی را حذف نمی‌کند؛ فقط محل آن جابه‌جا می‌شود.

The former is like dynamic type checking: implicit structure interpreted at read time; fits heterogeneous data. The latter is like static type checking: explicit schema enforced at write time. Format change reveals the difference: code coping with old documents vs heavy table migrations.

۹. کی سراغ مدل گراف برویم؟9. When should we reach for the graph model?

وقتی روابطِ متنوع و چندمرحله‌ای موضوعِ اصلیِ سؤال باشند، گراف نمایشِ طبیعی‌تری می‌شود. صرفِ وجودِ یک رابطه دلیلِ استفاده از گراف نیست.

مثال فنی: پیمایشِ دو رابطه

وضعیت اولیه: مریم به علی با رابطهٔ KNOWS وصل است و علی در شرکت C7 کار می‌کند.

اجرای مرحله‌به‌مرحله: پرسش «دوستان مریم در کدام شرکت‌اند؟» ابتدا یال دوستی و سپس یال اشتغال را طی می‌کند.

نتیجه و دلیل: وقتی عمق و نوع رابطه‌ها متغیر است، مدل گراف این مسیر را مستقیم بیان می‌کند؛ برای یک lookup ساده لزوماً لازم نیست.

When many-to-many relationships abound and queries must traverse multiple hops, or when varied object types must share one model. Being pattern-oriented makes graphs highly evolvable.

۱۰. property graph از چه ساخته شده؟10. What is a property graph made of?

Property graph گره‌های دارای شناسه، برچسب و ویژگی و یال‌های دارای مقصد، نوع و ویژگی دارد. جهتِ یال و مشخصاتِ مدل در معنای رابطه اثر دارند.

مثال فنی: ویژگی روی گره یا یال

وضعیت اولیه: گره‌های Person:17 و Company:7 داریم و شروع همکاری سال 2020 است.

اجرای مرحله‌به‌مرحله: در property graph، یال WORKS_AT ویژگی since=2020 دارد؛ در triple store رابطه‌ها به subject، predicate و object بیان می‌شوند و مشخصات رابطه مدل‌سازی اضافه می‌خواهد.

نتیجه و دلیل: زمان شروع به رابطه تعلق دارد، نه به همهٔ شغل‌های شخص یا همهٔ کارکنان شرکت.

Vertex: id, label, properties, in/out edges. Edge: id, tail and head vertices, relationship label, properties. Equivalent to two relational tables with indexes on both ends.

۱۱. چرا همان گراف را در SQL نگه نداریم؟11. Why not keep the same graph in SQL?

گراف را می‌توان در جدول هم نگه داشت. مسئله، راحتیِ بیان و اجرای پیمایشِ چندمرحله‌ای و متغیر است؛ SQL بازگشتی نیز راهی برای این کار دارد.

مثال فنی: گراف در جدول هم ممکن است

وضعیت اولیه: جدول edges(from_id,to_id,type) یال‌های دوستی را نگه می‌دارد.

اجرای مرحله‌به‌مرحله: برای دو گام می‌توان جدول را به خودش join کرد؛ برای عمق متغیر از query بازگشتی یا موتور گراف استفاده می‌شود و هزینه با دادهٔ واقعی سنجیده می‌شود.

نتیجه و دلیل: رابطه‌ای بودن مانع ذخیرهٔ گراف نیست؛ تفاوت در راحتی بیان مسیر، ایندکس و طرح اجرای پیمایش است.

We can, but each edge means a join and traversal needs a variable number of joins; that is why a 4-line Cypher query becomes ~31 lines of SQL with WITH RECURSIVE.

۱۲. SPARQL و Datalog چه فرقی با Cypher دارند؟12. How do SPARQL and Datalog differ from Cypher?

Cypher الگوی گرافِ ویژگی‌دار را بیان می‌کند؛ SPARQL الگوی سه‌تایی‌های RDF را؛ Datalog نتیجه را با قواعدِ منطقی، از جمله قواعدِ بازگشتی، تعریف می‌کند.

مثال فنی: همان مسیر با زبان گراف

وضعیت اولیه: گراف شامل مریم، دوستانش و شرکت‌های آن‌هاست.

اجرای مرحله‌به‌مرحله: Cypher می‌تواند الگویی مانند (p)-[:KNOWS]->(f)-[:WORKS_AT]->(c) را match کند و نام شرکت را برگرداند.

نتیجه و دلیل: موتور نحوهٔ پیمایش را انتخاب می‌کند؛ SPARQL روی الگوی triple و Datalog با قواعد رابطه‌ای همین نوع مسئله را صورت‌بندی می‌کنند.

SPARQL works over RDF triples, and since attributes and edges are one, its pattern is uniform. Datalog defines rules instead of patterns and builds virtual tables; rules can recurse, making it very powerful for complex queries.

۱۳. GraphQL دقیقاً چه می‌کند و چرا محدود است؟13. What exactly does GraphQL do, and why is it limited?

GraphQL قراردادِ API است که کلاینت با آن فیلدهای موردنیازِ پاسخ را انتخاب می‌کند. اسمش به‌معنی موتورِ پایگاهِ گراف نیست؛ قابلیتِ پرسش به اسکیمای API و resolverها محدود است.

مثال فنی: GraphQL و N+1 پشت API

وضعیت اولیه: کلاینت people { name jobs { title } } می‌خواهد.

اجرای مرحله‌به‌مرحله: GraphQL شکل پاسخ API را مشخص می‌کند؛ resolver بد ممکن است برای هر فرد یک query بزند و resolver دسته‌ای این تعداد را کم کند.

نتیجه و دلیل: GraphQL نوع پایگاه داده یا بهینه بودن join را تعیین نمی‌کند؛ رفتار واقعی به اجرای resolver بستگی دارد.

The client dictates the exact JSON response shape (only the UI’s needed fields). It is limited because requests come from untrusted sources: no recursive queries or arbitrary conditions, so no DoS. Joins only follow the declared schema, and it runs over any database.

۱۴. event sourcing یعنی چه و چرا رویدادها را به زمان گذشته نام‌گذاری می‌کنیم؟14. What is event sourcing, and why past-tense event names?

در Event Sourcing، تغییرِ پذیرفته‌شده به‌شکل رویداد ذخیره می‌شود و وضعیت از آن ساخته می‌شود. نامِ گذشته نشان می‌دهد اتفاق رخ داده، نه اینکه درخواستِ انجامِ آن ارسال شده است.

مثال فنی: رویداد و نمای خواندن

وضعیت اولیه: برای سفارش ۴۲ رویدادهای OrderPlaced و سپس OrderCancelled با شمارهٔ ترتیب ثبت می‌شوند.

اجرای مرحله‌به‌مرحله: projector آن‌ها را به‌ترتیب اعمال می‌کند و ردیف خواندنی status=cancelled می‌سازد؛ command مسیر اعتبارسنجی و ثبت رویداد را دارد.

نتیجه و دلیل: Event Sourcing تاریخچه را مرجع می‌گیرد؛ CQRS مسیر ثبت فرمان و نمایش نتیجه را جدا می‌کند و الزاماً نیازمند هم نیستند.

Every state change is recorded as an immutable event in an append-only log. Past tense because an event reports something that happened; canceling a booking doesn’t void the earlier event — it is a new event.

۱۵. CQRS چه چیزی را جدا می‌کند؟15. What does CQRS separate?

CQRS مدلِ ثبتِ تغییر و مدلِ خواندن را جدا می‌کند. مدلِ خواندن می‌تواند برای پرس‌وجو آماده شود؛ استفاده از لاگِ رویداد شرطِ الزامیِ همهٔ پیاده‌سازی‌های CQRS نیست.

مثال فنی: رویداد و نمای خواندن

وضعیت اولیه: برای سفارش ۴۲ رویدادهای OrderPlaced و سپس OrderCancelled با شمارهٔ ترتیب ثبت می‌شوند.

اجرای مرحله‌به‌مرحله: projector آن‌ها را به‌ترتیب اعمال می‌کند و ردیف خواندنی status=cancelled می‌سازد؛ command مسیر اعتبارسنجی و ثبت رویداد را دارد.

نتیجه و دلیل: Event Sourcing تاریخچه را مرجع می‌گیرد؛ CQRS مسیر ثبت فرمان و نمایش نتیجه را جدا می‌کند و الزاماً نیازمند هم نیستند.

It separates the write path (the optimized log) from the read path (materialized views). Views are reproducible: replaying the same events rebuilds them.

۱۶. بزرگ‌ترین چالش‌های event sourcing؟16. The biggest event-sourcing challenges?

بازتولیدِ نتیجه به نسخهٔ رویداد و منطقِ پردازش وابسته است. ورودیِ بیرونی، حذفِ داده و اثرهای جانبیِ تکرارشونده هم باید جدا مدیریت شوند.

مثال فنی: Replay نباید پیامک تاریخی بفرستد

وضعیت اولیه: projector گزارش، رویداد OrderPlaced سال گذشته را دوباره می‌خواند.

اجرای مرحله‌به‌مرحله: مسیر بازسازی فقط جدول گزارش را می‌نویسد؛ ارسال پیامک مسیرِ اثر جانبی جدا با شناسهٔ یکتا و سیاست جلوگیری از تکرار دارد.

نتیجه و دلیل: بازپخش محاسبه با تکرارِ اثر بیرونی فرق دارد؛ کپی کردن همهٔ handlerهای زنده در replay خطرناک است.

Deterministic processing (outside data like exchange rates must live inside events), immutability vs personal-data erasure rights (crypto-shredding), and never repeating side effects on replay.

۱۷. DataFrame چه جایگاهی دارد؟17. Where do DataFrames fit?

DataFrame نمایشِ جدولی برای تبدیل و تحلیل است و با مدل‌های عددیِ یادگیریِ ماشین هم ترکیب می‌شود. عملیاتِ مشابه در اجرای محلی و توزیع‌شده هزینه و رفتارِ یکسانی ندارند.

مثال فنی: جدول در برابر آرایه

وضعیت اولیه: فروش روزانه ستون‌های day, amount دارد؛ تصویر رنگی اندازهٔ 256×256×3 دارد.

اجرای مرحله‌به‌مرحله: روی DataFrame، فیلتر روز و جمع مبلغ اجرا می‌شود؛ روی تصویر، محاسبه با اندیس محورهای ارتفاع، عرض و رنگ انجام می‌شود.

نتیجه و دلیل: هر دو دادهٔ ساخت‌یافته‌اند، اما عملیات و معنای محورهایشان متفاوت است؛ تبدیل کورکورانه مدل را نامفهوم می‌کند.

A bridge between the relational world and ML matrices: join-like and group-by operators, manipulated via commands, able to turn data into numeric sparse matrices (one-hot for categorical data).

۶.۳ اشتباهات رایج در جواب‌های مصاحبه6.3 Common interview mistakes

  • گفتن «SQL قدیمی شده» یا «NoSQL بهتر است» بدون بحثِ شکلِ رابطه‌ها و الگوی دسترسی.
  • فراموش کردن اینکه دنرمال‌سازی = دادهٔ مشتق‌شده و نیازمند به‌روزرسانیِ کپی‌ها.
  • ادعای «join ذاتاً کند است» — مثال تایم‌لاین خلافش را نشان می‌دهد.
  • درک نکردن تفاوت schema-on-read و schema-on-write در تکامل اسکیما.
  • یکی دانستن GraphQL با گراف، یا ندانستن محدودیت‌های آگاهانهٔ آن.
  • توصیف event sourcing بدون اشاره به قطعیت (determinism) و بازتولیدپذیری نماها.
  • Saying “SQL is outdated” or “NoSQL is better” without discussing relationship shape and access pattern.
  • Forgetting that denormalization = derived data needing copy updates.
  • Claiming “joins are inherently slow” — the timeline example disproves it.
  • Missing the schema-on-read vs schema-on-write difference in schema evolution.
  • Confusing GraphQL with graphs, or missing its deliberate limits.
  • Describing event sourcing without determinism and view reproducibility.

۶.۴ چک‌لیست مرور ۵ دقیقه‌ای قبل از مصاحبه6.4 Five-minute pre-interview checklist

  1. سه تفاوت کلیدی مدل سندی و رابطه‌ای + کی کدام را انتخاب کنیم.
  2. جدول نرمال‌شده در برابر دنرمال‌شده (نوشتن/خواندن/سازگاری).
  3. تعریف star schema: fact و dimension.
  4. ساختار property graph + یک جمله دربارهٔ Cypher در برابر SQL بازگشتی.
  5. سه جمله دربارهٔ event sourcing، CQRS و بازتولیدپذیری.
  6. قاعدهٔ انتخاب مدل: اول شکل رابطه‌ها و الگوی دسترسی.
  1. Three key document-vs-relational differences + when to pick each.
  2. Normalized vs denormalized table (writes/reads/consistency).
  3. Star schema definition: fact and dimension.
  4. Property-graph structure + one sentence on Cypher vs recursive SQL.
  5. Three sentences on event sourcing, CQRS and reproducibility.
  6. Model-selection rule: relationship shape and access pattern first.

۷7 تمرین‌های مصاحبه و آزمایشگاهInterview exercises and hands-on labs

این تمرین‌ها برای مسیر Backend / Distributed Systems در ۱۴ اکتبر ۲۰۲۶ بازبینی شدند. سطح مربوط به نسخهٔ مصاحبه‌ای است؛ زمان آزمایشگاه جداست.Reviewed on 14 October 2026 for Backend / Distributed Systems. Level refers to the interview brief; full-lab time is separate. روش ارزیابی و مسیر مطالعهMethod and study path

تمرین ۱ — مدل‌سازی کاتالوگ فروشگاهExercise 1 — modelling a shop catalog

L2 · SeniorL2 · Seniorاولویت اصلیCore priority40 دقیقه بحث40 min discussion

مدل‌سازی بر اساس query و constraint ارزشمند است؛ پاسخ «SQL یا NoSQL» بدون workload ضعیف است.Model choices should follow queries, cardinality and constraints.

نسخهٔ بازبینی‌شده برای مصاحبهReviewed interview brief

کاتالوگ محصول، برند، دسته‌بندی چندگانه و نظر کاربران را با مدل رابطه‌ای و سندی طراحی کن. سه query پرتکرار، pagination، رشد نظرات، uniqueness و تغییر برند را مشخص کن. schema و index را رسم کن و یک انتخاب متناسب با بار داشته باش.Model products, brands, multiple categories and unbounded reviews in relational and document forms. Specify three queries, pagination, indexes and uniqueness; justify a workload-specific choice.

معیار پاسخ خوبWhat a good answer demonstrates

  • cardinality، FK و index روشن‌اندCardinality, keys and indexes
  • آرایهٔ بی‌حد نظرات را در سند جا نمی‌دهدBounded document growth
  • انتخاب را با query و consistency توجیه می‌کندQuery-driven consistency choices

پیگیری مصاحبه:Interview follow-up: اگر محصول در چند دسته باشد و نظرات میلیون‌تایی شوند مدل چگونه تغییر می‌کند؟How does the model change when a product has multiple categories and millions of reviews?

آزمایشگاه تکمیلی · زمان تقریبی ۱–۲ ساعتExtended hands-on laboratory · estimated 1–2 hours

یک فروشگاه اینترنتی با محصول، دسته‌بندی، برند و نظر کاربران را در نظر بگیر. دو مدل‌سازی بنویس: یکی رابطه‌ای (با جدول‌های لازم و کلیدها) و یکی سندی (JSON). برای هرکدام بگو: چه کوئری‌هایی ساده‌اند، چه کوئری‌هایی نیاز به join یا کد اضافه دارند، و کدام را انتخاب می‌کنی — چرا؟

Take an online shop with products, categories, brands and user reviews. Write two modellings: one relational (with tables and keys) and one document-based (JSON). For each, say: which queries are simple, which need joins or extra code, and which you would pick — why?

برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمی‌رود.The full laboratory is optional depth work and is not expected within the discussion time.

ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · Amazon · SDE III interview preparation · Atlassian · engineering interview handbook

تمرین ۲ — تصمیم نرمال‌سازیExercise 2 — the normalization call

L1 · پایهL1 · Foundationsاولویت اصلیCore priority20 دقیقه بحث20 min discussion

نمونهٔ خوبی برای normalization است؛ نسخهٔ فعلی برای لول بالا باید به update propagation و cache invalidation برسد.Useful foundations; propagation failures provide the senior follow-up.

نسخهٔ بازبینی‌شده برای مصاحبهReviewed interview brief

لوگوی یک برند روی صد هزار محصول دیده می‌شود. reference، embedding و read model مشتق را مقایسه کن. هزینهٔ خواندن، fan-out تغییر، stale data و failure میان batch update را بیان کن؛ انتخابت را با حد تازگی قابل قبول توجیه کن.Compare references, embedding and a derived read model for one brand logo across 100,000 products. Analyze read cost, update fan-out, partial failure, staleness and versioning.

معیار پاسخ خوبWhat a good answer demonstrates

  • کپی را بدون مسیر update پیشنهاد نمی‌دهدCopy propagation
  • خواندن سریع را با هزینه نوشتن مبادله می‌کندRead/write cost trade-off
  • منبع حقیقت و نسخهٔ logo را مشخص می‌کندAuthority and versioning

پیگیری مصاحبه:Interview follow-up: با شکست به‌روزرسانی در نیمهٔ محصولات چه چیزی را retry می‌کنی؟What do you retry when a logo update fails halfway through the products?

آزمایشگاه تکمیلی · زمان تقریبی ۳۰–۶۰ دقیقهExtended hands-on laboratory · estimated 30–60 minutes

در همان فروشگاه، لوگوی برند در صفحهٔ محصول نمایش داده می‌شود. اگر لوگو عوض شود چه اتفاقی می‌افتد؟ دو طرح بده (کپی لوگو در هر محصول، در برابر جدول برند با ارجاع) و هر کدام را با هزینهٔ خواندن/نوشتن و ریسک ناسازگاری بسنج.

In the same shop, the brand logo shows on the product page. What happens if the logo changes? Sketch two designs (logo copied into each product vs a referenced brand table) and weigh each by read/write cost and inconsistency risk.

برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمی‌رود.The full laboratory is optional depth work and is not expected within the discussion time.

ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · Atlassian · engineering interview handbook · Debezium · outbox event router

تمرین ۳ — event sourcing برای رزرو سینماExercise 3 — event sourcing for cinema booking

L2 · SeniorL2 · Seniorاولویت اصلیCore priority40 دقیقه بحث40 min discussion

رزرو و replay سناریوی قوی‌ای است؛ نام‌گذاری event به‌تنهایی کافی نیست و concurrency قید صندلی مهم‌تر است.Reservation invariants matter more than naming events.

نسخهٔ بازبینی‌شده برای مصاحبهReviewed interview brief

رزرو سینما را با event log و سه projection وضعیت صندلی، فروش و بلیت طراحی کن. دو درخواست هم‌زمان برای یک صندلی، انقضای hold، لغو و اصلاح رزرو را توضیح بده. invariant را در write path اعمال کن و replay را از ارسال دوبارهٔ بلیت جدا کن.Design cinema reservations with a log and seat, sales and ticket projections. Handle concurrent requests, hold expiry, cancellation and corrections. Enforce seat exclusivity at the write boundary and prevent replayed external effects.

معیار پاسخ خوبWhat a good answer demonstrates

  • یک صندلی دو رزرو تأییدشده نداردSeat exclusivity
  • command و event و compensating event را جدا می‌کندCommands versus events
  • projection و اثر بیرونی idempotent هستندIdempotent projections and side effects

پیگیری مصاحبه:Interview follow-up: رویداد SeatHeld دیر برسد ولی SeatReleased قبلاً پردازش شده باشد چه می‌کنی؟What happens if SeatHeld arrives after SeatReleased has already been processed?

آزمایشگاه تکمیلی · زمان تقریبی ۲–۴ ساعتExtended hands-on laboratory · estimated 2–4 hours

برای سیستم رزرو صندلی سینما: فهرست رویدادها را بنویس (نام‌ها به زمان گذشته)، سه projection تعریف کن (وضعیت صندلی‌ها، گزارش فروش، چاپ بلیت)، و بگو اگر کاربری اشتباه رزرو کرد چطور با یک رویداد تازه اصلاحش می‌کنی. یک حالت هم پیدا کن که پردازشِ دوباره در آن خطرناک باشد.

For a cinema seat-booking system: list the events (past-tense names), define three projections (seat status, sales report, ticket printing), and say how a mistaken booking is fixed with a fresh event. Also find one case where replaying is dangerous.

برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمی‌رود.The full laboratory is optional depth work and is not expected within the discussion time.

ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · AWS · safe retries with idempotent APIs · Amazon · SDE III interview preparation