千万级大表如何优化，这是一个很有技术含量的问题，通常我们的直觉思维都会跳转到拆分或者数据分区，在此我想做一些补充和梳理，想和大家做一些这方面的经验总结，也欢迎大家提出建议。 从一开始脑海里火光四现，到不断的自我批评，后来也参考了一些团队的经验，我整理了下面的大纲内容。既然要吃透这个问题，我们势必要回到本源，我把这个问题分为三部分：“千万级”，“大表”，“优化”，也分别对应我们在图中标识的“数据量”，“对象”和“目标”。我来逐步展开说明一下，从而给出一系列的解决方案。数据量：千万级千万级其实只是一个感官的数字，就是我们印象中的数据量大。这里我们需要把这个概念细化，因为随着业务和时间的变化，数据量也会有变化，我们应该是带着一种动态思维来审视这个指标，从而对于不同的场景我们应该有不同的处理策略。①数据量为千万级，可能达到亿级或者更高通常是一些数据流水，日志记录的业务，里面的数据随着时间的增长会逐步增多，超过千万门槛是很容易的一件事情。②数据量为千万级，是一个相对稳定的数据量如果数据量相对稳定，通常是在一些偏向于状态的数据，比如有 1000 万用户，那么这些用户的信息在表中都有相应的一行数据记录，随着业务的增长，这个量级相对是比较稳定的。③数据量为千万级，不应该有这么多的数据这种情况是我们被动发现的居多，通常发现的时候已经晚了，比如你看到一个配置表，数据量上千万；或者说一些表里的数据已经存储了很久，99% 的数据都属于过期数据或者垃圾数据。数据量是一个整体的认识，我们需要对数据做更近一层的理解，这就可以引出第二个部分的内容。对象：数据表数据操作的过程就好比数据库中存在着多条管道，这些管道中都流淌着要处理的数据，这些数据的用处和归属是不一样的。一般根据业务类型把数据分为三种：①流水型数据流水型数据是无状态的，多笔业务之间没有关联，每次业务过来的时候都会产生新的单据。比如交易流水、支付流水，只要能插入新单据就能完成业务，特点是后面的数据不依赖前面的数据，所有的数据按时间流水进入数据库。②状态型数据状态型数据是有状态的，多笔业务之间依赖于有状态的数据，而且要保证该数据的准确性，比如充值时必须要拿到原来的余额，才能支付成功。③配置型数据此类型数据数据量较小，而且结构简单，一般为静态数据，变化频率很低。至此，我们可以对整体的背景有一个认识了，如果要做优化，其实要面对的是这样的 33 的矩阵，如果要考虑表的读写比例（读多写少，读少写多...），那么就会是 33*4=24 种，显然做穷举是不显示的，而且也完全没有必要，可以针对不同的数据存储特性和业务特点来指定不同的业务策略。对此我们采取抓住重点的方式，把常见的一些优化思路梳理出来，尤其是里面的核心思想，也是我们整个优化设计的一把尺子，而难度决定了我们做这件事情的动力和风险。而对于优化方案，我想采用面向业务的维度来进行阐述。目标：优化在这个阶段，我们要说优化的方案了，总结的有点多，相对来说是比较全了。整体分为五个部分：其实我们通常所说的分库分表等方案只是其中的一小部分，如果展开之后就比较丰富了。不难理解，我们要支撑的表数据量是千万级别，相对来说是比较大了，DBA 要维护的表肯定不止一张，如何能够更好的管理，同时在业务发展中能够支撑扩展，同时保证性能，这是摆在我们面前的几座大山。我们分别来说一下这五类改进方案：规范设计业务层优化架构层优化数据库优化管理优化规范设计在此我们先提到的是规范设计，而不是其他高大上的设计方案。黑格尔说：秩序是自由的第一条件。在分工协作的工作场景中尤其重要，否则团队之间互相牵制太多，问题多多。我想提到如下的几个规范，其实只是属于开发规范的一部分内容，可以作为参考。规范的本质不是解决问题，而是有效杜绝一些潜在问题，对于千万级大表要遵守的规范，我梳理了如下的一些细则，基本可以涵盖我们常见的一些设计和使用问题。比如表的字段设计不管三七二十一，都是 varchar（500），其实是很不规范的一种实现方式，我们来展开说一下这几个规范。配置规范：MySQL 数据库默认使用 InnoDB 存储引擎。保证字符集设置统一，MySQL 数据库相关系统、数据库、表的字符集都使用 UTF8，应用程序连接、展示等可以设置字符集的地方也都统一设置为 UTF8 字符集。注：UTF8 格式是存储不了表情类数据，需要使用 UTF8MB4，可在 MySQL 字符集里面设置。在 8.0 中已经默认为 UTF8MB4，可以根据公司的业务情况进行统一或者定制化设置。MySQL 数据库的事务隔离级别默认为 RR（Repeatable-Read），建议初始化时统一设置为 RC（Read-Committed），对于 OLTP 业务更适合。数据库中的表要合理规划，控制单表数据量，对于 MySQL 数据库来说，建议单表记录数控制在 2000W 以内。MySQL 实例下，数据库、表数量尽可能少；数据库一般不超过 50 个，每个数据库下，数据表数量一般不超过 500 个（包括分区表）。建表规范：InnoDB 禁止使用外键约束，可以通过程序层面保证。存储精确浮点数必须使用 DECIMAL 替代 FLOAT 和 DOUBLE。整型定义中无需定义显示宽度，比如：使用 INT，而不是 INT(4)。不建议使用 ENUM 类型，可使用 TINYINT 来代替。尽可能不使用 TEXT、BLOB 类型，如果必须使用，建议将过大字段或是不常用的描述型较大字段拆分到其他表中；另外，禁止用数据库存储图片或文件。存储年时使用 YEAR(4)，不使用 YEAR(2)。建议字段定义为 NOT NULL。建议 DBA 提供 SQL 审核工具，建表规范性需要通过审核工具审核后。命名规范：库、表、字段全部采用小写。库名、表名、字段名、索引名称均使用小写字母，并以“_”分割。库名、表名、字段名建议不超过 12 个字符。（库名、表名、字段名支持最多 64 个字符，但为了统一规范、易于辨识以及减少传输量，统一不超过 12 字符）库名、表名、字段名见名知意，不需要添加注释。对于对象命名规范的一个简要总结如下表所示，供参考：命名列表索引规范：索引建议命名规则：idx_col1_col2[_colN]、uniq_col1_col2[_colN]（如果字段过长建议采用缩写）。索引中的字段数建议不超过 5 个。单张表的索引个数控制在 5 个以内。InnoDB 表一般都建议有主键列，尤其在高可用集群方案中是作为必须项的。建立复合索引时，优先将选择性高的字段放在前面。UPDATE、DELETE 语句需要根据 WHERE 条件添加索引。不建议使用 % 前缀模糊查询，例如 LIKE “%weibo”，无法用到索引，会导致全表扫描。合理利用覆盖索引，例如：SELECT email，uid FROM user_email WHERE uid=xx，如果 uid 不是主键，可以创建覆盖索引 idx_uid_email（uid，email）来提高查询效率。避免在索引字段上使用函数，否则会导致查询时索引失效。确认索引是否需要变更时要联系 DBA。应用规范：避免使用存储过程、触发器、自定义函数等，容易将业务逻辑和DB耦合在一起，后期做分布式方案时会成为瓶颈。考虑使用 UNION ALL，减少使用 UNION，因为 UNION ALL 不去重，而少了排序操作，速度相对比 UNION 要快，如果没有去重的需求，优先使用 UNION ALL。考虑使用 limit N，少用 limit M，N，特别是大表或 M 比较大的时候。减少或避免排序，如：group by 语句中如果不需要排序，可以增加 order by null。统计表中记录数时使用 COUNT(*)，而不是 COUNT(primary_key) 和 COUNT(1)。InnoDB 表避免使用 COUNT(*) 操作，计数统计实时要求较强可以使用 Memcache 或者 Redis，非实时统计可以使用单独统计表，定时更新。做字段变更操作（modify column/change column）的时候必须加上原有的注释属性，否则修改后，注释会丢失。使用 prepared statement 可以提高性能并且避免 SQL 注入。SQL 语句中 IN 包含的值不应过多。UPDATE、DELETE 语句一定要有明确的 WHERE 条件。WHERE 条件中的字段值需要符合该字段的数据类型，避免 MySQL 进行隐式类型转化。SELECT、INSERT 语句必须显式的指明字段名称，禁止使用 SELECT * 或是 INSERT INTO table_name values()。INSERT 语句使用 batch 提交（INSERT INTO table_name VALUES(),(),()……），values 的个数不应过多。业务层优化业务层优化应该是收益最高的优化方式了，而且对于业务层完全可见，主要有业务拆分，数据拆分和两类常见的优化场景（读多写少，读少写多）！①业务拆分业务拆分分为如下两个方面：将混合业务拆分为独立业务将状态和历史数据分离业务拆分其实是把一个混合的业务剥离成为更加清晰的独立业务，这样业务 1，业务 2......独立的业务使得业务总量依旧很大，但是每个部分都是相对独立的，可靠性依然有保证。对于状态和历史数据分离，我可以举一个例子来说明。例如：我们有一张表 Account，假设用户余额为 100。我们需要在发生数据变更后，能够追溯数据变更的历史信息，如果对账户更新状态数据，增加 100 的余额，这样余额为 200。这个过程可能对应一条 update 语句，一条 insert 语句。对此我们可以改造为两个不同的数据源，account 和 account_hist。在 account_hist 中就会是两条 insert 记录，如下：而在 account 中则是一条 update 语句，如下：这也是一种很基础的冷热分离，可以大大减少维护的复杂度，提高业务响应效率。②数据拆分按照日期拆分：这种使用方式比较普遍，尤其是按照日期维度的拆分，其实在程序层面的改动很小，但是扩展性方面的收益很大。数据按照日期维度拆分，如 test_20191021。数据按照周月为维度拆分，如 test_201910。数据按照季度，年维度拆分，如 test_2019。采用分区模式：分区模式也是常见的使用方式，采用 hash，range 等方式会多一些。在 MySQL 中我是不大建议使用分区表的使用方式，因为随着存储容量的增长，数据虽然做了垂直拆分，但是归根结底，数据其实难以实现水平扩展，在 MySQL 中是有更好的扩展方式。③读多写少优化场景采用缓存，采用 Redis 技术，将读请求打在缓存层面，这样可以大大降低 MySQL 层面的热点数据查询压力。④读少写多优化场景读少写多优化场景，可以采用三步走：采用异步提交模式，异步对于应用层来说最直观的就是性能的提升，产生最少的同步等待。使用队列技术，大量的写请求可以通过队列的方式来进行扩展，实现批量的数据写入。降低写入频率，这个比较难理解，我举个例子：对于业务数据，比如积分类，相比于金额来说业务优先级略低的场景，如果数据的更新过于频繁，可以适度调整数据更新的范围（比如从原来的每分钟调整为 10 分钟）来减少更新的频率。例如：更新状态数据，积分为 200，如下图所示：可以改造为，如下图所示：如果业务数据在短时间内更新过于频繁，比如 1 分钟更新 100 次，积分从 100 到 10000，则可以根据时间频率批量提交。例如：更新状态数据，积分为 100，如下图所示：无需生成 100 个事务（200 条 SQL 语句）可以改造为 2 条 SQL 语句，如下图所示：对于业务指标，比如更新频率细节信息，可以根据具体业务场景来讨论决定。架构层优化架构层优化其实就是我们认为的那种技术含量很高的工作，我们需要根据业务场景在架构层面引入一些新的花样来。①系统水平扩展场景采用中间件技术：可以实现数据路由，水平扩展，常见的中间件有 MyCAT，ShardingSphere，ProxySQL 等。采用读写分离技术：这是针对读需求的扩展，更侧重于状态表，在允许一定延迟的情况下，可以采用多副本的模式实现读需求的水平扩展，也可以采用中间件来实现，如 MyCAT，ProxySQL，MaxScale，MySQL Router 等。采用负载均衡技术：常见的有 LVS 技术或者基于域名服务的 Consul 技术等。②兼顾 OLTP+OLAP 的业务场景可以采用 NewSQL，优先兼容 MySQL 协议的 HTAP 技术栈，如 TiDB。③离线统计的业务场景有几类方案可供选择：采用 NoSQL 体系，主要有两类，一类是适合兼容 MySQL 协议的数据仓库体系，常见的有 Infobright 或者 ColumnStore，另外一类是基于列式存储，属于异构方向，如 HBase 技术。采用数仓体系，基于 MPP 架构，如使用 Greenplum 统计，如 T+1 统计。数据库优化数据库优化，其实可打的牌也不少，但是相对来说空间没有那么大了，我们来逐个说一下。①事务优化根据业务场景选择事务模型，是否是强事务依赖。对于事务降维策略，我们来举出几个小例子来。降维策略 1：存储过程调用转换为透明的 SQL 调用对于新业务而言，使用存储过程显然不是一个好主意，MySQL 的存储过程和其他商业数据库相比，功能和性能都有待验证，而且在目前轻量化的业务处理中，存储过程的处理方式太“重”了。有些应用架构看起来是按照分布式部署的，但在数据库层的调用方式是基于存储过程，因为存储过程封装了大量的逻辑，难以调试，而且移植性不高。这样业务逻辑和性能压力都在数据库层面了，使得数据库层很容易成为瓶颈，而且难以实现真正的分布式。所以有一个明确的改进方向就是对于存储过程的改造，把它改造为 SQL 调用的方式，可以极大地提高业务的处理效率，在数据库的接口调用上足够简单而且清晰可控。降维策略 2：DDL 操作转换为 DML 操作有些业务经常会有一种紧急需求，总是需要给一个表添加字段，搞得 DBA 和业务同学都挺累，可以想象一个表有上百个字段，而且基本都是 name1，name2……name100，这种设计本身就是有问题的，更不用考虑性能了。究其原因，是因为业务的需求动态变化，比如一个游戏装备有 20 个属性，可能过了一个月之后就增加到了 40 个属性，这样一来，所有的装备都有 40 个属性，不管用没用到，而且这种方式也存在诸多的冗余。我们在设计规范里面也提到了一些设计的基本要素，在这些基础上需要补充的是，保持有限的字段，如果要实现这些功能的扩展，其实完全可以通过配置化的方式来实现，比如把一些动态添加的字段转换为一些配置信息。配置信息可以通过 DML 的方式进行修改和补充，对于数据入口也可以更加动态、易扩展。降维策略 3：Delete 操作转换为高效操作有些业务需要定期来清理一些周期性数据，比如表里的数据只保留一个月，那么超出时间范围的数据就要清理掉了。而如果表的量级比较大的情况下，这种 Delete 操作的代价实在太高，我们可以有两类解决方案来把 Delete 操作转换为更为高效的方式。 第一种是根据业务建立周期表，比如按照月表、周表、日表等维度来设计，这样数据的清理就是一个相对可控而且高效的方式了。 第二种方案是使用 MySQL rename 的操作方式，比如一张 2 千万的大表要清理 99% 的数据，那么需要保留的 1% 的数据我们可以很快根据条件过滤补录，实现“移形换位”。②SQL 优化其实相对来说需要的极简的设计，很多点都在规范设计里面了，如果遵守规范，八九不离十的问题都会杜绝掉。在此补充几点：SQL 语句简化，简化是 SQL 优化的一大利器，因为简单，所以优越。尽可能避免或者杜绝多表复杂关联，大表关联是大表处理的噩梦，一旦打开了这个口子，越来越多的需求需要关联，性能优化就没有回头路了，更何况大表关联是 MySQL 的弱项，尽管 Hash Join 才推出，不要像掌握了绝对大杀器一样，在商业数据库中早就存在，问题照样层出不穷。SQL 中尽可能避免反连接，避免半连接，这是优化器做得薄弱的一方面，什么是反连接，半连接？其实比较好理解，举个例子：not in，not exists 就是反连接，in，exists 就是半连接，在千万级大表中出现这种问题，性能是几个数量级的差异。③索引优化应该是大表优化中需要把握的一个度：首先必须有主键，规范设计中第一条就是，此处不接收反驳。其次，SQL 查询基于索引或者唯一性索引，使得查询模型尽可能简单。最后，尽可能杜绝范围数据的查询，范围扫描在千万级大表情况下还是尽可能减少。管理优化这部分应该是在所有的解决方案中最容易被忽视的部分了，我放在最后，在此也向运维同事致敬，总是为很多认为本应该正常的问题尽职尽责（背锅）。千万级大表的数据清理一般来说是比较耗时的，在此建议在设计中需要完善冷热数据分离的策略，可能听起来比较拗口，我来举一个例子，把大表的 Drop 操作转换为可逆的 DDL 操作。Drop 操作是默认提交的，而且是不可逆的，在数据库操作中都是跑路的代名词，MySQL 层面目前没有相应的 Drop 操作恢复功能，除非通过备份来恢复，但是我们可以考虑将 Drop 操作转换为一种可逆的 DDL 操作。MySQL 中默认每个表有一个对应的 ibd 文件，其实可以把 Drop 操作转换为一个 rename 操作，即把文件从 testdb 迁移到 testdb_arch 下面。从权限上来说，testdb_arch 是业务不可见的，rename 操作可以平滑的实现这个删除功能，如果在一定时间后确认可以清理，则数据清理对于已有的业务流程是不可见的，如下图所示：此外，还有两个额外建议，一个是对于大表变更，尽可能考虑低峰时段的在线变更，比如使用 pt-osc 工具或者是维护时段的变更，就不再赘述了。最后总结一下，其实就是一句话：千万级大表的优化是根据业务场景，以成本为代价进行优化的，绝对不是孤立的一个层面的优化。
前言我们在面试时，经常会被面试官问到：线上服务频繁Full GC该如何优化？今天这篇文章跟大家一起聊聊这个话题，希望对你会有所帮助。1. 什么是Full GC？当老年代空间不足时，JVM会触发Stop-The-World的全局回收（Full GC），暂停所有应用线程。致命危害（生产环境实测）：对象的晋升之路流程图：关键代码：年龄计数器// HotSpot虚拟机源码片段（objectMonitor.cpp） void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock) { if (obj->age() >= MaxTenuringThreshold) { // 年龄阈值检查 promote_to_old_gen(obj); // 晋升老年代 } }2.如何排查定位问题？2.1 实时监控：GC健康度速诊jstat -gcutil <pid> 1000 # 每秒输出GC数据关键指标解读：OU：老年代使用率 > 90% = 危险区FGCT：Full GC总耗时 > 应用运行时间10% = 严重问题2.2. 堆内存转储：揪出内存黑洞jmap -dump:live,format=b,file=heap.bin <pid> # 生产环境慎用live2.3 MAT深度分析：解剖内存泄漏3.优化方案方案1：对象池化——大对象的救赎场景：高频创建10MB的文件缓存// 反例：每次请求创建新对象 public void processRequest(Request req) { byte[] buffer = newbyte[10 * 1024 * 1024]; // 10MB // ...处理逻辑 } // 优化：对象池复用 privatestaticfinal ObjectPool<byte[]> pool = new GenericObjectPool<>( new BasePooledObjectFactory<byte[]>() { @Override publicbyte[] create() { returnnewbyte[10 * 1024 * 1024]; } } ); public void processRequest(Request req) throws Exception { byte[] buffer = pool.borrowObject(); try { // ...处理逻辑 } finally { pool.returnObject(buffer); } }效果：老年代分配速率下降85%方案2：手动控制晋升问题：Survivor区过小导致对象提前晋升优化参数：-XX:TargetSurvivorRatio=60 # Survivor区使用阈值 -XX:MaxTenuringThreshold=15 # 最大晋升年龄 -XX:+NeverTenure # 若Survivor足够，永不晋升（慎用！）晋升原理：方案3：合理分配堆空间经典误区：-Xmx4g -Xms4g # 错误！未配置新生代优化公式：新生代大小 = 总堆 * 3/8 Eden:Survivor = 8:1:1 正确配置：-Xmx8g -Xms8g -Xmn3g # 新生代3G (8*3/8≈3) -XX:SurvivorRatio=8 # Eden:Survivor=8:1:1方案4：卸载无用类场景：热部署频繁的应用（如JRebel）诊断命令：jcmd <pid> VM.class_stats # JDK8+ jcmd <pid> GC.class_stats # JDK11+根治代码：// 自定义类加载器必须实现close() public class HotSwapClassLoader extends URLClassLoader { @Override public void close() throws IOException { // 1. 停止新请求 // 2. 卸载所有类 // 3. 关闭资源 } }方案5：颠覆传统的ZGC传统GC痛点：CMS：内存碎片问题G1：Mixed GC不可控ZGC迁移步骤：升级JDK至17+添加参数：-XX:+UseZGC -XX:ZAllocationSpikeTolerance=5.0 # 容忍内存分配速率波动 -Xmx16g -Xlog:gc*:file=gc.log效果对比：方案6：堆外内存治理现象：堆内存正常，但Full GC频繁根源：DirectByteBuffer的清理依赖Full GC防御方案：// 方案1：限制堆外内存 -XX:MaxDirectMemorySize=512m // 方案2：主动调用Cleaner ByteBuffer buffer = ByteBuffer.allocateDirect(1024); Cleaner cleaner = ((DirectBuffer) buffer).cleaner(); if (cleaner != null) cleaner.clean(); // 方案3：Netty的内存管理 PooledByteBufAllocator allocator = new PooledByteBufAllocator(true); ByteBuf buffer = allocator.directBuffer(1024); // ...使用后必须release! buffer.release(); 4.实战案例背景：某支付系统日均交易10亿症状：每分钟5次Full GC，暂停4.2秒99线响应时间从50ms飙升至3秒排查过程：jstat显示老年代10秒内从60%→99%MAT分析发现ConcurrentHashMap$Node[]占78%内存溯源代码找到缓存黑洞：// 问题代码：永不失效的缓存 Map<String, Transaction> cache = new ConcurrentHashMap<>(); public void cacheTransaction(Transaction tx) { cache.put(tx.getId(), tx); // Key冲突时旧对象未移除！ }解决方案：改用Caffeine缓存：Cache<String, Transaction> cache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(5, TimeUnit.MINUTES) .build();添加ZGC参数重写线程池任务队列：// 用有界队列替代LinkedBlockingQueue new ThreadPoolExecutor(..., new ArrayBlockingQueue<>(1000));效果：Full GC降为099线回落至68ms总结监控三件套：jstat -gcutil <pid> 1000 # 实时监控 -Xlog:gc*:file=gc.log # GC日志 Prometheus + Grafana # 可视化大盘参数黄金法则：代码军规：大对象必须池化缓存必须设置上限线程池必须用有界队列GC算法选择：Full GC不是优化出来的，是设计出来的！永远在架构设计阶段预留30%内存缓冲空间，比任何调参技巧都重要。附录：急救工具箱
前言不管你是面试还是日常调优，只要聊到数据库性能，“索引”几乎一定是绕不开的话题。很多小伙伴知道索引能加速查询，但问到“怎么建索引才能让查询最快”时，往往只能答出“给 where 后面的列加索引”。其实，索引优化远没有这么简单。今天这篇文章我就把工作中最常用的10个索引优化技巧总结出来，希望对你会有所帮助。技巧一：选择高选择性的列作为索引1.1 问题现象有些小伙伴习惯性地给所有出现在 WHERE 子句中的列都加上索引，结果发现加了索引反而更慢。比如给“性别”列加索引，查询男性用户时，依然扫描了大量数据。1.2 什么是选择性？选择性 = COUNT(DISTINCT col) / COUNT(*)，比值越接近 1，选择性越高。选择性高的列能快速过滤掉大量数据。-- 查看选择性 SELECT COUNT(DISTINCT gender) / COUNT(*) AS gender_selectivity, COUNT(DISTINCT user_id) / COUNT(*) AS user_selectivity FROM users;低选择性：性别（0.5 左右），索引几乎没用。高选择性：用户 ID、订单号（接近 1），索引价值巨大。1.3 底层原理InnoDB 的 B+Tree 索引结构，每个叶子节点存储一个键值 + 行指针。当选择性很低时（比如性别只有男/女），索引树高度虽然低，但每个键值对应海量行，依然需要回表读取大量数据，代价甚至超过全表扫描。优化器会选择全表扫描。1.4 正确做法只在高选择性列上建索引。若业务必须过滤低选择性字段（如 status），可将其放在联合索引的末尾，作为二次筛选项。-- 不推荐 ALTER TABLE orders ADD INDEX idx_status (status); -- 推荐：status 作为联合索引的后续列 ALTER TABLE orders ADD INDEX idx_user_status (user_id, status);1.5 适用场景与优缺点优点：高选择性索引能迅速定位少量数据，极大减少 IO。缺点：低选择性索引浪费空间，且写入时需要维护。适用场景：用户 ID、订单号、手机号等唯一或接近唯一的列。技巧二：联合索引遵循最左前缀法则2.1 问题现象创建了 INDEX(a, b, c)，却写出 WHERE b = 1 AND c = 2，结果不走索引，查询极慢。2.2 最左前缀原则联合索引 (a, b, c) 本质上是一颗按 a 排序，a 相同再按 b 排序，b 相同再按 c 排序的 B+Tree。查询条件必须包含最左边的列，才能用到索引。2.3 正确与错误示例-- 正确：用到索引所有列 SELECT * FROM t WHERE a = 1 AND b = 2 AND c = 3; -- 正确：用到 a 和 b，c 无法完全匹配，但已过滤大量数据 SELECT * FROM t WHERE a = 1 AND b = 2; -- 正确：只用 a，依然能用索引 SELECT * FROM t WHERE a = 1; -- 错误：跳过 a，完全无法使用索引 SELECT * FROM t WHERE b = 2 AND c = 3; -- 错误：范围查询后的列无法使用索引（后面会讲） SELECT * FROM t WHERE a = 1 AND b > 2 AND c = 3; -- c 不会走索引，因为 b 是范围2.4 优化建议把等值查询的列放在最左边，范围查询（>、<、between）的列放在右边。尽量保证查询条件覆盖最左前缀，避免跳过前列。2.5 适用场景多条件组合查询频繁的表。需要排序、分组、过滤的综合业务。技巧三：尽量使用覆盖索引，避免回表3.1 什么是回表？InnoDB 的二级索引叶子节点存储的是主键值。当你通过二级索引找到主键后，还需要回聚簇索引查找完整行，这个过程叫“回表”。回表是随机 IO，极其昂贵。3.2 覆盖索引如果索引中已经包含了查询需要的所有列，MySQL 就不需要回表，直接从索引中获取数据，称为“覆盖索引”。-- 低效：需要回表读取 name 和 age SELECT name, age FROM user WHERE id_card = 'xxx'; -- 仅有 idx_id_card，需回表 -- 高效：覆盖索引 ALTER TABLE user ADD INDEX idx_id_card_name_age (id_card, name, age); SELECT name, age FROM user WHERE id_card = 'xxx'; -- 索引已包含所需列，直接返回，不回表3.3 优缺点优点：大幅减少随机 IO，查询速度可提升数倍甚至数十倍。缺点：索引占用空间增大，写入维护成本增加。适用场景：高频查询的固定字段组合（如用户个人信息、订单简要列表）。技巧四：避免在索引列上使用函数或表达式4.1 问题现象-- 不走索引 SELECT * FROM orders WHERE DATE(create_time) = '2026-01-01';原因：对索引列 create_time 使用了 DATE() 函数，MySQL 无法直接使用索引。4.2 正确写法-- 正确：使用范围查询 SELECT * FROM orders WHERE create_time >= '2026-01-01 00:00:00' AND create_time < '2026-01-02 00:00:00';4.3 其他常见错误-- 错误： 隐式类型转换（phone 是 VARCHAR） SELECT * FROM user WHERE phone = 13800000000; -- ✅ 正确：加引号 SELECT * FROM user WHERE phone = '13800000000'; -- 错误： 使用运算 SELECT * FROM product WHERE price * 0.8 > 100; -- ✅ 正确：移运算到右侧 SELECT * FROM product WHERE price > 100 / 0.8;4.4 底层原理B+Tree 索引按照列原始值排序存储，对列做任何函数运算都会破坏排序规则，使索引无法按范围快速定位。4.5 适用场景任何需要过滤、排序的查询，都应保证索引列本身不被函数包裹。技巧五：利用索引下推减少回表5.1 什么是索引下推（ICP）？MySQL 5.6 引入索引条件下推（Index Condition Pushdown），允许在索引遍历过程中，先对索引中包含的列进行条件过滤，减少不必要的回表。5.2 举例说明-- 联合索引 (name, age) SELECT * FROM user WHERE name LIKE '张%' AND age = 25;无 ICP：先通过 name LIKE '张%' 找到所有主键，再回表过滤 age = 25。有 ICP：在索引层同时判断 age = 25，只对符合两个条件的记录回表。5.3 开启方式ICP 默认开启，可通过 SHOW VARIABLES LIKE 'optimizer_switch' 查看。SET optimizer_switch='index_condition_pushdown=on';5.4 适用场景联合索引，且 where 条件中有索引列的非前缀匹配（如 LIKE '张%'）。索引列有范围查询，且后面还有等值条件。技巧六：避免使用 OR，改用 UNION 或 IN6.1 问题现象SELECT * FROM user WHERE status = 1 OR status = 2;如果 status 选择性很低，OR 可能导致索引失效，优化器选择全表扫描。6.2 优化方案使用 IN：WHERE status IN (1,2)，通常能走索引。使用 UNION：强制走索引再合并。SELECT * FROM user WHERE status = 1 UNION SELECT * FROM user WHERE status = 2;注意：UNION 会去重，若确定无重复可用 UNION ALL，性能更好。6.3 原理OR 会将条件拆分成多个独立查询再合并，但优化器可能低估成本。IN 是被优化器特别处理的等值集合，通常能利用索引。技巧七：使用前缀索引节省空间7.1 问题背景对于长字符串列（如 VARCHAR(255)），整个列建索引会占用大量空间，且影响写入性能。7.2 前缀索引只索引列的前 N 个字符。例如邮箱前 10 个字符几乎唯一。ALTER TABLE user ADD INDEX idx_email_prefix (email(10));7.3 选择合适长度-- 计算不同前缀长度的选择性 SELECT COUNT(DISTINCT LEFT(email, 5))/COUNT(*) AS sel5, COUNT(DISTINCT LEFT(email, 10))/COUNT(*) AS sel10, COUNT(DISTINCT email)/COUNT(*) AS total FROM user;选择选择性接近 1 的最小长度。7.4 注意前缀索引不能用于 ORDER BY 和 GROUP BY，也无法成为覆盖索引。技巧八：定期监控并删除未使用的索引8.1 冗余索引举例INDEX(a) 和 INDEX(a,b)，前者完全多余。INDEX(a) 和 INDEX(b,a)，对于查询 WHERE a = 1 两者都可用，但可保留其一。8.2 查找未使用的索引（需开启 performance_schema）SELECT * FROM performance_schema.table_io_waits_summary_by_index_usage WHERE object_schema = 'your_db' AND index_name IS NOT NULL ORDER BY count_read, count_write;COUNT_READ 长期为 0 的索引可以考虑删除。8.3 查找冗余索引SELECT * FROM sys.schema_redundant_indexes WHERE table_schema = 'your_db';技巧九：深分页查询优化（延迟关联）9.1 问题LIMIT 100000, 10 需要扫描 100010 行，然后丢弃前 100000 行，效率极低。9.2 优化方案延迟关联：先利用覆盖索引查出主键，再回表取完整行。-- 慢 SELECT * FROM orders ORDER BY id LIMIT 100000, 10; -- 快 SELECT * FROM orders t1 JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000, 10) t2 ON t1.id = t2.id;游标分页：适合数据只增不减的场景，记住上一页最后一条 ID。SELECT * FROM orders WHERE id > last_id ORDER BY id LIMIT 10;技巧十：定期分析表，更新统计信息10.1 为什么需要？优化器根据统计信息选择索引。若统计信息陈旧，可能选错索引。10.2 手动更新ANALYZE TABLE orders;OPTIMIZE TABLE 可重建表、整理碎片，适合大批量删除后执行。OPTIMIZE TABLE orders;10.3 自动化MySQL 8.0 默认开启持久化统计信息，且会在表数据变化超过 10% 时自动更新。但仍建议在批量导入/删除后手动执行。总结最后送大家一句话：索引是把双刃剑，用得好是屠龙刀，用不好就是自残剑。建索引之前，先用 EXPLAIN 看执行计划；建完之后，定期通过监控系统清理无效索引。你的数据库会感谢你的。你在实际工作中还遇到过哪些索引相关的“坑”？欢迎评论区分享你的经历。
近年来公司业务迅猛发展，数据量爆炸式增长，随之而来的的是海量数据查询等带来的挑战，我们需要数据量在十亿，甚至百亿级别的规模时依然能以秒级甚至毫秒级的速度返回，这样的话显然离不开搜索引擎的帮助，在搜索引擎中，ES（ElasticSearch）毫无疑问是其中的佼佼者，连续多年在 DBRanking 的搜索引擎中评测中排名第一，也是绝大多数大公司的首选，那么它与传统的 DB 如 MySQL 相比有啥优势呢，ES 的数据又是如何生成的，数据达到 PB 时又是如何保证 ES 索引数据的实时性以更好地满足业务的需求的呢。本文会结合我司在 ES 上的实践经验与大家谈谈如何构建准实时索引的一些思路，希望对大家有所启发。本文目录如下为什么要用搜索引擎，传统 DB 如 MySQL 不香吗MySQL 的不足ES 简介ES 索引数据构建PB 级的 ES 准实时索引数据构建之道为什么要用搜索引擎，传统 DB 如 MySQL 不香吗MySQL 的不足MySQL 架构天生不适合海量数据查询，它只适合海量数据存储，但无法应对海量数据下各种复杂条件的查询，有人说加索引不是可以避免全表扫描，提升查询速度吗，为啥说它不适合海量数据查询呢，有两个原因：_1、_加索引确实可以提升查询速度，但在 MySQL 中加多个索引最终在执行 SQL 的时候它只会选择成本最低的那个索引，如果没有索引满足搜索条件，就会触发全表扫描，而且即便你使用了组合索引，也要符合最左前缀原则才能命中索引，但在海量数据多种查询条件下很有可能不符合最左前缀原则而导致索引失效，而且我们知道存储都是需要成本的，如果你针对每一种情况都加索引，以 innoDB 为例，每加一个索引，就会创建一颗 B+ 树，如果是海量数据，将会增加很大的存储成本，之前就有人反馈说他们公司的某个表实际内容的大小才 10G, 而索引大小却有 30G！这是多么巨大的成本！所以千万不要觉得索引建得越多越好。_2、_有些查询条件是 MySQL 加索引都解决不了的，比如我要查询商品中所有 title 带有「格力空调」的关键词，如果你用 MySQL 写，会写出如下代码SELECT*FROMproductWHEREtitlelike'%格力空调%'这样的话无法命中任何索引，会触发全表扫描，而且你不能指望所有人都能输对他想要的商品，是人就会犯错误，我们经常会犯类似把「格力空调」记成「格空间」的错误，那么 SQL 语句就会变成：SELECT*FROMproductWHEREtitlelike'%格空调%'这种情况下就算你触发了全表扫描也无法查询到任何商品，综上所述，MySQL 的查询确实能力有限。ES 简介与其说上面列的这些点是 MySQL 的不足，倒不如说 MySQL 本身就不是为海量数据查询而设计的，术业有专攻，海量数据查询还得用专门的搜索引擎，这其中 ES 是其中当之无愧的王者，它是基于 Lucene 引擎构建的开源分布式搜索分析引擎，可以提供针对 PB 数据的近实时查询，广泛用在全文检索、日志分析、监控分析等场景。它主要有以下三个特点:轻松支持各种复杂的查询条件: 它是分布式实时文件存储，会把每一个字段都编入索引（倒排索引），利用高效的倒排索引，以及自定义打分、排序能力与丰富的分词插件等，能实现任意复杂查询条件下的全文检索需求可扩展性强：天然支持分布式存储，通过极其简单的配置实现几百上千台服务器的分布式横向扩容，轻松处理 PB 级别的结构化或非结构化数据。高可用，容灾性能好：通过使用主备节点，以及故障的自动探测与恢复，有力地保障了高可用我们先用与 MySQL 类比的形式来理解 ES 的一些重要概念通过类比的形式不难看出 ES 的以下几个概念_1、_MySQL 的数据库（DataBase）相当于 Index（索引），数据的逻辑集合，ES 的工作主要也是创建索引，查询索引。_2、_一个数据库里会有多个表，同样的一个 Index 也会有多个 type_3、_一个表会有多行（Row），同样的一个 Type 也会有多个 Document。_4、_Schema 指定表名，表字段，是否建立索引等，同样的 Mapping 也指定了 Type 字段的处理规则，即索引如何建立，是否分词，分词规则等_5、_在 MySQL 中索引是需要手动创建的，而在 ES 一切字段皆可被索引，只要在 Mapping 在指定即可那么 ES 中的索引为何如此高效，能在海量数据下达到秒级的效果呢？它采用了多种优化手段，最主要的原因是它采用了一种叫做倒排索引的方式来生成索引，避免了全文档扫描，那么什么是倒排索引呢，通过文档来查找关键词等数据的我们称为正排索引，返之，通过关键词来查找文档的形式我们称之为倒排索引，假设有以下三个文档（Document）要在其中找到含有 comming 的文档，如果要正排索引，那么要把每个文档的内容拿出来查找是否有此单词，毫无疑问这样的话会导致全表扫描，那么用倒排索引会怎么查找呢，它首先会将每个文档内容进行分词，小写化等，然后建立每个分词与包含有此分词的文档之前的映射关系，如果有多个文档包含此分词，那么就会按重要程度即文档的权重（通常是用 TF-IDF 给文档打分）将文档进行排序，于是我们可以得到如下关系这样的话我们我要查找所有带有 comming 的文档，就只需查一次，而且这种情况下查询多个单词性能也是很好的，只要查询多个条件对应的文档列表，再取交集即可，极大地提升了查询效率。画外音：这里简化了一些流程，实际上还要先根据单词表来定位单词等，不过这些流程都很快，可以忽略，有兴趣的读者可以查阅相关资料了解。除了倒排索引外，ES 的分布式架构也天然适合海量数据查询，来看下 ES 的架构一个 ES 集群由多个 node 节点组成，每个 index 是以分片（Shard，index 子集）的数据存在于多个 node 节点上的，这样的话当一个查询请求进来，分别在各个 node 查询相应的结果并整合后即可，将查询压力分散到多个节点，避免了单个节点 CPU，磁盘，内存等处理能力的不足。另外当新节点加入后，会自动迁移部分分片至新节点，实现负载均衡，这个功能是 ES自动完成的，对比一个下 MySQL 的分库分表需要开发人员引入 Mycat 等中间件并指定分库分表规则等繁琐的流程是不是一个巨大的进步？这也就意味着 ES 有非常强大的水平扩展的能力，集群可轻松扩展致几百上千个节点，轻松支持 PB 级的数据查询。当然 ES 的强大不止于此，它还采用了比如主备分片提升搜索吞率，使用节点故障探测，Raft 选主机制等提升了容灾能力等等，这些不是本文重点，读者可自行查阅，总之经过上面的简单总结大家只需要明白一点：ES 的分布式架构设计天生支持海量数据查询。那么 ES 的索引数据（index）如何生成的呢，接下来我们一起来看看本文的重点如何构建 ES 索引要构建 ES 索引数据，首先得有数据源，一般我们会使用 MySQL 作为数据源，你可以直接从 MySQL 中取数据然后再写入 ES，但这种方式由于直接调用了线上的数据库查询，可能会对线上业务造成影响，比如考虑这样的一个场景：在电商 APP 里用的最多的业务场景想必是用户输入关键词来查询相对应的商品了，那么商品会有哪些信息呢，一个商品会有多个 sku（sku 即同一个商品下不同规格的品类，比如苹果手机有 iPhone 6, iPhone 6s等）,会有其基本属性如价格，标题等，商品会有分类（居家，服饰等），品牌，库存等，为了保证表设计的合理性，我们会设计几张表来存储这些属性，假设有 product_sku（sku 表）, product_property（基本属性表）,sku_stock（库存表），product_category（分类表）这几张表，那么为了在商品展示列表中展示所有这些信息，就必须把这些表进行 join，然后再写入 ES，这样查询的时候就会在 ES 中获取所有的商品信息了。这种方案由于直接在 MySQL 中执行 join 操作，在商品达到千万级时会对线上的 DB 服务产生极大的性能影响，所以显然不可行，那该怎么生成中间表呢，既然直接在 MySQL 中操作不可行，能否把 MySQL 中的数据同步到另一个地方再做生成中间表的操作呢，也就是加一个中间层进行处理，这样就避免了对线上 DB 的直接操作，说到这相信大家又会对计算机界的名言有进一步的体会：没有什么是加一个中间层不能解决的，如果有，那就再加一层。这个中间层就是 hive什么是 hivehive 是基于 Hadoop 的一个数据仓库工具，用来进行数据提取、转化、加载，这是一种可以存储、查询和分析存储在 Hadoop 中的大规模数据的机制，它的意义就是把好写的 hive 的 sql 转换为复杂难写的 map-reduce 程序（map-reduce 是专门用于用于大规模数据集（大于1TB）的并行运算编程模型），也就是说如果数据量大的话通过把 MySQL 的数据同步到 hive，再由 hive 来生成上述的 product_tmp 中间表，能极大的提升性能。hive 生成临时表存储在 hbase（一个分布式的、面向列的开源数据库） 中，生成后会定时触发 dump task 调用索引程序，然后索引程序主要从 hbase 中读入全量数据，进行业务数据处理，并刷新到 es 索引中，整个流程如下这样构建索引看似很美好，但我们需要知道的是首先 hive 执行 join 任务是非常耗时的，在我们的生产场景上，由于数据量高达几千万，执行 join 任务通常需要几十分钟，从执行 join 任务到最终更新至 ES 整个流程常常需要至少半小时以上，如果这期间商品的价格，库存，上线状态（如被下架）等重要字段发生了变更，索引是无法更新的，这样会对用户体验产生极大影响，优化前我们经常会看到通过 ES 搜索出的中有状态是上线但实际是下架的产品，严重影响用户体验，那么怎么解决呢，有一种可行的方案：建立宽表既然我们发现 hive join 是性能的主要瓶颈，那么能否规避掉这个流程呢，能否在 MySQL 中将 product_sku，product_property，sku_stock 等表组合成一个大表（我们称其为宽表）这样在每一行中商品涉及到的的数据都有了，所以将 MySQL 同步到 hive 后，hive 就不需要再执行耗时的 join 操作了，极大的提升了整体的处理时间，从 hive 同步 MySQL 再到 dump 到 ES 索引中从原来的半小时以上降低到了几分钟以内，看起来确实不错，但几分钟的索引延迟依然是无法接受的。为什么 hive 无法做到实时导入索引因为 hive 构建在基于静态批处理的Hadoop 之上，Hadoop 通常都有较高的延迟并且在作业提交和调度的时候需要大量的开销。因此，hive 并不能够在大规模数据集上实现低延迟快速的查询等操作，再且千万级别的数据全量从索引程序导入到 ES 集群至少也是分钟级。另外引入了宽表，它的维护也成了一个新问题，设想 sku 库存变了，产品下架了，价格调整了，那么除了修改原表（sku_stock,product_categry 等表）记录之外，还要将所有原表变更到的记录对应到宽表中的所有记录也都更新一遍，这对代码的维护是个噩梦，因为你需要在所有商品相关的表变更的地方紧接着着变更宽表的逻辑，与宽表的变更逻辑变更紧藕合！PB 级的 ES 准实时索引构建之道该如何解决呢？仔细观察上面两个问题，其实都是同一个问题，如果我们能实时监听到 db 的字段变更，再将变更的内容实时同步到 ES 和宽表中不就解决了我们的问题了。怎么才能实时监听到表字段的变更呢？答案：binlog来一起复习下 MySQL 的主从同步原理MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events，可以通过 show binlog events 进行查看)MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)MySQL slave 重放 relay log 中事件，将数据变更反映它自己的数据可以看到主从复制的原理关键是 Master 和 Slave 遵循了一套协议才能实时监听 binlog 日志来更新 slave 的表数据，那我们能不能也开发一个遵循这套协议的组件，当组件作为 Slave 来获取 binlog 日志进而实时监听表字段变更呢？阿里的开源项目 Canal 就是这个干的，它的工作原理如下:canal 模拟 MySQL slave 的交互协议，伪装自己为 MySQL slave ，向 MySQL master 发送dump 协议MySQL master 收到 dump 请求，开始推送 binary log 给 slave (即 canal )canal 解析 binary log 对象(原始为 byte 流)这样的话通过 canal 就能获取 binlog 日志了，当然 canal 只是获取接收了 master 过来的 binlog，还要对 binlog 进行解析过滤处理等，另外如果我们只对某些表的字段感兴趣，该如何配置，获取到 binlog 后要传给谁? 这些都需要一个统一的管理组件，阿里的 otter 就是干这件事的。什么是 otterOtter 是由阿里提供的基于数据库增量日志解析，准实时同步到本机房或异地机房 MySQL 数据库的一个分布式数据库同步系统，它的整体架构如下：注：以上是我司根据 otter 改造后的业务架构，与原版 otter 稍有不同，不过大同小异主要工作流程如下在 Manager 配置好 zk，要监听的表 ，负责监听表的节点，然后将配置同步到 Nodes 中node 启动后则其 canal 会监听 binlog，然后经过 S（select），E（extract），T(transform)，L(load) 四个阶段后数据发送到 MQ然后业务就可以订阅 MQ 消息来做相关的逻辑处理了画外音：zookeeper 主要协调节点间的工作，如在跨机房数据同步时，可能要从 A 机房的节点将数据同步到 B 机房的节点，要用 zookeeper 来协调，大家应该注意到了node 中有 S，E，T，L 四个阶段，它们的主要作用如下Select 阶段: 为解决数据来源的差异性，比如接入 canal 获取增量数据，也可以接入其他系统获取其他数据等。Extract阶段: 组装数据,针对多种数据来源，mysql,oracle,store,file等,进行数据组装和过滤。Transform 阶段: 数据提取转换过程，把数据转换成目标数据源要求的类型Load 阶段: 数据载入,把数据载入到目标端，如写入迁移后的数据库， MQ，ES 等以上这套基于阿里 otter 改造后的数据服务我们将它称为 DTS（Data Transfer Service），即数据传输服务。搭建这套服务后我们就可以通过订阅 MQ 来实时写入 ES 让索引实时更新了，而且也可以通过订阅 MQ 来实现宽表字段的更新，解决了上文中所说的宽表字段更新与原表紧藕合的问题，基于 DTS 服务的索引改进架构如下:注意：「build 数据」这一模块对实时索引更新是透明的，这个模块主要用在更新或插入 MySQL 宽表，因为对于宽表来说，它是几个表数据的并集，所以并不是监听到哪个字段变更就更新哪个，它要把所有商品涉及到的所有表数据拉回来再更新到宽表中。于是，通过 MySQL 宽表全量更新+基于 DTS 的实时索引更新我们很好地解决了索引延迟的问题，能达到秒级的 ES 索引更新！这里有几个问题可能大家比较关心，我简单列一下需要订阅哪些字段对于 MySQL 宽表来说由于它要保存商品的完整信息，所以它需要订阅所有字段，但是对于红框中的实时索引更新而言，它只需要订阅库存，价格等字段，因为这些字段如果不及时更新，会对销量产生极大的影响，所以我们实时索引只关注这些敏感字段即可。有了实时索引更新，还需要全量索引更新吗需要，主要有两个原因：实时更新依赖消息机制，无法百分百保证数据完整性，需要全量更新来支持，这种情况很少，而且消息积压等会有告警，所以我们一天只会执行一次全量索引更新索引集群异常或崩溃后能快速重建索引全量索引更新的数据会覆盖实时索引吗会，设想这样一种场景，你在某一时刻触发了实时索引，然后此时全量索引还在执行中，还未执行到实时索引更新的那条记录，这样在的话当全量索引执行完之后就会把之前实时索引更新的数据给覆盖掉，针对这种情况一种可行的处理方式是如果全量索引是在构建中，实时索引更新消息可以延迟处理，等全量更新结束后再消费。也正因为这个原因，全量索引我们一般会在凌晨执行，由于是业务低峰期，最大可能规避了此类问题。总结本文简单总结了我司在 PB 级数据下构建实时 ES 索引的一些思路，希望对大家有所帮助，文章只是简单提到了 ES，canal，otter 等阿里中间件的应用，但未对这些中间件的详细配置，原理等未作过多介绍，这些中间件的设计非常值得我们好好研究下，比如 ES 为了提高搜索效率、优化存储空间做了很多工作，再比如 canal 如何做高可用，otter 实现异地跨机房同步的原理等，建议感兴趣的读者可以之后好好研究一番，相信你会受益匪浅。
百万excel数据导入功能如何实现？（知识星球专享）
前言最近星球中有两位小伙伴问了我关于excel数据导入的问题：使用Java如何处理复杂大数据量的excel导入解析和导入表。导入千万级excel文件到数据库，有没有比较快的方法？这两个问题有些相似之处，业务场景都是需要导入大数据量的excel文件到数据库。今天就跟大家一起聊聊，千万级excel数据导入功能该如何实现，希望这篇文章对你会有所帮助。1. 问题对于千万级excel数据导入功能，可能会遇到一些问题，我们先聊聊会到哪些问题。1.1 单个Sheet存不了我们都知道excel的一个Sheet，对于2003版，最大行数是65536行，对于2007以上版本，最大行数是1048576行。由此可见，千万级的数据是没法保存到excel的单个Sheet当中的。那我们该怎么办呢？1.2 出现OOM问题即使我们想办法，把一千万的数据保存到excel当中了，但如果程序在导入数据时，将这些数据加载到内存中，内存会直接爆掉，出现OOM问题。我们又该怎么办呢？1.3 导入耗时太长如果你在程序中使用单个线程，在循环中一个个将excel的数据写入到数据库，会有大量的远程IO操作，极有可能会导致该excel程序导入耗时很长。如何优化excel的导入性能呢？1.4 丢数据了如果excel中有1000万的数据，但是你导入数据的数据只有995万，少了5万数据。如何快速查询哪些数据导入失败了呢？如何处理异常数据呢？1.5 CPU使用率过高如果我们将excel改成多线程导入，并且每个线程导入时间非常短，可能会导致线程之间在不停的切换上下文，可能会导致CPU使用率过高问题。这时又该怎么办呢？带着这些问题，开始今天的文章之旅。2. EasyExcel导入海量数据的Excel文件，最大的问题莫过于程序出现OOM了。一旦出现OOM，程序将会直接挂掉。为了防止出现OOM问题，建议使用阿里开源的excel导入导出工具：EasyExcel。EasyExcel采用流式读取方式，我们可以直接从Excel文件中进行读取数据，不需要加载整个Excel文件到内存中。例如下面这段代码：public void readExcel(String filePath) { EasyExcel.read(filePath, MySheet.class, new MySheetListener()).sheet().headRowNumber(1).doRead(); } public class MySheetListener extends AnalysisEventListener { private static final int DATA_SIZE = 100000; private List dataList = new ArrayList<>(); @Override public void invoke(MySheet data, AnalysisContext context) { dataList.add(data); if (dataList.size() >= DATA_SIZE) { saveData(dataList); dataList.clear(); } } @Override public void doAfterAllAnalysed(AnalysisContext context) { saveData(dataList); } private void saveData(List dataList) { //保存数据 } }在上面的这段代码中，DATA_SIZE为100000，即每读取100000条数据，就会调用一次MySheetListener的invoke()方法，并将数据加入到dataList中，最终通过saveData()方法进行批量保存。这样可以避免一次性将全部数据读取到内存中，降低内存使用的风险。对于百万以内的数据，放在excel的一个Sheet中问题不大。但如果有千万级的数据，放在一个Sheet显然是保存不下的，我们可以将数据保存到多个Sheet中，或者直接将数据保存CSV文件中。EasyExcel3.1.1版本已支持csv读取了。3. 性能问题通过前面使用EasyExcel能够保证读取excel，不会出现OOM问题。但可能会出现性能问题。那么该如何解决呢？3.1 批量保存数据我们的程序在读取到一批excel数据之后，可能会有些业务逻辑处理，然后将数据保存到数据库当中。你可以在一个循环中，每一条数据进行一次业务逻辑处理，然后保存数据。例如这样的：for(Entity entity: dataList) { //业务逻辑处理 doBusiness(entity); //将数据保存到数据库 save(entity); }如果是这样保存数据，性能可能会大打折扣，因为上面的这段代码，在for循环中一次只保存一条数据，每次都需要访问远程的数据库，影响数据写入的性能。优化如下：List<Entity> saveList = Lists.newArrayList(); for(Entity entity: dataList) { //业务逻辑处理 doBusiness(entity); saveList.add(entity); } //将数据批量保存到数据库 batchSave(saveList);每次对数据库操作都是批量操作，这样可以减少访问数据库的频率，从而可以显著的提升写入数据的性能。但需要注意的是：批量写入数据时，一次性也不要写入太多的数据。比如如果一次性写入10万的数据，可能会导致写入数据库超时，或者访问该数据库的其他程序出现异常。如果数据太多了，可以分配写入数据库。例如：List<Entity> saveList = Lists.newArrayList(); for(Entity entity: dataList) { //业务逻辑处理 doBusiness(entity); saveList.add(entity); } //将数据批量保存到数据库 List<List<Entity>> parationList = Lists.partition(saveList,1000); for(List<Entity> subDataList: parationList) { batchSave(subDataList); }我们可以通过Lists.partition方法将大数据saveList进行分割，每1000条记录为一组。在这里推荐大家使用guava包下的Lists类进行集合操作，真的非常方便。3.2 多线程通过上面批量保存数据的优化，将数据的写入数据库的性能提升了一些。但如果程序除了保存数据之外，还有一些业务逻辑，这样优化还是不够的。如果你的程序用的是单线程，并且在一个循环中一条数据一条数据去处理业务逻辑，显然效率有点低。你可以将代码优化成批量处理业务逻辑，但有的时候，单个处理逻辑非常复杂，改成批量处理改造成本非常大。这时该如何优化性能呢？答：使用多线程。我们可以使用CompletableFuture类进行多线程调用，并且将生成的Entity对象保存到CopyOnWriteArrayList集合当中。例如：List<Entity> resultList = Lists.newCopyOnWriteArrayList(); CompletableFuture.allOf(dataList.stream() .map(data -> CompletableFuture.supplyAsync(() -> { //业务处理 return entity; }, executor) .whenComplete((r,ex)-> { resultList.add(r); } ) ) .toArray(CompletableFuture[]::new) ).join();最后将批量保存数据的地方改成resultList的批量保存。其中executor是配置的线程池，注意我们在日常工作中，使用CompletableFuture的场景中，记得要配置线程池，否则每次都是new一个新的线程。此外，如果使用多线程处理业务，可能会导致CPU使用率过高，要控制线程的数量，不能配置太多。但线程数也不能配置太少，会影响业务处理的速度，最后可以做成配置项，可以根据项目的实际情况进行动态调整。3.3 原始JDBC为了方便对数据库进行操作，少写一些访问数据库的代码，通常情况下，我们在项目中会使用一些开源的ORM映射框架，比如：Mybatis或者Hibernate等。这些ORM映射框架，虽说确实可以帮我们提升开发效率。但它们也引入了额外的逻辑，提升了代码的复杂度，比如底层使用了反射机制，相对于直接new一个对象，它的效率更低。使用原生JDBC进行数据库操作，确实需要多写一些代码，比如获取数据库Connection、获取PreparedStatement等。例如：@Resource(name="dataSource") private DataSource dataSource; /* JDBC 的批量操作 */ @Test public void insertBatch() { Connection connection=dataSource.getConnection(); connection.setAutoCommit(false); String sql="insert into test(a,b, c, d, d, f, g, , i) " + " values(?,?,?,?,?,?,?,?,?) "; PreparedStatement statement=connection.prepareStatement(sql); for (int i = 0; i < 1000 ; i++) { statement.setString(1, "aaa"+i); statement.setString(2, "bbb"+i); statement.setInt(3, i); statement.setInt(4, i); statement.setString(5, ""+i); statement.setString(6, "ddd"+i); statement.setString(7, "eee"+i); statement.setString(8, "fff"+i); statement.setString(9, "ggg"+i); statement.addBatch(); } long start=System.currentTimeMillis(); statement.executeBatch(); connection.commit(); connection.close(); statement.close(); }但这样的批量操作效率却非常高。4. 异常处理在数据导入的过程中，可能会出现一些异常情况，比如：网络抖动，导致某一次写入数据失败了。这时该如何处理呢？答：增加失败自动重试机制。我们在批量保存数据时，可以把异常捕获一下，如果判断是数据库的某些异常，可以自动重试2次，如果有任意一次成功了，则直接返回成功，否则返回失败。在日志中打印一条异常日志，记录批次编号，集合的第一个元素和最后一个元素的数据，方便后面可以快速定位数据。导入成功之后，一定要检查excel中的数据总条数，跟数据库中导入成功的数据总条数是否相等。看看有没有漏数据的情况。对于重试之后还是没有导入成功的数据，我们可以通过日志搜索出来，分析失败的原因，是程序bug，还是数据问题。如果是程序bug，修复了bug之后重新导入数据。如果是数据问题，要咨询相关业务人员，是直接丢弃数据，还是修改excel重新导入。当然这时的excel不是完整的excel，而是只包含异常数据的excel。5.数据重复我们的程序在导入excel文件时，通常情况下，需要先检查一下，该excel中是否存在重复的数据。但如果excel数据太多，没办法一次加载到内存，根本没法校验整个excel的数据的重复性，这时该怎么办呢？我们可以校验每一批次的数据中，是否包含了重复的数据，如果有重复的数据，则直接忽略，不用写入数据库。防止数据库中出现重复数据最有效的办法是，在表中给关键字段增加唯一索引。这时如果我们的程序在并发批量插入数据时，刚好插入了重复的数据，就会报：Duplicate entry * for key，导致程序导入失败。其实在导入数据时，可以使用insert ignore语法，它会判断如果存在主键或者唯一索引重复的数据，则直接忽略。这样及时excel文件不小了导入了两次，产生了并发写入的问题，也不会对程序造成太大的影响。
前言对于互联网公司来说，随着用户量和数据量的不断增加，慢查询是无法避免的问题。一般情况下如果出现慢查询，意味着接口响应慢、接口超时等问题。如果是高并发的场景，可能会出现数据库连接被占满的情况，直接导致服务不可用。慢查询的确会导致很多问题，我们要如何优化慢查询呢？主要解决办法有：监控sql执行情况，发邮件、短信报警，便于快速识别慢查询sql打开数据库慢查询日志功能简化业务逻辑代码重构、优化异步处理sql优化索引优化其他的办法先不说，后面有机会再单独介绍。今天我重点说说索引优化，因为它是解决慢查询sql问题最有效的手段。如何查看某条sql的索引执行情况呢？没错，在sql前面加上explain关键字，就能够看到它的执行计划，通过执行计划，我们可以清楚的看到表和索引执行的情况，索引有没有执行、索引执行顺序和索引的类型等。索引优化的步骤是：使用explain查看sql执行计划判断哪些索引使用不当优化sql，sql可能需要多次优化才能达到索引使用的最优值既然索引优化的第一步是使用explain，我们先全面的了解一下它。explain介绍先看看mysql的官方文档是怎么描述explain的：EXPLAIN可以使用于 SELECT， DELETE， INSERT， REPLACE，和 UPDATE语句。当EXPLAIN与可解释的语句一起使用时，MySQL将显示来自优化器的有关语句执行计划的信息。也就是说，MySQL解释了它将如何处理该语句，包括有关如何连接表以及以何种顺序连接表的信息。当EXPLAIN与非可解释的语句一起使用时，它将显示在命名连接中执行的语句的执行计划。对于SELECT语句， EXPLAIN可以显示的其他执行计划的警告信息。explain详解explain的语法：{EXPLAIN | DESCRIBE | DESC} tbl_name [col_name | wild] {EXPLAIN | DESCRIBE | DESC} [explain_type] {explainable_stmt | FORCONNECTION connection_id} explain_type: { EXTENDED | PARTITIONS | FORMAT = format_name } format_name: { TRADITIONAL | JSON } explainable_stmt: { SELECTstatement | DELETEstatement | INSERTstatement | REPLACEstatement | UPDATEstatement }用一条简单的sql看看使用explain关键字的效果：explain select * from test1;执行结果：从上图中看到执行结果中会显示12列信息，每列具体信息如下：说白了，我们要搞懂这些列的具体含义才能正常判断索引的使用情况。话不多说，直接开始介绍吧。id列该列的值是select查询中的序号，比如：1、2、3、4等，它决定了表的执行顺序。某条sql的执行计划中一般会出现三种情况：id相同id不同id相同和不同都有那么这三种情况表的执行顺序是怎么样的呢？1.id相同执行sql如下：explain select * from test1 t1 inner join test1 t2 on t1.id=t2.id结果：我们看到执行结果中的两条数据id都是1，是相同的。这种情况表的执行顺序是怎么样的呢？答案：从上到下执行，先执行表t1，再执行表t2。执行的表要怎么看呢？答案：看table字段，这个字段后面会详细解释。2.id不同执行sql如下：explain select * from test1 t1 where t1.id = (select id from test1 t2 where t2.id=2);结果：我们看到执行结果中两条数据的id不同，第一条数据是1，第二条数据是2。这种情况表的执行顺序是怎么样的呢？答案：序号大的先执行，这里会从下到上执行，先执行表t2，再执行表t1。3.id相同和不同都有执行sql如下：explain select t1.* from test1 t1 inner join (select max(id) mid from test1 group by id) t2 on t1.id=t2.mid结果：我们看到执行结果中三条数据，前面两条数据的的id相同，第三条数据的id跟前面的不同。这种情况表的执行顺序又是怎么样的呢？答案：先执行序号大的，先从下而上执行。遇到序号相同时，再从上而下执行。所以这个列子中表的顺序顺序是：test1、t1、也许你会在这里心生疑问：是什么鬼？它表示派生表，别急后面会讲的。还有一个问题：id列的值允许为空吗？答案在后面揭晓。select_type列该列表示select的类型。具体包含了如下11种类型：但是常用的其实就是下面几个：SIMPLE简单SELECT查询，不包含子查询和UNIONPRIMARY复杂查询中的最外层查询，表示主要的查询SUBQUERYSELECT或WHERE列表中包含了子查询DERIVEDFROM列表中包含的子查询，即衍生UNIONUNION关键字之后的查询UNION RESULT从UNION后的表获取结果集下面看看这些SELECT类型具体是怎么出现的：SIMPLE执行sql如下：explain select * from test1;结果：它只在简单SELECT查询中出现，不包含子查询和UNION，这种类型比较直观就不多说了。PRIMARY和SUBQUERY执行sql如下：explain select * from test1 t1 where t1.id = (select id from test1 t2 where t2.id=2);结果：我们看到这条嵌套查询的sql中，最外层的t1表是PRIMARY类型，而最里面的子查询t2表是SUBQUERY类型。DERIVED执行sql如下：explain select t1.* from test1 t1 inner join (select max(id) mid from test1 group by id) t2 on t1.id=t2.mid结果：最后一条记录就是衍生表，它一般是FROM列表中包含的子查询，这里是sql中的分组子查询。UNION 和UNION RESULT执行sql如下：explain select * from test1 union select* from test2结果：test2表是UNION关键字之后的查询，所以被标记为UNION，test1是最主要的表，被标记为PRIMARY。而<union1,2>表示id=1和id=2的表union，其结果被标记为UNION RESULT。UNION 和UNION RESULT一般会成对出现。此外，回答上面的问题：id列的值允许为空吗？如果仔细看上面那张图，会发现id列是可以允许为空的，并且是在SELECT类型为：UNION RESULT的时候。table列该列的值表示输出行所引用的表的名称，比如前面的：test1、test2等。但也可以是以下值之一：<unionM,N>：具有和id值的行的M并集N。：用于与该行的派生表结果id的值N。派生表可能来自（例如）FROM子句中的子查询 。：子查询的结果，其id值为Npartitions列该列的值表示查询将从中匹配记录的分区type列该列的值表示连接类型，是查看索引执行情况的一个重要指标。包含如下类型：执行结果从最好到最坏的的顺序是从上到下。我们需要重点掌握的是下面几种类型：system >const >eq_ref > ref >range > index >ALL在演示之前，先说明一下test2表中只有一条数据：并且code字段上面建了一个普通索引：下面逐一看看常见的几个连接类型是怎么出现的：system这种类型要求数据库表中只有一条数据，是const类型的一个特例，一般情况下是不会出现的。const通过一次索引就能找到数据，一般用于主键或唯一索引作为条件的查询sql中，执行sql如下：explain select * from test2 where id=1;结果：eq_ref常用于主键或唯一索引扫描。执行sql如下：explain select * from test2 t1 inner join test2 t2 on t1.id=t2.id;结果：此时，有人可能感到不解，const和eq_ref都是对主键或唯一索引的扫描，有什么区别？答：const只索引一次，而eq_ref主键和主键匹配，由于表中有多条数据，一般情况下要索引多次，才能全部匹配上。ref常用于非主键和唯一索引扫描。执行sql如下：explain select * from test2 where code = '001';结果：range常用于范围查询，比如：between ... and 或 In 等操作，执行sql如下：explain select * from test2 where id between 1 and 2;结果：index全索引扫描。执行sql如下：explain select code from test2;结果：ALL全表扫描。执行sql如下：explain select * from test2;结果：possible_keys列该列表示可能的索引选择。请注意，此列完全独立于表的顺序，这就意味着possible_keys在实践中，某些键可能无法与生成的表顺序一起使用。如果此列是NULL，则没有相关的索引。在这种情况下，您可以通过检查该WHERE 子句以检查它是否引用了某些适合索引的列，从而提高查询性能。key列该列表示实际用到的索引。可能会出现possible_keys列为NULL，但是key不为NULL的情况。演示之前，先看看test1表结构：test1表中数据：使用的索引：code和name字段使用了联合索引。执行sql如下：explain select code from test1;结果：这条sql预计没有使用索引，但是实际上使用了全索引扫描方式的索引。key_len列该列表示使用索引的长度。上面的key列可以看出有没有使用索引，key_len列则可以更进一步看出索引使用是否充分。不出意外的话，它是最重要的列。有个关键的问题浮出水面：key_len是如何计算的？决定key_len值的三个因素：1.字符集2.长度3.是否为空常用的字符编码占用字节数量如下：目前我的数据库字符编码格式用的：UTF8占3个字节。mysql常用字段占用字节数：char(n)nvarchar(n)n + 2tinyint1smallint2int4bigint8date3timestamp4datetime8此外，如果字段类型允许为空则加1个字节。上图中的 184是怎么算的？184 =30 3 + 2+ 30 3 + 2再把test1表的code字段类型改成char，并且改成允许为空：执行sql如下：explain select code from test1;结果：怎么算的？183 =30 3 + 1+ 30 3 + 2还有一个问题：为什么这列表示索引使用是否充分呢，还有使用不充分的情况？执行sql如下：explain select code from test1 where code='001';结果：上图中使用了联合索引：idx_code_name，如果索引全匹配key_len应该是183，但实际上却是92，这就说明没有使用所有的索引，索引使用不充分。ref列该列表示索引命中的列或者常量。执行sql如下：explain select * from test1 t1 inner join test1 t2 on t1.id=t2.id where t1.code='001';结果：我们看到表t1命中的索引是const(常量)，而t2命中的索引是列sue库的t1表的id字段。rows列该列表示MySQL认为执行查询必须检查的行数。对于InnoDB表，此数字是估计值，可能并不总是准确的。filtered列该列表示按表条件过滤的表行的估计百分比。最大值为100，这表示未过滤行。值从100减小表示过滤量增加。rows显示了检查的估计行数，rows× filtered显示了与下表连接的行数。例如，如果 rows为1000且 filtered为50.00（50％），则与下表连接的行数为1000×50％= 500。Extra列该字段包含有关MySQL如何解析查询的其他信息，这列还是挺重要的，但是里面包含的值太多，就不一一介绍了，只列举几个常见的。Impossible WHERE表示WHERE后面的条件一直都是false，执行sql如下：explain select code from test1 where 'a' = 'b';结果：Using filesort表示按文件排序，一般是在指定的排序和索引排序不一致的情况才会出现。执行sql如下：explain select code from test1 order by name desc;结果：这里建立的是code和name的联合索引，顺序是code在前，name在后，这里直接按name降序，跟之前联合索引的顺序不一样。Using index表示是否用了覆盖索引，说白了它表示是否所有获取的列都走了索引。上面那个例子中其实就用到了：Using index，因为只返回一列code，它字段走了索引。Using temporary表示是否使用了临时表，一般多见于order by 和 group by语句。执行sql如下：explain select name from test1 group by name;结果：Using where表示使用了where条件过滤。Using join buffer表示是否使用连接缓冲。来自较早联接的表被部分读取到联接缓冲区中，然后从缓冲区中使用它们的行来与当前表执行联接。索引优化的过程1.先用慢查询日志定位具体需要优化的sql2.使用explain执行计划查看索引使用情况3.重点关注：key（查看有没有使用索引）key_len（查看索引使用是否充分）type（查看索引类型）Extra（查看附加信息：排序、临时表、where条件为false等）一般情况下根据这4列就能找到索引问题。4.根据上1步找出的索引问题优化sql5.再回到第2步
明明是同一条SQL，为什么有时候走索引a，有时候却走索引b ？
前言想象你是一家餐厅的服务员，面前有两个菜单：菜单A：按菜品分类排列（前菜、主菜、甜点）菜单B：按价格从低到高排列当顾客说："我要最便宜的川菜"。你会：先用菜单B找到所有低价菜从中筛选川菜或者：先用菜单A找到所有川菜再按价格排序这就是MySQL优化器的日常决策！明明是同一条SQL，有时候走的索引a，而有时候走的索引b，就是它的锅。今天这篇文章跟大家一起聊聊，MySQL选错索引的问题，希望对你会有所帮助。1 一个让程序员崩溃的案例现在有个需求：查询今年开始已付款的前100个订单。给status字段创建了索引idx_status。给create_time字段创建了索引idx_create_time。查询订单的sql如下：SELECT * FROM orders WHERE status = 'paid' -- 状态条件 AND create_time > '2025-01-01' -- 时间条件 ORDER BY amount DESC LIMIT 100;周一执行计划如下：使用索引：idx_status（状态索引） 扫描行数：500行 耗时：0.1秒周二执行计划如下：使用索引：idx_create_time（时间索引） 扫描行数：50万行 耗时：8秒周一只扫描了500行数据，而周二却扫描了50万行数据。周一耗时0.1秒，而周二耗时却又8秒。同一SQL在不同时间性能差异80倍！让我们拆解背后的原因。2 揭秘优化器的"决策三步曲"MySQL优化器的决策流程如下：成本计算示例：idx_status50万50万次需要排序1050分idx_create_time5万5万次无需排序600分根据扫描行数、回表次数、排序成本，计算一个总成本的分数。优化器会选择总成本更低的idx_create_time索引。3 导致索引切换的四大真凶真凶1：数据分布变化场景还原：周一数据：已支付订单5万条，其中2025年的5万条周二数据：已支付订单50万条，其中2025年的50万条这个例子中数据分布变化很大，周二的数据，比周一的数据一下子多了45万。可能会影响总成本的分数。我们可以通过下面的SQL查看数据分布：SELECT COUNT(*) AS total, SUM(status='paid') AS paid_count, SUM(create_time>'2023-01-01') AS new_orders FROM orders;真凶2：统计信息过期统计信息过期，就像用去年的地图导航，新修的路不会出现在地图上。MySQL的"地图"就是统计信息。我们可以通过ANALYZE TABLE ... DELETE STATISTICS命令删除统计信息：ANALYZE TABLE orders DELETE STATISTICS;这时候查询可能变成全表扫描：EXPLAIN SELECT...显示type: ALL那么，如何解决这个问题呢？使用ANALYZE TABLE命令，刷新统计信息（相当于更新地图）：ANALYZE TABLE orders;真凶3：索引覆盖度差异点餐类比：菜单A能直接看到菜品价格 → 无需问厨师（覆盖索引）菜单B只能看到菜品名 → 需要问厨师详情（回表查询）下面的SQL会走idx_status（需要回表）：SELECT * FROM orders WHERE status='paid';下面的SQL会走idx_create_time（覆盖索引）：SELECT create_time FROM orders WHERE create_time>'2023-01-01';真凶4：索引碎片化索引碎片化就像书本的目录页被撕破，找内容变得困难。检查方法：SHOW TABLE STATUS LIKE 'orders';查看Data_free字段，值越大碎片越多。优化方案：使用ALTER TABLE命令重建索引。ALTER TABLE orders ENGINE=INNODB;4 问题排查四步法第一步：查看当前执行计划使用EXPLAIN查看当前SQL的执行计划：EXPLAIN SELECT * FROM orders WHERE status='paid' AND create_time>'2023-01-01';第二步：检查统计信息使用SHOW INDEX命令检查索引的统计信息：SHOW INDEX FROM orders;关注Cardinality字段，值越接近真实数据越好。第三步：分析数据分布使用下面的SQL分析数据分布:SELECT COUNT(*) AS total, AVG(LENGTH(status)) AS status_avg_len FROM orders;第四步：追踪优化器思考过程SET optimizer_trace="enabled=on"; SELECT * FROM orders WHERE ...; SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;开启optimizer_trace，然后通过INFORMATION_SCHEMA.OPTIMIZER_TRACE表查看追踪优化器思考过程。5 三大终极解决方案方案1：引导优化器选择使用FORCE INDEX强制使用指定索引：SELECT * FROM orders FORCE INDEX(idx_status) WHERE ...;方案2：创建更优索引创建更优的联合索引：ALTER TABLE orders ADD INDEX idx_status_create_time(status,create_time);方案3：定期维护计划定期统计信息更新定期碎片率检查定期索引重建总结六个必须检查的点WHERE条件字段是否有合适索引ORDER BY/GROUP BY是否利用索引排序统计信息是否最新（尤其大表每天更新）是否存在索引碎片（每月检查一次）是否出现索引合并（INDEX_MERGE）是否使用覆盖索引（减少回表）三条黄金法则二八定律：20%的索引满足80%的查询数据驱动：定期分析查询模式调整索引防御编程：核心查询明确指定索引
一、何为压力测试1.1、 大白话解释性能压测是什么：就是考察当前软件和硬件环境下，系统所能承受的最大负荷，并帮助找出系统的瓶颈所在。性能压测的目的：为了系统在线上的处理能力和稳定性维持在一个标准范围内，做到知己知彼，百战不殆。还可以发现内存泄漏、并发与同步的问题。1.2、性能指标RepsonseTime - RT：响应时间，用户从客户端发起一个请求开始计算，到客户端接收到服务端的响应结束，整个过程所耗费的时间。Hits Per Second - HPS：用户每秒点击次数，也就是每秒向后台发送的请求次数。QPS：系统每秒内处理查询的次数。MaxRT：最大响应时间，指用户发出请求到服务端返回响应的最大时间。MiniRT：最少响应时间，指用户发出请求到服务端返回响应的最少时间。90%响应时间：将所有用户的响应时间进行升序排序，取 90 % 的位置。性能测试关注点：吞吐量：每秒钟系统能处理的请求数、任务数。响应时间：服务处理一个请求或一个任务的耗时。错误率：一批请求中结果出过错的请求所占比例。二、Jmeter 压测工具1、Jmeter 工具下载和安装 Jmeter 工具下载地址：https://jmeter.apache.org/download_jmeter.cgi我下载的版本是 apache-jmeter-5.3运行 JMeter 程序打开批处理文件：\apache-jmeter-5.3\bin[jmeter.bat](http://jmeter.bat/)。如下图所示：添加线程组，如下图所示：添加线程组1s 内启动 200 个线程，循环次数 100 次。2 w 个请求。如下图所示：测试 HTTP 请求，如下图所示：配置要测试的协议、服务器地址、端口号。配置信息如下：协议：使用 http 协议。服务器名称或 IP: www.baidu.com (只是为了演示)。端口号：80 端口。如下图所示：添加查看结果树、汇总报告和聚合报告。如下图所示：开始压力测试。点击播放按钮就开始启动了。注意启动之前需要先设置线程组的参数配置和 HTTP 请求的配置。如下图所示：查看每个请求结果，如下图所示：查看汇总报告。主要关心平均值和吞吐量。200 个线程，每个线程调用 100 次，总共 2 w 次，可以看到下图中表格中的样本列也是 2 w，请求所耗费的时间是 151 ms，吞吐量是 880 个请求每秒。如下图所示：查看聚合报告。主要看中位数和90%百分位，中位数是 59 ms，说明大部分请求的响应时间是 59 ms。90 % 的请求 都是在 271 ms 以内响应完成的。异常 0.41% 说明 2 w 个请求中有 82 个请求异常（20000 * 0.0041 = 82 ）。吞吐量 880.2/sec 说明百度这个网站每秒能处理 880 个请求，性能一般（可能跟本地机器性能有关）。如下图所示：查看汇总图。查看汇总图时，需要先勾选想要查看的信息，如下图所示：然后查看图形汇总：可以看到勾选的几列在图表中是用不同颜色表示的，比如绿色的柱状条就是 90 % 百分位。我们来测试下 PassJava（佳必过）的管理后台的性能，吞吐量接近 2000/s。如下图所示：三、性能监控之 jconsolejconsole 和 jvisualvm 是 Java JDK 的两个小工具，用来监控内存泄漏、跟踪垃圾回收、执行时的内存情况、对 CPU 进行分析、线程的分析。都可以通过命令行启动，而且可以监控本地和远程应用。而 jvisualvm 是升级版的 jconsole。我们先来看下 jconsole 的使用。首先用 cmd 命令行的方式启动 jconsole。启动 jconsole命令行启动 jconsole选择监控哪个应用然后选择 passjava 项目的 question 服务。选择 passjava-question 微服务对应的就是下面这个微服务：passjava-question。对应 passjava-question 微服务概览从监控界面上有 6 个菜单，首先看到的是概览功能，上面有堆内存使用量、线程数、类的使用情况、CPU 占用率，都是用趋势图来表示的，能很方便的看出当前性能的概览。注意：这些监控都是实时的。概览内存下面是内存的使用情况，可以从下图中看到有个下拉框，里面可以选择不同的内存维度，然后下面的图标和柱状图也会跟着选择的维度而展示不同。内存下面是线程的使用情况，可以看到线程峰值和活动线程的总数量，目前看到的峰值是59，活动线程数是 57。下半部分可以看到具体是哪些线程，以及线程的堆栈信息，非常详细。线程使用情况类下面是类的加载和卸载情况，已加载类总数是 10679，而已卸载的类是 1 个，所以当前已加装当前类的总数是 10679 - 1 = 10678 个。类的加载和卸载情况VM 概要我们再来看下VM（虚拟机）的情况。如下图所示，可以看到虚拟机情况，线程、类、堆的概要信息，以及 VM 的参数，是不是很方便呀~VM 概要MBean 信息接下来我们来看下 MBean 信息。对于 MBean，可能很多同学不知道是啥，下面做个解释：MBean就是一种规范的JavaBean，通过集成和实现一套标准的Bean接口，这种叫MBean。MBean可以用来干嘛？就是可以有一套JDK级别的对外的服务接口。比如，你写了一个JVM允许状态辅助查询的Bean,你希望别人下载一个Jconsole就可以看到你写的杰作。那你就可以考虑用MBean规范来实现。很多垃圾收集器算法Bean就这么干的（说的就是这个类sun.management.MemoryImpl）。MBean 信息四、性能监控之 jvisualvmjvisualvm 比 jconsole 更强大，界面展示的信息更丰富。还可以安装插件，太赞了。启动 jvisualvm 和概述启动方式和 jconsole 一样，也是通过 cmd 命令行启动。还是选择 passjava-question 微服务，然后选择第一个菜单栏：概述。可以看到 JVM 的版本，启动参数等信息。启动jvisualvm监视监视 CPU、堆、类、线程的情况。整体显示的效果比 jconsole 更美观。监视线程再来查下线程的情况。可以看到有 5 种状态的线程：运行：正在运行的线程。休眠：休眠状态的线程。等待：等待执行的线程。驻留：线程里面的空闲线程。监视：阻塞的线程，正在等待锁。抽样器另外我们也可以抽样器对 CPU 或内存进行抽样。如下图所示，对内存进行抽样。抽样插件的使用安装Visual GC 插件安装步骤：工具->插件->可用插件->Visual GC->安装。安装完成后，重启就可以使用插件功能了。安装完成后，就可以看到Visual GC 插件下图是实时监控垃圾回收的情况。Visual GC五、对网关的性能测试现在我想对 Passjava 系统的 question 微服务的接口进行一个压测，该如何进行呢？首先我们来看下 passjava 的架构是怎么样的，如下图所示：客户端分为手机端和 PC 端，http 请求先经过 API Gateway，然后再转发到 question 微服务。其中涉及到了中间件：Gateway 网关。我们来对 Gateway 网关进行压力测试。网关的端口号是 8060，我们配置下 JMeter。如下图所示：配置每秒发送 200 个请求，一直循环执行，直到手动停止压测。如下图所示：可以看下执行结果，吞吐量在 2422 个每秒，还是比较高的。吞吐量：2422/s 。90% 响应时间：142 毫秒。99% 响应时间：281 毫秒。我们再来看看垃圾回收的情况，Eden 区垃圾回收用时 2.7 s，用时太长了吧，看看这里怎么能优化下。通常的优化方向是增大新生代堆内存配置。六、对微服务的性能测试根据上面的架构原理图，我们知道客户端请求都是经过 Gateway 转发了一次的，如果我们想单独看下微服务的性能该怎么测试呢？下面我来演示下如何测试 passjava-question 微服务的性能。首先需要在 passjava-question 微服务中添加一个测试方法：测试方法有两种方式测试这个 api 是否添加正确。第一种用 postman 测试下这个请求是否能正确响应，返回 “test” 则表示响应正确。test api 是否能正确响应第二种通过浏览器进行测试。浏览器地址栏输入以下链接后，回车，看下浏览器窗口是否显示 “test”，是则表示响应正确。然后我们需要用 Jmeter 压测工具来测试这个微服务下的 api 的性能究竟如何。单独压测微服务的 api的结果吞吐量：3542/s 。90% 响应时间：100 毫秒。99% 响应时间：152 毫秒。七、对网关+微服务的性能测试如果我们想对这个整个请求链路进行性能测试该怎么做？首先请求需要先经过网关，然后由网关转发到微服务。在之前的文章中，我已经将网关配置好了，所以要想通过网关转发到 test 请求，只需要对请求路径稍作修改即可，如下所示：http://localhost:8060/api/question/v1/admin/question/test然后在浏览器输入该网址，返回 “test” 即表示响应正确。然后我们还是用 Jmeter 压测工具测试下 test api 的性能。测试结果如下图所示：网关+微服务的压力测试结果从结果可以看到：吞吐量：982/s 。90% 响应时间：437 毫秒。99% 响应时间：790毫秒。这里做个横向对比：横向对比说明微服务 api 经过网关转发一次后，性能至少下降了一半。可以得出结果：中间件越多，性能损失越大，大部分损失都是网络交互导致的。可以通过增强网络通信质量来减少网络的延迟。八、对数据库查询进行优化一般情况下，出现性能问题更多的是业务中查询数据库的耗时。接下来看下如何优化数据的查询。下面是一个查询问题列表的 api：通过问题类型 type 字段过滤问题列表。api 路径如下：http://localhost:11000/question/v1/admin/question/list?type=5这个 api 的代码如下，很容易看懂。查询问题列表的 api我们加些测试代码：统计查询数据库的耗时。如下所示：耗时统计然后重启 passjava-question 服务，再次测试这个 api，耗时 43 ms怎么对查询进行优化呢？很容易想到加索引，我们来试下加在 question 表加索引后的效果。给 type 字段加上普通索引，如下图所示：添加索引我们再来看下加了索引后的耗时情况：耗时 18 ms，确实比之前的 43 ms 快了很多。加了索引后的情况九、优化垃圾回收我们可以通过 jvisulavm工具查看垃圾回收的情况，Eden 区频繁发生 GC，短时间（1分钟）内共造成了 480 次 stop the world。另外从压测工具中也可以看到，吞吐量为 275/s。原因是 Eden 区的内存分配得太小了，只有 32 M，我们来调大一点。32M Eden 区频繁进行垃圾回收增大 Eden 区大小通过在 IDEA 工具中配置以下参数，调整堆内存最大为 1024 M，新生代内存为 512 M。-Xmx1024m -Xms1024m -Xmn512m然后可以观察到在相当长的短时间（1分钟）内只进行了 92 次垃圾回收，说明垃圾回收的频率降低了。应用程序的性能也提升了。另外从压测工具中也可以看到，吞吐量为 347/s，吞吐量也有较大提升。十、总结本文通过压测工具 Jmeter 讲解压测如何实施，然后用性能监控工具 jconsole 和 jvisualvm 来监控 Java 应用程序的性能，以及如何用工具来优化开源项目 passjava 的性能，并且非常详细地介绍了每一步以及执行结果，通过对比的方式，更加清晰地知道如何做性能优化。下面是对系统性能的常规优化手段：中间件较多时，优化网络通信质量。数据库查询耗时时，需要对查询进行优化，比如添加索引。模板的渲染速度，可以通过设置模板缓存。静态资源的获取，可以通过 Nginx 动静分离来解决。（下期再讲）日志太多，需要减少不必要的打 log 操作。
慢查询日志概念MySQL的慢查询日志是MySQL提供的一种日志记录，它用来记录在MySQL中响应时间超过阀值的语句，具体指运行时间超过long_query_time值的SQL，则会被记录到慢查询日志中。long_query_time的默认值为10，意思是运行10S以上的语句。默认情况下，Mysql数据库并不启动慢查询日志，需要我们手动来设置这个参数，当然，如果不是调优需要的话，一般不建议启动该参数，因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件，也支持将日志记录写入数据库表。慢查询日志相关参数MySQL 慢查询的相关参数解释：slow_query_log ：是否开启慢查询日志，1表示开启，0表示关闭。log-slow-queries ：旧版（5.6以下版本）MySQL数据库慢查询日志存储路径。可以不设置该参数，系统则会默认给一个缺省的文件host_name-slow.logslow-query-log-file：新版（5.6及以上版本）MySQL数据库慢查询日志存储路径。可以不设置该参数，系统则会默认给一个缺省的文件host_name-slow.loglong_query_time ：慢查询阈值，当查询时间多于设定的阈值时，记录日志。log_queries_not_using_indexes：未使用索引的查询也被记录到慢查询日志中（可选项）。log_output：日志存储方式。log_output='FILE'表示将日志存入文件，默认值是'FILE'。log_output='TABLE'表示将日志存入数据库，这样日志信息就会被写入到mysql.slow_log表中。MySQL数据库支持同时两种日志存储方式，配置的时候以逗号隔开即可，如：log_output='FILE,TABLE'。日志记录到系统的专用日志表中，要比记录到文件耗费更多的系统资源，因此对于需要启用慢查询日志，又需要能够获得更高的系统性能，那么建议优先记录到文件。一，第一步.开启mysql慢查询方式一：修改配置文件 在 my.ini 增加几行: 主要是慢查询的定义时间，以及慢查询log日志记录（ slow_query_log）方法二：通过MySQL数据库开启慢查询:二，分析慢查询日志直接分析mysql慢查询日志 ,利用explain关键字可以模拟优化器执行SQL查询语句，来分析sql慢查询语句例如：执行EXPLAIN SELECT * FROM res_user ORDER BYmodifiedtime LIMIT 0,1000得到如下结果：显示结果分析： table | type | possible_keys | key |key_len | ref | rows | Extra EXPLAIN列的解释：table 显示这一行的数据是关于哪张表的type 这是重要的列，显示连接使用了何种类型。从最好到最差的连接类型为const、eq_reg、ref、range、indexhe和ALLrows 显示需要扫描行数key 使用的索引三，常见的慢查询优化（1）索引没起作用的情况使用LIKE关键字的查询语句在使用LIKE关键字进行查询的查询语句中，如果匹配字符串的第一个字符为“%”，索引不会起作用。只有“%”不在第一个位置索引才会起作用。使用多列索引的查询语句MySQL可以为多个字段创建索引。一个索引最多可以包括16个字段。对于多列索引，只有查询条件使用了这些字段中的第一个字段时，索引才会被使用。（2）优化数据库结构合理的数据库结构不仅可以使数据库占用更小的磁盘空间，而且能够使查询速度更快。数据库结构的设计，需要考虑数据冗余、查询和更新的速度、字段的数据类型是否合理等多方面的内容。将字段很多的表分解成多个表对于字段比较多的表，如果有些字段的使用频率很低，可以将这些字段分离出来形成新表。因为当一个表的数据量很大时，会由于使用频率低的字段的存在而变慢。增加中间表对于需要经常联合查询的表，可以建立中间表以提高查询效率。通过建立中间表，把需要经常联合查询的数据插入到中间表中，然后将原来的联合查询改为对中间表的查询，以此来提高查询效率。（3）分解关联查询将一个大的查询分解为多个小查询是很有必要的。很多高性能的应用都会对关联查询进行分解，就是可以对每一个表进行一次单表查询，然后将查询结果在应用程序中进行关联，很多场景下这样会更高效，例如：SELECT * FROM tagJOIN tag_post ON tag_id = tag.idJOIN post ON tag_post.post_id = post.idWHERE tag.tag = 'mysql';分解为：SELECT * FROM tag WHERE tag = 'mysql';SELECT * FROM tag_post WHERE tag_id = 1234;SELECT * FROM post WHERE post.id in (123,456,567);（4）优化LIMIT分页在系统中需要分页的操作通常会使用limit加上偏移量的方法实现，同时加上合适的order by 子句。如果有对应的索引，通常效率会不错，否则MySQL需要做大量的文件排序操作。一个非常令人头疼问题就是当偏移量非常大的时候，例如可能是limit 10000,20这样的查询，这是mysql需要查询10020条然后只返回最后20条，前面的10000条记录都将被舍弃，这样的代价很高。优化此类查询的一个最简单的方法是尽可能的使用索引覆盖扫描，而不是查询所有的列。然后根据需要做一次关联操作再返回所需的列。对于偏移量很大的时候这样做的效率会得到很大提升。对于下面的查询：select id,title from collect limit 90000,10;该语句存在的最大问题在于limit M,N中偏移量M太大（我们暂不考虑筛选字段上要不要添加索引的影响），导致每次查询都要先从整个表中找到满足条件 的前M条记录，之后舍弃这M条记录并从第M+1条记录开始再依次找到N条满足条件的记录。如果表非常大，且筛选字段没有合适的索引，且M特别大那么这样的代价是非常高的。试想，如我们下一次的查询能从前一次查询结束后标记的位置开始查找，找到满足条件的100条记录，并记下下一次查询应该开始的位置，以便于下一次查询能直接从该位置 开始，这样就不必每次 查询都先从整个表中先找到满足条件的前M条记录，舍弃，在从M+1开始再找到100条满足条件的记录了。方法一：虑筛选字段（title）上加索引。title字段加索引（此效率如何未加验证）方法二：先查询出主键id值select id,title from collect where id>=(select id from collect order by id limit 90000,1) limit 10;原理：先查询出90000条数据对应的主键id的值，然后直接通过该id的值直接查询该id后面的数据。方法三：“关延迟联”如果这个表非常大，那么这个查询可以改写成如下的方式：Select news.id, news.description from news inner join (select id from news order by title limit 50000,5) as myNew using(id); 这里的“关延迟联”将大大提升查询的效率，它让MySQL扫描尽可能少的页面，获取需要的记录后再根据关联列回原表查询需要的所有列。这个技术也可以用在优化关联查询中的limit。方法四：建立复合索引 acct_id和create_timeselect * from acct_trans_log WHERE acct_id = 3095 order by create_time desc limit 0,10日志分析工具mysqldumpslow在生产环境中，如果要手工分析日志，查找、分析SQL，显然是个体力活，MySQL提供了日志分析工具mysqldumpslow。查看mysqldumpslow的帮助信息：Usage: mysqldumpslow [ OPTS... ] [ LOGS... ]Parse and summarize the MySQL slow query log. Options are--verbose verbose--help write this text to standard output-v verbose-s ORDER what to sort by (al, at, ar, c, l, r, t), 'at' is defaultar: average rows sentc: countr: rows sent-r reverse the sort order (largest last instead of first)-a don't abstract all numbers to N and strings to 'S'-g PATTERN grep: only consider stmts that include this stringdefault is '*', i.e. match all-l don't subtract lock time from total time-s, 是表示按照何种方式排序，c: 访问计数l: 锁定时间r: 返回记录t: 查询时间al:平均锁定时间ar:平均返回记录数at:平均查询时间-t, 是top n的意思，即为返回前面多少条的数据；-g, 后边可以写一个正则匹配模式，大小写不敏感的；比如：得到返回记录集最多的10个SQL。mysqldumpslow -s r -t 10 /database/mysql/mysql06_slow.log；得到访问次数最多的10个SQL，mysqldumpslow -s c -t 10 /database/mysql/mysql06_slow.log；得到按照时间排序的前10条里面含有左连接的查询语句，mysqldumpslow -s t -t 10 -g “left join” /database/mysql/mysql06_slow.log；另外建议在使用这些命令时结合 | 和more 使用 ，否则有可能出现刷屏的情况。mysqldumpslow -s r -t 20 /mysqldata/mysql/mysql06-slow.log | more
前言"苏工，订单列表又崩了！"接到电话时，我对着监控大屏上999ms的SQL响应时间哭笑不得。几年来，我发现一个定律：所有SQL问题都是在凌晨三点爆发！今天抽丝剥茧，教你用架构师的思维给慢SQL开刀手术。希望对你会有所帮助。1 术前检查：找准病灶1.1 EXPLAIN 查看执行计划使用EXPLAIN查看SQL语句的执行计划，相当于给SQL拍了张X光。下面是一个典型的SQL问题，它是某电商平台历史订单查询的SQL语句：SELECT * FROM orders o LEFTJOINusers u ON o.user_id = u.id LEFTJOIN products p ON o.product_id = p.id WHERE o.create_time > '2023-01-01' AND u.vip_level > 3 AND p.category_id IN (5,8) ORDERBY o.amount DESC LIMIT1000,20;使用EXPLAIN关键字查看执行计划的结果如下：+----+-------------+-------+------+---------------+------+---------+------+---------+---------------------------------+ | id | select_type | table | type | possible_keys | key | rows | Extra| key_len | +----+-------------+-------+------+---------------+------+---------+------+---------+---------------------------------+ | 1 | SIMPLE | o | ALL | idx_user_time | NULL | 1987400 | Using where; Using filesort | | 1 | SIMPLE | u | ALL | PRIMARY | NULL | 100000 | Using where | | 1 | SIMPLE | p | ALL | PRIMARY | NULL | 50000 | Using where | +----+-------------+-------+------+---------------+------+---------+------+---------+---------------------------------+诊断报告：全表扫描三连击（type=ALL）filesort暴力排序（内存警告）索引全军覆没2 手术方案：精准打击2.1 单表代谢手术如果通过执行计划查到是索引有问题，我们就需要单独优化索引。病根：JSON字段索引失效错误用法：ALTER TABLE users ADD INDEX idx_extend ((extend_info->'$.is_vip')); extend_info字段是JSON类型的字段，即使创建了索引，索引也会丢失。正解姿势（MySQL 8.0+）：ALTER TABLE users ADD INDEX idx_vip_level (vip_level); ALTER TABLE orders ADD INDEX idx_create_user (create_time, user_id) COMMENT '组合索引覆盖查询';创建组合索引覆盖查询。2.2 血管疏通术卡点分析：原始join顺序是：orders → users → products优化后的方案：(子查询过滤users) → products → orders调整执行顺序，用小表驱动大表。重写后的SQL：SELECT o.* FROM products p INNERJOIN ( SELECT o.id, o.amount, o.create_time FROM orders o WHERE o.create_time > '2023-01-01' ) o ON p.id = o.product_id INNERJOIN ( SELECTid FROMusers WHERE vip_level > 3 ) u ON o.user_id = u.id WHERE p.category_id IN (5,8) ORDERBY o.amount DESC LIMIT1000,20;术后效果：先扫小表（users过滤后只有100条）消除冗余字段传输减少Join时临时表生成2.3 开颅手术通过执行计划锁定了问题，走错索引了，该怎么处理呢？可以通过FORCE INDEX强制指定索引：SELECT /*+ INDEX(o idx_create_user) */ o.id, o.amount FROM orders o FORCE INDEX (idx_create_user) WHERE o.create_time > '2023-01-01';使用衍生表加速：SELECT * FROM ( SELECTid, amount FROM orders WHERE create_time > '2023-01-01' ORDERBY amount DESC LIMIT1020 ) tmp ORDERBY amount DESC LIMIT1000,20;医嘱：警惕OR导致的索引失效用覆盖索引避免回表查询CTE表达式谨慎使用2.4 生命体征监测查看索引使用：SHOW INDEX FROM orders;监控索引使用率：SELECT object_schema, object_name, index_name, count_read, count_fetch FROM performance_schema.table_io_waits_summary_by_index_usage WHERE index_name IS NOT NULL;3 术后护理：体系化治理3.1 SQL消毒中心需要制定优秀的代码规范，否则可能会出现全表扫描的问题。在日常工作中，我们要尽可能减少Java代码感染源。MyBatis危险写法：@Select("SELECT * FROM orders WHERE #{condition}") List<Order> findByCondition(@Param("condition") String condition);condition参数可以传入任何内容，如何传入了1=1，可能会导致查询所有的数据，走全表扫描，让查询效率变得非常低。正确做法（参数化查询）：@Select("SELECT * FROM orders WHERE create_time > #{time}") List<Order> findByTime(@Param("time") Date time);消毒方案：SQL审核平台接入（如Yearning）MyBatis拦截器拦截全表更新自动化EXPLAIN分析流水线3.2 查杀大表癌症如果遇到大表的癌症病例，可以用分库分表的方案解决。病历案例：3亿订单表终极解决方案// Sharding-JDBC分片配置 spring.shardingsphere.rules.sharding.tables.orders.actual-data-nodes=ds$0..1.orders_$->{2020..2023} spring.shardingsphere.rules.sharding.tables.orders.table-strategy.standard.sharding-column=create_time spring.shardingsphere.rules.sharding.tables.orders.table-strategy.standard.sharding-algorithm-name=time_range化疗方案：时间维度分片（2020~2023年度表）用户ID取模分库冷热分离（OSS归档历史数据）医嘱总结优化三板斧：定位：慢查询日志+执行计划分析切割：化繁为简拆分多步执行重建：符合业务场景的数据结构避坑口诀：索引不是银弹，覆盖才是王道Join水深，能拆就拆Order By+Limit≠分页优化最后送上苏三的传秘方：当你优化SQL到怀疑人生时，不妨试试这三味药：删业务逻辑加缓存换数据库保证药到病除（老板打不打死你我就不管了，哈哈哈）！