SQL 与 ORM 之争,今天来好好聊一聊
该使用 SQL 还是 ORM 一直是后端开发领域中一个可以拿出来聊的话题,有人偏爱 SQL 的极致性能,有人推崇 ORM 的高效便捷。当然,今天我要说的 SQL 不是指纯手写 SQL,而是使用 SQL Builder 来写 SQL。所以,今天我们一起来聊一下 SQL(SQL Builder) 和 ORM 框架的话题,也顺便抒发下我的一点个人看法。
ORM 框架的优缺点
ORM 最大的价值在于让开发者更专注于业务逻辑而非数据操作细节,这点我想用过 ORM 框架的开发者都能感受到。使用 ORM 能极大的提升开发效率,对于大部分的 CURD 业务也极快的完成,无需写稍显繁琐的 SQL 语句。
# 使用 Python SQLAlchemy
new_user = User(name="张三", email="zhangsan@example.com")
db.session.add(new_user)
db.session.commit()
# 对应的 SQL
cursor.execute(
"INSERT INTO users (name, email) VALUES (%s, %s)",
("张三", "zhangsan@example.com")
)
ORM 也是更偏向于面向对象的,它将数据库表映射为业务对象,代码逻辑更贴合业务语义,这也使得代码更易读和易维护。比如下面这个查询用户的所有订单示例:
user = db.session.get(User, 1)
print(user.orders) # 自动关联查询订单表
# 对应的 SQL
cursor.execute("""
SELECT o.* FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.id = %s
""", (1,))
成熟的 ORM 框架通常会提供额外高级特性来提高开发体验,比如:
- 钩子(Hook):在数据操作前后自动执行逻辑(如 BeforeCreate 自动加密密码);
- 缓存:一级缓存(Session 级)减少重复查询;
说了这么多 ORM 的优点,其实 ORM 的缺点也是很明显的,最大的痛点就是它的性能开销比较大,简单 CRUD 场景下,GORM(Golang) 比原生 SQL 慢 20%-50%,在复杂查询、大数据量的情况下差异会更大。
对于极度复杂的 SQL(多层子查询、窗口函数、数据库特有语法),ORM 的写法往往比原生 SQL 更繁琐,甚至无法表达。不过好在成熟的 ORM 框架一般都会支持使用 SQL 语句,对于这种场景直接写 SQL 就好了。
ORM 本身其实是个黑箱,开发者无法干涉到 ORM 框架的底层实现,这也就会带来生成冗余 SQL(如多余的括号、子查询)、复杂关联查询性能低下,却难以定位原因等问题。
最后,其实 ORM 框架的学习成本不低,ORM 并没有统一的 API 标准,这也就意味着你每学一个 ORM 框架就得重新学习,虽然大致差不多,但是涉及到高级特性、ORM 框架自身的坑等等你都得重点去查看。然后,你依旧还是得学 SQL 来应对复杂查询的场景。既然这样,我干嘛不干脆就用 SQL Builder 呢?
SQL Builder 的优缺点
SQL Builder 对于我这种热爱 SQL 的程序员来说是真的非常友好,你不需要去“翻译” SQL,完全就能按照写 SQL 的思路来写代码。就比如下面这个 Drizzle ORM(虽然名字带 ORM,但其实是一个更偏 SQL Builder 的框架)的示例:
import { db } from './db';
import { users } from './schema';
const data = await db
.select({
id: users.id,
name: users.name,
email: users.email
})
.from(users);
同时,对于一些开发中的痛点也有相应的解决方案,比如动态拼接、字段映射实体等。这里我们看一个 Golang 示例:
query := `SELECT id, name, email, created_at FROM users`
rows, _ := db.Query(query)
var users []User
for rows.Next() {
var user User
_ = rows.Scan(
&user.ID,
&user.Name,
&user.Email,
&user.CreatedAt,
)
users = append(users, user)
}
上面的 Golang 示例其实写得不够严谨,但这不是重点。其实我们可以看到自己手写 SQL 的话会不可比避免需要自己手动写映射字段到实体的代码,这会带来样板代码的问题,而 SQL Builder 一般都会提供字段映射实体的能力,比如前面写的 Drizzle ORM 的示例所展示的。
当然,由于存在字段映射实体的能力,性能损耗也是无法避免的,但 SQL Builder 的映射开销相对较小。根据社区基准测试的结果,SQL Builder(sqlx) 相比于原生 SQL,性能损耗约为5%-10%,主要是来自于简单映射开销,而 ORM(GORM) 相比于原生 SQL,性能损耗约为20%-50%,它的高级功能特性,如钩子、关联处理等也会带来的额外开销。
其实,还有一些很特别的 SQL Builder,比如 sqlx(Rust),更接近于写原生 SQL,但又提供了诸如动态拼接、字段映射实体等功能特性来解决开发中的特性。
#[derive(Debug, FromRow)]
struct User {
id: i32,
name: String,
email: String,
}
let users: Vec<User> = sqlx::query_as!(
User, // 目标实体
r#"
SELECT id, name, email
FROM users
"#
)
.fetch_all(&pool) // 执行查询并获取所有结果
.await?;
个人看法
说实话,现在我要在实际业务中直接手写 SQL 语句是不可能的,我可受不了要我将查询出来的字段一个个映射到实体模型中,以及等等之类的开发痛点。同样的,如果是我主导的新项目,要我用 ORM 也是不可能的,既要忍受它本身带来的较大的性能损耗,同时,对于我这种多语言开发者来说,换一门编程语言就要学一个 ORM 框架,实在是不想学的那么杂,还不如就学 SQL 这一套语法,最重要的原因是我本身热爱 SQL。
综上来看,SQL Builder 可以说是我唯一的选择,在你不需要“翻译” SQL,直接按 SQL 的思路来写代码即可,性能这块也不会有较大的损耗,同时,一些开发痛点也提供了相应解决方案。如果你感兴趣这个话题,也可以在评论区里说说你的看法。