尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MySQL后缀模糊查询优化:反向存储让LIKE ‘%xxx‘提速100倍

MySQL后缀模糊查询优化:反向存储让LIKE ‘%xxx‘提速100倍 下午刚睡醒同事就丢了句话过来“线上有个查询WHERE order_no LIKE %123456查单号后六位跑一次要两三秒订单表两千万行再这么搞下去数据库要被打爆了。”我一看这SQL就明白了。又是后缀模糊匹配又是%通配符开头。这类查询在MySQL里基本等于“全表扫描”的代名词任你索引建得再漂亮它也能给你绕过去把整张表从头到尾摸一遍。我先安抚了同事然后给出了一个听起来有点“反直觉”的方案——反向存储大法。把字符串倒过来存一份让原本在尾巴上的匹配条件跑到头部去索引就能用起来了。实测在一个百万级订单表上相同条件的查询时间从1.2秒压到了9毫秒提升超过100倍。这篇文章就把这个思路从原理到落地完整拆开讲包含加列、回填、改写SQL、验证效果的全过程也顺便把里面容易踩的坑一次性说清楚。1. LIKE %abc为什么慢到让人抓狂索引失效的底层逻辑1.1 一次典型事故按订单号尾部查单先还原一下当时的场景。业务上有个很常见的需求用户知道订单号的最后几位想要查出完整订单。例如订单号是DD20250101123456客服那边只记住了一个尾巴123456于是前端传参后端拼SQLSELECT * FROM t_order WHERE order_no LIKE %123456 ORDER BY create_time DESC LIMIT 20;这条SQL看着简单但LIKE %123456意味着只要字符串以任意内容开头、以123456结尾都算命中。MySQL的优化器看到这个条件时心里只有一句话没法用索引老实地全表扫吧。当时这个表两千多万行单条查询的扫描行数接近全表。好在后面还有LIMIT 20和排序否则情况更糟。慢日志里全是这条SQL平均执行时间2.8秒每次执行都要把几GB的索引页和数据页重新读一遍CPU和IO直接被拉满。1.2 最左前缀原则B树为什么帮不上“后缀查询”的忙要理解为什么LIKE %123456走不了索引得先搞懂InnoDB的B树索引到底是怎么工作的。B树的叶子节点按照索引键值从左到右有序排列。比如order_no列上建了普通索引那么索引页里存的就是order_no的完整值并且按字典序排好。一个查询想要用上这个索引优化器必须能算出“从哪个索引项开始扫、扫到哪里结束”也就是需要定位一个左边界和一个右边界。LIKE 123456%可以用索引是因为优化器知道所有以123456开头的字符串无论后面是什么索引排序后都连续落在一个区间里。它只需要在索引树上找到123456这个位置然后一路往后扫到下一个前缀不同的地方区间扫描就够了。但LIKE %123456完全不同。%放在最前面说明前面可以是任意字符。在B树的有序序列里abc123456、xyz123456、999123456这些值分散在各个角落根本没有“从某个位置开始连续扫描”这回事。优化器算不出边界只能放弃索引。这就是数据库里常说的最左前缀匹配原则索引有序性只能帮助“从左往右”的匹配通配符一旦出现在最左边索引就失效了。1.3 全表扫描和回表到底有多贵有人可能会问全表扫描就走一下呗能有多慢这里要掰开算一下。假设表的行平均大小是300字节两千万行数据大概占6GB左右。InnoDB默认页大小16KB一页大约能存50行左右那么全表扫描需要读40万个数据页。如果是机械盘一个页随机读耗时按5ms算扫完这些页理论耗时就要2000秒以上。即使是SSD随机读1ms左右也要400秒。当然InnoDB有预读机制实际情况不会这么夸张但一次扫描把几GB数据从磁盘搬到内存是躲不掉的。再加上还有个更隐蔽的问题如果是二级索引你能走索引不等于不用回表。InnoDB的二级索引叶子节点只存索引字段和主键值要拿其他列还得根据主键回聚簇索引查。索引帮你把候选集缩小到很小范围的时候回表成本可控但如果是全表扫描压根连“候选集缩小”这步都没有成本直接拉满。所以LIKE %abc这种查询的真正问题是它让B树索引的有序性完全失效把查询退化成了O(n)的全表扫描。SQL越频繁表越大死得越快。2. “反向存储大法”的思路把后缀变成前缀2.1 核心思想REVERSE()一反转后缀就成了前缀既然LIKE %123456的问题在于“匹配条件在字符串的尾巴上”那换个思路把字符串倒过来存尾巴不就变成头了吗MySQL有个内置函数REVERSE()作用就是把字符串整体反转。看几个例子SELECT REVERSE(DD20250101123456); -- 结果654321110105202DD SELECT REVERSE(123456); -- 结果654321注意原订单号的后缀123456反转后变成了654321而且正好出现在反转字符串的最前面。这时候查询条件从LIKE %123456就变成了LIKE 654321%。前者是后缀模糊匹配后者是前缀模糊匹配而前缀模糊匹配是可以用索引的。于是在表里新增一个列比如order_no_rev专门存REVERSE(order_no)的值。每次写入订单时同步写入反转值在这个反转列上建索引。查询的时候应用层先把用户输入的尾巴反转一下再拼上%去匹配反转列。效果就是原来的LIKE %123456被改写成LIKE 654321%索引生效扫描范围从全表缩小到所有“反转后以654321开头”的行。这个方法在行业内不算什么黑科技但知道的人确实不多。很多同学遇到LIKE %xxx就习惯性喷业务、喷SQL不规范其实数据库本身已经给了你一条路只是需要在存储层面做一点小小的冗余设计。2.2 适用场景与严格边界不是所有LIKE都能用这招反向存储大法解决的是后缀匹配问题也就是通配符只出现在左侧、右侧是确定字符串的场景。这个边界必须先说清楚LIKE abc%本来就能用索引不需要任何优化。LIKE %abc典型后缀匹配反向存储大法的目标场景。LIKE %abc%两边都有通配符匹配字符串在中间任意位置。反转之后abc虽然会反转成cba但原字符串中的abc前面还有别的内容、后面也有别的内容反转后cba依然落在中间还是无法作为前缀来匹配。所以反向存储对%abc%无效。LIKE %abc_ee%中间还有单字符通配符更复杂也不适用。另外即使查询形式是LIKE %abc如果abc这部分本身长度不固定、业务上就是个模糊框那也不一定能完全命中索引的边界因为优化器需要的是“反转后的前缀是一个可控区间”。实际编码时只要把输入参数反转后拼成cba%这个前缀就确定了索引就能用。还有一个需要特别说明的场景如果业务要求的是“字符串以abc结尾但abc本身是一个子串、前面必须再满足某些条件”比如WHERE status1 AND order_no LIKE %123456反向列同样能帮助后半部分但整体SQL是否走索引还要看status等其他条件的组合。这个下文会详细讲联合索引怎么配合。2.3 能提升多少先给结论在索引和数据分布合理的前提下效果提升是几个数量级的不是百分之几十的提升。我给同事做的那次实测原查询全表扫描耗时约1.2秒改完后走反向列索引只花了9毫秒。快133倍。标题里的100倍不是夸张是有实测依据的。但要注意这背后有个前提匹配到的行数必须很少。如果一查出来就是几十万行就算走了索引也快不了因为索引帮你省的是“扫描无关数据”的时间不是“返回命中数据”的时间。这个区别在第4节会详细讲。3. 从0到1实现反向存储完整落地实战3.1 方案A新增字段应用层维护通用兼容MySQL 5.7如果你的数据库是MySQL 5.7或者公司规范不允许用生成列最稳妥的方式就是显式加一列由应用层在写入时同步维护。整体分四步走。**第一步加列。**选业务低峰期执行DDL注意MySQL 5.7的ALTER TABLE会锁表或者做online DDL量大的表建议用pt-online-schema-change这类工具或者等维护窗口。ALTER TABLE t_order ADD COLUMN order_no_rev VARCHAR(64) NOT NULL DEFAULT COMMENT 订单号反转值用于后缀查询优化 AFTER order_no;列的长度要和原字段匹配不要拍脑袋定个固定值。如果订单号最长32位反转列也设VARCHAR(64)冗余一点无妨但不要太长太长的索引会更占空间、更慢。**第二步存量数据回填。**千万级以上的表不要直接UPDATE t_order SET order_no_rev REVERSE(order_no)一把梭会锁大量行、产生超大事务、把主从延迟拉满。正确姿势是分批更新比如按主键范围切段UPDATE t_order SET order_no_rev REVERSE(order_no) WHERE id BETWEEN 1 AND 100000 AND order_no_rev ;循环执行每批10万行以内观察主从延迟和CPU情况再决定下一批速度。如果表特别大建议在上线窗口做或者用数据订正平台。**第三步建索引。**回填完成后在反转列上建索引ALTER TABLE t_order ADD INDEX idx_order_no_rev (order_no_rev);如果业务上要求“尾巴唯一”也可以建唯一索引反转函数是数学上的单射函数原列不同的值反转后一定不同所以不会因为反转产生额外冲突唯一索引也不会误伤。**第四步改写写入逻辑和查询SQL。**写入时应用层在同一个事务里同时更新原列和反转列。比如Java里可以这样处理String orderNo DD20250101123456; String orderNoRev new StringBuilder(orderNo).reverse().toString(); // 同一个INSERT/UPDATE里 set order_no ?, order_no_rev ?查询时把用户输入的尾巴反转再拼上前缀匹配-- 原SQL SELECT * FROM t_order WHERE order_no LIKE %123456; -- 改后SQL SELECT * FROM t_order WHERE order_no_rev LIKE 654321%;这里我建议在应用层完成反转SQL里只传一个已经算好的前缀字符串不要偷懒在SQL里写REVERSE(#{param})。原因很简单SQL里出现函数优化器对索引列的应用方式和预编译参数的处理会变得复杂而且一旦写错表达式可能还是走不上索引。在应用层把123456算成654321SQL就只是个干净的LIKE 654321%没有任何歧义。3.2 方案BMySQL 8.0生成列/函数索引最省心MySQL 8.0.13开始支持函数索引底层实现就是隐藏的生成列。如果你能用MySQL 8.0甚至可以不加显式列直接建函数索引ALTER TABLE t_order ADD INDEX idx_order_no_rev ((REVERSE(order_no)));这样写入时数据库自动维护反转值查询时直接写SELECT * FROM t_order WHERE REVERSE(order_no) LIKE 654321%;MySQL会识别出REVERSE(order_no)这个表达式和函数索引的定义一致于是走索引扫描。不过这个写法有几个需要注意的地方表达式必须完全一致。你建索引时写的是REVERSE(order_no)查询时也必须是REVERSE(order_no)大小写、空格、换行都不能乱否则优化器可能认不出来。如果查询里写的是REVERSE(order_no) LIKE CONCAT(REVERSE(123456), %)虽然右侧最终是常量但优化器对表达式和常量的判定在不同版本上可能有差异不推荐在SQL里做运算。最稳妥的写法还是应用层把654321%算好SQL写成WHERE REVERSE(order_no) LIKE 654321%。函数索引查询时如果条件写成了WHERE REVERSE(order_no) REVERSE(?)这种等值匹配也会命中索引但那是另外的用法了不在本文讨论范围。如果你既要兼容5.7又不想在应用层维护可以用显式生成列ALTER TABLE t_order ADD COLUMN order_no_rev VARCHAR(64) GENERATED ALWAYS AS (REVERSE(order_no)) STORED, ADD INDEX idx_order_no_rev (order_no_rev);这种方案下写入时数据库自动维护反转列查询时直接查order_no_rev既省应用层代码又不会出现两个字段不一致的问题。唯一代价是存储空间增加一个列但换取查询效率值得。3.3 验证手段EXPLAIN和慢日志改完之后不要急着说“搞定”先跑一遍执行计划确认索引真的被用上了。EXPLAIN SELECT * FROM t_order WHERE order_no_rev LIKE 654321% LIMIT 20;重点看三列type应该是range这代表索引范围扫描。如果还是ALL说明没走索引。key应该显示idx_order_no_rev。rows扫描行数应该是个很小的数值比如几十或者几百。如果这里显示几百万说明优化器判断这个索引的区分度不够选择不用它。再看慢日志。慢查询时间阈值如果是1秒优化前的SQL应该天天上榜优化后这条SQL的耗时应该在几十毫秒以内慢日志里基本绝迹。把优化前后的执行计划和rows对比截图存个档后面做复盘或者跟领导汇报都有素材。4. 性能实测两个查询的差距到底有多大4.1 实验环境与数据准备拿真实案例说话。为了讲清楚这个优化效果我准备了一个实验环境MySQL 8.0.28跑在普通的8核16G云主机上SSD磁盘。表结构模拟真实订单CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, customer_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 1, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, order_no_rev VARCHAR(32) GENERATED ALWAYS AS (REVERSE(order_no)) STORED, INDEX idx_order_no_rev (order_no_rev) ) ENGINEInnoDB;用存储过程灌入200万行测试数据订单号格式模拟生产环境前缀字母时间戳随机数总共20位左右。最后6位随机分布保证区分度正常。DELIMITER $$ CREATE PROCEDURE gen_test_data() BEGIN DECLARE i INT DEFAULT 0; WHILE i 2000000 DO INSERT INTO t_order (order_no, customer_id) VALUES ( CONCAT(DD, DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -FLOOR(RAND()*365) DAY), %Y%m%d%H%i%s), LPAD(FLOOR(RAND()*1000000), 6, 0)), FLOOR(RAND()*100000) ); SET i i 1; -- 每1000条提交一次避免长事务 IF i % 1000 0 THEN COMMIT; END IF; END WHILE; END$$ DELIMITER ;4.2 实测结果对比分别执行两次查询第一次用传统写法第二次用反向列查询都加上LIMIT 20避免返回数据量差异影响判断。-- 传统写法 SELECT * FROM t_order WHERE order_no LIKE %482713 ORDER BY id DESC LIMIT 20; -- 反向列写法 SELECT * FROM t_order WHERE order_no_rev LIKE 317284% ORDER BY id DESC LIMIT 20;统计执行时间多次取中位数查询方式执行计划扫描行数平均耗时LIKE %482713typeALL全表扫描200万行1180msLIKE 317284%反向列typerange索引扫描约23行9ms扫描行数的差距直接决定了执行时间。反向列走的是B树定位直接跳到匹配前缀的起始位置只扫描那一小段区间的行传统写法要把200万行全部读一遍然后判段每一行的字符串结尾是否匹配。一个在“找”一个在“翻箱倒柜找”差距自然巨大。这里其实还没把回表代价算进去。反向列查询最终只回表20行回表成本忽略不计全表扫描虽然在LIMIT 20后不需要把200万行都返回但判断过程必须全表过一遍这等于把所有数据页都从磁盘加载一遍IO开销完全躲不掉。4.3 为什么提升效果是“100倍”而不是必然很多人看完这个结果会兴奋觉得“以后遇到LIKE后缀就反向存储”。别急有四个前提条件缺一个效果就大打折扣。前提一匹配结果集本身很小。如果业务要查的是手机号尾号0000而表里正好有几十万行都以0000结尾反向列索虽然能把扫描范围缩小到“反转后以0000开头”的行但这几十万行依然都要回表、都要返回。这种情况下优化器甚至可能评估出“走全表扫描比走索引更快”因为回表成本太高索引反而成了累赘。所以反转列建了不意味着一定走索引数据分布是底层约束。前提二后缀必须是“确定性文本”。你提供的查询条件得是一个明确的字符串而不是又一段模糊匹配。比如用户输入了一个%或者_拼到SQL里就会让前缀匹配重新变成模糊匹配索引虽然还能用但扫描范围可能扩大很多。应用层要做好转义或者直接限制输入不允许包含通配符。前提三写入频率不能高到无法承担维护成本。反向列每次INSERT/UPDATE都要多写一个字段生成列会由数据库自动算显式列则要应用层多拼一次反转索引也要跟着更新。对于写多读少的表这个代价可能会影响写入性能。判断标准很简单读多写少、查询频繁且单次查询贵适合做每天几亿次写入、查询很少的表不如保持原样。前提四表大、查询慢才值得优化。几千行的表全表扫也就几十毫秒没必要为了它引入额外列和维护成本。优化前先评估表行数和查询频率别把架构搞复杂。5. 避坑指南反向存储大法的九成坑都在这里5.1 问题1索引建了却没走——EXPLAIN看type和key最常遇到的情况就是索引建好了SQL也改了结果EXPLAIN一看还是typeALL白忙一场。排查方向有这几个表达式不一致。用函数索引时WHERE REVERSE(order_no) LIKE 654321%缺个空格、多写一个CAST都可能导致优化器匹配不上索引。解决办法是复制建索引时的表达式原样使用别凭感觉重写。参数类型不匹配触发了隐式类型转换。如果order_no_rev是VARCHAR而应用层传入的数字型参数拼进去MySQL会先把列转成数字再比较索引就废了。比如order_no_rev的值是654321...条件order_no_rev LIKE 654321%这种写法会导致全列转数字。建议参数保持字符串类型不要从数字类型字段直接传。排序规则不一致。反向列和原列的字符集、collation最好保持一致。如果原列是utf8mb4_bin生成列默认继承了原列的collation这通常没问题但手工DDL时如果没指定列可能会采用默认utf8mb4_0900_ai_ci大小写处理方式不一样可能导致匹配结果和预期不同。优化器认为全表扫描更快。如果匹配行数占比超过10%~20%优化器大概率会选择全表扫描。这时你要审视业务是不是后缀太短、太集中能不能通过增加长度、加其他等值条件让结果集变小5.2 问题2rev_col和原数据不一致——回填与并发写显式列方案里最容易出问题的就是数据一致性。特别是有存量数据要回填的时候如果刚回填完一批业务又更新了原列而应用层没同步更新反转列就会出现“原列尾巴是123456反转列却没有以654321开头”的尴尬情况查询直接漏数据。我的建议是如果数据库支持生成列优先用生成列从源头杜绝不一致。如果只能用显式列配合触发器来维护而不是完全依赖应用层代码CREATE TRIGGER trg_order_no_rev_insert BEFORE INSERT ON t_order FOR EACH ROW SET NEW.order_no_rev REVERSE(NEW.order_no); CREATE TRIGGER trg_order_no_rev_update BEFORE UPDATE ON t_order FOR EACH ROW SET NEW.order_no_rev REVERSE(NEW.order_no);这样无论应用层写成什么样数据库兜底保证一致性。存量数据回填完后可以跑一段校验SQL把不一致的记录捞出来SELECT COUNT(*) FROM t_order WHERE order_no_rev REVERSE(order_no);我习惯把这个校验做成定时任务每半小时跑一次即使有异常也能及时发现。5.3 问题3字符集、大小写、空格、通配符的连环坑这一节全是血泪经验每一个都坑过人。大小写问题。MySQL默认的utf8mb4_0900_ai_ci排序规则不区分大小写所以LIKE ABC和LIKE abc效果一样。如果你原列用的是_bin排序规则反转列也必须是_bin否则会出现“原列查得到、反转列查不到”的情况因为反转列的collation可能把大小写差异忽略掉造成结果集膨胀或缩小。建列时明确指定collation别依赖默认值。尾部空格。如果原列是VARCHAR且存储了尾部空格REVERSE后空格会跑到字符串最前面。比如abc123 反转成 321cba这时候匹配LIKE 321cba%反而匹配不上因为前缀是空格。解决办法是写入时统一TRIM或者在定义生成列时用REVERSE(TRIM(order_no))。但要注意如果你TRIM了原列原列上基于原始值的其他查询可能受影响需要全盘考虑。中文和emoji。MySQL的REVERSE()按字符反转不是按字节反转所以多字节字符不会乱码。比如订单123反转成321单订中文整体移动没问题。但要注意如果一个查询条件是中文后缀比如LIKE %订单反转后就是单订%匹配反向列时索引依然有效这个可以放心用。通配符转义。用户如果输入了_或%比如查尾巴是a_b数据库会把_当成单字符通配符LIKE b_a%可能匹配出意料之外的记录。如果业务允许用户输入任意文本建议在拼接SQL前先对%和_做转义SELECT * FROM t_order WHERE order_no_rev LIKE CONCAT(REVERSE(REPLACE(REPLACE(?, %, \\%), _, \\_)), %)或者更干脆在应用层用参数化查询并显式指定ESCAPE字符。5.4 问题4区分度低的“假命中”场景反向存储真正要防的是“索引命中了但性能依然拉胯”的情况。举个真实例子某个表有个字段存的是邮箱域名后缀固定是qq.com业务还非得查LIKE %qq.com。你做了反转列结果反转列里几千行的前缀都是moc.qq索引确实能定位到这个前缀但扫出来的行数占全表80%。这时候走索引要回表几千次还不如全表扫。最终EXPLAIN里type依然显示range但rows数量大得惊人查询照样慢。遇到这种低区分度后缀反向列只是把“全表扫”变成“索引扫大片区域”治标不治本。真正的解法是调整业务要么改成前缀匹配要么把后缀本身拆出来作为独立字段比如单独存email_domain列要么配合其他高区分度等值条件一起用让联合索引先通过其他字段把范围缩小。5.5 常见问题速查表现象可能原因排查/修复方式索引建了但EXPLAIN还是ALL表达式不一致、类型转换、优化器判断低区分度检查SQL表达式EXPLAIN看type/key/rows确认匹配行数占比查出来的数据少了反转列和原列不一致运行WHERE order_no_rev REVERSE(order_no)校验清理后重刷大写字母能查到小写查不到排序规则不一致统一两列collation为utf8mb4_bin或utf8mb4_0900_ai_ci匹配结果比预期多用户输入%/_被当通配符应用层转义或SQL使用ESCAPE反转列写入性能变差生成列计算索引更新开销评估写放大必要时改用显式列应用层维护必要时换方案后缀区分度低走了索引也慢匹配行数占比过高拆独立后缀列其他等值条件联合索引或考虑全文索引6. 反向存储的进阶玩法与替代方案6.1 固定后缀截取列比反转更朴素但更高效如果业务场景里“后缀”长度是固定的比如永远查订单号后6位、手机号后4位、身份证后6位那可以不反转直接拆一个固定后缀列出来ALTER TABLE t_order ADD COLUMN order_no_tail6 VARCHAR(6) GENERATED ALWAYS AS (RIGHT(order_no, 6)) STORED, ADD INDEX idx_order_no_tail6 (order_no_tail6);查询写法SELECT * FROM t_order WHERE order_no_tail6 123456 LIMIT 20;注意这时连LIKE都不需要了直接等值匹配索引效率更高。因为等值匹配在B树上只要定位一次连范围扫描都省了。固定长度后缀列比反转列的优势在于索引更窄只需要存6个字符排序规则更简单查询表达式没有函数。缺点是适用面窄只能处理“固定长度后缀”这一种模式。如果需求变化比如要按“最后7位”查又得加一列。反转列则一劳永逸任何长度的后缀都能匹配。两种方案选哪个我倾向于后缀长度固定且不会变优先拆固定列后缀长度不固定用反转列。6.2 中间匹配%abc%怎么办全文索引与ngram前面说了LIKE %abc%这种中间匹配反转列救不了。真遇到这种需求可以考虑MySQL的全文索引。InnoDB默认的全文索引不支持中文分词需要ngram parser插件。建索引时指定语法ALTER TABLE t_order ADD FULLTEXT INDEX ft_order_no (order_no) WITH PARSER ngram;查询时用MATCH ... AGAINSTSELECT * FROM t_order WHERE MATCH(order_no) AGAINST(123456 IN BOOLEAN MODE);全文索引能覆盖一部分中间匹配场景但它不是万能的全文索引有自己的评分机制和停用词规则对短字符串、高频词支持不好而且MATCH的语法和LIKE不完全等价需要应用层配合。如果MySQL全文索引也搞不定那就只能考虑外置搜索引擎或者把数据冗余到专门的检索系统里了。不过这是另一个话题这里点到为止。6.3 顺带回答a and b的联合索引到底怎么建写这篇时看到很多人在问“MySQL where条件a and b应该怎么建索引”这正好和反向列的使用场景有交集。如果一条SQL里有等值条件也有LIKE %xxx反向匹配联合索引的顺序很关键。通用原则是等值条件放前面范围/排序条件放后面。比如查询条件WHERE status 1 AND order_no_rev LIKE 654321%联合索引建议这么建ALTER TABLE t_order ADD INDEX idx_status_rev (status, order_no_rev);先通过status1这个等值条件把结果集缩小再用order_no_rev的索引有序性做范围扫描。反过来如果索引列顺序是(order_no_rev, status)那么LIKE前缀匹配之后还要用status做过滤虽然也能用索引但范围扫描部分会带着一堆status ! 1的行做回表效率不如前者。这个道理和主键索引、唯一索引的选择一样先想清楚查询里哪些条件是等值、哪些是范围再定索引顺序。顺便说一句主键索引和唯一索引的区别主键索引是InnoDB的聚簇索引决定了数据在磁盘上的物理存储顺序一个表只能有一个主键唯一索引只保证索引列值不重复是一个二级索引可以有多个。反向列如果需要保证“尾部不重复”完全可以用唯一索引它不会和主键冲突因为两者是不同列的索引。写在最后的一点体会反向存储大法不是什么新概念它本质上是在跟B树索引的有序性“做朋友”——你改变不了一棵有序树的定位方式但你可以改变数据的形状让无序的需求变得有序。我做了这么多年数据库优化越来越觉得很多慢SQL不是玄学而是存储设计没有跟上查询形态。你要是早点把“数据以什么形态被查询”想清楚很多性能问题都能在源头化解。不过这招也不是万能的。它适合“后缀匹配、结果集小、读多写少”的场景判断要不要用先跑一条EXPLAIN看全表扫描的rows有多大再预估一下命中行占比心里有数了再动手。最后再提醒一句无论用什么方案上线前一定要验证数据一致性跑一遍WHERE rev_col REVERSE(col)这种校验SQL别让索引救了性能、坑了准确性。
返回列表