SQL Server索引选择实战:从查询优化器到慢查询诊断与索引设计 1. 一个跑了二十分钟的查询让我重新审视索引选择先说一个真实场景。前一阵子帮一个朋友排查生产库的慢查询现象很简单一个订单汇总报表数据量一千多万行以前跑三十秒左右最近突然变成二十分钟而且每天跑一次用户天天催。我把执行计划抓出来第一步看的就是SQL Server的索引选择。结果发现两张大表做Hash Join时优化器把只有几万行的小表设成了探测输入却把大表整个扫描了一遍去生成哈希表。表面上看是连接顺序问题根子却出在统计信息和索引可用性上——优化器做索引选择时严重依赖统计信息里的分布数据来估算每列的选择性而这张大表的统计信息已经三个月没更新直方图和真实数据分布早就对不上了。这类问题我处理过太多次所以我一直认为SQL Server的索引选择不是一个建几个索引就完事的简单话题而是一整套从查询优化器决策机制、统计信息维护、索引设计取舍到执行计划分析的综合体系。只有吃透这个体系你才能回答那些长期困扰开发人员的灵魂拷问为什么我建了索引查询还是慢为什么同一个查询有时快有时慢为什么优化器就是不选我想要的那个索引这篇文章不是我凭空编出来的理论而是我这些年排查线上数据库慢查询时反复用到的一套方法。我会从优化器选择索引的底层逻辑讲起然后给出一套可复用的诊断链路再拆解几个最容易让索引选择失效的典型场景最后用一个真实的生产优化案例把完整过程串起来。无论你是正在调接口性能的后端开发还是刚接手SQL Server维护的DBA这套思路都能直接拿去用。2. 查询优化器到底是怎么挑索引的成本估算、选择性、统计信息的三角关系要理解索引选择先要理解做选择的这个角色——查询优化器Query Optimizer。它的工作方式可以类比成你出门前用导航软件选路线导航会收集当前路况数据估算每条路的通行时间然后挑一条它认为最快到达的路线。SQL Server的优化器也一样它会基于当前的路况信息——也就是统计信息Statistics——来估算执行计划中每个步骤的代价最后选它认为总代价最小的那一个计划。2.1 统计信息优化器的眼睛统计信息在SQL Server里的角色极其重要。它记录了每个字段的分布情况最核心的数据结构是直方图Histogram和密度Density。直方图把字段值分成若干个区间记录每个区间有多少行密度则反映不同值的数量。优化器拿到一个查询条件比如WHERE Status Active它就去统计信息里查Active这个值在直方图里落在哪个区间、这一区间大概有多少行从而估算出这条查询会返回多少行也就是选择性Selectivity。选择性是索引选择的关键指标。通常来说能过滤掉90%以上数据量的条件就适合走索引的Seek操作只能过滤10%的优化器可能会觉得干脆把整张表扫描一遍反而更划算。但这个判断完全依赖统计信息的数据准确性——如果统计信息过期直方图显示某个值只有一行实际却有一百万行优化器就会选错索引。2.2 成本模型的代数学优化器并不会真的执行查询来决定快慢它只是做代数题。它会给全表扫描Table Scan / Clustered Index Scan、索引查找Index Seek、Key Lookup书签查找、Hash Join、Nested Loop Join、Sort等操作分别估算IO代价和CPU代价然后加总。这些估算值的背后是SQL Server内建的一套成本模型包含了对随机I/O和顺序I/O的差异、内存中的算子开销等因素的建模。一个经典的成本判断场景是非聚簇索引上找到100行满足条件的记录但查询要返回的列不全在索引中每行都需要通过主键再到聚簇索引上做一次Key Lookup。如果满足条件的行数很少比如十几行优化器认为Key Lookup的随机I/O可以接受所以会走非聚簇索引Seek Key Lookup如果满足条件的行数非常大比如几十万行优化器会认为每次都要随机I/O太贵了不如直接全表扫描聚簇索引顺序读一遍反而快。这个临界点没有绝对的数字取决于行宽、页大小、缓存命中率等因素但方向是明确的——优化器永远在计算哪种读法更便宜。2.3 为什么优化器会选错索引很多人一遇到慢查询就骂优化器是傻子其实优化器绝大多数时候是被蒙蔽了。常见的蒙蔽手段有统计信息过期数据量早就变了但统计信息还是老样子参数嗅探Parameter Sniffing第一次用某个参数值编译出来的计划被缓存后面遇到不同分布的值仍然复用旧计划索引缺失导致没有更好的选择优化器只能在现有索引中做选择你没有给它更多备选方案它只能在矮子里拔将军升序键问题Ascending Key Problem新插入的数据一直超过直方图记录的最大值而统计信息还没更新优化器会产生0行估算选出的计划自然离谱。提示判断一个计划选错了不是看它有没有走某个索引而是要看它在当前统计信息和成本模型下的估算行数是否严重偏离实际行数。有时候逻辑读很高但优化器估算的也是这个数那不一定是优化器的问题可能是统计信息的问题。3. 索引选择的完整诊断链路从慢查询到最优索引这部分我直接给一套可复用的排查链路。我自己处理线上慢查询时基本就按这几步来很少空跑。3.1 第一步抓执行计划看实际选择的索引抓执行计划是排查的第一步也是最基础的一步。如果是临时排查一个查询最简单的方法是在SSMS里选中SQL语句按下CtrlM开启包含实际执行计划然后执行在执行完的结果面板里就能看到图形化执行计划。更严谨的做法是用SET STATISTICS IO ON和SET STATISTICS TIME ON两个开关把每条语句的逻辑读次数、物理读次数、CPU时间和耗时打出来。SET STATISTICS IO ON; SET STATISTICS TIME ON; -- 你的查询 SELECT o.OrderId, c.Name, o.TotalAmount FROM dbo.Orders o INNER JOIN dbo.Customers c ON o.CustomerId c.Id WHERE o.OrderDate 2025-01-01 AND o.Status Active;看执行计划时重点关注三件事第一每条表访问操作是Index Seek、Index Scan还是Table Scan第二Seek后面的谓词Predicate是落在了索引键上还是落在了普通列上第三有没有Key Lookup或RID Lookup以及它们的行数有多大。执行计划里的每一个算子旁边会显示估计行数你可以对比实际行数如果差距非常大那基本可以断定统计信息或者参数嗅探出了问题。3.2 第二步用动态管理视图看索引的使用率光看单条查询还不够要系统排查一个库里哪些索引在空转、哪些查询缺索引可以借助两个核心的DMV。第一个是sys.dm_db_missing_index_details和它的几个关联视图。SQL Server在每次查询编译时会记录它想要但没找到的索引信息。虽然这些信息在服务器重启后会清空但在一台稳定运行了较久的库上仍然很有参考价值。SELECT TOP 20 d.statement, d.equality_columns, d.inequality_columns, d.included_columns, s.unique_compiles, s.user_seeks, s.user_scans, s.avg_user_impact FROM sys.dm_db_missing_index_details d CROSS APPLY sys.dm_db_missing_index_group_stats s ON d.index_group_handle s.group_handle WHERE d.database_id DB_ID() ORDER BY s.avg_user_impact DESC;第二个是sys.dm_db_index_usage_stats它能告诉你库里每个索引被使用了多少次以及被更新了多少次。这个视图特别适合用来发现僵尸索引——建了一堆索引但user_seeks和user_scans长期是0反而是user_updates很高说明每次写操作都要付出维护索引的代价这些索引应该在确认没有特殊用途后考虑删除。SELECT OBJECT_NAME(s.object_id) AS TableName, i.name AS IndexName, s.user_seeks, s.user_scans, s.user_lookups, s.user_updates FROM sys.dm_db_index_usage_stats s INNER JOIN sys.indexes i ON s.object_id i.object_id AND s.index_id i.index_id WHERE s.database_id DB_ID() AND OBJECTPROPERTY(s.object_id, IsUserTable) 1 ORDER BY s.user_updates DESC;3.3 第三步分析等待类型和查询特征索引选择问题有时候不会直接体现在某一条SQL的执行计划上而是表现在系统的整体等待上。我通常习惯在排查完单条SQL后再看一眼sys.dm_os_wait_stats或者用现成的脚本看历史等待。PAGEIOLATCH_SH或PAGEIOLATCH_UP等待占比高通常说明磁盘I/O是瓶颈而索引选择不当导致的大量扫描会让这个等待异常突增。PAGE_LATCH_*等待突增可能是查询在做大量页分裂或页访问竞争和索引键设计有关。CXPACKET等待高通常出现在并行计划中可能也和优化器选择了一个需要大量并行扫描的执行计划有关。当你发现某类等待持续偏高就可以顺藤摸瓜找到相关SQL然后回到第一步去抓执行计划。等待统计本身不直接告诉你索引应该怎么建但它会告诉你哪里该深入排查。3.4 第四步综合设计新的索引方案诊断完问题最后一步才是设计索引。这里我的原则是先尽量复用现有索引不要急着新建。很多索引问题其实可以通过改写SQL来解决比如把WHERE YEAR(OrderDate) 2025改成WHERE OrderDate 2025-01-01 AND OrderDate 2026-01-01让条件成为一个可以Seek的范围而不是无法使用索引的函数表达式。确认SQL无法改写或改写收益有限后才去考虑新增索引。新增索引时要综合考虑等值条件、范围条件、排序字段和SELECT中需要回表的列用合理的顺序填充索引键和INCLUDE列。4. 索引类型与设计取舍聚簇、非聚簇、覆盖、过滤到底怎么选这一节我把SQL Server里几种常用索引的选择思路捋一遍。很多人背概念没问题但一到实际设计就犯难核心原因是没有把查询访问模式和索引结构对应起来。4.1 聚簇索引表的物理排序不要随便建聚簇索引决定表的物理存储顺序一个表只能有一个。它的叶子节点就是数据本身所以聚簇索引的Seek实际上就是直接定位到数据行不需要额外的回表操作。选择聚簇索引键时最理想的几个特征唯一、递增、宽度小、不会被频繁更新。自增主键就是最经典的选择。反之用GUID做主键是一个常见的反面案例——由于GUID是随机的插入时会造成大量的页拆分和碎片数据量大了以后性能会肉眼可见地下降。如果业务表本身没有合适的主键可以考虑用一个自增的BIGINT列做代理键。但这并不绝对有些表使用业务唯一键作为聚簇索引其实更合理但要评估好插入模式和查询模式。在OLTP环境里我的经验是不太建议把聚簇索引建在有很多重复值的列上比如状态列、类型列因为重复值多会导致插入时频繁存在叶子节点的位置竞争和页分裂。4.2 非聚簇索引索引键顺序决定能不能Seek非聚簇索引的叶子节点不包含完整数据行只包含索引键和聚簇索引键或RID。设计一个非聚簇索引索引键列的顺序非常关键。一个通用原则是等值条件的列放在前面范围条件的列次之影响排序或分组的列再往后放最后考虑把SELECT中需要的其他列放进INCLUDE。为什么等值列要在前面因为等值列可以使用索引的前缀Leftmost Prefix规则让优化器能Seek到精确的起点。如果你把范围条件放在最前面后面的等值列就无法参与Seek定位只能做过滤器Predicate索引利用效率大打折扣。4.3 覆盖索引避免Key Lookup的银弹当非聚簇索引包含了查询所需要的全部列时就不需要回表了这个索引就是覆盖索引Covering Index。覆盖索引是OLTP里消除Key Lookup最直接的手段但要注意它不是万能的INCLUDE列越多索引占用的空间越大写维护成本越高所以只把高频查询需要的列放进去。我用一个实际例子来说明。有一张订单表频繁执行这样一个查询按客户ID查最近订单的金额和状态。SELECT OrderId, OrderDate, TotalAmount, Status FROM dbo.Orders WHERE CustomerId 12345 ORDER BY OrderDate DESC;如果只有非聚簇索引(CustomerId)那么找到CustomerId12345的所有行后每次都要通过聚簇索引键回表取OrderDate、TotalAmount、Status然后排序。遇到一个客户有几万条订单回表次数就非常可观了。但如果我们把索引设计成CREATE INDEX IX_Orders_CustomerId_Covering ON dbo.Orders (CustomerId, OrderDate DESC) INCLUDE (TotalAmount, Status);那么OrderDate已经在索引里而且索引用DESC排序可以直接反向扫描TotalAmount和Status在INCLUDE里整个查询完全不用回表ORDER BY也省去了额外的Sort算子。4.4 过滤索引给小部分行一个专用索引过滤索引Filtered Index是SQL Server 2008之后引入的功能它允许你只给表中满足某一条件的行建立索引。比如订单表里只有不到1%的订单状态是待支付而系统高频查询就是扫这张待支付清单这时候完全可以建一个过滤索引CREATE INDEX IX_Orders_Pending_Customer ON dbo.Orders (CustomerId, CreatedDate) WHERE Status Pending;过滤索引的好处是体积小、维护成本低、查询命中精准。但要注意过滤条件必须写对使用它的查询也必须包含完全一致的条件否则优化器无法匹配索引就不会被用到。这算是过滤索引在索引选择上的一个特殊约束。4.5 列存储索引分析型查询的另一条路如果你的SQL是典型的大范围聚合查询比如对整月数据做SUM、COUNT、GROUP BY传统的行存储索引再优化也有上限这时候可以考虑列存储索引Columnstore Index。列存储索引把数据按列压缩存储配合批处理执行模式在扫描上亿行做聚合时的性能提升是数量级的。不过列存储索引不太适合频繁更新、单行定位的OLTP查询。现在SQL Server也提供了可更新的聚集列存储索引但它更适合批量写入、读多写少的混合场景。在设计列存储索引前你要先判断查询模式是大量行、少列的聚合还是少量行、多列的点查。下面用一个表格把这几种索引的选择要点放在一起方便平时翻看索引类型适用场景优势注意事项聚簇索引OLTP表主键访问、范围扫描数据即索引无需回表键值应唯一、递增、窄非聚簇索引各种查询条件的辅助索引灵活组合覆盖不同查询键列顺序影响Seek能力覆盖索引高频固定查询避免回表消除Key LookupINCLUDE列不宜过多过滤索引只查表中少数状态行的场景体积小、维护成本低查询条件必须与过滤条件匹配列存储索引大数据量聚合分析高压缩比扫描聚合性能强不适合单行点查和高频更新5. 索引选择翻车现场参数嗅探、隐式转换、函数包裹这一节全是实战中踩过的坑。前几年我排查过的慢查询里相当大一部分不是索引设计本身的问题而是SQL写法或执行计划缓存造成的索引选择异常。5.1 参数嗅探慢的问题被缓存了参数嗅探指的是SQL Server在第一次编译一个参数化SQL时用了当时传入的参数值去估算和选择执行计划然后把计划缓存起来。后续执行时即使参数值变了只要参数化文本一样就会复用同一个缓存计划。如果第一次传入的参数值是选择性很高的值比如查一个只返回几行的客户优化器选择了非聚簇索引Seek Key Lookup计划第二次换成一个返回几十万行的客户计划还是那个计划于是几十万次Key Lookup直接把库拖垮。这是索引选择最容易让人冤枉优化器的场景。排查方法用下面的语句找到缓存中该SQL的编译时间、执行次数和平均耗时再查看具体计划SELECT qs.plan_handle, qs.execution_count, qs.total_elapsed_time / qs.execution_count AS avg_elapsed_ms, SUBSTRING(st.text, (qs.statement_start_offset/2)1, ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text) ELSE qs.statement_end_offset END - qs.statement_start_offset)/2 1)) AS statement_text FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st ORDER BY qs.total_elapsed_time DESC;常见解决方案包括在不频繁执行且对计划准确性要求高的查询上加OPTION (RECOMPILE)让每次执行都重新编译、重新选择索引用OPTION (OPTIMIZE FOR UNKNOWN)让优化器使用平均选择性而不是某一个具体参数值把动态SQL改为存储过程并确保存储过程内没有会抑制参数化的写法如果问题出在统计信息过期先更新统计信息再清掉相关缓存计划。5.2 隐式转换让索引看得见摸不着隐式转换是让索引选择失效的一个高频原因。当SQL中比较的两边数据类型不一致时SQL Server会按优先级把一侧转为另一侧而这个转换如果发生在索引列上优化器就没法使用它对索引做Seek。最典型的例子表里的CustomerId是INT但应用传入的参数被实体框架或ORM映射成了VARCHARSELECT * FROM dbo.Orders WHERE CustomerId 12345;上面这个查询里CustomerId列会被隐式转换为VARCHAR再比较索引键上做了CONVERT于是Seek变成了Scan更准确说是Index Scan因为无法对转换后的表达式做精确匹配。排查方式是在执行计划里找CONVERT_IMPLICIT提示它通常出现在谓词附近。修复方式很简单把参数类型改成和列一致或者在SQL里显式转换参数一侧而不是列一侧。5.3 函数包裹列写了等值条件也走不了索引和隐式转换类似如果一个查询在索引列上套了函数比如SELECT * FROM dbo.Orders WHERE DATEPART(YEAR, OrderDate) 2025;或者SELECT * FROM dbo.Orders WHERE CONVERT(DATE, OrderDate) 2025-01-01;优化器无法对函数的输出建立B树索引定位所以即便OrderDate上有一个性能很好的索引也只能遍历所有索引行再做函数计算索引选择完全失灵。正确的做法是把函数从列上拆掉改成范围条件SELECT * FROM dbo.Orders WHERE OrderDate 2025-01-01 AND OrderDate 2026-01-01;这种改写在语义上和原来等价但能让优化器正确利用OrderDate上的索引对日期时间列尤其有效。5.4 统计信息过期与碎片沉默的计划毒药最后必须提一下统计信息过期。SQL Server在数据变化量达到一定阈值后会自动更新统计信息但阈值计算在数据量大的表上会变得保守有时候长期不触发自动更新也是正常的。判断统计信息是否过期可以用DBCC SHOW_STATISTICS来查看直方图重点看RANGE_HI_KEY、RANGE_ROWS和EQ_ROWS和实际数据的分布比对一下有没有明显偏差。如果统计信息确实过期更新统计信息通常能立刻改善优化器的索引选择UPDATE STATISTICS dbo.Orders WITH FULLSCAN;但要注意频繁全表扫描去更新统计信息在大表上本身代价很高所以在生产环境要利用维护窗口执行或者根据数据量选择WITH SAMPLE采样比例。而索引碎片的问题通常影响的是扫描效率和页密度不代表索引选错了但碎片严重时本来该选Seek的查询因为扫描路径太慢也可能在成本计算中改变优化器对某一方案的评估权重所以定期做索引重建或重组仍然是必要的。我把这几类翻车场景整理成一个速查表现场排查时可以对照看翻车类型典型特征排查手段推荐解决方式参数嗅探相同SQL不同参数时快时慢查看缓存计划与首次编译参数OPTION (RECOMPILE) / OPTIMIZE FOR UNKNOWN隐式转换计划中出现CONVERT_IMPLICIT核对参数与列的数据类型统一数据类型显式转换参数侧函数包裹等值条件走全表扫描检查谓词中列上是否有函数改写为范围条件统计信息过期估算行数严重偏离实际行数DBCC SHOW_STATISTICS对比直方图UPDATE STATISTICS 或调整自动更新阈值6. 一个生产环境索引选择优化的完整复盘前面把原理和坑讲完了最后用一个我实际经历过的生产案例把整个过程串起来。表结构和部分字段做了简化但思路是真实的。6.1 案例背景与慢查询症状这是一个B2C商城项目的订单查询接口。服务端有个接口支持按客户、状态、时间范围、金额范围组合查询订单列表表Orders数据量大约2800万行OrderItems约8000万行。问题是从某个大促之后接口经常超时数据库服务器的PAGEIOLATCH_SH等待持续走高监控平台显示部分查询单次耗时超过20秒。6.2 诊断过程从执行计划到索引使用统计我按前面说的诊断链路走了一遍。先抓了一条典型的慢SQL执行计划SELECT o.OrderId, o.OrderDate, o.TotalAmount, c.CustomerName FROM dbo.Orders o INNER JOIN dbo.Customers c ON o.CustomerId c.CustomerId WHERE o.CustomerId CustomerId AND o.OrderDate StartDate AND o.OrderDate EndDate ORDER BY o.OrderDate DESC;计划显示Orders表走了非聚簇索引IX_Orders_CustomerId上的Seek但每行都伴随一次Key Lookup回表OrderDate的过滤是在Index Scan层面完成的整个查询的逻辑读高达40万次。原因是约95%的订单状态是已完成而该索引上没有OrderDate作为键SQL Server从客户ID的索引入口拿到该客户大量订单后还要逐行回表取OrderDate和TotalAmount最终在内存里做过滤和排序。再看sys.dm_db_index_usage_stats表上总共有8个索引其中两个索引的user_seeks为0只有一个旧索引偶尔被用到属于典型的僵尸索引。而sys.dm_db_missing_index_details里明确列出了优化器一直想要的INCLUDE(orderdate, totalamount)列说明这个库已经很久没有人按照执行计划的建议去优化索引了。6.3 优化方案重新设计两个关键索引我没有直接按缺失索引建议一股脑全建而是把所有高频查询的参数组合整理了一遍最终只动了两处。第一处重建覆盖索引。把原来的单列索引改造成多列覆盖索引DROP INDEX IX_Orders_CustomerId ON dbo.Orders; CREATE INDEX IX_Orders_CustomerId_OrderDate ON dbo.Orders (CustomerId, OrderDate DESC) INCLUDE (TotalAmount, Status, PaymentMethod, ShippingAddressId);这个索引让按照客户时间范围查询的SQL可以在索引里完成全部定位、排序和字段读取完全避免了Key LookupORDER BY OrderDate DESC也能直接利用索引的有序性不再额外生成Sort算子。第二处针对组合筛选场景设计过滤索引。由于已完成订单占绝对大头而业务查询里频繁使用的是待支付和已发货这类状态通过过滤索引能显著缩小扫描范围CREATE INDEX IX_Orders_Status_PendingTime ON dbo.Orders (Status, CreatedDate DESC) INCLUDE (CustomerId, TotalAmount) WHERE Status IN (Pending, Shipped);被删除的是那两个长时间没有被访问的僵尸索引其中有一个是在低区分度列上建的复合索引删除后写操作的维护开销降了不少。6.4 优化结果从20秒到1.5秒在同样的数据量下重新收集统计信息后测试接口的主要SQL从20秒左右降到1.5秒左右逻辑读从40万次降到不到2000次。整个库的PAGEIOLATCH_SH等待占比在观察一周后明显下降CPU压力也随之降低。后续大促期间接口没有再出现过超时告警。这次复盘可以浓缩成几条教训第一索引选择不是优化器单方面的事它只会在你给它的索引集合里选所以要保证每个关键查询都有足够好的备选索引第二新增索引之前先用DMV盘点现有索引的使用情况删除无效索引、改造低效索引优于不断扩大索引数量第三任何索引优化做完之后都要更新统计信息并重新测试执行计划否则旧的计划缓存会让你误以为优化没有效果。7. 索引选择优化的个人经验清单最后分享几条我在实际维护SQL Server数据库时沉淀下来的经验。说经验而不是最佳实践是因为每条都曾经踩过坑属于拿时间换来的教训。第一不要在凌晨拍脑袋建索引。索引选择优化是一个死于增量的过程——每多一个索引写路径就多一分负担。没有慢查询证据支撑的索引本质上都是猜测。所以我的流程永远是先抓慢查询再分析执行计划然后设计或改造索引最后压测验证顺序不能反。第二参数化查询和存储过程的参数类型一定要和列类型对齐。很多看起来是索引选择失败的问题根因只是一个小小的隐式转换。在写ORM映射或定义存储过程参数时仔细核对表的字段类型能省掉后面大量排查时间。第三建立一个周期性的索引维护机制。统计信息更新和索引碎片整理不能全靠手动。对于OLTP库我一般建议把索引碎片超过30%的做重建、5%到30%的做重组碎片低于5%的可以不动统计信息则根据数据变化量设置合理的更新计划大表可以用足够的采样比例来均衡代价与准确性。第四学会正确使用查询提示但不要滥用。OPTION (RECOMPILE)、OPTION (OPTIMIZE FOR UNKNOWN)、WITH (FORCESEEK)这些提示在特定场景下很有效但它是人肉干预优化器的方式只适合用在确实需要固定计划的少数高频查询上。如果一个系统到处都挂着FORCESEEK说明你的索引设计大概率出了问题而不是优化器太蠢。有段时间我特别热衷于给每张表堆索引觉得索引越多越快。后来在一次大促前的容量评估中我发现多个冗余索引不仅没有帮到查询反而让每次INSERT和UPDATE的开销涨了不少磁盘空间也白白浪费。从那以后我给自己定了一条规矩任何新索引都要回答清楚它服务的具体查询是什么答不上来就不建。索引选择优化这件事方法再多最终还是要回到降低真实查询成本这个本质上来。