那些Explain参数代表着什么?
官网介绍:Explain官网详情入口
目录
用法
开胃例
Explain参数(重点!!!)
参数1:table:表名
参数2:id:在一个大的查询语句中每个select关键字都对应一个唯一的id
参数3:select_type:SELECT关键字对应查询的类型,确定小查询在整个大查询中扮演了一个什么角色
参数4:partition(略):匹配的分区信息
参数5:type:针对单表的访问方法
参数6:possible_keys和key:可能用到的索引 和 实际上使用的索引
参数7:key_len:实际使用到的索引长度(即:字节数),帮你检查`是否充分的利用上了索引`,`当前key长度值越大越好`(主要针对于联合索引,有一定的参考意义)
参数8:ref:当使用索引列等值查询时,与索引列进行等值匹配的对象信息(比如只是一个常数或者是某个列)
参数9:rows:预估的需要读取的记录条数(值越小越好)
参数10:filtered: 某个表经过搜索条件过滤后剩余记录条数的百分比
参数11:Extra:一些额外的信息,更准确的理解MySQL到底将如何执行给定的查询语句
json格式的explain
用法
如果我们想看看某个查询的执行计划,就可以在具体的查询语句前面加一个EXPLAIN或者DESCRIBE,可用于优化sql语句;
开胃例
1.删除id为2的学生信息,sql语句头部添加关键字EXPLAIN查看该sql语句的执行计划
EXPLAIN DELETE FROM student_info WHERE id = 2;
2.执行结果
参数详解
- partitions -- 匹配的分区信息
- type -- 针对单表的访问方法
- possible_keys -- 可能用到的索引
- key -- 实际用到的索引
- key_len -- 实际使用到的索引长度
- ref -- 当使用索引列等值查询时,与索引列进行等值匹配的对象信息
- rows -- 预估的需要读取的记录条数
- filtered -- 某个表经过搜索条件过滤后剩余记录条数的百分比
- extra -- 额外信息
3.查看id为2的学生信息
SELECT * FROM student_info WHERE id = 2;
4.执行可以获取结果
结论:EXPLAIN不对表产生实际操作
Explain参数(重点!!!)
1.我们先使用以下存储函数创建两个表s1、s2(建表之前记得切换数据库哦)
#创建表
CREATE TABLE s1 (id INT AUTO_INCREMENT,key1 VARCHAR(100),key2 INT,key3 VARCHAR(100),key_part1 VARCHAR(100),key_part2 VARCHAR(100),key_part3 VARCHAR(100),common_field VARCHAR(100),PRIMARY KEY (id),INDEX idx_key1 (key1),UNIQUE INDEX idx_key2 (key2),INDEX idx_key3 (key3),INDEX idx_key_part(key_part1, key_part2, key_part3)
) ENGINE=INNODB CHARSET=utf8;CREATE TABLE s2 (id INT AUTO_INCREMENT,key1 VARCHAR(100),key2 INT,key3 VARCHAR(100),key_part1 VARCHAR(100),key_part2 VARCHAR(100),key_part3 VARCHAR(100),common_field VARCHAR(100),PRIMARY KEY (id),INDEX idx_key1 (key1),UNIQUE INDEX idx_key2 (key2),INDEX idx_key3 (key3),INDEX idx_key_part(key_part1, key_part2, key_part3)
) ENGINE=INNODB CHARSET=utf8;
2.使用函数插入数据(这里的存储函数类似Java的循环结构)
#创建存储函数:
DELIMITER //
CREATE FUNCTION rand_string1(n INT) RETURNS VARCHAR(255) #该函数会返回一个字符串
BEGIN DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';DECLARE return_str VARCHAR(255) DEFAULT '';DECLARE i INT DEFAULT 0;WHILE i < n DOSET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));SET i = i + 1;END WHILE;RETURN return_str;
END //
DELIMITER ;SET GLOBAL log_bin_trust_function_creators=1; #创建存储过程:
DELIMITER //
CREATE PROCEDURE insert_s1 (IN min_num INT (10),IN max_num INT (10))
BEGINDECLARE i INT DEFAULT 0;SET autocommit = 0;REPEATSET i = i + 1;INSERT INTO s1 VALUES((min_num + i),rand_string1(6),(min_num + 30 * i + 5),rand_string1(6),rand_string1(10),rand_string1(5),rand_string1(10),rand_string1(10));UNTIL i = max_numEND REPEAT;COMMIT;
END //
DELIMITER ;DELIMITER //
CREATE PROCEDURE insert_s2 (IN min_num INT (10),IN max_num INT (10))
BEGINDECLARE i INT DEFAULT 0;SET autocommit = 0;REPEATSET i = i + 1;INSERT INTO s2 VALUES((min_num + i),rand_string1(6),(min_num + 30 * i + 5),rand_string1(6),rand_string1(10),rand_string1(5),rand_string1(10),rand_string1(10));UNTIL i = max_numEND REPEAT;COMMIT;
END //
DELIMITER ;#调用存储过程
CALL insert_s1(10001,10000);CALL insert_s2(10001,10000);
3.查看我们插入的数据总量
SELECT COUNT(*) AS s1_count,COUNT(*) AS s2_count FROM s1,s2;
两个表各插入了一万条数据如下显示:
数据创建好了,下面来揭开Explain参数的神秘面纱吧~~
参数1:table:表名
查询的每一行记录都对应着一个单表
例子1:单表查询
EXPLAIN SELECT * FROM s1;
执行结果:table = s1
例子2(s1:驱动表 s2:被驱动表):
EXPLAIN SELECT * FROM s1 INNER JOIN s2;
执行结果:table = s1、s2
参数2:id:在一个大的查询语句中每个select关键字都对应一个唯一的id
- 单表查询
SELECT * FROM s1 WHERE key1 = 'a';
结果:因为只涉及到一张表,因此id为1
- 连表查询
EXPLAIN SELECT * FROM s1 INNER JOIN s2;
结果:这里涉及到两张表,执行顺序相同,因此两条表的id相同均为1
- 子查询
EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2) OR key3 = 'a';
结果:这里同样涉及到了两张表,执行查询的顺序不同,因此结果中id值不同
- 优化子查询(优化器对子查询语句的重写)
EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key2 FROM s2 WHERE common_field = 'a');
结果:这里同样出现了子查询,但是表id相同,原因是优化器将该子查询优化成了连表查询,Explain解析没有问题,相当于以下sql语句,两条sql语句Explain查询结果相同
EXPLAIN SELECT s1.* FROM s1,s2 WHERE s1.`key1`= s2.`key2` AND s2.common_field = 'a';
- Union去重
EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2;
结果:union连表,前两条记录分表代表s1、s2,这里第三条记录结果集取两表的并集,并去重建立一个临时表,临时表id为空,
- union all
EXPLAIN SELECT * FROM s1 UNION ALL SELECT * FROM s2;
结果:union all连表不去重,因此不需要对两表取并集建立临时表,s2查询优先级高于s2
id参数小结:
- id如果相同,可以认为是一组,从上往下顺序执行
- 在所有组中,id值越大,优先级越高,越先执行
- 关注点:id号相同,代表一趟独立的查询,一个sql的查询趟数越少越好
参数3:select_type:SELECT关键字对应查询的类型,确定小查询在整个大查询中扮演了一个什么角色
- simple:simple SELECT(not using UNION or subqueries) -- 即非union、子查询的select_type为“SIMPLE”
EXPLAIN SELECT * FROM s1 INNER JOIN s2;
结果: select_type = SIMPLE
- primary:Outermost SELECT -- 即最后执行的SELECT的select_type为“PRIMARY”
EXPLAIN SELECT * FROM s1 UNION ALL SELECT * FROM s2;
结果:表s1的select_type = PRIMARY
- union:Second or later SELECT statement in a UNION -- 即union连表查询时除了最左侧的SELECT之外所有联表的SELECT的Select_type都是 “UNION”
- union result:Result of a UNION -- 即union去重时取并集时所建立的临时表的Select_type为“UNION RESULT”
EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2;
结果:表s2的 select_type = UNION;表<union1,2>的 select_type = UNION RESULT
- subquery:first SELECT in subquery -- 即子查询的第一个SELECT(前提:不会被优化成连表查询且不是相关子查询)的Select_type为“SUBQUERY”
EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2) OR key3 = 'a';
结果:满足前提s2的select_type = SUBQUERY
- default subquery:first SELECT in subquery,dependent on outer query -- 即子查询的第一个SELECT(前提:不会被优化成连表查询且是相关子查询)的Select_type为“DEPENDENT SUBQUERY”
EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2 WHERE s1.key2 = s2.key2) OR key3 = 'a';
结果:满足前提s2的select_type = DEPENDENT SUBQUERY
- dependent union:Second or later SELECT statement in a UNION,dependent on outer query -- 即在包含`UNION`或者`UNION ALL`的大查询中,如果各个小查询都依赖于外层查询的话,那除了最左边的那个小查询之外,其余的小查询的`select_type`的值就是`DEPENDENT UNION`
EXPLAIN SELECT * FROM s1 WHERE key1 IN
(SELECT key1 FROM s2 WHERE key1 = 'a' UNION SELECT key1 FROM s1 WHERE key1 = 'b');
结果:该sql语句经过优化器优化为相关子查询,因此s2的select_type = DEPENDENT SUBQUERY,而这里id为3的s1的select_type = DEPENDENT UNION
- derived(中文意思:起源;产生):Derived table -- 即派生表的子查询的select_type为“DERIVED”
EXPLAIN SELECT *
FROM (SELECT key1, COUNT(*) AS c FROM s1 GROUP BY key1) AS derived_s1 WHERE c > 1;
结果:s1的select_type = DERIVED
- materialized(中文意思:实现; 发生):Materialized subquery -- 即查询优化器在执行包含子查询的语句时,选择将子查询物化之后与外层查询进行连接查询时子查询对应`select_type`属性就是`MATERIALIZED`
EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2); #子查询被转为了物化表
结果:s2的select_type = MATERIALIZED
参数4:partition(略):匹配的分区信息
参数5:type:针对单表的访问方法
解释:当表中只有一条记录,并且该表使用的存储引擎的统计数据是精确,那么对该表的访问方法就是“system”
举例1:创建一个引擎为myisam的表t,往里插入一条数据
create table t(int i)engine = myisam;
insert into t values(1);Explain select * from t;
结果:type = system,代表查询性能最好
再测试:往表中再插入一条数据时,查询结果为full,代表全表遍历,是查询性能最差的一种方式
举例2:创建一个引擎为innodb的表t,往里插入一条数据
create table t(int i)engine = innodb;
insert into t values(1);
结果:type = full,代表全表遍历,性能较差
小结:type反应了查询方式的好坏,在开发中我们应该尽量避免全表遍历,即出现full的情况,我们希望出现的type顺序依此如下(越靠前性能越高):
system > const > eq_ref (被驱动表通过主键索引或者二级唯一索引匹配方式) > ref (二级索引与常量匹配方式,不包含隐式转换)> fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > all(最糟糕的方式)
在开发中性能调优至少要达到range级别,最好是const级别
参数6:possible_keys和key:可能用到的索引 和 实际上使用的索引
注意:在执行计划中通常会选择成本最低的索引进行查询,但不代表时间最短;另外并不是索引越多越好,挑选索引也是需要消耗成本的;通常执行计划会对sql进行优化,有可能出现possible keys为空,key上存在索引的情况
参数7:key_len:实际使用到的索引长度(即:字节数),帮你检查`是否充分的利用上了索引`,`当前key长度值越大越好`(主要针对于联合索引,有一定的参考意义)
Explain select * from s1 where key_part1 = 'a';Explain select * from s1 where key_part1 = 'a' and key_part2 = 'b';Explain select * from s1 where key_part1 = 'a' and key_part2 = 'b' and key_part3 = 'c';
结果:第一条语句的key_len为303(300字节+null占1字节+2个变长字节)
第二条语句的key_len为606,第三条语句key_len为909,说明联合索引都派上用场了,使用到的key越多那么定位越精准,查询页越少,效率越高
参数8:ref:当使用索引列等值查询时,与索引列进行等值匹配的对象信息(比如只是一个常数或者是某个列)
EXPLAIN SELECT * FROM s1 WHERE key1 = 'a';
结果:ref = const
参数9:rows:预估的需要读取的记录条数(值越小越好)
EXPLAIN SELECT * FROM s1 WHERE key1 > 'z';
结果:rows = 372 通常预估行数越小查询效率越高,数据页越少,若需要去磁盘进行io拉取数据,那么相对io次数也会减少,但若是在缓存中则影响不太明显
参数10:filtered: 某个表经过搜索条件过滤后剩余记录条数的百分比
Explain select * from s1 inner join s2 on s1.key1 = s2.key1 where s1.common_field = 'a';
注意:通常百分比越高查询效率越高;对于单表查询来说filtered的意义不大,但在连接查询中驱动表对应的执行计划记录的filtered的值决定了被驱动表要执行的次数(即rowa*filtered)
参数11:Extra:一些额外的信息,更准确的理解MySQL到底将如何执行给定的查询语句
常见场景:
当查询语句的没有`FROM`子句时将会提示该额外信息
EXPLAIN SELECT 1;
结果:extra = No tables used
查询语句的`WHERE`子句永远为`FALSE`时将会提示该额外信息
EXPLAIN SELECT * FROM s1 WHERE 1 != 1;
结果:extra = Impossible WHERE
当我们使用全表扫描来执行对某个表的查询,并且该语句的`WHERE`子句中有针对该表的搜索条件时,在`Extra`列中会提示上述额外信息。
EXPLAIN SELECT * FROM s1 WHERE common_field = 'a';
结果:extra = Using where
当使用索引访问来执行对某个表的查询,并且该语句的`WHERE`子句中有除了该索引包含的列之外的其他搜索条件时,在`Extra`列中也会提示上述额外信息。
EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' AND common_field = 'a';
结果:extra = Using where
当查询列表处有`MIN`或者`MAX`聚合函数,但是并没有符合`WHERE`子句中的搜索条件的记录时,将会提示该额外信息
EXPLAIN SELECT MIN(key1) FROM s1 WHERE key1 = 'abcdefg';
结果:extra = No matching min/max row
EXPLAIN SELECT MIN(key1) FROM s1 WHERE key1 = 'PMrztw'; #PMrztw 是 s1表中key1字段真实存在的数据
结果:extra = Select tables optimized away 只查询出一条数据,相当于对聚合函数进行了优化(只需返回当前单条数据即可,聚合函数没有做任何操作)
当我们的查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以使用覆盖索引(相当于二级索引的叶子节点数据,如key1、id)的情况下,在`Extra`列将会提示该额外信息。比方说下边这个查询中只需要用到`idx_key1`而不需要回表操作
EXPLAIN SELECT key1,id FROM s1 WHERE key1 = 'a';
结果:extra = using index 代表查询列为覆盖索引,无需回表查询,因此查询效率高
有些搜索条件中虽然出现了索引列,但却不能使用到索引(此时需要进行索引下推优化)
EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND key1 LIKE '%a';
结果:extra = Using index condition 代表进行了索引条件下推(在有like查询条件下减少回表次数)的优化,即先进行所有索引条件的过滤,再根据条件结果进行回表
在连接查询执行过程中,当被驱动表不能有效的利用索引加快访问速度,MySQL一般会为其分配一块名叫`join buffer`的内存块来加快查询速度,也就是我们所讲的`基于块的嵌套循环算法`
EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.common_field = s2.common_field;
结果:s2(被驱动表): extra = Using where; Using join buffer (hash join) 没有用到索引,提供缓冲块提高查询速度
当我们使用左(外)连接时,如果`WHERE`子句中包含要求被驱动表的某个列等于`NULL`值的搜索条件,而且那个列又是不允许存储`NULL`值的,那么在该表的执行计划的Extra列就会提示`Not exists`额外信息
EXPLAIN SELECT * FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1 WHERE s2.id IS NULL;
结果:s2(被驱动表):extra = Using where; Not exists
如果执行计划的`Extra`列出现了`Using intersect(...)`提示,说明准备使用`Intersect`索引合并的方式执行查询,括号中的`...`表示需要进行索引合并的索引名称;如果出现了`Using union(...)`提示,说明准备使用`Union`索引合并的方式执行查询;出现了`Using sort_union(...)`提示,说明准备使用`Sort-Union`索引合并的方式执行查询。
EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key3 = 'a';
结果:extra = Using union(idx_key1,idx_key3); Using where
当我们的`LIMIT`子句的参数为`0`时,表示压根儿不打算从表中读出任何记录,将会提示该额外信息
EXPLAIN SELECT * FROM s1 LIMIT 0;
结果:extra = Zero limit
很多情况下排序操作无法使用到索引,只能在内存中(记录较少的时候)或者磁盘中(记录较多的时候)进行排序,MySQL把这种在内存中或者磁盘上进行排序的方式统称为文件排序(英文名:`filesort`)。
如果某个查询需要使用文件排序的方式执行查询,就会在执行计划的`Extra`列中显示`Using filesort`提示EXPLAIN SELECT * FROM s1 ORDER BY common_field LIMIT 10;
结果:extra = Using filesort
在许多查询的执行过程中,MySQL可能会借助临时表来完成一些功能,比如去重、排序之类的,比如我们在执行许多包含`DISTINCT`、`GROUP BY`、`UNION`等子句的查询过程中,如果不能有效利用索引来完成查询,MySQL很有可能寻求通过建立内部的临时表来执行查询。如果查询中使用到了内部的临时表,在执行计划的`Extra`列将会显示`Using temporary`提示
EXPLAIN SELECT DISTINCT common_field FROM s1;
结果:extra = Using temporary
EXPLAIN SELECT common_field, COUNT(*) AS amount FROM s1 GROUP BY common_field;
结果:extra = Using temporary
小结:执行计划中出现`Using temporary`并不是一个好的征兆,因为建立与维护临时表要付出很大成本的,所以我们`最好能使用索引来替代掉使用临时表`。比如:扫描指定的索引idx_key1即可
EXPLAIN SELECT key1, COUNT(*) AS amount FROM s1 GROUP BY key1;
结果:extra = Using index
Explain小结:
- Explain不考虑各种cache
- Explain不能显示MySQL在执行查询时所作的优化工作
- Explain不会告诉你关于触发器、存储过程信息或用户定义函数对查询的影响情况
- 部分统计信息是估算而非精确值
json格式的explain
EXPLAIN FORMAT=JSON SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key2
WHERE s1.common_field = 'a';
{"query_block": {"select_id": 1,"cost_info": {"query_cost": "2165.52"},"nested_loop": [{"table": {"table_name": "s1","access_type": "ALL","possible_keys": ["idx_key1"],"rows_examined_per_scan": 9895,"rows_produced_per_join": 989,"filtered": "10.00","cost_info": {"read_cost": "978.12","eval_cost": "98.95","prefix_cost": "1077.07","data_read_per_join": "1M"},"used_columns": ["id","key1","key2","key3","key_part1","key_part2","key_part3","common_field"],"attached_condition": "((`atguigudb1`.`s1`.`common_field` = 'a') and (`atguigudb1`.`s1`.`key1` is not null))"}},{"table": {"table_name": "s2","access_type": "eq_ref","possible_keys": ["idx_key2"],"key": "idx_key2","used_key_parts": ["key2"],"key_length": "5","ref": ["atguigudb1.s1.key1"],"rows_examined_per_scan": 1,"rows_produced_per_join": 989,"filtered": "100.00","index_condition": "(cast(`atguigudb1`.`s1`.`key1` as double) = cast(`atguigudb1`.`s2`.`key2` as double))","cost_info": {"read_cost": "989.50","eval_cost": "98.95","prefix_cost": "2165.52","data_read_per_join": "1M"},"used_columns": ["id","key1","key2","key3","key_part1","key_part2","key_part3","common_field"]}}]}
}
那些Explain参数代表着什么?相关推荐
- MySQL-索引优化篇(1)_安装演示库 [前缀索引、联合索引、覆盖索引] explain参数
文章目录 生猛干货 官方文档 安装演示数据库sakila 索引优化策略 索引列上不能使用表达式或者函数 前缀索引和索引列的选择性 前缀索引的创建 索引列的选择性 前缀索引的优缺点 联合索引 如何选择索 ...
- MySQL执行计划 EXPLAIN参数
MySQL执行计划参数详解 转http://www.jianshu.com/p/7134286b3a09 MySQL数据库中,在SELECT查询语句前边加上"EXPLAIN"或者& ...
- 晶振的各种参数代表什么意思?
随着科技发展,晶振作为一种频率元器件被广泛应用于工业,科技,车载,数码,电子等各种领域,因为作用大.应用范围广,所以晶振素有电路心脏的称谓.常见的晶振有贴片.直插.车规级.石英.陶瓷晶振.硅晶振等等, ...
- mysql explain参数_MySQL命令 Explain参数说明
MySQL EXPLAIN命令是查询性能优化不可缺少的一部分,该文主要讲解explain命令的使用及相关参数说明. EXPLAIN Output Columns 列名说明id执行编号,标识select ...
- php cgminer,CGMINER中各个参数代表的意义(挖矿黑框参数)
CGMINER中各个代表的意义(avg,A,R,HW,WU,ST,SS,NB,LW,GF,RF-) 我们在用CGMINER挖矿时出现的黑色界面中里面有很多参数,可能我们弄不明白他们究竟代表些什么意思, ...
- Android BitmapFactory.decodeResource()方法参数代表什么意思
一.方法介绍 1.在Android开发中加载图片的时候会碰到,如果在Activity中用BitmapFactory.decodeResource( ,)第一个参数一般写成 getResources() ...
- top命令各个参数代表意义详解
top命令是Linux下常用的系统性能分析工具,能实时查看系统中各个进程资源占用情况 第一行: 当前时间.系统启动时间.当前系统登录用户数目.平均负载(1分钟,10分钟,15分钟). 平均负载(loa ...
- oracle修改rman参数,Oracle数据库中RMAN默认配置参数代表什么意思
RMAN> show all; db_unique_name 为 ORCL11G64B 的数据库的 RMAN 配置参数为: CONFIGURE RETENTION POLICY TO REDUN ...
- linux解压命令中 zxvf中四种参数代表的是什么含义
分别是四个参数 x : 从 tar 包中把文件提取出来 z : 表示 tar 包是被 gzip 压缩过的,所以解压时需要用 gunzip 解压 v : 显示详细信息 f xxx.tar.gz : 指 ...
- basicLSTMCELL() num_units参数代表了LSTM输出向量的维数
https://blog.csdn.net/notHeadache/article/details/81164264 如下图片解释的清楚,来自stackoverflow 通俗易懂例子 https:// ...
最新文章
- 下一代图片压缩格式 AVIF怎么样?
- 亮剑:PHP,我的未来不是梦(13)
- IT精英们!一路走好!
- 如何在查询分析器中执行dos命令
- 组态王中时间存access怎么存,组态王通过Access数据库起始截止日期查询方法
- RocketMQ核心架构和概
- Eclipse用法和技巧
- (二)以太网与WiFi协议
- 内网服务器通过CCproxy代理上网
- 几种spootboot配置参数线上修改方法
- eviews做回归分析时输出值的理解
- spleeter分离伴奏和人声
- 高数考研归纳 - 微分学 - 一元微分学
- 了不起的盖茨比读后感---Java程序员学Python学习笔记(二)
- 自己总结的Unity3d RPG网络游戏 UI逻辑 框架(基于NGUI)
- 范冰冰李晨爱巢疑曝光 高科技智能停车场甩狗仔
- ASP.NET企业项目管理系统(适用于PM及PMO等)
- Shell函数的定义及用法
- 移动应用开发测试工具Bugtags集成和使用教程【转载】
- 梯度下降法的推导(非常详细、易懂的推导)