数据存储策略:关系型数据库 vs NoSQL 与 ORM 实战
数据存哪?怎么存?这个问题,几乎每个项目都会遇到。我见过太多团队,一上来就选 MySQL,结果存了一堆 JSON 字段,查询时痛苦不堪。也有人盲目追 MongoDB,最后发现事务一致性根本兜不住。
说白了,选存储就是选 trade-off。今天咱们就把 MySQL 和 MongoDB 掰开揉碎,再聊聊 SQLAlchemy 这个 ORM 到底该怎么用。
关系型数据库 MySQL:稳,但有代价
MySQL 的好处不用我多说。ACID 事务、强一致性、成熟的生态。我最早做电商后台时,订单、库存、账户这些核心数据,全压在 MySQL 上。为什么?因为钱不能算错。
但 MySQL 也有它的短板。你想想看,一张表几千万行,join 五六个表,查询直接慢成狗。这时候就得靠分库分表、读写分离来救场。我有个项目,初期表结构设计得特别「灵活」,把用户标签存成逗号分隔的字符串。后来要按标签筛选用户,那 SQL 写得…嗯,我自己都不想看。
NoSQL 数据库 MongoDB:灵活,但别滥用
MongoDB 的文档模型,说白了就是「存 JSON」。字段可以随意增减,嵌套结构随便搞。我在做物联网数据采集平台时,传感器上报的数据结构经常变。今天多一个温度字段,明天少一个湿度字段。用 MySQL 改表结构?改一次 DDL 锁表半天。用 MongoDB,直接往里怼就完事了。
但 MongoDB 的坑也不少。最典型的就是「无事务」——早期版本确实没有。后来 4.0 开始支持多文档事务,但性能开销不小。我建议你:别把 MongoDB 当万能存储。跨文档的强一致性需求,还是交给 MySQL 更靠谱。
| 对比维度 | MySQL | MongoDB |
|---|---|---|
| 数据模型 | 表、行、列,严格 schema | 文档、集合,schema-less |
| 事务支持 | ACID 事务,成熟稳定 | 4.0+ 支持多文档事务,性能有损耗 |
| 查询能力 | SQL 强大,join、子查询、聚合 | 类 JSON 查询,聚合管道灵活 |
| 扩展方式 | 主从复制、分库分表(较复杂) | 原生分片,水平扩展方便 |
| 典型场景 | 金融、电商、ERP 等强一致场景 | 日志、物联网、内容管理、实时分析 |
ORM 框架 SQLAlchemy:写 Python 就该用它
直接写 SQL 很爽,但项目大了就难受。字段名改一下,所有 SQL 都得翻一遍。ORM 就是干这个的——把数据库表映射成 Python 对象。
SQLAlchemy 是 Python 生态里最成熟的 ORM。我个人习惯用它的 declarative base 模式,写起来最自然。
from sqlalchemy import create_engine, Column, Integer, String, Float
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class SensorData(Base):
__tablename__ = 'sensor_data'
id = Column(Integer, primary_key=True)
device_id = Column(String(50), nullable=False)
temperature = Column(Float)
humidity = Column(Float)
timestamp = Column(Integer)
# 创建引擎和会话
engine = create_engine('mysql+pymysql://user:pass@localhost/db')
Session = sessionmaker(bind=engine)
session = Session()
# 插入
data = SensorData(device_id='sensor_01', temperature=25.6, humidity=60.2)
session.add(data)
session.commit()
# 查询
results = session.query(SensorData).filter(
SensorData.device_id == 'sensor_01',
SensorData.temperature > 20
).all()
避坑指南:ORM 的 N+1 查询问题
我曾经接手过一个项目,接口响应时间超过 10 秒。查了半天,发现是 ORM 的懒加载搞的鬼。查询 100 条订单,每条订单又懒加载用户信息,结果发了 101 条 SQL。
解决方案很简单:用 joined loading 或 subquery loading。
# 错误示范:N+1 查询
orders = session.query(Order).all()
for order in orders:
print(order.user.name) # 每次循环都发一条 SQL
# 正确做法:一次性 join 加载
from sqlalchemy.orm import joinedload
orders = session.query(Order).options(
joinedload(Order.user)
).all()
for order in orders:
print(order.user.name) # 不会再有额外查询
选型决策:一张图说清楚
下面这张图,是我做技术选型时常用的判断逻辑。你可以直接拿去用。
实战建议:混合存储才是王道
我现在的项目,基本都是 MySQL + MongoDB 搭配着用。订单、用户、账户这些核心数据放 MySQL。操作日志、埋点数据、临时缓存放 MongoDB。
ORM 方面,SQLAlchemy 配合 Alembic 做数据库迁移,简直绝配。每次改模型,自动生成迁移脚本,再也不用担心「哎呀,测试库和生产库表结构不一样了」。