背景
本次 SQL 优化是针对 javaweb 中的表格查询做的.
部分网络架构图
业务简单说明
N 个机台将业务数据发送至服务器, 服务器程序将数据入库至 MySQL 数据库. 服务器中的 javaweb 程序将数据展示到网页上供用户查看.
原数据库设计
Windows 单机主从分离
已分表分库, 按年分库, 按天分表
每张表大概 20w 左右的数据
原查询效率
3 天数据查询 70-80s
目标
3-5s
业务缺陷
无法使用 sql 分页, 只能用 java 做分页.
问题排查
前台慢 or 后台慢
如果你配置了 druid, 可在 druid 页面中直接查看 sql 执行时间和 uri 请求时间
在后台代码中用 System.currentTimeMillis 计算时间差.
结论 : 后台慢, 且查询 sql 慢
sql 有什么问题
sql 拼接过长, 达到了 3000 行, 有的甚至到 8000 行, 大多都是 union all 的操作, 且有不必要的嵌套查询和查询了不必要的字段
利用 explain 查看执行计划, where 条件中除时间外只有一个字段用到了索引
备注 : 因优化完了, 之前的 sql 实在找不到了, 这里只能 YY 了.
查询优化
去除不必要的字段
效果没那么明显
去除不必要的嵌套查询
效果没那么明显
分解 sql
将 union all 的操作分解, 例如(一个 union all 的 sql 也很长)
- select aa from bb_2018_10_01 left join ... on .. left join .. on .. where ..
- union all
- select aa from bb_2018_10_02 left join ... on .. left join .. on .. where ..
- union all
- select aa from bb_2018_10_03 left join ... on .. left join .. on .. where ..
- union all
- select aa from bb_2018_10_04 left join ... on .. left join .. on .. where ..
将如上 sql 分解成若干个 sql 去执行, 最终汇总数据, 最后快了 20s 左右.
select aa from bb_2018_10_01 left join ... on .. left join .. on .. where ..
将分解的 sql 异步执行
利用 java 异步编程的操作, 将分解的 sql 异步执行并最终汇总数据. 这里用到了 CountDownLatch 和 ExecutorService, 示例代码如下:
- // 获取时间段所有天数
- List days = MyDateUtils.getDays(requestParams.getStartTime(), requestParams.getEndTime());
- // 天数长度
- int length = days.size();
- // 初始化合并集合, 并指定大小, 防止数组越界
List < 你想要的数据类型> list = Lists.newArrayListWithCapacity(length);
- // 初始化线程池
- ExecutorService pool = Executors.newFixedThreadPool(length);
- // 初始化计数器
- CountDownLatch latch = new CountDownLatch(length);
- // 查询每天的时间并合并
- for (String day : days) {
- Map param = Maps.newHashMap();
- // param 组装查询条件
- pool.submit(new Runnable() {
- @Override
- public void run() {
- try {
- // mybatis 查询 sql
- // 将结果汇总
- list.addAll(查询结果);
- } catch (Exception e) {
- logger.error("getTime 异常", e);
- } finally {
- latch.countDown();
- }
- }
- });
- }
- try {
- // 等待所有查询结束
- latch.await();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // list 为汇总集合
- // 如果有必要, 可以组装下你想要的业务数据, 计算什么的, 如果没有就没了
结果又快了 20-30s
优化 MySQL 配置
以下是我的配置示例. 加了 skip-name-resolve, 快了 4-5s. 其他配置自行断定
- [client]
- port=3306
- [MySQL]
- no-beep
- default-character-set=utf8
- [mysqld]
- server-id=2
- relay-log-index=slave-relay-bin.index
- relay-log=slave-relay-bin
- slave-skip-errors=all #跳过所有错误
- skip-name-resolve
- port=3306
- datadir="D:/mysql-slave/data"
- character-set-server=utf8
- default-storage-engine=INNODB
- sql-mode="STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"
- log-output=FILE
- general-log=0
- general_log_file="WINDOWS-8E8V2OD.log"
- slow-query-log=1
- slow_query_log_file="WINDOWS-8E8V2OD-slow.log"
- long_query_time=10
- # Binary Logging.
- # log-bin
- # Error Logging.
- log-error="WINDOWS-8E8V2OD.err"
- # 整个数据库最大连接 (用户) 数
- max_connections=1000
- # 每个客户端连接最大的错误允许数量
- max_connect_errors=100
- # 表描述符缓存大小, 可减少文件打开 / 关闭次数
- table_open_cache=2000
- # 服务所能处理的请求包的最大大小以及服务所能处理的最大的请求大小(当与大的 BLOB 字段一起工作时相当必要)
- # 每个连接独立的大小. 大小动态增加
- max_allowed_packet=64M
- # 在排序发生时由每个线程分配
- sort_buffer_size=8M
- # 当全联合发生时, 在每个线程中分配
- join_buffer_size=8M
- # cache 中保留多少线程用于重用
- thread_cache_size=128
- # 此允许应用程序给予线程系统一个提示在同一时间给予渴望被运行的线程的数量.
- thread_concurrency=64
- # 查询缓存
- query_cache_size=128M
- # 只有小于此设定值的结果才会被缓冲
- # 此设置用来保护查询缓冲, 防止一个极大的结果集将其他所有的查询结果都覆盖
- query_cache_limit=2M
- # InnoDB 使用一个缓冲池来保存索引和原始数据
- # 这里你设置越大, 你在存取表里面数据时所需要的磁盘 I/O 越少.
- # 在一个独立使用的数据库服务器上, 你可以设置这个变量到服务器物理内存大小的 80%
- # 不要设置过大, 否则, 由于物理内存的竞争可能导致操作系统的换页颠簸.
- innodb_buffer_pool_size=1G
- # 用来同步 IO 操作的 IO 线程的数量
- # 此值在 Unix 下被硬编码为 4, 但是在 Windows 磁盘 I/O 可能在一个大数值下表现的更好.
- innodb_read_io_threads=16
- innodb_write_io_threads=16
- # 在 InnoDb 核心内的允许线程数量.
- # 最优值依赖于应用程序, 硬件以及操作系统的调度方式.
- # 过高的值可能导致线程的互斥颠簸.
- innodb_thread_concurrency=9
- # 0 代表日志只大约每秒写入日志文件并且日志文件刷新到磁盘.
- # 1 ,InnoDB 会在每次提交后刷新 (fsync) 事务日志到磁盘上
- # 2 代表日志写入日志文件在每次提交后, 但是日志文件只有大约每秒才会刷新到磁盘上
- innodb_flush_log_at_trx_commit=2
- # 用来缓冲日志数据的缓冲区的大小.
- innodb_log_buffer_size=16M
- # 在日志组中每个日志文件的大小.
- innodb_log_file_size=48M
- # 在日志组中的文件总数.
- innodb_log_files_in_group=3
- # 在被回滚前, 一个 InnoDB 的事务应该等待一个锁被批准多久.
- # InnoDB 在其拥有的锁表中自动检测事务死锁并且回滚事务.
- # 如果你使用 LOCK TABLES 指令, 或者在同样事务中使用除了 InnoDB 以外的其他事务安全的存储引擎
- # 那么一个死锁可能发生而 InnoDB 无法注意到.
- # 这种情况下这个 timeout 值对于解决这种问题就非常有帮助.
- innodb_lock_wait_timeout=30
- # 开启定时
- event_scheduler=ON
被批准多久. # InnoDB 在其拥有的锁表中自动检测事务死锁并且回滚事务. # 如果你使用 LOCK TABLES 指令, 或者在同样事务中使用除了 InnoDB 以外的其他事务安全的存储引擎 # 那么一个死锁可能发生而 InnoDB 无法注意到. # 这种情况下这个 timeout 值对于解决这种问题就非常有帮助. innodb_lock_wait_timeout=30# 开启定时 event_scheduler=ON
根据业务, 再加上筛选条件
快 4-5s
将 where 条件中除时间条件外的字段建立联合索引
效果没那么明显
将 where 条件中索引条件使用 inner join 的方式去关联
针对这条, 我自身觉得很诧异. 原 sql,b 为索引
select aa from bb_2018_10_02 left join ... on .. left join .. on .. where b = 'xxx'
应该之前有 union all,union all 是一个一个的执行, 最后汇总的结果. 修改为
- select aa from bb_2018_10_02 left join ... on .. left join .. on .. inner join
- (
- select 'xxx1' as b2
- union all
- select 'xxx2' as b2
- union all
- select 'xxx3' as b2
- union all
- select 'xxx3' as b2
- ) t on b = t.b2
结果快了 3-4s
性能瓶颈
根据以上操作, 3 天查询效率已经达到了 8s 左右, 再也快不了了. 查看 MySQL 的 CPU 使用率和内存使用率都不高, 到底为什么查这么慢了, 3 天最多才 60w 数据, 关联的也都是一些字典表, 不至于如此. 继续根据网上提供的资料, 一系列骚操作, 基本没用, 没辙.
环境对比
因分析过 sql 优化已经 ok 了, 试想是不是磁盘读写问题. 将优化过的程序, 分别部署于不同的现场环境. 一个有 ssd, 一个没有 ssd. 发现查询效率悬殊. 用软件检测过发现 ssd 读写速度在 700-800M/s, 普通机械硬盘读写在 70-80M/s.
优化结果及结论
优化结果: 达到预期.
优化结论: sql 优化不仅仅是对 sql 本身的优化, 还取决于本身硬件条件, 其他应用的影响, 外加自身代码的优化.
小结
优化的过程是自身的一个历练和考验, 珍惜这种机会, 不做只写业务代码的程序员. 希望以上可以有助于你的思考, 不足之处望指正.
来源: http://database.51cto.com/art/201810/585303.htm