SQL优化之博客案例
问题背景:
博客首页随着数据量的增加,最初是几百上千的数据,访问正常,这是开发环境,当切换测试环境(通过爬虫已有数据六万多),这时候访问非常缓慢,长达一分钟。
问题SQL:
SELECT
DISTINCT post.`ID` AS postId,post.`post_title`,post.`post_content`,post.`post_excerpt`,u.`display_name`,
(SELECT IFNULL(GROUP_CONCAT(term.name),'') FROM wp_term_relationships AS r LEFT JOIN wp_term_taxonomy AS t ON(r.term_taxonomy_id = t.term_taxonomy_id) LEFT JOIN wp_terms AS term ON(term.term_id = t.term_id)
WHERE r.object_id = post.ID AND t.taxonomy = 'category') AS categoryName,
(SELECT IFNULL(GROUP_CONCAT(term.name),'') FROM wp_term_relationships AS r LEFT JOIN wp_term_taxonomy AS t ON(r.term_taxonomy_id = t.term_taxonomy_id) LEFT JOIN wp_terms AS term ON(term.term_id = t.term_id)
WHERE r.object_id = post.ID AND t.taxonomy = 'post_tag') AS tagName,
post.`comment_count`,post.`post_status`,post.`post_date`
FROM wp_posts AS post
LEFT JOIN wp_users AS u ON(post.`post_author` = u.`ID`)
LEFT JOIN wp_term_relationships AS relation ON(relation.`object_id` = post.`ID`)
LEFT JOIN wp_term_taxonomy AS taxonomy ON(taxonomy.`term_taxonomy_id` =relation.`term_taxonomy_id`)
LEFT JOIN wp_terms AS term ON(term.`term_id` = taxonomy.`term_id`)
WHERE post.`post_type` = 'post'
AND post.`post_status` IN ('publish')
ORDER BY post.`post_date` DESC
LIMIT ,
将这段sql放在sqlyog里执行,结果花费时间如下:
执行:59.204sec 总数:59.239 10行(仅仅显示10条数据)
优化后的SQL:
SELECT
DISTINCT post.`ID` AS postId,post.`post_title`,post.`post_content`,post.`post_excerpt`,u.`display_name`,
(SELECT IFNULL(GROUP_CONCAT(term.name),'') FROM wp_term_relationships AS r LEFT JOIN wp_term_taxonomy AS t ON(r.term_taxonomy_id = t.term_taxonomy_id) LEFT JOIN wp_terms AS term ON(term.term_id = t.term_id)
WHERE r.object_id = post.ID AND t.taxonomy = 'category') AS categoryName,
(SELECT IFNULL(GROUP_CONCAT(term.name),'') FROM wp_term_relationships AS r LEFT JOIN wp_term_taxonomy AS t ON(r.term_taxonomy_id = t.term_taxonomy_id) LEFT JOIN wp_terms AS term ON(term.term_id = t.term_id)
WHERE r.object_id = post.ID AND t.taxonomy = 'post_tag') AS tagName,
post.`comment_count`,post.`post_status`,post.`post_date`
FROM ( SELECT * FROM wp_posts WHERE `post_type` = 'post' AND `post_status` IN ('publish') ORDER BY `post_date` DESC LIMIT , ) AS post
LEFT JOIN wp_users AS u ON(post.`post_author` = u.`ID`)
LEFT JOIN wp_term_relationships AS relation ON(relation.`object_id` = post.`ID`)
LEFT JOIN wp_term_taxonomy AS taxonomy ON(taxonomy.`term_taxonomy_id` =relation.`term_taxonomy_id`)
LEFT JOIN wp_terms AS term ON(term.`term_id` = taxonomy.`term_id`)
将这段sql放在sqlyog里执行,结果花费时间如下:
执行:0.056sec 总数:0.288sec 10行(仅仅显示10条数据)
优化过后,直接是毫秒级。结果项目在测试环境下访问不卡了。
主要的改动把查询和过滤条件从最后面嵌入到主表的子查询里。
那么问题SQL为什么会这么慢?而优化过后的SQL为什么会突然一下如此迅速到毫秒级呢?
先看问题一,为什么问题SQL会这么慢,问题SQL:
SELECT
DISTINCT post.`ID` AS postId,post.`post_title`,post.`post_content`,post.`post_excerpt`,u.`display_name`,
(SELECT IFNULL(GROUP_CONCAT(term.name),'') FROM wp_term_relationships AS r LEFT JOIN wp_term_taxonomy AS t ON(r.term_taxonomy_id = t.term_taxonomy_id) LEFT JOIN wp_terms AS term ON(term.term_id = t.term_id)
WHERE r.object_id = post.ID AND t.taxonomy = 'category') AS categoryName,
(SELECT IFNULL(GROUP_CONCAT(term.name),'') FROM wp_term_relationships AS r LEFT JOIN wp_term_taxonomy AS t ON(r.term_taxonomy_id = t.term_taxonomy_id) LEFT JOIN wp_terms AS term ON(term.term_id = t.term_id)
WHERE r.object_id = post.ID AND t.taxonomy = 'post_tag') AS tagName,
post.`comment_count`,post.`post_status`,post.`post_date`
FROM wp_posts AS post
LEFT JOIN wp_users AS u ON(post.`post_author` = u.`ID`)
LEFT JOIN wp_term_relationships AS relation ON(relation.`object_id` = post.`ID`)
LEFT JOIN wp_term_taxonomy AS taxonomy ON(taxonomy.`term_taxonomy_id` =relation.`term_taxonomy_id`)
LEFT JOIN wp_terms AS term ON(term.`term_id` = taxonomy.`term_id`)
WHERE post.`post_type` = 'post'
AND post.`post_status` IN ('publish')
ORDER BY post.`post_date` DESC
LIMIT ,
程序自上而下,从左到右执行,先SELECT 再LEFT JOIN 多个表,最后再WHERE 以及 ORDER BY 和 LIMIT,咋看一下也没有问题啊,但实际上很有问题。
问题分析???
- 首先,我并没有使用SELECT * 而是列举我需要的字段。
- 使用explain 关键字看问题SQL,结果如下:

以type字段为主要的来说,表扫描方式:system>const>eq_ref>ref>range>index>all,最慢的是all,也就是全表扫描。
再看优化后的SQL:
为了更好的比较它们究竟有何区别,需要理解explain获取参数的含义。
explain关键字含义
(1)id
MySQL QueryOptimizer选定的执行计划中查询的序列号,表达查询中执行select子句或操作表顺序。id值越大优先级越高,优先级越高就会先被执行。id相同,执行顺序由上至下。
(2)select_type
- SIMPLE(简单的select查询(不使用union及子查询))
- PRIMARY(最外层的select查询,如果两表存在则查询,则外层的表操作为PRIMARY,内层(子查询)的操作为SUBQUERY)
- SUBQUERY(子查询中首个SELECT(如果有多个子查询存在),不依赖于外层的表。除from子句中包含的子查询外,其他地方出现的子查询都可能是SUBQUERY)/DEPENDENT SUBQUERY(子查询中首个SELECT(如果有多个子查询存在),就依赖于外层的表)
(3)table
输出行所引用的表。显示的查询表名,如果查询使用了别名,那么这里显示的是别名,如果不涉及对数据表的操作,那么这显示为null,如果显示为尖括号括起来的就表示这个是临时表,后边的N就是执行计划的id,表示结果来自这个查询产生。如果是尖括号括起来的,与类似,也是一个临时表,表示这个结果来自于union查询的id为M,N的结果集。
(4)type
从优到差的顺序如下:system->const->eq_ref->ref->fulltext->ref_or_null->index_merge->unique_subquery->index_subquery->range->index->all
一般来说,开发人员写的SQL基本要求是eq_ref级别。
(5)possible_keys
指出能再该表中使用哪些索引有助于查询,查询可能使用的索引都会再这里列出来。如果为空,说明没有可用的索引。
(6)key
实际从possible_key选择使用的索引,如果为null,则没有使用索引。select_type为index_merge时,这里可能出现两个以上的索引,其他的select_type这里只会出现一个。很少的情况下,MySQL会选择优化不足的索引。这种情况下,可以再SELECT语句中使用USE INDEX来强制使用一个索引或者用IGNORE INDEX来强制MySQL忽略索引。
(7)key_len
用于处理查询的索引长度,再不损失精确性的情况下,长度越短越好。如果是单列索引,那就整个索引长度算进去,如果是多列索引,那么查询不一定都能使用到所有的列,具体使用了多少个列的索引,这里就会计算进去,没有使用的列,这里不会计算进去。key_len只计算where条件用到的索引长度,而排序和分组就算用到了索引,也不会计算到ken_len中。
(8)ref
显示索引的哪一列被使用。如果使用的常数等值查询,这里会显示const,如果是连接查询,被驱动表的执行计划这里会显示驱动表的关联字段,如果条件使用了表达式或者函数,或者条件列发生内部隐式转换,这里可能会显示func。
(9)rows
认为必须检查的用来返回请求数据的行数,即需要扫描的次数。
(10)extra
这个列可以显示的信息很,如果出现Using filesort、Using temporary两项意味着不能使用索引,效率会受到重大影响。应尽可能对其进行优化。
- distinct:在select部分使用了distinct关键字
- using filesort:排序时无法使用到索引时,就会出现这个。常见于order by 和group by语句中。没有办法利用现有索引进行排序,需要额外排序,建议:根据排序需要,创建相应合适的索引。
- using index:查询时不需要回表查询,直接通过索引就可以获取查询的数据。利用覆盖索引,无需回表即可取得结果数据,这种结果是好的。
- using temporay:表示使用了临时表存储中间结果。
- using where:表示存储引擎返回的记录并不是所有的都满足查询条件,需要在server层进行过滤。
理解完explain后,用explain重要参数来解释这段问题SQL:
即key、type 、rows、extra。以其中的rows来看,该段sql执行之初直接就扫描28386次数。而优化后的SQL仅仅扫描10次,由此可知慢在该地方,针对次进行修改。这是问题SQL慢的根本原因。
但最后我发现优化后的SQL还算很冗余,因为作为首页展示,其实没必要这么多表关联,如果是查看详情的话还可以通过拆分,然后分段执行即可。
最终首页SQL如下(也相当于另外一种解法,优化为单表):
SELECT
DISTINCT post.`ID` AS postId,post.`post_title`,post.`post_content`,post.`post_excerpt`,
(SELECT `display_name` FROM wp_users WHERE ID = post_author) AS display_name,
post.`comment_count`,post.`post_status`,post.`post_date`
FROM wp_posts AS post
WHERE `post_type` = 'post' AND `post_status` IN ('publish')
ORDER BY `post_date` DESC
LIMIT ,
另外除此之外,归档查询也是用的这段SQL,这样一来也需要优化,于是我将其分离写成不同的DAO,针对性优化(如果不优化,数据量过大也会有问题)。
问题SQL(归档,消耗8.183sec):
SELECT
DISTINCT post.`ID` AS postId,post.`post_title`,post.`post_content`,post.`post_excerpt`,u.`display_name`,
post.`comment_count`,post.`post_status`,post.`post_date`
FROM ( SELECT * FROM wp_posts WHERE `post_type` = 'post' AND `post_status` IN ('publish')
AND DATE_FORMAT(`post_date`, '%Y年%m月') = '2020年06月'
ORDER BY `post_date` DESC LIMIT , ) AS post
LEFT JOIN wp_users AS u ON(post.`post_author` = u.`ID`)
问题SQL再度优化(这次执行时间是55.496sec):
SELECT
DISTINCT post.`ID` AS postId,post.`post_title`,post.`post_content`,post.`post_excerpt`,u.`display_name`,
post.`comment_count`,post.`post_status`,post.`post_date`
FROM ( SELECT * FROM wp_posts WHERE `post_type` = 'post' AND `post_status` IN ('publish')
AND DATE_FORMAT(`post_date`, '%Y年%m月') = '2020年06月'
ORDER BY `post_date` DESC LIMIT , ) AS post
LEFT JOIN wp_users AS u ON(post.`post_author` = u.`ID`)
最终优化版:
SELECT
DISTINCT post.`ID` AS postId,post.`post_title`,post.`post_content`,post.`post_excerpt`,
(SELECT `display_name` FROM wp_users WHERE ID = post_author) AS display_name,
post.`comment_count`,post.`post_status`,post.`post_date`
FROM wp_posts AS post WHERE ID IN
(SELECT `ID` FROM wp_posts WHERE `post_type` = 'post' AND `post_status` IN ('publish') AND DATE_FORMAT(`post_date`, '%Y年%m月') = '2020年06月' ORDER BY `post_date` DESC)
LIMIT ,
这个优化版本,我的思路是归档抽取为一个子查询条件查询和排序获取ID,获取ID这段SQL是毫秒级,然后再在外层LIMIT即可。
通用规律和方法
- 通过explain关键字理解SQL走向和慢的原因(explain中的id可以了解sql是如何执行的)
- 学会拆分,一分为二写(以查看文章详情为例,可分为两部分,一部分为获取详情,另外一部分获取文章对应的分类或标签,这样一来sql基本上都可以确保为eq_ref级别且毫秒级)
- 合理使用子查询(例如归档这部分查id,拿获取的id作为where查询条件,背后的原理走主键索引,那么为什么主键索引快,因为主键索引比普通索引快是因为主键索引只检索一次)
以上述方法为例,解决数据量大分页性能问题(解决博客系统点击尾页加载慢问题(本质上还是SQL原因,优化了下,主要利用主键索引)),优化后的代码如下:
SELECT DISTINCT post.`ID` AS postId,post.`post_title`,post.`post_content`,post.`post_excerpt`,
(SELECT `display_name` FROM wp_users WHERE ID = post_author) AS display_name, post.`comment_count`,post.`post_status`,post.`post_date`
FROM wp_posts AS post JOIN (SELECT ID FROM wp_posts WHERE `post_type` = 'post' AND `post_status` IN ('publish') LIMIT ,) AS post_b ON(post.ID = post_b.ID)
WHERE `post_type` = 'post' AND `post_status` IN ('publish') ORDER BY `post_date` DESC
FAQ
为什么SQL查询缓慢?
通常可归纳为如下:
- 没有索引或没有用到索引
- I/O吞吐量小
- 内存不足
- 网络速度慢
- 查询出的数据量过大
- 锁或死锁
- 返回不必要的行和列
以我本次为例,首页之所以慢,是因为最开始那段SQL扫描行数大。等到扫描完后再关联表,再子查询。
而优化过后的扫描行数仅仅就10行,然后再关联再子查询。
两者的区别是前者是全部扫描一遍再关联再条件,后者直接根据条件过滤再关联。
子查询(内嵌查询)的执行过程是什么?
由内向外处理,对应本文举的首页文章优化语句。
为什么SQL查单个字段不分页同样也是六万条数据,消耗时间却是毫秒级?
因为IO的消耗(输入/输出),输出数据量大会导致吞吐量小(吞吐量与磁盘、CPU、内存相关)。
SQL优化之博客案例的更多相关文章
- SQL Server 学习博客分享列表(应用式学习 + 深入理解)
SQL Server 学习博客分享列表(应用式学习 + 深入理解) 转自:https://blog.csdn.net/tianjing0805/article/details/75047574 SQL ...
- Github、Jekyll 搭建及优化静态博客方法指南
尝试自己写 Blog 的人,一般会经历三个阶段. 第一阶段,刚接触 Blog,觉得很新鲜,试着选择一个免费空间来写. 第二阶段,发现免费空间限制太多,就自己购买域名和空间,搭建独立博客. 第三阶段,觉 ...
- 2020BUAA-个人博客-案例分析
个人博客作业-软件案例分析 项目 内容 北航2020软工 班级博客 作业要求 具体要求 我的课程目标 通过案例分析提升自己对于软件工程的认识 课程收获 分析软件,了解软件的定位 第一部分 调研,评测( ...
- sql server 老外博客
Aaron Bertrand Grant Fritchey Brent Ozar Thomas LaRock Pinal Dave Phil Factor SQL Skills w/ Paul Ran ...
- SQL SERVER 技术博客 外文
https://www.sqlskills.com/blogs/paul/capturing-io-latencies-period-time/ http://www.sqlskills.com/bl ...
- nodeJs学习-19 个人博客案例-(1)数据字典
智能社视频27.28 数据字典: 定义: url 300字 admin_table 管理员用户表 ID username varchar(32) password varchar(32) banner ...
- nodeJs学习-17 博客案例
源码:智能社视频20节课件 const express=require('express'); const static=require('express-static'); const cookie ...
- sql优化个人总结(全)
sql优化总结--博客 第一次自己写博客,以后要坚持每掌握一个技能点,就要写一篇博客出来,做一个不满足于一个只会写if...else的程序员. 最近三个月入职了一家新的公司,做的是CRM系统,将公司多 ...
- 基于oracle的sql优化
[基于oracle的sql优化] 基于oracle的sql优化 [博主]高瑞林 [博客地址]http://www.cnblogs.com/grl214 一.编写初衷描述 在应有系统开发初期,由于数据库 ...
随机推荐
- HDU 4352 XHXJ's LIS HDU(数位DP)
HDU 4352 XHXJ's LIS HDU 题目大意 给你L到R区间,和一个数字K,然后让你求L到R区间之内满足最长上升子序列长度为K的数字有多少个 solution 简洁明了的题意总是让人无从下 ...
- 1. 初识Jackson -- 世界上最好的JSON库
要想人前显贵,必须背后受罪.关注公众号[BAT的乌托邦]开启专栏式学习,拒绝浅尝辄止.本文 https://www.yourbatman.cn 已收录,里面一并有Spring技术栈.MyBatis.中 ...
- PE文件格式详解(一)
PE文件格式介绍(一) 0x00 前言 PE文件是portable File Format(可移植文件)的简写,我们比较熟悉的DLL和exe文件都是PE文件.了解PE文件格式有助于加深对操作系统的理解 ...
- c++ 数字与字符串的相互转换
首先推荐用用C++的stringstream. 主要原因是操作简单. 0x00 字符串转数字 // zcj_14.cpp : //该程序是一个注册机,原理是对输入的字符每个与2求异或的结果取低位即为注 ...
- 数据可视化之powerBI入门(二)体验PowerBI:零基础分分钟生成一份交互报表
https://zhuanlan.zhihu.com/p/64144595 体验PowerBI:零基础分分钟生成一份交互报表 首先我们准备一份数据,Excel格式 数据是从2006年到2015年10年 ...
- 机器学习实战基础(十二):sklearn中的数据预处理和特征工程(五) 数据预处理 Preprocessing & Impute 之 处理分类特征:处理连续性特征 二值化与分段
处理连续性特征 二值化与分段 sklearn.preprocessing.Binarizer根据阈值将数据二值化(将特征值设置为0或1),用于处理连续型变量.大于阈值的值映射为1,而小于或等于阈值的值 ...
- 数据可视化之DAX篇(二)Power BI中的度量值和计算列,你搞清楚了吗?
https://zhuanlan.zhihu.com/p/75462046 对于初学者,总是会把度量值和计算列搞混,我也经常碰到这样的问题,有些星友用文章中的代码总是报错,发给我一看,才知道TA把本来 ...
- python并发编程02 /多进程、进程的创建、进程PID、join方法、进程对象属性、守护进程
python并发编程02 /多进程.进程的创建.进程PID.join方法.进程对象属性.守护进程 目录 python并发编程02 /多进程.进程的创建.进程PID.join方法.进程对象属性.守护进程 ...
- Python Ethical Hacking - MAC Address & How to Change(1)
MAC ADDRESS Media Access Control Permanent Physical Unique Assigned by manufacturer WHY CHANGE THE M ...
- 机器学习 | SVD矩阵分解算法,对矩阵做拆分,然后呢?
本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是机器学习专题第28篇文章,我们来聊聊SVD算法. SVD的英文全称是Singular Value Decomposition,翻译过来 ...