可扩展数据存储:SQL 与 NoSQL
根据访问模式、一致性和扩展需求,在关系型数据库、键值存储、文档数据库和宽列存储之间做出选择。
可扩展数据存储:SQL 与 NoSQL 是 CoddyKit 上的免费 Coding Interview Prep 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 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),但水平扩展较为复杂。只读副本通过将读取路由到副本节点、将写入路由到主节点,来处理读密集型负载。但除非增加分片,否则写入吞吐量会受限于单个主节点。
分片会根据分片键(例如用户标识对 N 取模)将数据划分到多个数据库实例中。这样可以提升写入规模,但会破坏跨分片联接和事务这两项 SQL 的核心优势。大多数网络应用在数据量达到约 5–10 TB 或每秒约 100K 次写入时,就会超出单节点 SQL 的承载能力。
# 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 由服务提供商托管,可实现个位数毫秒级延迟,并自动扩展到任意吞吐量。
以下场景适合使用键值存储:会话存储、缓存、功能开关、短网址映射、购物车和实时排行榜。以下情况请勿使用:需要复杂查询、实体间关系或临时筛选——您只能按精确键查找。
# 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」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Coding Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 Coding Interview Prep 课程共包含 4 节课。
「可扩展数据存储:SQL 与 NoSQL」这节课中我会学到什么?
根据访问模式、一致性和扩展需求,在关系型数据库、键值存储、文档数据库和宽列存储之间做出选择。 你通过在浏览器中直接运行的动手代码来练习 Coding Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Coding Interview Prep 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Coding Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「可扩展数据存储:SQL 与 NoSQL」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Coding Interview Prep 课中编写并运行代码吗?
能。每节 Coding Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 系统设计面试框架
- 可扩展数据存储:SQL 与 NoSQL
- 缓存、CDN 与负载均衡
- 设计限流器与 Twitter 信息流