参考:使你的 SQL 语句完全优化--Oracle
分析问题
许多程序员认为查询优化是DBMS(数据库管理系统)的任务,与程序员所编写的SQL语句关系不大,这是错误的。一个好的查询计划往往可以使程序性能提高数十倍。查询计划是用户所提交的SQL语句的集合,查询规划是经过优化处理之后所产生的语句集合。
DBMS处理查询计划的过程是这样的:
SELECT * FROM orders WHERE (customer_num=104 AND order_num>1001) OR order_num=1008 |
虽然在customer_num和order_num上建有索引,但是在上面的语句中优化器还是使用顺序存取路径扫描整个表。因为这个语句要检索的是分离的行的集合,所以应该改为如下语句:
SELECT * FROM orders WHERE customer_num=104 AND order_num>1001 UNION SELECT * FROM orders WHERE order_num=1008 |
select*from yanxue8_visit limit 10000,10
多次运行,时间保持在0.0187左右
Select*From yanxue8_visit Where vid>=(
Select vid From yanxue8_visit Order By vid limit 10000,1
) limit 10
多次运行,时间保持在0.0061左右,只有前者的1/3。可以预计offset越大,后者越优。
以后要注意改正自己的limit语句,优化一下mysql了
索引误区:
关于建立索引的几个准则:
对于这个语句,大家可能是加一个这样的索引(x,y,uid)。但实际上更好的效果是(uid,x,y)。这是由MySQL处理排序的机制造成的。【数据索引集合】[X,XY,XYUID]
利用最左前缀。在创建一个n 列的索引时,实际是创建了MySQL可利用的n 个索引。多列索引可起几个索引的作用,因为可利用索引中最左边的列集来匹配行。这样的列集称为最左前缀。(这与索引一个列的前缀不同,索引一个列的前缀是 利用该的前n 个字符作为索引值。)
CREATE TABLE test ( id INT NOT NULL, last_name CHAR(30) NOT NULL, first_name CHAR(30) NOT NULL, PRIMARY KEY (id), INDEX name (last_name,first_name));
SELECT * FROM test WHERE last_name='Widenius'; SELECT * FROM test WHERE last_name='Widenius' AND first_name='Michael'; SELECT * FROM test WHERE last_name='Widenius' AND (first_name='Michael' OR first_name='Monty'); SELECT * FROM test WHERE last_name='Widenius' AND first_name >='M' AND first_name < 'N';
SELECT * FROM test WHERE first_name='Michael'; SELECT * FROM test WHERE last_name='Widenius' OR first_name='Michael';
以上例子你在实际项目中应用的时候,不要忘记在添加索引后,用EXPLAIN看看效果。
索引(index)设计
InnoDB表会包含一个聚集索引
一般是按照下面的规则来设定聚集索引的:
1,假如表包含PRIMARY KEY,InnoDB使用它作为聚集索引
2,假如表没有定义PRIMARY KEY,InnoDB将第一个只包含NOT NULL属性列的UNIQUE index作为主键并且将它设置为聚集索引
3,前两者都不满足的时候,mysql就增加一个隐藏的autocreament
HASH (仅仅memory和 ndb引擎支持)
full-text (仅仅myisam支持,并且只支持 char varchar text三种数据类型)
r-tree
2)索引用途:
# 提高数据表检索效率
# 降低数据排序成本(排序主要消耗cpu和内存, 对于分组操作同样是先排序后分组)
2)如何判定 是否建立索引:
# 只有在操作频繁的字段建立索引,绝不建不必要或者想当然的索引,这个在设计表的时候要能大致估计SQL要怎么写。
# 唯一性太差的字段不建立索引,基本上当一条QUERY返回的数据占15%以上就不适合建立索引(通常的像像性别这样的字段绝对不建立索引)
# 更新频繁的字段不建立索引。更新表数据的时候同时要更新索引数据,导致IO访问增大以及影响整个存储系统的消耗。(如果查询更新都较多的情况下,则要比较查询与更新的比例,当比例较大的时候,更新附带的成本是可以接受的)
3) 单键索引和联合索引
# 当where语句的过滤条件比较多的时候,考虑几个字段同时出现的频繁度,对频繁度出现较高的字段集建立联合索引。
# 联合索引的缺点是多个字段同时存在,更新可能性更高,索引存储长度也越大。但就查询角度来讲这个因为它过滤掉更多的数据,所以效率更高。同时比在多个字段都建立单键索引效果好(因为mysql query optimizer需要将多个索引 index_merge 成本更高,有时候还会放弃其他索引)
# 联合索引左前缀原则
使用最左(leftmost)前缀。建立多列复合索引的时候,你实际上建立了MySQL可以使用的多个索引。复合索引可以作为多个索引使用,因为索引中 最左边的列集合都可以用于匹配数据行。这种列集合被称为”最左前缀”(它与索引某个列的前缀不同,那种索引把某个列的前面几个字符作为索引值)。
假设你在表的state、city和zip数据列上建立了复合索引。索引中的数据行按照state/city/zip次序排列,因此它们也会自动地按照 state/city和state次序排列。这意味着,即使你在查询中只指定了state值,或者指定state和city值,MySQL也可以使用这个 索引。因此,这个索引可以被用于搜索如下所示的数据列组合:
state, city, zip
state, city
state
MySQL不能利用这个索引来搜索没有包含在最左前缀的内容。例如,如果你按照city 或zip来搜索,就不会使用到这个索引。如果你搜索给定的 state和具体的ZIP代码(索引的1和3列),该索引也是不能用于这种组合值的,尽管MySQL可以利用索引来查找匹配的state从而缩小搜索的范 围。
# 前缀索引(只使用某个字段前面部分内容作为索引键索引该字段)
4) MYSQL索引限制
# 索引键长度总和不超过1000字节
# BLOB TEXT类型列只创建前缀索引
# 不支持函数索引
# 使用(!= <> )的时候, mysql不支持索引
# 过滤字段使用了函数运算(如abs() ) 等,不支持索引
# join语句中JOIN字段类型不一致,不支持索引
# LIKE语句中以(‘%abc’)开始,不支持索引
# 非等值查询时,不支持HASH索引
5) JOIN语句优化
# 永远用小结果集驱动大结果集(资源消耗存在较大区别外),比如A B联查,
A过滤后 10rows
B过滤后 20rows
A作为驱动表,JOIN过滤则为10次
B作为驱动表,JOIN过滤则为20次
所以选择结果集小的作驱动表
# join字段优化,保证每次查询节省资源
6)ORDER BY、 GROUP BY、 DISTINCT优化
原理都需要进行排序,除对字段索引外,需要去掉不必要的返回字段,节省内存(排序的原则)
5. QUERY语句优化
优化10原则:
1)优化更需要的优化
说明:执行对系统影响更大的QUERY,一般指的是高并发,执行更加频繁的SQL)
2)定位优化对象性能瓶颈
3)明确优化目标
4)explain
5)profule
6)小结果集驱动大结果集
7)尽可能在索引中完成排序
8)只取自己需要的字段
9)仅仅使用最有效的过滤条件
10)尽可能避免复杂的JOIN和子查询
6. 实例分析
Demo1:过度弱化query造成性能消耗
Table1: users表(user_id, user_name, last_feed_time)
Table2: feeds表(feed_id, user_id, feed_data, dateline)
显然users表与feeds表是一对多的关系,现在要查询最近有动作的10个用户,同时在列表页要显示该用户最近24小时的动作。
解决方法1:
// 得到10个最近有动作的用户
$sql = ‘SELECT user_id, user_name
FROM users ORDER BY last_feed_time DESC limit 10’;
// PHP获得10条数据 $rs
// 循环query查询对应ID的最近三个动作
foreach($rs as $k=>$v) {
$sql = “SELECT feed_data
FROM feeds WHERE user_id = ‘{$v[‘user_id’]}’ AND dateline <’’ ORDER BY dateline DESC “;
}
解决方法2:
// 同第一步
// 查询10个用户最近24小时动作
$sql = “SELECT user_id, feed_data
FROM feeds WHERE user_id IN () AND dateline < ‘’ ORDER BY dateline DESC ”:
// 数组组装
总结:我们在开发中经常碰到一些二级栏目的列表页,但数据来自不同的数据表,通常的做法是循环里面执行query,殊不知这样增加了QUERY的次数,而每次QUERY都需要MYSQL进行解析,在这种情况下,宁愿QUERY复杂点或者在程序端复杂点,保证性能。而从另一个理论来讲,这种情况属于弱化了QUERY造成性能问题。
Demo2:过度依赖query造成性能消耗
Table1: users表(user_id, user_name)
Table2: user_profile表(user_id, profile_data)
Table3: users_group表(id, group_id, user_id, level)
现在要查询群组ID为1的群组成员信息以及群主的详细信息。
解决方法1:
// 一次查询所有信息
SELECT u.user_name, up.profile_data
FROM users_group ug LEFT JOIN users u ON ug.user_id = u.user_id LEFT JOIN users_profile up ON u.user_id = up.user_id
WHERE ug.group_id = ‘1’
解决方法2:
// 先查询群组下用户的基本信息
SELECT u.user_id, u.user_name,
FROM users_group ug LEFT JOIN users u ON ug.user_id = u.user_id
WHERE ug.group_id = ‘1’
// 查群主信息
SELECT *
FROM users_profile WHERE user_id = ‘’;
总结:与上例相反,这个DEMO操作是增加一个query减少不必要的访问(只需要群主的详细信息,而profile存储的是数据类型比较大的数据,这样操作减少的是IO的访问)
Demo3:小结果集驱动大结果集
Table1: users表(user_id, user_name, sex)
Table3: users_group表(id, group_id, user_id, level, join_time)
现在要查询某个群组下面(id = 1)用户的名称和性别,按加入时间倒序取100-120条的记录
解决方法1:
SELECT u.user_id, u.username, u.sex
FROM users_group ug LEFT JOIN users u ON ug.user_id = u.user_id
WHERE ug.group_id = ‘1’ ORDER BY ug.join_time DESC LIMIT 100,120;
解决方法2:
SELECT u.user_id, u.username, u.sex
FROM (
SELECT user_id
FROM user_group
WHERE user_group.group_id = 1
ORDER BY join_time DESC
LIMIT 100,20) ug, user
WHERE ug.user_id = user.user_id;
总结:方法1参与join操作的是全部user_group中group_id = 1的数据、而方法2 参与join操作的数据仅仅是过滤过的20条数据。所以SQL优化永远记住小结果集驱动大结果集,节省的是CPU和IO的消耗。