为了账号安全,请及时绑定邮箱和手机立即绑定

避免使用 SELECT ,即使是针对单列表 тоже这样

尽量避免使用 SELECT *,即使是在单列表上。即便你不认同这一点,也请记住。到文章结尾时,我可能会让你好好想想。

2012年的一个故事

这是大约十二年前(2012年到2013年左右)我遇到的一个真实故事,那时我在处理一个客户的后端系统问题。

一个稳定运行且延迟在个位数毫秒内的后端 API。有一天,用户们遇到了卡顿和不流畅的使用体验。

我们检查了提交记录,没有什么显然的问题,大部分改动看起来没啥问题。为了谨慎起见,我们撤销了所有提交(你们中有些人应该会有同感,当你开始怪罪那些不合常理的事时,你就知道情况已经很糟糕了)。

不过,这款软件还是很慢。

在查看诊断时,我们注意到API响应时间有时达到2秒,而之前原本是500毫秒左右,响应时间仅为几毫秒。

我们知道后台没有做出任何可能引起变慢的改动,但我们开始检查数据库查询。

从一个含有3个 blob 字段的表中选择所有数据返回给后端应用,这些 blob 字段中的大数据量非常大。

发现这个表只有两个整数字段,而API执行了一个SELECT *来返回并使用这两个字段。后来,管理员又添加了三个blob字段,这些字段被另一个应用程序使用和填充了。

当那些二进制大对象(blob)字段没有返回给客户端时,后端API在处理额外字段时承受了损耗,这些字段由其他应用程序填充,导致了数据库、网络和协议的序列化的额外开销。

你知道数据库是如何读取数据的吗?

在行存数据库引擎中,行是以称为“页”的单位存储的。每一页都包含一个固定的页头,并且包含多个行,每一行前面有记录头,后面跟着相应的列数据。例如在PostgreSQL中,可以参考以下示例:

当数据库检索到一页并将其放入共享缓冲池时,我们就可以访问该页中的所有行和列。因此,一个问题出现了:如果我们已经在内存中拥有了所有列,为什么这样的 SELECT * 查询会变得缓慢且消耗资源?它真的像人们所说的那样慢吗?如果是的话,为什么它会如此呢?在本文中,我们将探讨这些问题及其他内容。

和只扫描索引说再见吧。

使用 SELECT * 表示数据库优化器无法选择仅索引扫描。例如,假设你需要获取成绩高于 90 分的学生的 ID,而你在 grades 列上有索引,该索引包含学生 ID 作为非键部分,这个索引非常适合这个查询语句。

然而,因为你要求获取所有字段,数据库需要访问堆数据页来获取剩余的字段,这增加了随机读取次数,导致了更多的I/O。在这种情况下,如果你没有使用SELECT *,数据库只需要扫描成绩索引并返回ID。

反序列化费用

反序列化,也就是解码,是将原始字节转换成数据类型的过程。这涉及将一串字节(通常来自文件、网络通信或其他来源)转换回更结构化的数据格式,比如编程语言中的对象和变量。

当你执行一个 SELECT * 查询时,数据库需要反序列化所有列,即使这些列对于你的特定需求不是必需的。这不仅会增加计算开销,还会降低查询性能。这样,你只选择必要的列就可以减少反序列化的成本,提高查询效率。

并非所有的列都在一行

使用 SELECT * 查询的一个重要问题是,并非所有列都存储在页面的内联部分。例如文本和二进制大对象这样的大列通常会被存储在外部表中,并且只有在请求时才会被检索(Postgres 中的 TOAST 表就是例子之一)。这些被压缩的列,当你执行包含许多文本字段、几何数据或二进制大对象的 SELECT * 查询时,会使数据库负担加重,因为需要从外部表获取值、解压缩它们,并返回给客户端。

上网费用:

在查询结果发送到客户端之前,必须进行序列化,以符合数据库支持的通信协议。需要序列化的数据越多,CPU的工作量就越大。序列化后的字节通过TCP/IP进行传输。需要发送的段越多,传输成本就越高,最终会影响网络延迟。

返回所有列可能需要对大的列(例如字符串或二进制大对象)进行反序列化,而客户端可能永远都不会用到。

客户端反序列化过程

当客户端接收到原始字节时,客户端应用程序必须将数据反序列化成客户端使用的格式,增加了总的处理时间。管道中的数据越多,处理过程就越慢。

不可预测

即使只有一个字段,你也可能会在客户端执行 SELECT * ,瞬间获取了两个整数字段。想想这个例子,你有一个表,里面有一个或两个字段。

然而,后来管理员决定添加一个XML字段和JSON字段、blob以及其他由其他应用程序填充并使用的字段。尽管你的代码没有任何改变,它会突然变慢下来,因为它现在需要处理那些原本并不需要的额外字段。

代码Grep

另一个使用显式 SELECT 语句的优点是,你可以在代码库中搜索被使用的列,如果你想重命名或删除某一列时,这会让数据库模式的 DDL 更加容易处理。

这是总结

总之,一个 SELECT * 查询涉及很多复杂的过程,因此最好只选择你需要的字段以避免不必要的负担。请记住,如果你的表只有少量简单的数据列,SELECT * 查询的开销可能可以忽略。不过,通常最好只在查询中选择你需要的列。

如果这篇文章让你喜欢,可以看看我关于Backend和Database的课程,在我的简介里有链接。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
40
获赞与收藏
125

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消