SQLite查询中为什么要慎用SELECT *?

来源:网站建设作者:小鱼头衔:草根站长
导读:本期聚焦于小鱼创作的《SQLite查询中为什么要慎用SELECT *?》,敬请观看详情。SQLite在执行SELECT *时并不会像想象中那样只是一次简单的数据读取。查询优化器需要先访问sqlite_master获取表的所有列定义,随后在每行数据上按列序号逐一提取值。如果业务代码只关心其中三列,却要求数据库返回全部二十列,那么每一页的数据读取量、反序列化成本和结果集的内存占用都会明显增加。更隐蔽的问题发生在表结构变更之后:新增字段会让旧代码拿到的列数发生变化,使用SELECT * 的代码往往不会报错,但字段映射会悄然错位。要准确评估这一影响,可以借助EXPLAIN QUERY PLAN观察扫描方式,并通过统计执行时间与内存变化进行对比。本文从存储引擎、表结构稳定性和性能测试三个角度说明为何在SQLite中应当慎用SELECT *,并给出可落地的替代方案。

在SQLite应用开发中,SELECT * 是一种看似省事的写法,但它会让数据库执行大量无意义的列读取操作。由于SQLite是嵌入式数据库,数据文件和应用程序往往在同一台机器上,这种浪费不会被网络延迟掩盖,反而在本地I/O和内存分配上表现得更加直接。文章接下来会从存储引擎、表结构变更和性能测试三个角度分析这个问题。

SQLite查询中为什么要慎用SELECT *?

一、SELECT * 给存储引擎带来的额外负担

SQLite的底层存储采用B-tree结构,每一行数据按照列的顺序紧凑存放。当执行SELECT *时,数据库需要先从sqlite_master系统表中读取该表的所有列元数据,包括列名、类型和序号。随后在扫描数据页时,每一行的所有列都要被解码为对应的SQL值。这里的关键在于,即使业务代码最终只使用其中两列,数据库也没有办法自动推断出来,它只能按照你写的SELECT *去读取全部列。

举个例子,假设users表有id、username、email、password_hash、created_at、last_login、avatar_url等20个字段。如果只需要展示用户名和注册时间,SELECT username, created_at FROM users WHERE id=1;这条语句会让SQLite只提取两个列。而SELECT * FROM users WHERE id=1;则会把所有20个字段都读出来,包括可能很大的avatar_url字符串和加密后的密码散列。额外读取并不会让结果集更正确,只会增加磁盘页的读取次数和CPU反序列化时间。

可以通过EXPLAIN QUERY PLAN查看执行计划,虽然它不会直接显示读取列数,但结合sqlite3_trace或STAT虚拟表,可以看到实际扫描的字节数差异。下面的示例展示了如何查看查询计划:

EXPLAIN QUERY PLAN
SELECT * FROM users WHERE id > 100;

输出通常会显示SCAN users或者SEARCH users USING INTEGER PRIMARY KEY,但这只是一个步骤。真正的列读取成本体现在VDBE指令中。使用EXPLAIN可以进一步查看具体操作码,SELECT * 对应的Column和ResultRow指令会明显多于显式列名。

二、表结构变更时SELECT * 引发的隐性错误

SQLite的ALTER TABLE支持有限,但ADD COLUMN和DROP COLUMN(3.35版本后)已经比较常见。当一个表新增字段后,使用SELECT * 的查询返回的列数会立即变化。如果你的应用程序按照列索引取值,比如ORM或手动映射,就会发生字段错位。更糟糕的是,这种错误不会报语法错误,因为SQLite不会校验接收方期望的列数。

例如Python自带的sqlite3模块中,row[3]原本指向email字段。某天管理员执行ALTER TABLE users ADD COLUMN bio TEXT;之后,row[3]可能就不再是email,而是旧的第4列被挤到第5列,代码中所有基于序号的下标都会静默取错数据。这种问题在测试环境通常难以暴露,因为测试库和线上库的表结构往往已经不一致。

import sqlite3
conn = sqlite3.connect('app.db')
cur = conn.cursor()
cur.execute('SELECT * FROM users LIMIT 1')
row = cur.fetchone()
# 假设原本第4列是email,新增列后第4列已经变成别的字段
print(row[3])

显式列出列名可以彻底避免这类问题。当表结构发生变更时,如果查询中引用了不存在的列,SQLite会直接抛出错误,开发者能立刻发现。而使用SELECT * 则永远会成功执行,并把问题推迟到业务逻辑读取数据的那一刻。

三、量化性能损耗:从扫描列数到返回结果集

性能损耗可以从三个维度衡量:磁盘I/O、CPU解码和结果集内存。SQLite以页为单位读取数据,每页大小默认4096字节。如果一行数据包含大文本或BLOB字段,SELECT * 可能多读几十个页。对于只需要小字段的查询,这种浪费尤其明显。覆盖索引也会失效:如果查询只涉及某个索引中的列,SQLite本可以直接从索引中返回结果,但SELECT * 强制访问主表,因为索引不可能包含所有列。

下面做一个简单对比。创建包含10万行、20个字段的表,其中一个字段为TEXT类型并填充较长字符串。然后分别执行SELECT id, username和SELECT *,记录执行时间。在本地SQLite环境中,前者的耗时通常只有后者的三分之一到四分之一,内存峰值差距更大。

-- 创建测试表
CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  username TEXT NOT NULL,
  email TEXT NOT NULL,
  password_hash TEXT NOT NULL,
  bio TEXT,
  avatar_url TEXT,
  settings TEXT,
  created_at INTEGER,
  updated_at INTEGER,
  last_login INTEGER,
  status INTEGER,
  role TEXT,
  locale TEXT,
  timezone TEXT,
  email_verified INTEGER,
  phone TEXT,
  address TEXT,
  city TEXT,
  country TEXT,
  notes TEXT
);

使用显式列名时,SQLite可以只提取索引或部分列;而SELECT * 会读取所有20个字段。如果表上有覆盖索引CREATE INDEX idx_users_id_username ON users(id, username);,那么SELECT id, username FROM users WHERE id > 50000可以利用该索引直接返回结果,不需要回表。但SELECT * 无法利用该索引避免回表,因为大部分字段不在索引里。

可以通过EXPLAIN QUERY PLAN确认:第一个查询会显示SEARCH users USING COVERING INDEX idx_users_id_username,第二个只会显示SEARCH users USING INTEGER PRIMARY KEY或者SCAN users。这个差异直接对应了I/O数量的差距。

四、替代SELECT * 的几条实用建议

首先是养成显式列名习惯。在编写SQL时,主动写出需要的每一列,即使有20个字段也值得。这不仅有利于性能,还能让SQL语句成为接口文档的一部分。阅读代码的人能立刻知道查询返回了哪些数据,而不需要去查表结构。

其次是利用ORM的字段白名单机制。很多ORM默认会生成SELECT *,但都提供了只选择部分字段的API。例如SQLAlchemy的query(User.username, User.email),或者Django ORM的values('username', 'email')。限制返回列后,ORM提取结果时也不会因为未知字段而做额外处理。

from sqlalchemy import select
# 只选择需要的列
stmt = select(User.username, User.email).where(User.id >= 1000)
results = session.execute(stmt).all()

如果确实需要全部列,例如做备份导出或生成完整对象,可以使用SELECT *,但应当明确知道这样做会读取所有字段,并评估数据量。对于数据迁移脚本,建议采用SELECT列名列表而不是*,避免源表和目标表结构差异导致混乱。

最后是定期审查。使用SQLite的sqlite3_profile或应用程序日志,记录慢查询。发现SELECT * 出现在高频路径中时,立即改为显式列名。可以把这条规则加入代码评审规范,比如在团队风格指南中写明:业务查询禁止使用SELECT *,数据分析脚本除外。

SQLiteSELECT *查询性能修改时间:2026-09-18 22:05:59

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0918/58983.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。