<p>SELECT * 在生产环境危险,不仅慢还可能拖垮数据库连接池;它会全字段读取、放大IO和网络开销,尤其含TEXT/BLOB时;应显式指定字段、用SELECT 1判断存在性、拆分大字段、建覆盖索引并避免函数索引失效。</p>

为什么 SELECT * 在生产环境里是个危险操作
它不只是慢,更可能拖垮整个数据库连接池。MySQL 会把所有字段数据从磁盘读到内存、经过网络发给客户端,哪怕你只用其中一两个字段。尤其当表里有 TEXT、BLOB 或多个大字段时,单行数据体积可能暴涨几倍,IO 和网络开销直线上升。
实操建议:
- 永远显式写出需要的字段名,例如
SELECT id, name, status FROM users,而不是SELECT * - 如果只是做存在性判断(比如检查某条记录是否存在),用
SELECT 1配合LIMIT 1 - ORM 框架中关闭默认全字段映射,例如 MyBatis 的
resultMap显式定义字段,Django 的.values('id', 'name')或.only('id', 'name') - 注意视图或子查询里也隐藏着
*,要逐层展开检查
JSON 字段和大文本字段必须单独评估是否查询
JSON 类型字段在 MySQL 8.0+ 虽支持路径查询(如 data->'$.user_id'),但整字段加载仍会触发完整解析和内存分配;TEXT 字段哪怕只查一行,也可能因长度不可控导致临时表落磁盘(Using temporary; Using filesort)。
实操建议:
- 除非业务明确需要解析整个 JSON 结构,否则优先用生成列(generated column)提取关键字段并建索引,例如:
ALTER TABLE logs ADD COLUMN user_id INT AS (json_unquote(json_extract(data, '$.user_id'))) STORED;
- 对
TEXT字段做模糊搜索时,避免WHERE content LIKE '%xxx%',改用全文索引或外部搜索引擎 - 日志类表中,把元数据(
created_at、level、trace_id)和正文(message)拆到不同表,查询列表页只联查元数据表
联合查询中字段选择直接影响执行计划
MySQL 优化器在决定是否使用覆盖索引(covering index)时,会严格比对 SELECT 列表和索引列的完全匹配。哪怕多选一个没被索引的字段,就会强制回表(Using where; Using index 变成 Using where),性能断崖下跌。
实操建议:
- 用
EXPLAIN检查Extra列:出现Using index表示走覆盖索引;出现Using where且没有Using index,说明要回表 - 如果经常按
status和created_at查询用户列表,并只展示id、name、status,那就建联合索引:INDEX idx_status_created (status, created_at, id, name)—— 把 SELECT 字段也塞进索引末尾 - 避免在 JOIN 中无脑
SELECT a.*, b.*,先确认关联表哪些字段真被用到,再精简
时间字段和计算字段容易被忽略的陷阱
DATETIME 字段本身不重,但加上函数(如 DATE(created_at)、YEAR(created_at))就无法走索引;而 COUNT(*)、SUM(amount) 这类聚合字段,如果没加 GROUP BY 却又 JOIN 多表,极易引发笛卡尔积放大结果集。
实操建议:
- 日期范围查询统一用
WHERE created_at >= '2024-01-01' AND created_at ,别写 <code>DATE(created_at) = '2024-01-15' - 聚合字段务必确认分组逻辑是否正确,例如
SELECT COUNT(*), SUM(price) FROM orders o JOIN order_items i ON o.id = i.order_id会把订单下每个商品行都算一次,应改用子查询或先聚合再 JOIN - 业务上需要“最近 7 天订单数 + 总金额”,不要在一个 SQL 里硬凑,拆成两条带
WHERE条件的COUNT和SUM更稳
字段选择不是语法问题,是数据通路的设计问题。最常被绕过的点是:你以为只少选了一个字段,其实它让优化器放弃覆盖索引、让 JSON 解析阻塞线程、让 JOIN 结果膨胀十倍——这些都不会报错,只会慢慢变慢。










