تقسیم افقی دادههاRows are split horizontally
هر شارد تمام ستونهای جدول را دارد، ولی فقط مالک بخشی از سطرهاست.Each shard has the schema's columns, but owns only part of the rows.
وقتی یک دیتابیس دیگر جوابگوی حجم دادهها، نرخ نوشتن یا تعداد درخواستها نیست، دادهها را بین چند دیتابیس مستقل توزیع میکنیم. در عوض، پیچیدگی مسیریابی، تراکنشها و نگهداری سیستم بر عهده ما میافتد. When one database can no longer handle the data volume, write rate, or request load, we distribute data across independent databases. In return, routing, transactions, and operations become our responsibility.
شاردینگ یعنی تقسیم افقی دادهها بین چند دیتابیس یا نود مستقل. هر شارد مالک بخش جداگانهای از دادههاست و معمولاً ساختار جداول (Schema) در همه شاردها یکسان است. برای رساندن هر درخواست به مقصد درست، یک روتر، پروکسی یا خود موتور دیتابیس از کلیدی به نام Shard Key استفاده میکند. Sharding is horizontal distribution of data across multiple independent databases or nodes. Each shard owns a disjoint subset of the data, usually with the same schema. A router, proxy, or the database engine uses a shard key to find the right destination.
یکی از مفاهیمی که اغلب با شارد اشتباه گرفته میشود Replica است. Replica یک کپی از همان شارد است که برای افزایش دسترسپذیری (Availability) یا توزیع بار خواندن ساخته میشود. شارد دادههای متفاوتی دارد؛ Replica دادههای یکسانی دارد. مثلاً اگر ۳ شارد داشته باشید و هرکدام ۲ Replica، در مجموع ۳ مجموعه داده دارید که هرکدام در ۳ نسخه (۱ اصلی + ۲ کپی) نگهداری میشوند. A concept often confused with a shard is a Replica. A replica is a copy of the same shard, created for higher availability or to distribute read load. A shard holds different data; a replica holds the same data. For example, 3 shards with 2 replicas each means 3 distinct datasets, each maintained in 3 copies (1 primary + 2 replicas).
مثلاً Shard A ممکن است Tenantهای ۱ تا ۱۰۰ را نگه دارد و Shard B Tenantهای ۱۰۱ تا ۲۰۰ را.Shard A may own tenants 1–100 while Shard B owns tenants 101–200.
Replica A دقیقاً همان دادههای Shard A را نگه میدارد. اگر Shard A از کار بیفتد، Replica جایگزین میشود. Replica شارد جدید نیست.Replica A holds the exact same data as Shard A. If Shard A fails, a replica can take over. A replica is not a new shard.
هر شارد تمام ستونهای جدول را دارد، ولی فقط مالک بخشی از سطرهاست.Each shard has the schema's columns, but owns only part of the rows.
برای هر درخواست باید بدانید کلید در کدام شارد است؛ وگرنه مجبورید درخواست را به همه شاردها بفرستید (Fan-out).Each request needs a destination; otherwise it may fan out to every shard.
ظرفیت، خرابی، بکاپ، مهاجرت داده و مانیتورینگ دیگر به یک نمونه محدود نیستند.Capacity, failure, backups, migrations, and monitoring are no longer single-instance concerns.
واژه «Partition» اصطلاحی عمومی برای تکهتکه کردن دادههاست. «Table Partitioning» معمولاً درون یک دیتابیس انجام میشود (موتور دیتابیس خودش دادهها را تکهتکه نگه میدارد)، ولی «Sharding» دادهها را بین چند دیتابیس یا سرور جداگانه توزیع میکند. مرز این دو در محصولات مختلف فرق دارد؛ همیشه معماری دیتابیس خودتان را بررسی کنید."Partition" is a broad term. Table partitioning usually keeps the data inside one database, while sharding distributes it across databases or nodes. Product terminology varies, so verify the actual architecture of the platform you use.
شاردینگ اولین راهحل برای کندی کوئریها نیست. قبل از آن راهکارهای سادهتری وجود دارد. جدول زیر این راهکارها را از سادهترین تا پیچیدهترین مرتب کرده و نشان میدهد هرکدام چه مشکلی را حل میکنند و چه هزینهای دارند. شاردینگ آخرین گزینه در این لیست است.Sharding is not the first tool for a slow query. Simpler options exist. The table below lists them from simplest to most complex, showing what each solves and what it costs. Sharding is the last resort.
| مرحلهStep | راهکارOption | چه مشکلی را حل میکند؟What problem it solves | هزینه و محدودیتCost and limitation |
|---|---|---|---|
| ۱ | بهینهسازی کوئری و ایندکسQuery and index optimization | کوئریهای کند، اسکنهای غیرضروری جدولSlow queries, unnecessary table scans | کم؛ بدون تغییر معماریLow; no architectural change |
| ۲ | ارتقای سختافزار (Scale-up)Scale up (bigger hardware) | کمبود CPU، RAM یا دیسک در یک سرورCPU, RAM, or storage shortage on one instance | کم؛ کوئریها تغییر نمیکنند. سقف فیزیکی و هزینهای داردLow; queries stay the same. Has a physical and cost ceiling |
| ۳ | Replica خواندنی (Read Replica)Read replicas | بار زیاد خواندن؛ درخواستهای خواندن به کپیها هدایت میشوندHigh read load; reads are routed to copies of the database | متوسط؛ فقط خواندن را مقیاس میدهد. گلوگاه نوشتن باقی میماندMedium; scales reads only. Write bottleneck remains |
| ۴ | پارتیشنبندی جدول (Table Partitioning)Table partitioning | جداول بسیار بزرگ درون یک دیتابیس؛ موتور دیتابیس خودش مدیریت میکندVery large tables inside one database; engine-managed | کم تا متوسط؛ همه دادهها هنوز روی یک سرورندLow to medium; all data still on one server |
| ۵ | شاردینگ (Sharding)Sharding | ظرفیت و توان پردازشی از یک سرور فراتر رفته؛ دادهها بین چند دیتابیس توزیع میشوندCapacity and throughput exceed one server; data is distributed across databases | زیاد؛ نیاز به مسیریابی، مدیریت تراکنش توزیعشده و عملیات پیچیدهترHigh; requires routing, distributed transaction handling, and complex operations |
این ترتیب قانون مطلق نیست، ولی یک قاعده سرانگشتی خوب است: همیشه سادهترین راهکاری که مشکل را حل میکند انتخاب کنید. شاردینگ را فقط وقتی انتخاب کنید که راهکارهای قبلی مشکل شما را حل نکنند.This order is not absolute, but it is a good rule of thumb: always pick the simplest option that solves the problem. Choose sharding only when the simpler options are insufficient.
حالا ببینیم چه نشانههایی نشان میدهد که واقعاً به شاردینگ نیاز دارید و چه نشانههایی میگویند هنوز زود است:Now let's see which signals suggest you actually need sharding, and which suggest it's too early:
Shard Key ستون یا ترکیبی از ستونهاست که مشخص میکند هر رکورد در کدام شارد ذخیره شود. یک کلید خوب هم بار را توزیع میکند و هم کوئریهای پرتکرار را تکشاردی نگه میدارد؛ هیچ کلیدی برای همه سناریوها بینقص نیست.A shard key is the field or compound key that determines where a row or entity lives. A good key balances load and keeps common queries targeted; no key is best for every workload.
یک Shard Key خوب باید سه ویژگی اصلی داشته باشد:A good shard key must have three main properties:
باید مقادیر متنوعی با توزیع نسبتاً یکنواخت داشته باشد. کلیدی مثل Status که فقط سه مقدار دارد برای توزیع بار مناسب نیست.It should have enough values and reasonably even distribution; a three-value field such as Status is usually a poor distribution key.
باید در کوئریهای پرکاربرد حضور داشته باشد تا روتر مجبور به Broadcast به همه شاردها نشود.It should appear in important queries so the router can avoid broadcasting to every shard.
تا حد امکان نباید تغییر کند؛ تغییر کلید یعنی جابهجایی داده و حفظ یکپارچگی حین انتقال.It should rarely change; changing it can mean moving an entity and coordinating consistency during the move.
اگر تقریباً هر درخواستی TenantId دارد، شارد کردن بر اساس Tenant میتواند بیشتر کوئریها را تکشاردی نگه دارد. اما یک Tenant خیلی بزرگ ممکن است Hot Shard ایجاد کند و کلید مرکب (Compound Key) ضروری شود.If almost every request has a TenantId, tenant-based sharding can keep most queries single-shard. But one very large tenant can create a hot shard; large tenants may need a compound key or special treatment.
کلیدی که دادهها را عالی پخش میکند ولی در کوئریها نیست، فقط باعث Fan-out میشود. همراه با تنوع مقادیر، باید الگوهای دسترسی و JOINهای آینده را هم در نظر بگیرید.A key that distributes evenly but never appears in queries only creates fan-out. Evaluate workload, joins, entity size, and future operations alongside cardinality.
این نامها دستهبندیهای رایجاند، نه سینتکسی یکسان برای همه دیتابیسها. تفاوت اصلی در نحوه تعیین مقصد هر رکورد، امکان اجرای کوئری بازهای و سادگی جابهجایی دادههاست.These are common families, not a universal syntax. The key differences are how each record's destination is determined, whether range queries can be targeted, and how easy it is to move data.
در هر تب، یک مجموعه داده را با مدل توزیع متفاوت ببینید. هدف درک trade-offهاست، نه نمایش سینتکس یک محصول خاص.Switch tabs to see the same data under different placement models. These diagrams explain trade-offs; they are not product-specific syntax or guarantees.
مقادیر نزدیک به هم در یک شارد قرار میگیرند. کوئریهای بازهای میتوانند هدفمند باشند، ولی اگر بار روی یک بازه خاص متمرکز شود، Hotspot ایجاد میشود.Nearby values stay together. Range queries can be targeted, but a busy recent range or a popular value can create skew and a hot shard.
هش معمولاً توزیع یکنواختتری میدهد و از Hotspot روی کلیدهای متوالی جلوگیری میکند؛ ولی ترتیب از بین میرود و کوئریهای بازهای به Fan-out نیاز پیدا میکنند.Hashing often distributes monotonically increasing keys more evenly; it sacrifices locality, so range queries on the hashed key are more likely to fan out.
یک جدول یا نقشه صریح مشخص میکند هر کلید در کدام شارد است. جابهجایی دادهها انعطافپذیرتر است، ولی خود این نقشه به وابستگی حیاتی تبدیل میشود و باید حتی در Failover یکپارچه بماند.An explicit map says where each key or range lives. Moves can be flexible, but the directory becomes a critical dependency that must stay consistent through moves and failovers.
فارغ از نوع استراتژی، دو نکته مهم وجود دارد:Regardless of the strategy, two important points apply:
اگر چند جدول (مثلاً Orders و OrderItems) همیشه با هم JOIN میشوند، بهتر است هر دو را بر اساس TenantId توزیع کنید. اینطوری دادههای یک Tenant در همه جداول روی همان شارد قرار میگیرند و JOIN بدون رفتن به شاردهای دیگر انجام میشود. به این کار Co-location (هممکانسازی) میگویند.If multiple tables (e.g. Orders and OrderItems) are always joined together, distribute both by TenantId. This way, one tenant's data across all tables lives on the same shard, and joins complete without crossing shard boundaries. This is called co-location.
حتی اگر تعداد رکوردها بین شاردها مساوی باشد، بار ترافیک ممکن است مساوی نباشد. مثلاً یک Tenant کوچک ممکن است ۸۰٪ کل درخواستها را تولید کند. برای تشخیص Hotspot، بهجای شمردن سطرها، CPU، I/O و Latency هر شارد را اندازه بگیرید.Even if row counts are equal across shards, traffic load may not be. A small tenant might generate 80% of all requests. To detect hotspots, measure each shard's CPU, I/O, and latency instead of just counting rows.
وقتی کوئریای اجرا میشود، روتر باید تصمیم بگیرد آن را به کدام شارد(ها) بفرستد. بسته به اینکه Shard Key در کوئری هست یا نه، یکی از این الگوها اتفاق میافتد:When a query runs, the router must decide which shard(s) to send it to. Depending on whether the shard key is in the query, one of these patterns occurs:
کوئری شامل Shard Key است. روتر دقیقاً میداند به کدام شارد(ها) برود. سریعترین و ارزانترین حالت.The query includes the shard key. The router knows exactly which shard(s) to hit. The fastest and cheapest pattern.
کوئری شامل Shard Key نیست. روتر مجبور است آن را به همه شاردها بفرستد، جوابها را جمعآوری (Gather) و ادغام (Merge) کند. زمان پاسخ وابسته به کندترین شارد است.The query does not include the shard key. The router must send it to all shards, gather and merge the results. Response time depends on the slowest shard.
در دموی زیر، سه سناریو را امتحان کنید و ببینید هر کوئری چند شارد را درگیر میکند. اعداد Latency صرفاً آموزشیاند.In the demo below, try three scenarios and see how many shards each query touches. Latency numbers are educational only.
در Fan-out، زمان پاسخ وابسته به کندترین شارد بهعلاوه هزینه شبکه و Merge است. حتی اگر درخواستها بهصورت موازی ارسال شوند، یک شارد کند میتواند Latency کل درخواست را بالا ببرد. موازیسازی این هزینه را حذف نمیکند، فقط کمترش میکند.For fan-out, response time is bounded by the slowest shard plus network and merge cost. Even with parallel requests, one slow shard can raise the entire request's latency. Parallelism reduces but does not eliminate this cost.
در یک دیتابیس معمولی، JOIN بین جداول و تراکنشهای اتمیک کاملاً طبیعیاند. ولی وقتی دادهها بین چند شارد پخش شدهاند، دو مشکل جدید پیش میآید:In a regular database, joins and atomic transactions are natural. But when data is spread across shards, two new problems arise:
وقتی تمام دادههای مورد نیاز یک کوئری یا تراکنش روی یک شارد هستند، همه چیز مثل یک دیتابیس عادی کار میکند: JOIN، تراکنش اتمیک و Constraintها همه در دسترساند. این بهترین حالت است.When all data needed by a query or transaction is on one shard, everything works like a normal database: joins, atomic transactions, and constraints are all available. This is the best case.
وقتی دادهها روی چند شارد مختلف پخشاند، JOIN مستقیم ممکن نیست. تراکنش اتمیک هم پیچیده میشود چون باید چند دیتابیس مستقل همزمان Commit کنند. خرابی شبکه بین شاردها ممکن است یکی Commit کند و دیگری نکند.When data is on different shards, direct joins are not possible. Atomic transactions become complex because multiple independent databases must commit together. A network failure between shards may cause one to commit while another does not.
هدف طراحی این است که تا حد ممکن از عملیات بینشاردی اجتناب کنید:The design goal is to avoid cross-shard operations as much as possible:
| نوع عملیاتOperation type | مثالExample | هزینهCost | راهحل طراحیDesign solution |
|---|---|---|---|
| خواندن/نوشتن درونشاردیSingle-shard read/write | خواندن سفارشهای TenantId = 42Read orders for TenantId = 42 | کم؛ مثل دیتابیس عادیLow; like a normal database | Shard Key را در API الزامی کنیدRequire the shard key in the API |
| JOIN درونشاردیCo-located join | JOIN بین Orders و OrderItems برای یک TenantJoin Orders with OrderItems for one tenant | کم؛ اگر هر دو جدول با کلید مشترک توزیع شده باشندLow; if both tables share the distribution key | جداول مرتبط را با کلید مشترک توزیع کنیدDistribute related tables with the same key |
| Scatter/GatherScatter/gather | جمع کل فروش همه TenantهاTotal sales across all tenants | متوسط تا زیاد؛ همه شاردها درگیرندMedium to high; all shards involved | Timeout بگذارید و نتایج را موازی جمع کنیدSet timeouts and gather results in parallel |
| تراکنش بینشاردیCross-shard transaction | انتقال موجودی بین دو Tenant روی شاردهای مختلفTransfer balance between two tenants on different shards | زیاد؛ هماهنگی و Rollback دشوارHigh; coordination and rollback are hard | بازطراحی با Saga یا 2PCRedesign with Saga or 2PC |
در EF Core، هر DbContext معمولاً به یک کانکشن و در نتیجه یک شارد متصل است. اگر بخواهید از یک Context برای چند شارد استفاده کنید، ممکن است ندانید داده از کجا میآید. روش درست این است: ابتدا شارد مقصد را پیدا کنید، سپس با کانکشن همان شارد یک Context بسازید. مثال زیر این الگو را نشان میدهد:In EF Core, each DbContext is normally connected to one connection and therefore one shard. If you try to use one context for multiple shards, you may lose track of where data comes from. The correct approach: first find the target shard, then create a context with that shard's connection. The example below shows this pattern:
// 1. Find which shard owns this tenant
var shard = await shardResolver.ResolveAsync(tenantId, cancellationToken);
// 2. Create a DbContext connected to that specific shard
await using var db = await contextFactory.CreateAsync(
shard.ConnectionString,
cancellationToken);
// 3. Query — this only touches the target shard
var orders = await db.Orders
.Where(x => x.TenantId == tenantId)
.ToListAsync(cancellationToken);
// 4. Write — transaction stays on one shard
db.Orders.Add(new Order { TenantId = tenantId, Total = 1200 });
await db.SaveChangesAsync(cancellationToken);
کد بالا مفهومی است. در محیط عملیاتی، ShardResolver باید کانکشناسترینگها را از Secret Manager بگیرد، Timeout و Retry داشته باشد، و حین Rebalancing از نقشههای قدیمی جلوگیری کند. هرگز کانکشن را بر اساس ورودی کاربر بدون اعتبارسنجی باز نکنید.The code above is conceptual. In production, the ShardResolver should get connection strings from a secret manager, have timeouts and retries, and prevent stale maps during rebalancing. Never open connections based on unvalidated user input.
اضافه کردن شارد جدید فقط ساخت یک دیتابیس نیست. باید اسکیما را Deploy کنید، نقشه مسیریابی را آپدیت کنید، بخشی از دادهها را منتقل کنید، درخواستهای همزمان حین جابهجایی را مدیریت کنید و صحت مسیرها را تأیید کنید.Adding a shard is more than creating a database. You must deploy the schema, update the map, move data, handle concurrent requests during the move, and verify counts and routes afterward.
این دمو فقط مفهوم Rebalancing را نشان میدهد: یک بازه از Shard A به Shard D منتقل میشود و نقشه باید همزمان با دادهها بهروز شود. در واقعیت، Locking، کپی داده، Cut-over و مدیریت خطا جزئیات حیاتیاند.This only illustrates rebalancing: one range moves from Shard A to Shard D, and the map must change consistently with the data. A real product must handle locking, copying, cut-over, and failures.
نقشه باید منبع قابل اعتمادی برای ارتباط کلید و مقصد باشد. کش کردن آن مفید است، ولی Invalidation، نسخهبندی و رفتار حین جابهجایی را از قبل تعریف کنید.The map must be a trusted source for the key-to-destination relationship. Caching helps, but define invalidation, versions, and behavior during moves.
مهاجرت دیتابیس باید روی تمام شاردها به نسخه یکسان برسد. استقرار مرحلهای و سازگاری رو به عقب (Backward Compatibility) امنتر از Deploy یکبارهاند.Migrations must converge on the same version across shards. Staged rollout and backward compatibility are safer than a blind one-shot deploy.
مایگریشنهای EF Core را برای هر شارد جداگانه و کنترلشده اجرا کنید. یک اسکریپت یا Pipeline بسازید که روی همه شاردها بهترتیب یا موازی Migration اجرا کند و نتیجه هرکدام را گزارش دهد. قبل از هر Deploy، سازگاری رو به عقب اسکیما را بررسی کنید.Run EF Core migrations for each shard separately through a controlled pipeline. Build a script that runs migrations on all shards and reports results. Always verify schema backward compatibility before deploying.
اگر پاسخ سوالی را نمیدانید، به همان بخش برگردید. هدف حفظ کردن نام ابزارها نیست؛ باید بتوانید هزینه و مسیر یک درخواست را توضیح دهید.If you cannot answer one, return to that section. The goal is not memorizing product names; it is explaining the cost and path of a request.