Смена базы данных в работающей системе -- одна из самых дорогих операций. Правильный выбор на старте экономит месяцы работы. На System Design интервью умение обосновать выбор БД -- ключевой навык.
Критерии выбора
1. Модель данных
Вопрос
Если "да"
Рекомендация
Данные строго структурированы?
Таблицы со связями
PostgreSQL/MySQL
Данные имеют переменную структуру?
JSON-документы
MongoDB/PostgreSQL JSONB
Данные -- пары ключ-значение?
Простой lookup
Redis/DynamoDB
Много связей между сущностями?
Графовые запросы
Neo4j/ArangoDB
Данные с временными метками?
Временные ряды
TimescaleDB/InfluxDB
2. Паттерны доступа (Access Patterns)
<?php
declare(strict_types=1);
/**
* Different access patterns require different databases
*/
final class AccessPatternAnalysis
{
/**
* Pattern 1: Point reads by primary key (any DB works well)
* Best: Redis > DynamoDB > PostgreSQL
*/
public function getUserById(\PDO $db, string $userId): ?array
{
$stmt = $db->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $userId]);
return $stmt->fetch(\PDO::FETCH_ASSOC) ?: null;
}
/**
* Pattern 2: Complex queries with JOINs (relational DB)
* Best: PostgreSQL > MySQL > CockroachDB
*/
public function getOrdersWithDetails(\PDO $db, string $userId): array
{
$stmt = $db->prepare(<<<SQL
SELECT o.*, p.name, oi.quantity
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.user_id = :user_id
ORDER BY o.created_at DESC
LIMIT 50
SQL);
$stmt->execute(['user_id' => $userId]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
/**
* Pattern 3: Full-text search
* Best: Elasticsearch > Meilisearch > PostgreSQL (tsvector)
*/
public function searchProducts(\PDO $db, string $query): array
{
$stmt = $db->prepare(<<<SQL
SELECT id, name, description,
ts_rank(search_vector, plainto_tsquery(:query)) as rank
FROM products
WHERE search_vector @@ plainto_tsquery(:query)
ORDER BY rank DESC
LIMIT 20
SQL);
$stmt->execute(['query' => $query]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
/**
* Pattern 4: Write-heavy with time-series
* Best: TimescaleDB > InfluxDB > Cassandra
*/
public function insertMetric(\PDO $db, array $metric): void
{
$stmt = $db->prepare(<<<SQL
INSERT INTO metrics (time, host, name, value)
VALUES (:time, :host, :name, :value)
SQL);
$stmt->execute($metric);
}
/**
* Pattern 5: Aggregations on large datasets (OLAP)
* Best: ClickHouse > BigQuery > PostgreSQL
*/
public function getRevenueByCategory(\PDO $db, int $year): array
{
$stmt = $db->prepare(<<<SQL
SELECT category, SUM(amount) as revenue, COUNT(*) as orders
FROM sales
WHERE EXTRACT(YEAR FROM created_at) = :year
GROUP BY category
ORDER BY revenue DESC
SQL);
$stmt->execute(['year' => $year]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
}
package selection
import (
"context"
"database/sql"
"fmt"
)
// AccessPatternAnalysis demonstrates different DB access patterns.
type AccessPatternAnalysis struct {
db *sql.DB
}
// Pattern 1: Point reads by primary key (any DB works well).
func (a *AccessPatternAnalysis) GetUserByID(ctx context.Context, userID string) (map[string]any, error) {
var id, name, email string
err := a.db.QueryRowContext(ctx, `SELECT id, name, email FROM users WHERE id = $1`, userID).
Scan(&id, &name, &email)
if err != nil {
return nil, fmt.Errorf("get user: %w", err)
}
return map[string]any{"id": id, "name": name, "email": email}, nil
}
// Pattern 2: Complex queries with JOINs (relational DB).
func (a *AccessPatternAnalysis) GetOrdersWithDetails(ctx context.Context, userID string) ([]map[string]any, error) {
rows, err := a.db.QueryContext(ctx, `
SELECT o.id, p.name, oi.quantity
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.user_id = $1 ORDER BY o.created_at DESC LIMIT 50`, userID)
if err != nil {
return nil, fmt.Errorf("get orders: %w", err)
}
defer rows.Close()
var results []map[string]any
for rows.Next() {
var orderID, productName string
var qty int
rows.Scan(&orderID, &productName, &qty)
results = append(results, map[string]any{"order_id": orderID, "product": productName, "qty": qty})
}
return results, rows.Err()
}
// Pattern 3: Full-text search (PostgreSQL tsvector).
func (a *AccessPatternAnalysis) SearchProducts(ctx context.Context, query string) ([]map[string]any, error) {
rows, err := a.db.QueryContext(ctx, `
SELECT id, name, ts_rank(search_vector, plainto_tsquery($1)) as rank
FROM products WHERE search_vector @@ plainto_tsquery($1)
ORDER BY rank DESC LIMIT 20`, query)
if err != nil {
return nil, fmt.Errorf("search: %w", err)
}
defer rows.Close()
var results []map[string]any
for rows.Next() {
var id, name string
var rank float64
rows.Scan(&id, &name, &rank)
results = append(results, map[string]any{"id": id, "name": name, "rank": rank})
}
return results, rows.Err()
}
using Npgsql;
public sealed record UserRow(string Id, string Name, string Email);
public sealed record OrderDetailRow(string OrderId, string ProductName, int Quantity);
public sealed record ProductHit(string Id, string Name, double Rank);
public sealed record MetricRow(DateTimeOffset Time, string Host, string Name, double Value);
public sealed record CategoryRevenue(string Category, decimal Revenue, long Orders);
// AccessPatternAnalysis demonstrates different DB access patterns.
public sealed class AccessPatternAnalysis(NpgsqlDataSource dataSource)
{
// Pattern 1: point reads by primary key (any DB works well)
// Best: Redis > DynamoDB > PostgreSQL
public async Task<UserRow?> GetUserByIdAsync(string userId, CancellationToken ct = default)
{
await using var cmd = dataSource.CreateCommand(
"SELECT id, name, email FROM users WHERE id = @id");
cmd.Parameters.AddWithValue("id", userId);
await using var reader = await cmd.ExecuteReaderAsync(ct);
return await reader.ReadAsync(ct)
? new UserRow(reader.GetString(0), reader.GetString(1), reader.GetString(2))
: null;
}
// Pattern 2: complex queries with JOINs (relational DB)
// Best: PostgreSQL > MySQL > CockroachDB
public async Task<IReadOnlyList<OrderDetailRow>> GetOrdersWithDetailsAsync(
string userId,
CancellationToken ct = default)
{
const string sql = """
SELECT o.id, p.name, oi.quantity
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.user_id = @userId
ORDER BY o.created_at DESC
LIMIT 50
""";
await using var cmd = dataSource.CreateCommand(sql);
cmd.Parameters.AddWithValue("userId", userId);
await using var reader = await cmd.ExecuteReaderAsync(ct);
var rows = new List<OrderDetailRow>();
while (await reader.ReadAsync(ct))
{
rows.Add(new OrderDetailRow(reader.GetString(0), reader.GetString(1), reader.GetInt32(2)));
}
return rows;
}
// Pattern 3: full-text search
// Best: Elasticsearch > Meilisearch > PostgreSQL (tsvector)
public async Task<IReadOnlyList<ProductHit>> SearchProductsAsync(
string query,
CancellationToken ct = default)
{
const string sql = """
SELECT id, name, ts_rank(search_vector, plainto_tsquery(@query)) AS rank
FROM products
WHERE search_vector @@ plainto_tsquery(@query)
ORDER BY rank DESC
LIMIT 20
""";
await using var cmd = dataSource.CreateCommand(sql);
cmd.Parameters.AddWithValue("query", query);
await using var reader = await cmd.ExecuteReaderAsync(ct);
var hits = new List<ProductHit>();
while (await reader.ReadAsync(ct))
{
hits.Add(new ProductHit(reader.GetString(0), reader.GetString(1), reader.GetFloat(2)));
}
return hits;
}
// Pattern 4: write-heavy with time-series
// Best: TimescaleDB > InfluxDB > Cassandra
public async Task InsertMetricAsync(MetricRow metric, CancellationToken ct = default)
{
await using var cmd = dataSource.CreateCommand(
"INSERT INTO metrics (time, host, name, value) VALUES (@time, @host, @name, @value)");
cmd.Parameters.AddWithValue("time", metric.Time);
cmd.Parameters.AddWithValue("host", metric.Host);
cmd.Parameters.AddWithValue("name", metric.Name);
cmd.Parameters.AddWithValue("value", metric.Value);
await cmd.ExecuteNonQueryAsync(ct);
}
// Pattern 5: aggregations on large datasets (OLAP)
// Best: ClickHouse > BigQuery > PostgreSQL
public async Task<IReadOnlyList<CategoryRevenue>> GetRevenueByCategoryAsync(
int year,
CancellationToken ct = default)
{
const string sql = """
SELECT category, SUM(amount) AS revenue, COUNT(*) AS orders
FROM sales
WHERE EXTRACT(YEAR FROM created_at) = @year
GROUP BY category
ORDER BY revenue DESC
""";
await using var cmd = dataSource.CreateCommand(sql);
cmd.Parameters.AddWithValue("year", year);
await using var reader = await cmd.ExecuteReaderAsync(ct);
var rows = new List<CategoryRevenue>();
while (await reader.ReadAsync(ct))
{
rows.Add(new CategoryRevenue(reader.GetString(0), reader.GetDecimal(1), reader.GetInt64(2)));
}
return rows;
}
}
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from psycopg.rows import class_row
from psycopg_pool import AsyncConnectionPool
@dataclass(frozen=True, slots=True)
class UserRow:
id: str
name: str
email: str
@dataclass(frozen=True, slots=True)
class OrderDetailRow:
order_id: str
product_name: str
quantity: int
@dataclass(frozen=True, slots=True)
class ProductHit:
id: str
name: str
rank: float
@dataclass(frozen=True, slots=True)
class MetricRow:
time: datetime
host: str
name: str
value: float
@dataclass(frozen=True, slots=True)
class CategoryRevenue:
category: str
revenue: Decimal
orders: int
class AccessPatternAnalysis:
"""Different access patterns require different databases."""
def __init__(self, pool: AsyncConnectionPool) -> None:
self._pool = pool
async def get_user_by_id(self, user_id: str) -> UserRow | None:
"""Pattern 1: point reads by primary key (any DB works well).
Best: Redis > DynamoDB > PostgreSQL
"""
async with self._pool.connection() as conn:
async with conn.cursor(row_factory=class_row(UserRow)) as cur:
await cur.execute(
"SELECT id, name, email FROM users WHERE id = %s", (user_id,)
)
return await cur.fetchone()
async def get_orders_with_details(self, user_id: str) -> list[OrderDetailRow]:
"""Pattern 2: complex queries with JOINs (relational DB).
Best: PostgreSQL > MySQL > CockroachDB
"""
sql = """
SELECT o.id AS order_id, p.name AS product_name, oi.quantity
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.user_id = %s
ORDER BY o.created_at DESC
LIMIT 50
"""
async with self._pool.connection() as conn:
async with conn.cursor(row_factory=class_row(OrderDetailRow)) as cur:
await cur.execute(sql, (user_id,))
return await cur.fetchall()
async def search_products(self, query: str) -> list[ProductHit]:
"""Pattern 3: full-text search.
Best: Elasticsearch > Meilisearch > PostgreSQL (tsvector)
"""
sql = """
SELECT id, name, ts_rank(search_vector, plainto_tsquery(%(q)s)) AS rank
FROM products
WHERE search_vector @@ plainto_tsquery(%(q)s)
ORDER BY rank DESC
LIMIT 20
"""
async with self._pool.connection() as conn:
async with conn.cursor(row_factory=class_row(ProductHit)) as cur:
await cur.execute(sql, {"q": query})
return await cur.fetchall()
async def insert_metric(self, metric: MetricRow) -> None:
"""Pattern 4: write-heavy with time-series.
Best: TimescaleDB > InfluxDB > Cassandra
"""
async with self._pool.connection() as conn, conn.cursor() as cur:
await cur.execute(
"INSERT INTO metrics (time, host, name, value) VALUES (%s, %s, %s, %s)",
(metric.time, metric.host, metric.name, metric.value),
)
async def get_revenue_by_category(self, year: int) -> list[CategoryRevenue]:
"""Pattern 5: aggregations on large datasets (OLAP).
Best: ClickHouse > BigQuery > PostgreSQL
"""
sql = """
SELECT category, SUM(amount) AS revenue, COUNT(*) AS orders
FROM sales
WHERE EXTRACT(YEAR FROM created_at) = %s
GROUP BY category
ORDER BY revenue DESC
"""
async with self._pool.connection() as conn:
async with conn.cursor(row_factory=class_row(CategoryRevenue)) as cur:
await cur.execute(sql, (year,))
return await cur.fetchall()
# Плохо: выбираем MongoDB потому что "модно"
"Давайте используем MongoDB для нашего интернет-магазина!"
# Факт: интернет-магазин = заказы + платежи = транзакции = PostgreSQL
2. One Size Fits All
# Плохо: PostgreSQL для всего
# Факт: для поиска по каталогу лучше Elasticsearch,
# для кэша -- Redis, для метрик -- TimescaleDB
3. Premature Optimization
# Плохо: сразу ставим Cassandra для "масштабирования"
# Факт: PostgreSQL на одном сервере обслужит миллионы пользователей.
# Cassandra нужна при > 100K writes/sec или multi-region
Практический шаблон для интервью
<?php
declare(strict_types=1);
/**
* Template: justify database choice on System Design interview
*/
final class DatabaseSelectionTemplate
{
/**
* Step 1: Identify data characteristics
*/
public function analyzeData(): array
{
return [
'structure' => 'structured', // structured / semi-structured / unstructured
'relationships' => 'many JOINs', // none / few / many JOINs
'schema_stability' => 'stable', // stable / evolving / unknown
'size' => '100 GB', // estimate
'growth_rate' => '1 GB/day', // estimate
];
}
/**
* Step 2: Identify access patterns
*/
public function analyzeAccessPatterns(): array
{
return [
'read_write_ratio' => '80:20', // read-heavy / write-heavy / balanced
'query_complexity' => 'complex', // point lookup / range / complex / aggregation
'latency_requirement' => 'p99 < 50ms',
'throughput' => '10K requests/sec',
'consistency' => 'strong', // strong / eventual / causal
];
}
/**
* Step 3: Consider non-functional requirements
*/
public function analyzeNFR(): array
{
return [
'availability' => '99.9%',
'durability' => 'critical',
'geographic' => 'single region',
'compliance' => 'GDPR',
'budget' => 'moderate',
];
}
/**
* Step 4: Make decision with justification
*
* "For this system I would choose PostgreSQL because:
* 1. Our data is highly structured with complex relationships
* 2. We need strong consistency for financial transactions
* 3. Read-heavy workload works well with read replicas
* 4. JSONB covers our semi-structured data needs
* 5. Team has PostgreSQL expertise
* 6. 100GB fits well in a single instance
*
* I would add Redis for session cache and rate limiting."
*/
public function makeDecision(): string
{
return 'PostgreSQL + Redis';
}
}
package selection
// DataCharacteristics describes the data model properties.
type DataCharacteristics struct {
Structure string // structured / semi-structured / unstructured
Relationships string // none / few / many JOINs
SchemaStability string // stable / evolving / unknown
Size string // estimate, e.g., "100 GB"
GrowthRate string // e.g., "1 GB/day"
}
// AccessPatterns describes how data is accessed.
type AccessPatterns struct {
ReadWriteRatio string // e.g., "80:20"
QueryComplexity string // point lookup / range / complex / aggregation
LatencyRequirement string // e.g., "p99 < 50ms"
Throughput string // e.g., "10K requests/sec"
Consistency string // strong / eventual / causal
}
// NonFunctionalReqs describes operational requirements.
type NonFunctionalReqs struct {
Availability string // e.g., "99.9%"
Durability string // critical / best-effort
Geographic string // single region / multi-region
Compliance string // GDPR / HIPAA / none
Budget string // low / moderate / high
}
// DatabaseSelectionTemplate provides a structured approach for DB selection.
//
// Step 4 decision example:
//
// "For this system I would choose PostgreSQL because:
// 1. Our data is highly structured with complex relationships
// 2. We need strong consistency for financial transactions
// 3. Read-heavy workload works well with read replicas
// 4. JSONB covers our semi-structured data needs
// 5. Team has PostgreSQL expertise
// 6. 100GB fits well in a single instance
// I would add Redis for session cache and rate limiting."
type DatabaseSelectionTemplate struct {
Data DataCharacteristics
Access AccessPatterns
NFR NonFunctionalReqs
}
func (t *DatabaseSelectionTemplate) MakeDecision() string {
return "PostgreSQL + Redis"
}
// DataCharacteristics describes the data model properties.
public sealed record DataCharacteristics(
string Structure, // structured / semi-structured / unstructured
string Relationships, // none / few / many JOINs
string SchemaStability, // stable / evolving / unknown
string Size, // estimate, e.g. "100 GB"
string GrowthRate); // e.g. "1 GB/day"
// AccessPatterns describes how data is accessed.
public sealed record AccessPatterns(
string ReadWriteRatio, // e.g. "80:20"
string QueryComplexity, // point lookup / range / complex / aggregation
string LatencyRequirement, // e.g. "p99 < 50ms"
string Throughput, // e.g. "10K requests/sec"
string Consistency); // strong / eventual / causal
// NonFunctionalReqs describes operational requirements.
public sealed record NonFunctionalReqs(
string Availability, // e.g. "99.9%"
string Durability, // critical / best-effort
string Geographic, // single region / multi-region
string Compliance, // GDPR / HIPAA / none
string Budget); // low / moderate / high
/// <summary>
/// Template: justify a database choice on a System Design interview.
///
/// Step 4 decision example:
/// "For this system I would choose PostgreSQL because:
/// 1. Our data is highly structured with complex relationships
/// 2. We need strong consistency for financial transactions
/// 3. Read-heavy workload works well with read replicas
/// 4. JSONB covers our semi-structured data needs
/// 5. Team has PostgreSQL expertise
/// 6. 100GB fits well in a single instance
/// I would add Redis for session cache and rate limiting."
/// </summary>
public sealed record DatabaseSelectionTemplate(
DataCharacteristics Data,
AccessPatterns Access,
NonFunctionalReqs Nfr)
{
public string MakeDecision() => "PostgreSQL + Redis";
}
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class DataCharacteristics:
"""Properties of the data model."""
structure: str # structured / semi-structured / unstructured
relationships: str # none / few / many JOINs
schema_stability: str # stable / evolving / unknown
size: str # estimate, e.g. "100 GB"
growth_rate: str # e.g. "1 GB/day"
@dataclass(frozen=True, slots=True)
class AccessPatterns:
"""How the data is accessed."""
read_write_ratio: str # e.g. "80:20"
query_complexity: str # point lookup / range / complex / aggregation
latency_requirement: str # e.g. "p99 < 50ms"
throughput: str # e.g. "10K requests/sec"
consistency: str # strong / eventual / causal
@dataclass(frozen=True, slots=True)
class NonFunctionalReqs:
"""Operational requirements."""
availability: str # e.g. "99.9%"
durability: str # critical / best-effort
geographic: str # single region / multi-region
compliance: str # GDPR / HIPAA / none
budget: str # low / moderate / high
@dataclass(frozen=True, slots=True)
class DatabaseSelectionTemplate:
"""Template: justify a database choice on a System Design interview.
Step 4 decision example:
"For this system I would choose PostgreSQL because:
1. Our data is highly structured with complex relationships
2. We need strong consistency for financial transactions
3. Read-heavy workload works well with read replicas
4. JSONB covers our semi-structured data needs
5. Team has PostgreSQL expertise
6. 100GB fits well in a single instance
I would add Redis for session cache and rate limiting."
"""
data: DataCharacteristics
access: AccessPatterns
nfr: NonFunctionalReqs
def make_decision(self) -> str:
return "PostgreSQL + Redis"
> **На интервью:** всегда обосновывайте выбор конкретными требованиями системы. Покажите, что знаете альтернативы и объясните, почему они не подходят.
Итоги
Начинайте выбор с анализа данных и паттернов доступа
PostgreSQL -- лучший выбор по умолчанию для большинства случаев
Добавляйте специализированные БД только для конкретных проблем
Операционная стоимость каждой дополнительной БД значительна
На интервью: структурированный подход важнее правильного ответа