原文地址
尽量避免 SELECT *,即使在单列表上也是如此 – 如果你现在不同意这一点,读完这篇文章,你可能就要动摇了。
2012年的一个故事
这是我 12 年前(约 2012-2013 年)在客户后台应用程序中遇到的一个真实故事。
当时,后端 API 一直稳定运行,速度仅为个位数毫秒。但突然有一天,应用程序变得非常迟钝。
我们检查了提交,没发现明显的问题,大部分改动都是良性的。即使恢复了所有提交(有人会懂,作这样无意义的尝试,就说明走投无路了),程序仍然很慢,API 响应时间从原先的几毫秒暴增到 500 毫秒至 2 秒不等。
鉴于后台没有导致速度变慢的变更,我们检查了数据库查询,发现 3 个具有超大文件的 blob 字段,它们表上的 SELECT * 正在返回到后台应用程序。
原来,这个表只有两个整数列,API 运行 SELECT * 来返回并使用这两个字段。但后来管理员添加了三个 blob 字段,由另一个应用程序使用和填充。这些 blob 字段没有返回给客户端,后端 API 却需要拉取其他应用程序填充的额外字段,造成数据库、网络和协议序列化开销。
数据库读取如何进行
在行存储数据库引擎中,行以「页面」为单位存储。每页有一个固定的页眉,包含多条记录;每条记录有一个记录页眉,后面跟着各自的列。例如 PostgreSQL 中的示例:
当数据库获取一个页面并将其放入共享缓冲池,我们就可以访问该页面中的所有行和列。如果内存中所有列都是现成的,那么 SELECT * 真的又慢又费钱吗?如果是,为什么会这样?以下将展开探讨这些问题。
跟仅索引扫描说拜拜
使用 SELECT * 意味着数据库优化器不能选择只扫描索引。
例如,假设需要 90 分以上学生的 ID,而成绩列上有一个索引包含作为非键的学生 ID,这个索引就非常适合查询。
但是由于需要所有字段,数据库需要访问堆数据页来获取剩余字段,从而增加了随机读取次数,进而增加 I/O。相对地,不使用 SELECT *,数据库只需扫描成绩索引并返回 ID 即可。
反序列化成本
反序列化或解码是将原始字节转换为数据类型。这包括获取字节序列(通常来自文件、网络通信等),并将其转换回结构化更强的数据格式,如编程语言中的对象或变量。
执行 SELECT * 查询时,数据库需要反序列化所有列,即使是特定使用情况下可能不需要的列。这会增加计算开销,降低查询性能。只选择必要的列,可以降低反序列化成本,提高查询效率。
并非所有列都是内联的
SELECT * 查询的一个重要问题是,并非所有列都存储在页面内。文本或 blob 等大型列可能存储在外部表中,只有请求时才会检索(例如 Postgres TOAST 表)。这些列通常都经过压缩,因此在执行包含大量文本字段、几何数据或 blob 的 SELECT * 查询时,会给数据库带来额外负担,即从外部表中获取值、解压缩并将结果返回给客户端。
网络成本
将查询结果发送到客户端前,必须根据数据库支持的通信协议对其序列化。需要序列化的数据越多,CPU 工作量越大。字节被序列化后通过 TCP/IP 传输,需要发送的段越多,传输成本越高,最终会影响网络延迟。
返回所有列可能需要反序列化字符串或 blob 等大列,即使客户可能永远不会使用这些列。
客户端反序列化
客户端收到原始字节后,应用程序必须将数据反序列化为客户端使用的任何语言,增加了整体处理时间。管道中的数据越多,这一过程就越慢。
不可预测性
即使只有一个字段,在客户端使用 SELECT * 也会带来不可预测性。
例如,对于含一到两个字段的表,程序会执行 SELECT * 快速处理两个整数字段;但是一旦添加 XML、JSON、blob 等新字段,它们就会被其他应用程序填充和使用。代码没有变化,但速度会突然变慢,因为现在程序要处理所有额外的字段。
代码搜索
显式 SELECT 的另一个优点是,可以在代码库中搜索正在使用的列,以防要重命名或删除某个列。这使得数据库 schema 变更(DDL)更加容易。
总结
总之,SELECT * 查询涉及许多复杂的过程,最好只选择需要的字段,以避免无谓的开销。如果表中的列很少,数据类型简单,SELECT * 查询的开销也许可以忽略不计;但一般来说,在查询中选择性地检索列是一种更好的做法。
💡 更多资讯,请关注 Bytebase 公号:Bytebase