staff-engineering-skills-sharding

작성자: triggerdotdev

데이터베이스 샤딩에 관한 올바른 결정을 내립니다. 언제 샤딩해야 하는지, 언제 하지 말아야 하는지, 그리고 샤딩할 때 무엇이 깨지는지에 대해 다룹니다. 데이터베이스 아키텍처를 설계하거나 선택할 때 사용하세요.

npx skills add https://github.com/triggerdotdev/staff-engineering-skills --skill staff-engineering-skills-sharding

Sharding Trap

Sharding is nearly impossible to undo and fundamentally changes what your code can do. Before sharding, ask: have you exhausted every simpler alternative?

Do You Actually Need to Shard?

Almost certainly not. Work through this checklist first:

StepSolutionHandles
1Indexes -- run EXPLAIN ANALYZE on slow queriesMissing indexes cause 90% of "database is slow"
2Query optimization -- fix N+1 queries, unnecessary JOINs, full scansBad queries, not database limits
3Connection pooling -- PgBouncer or built-in poolingConnection exhaustion
4Caching -- Redis for hot data, query result cachesRead throughput
5Read replicas -- route reads to replicasRead scaling
6Table partitioning -- partition by date range within one databaseLarge table performance, transparent to app code
7Vertical scaling -- bigger machine (64-core, 256GB RAM)Everything, up to a point

Only if ALL of these are insufficient should you consider sharding. A single PostgreSQL instance with proper indexing and read replicas handles far more than most people expect. If you have less than 1TB of data or fewer than 10,000 writes per second, you almost certainly don't need to shard.

What Sharding Breaks

Once data is distributed across shards, these operations become expensive or impossible:

Before sharding (trivial)After sharding (painful)
JOIN users ON orders.user_id = users.idCross-shard join: fetch from both shards, join in memory
BEGIN; UPDATE orders; UPDATE inventory; COMMIT;Cross-shard transaction: 2PC or saga pattern
SELECT COUNT(*) FROM ordersScatter to all shards, aggregate results
CREATE UNIQUE INDEX ON users(email)Cross-shard uniqueness: separate lookup table
SELECT * FROM orders ORDER BY created_at LIMIT 20Fetch 20 from each shard, merge-sort

Every feature you build must now answer: "which shard is this data on?"

Detection: Sharding-Unsafe Code

If the system is sharded or may be sharded, stop and reassess if you see:

  1. JOINs across entity types -- orders JOIN products JOIN users. These tables may be on different shards. Each join may need to become a separate query + application-level join.

  2. Multi-entity transactions -- BEGIN; update order; update inventory; update user; COMMIT;. If these entities are on different shards, this transaction cannot work.

  3. Global unique constraints -- UNIQUE(email) only works within a single database. Cross-shard uniqueness requires a coordination layer.

  4. Unscoped aggregations -- SELECT COUNT(*) FROM orders. Without a shard key in the WHERE clause, this hits every shard.

  5. ORDER BY ... LIMIT without shard key -- requires fetching from all shards and merge-sorting.

Shard Key Selection

The shard key determines everything. Get it wrong and sharding makes things worse.

The shard key must be in your most common query's WHERE clause. If 80% of queries are scoped to a tenant, shard by tenant. If 80% are scoped to a user, shard by user.

Shard key choiceGood whenBad when
Tenant/org IDMulti-tenant SaaS, queries scoped to tenantOne tenant is 1000x bigger than others (hot shard)
User IDUser-scoped apps (social, messaging)Queries need cross-user views (analytics, search)
Hash of primary keyEven distribution neededRange queries on the key (date ranges, alphabetical)
Geographic regionLatency-sensitive, data residency requirementsUneven population distribution

Red flags in shard key selection:

  • Low cardinality (country code: US gets 50% of traffic)
  • Skewed distribution (first letter of name: "S" has 4x more than "Q")
  • Doesn't appear in the most frequent query's WHERE clause
  • Changes over time (user can move between tenants)

Patterns

Tenant-based sharding (most common for SaaS)

function getShardForTenant(tenantId: string): DatabaseConnection {
  const shardIndex = consistentHash(tenantId, shardCount);
  return shardConnections[shardIndex];
}

async function getOrders(tenantId: string, filters: OrderFilters) {
  const shard = getShardForTenant(tenantId);
  // Single-shard query -- no scatter-gather
  return shard.query(
    `SELECT * FROM orders WHERE tenant_id = $1 AND status = $2
     ORDER BY created_at DESC LIMIT $3`,
    [tenantId, filters.status, filters.limit]
  );
}

Works because almost all SaaS operations are scoped to one tenant. Cross-tenant analytics go to a separate data warehouse fed by event streaming.

Handling cross-shard queries with a read store

// Writes go to the shard
async function createOrder(tenantId: string, data: OrderInput) {
  const shard = getShardForTenant(tenantId);
  const order = await shard.insert("orders", { tenantId, ...data });

  // Async: replicate to a global read store for cross-shard queries
  await eventBus.publish("order.created", { orderId: order.id, tenantId, ...data });
  return order;
}

// Cross-shard queries hit the read store (eventually consistent)
async function globalOrderStats() {
  return analyticsDb.query(`
    SELECT DATE_TRUNC('day', created_at) as day, COUNT(*), SUM(amount)
    FROM orders_read_model GROUP BY day ORDER BY day DESC
  `);
}

Accept that cross-shard queries need a different store. The read store is eventually consistent but avoids scatter-gather.

Hot tenant isolation

const DEDICATED_TENANTS = new Map([
  ["acme-corp", dedicatedShardConnection],
  ["megacorp", dedicatedShardConnection2],
]);

function getShardForTenant(tenantId: string): DatabaseConnection {
  // Large tenants get their own shard
  const dedicated = DEDICATED_TENANTS.get(tenantId);
  if (dedicated) return dedicated;

  // Everyone else shares the pool
  const shardIndex = consistentHash(tenantId, sharedShardCount);
  return sharedShardConnections[shardIndex];
}

When one tenant generates 40% of traffic, hash-based distribution doesn't help. Move them to dedicated infrastructure.

Anti-Patterns

// Premature: "We might have millions of users someday"
// You have 50,000 users and 5GB of data. A single PostgreSQL instance
// on a $600/month machine handles 100x this. Sharding infrastructure
// will cost more in engineering time than 5 years of vertical scaling.

// Wrong shard key: sharded by user_id but dashboard queries by org_id
// Every dashboard query must scatter to ALL shards and aggregate
const results = await Promise.all(
  shards.map(shard =>
    shard.query(`SELECT count(*), sum(amount) FROM orders WHERE org_id = $1`, [orgId])
  )
);
// If the primary access pattern is per-org, shard by org, not user.

// Assumes single database: JOIN across entity types
const orderDetails = await db.query(`
  SELECT o.*, p.name, u.email
  FROM orders o
  JOIN products p ON o.product_id = p.id
  JOIN users u ON o.user_id = u.id
  WHERE o.id = $1
`, [orderId]);
// If orders, products, and users are on different shards, this doesn't work.

Related Traps

  • Race Conditions -- SELECT FOR UPDATE doesn't work across shards. Distributed locking adds latency and failure modes. Cross-shard operations need saga patterns or optimistic concurrency.
  • Idempotency -- the idempotency key and the data it protects must be on the same shard. Otherwise you can't atomically check for duplicates and perform the operation.
  • Hot Partitions -- even with sharding, one shard can receive disproportionate traffic. Shard key distribution determines whether you've solved the problem or just renamed it.
  • Consistency Models -- within a shard: strong consistency. Across shards: eventual consistency at best. Cross-shard queries see data at different points in time.
  • Denormalization -- cross-shard queries often require maintaining denormalized read stores. This creates consistency obligations (see denormalization trap).

triggerdotdev의 다른 스킬

trigger-dev-tasks
triggerdotdev
Trigger.dev 백그라운드 작업과 워크플로우를 작성, 설계 또는 최적화할 때 이 스킬을 사용하세요. 여기에는 안정적인 비동기 작업 생성, AI 구현 등이 포함됩니다.
official
trigger-authoring-chat-agent
triggerdotdev
@trigger.dev/sdk/ai의 chat.agent를 사용하여 지속형 AI 채팅 에이전트를 작성하고 실행합니다: 턴별 실행 루프, ...chat.toStreamTextOptions()를 반드시 펼쳐야 하는 이유
official
trigger-agents
triggerdotdev
Trigger.dev를 사용한 AI 에이전트 패턴 - 오케스트레이션, 병렬화, 라우팅, 평가자-최적화기, 인간-인-더-루프. LLM 기반 작업을 구축할 때 사용합니다…
official
trigger-config
triggerdotdev
Trigger.dev 프로젝트를 trigger.config.ts로 구성합니다. Prisma, Playwright, FFmpeg, Python용 빌드 확장을 설정하거나 배포를 사용자 지정할 때 사용합니다…
official
trigger-cost-savings
triggerdotdev
Trigger.dev 작업, 일정 및 실행을 분석하여 비용 최적화 기회를 찾습니다. 지출 절감, 비용 최적화, 사용량 감사, 적정 규모 조정 등을 요청받을 때 사용하세요.
official
trigger-realtime
triggerdotdev
Trigger.dev 작업 실행을 프론트엔드와 백엔드에서 실시간으로 구독합니다. 진행률 표시기, 라이브 대시보드, 스트리밍 AI/LLM 응답 등을 구축할 때 사용하세요.
official
trigger-setup
triggerdotdev
프로젝트에 Trigger.dev를 설정합니다. Trigger.dev를 처음 추가하거나, trigger.config.ts를 생성하거나, trigger 디렉토리를 초기화할 때 사용하세요.
official
trigger-tasks
triggerdotdev
AI 에이전트, 워크플로우 및 지속적인 백그라운드 작업을 Trigger.dev로 구축하세요. 작업 생성, 작업 트리거, 재시도 처리, 크론 작업 예약 시 사용하거나...
official