Масштабируемое хранение данных: SQL и NoSQL
Выбирайте между реляционными базами данных, хранилищами «ключ—значение», документными базами и ширококолоночными хранилищами с учётом способов доступа, согласованности и требований к масштабированию
«Масштабируемое хранение данных: SQL и NoSQL» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding Interview Prep содержит 4 уроков всего.
Выбор хранилища — это компромисс
Выбор хранилища данных — одно из самых важных решений при проектировании системы. Не существует базы данных, которая была бы лучшей во всех случаях — каждый тип оптимизирован под разные шаблоны доступа, гарантии согласованности и характеристики масштабирования. Ошибка в этом выборе в рабочей системе приводит к месяцам трудоёмкой работы по миграции.
На собеседованиях интервьюеры проверяют, понимаете ли Вы фундаментальные различия и можете ли сопоставить движок хранения с требованиями задачи. Вопрос никогда не звучит как «что лучше?», а звучит как «что лучше для этой конкретной рабочей нагрузки?». Всегда обосновывайте свой выбор конкретными требованиями.
# Storage decision matrix summary
factors = [
'Data structure (tabular, documents, key-value, graph, time-series)',
'Read vs write ratio (read-heavy, write-heavy, balanced)',
'Query patterns (point lookups, range scans, aggregations, joins)',
'Consistency requirements (ACID vs eventual consistency)',
'Scale requirements (single node, sharding, global distribution)',
'Latency requirements (milliseconds vs microseconds)',
'Team familiarity and operational complexity',
]
print('Key factors for storage selection:')
for f in factors:
print(f' - {f}')Преимущества реляционных баз данных (SQL)
Реляционные базы данных (PostgreSQL, MySQL, SQLite) хранят данные в таблицах с фиксированными схемами и поддерживают транзакции ACID — атомарность, согласованность, изоляцию и долговечность. Они отлично подходят для сложных запросов с объединениями, агрегатами и фильтрами, поэтому оптимальны для структурированных данных с чётко определёнными связями.
Основные преимущества: сложные запросы к нескольким таблицам с помощью SQL, ограничения внешних ключей для целостности данных, мощная индексация (B-дерево, хеширование, полнотекстовый поиск), зрелая экосистема с репликацией и резервным копированием. Используйте SQL, когда данные тесно связаны, согласованность критически важна, а запросы сложные и разнообразные.
# SQL excels at: complex queries, joins, transactions
# Example: find top 5 products by revenue this month
sql_query = '''
SELECT p.name, SUM(oi.quantity * oi.price) AS revenue
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.created_at >= DATE_TRUNC('month', NOW())
GROUP BY p.id, p.name
ORDER BY revenue DESC
LIMIT 5;
'''
print('SQL shines for relational queries with JOINs:')
print(sql_query)
print('ACID guarantees example (transfer $100 between accounts):')
transfer_sql = '''
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- either both succeed or neither does
'''
print(transfer_sql)Проблемы масштабирования SQL
Базы данных SQL естественным образом масштабируются вертикально (более мощный сервер, больше CPU/RAM), но горизонтальное масштабирование сложно. Реплики для чтения справляются с нагрузками, в которых преобладает чтение, направляя операции чтения на узлы-реплики, а операции записи — на основной узел. Однако пропускная способность записи ограничена одним основным узлом, если не добавить шардирование.
Шардирование разделяет данные между несколькими экземплярами базы данных по ключу шарда (например, user_id mod N). Это обеспечивает масштабирование записи, но нарушает объединения и транзакции между шардами — две наиболее сильные стороны SQL. Большинство веб-приложений перерастают возможности односерверной базы данных SQL примерно при объёме данных 5–10 TB или около 100K операций записи в секунду.
# SQL scaling strategies
strategies = {
'Read replicas': {
'how': 'One primary (writes), multiple replicas (reads)',
'scales': 'Read throughput (10x+)',
'limit': 'Write throughput still bounded by single primary',
},
'Connection pooling (PgBouncer)': {
'how': 'Pool of persistent DB connections shared among app servers',
'scales': 'Connection count (PostgreSQL max ~500 connections)',
'limit': 'Does not increase query throughput',
},
'Horizontal sharding': {
'how': 'Partition rows by shard key across N database instances',
'scales': 'Both reads and writes (N×)',
'limit': 'Cross-shard joins and transactions broken; complex routing',
},
'CQRS': {
'how': 'Separate write model (SQL) from read model (denormalised/NoSQL)',
'scales': 'Optimise each path independently',
'limit': 'Eventual consistency between write and read models',
},
}
for strategy, info in strategies.items():
print(f'{strategy}:\n How: {info["how"]}\n Scales: {info["scales"]}\n Limit: {info["limit"]}\n')Хранилища «ключ — значение»: Redis и DynamoDB
Хранилища «ключ — значение» хранят данные как пары «ключ → значение» и обеспечивают чтение и запись по ключу за O(1). Они жертвуют гибкостью запросов ради чрезвычайной производительности и горизонтальной масштабируемости. Redis (в памяти) обеспечивает задержку в микросекундах, а DynamoDB (управляемый сервис) — задержку в несколько миллисекунд с автоматическим масштабированием до любой пропускной способности.
Используйте хранилища «ключ — значение» для: хранения сеансов, кэширования, флагов функций, сопоставлений для сокращателя URL, корзин покупок и таблиц лидеров в реальном времени. Не используйте их, если Вам нужны: сложные запросы, связи между сущностями или специальная фильтрация — искать можно только по точному ключу.
# Key-value store use cases
kv_operations = {
'GET key': 'O(1) point lookup — the core operation',
'SET key value': 'O(1) insert or update',
'DEL key': 'O(1) delete',
'EXPIRE key ttl': 'Set time-to-live; key auto-deleted after ttl seconds',
'INCR key': 'Atomic increment — useful for counters and rate limiting',
'LPUSH/LRANGE': 'List operations — useful for queues and recent-items feeds',
'ZADD/ZRANGE': 'Sorted set — leaderboards, rate limiting with sliding window',
}
print('Redis operation set:')
for op, desc in kv_operations.items():
print(f' {op:25s}: {desc}')
print('\nDynamoDB vs Redis:')
print(' Redis: microsecond latency, in-memory, needs persistence config')
print(' DynamoDB: single-digit ms, managed, auto-scaling, durable by default')Документные хранилища: MongoDB
Документные хранилища (MongoDB, Couchbase) хранят данные в виде JSON-подобных документов, что позволяет использовать гибкие схемы — набор полей может различаться у документов одной коллекции. Они поддерживают индексацию любого поля и достаточно сложные запросы (фильтры, проекции, агрегаты), хотя транзакции с несколькими документами более ограничены, чем в SQL.
Документные хранилища подходят для приложений, в которых: форма данных различается у разных сущностей (например, профили пользователей с разными атрибутами), ожидается быстрое изменение схемы (например, стартапы часто меняют модели данных) или в шаблонах чтения преобладает получение целых сущностей, а не объединение таблиц.
# MongoDB document example
user_doc = {
'_id': 'user123',
'name': 'Alice',
'email': 'alice@example.com',
'preferences': {
'theme': 'dark',
'language': 'en',
'notifications': ['email', 'push']
},
'addresses': [
{'type': 'home', 'city': 'Berlin', 'country': 'DE'},
{'type': 'work', 'city': 'Munich', 'country': 'DE'}
],
'subscription_tier': 'pro',
# Note: not all users have all fields -- flexible schema!
}
import json
print('Document structure (flexible schema):')
print(json.dumps(user_doc, indent=2))
print('\nDocument store strengths:')
print(' - Nested/array fields without joins')
print(' - Flexible schema (different fields per document)')
print(' - Scales horizontally by sharding on _id')Хранилища с широкими столбцами: Cassandra
Хранилища с широкими столбцами (Apache Cassandra, HBase) хранят данные в строках и столбцах, но позволяют каждой строке иметь свой набор столбцов. Они предназначены для огромной пропускной способности записи, распределённой между множеством узлов, с согласованностью в конечном счёте по умолчанию. Cassandra обеспечивает линейную масштабируемость записи — удвоение числа узлов удваивает пропускную способность записи.
Компромисс заключается в том, что запросы необходимо проектировать с учётом ключа секционирования. Нельзя эффективно фильтровать или сортировать данные по произвольным столбцам — сначала нужно определить шаблон запроса, а затем спроектировать таблицу под него. Это противоположность подходу SQL «сначала спроектировать данные, а затем писать любые запросы».
# Cassandra table design for time-series events
# Design around the query: 'give me all events for user X, most recent first'
cassandra_table = '''
CREATE TABLE user_events (
user_id UUID,
event_time TIMESTAMP,
event_type TEXT,
metadata MAP<TEXT, TEXT>,
PRIMARY KEY (user_id, event_time)
) WITH CLUSTERING ORDER BY (event_time DESC);
-- Query (matches partition key exactly):
SELECT * FROM user_events WHERE user_id = ? LIMIT 100;
'''
print('Cassandra wide-column design:')
print(cassandra_table)
print('Properties:')
print(' - user_id = partition key (all rows for one user on same node)')
print(' - event_time = clustering key (sorted within partition)')
print(' - Very fast writes: append-only, no locking')
print(' - Cannot query by event_type alone (no partition key)')Теорема CAP: согласованность, доступность и устойчивость к разделению сети
Теорема CAP утверждает, что распределённая система может гарантировать не более двух из трёх свойств: Согласованность (каждое чтение возвращает последнюю запись), Доступность (каждый запрос получает ответ без ошибки) и Устойчивость к разделению сети (система продолжает работать несмотря на разделение сети).
Поскольку в распределённых системах разделение сети неизбежно, реальный выбор сводится к CP (приоритет согласованности, запросы могут отклоняться во время разделения сети) и AP (приоритет доступности, могут возвращаться устаревшие данные). Базы данных SQL обычно относятся к CP; Cassandra и DynamoDB — к AP (согласованность в конечном счёте используется по умолчанию). Redis Cluster относится к CP.
# CAP theorem applied to common databases
databases = {
'PostgreSQL (single node)': {'C': True, 'A': True, 'P': False, 'note': 'Not distributed; CA'},
'PostgreSQL (multi-AZ)': {'C': True, 'A': False, 'P': True, 'note': 'CP: primary fails over, brief downtime'},
'MySQL Cluster': {'C': True, 'A': False, 'P': True, 'note': 'CP'},
'Cassandra': {'C': False, 'A': True, 'P': True, 'note': 'AP: eventual consistency default'},
'DynamoDB (default)': {'C': False, 'A': True, 'P': True, 'note': 'AP: eventual consistency'},
'DynamoDB (strong read)': {'C': True, 'A': False, 'P': True, 'note': 'CP: strongly consistent reads'},
'Redis Cluster': {'C': True, 'A': False, 'P': True, 'note': 'CP'},
'MongoDB (default)': {'C': True, 'A': False, 'P': True, 'note': 'CP: reads from primary'},
}
for db, caps in databases.items():
c_str = 'C' if caps['C'] else '-'
a_str = 'A' if caps['A'] else '-'
p_str = 'P' if caps['P'] else '-'
print(f'{db:35s} [{c_str}{a_str}{p_str}] {caps["note"]}')Схема выбора хранилища
Практическая схема выбора хранилища на собеседованиях:
- Нужны транзакции ACID? → Реляционная база данных (PostgreSQL, MySQL)
- Нужны обращения с задержкой менее миллисекунды или кэширование? → Хранилище «ключ — значение» (Redis)
- Нужна гибкая схема или данные, ориентированные на документы? → Документное хранилище (MongoDB)
- Нужна огромная пропускная способность записи (>100K/с) для данных временных рядов или событий? → Хранилище с широкими столбцами (Cassandra)
- Нужно глобальное распределение с управляемым масштабированием? → DynamoDB или Cosmos DB
- Нужны обходы графа? → Графовая база данных (Neo4j)
# Decision tree in code form
def choose_storage(needs_acid, high_write_throughput, flexible_schema,
sub_ms_latency, graph_queries, global_scale):
if sub_ms_latency:
return 'Redis (in-memory key-value)'
if graph_queries:
return 'Neo4j (graph database)'
if needs_acid:
return 'PostgreSQL / MySQL (relational)'
if high_write_throughput and not flexible_schema:
return 'Cassandra (wide-column, write-optimised)'
if flexible_schema:
return 'MongoDB (document store)'
if global_scale:
return 'DynamoDB or Cosmos DB (managed global KV/document)'
return 'PostgreSQL (safe default for most web apps)'
# Example scenarios
scenarios = [
{'needs_acid': True, 'high_write_throughput': False, 'flexible_schema': False,
'sub_ms_latency': False, 'graph_queries': False, 'global_scale': False},
{'needs_acid': False, 'high_write_throughput': True, 'flexible_schema': False,
'sub_ms_latency': False, 'graph_queries': False, 'global_scale': False},
{'needs_acid': False, 'high_write_throughput': False, 'flexible_schema': False,
'sub_ms_latency': True, 'graph_queries': False, 'global_scale': False},
]
for s in scenarios:
print(f'{choose_storage(**s)}')Полиглотное хранение: использование нескольких хранилищ
Рабочие системы редко используют одну базу данных для всего. Полиглотное хранение означает использование подходящей технологии хранения для каждой части системы. Типичное веб-приложение может использовать: PostgreSQL для эталонных данных пользователей и заказов, Redis для хранения сеансов и кэширования, Elasticsearch для полнотекстового поиска, S3 для хранения файлов и Cassandra для журналов событий и аналитики.
Компромисс заключается в том, что с каждым новым типом базы данных растёт эксплуатационная сложность. Команде приходится обслуживать, отслеживать и резервировать несколько систем. Обоснование выбора: при больших масштабах прирост производительности и масштабируемости благодаря подбору подходящего хранилища для каждой рабочей нагрузки перевешивает эксплуатационные затраты.
# Polyglot persistence in an e-commerce system
components = {
'User accounts, orders, payments': {
'storage': 'PostgreSQL',
'reason': 'ACID transactions (payment integrity), complex queries',
},
'Product catalogue': {
'storage': 'MongoDB or PostgreSQL with JSONB',
'reason': 'Flexible product attributes vary by category',
},
'Session tokens': {
'storage': 'Redis (with TTL)',
'reason': 'O(1) lookup, automatic expiry, high throughput',
},
'Product search': {
'storage': 'Elasticsearch',
'reason': 'Full-text search, faceted filtering, relevance scoring',
},
'Activity/event log': {
'storage': 'Cassandra or Kafka + S3',
'reason': 'High write throughput, append-only, time-series queries',
},
'Product images / videos': {
'storage': 'S3 + CloudFront CDN',
'reason': 'Cheap object storage, global distribution via CDN',
},
}
for component, info in components.items():
print(f'{component}:\n {info["storage"]}: {info["reason"]}\n')Стратегия индексации для разных типов хранилищ
Все системы хранения используют индексы, чтобы ускорять чтение ценой замедления записи и увеличения объёма хранилища. Понимание индексации в разных типах хранилищ крайне важно для проектирования систем:
- SQL: индекс B-дерева для любого столбца; составные индексы для запросов по нескольким столбцам; покрывающие индексы, позволяющие избежать обращения к таблице
- MongoDB: индекс для любого поля; составные индексы; индексы TTL для автоматического удаления устаревших данных
- Cassandra: штатно индексируются только ключ секционирования и столбцы кластеризации; вторичные индексы дороги
- Redis: отсортированные множества как индексы для запросов по диапазонам; для хранилищ «ключ — значение» традиционная индексация не нужна
# Indexing examples across storage types
# PostgreSQL: B-tree composite index
postgres_index = '''
CREATE INDEX idx_orders_user_created
ON orders(user_id, created_at DESC);
-- Optimises: SELECT * FROM orders WHERE user_id=? ORDER BY created_at DESC
'''
# MongoDB: compound index
mongo_index = '''
db.products.createIndex({ category: 1, price: -1 })
// Optimises: db.products.find({category:'Electronics'}).sort({price:-1})
'''
# Cassandra: cluster key ordering (built into table design)
cassandra_index = '''
-- No separate index needed; clustering key IS the index:
PRIMARY KEY (user_id, event_time) WITH CLUSTERING ORDER BY (event_time DESC)
'''
print('PostgreSQL:', postgres_index)
print('MongoDB:', mongo_index)
print('Cassandra:', cassandra_index)SQL и NoSQL на собеседованиях: что сказать
Когда на собеседовании по проектированию систем Вас спрашивают «SQL или NoSQL?», никогда не отвечайте одним словом. Вместо этого используйте следующую структуру:
- Опишите рабочую нагрузку: «Здесь преобладает чтение и требуется сложная фильтрация, поэтому…»
- Сформулируйте требование: «Для финансовых транзакций нам нужна строгая согласованность, поэтому…»
- Назовите выбор: «Я бы использовал PostgreSQL с репликами для чтения»
- Обозначьте компромисс: «Компромисс заключается в том, что горизонтальное масштабирование записи требует шардирования, которое добавляет сложности»
- Упомяните альтернативу: «Если бы объём записи был выше, мы могли бы рассмотреть DynamoDB»
# Sample answer structure for 'SQL or NoSQL?'
def answer_storage_question(workload, consistency_need, scale):
print(f'Workload: {workload}')
print(f'Consistency: {consistency_need}')
print(f'Scale: {scale}')
print()
if 'financial' in workload.lower() or consistency_need == 'strong':
choice = 'PostgreSQL (ACID, strong consistency)'
tradeoff = 'Horizontal write scaling requires sharding'
alternative = 'Google Spanner for global transactions'
elif 'event' in workload.lower() or 'log' in workload.lower():
choice = 'Cassandra (high write throughput, time-series)'
tradeoff = 'Eventual consistency; queries limited to partition key'
alternative = 'Kafka + S3 for long-term event archival'
else:
choice = 'DynamoDB (managed, auto-scale, low latency)'
tradeoff = 'Limited query flexibility; cross-item transactions limited'
alternative = 'PostgreSQL if complex queries emerge'
print(f'Choice: {choice}\nTrade-off: {tradeoff}\nAlternative: {alternative}')
answer_storage_question('Social media feed', 'eventual', '10M users')Быстрая проверка
Проверьте, насколько Вы поняли концепции «Структуры данных и алгоритмы — подготовки к собеседованию по программированию» из этого урока.
Итоги урока
В этом уроке Вы узнали: базы данных SQL обеспечивают транзакции ACID и сложные запросы, но с трудом масштабируют запись, тогда как базы данных NoSQL жертвуют согласованностью или гибкостью запросов ради огромного масштаба и доступности, теорема CAP вынуждает распределённые системы выбирать между согласованностью и доступностью во время разделения сети, а также рабочие системы обычно используют полиглотное хранение — каждой рабочей нагрузке соответствует подходящий движок хранения. Далее мы рассмотрим уровни кэширования, CDN и балансировку нагрузки, чтобы ещё больше масштабировать системы с преобладанием чтения.
Часто задаваемые вопросы
Урок «Масштабируемое хранение данных: SQL и NoSQL» бесплатный?
Да — полный текст урока «Масштабируемое хранение данных: SQL и NoSQL» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Масштабируемое хранение данных: SQL и NoSQL»?
Выбирайте между реляционными базами данных, хранилищами «ключ—значение», документными базами и ширококолоночными хранилищами с учётом способов доступа, согласованности и требований к масштабированию Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Coding Interview Prep?
Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Масштабируемое хранение данных: SQL и NoSQL»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Coding Interview Prep?
Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Структура собеседования по проектированию систем
- Масштабируемое хранение данных: SQL и NoSQL
- Кэширование, CDN и балансировка нагрузки
- Проектирование ограничителя частоты и ленты Twitter