在写SQL更新语句时,很多人习惯把时间字段用函数处理后再做条件判断,比如按天过滤记录。这种写法表面上逻辑正确,但放到有索引的表上,常常让更新性能断崖式下跌。理解优化器对索引的使用规则,就能用很小的改动换回明显的效率提升。

一、为什么对索引列用函数会让更新变慢
数据库索引本质上是按列原始值构建的有序结构,例如B+树索引会依据create_time字段的真实时间戳排序。当更新语句的WHERE条件写成WHERE DATE(create_time) = '2023-05-01'时,优化器需要先对每一行的create_time调用DATE函数,得到结果后再和常量比较。由于索引里存的是原始值,函数计算后的结果并不在索引顺序中,优化器无法利用索引做快速定位,只能选择全表扫描,逐行计算函数并判断。
全表扫描意味着即使你只想更新一百行,数据库也要读取整张表的数据页,在大数据表上这会直接拖垮更新性能,还会增加锁持有时间,影响并发。我们可以通过EXPLAIN看到,这类语句的type列通常显示ALL,key列为NULL,而改写后则能见到ref或range以及具体的索引名。
二、常见的错误写法与正确改写
下面是一段典型的慢更新语句,在百万级订单表上对索引列使用函数:
-- 错误写法:对索引列create_time使用DATE函数 UPDATE orders SET status = 'closed' WHERE DATE(create_time) = '2023-05-01';
这个语句会让优化器忽略create_time上的索引。正确的思路是把函数从列上移开,改为对比较值做范围限定,让原始列可以直接比对:
-- 正确写法:使用范围条件,保留索引列原始形态 UPDATE orders SET status = 'closed' WHERE create_time >= '2023-05-01 00:00:00' AND create_time < '2023-05-02 00:00:00';
改写后,优化器能识别这是一个闭开区间,直接通过索引定位到对应时间片的数据页,更新所涉及行数相同,但逻辑读从全表级别降到仅命中索引叶子节点附近。如果业务只需要日期部分,也可以提前在应用层算出起止时间字符串再传入,避免在SQL里写函数。
其他容易踩坑的函数场景
- 对数值索引列使用
ABS(amount) = 100,应改为amount = 100 OR amount = -100或调整业务不取绝对值。 - 对字符串索引列使用
UPPER(name) = 'TOM',可改为在存入时就统一大小写,或建一个函数索引(部分库支持)。 - 使用
YEAR(created_at)过滤年份,应改为created_at >= '2023-01-01' AND created_at < '2024-01-01'。
三、函数索引是否是万能解
有些数据库如MySQL 8.0、PostgreSQL支持创建函数索引,也就是对DATE(create_time)单独建索引。语法类似:
-- MySQL 8.0 函数索引示例 CREATE INDEX idx_date_ct ON orders ((DATE(create_time)));
函数索引确实能让原语句重新走索引,但它会占用额外存储空间,并且在写入时数据库要同时维护原列索引和函数索引,更新和插入开销变大。对于只是偶尔执行的更新,不如直接改写SQL;对于高频按函数查询的场景,函数索引才更划算。调优时应先尝试改写,再评估是否建函数索引。
四、批量更新时的补充建议
即便改写了条件,一次更新几十万行仍可能锁表过久。建议结合limit分批,并利用索引列排序来让每批都走索引:
-- 分批更新示例,利用主键和索引列控制范围 UPDATE orders SET status = 'closed' WHERE create_time >= '2023-05-01 00:00:00' AND create_time < '2023-05-02 00:00:00' AND id > 0 ORDER BY id LIMIT 1000;
每批千行左右,循环执行直到受影响行为零,可以把长事务拆短,降低主从延迟和锁冲突。同时务必确认WHERE里所有条件字段组合起来仍能命中索引,否则分批也救不了全表扫描。掌握这些技巧后,面对类似更新慢的问题就能快速定位是否为索引列被函数包裹导致。