【SQL】性能优化总结
- SQL 性能优化建议
- 1. 参数是子查询时,使用 EXISTS 代替 IN
- 2. 尽量避免排序
- 3. 尽量避免使用否定形式
- 4. 查询条件中避免发生隐式类型转换
- 5. 减少使用中间表
- 6. 使用延迟查询优化 limit [offset], [rows]
- 7. 利用 LIMIT 1 取得唯一行
- 8. 使用联合索引时,注意索引列的顺序,一般遵循最左匹配原则
- 9. 尽量使用自增 id 作为主键
- 10. 在无 WHERE 条件下要计算表的行数,优先使用 count(*)
- 11. 避免使用 SELECT * ,尽量利用覆盖索引来优化性能
- 12. 使用 force index() 强制走某个索引
- 13. EXPLAIN 来查看 SQL 执行计划
- 14. 批量插入,速度更快
- 15. 慢日志 SQL 定位
- 16. Inner join 、left join、right join
- 17. 删除冗余和重复索引
- 18. 如果数据量较大,优化你的修改/删除语句
- 19. where子句中考虑使用默认值代替null
- 20. 索引不宜太多,一般5个以内
- 21. 尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型
- 22. 索引不适合建在有大量重复数据的字段上,如性别这类型数据库字段
- 23. 如何字段类型是字符串,where时一定用引号括起来,否则索引失效
SQL 性能优化建议
1. 参数是子查询时,使用 EXISTS 代替 IN
查出同时存在于两个表的员工
A表
+------+------+
| id | name |
+------+------+
| 1 | 小明 |
| 2 | 小东 |
| 3 | 小强 |
+------+------+
B表
+------+------+
| id | name |
+------+------+
| 1 | 小明 |
| 2 | 小东 |
| 4 | 小红 |
+------+------+
#sql
-- 慢
SELECT *
FROM Class_A
WHERE id IN (SELECT id
FROM CLASS_B);
-- 快
SELECT *
FROM Class_A A
WHERE EXISTS
(SELECT *
FROM Class_B B
WHERE A.id = B.id);
为啥使用 EXISTS 的 SQL 运行更快呢,有两个原因
- 可以`用到索引,如果连接列 (id) 上建立了索引,那么查询 Class_B 时不用查实际的表,只需查索引就可以了。
- 如果使用 EXISTS,那么只要查到一行数据满足条件就会终止查询, 不用像使用 IN 时一样扫描全表。在这一点上 NOT EXISTS 也一样
另外如果 IN 后面如果跟着的是子查询,由于 SQL 会先执行 IN 后面的子查询,会将子查询的结果保存在一张临时的工作表里(内联视图),然后扫描整个视图,显然扫描整个视图这个工作很多时候是非常耗时的,而用 EXISTS 不会生成临时表。
当然了,如果 IN 的参数是子查询时,也可以用连接来代替,如下:
-- 使用连接代替 IN SELECT A.id, A.name
FROM Class_A A INNER JOIN Class_B B ON A.id = B.id;
用到了 「id」列上的索引,而且由于没有子查询,也不会生成临时表
2. 尽量避免排序
SQL 是声明式语言,即对用户来说,只关心它能做什么,不用关心它怎么做。这样可能会产生潜在的性能问题:排序,会产生排序的代表性运算有下面这些
- GROUP BY 子句
- ORDER BY 子句
- 聚合函数(SUM、COUNT、AVG、MAX、MIN)
- DISTINCT
- 集合运算符(UNION、INTERSECT、EXCEPT)
- 窗口函数(RANK、ROW_NUMBER 等)
如果在内存中排序还好,但如果内存不够导致需要在硬盘上排序上的话,性能就会急剧下降,所以我们需要减少不必要的排序。怎样做可以减少排序呢。
2.1【sql 优化】union 、union all、or使用
- union:对两个结果集进行并集操作,去重,按照默认规则排序
- union all:对两个结果集并集操作,不去重,不排序
- intersect:对两个结果集进行交集操作,去重,按照默认规则排序
- minus:对两个结果集进行差操作,去重,按照默认规则排序
- or:满足两个条件的并集,不去重,不排序
经以上的总结得出以下结论:
1、如果or字段是索引字段,那么使用union all代替or操作,可以走索引
2、如果能用union all ,尽量不要用union,相当于想distinct又执行了order by
3、这里所说的默认排序规则是按照select后边的字段顺序排序的,先按照第一个字段排序,如果第一个字段相同,就按照第二个字段排序
2.2 应尽量避免在where子句中使用or来连接条件
-
使用or可能会使索引失效,从而全表扫描。
对于or+没有索引的age这种情况,假设它走了userId的索引,但是走到age查询条件时,它还得全表扫描,也就是需要三步过程:全表扫描+索引扫描+合并 如果它一开始就走全表扫描,直接一遍扫描就完事。mysql是有优化器的,处于效率与成本考虑,遇到or条件,索引可能失效,看起来也合情合理。
-
如果or字段是索引字段,那么使用union all代替or操作,可以走索引
-- 反例 select * from user where userid=1 or age =18 -- 正例 -- 使用union all select * from user where userid=1 union all select * from user where age = 18 //或者分开两条sql写: select * from user where userid=1 select * from user where age = 18
-
使用集合运算符的 ALL 可选项
SQL 中有 UNION,INTERSECT,EXCEPT 三个集合运算符,默认情况下,这些运算符会为了避免重复数据而进行排序,对比一下使用 UNION 运算符加和不加 ALL 的情况:
-- 显示排序后的结果 select * from Class_A UNION select * from Class_B; +-----------+----------+ | id | name | +-----------+----------+ | 1 | 小明 | | 2 | 小红 | | 3 | 小强 | | 4 | 小东 | +-----------+----------+ -- 不排除重复数据,所以无需排序 select * from Class_A UNION ALL select * from Class_B; +-----------+----------+ | id | name | +-----------+----------+ | 1 | 小明 | | 2 | 小红 | | 3 | 小强 | | 4 | 小东 | | 2 | 小红 | | 4 | 小东 | +-----------+----------+
注意:加 ALL 是优化性能非常有效的手段,不过前提是不在乎结果是否有重复数据。
2.3 慎用distinct关键字
-
使用 EXISTS 代表 DISTINCT
为了排除重复数据, DISTINCT 也会对结果进行排序,如果需要对两张表的连接结果进行去重,可以考虑用 EXISTS 代替 DISTINCT,这样可以避免排序。既用到了索引,又避免了排序对性能的损耗。
items +-----------+----------+ | item_no | item | +-----------+----------+ | 1 | 面包 | | 2 | 香蕉 | | 3 | 苹果 | +-----------+----------+ salesHistory +---------+----------+----------+ | item_no | quantity | sale_time| +---------+----------+----------+ | 1 | 4 | 2023-1-2 | | 2 | 10 | 2023-1-3 | | 3 | 30 | 2023-1-4 | | 1 | 25 | 2023-1-5 | +---------+----------+----------+ # 如何找出有销售记录的商品,使用如下 DISTINCT 可以: SELECT DISTINCT I.item_no FROM Items I INNER JOIN SalesHistory SH ON I. item_no = SH. item_no; # 不过更好的方式是使用 EXISTS: SELECT item_no FROM Items I WHERE EXISTS (SELECT * FROM SalesHistory SH WHERE I.item_no = SH.item_no);
-
distinct 关键字一般用来过滤重复记录,以返回不重复的记录。在查询一个字段或者很少字段的情况下使用时,给查询带来优化效果。但是在字段很多的时候使用,却会大大降低查询效率。
-- 反例 SELECT DISTINCT * from user; -- 正例 select DISTINCT name from user;
理由:带distinct的语句cpu时间和占用时间都高于不带distinct的语句。因为当查询很多字段时,如果使用distinct,数据库引擎就会对数据进行比较,过滤掉重复数据,然而这个比较,过滤的过程会占用系统资源,cpu时间。
2.4 在极值函数中使用索引(MAX/MIN)
使用 MAX/ MIN 都会对进行排序,如果参数字段上没加索引会导致全表扫描,如果建有索引,则只需要扫描索引即可。注意:极值函数参数推荐为索引列中并不是不需要排序,而是优化了排序前的查找速度(毕竟索引本身就是有序排列的)。
2.5 能写在 WHERE 子句里的条件不要写在 HAVING 子句里
-- 聚合后使用 HAVING 子句过滤
SELECT sale_date, SUM(quantity)
FROM SalesHistory GROUP BY sale_date
HAVING sale_date = '2023-1-1';
-- 聚合前使用 WHERE 子句过滤
SELECT sale_date, SUM(quantity)
FROM SalesHistory
WHERE sale_date = '2023-1-1'
GROUP BY sale_date;
使用第二条语句效率更高,原因主要有两点
- 使用 GROUP BY 子句进行聚合时会进行排序,如果事先通过 WHERE 子句能筛选出一部分行,能减轻排序的负担
- 在 WHERE 子句中可以使用索引,而 HAVING 子句是针对聚合后生成的视图进行筛选的,但很多时候聚合后生成的视图并没有保留原表的索引结构
2.6 在 GROUP BY 子句和 ORDER BY 子句中使用索引
GROUP BY 子句和 ORDER BY 子句一般都会进行排序,以对行进行排列和替换,不过如果指定带有索引的列作为这两者的参数列,由于用到了索引,可以实现高速查询,由于索引是有序的,排序本身都会被省略掉
-- 对查询进行优化,应考虑在 where 及 order by 涉及的列上建立索引,尽量避免全表扫描
select * from user where address ='深圳' order by age ;
-- 添加索引
alter table user add index idx_address_age (address,age)
2.7 使用索引时,索引的列不能够是一个表达式
- 所以我们在使用索引时,条件表达式的左侧应该是原始字段
- 尽量避免在索引列上使用mysql的内置函数
-- 假设我们在 col 列上建立了索引,则下面这些 SQL 语句无法用到索引
SELECT *
FROM SomeTable
WHERE col * 1.1 > 100;
-- 尽量避免在索引列上使用mysql的内置函数
SELECT *
FROM SomeTable
WHERE SUBSTR(col, 1, 1) = 'a';
以上第一个 SQL 在索引列上进行了运算, 第二个 SQL 对索引列使用了函数,均无法用到索引,正确方式是把列单独放在左侧,如下:
SELECT *
FROM SomeTable
WHERE col_1 > 100 / 1.1;
3. 尽量避免使用否定形式
如下的几种否定形式不能用到索引:
- <>
- !=
- NOT IN
所以以下 了SQL 语句会导致全表扫描
SELECT *
FROM SomeTable
WHERE col_1 <> 100;
-- 可以改成以下形式
SELECT *
FROM SomeTable
WHERE col_1 > 100 or col_1 < 100;
4. 查询条件中避免发生隐式类型转换
假设 col 是 char 类型,则推荐使用以下第二,三条 SQL 的写法,不推荐第一条 SQL 的写法
× SELECT * FROM SomeTable WHERE col_1 = 10;
○ SELECT * FROM SomeTable WHERE col_1 = '10';
○ SELECT * FROM SomeTable WHERE col_1 = CAST(10, AS CHAR(2));
# 注意:CAST函数用于将某种数据类型的表达式显式转换为另一种数据类型
虽然第一条 SQL 会默认把 10 转成 '10',但这种默认类型转换不仅会增加额外的性能开销,还会导致索引不可用,所以建议使用的时候进行类型转换。
5. 减少使用中间表
在 SQL 中,子查询的结果会产生一张新表,不过如果不加限制大量使用中间表的话,会带来两个问题,一是展示数据需要消耗内存资源,二是原始表中的索引不容易用到,所以尽量减少中间表也可以提升性能。
- 例如:灵活使用 HAVING 子句
SELECT *
FROM (SELECT sale_date, MAX(quantity) AS max_qty
FROM SalesHistory
GROUP BY sale_date) TMP
WHERE max_qty >= 10;
-- 虽然上面这样的写法能达到目的,但会生成 TMP 这张临时表,所以应该使用下面这样的写法:
SELECT sale_date, MAX(quantity)
FROM SalesHistory
GROUP BY sale_date
HAVING MAX(quantity) >= 10;
HAVING 子句和聚合操作是同时执行的,所以比起生成中间表后再执行 HAVING 子句,效率会更高,代码也更简洁
- 例如:需要对多个字段使用 IN 谓词时,将它们汇总到一处
-- 一个表的多个字段可能都使用了 IN 谓词,如下:
SELECT id, state, city
FROM Addresses1 A1
WHERE state IN (SELECT state
FROM Addresses2 A2
WHERE A1.id = A2.id)
AND city IN (SELECT city
FROM Addresses2 A2
WHERE A1.id = A2.id);
-- 这段代码用到了两个子查询,也就产生了两个中间表,可以像下面这样写
-- 这样子查询不用考虑关联性,没有中间表产生,而且只执行一次即可。
SELECT *
FROM Addresses1 A1
WHERE id || state || city
IN (SELECT id || state|| city
FROM Addresses2 A2);
6. 使用延迟查询优化 limit [offset], [rows]
经常出现类似以下的 SQL 语句,当偏移量最大的时候,查询效率就会越低,因为Mysql并非是跳过偏移量直接去取后面的数据,而是先把偏移量+要取的条数,然后再把前面偏移量这一段的数据抛弃掉再返回的。
-- offset 特别大!
SELECT * FROM film LIMIT 100000, 10
方案一:
可以通过延迟查询的方式来优化,假设有以下 SQL,有组合索引(sex, rating)
SELECT <cols> FROM profiles where sex='M' order by rating limit 100000, 10;
-- 则上述写法可以改成如下写法
SELECT <cols>
FROM profiles
inner join
(SELECT id form FROM profiles where x.sex='M' order by rating limit 100000, 10)as x
using(id);
-- 注意:using关键字的概念:连接查询时如果是同名字段作为连接条件,using可以代替on出现(比on更好)
可以看到,在上述语句的子查询中,“SELECT id FROM student LIMIT 100000,10”只查询了主键id一个字段,对于这样的索引覆盖情况,查询速度还是可以接受的。就等于说,该子查询只返回了10个id给上级查询。
上级查询接收到这10个id之后,迅速的查询出了这10个id的集合
方案二:
可以使用 id > 10000 的方式诱导MySQL使用主键索引。
这样的Where语句MySQL是非常喜欢并且乐于执行的,因为你给到MySQL的是一个范围,它最容易执行的就是有序和范围的查询,这对它来说易如反掌。
当然,使用这个方法需要注意,id最好是连续的,中间的记录没有被物理删除过。如果其中有数据被物理删除过,用在分页场景的话,就看起来不是那么的优雅。当然,大部分数据库的参与者都倾向于逻辑删除。即便是物理删除的话,也可以使用程序去做区分显示,这不是
-- 返回上次查询的最大记录(偏移量)
select id,name from employee where id>10000 limit 10
-- 还可以在业务允许的情况下限制页数:
7. 利用 LIMIT 1 取得唯一行
数据库引擎只要发现满足条件的一行数据则立即停止扫描,这种情况适用于只需查找一条满足条件的数据的情况
-- 假设现在有employee员工表,要找出一个名字叫jay的人.
-- 反例
select id,name from employee where name='jay'
-- 正例
select id,name from employee where name='jay' limit 1;
理由:
- 加上limit 1后,只要找到了对应的一条记录,就不会继续向下扫描了,效率将会大大提高。
- 当然,如果name是唯一索引的话,是不必要加上limit 1了,因为limit的存在主要就是为了防止全表扫描,从而提高性能,如果一个语句本身可以预知不用全表扫描,有没有limit ,性能的差别并不大。
8. 使用联合索引时,注意索引列的顺序,一般遵循最左匹配原则
-
当我们创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。
-
假设存在这样顺序的一个联合索引“col_1, col_2, col_3”。这时,指定条件的顺序就很重要。前面两条会命中索引,第三条由于没有先匹配 col_1,导致无法命中索引, 另外如果无法保证查询条件里列的顺序与索引一致,可以考虑将联合索引 拆分为多个索引。
○ SELECT * FROM SomeTable WHERE col_1 = 10 AND col_2 = 100 AND col_3 = 500; ○ SELECT * FROM SomeTable WHERE col_1 = 10 AND col_2 = 100 ; × SELECT * FROM SomeTable WHERE col_2 = 100 AND col_3 = 500 ;
-
使用 LIKE 谓词时,只有前方一致的匹配才能用到索引(最左匹配原则)
× SELECT * FROM SomeTable WHERE col_1 LIKE '%a'; × SELECT * FROM SomeTable WHERE col_1 LIKE '%a%'; ○ SELECT * FROM SomeTable WHERE col_1 LIKE 'a%';
-
简单字符串表达式,模型字符串可以使用 _ 时, 尽可能避免使用 %, 假设某一列上为 char(5)
-- 不推荐 SELECT first_name, last_name, homeroom_nbr FROM Students WHERE homeroom_nbr LIKE 'A-1%'; -- 推荐 SELECT first_name, last_name homeroom_nbr FROM Students WHERE homeroom_nbr LIKE 'A-1__'; --模式字符串中包含了两个下划线
9. 尽量使用自增 id 作为主键
比如现在有一个用户表,有人说身份证是唯一的,也可以用作主键,理论上确实可以,不过用身份证作主键的话,一是占用空间相对于自增主键大了很多,二是很容易引起频繁的页分裂,造成性能问题。
主键选择的几个原则:自增,尽量小,不要对主键进行修改
10. 在无 WHERE 条件下要计算表的行数,优先使用 count(*)
优先使用以下语句来统计行数, innoDB 5.6之后已经对此语句进行了优化
SELECT COUNT(*) FROM SomeTable
按照效率排序的话,count(字段)<count(主键 id)<count(1)≈count(),count() 会选用性能最好的索引来进行排序
11. 避免使用 SELECT * ,尽量利用覆盖索引来优化性能
SELECT * 会提取出一整行的数据,如果查询条件中用的是组合索引进行查找,还会导致回表(先根据组合索引找到叶子节点,再根据叶子节点上的主键回表查询一整行),降低性能,而如果我们所要的数据就在组合索引里,只需读取组合索引列,这样网络带宽将大大减少,假设有组合索引列 (col_1, col_2)
-- 推荐
SELECT col_1, col_2
FROM SomeTable
WHERE col_1 = xxx AND col_2 = xxx
-- 不推荐
SELECT *
FROM SomeTable
WHERE col_1 = xxx AND col_2 = xxx
理由:
- 只取需要的字段,节省资源、减少网络开销。
- select * 进行查询时,很可能就不会使用到覆盖索引了,就会造成回表查询。
12. 使用 force index() 强制走某个索引
例如:现类似以下的慢 SQL 查询
SELECT *
FROM SomeTable
WHERE `status` = 0
AND `gmt_create` > 1490025600
AND `gmt_create` < 1490630400
AND `id` > 0
AND `post_id` IN ('67778', '67811', '67833', '67834', '67839', '67852', '67861', '67868', '67870', '67878', '67909', '67948', '67951', '67963', '67977', '67983', '67985', '67991', '68032', '68038'/*... omitted 480 items ...*/)
order by id asc limit 200;
post_id 也加了索引,理论上走 post_id 索引会很快查询出来,但实现了通过 EXPLAIN 发现走的却是 id 的索引(这里隐含了一个常见考点,在多个索引的情况下, MySQL 会如何选择索引),而 id > 0 这个查询条件没啥用,直接导致了全表扫描, 所以在有多个索引的情况下一定要慎用,可以使用 force index 来强制走某个索引,以这个例子为例,可以强制走 post_id 索引,效果立杆见影。
13. EXPLAIN 来查看 SQL 执行计划
使用 EXPLAIN 来分析 SQL 的执行情况,如怎么发现上文中的最左匹配原则不生效呢,执行 「EXPLAIN + SQL 语句」可以发现 key 为 None ,说明确实没有命中索引
1)、type
依次从好到差:system,const,eq_ref,ref,fulltext,ref_or_null,unique_subquery,index_subquery,range,index_merge,index,ALL。
除了all之外,其他的type都可以使用到索引,除了index_merge之外,其他的type只可以用到一个索引。
14. 批量插入,速度更快
当需要插入数据时,批量插入比逐条插入性能更高
-- 推荐
INSERT INTO TABLE (id, user_id, title) VALUES (1, 2, 'a'),(2,3,'b');
-- 不推荐用
INSERT INTO TABLE (id, user_id, title) VALUES (1, 2, 'a');
INSERT INTO TABLE (id, user_id, tit le) VALUES (2,3,'b');
批量插入 SQL 执行效率高的主要原因是合并后日志量 MySQL 的 binlog 和 innodb 的事务让日志减少了,降低日志刷盘的数据量和频率,从而提高了效率
15. 慢日志 SQL 定位
sql定位主要用到了以下几个参数:
- slow_query_log:这个参数设置为on,可以捕获执行时间超过一定数值的sql语句
- slow_query_log_file:记录日志的文件名
- long_query_time:当sql语句执行时间超过此数值时,就会被记录到日志中,建议设置为1或者更短
16. Inner join 、left join、right join
- Inner join 内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集
- left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。
- right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。
都满足SQL需求的前提下,推荐优先使用Inner join(内连接),如果要使用left join,左边表数据结果尽量小,如果有条件的尽量放到左边处理。
-- 反例
select * from tab1 t1 left join tab2 t2 on t1.size = t2.size where t1.id>2;
-- 正例
select * from (select * from tab1 where id >2) t1 left join tab2 t2 on t1.size = t2.size;
理由:
- 如果inner join是等值连接,或许返回的行数比较少,所以性能相对会好一点。
- 使用了左连接,左边表数据结果尽量小,条件尽量放到左边处理,意味着返回的行数可能比较少。
17. 删除冗余和重复索引
-- 反例:
KEY `idx_userId` (`userId`)
KEY `idx_userId_age` (`userId`,`age`)
-- 正例
-- 删除userId索引,因为组合索引(A,B)相当于创建了(A)和(A,B)索引
KEY `idx_userId_age` (`userId`,`age`)
- 重复的索引需要维护,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能的。
18. 如果数据量较大,优化你的修改/删除语句
避免同时修改或删除过多数据,因为会造成cpu利用率过高,从而影响别人对数据库的访问。
-- 反例
//一次删除10万或者100万+?
delete from user where id <100000;
//或者采用单一循环操作,效率低,时间漫长
for(User user:list){
delete from user;
}
-- 正例
//分批进行删除,如每次500
delete user where id<500
delete product where id>=500 and id<1000;
- 一次性删除太多数据,可能会有lock wait timeout exceed的错误,所以建议分批操作
19. where子句中考虑使用默认值代替null
-- 反例
select * from user where age is not null;
-- 正例
//设置0为默认值
select * from user where age>0;
理由:
- 并不是说使用了is null 或者 is not null 就会不走索引了,这个跟mysql版本以及查询成本都有关。
如果mysql优化器发现,走索引比不走索引成本还要高,肯定会放弃索引,这些条件
!=,>is null,is not null
经常被认为让索引失效,其实是因为一般情况下,查询的成本高,优化器自动放弃的。
- 如果把null值,换成默认值,很多时候让走索引成为可能,同时,表达意思会相对清晰一点。
20. 索引不宜太多,一般5个以内
- 索引并不是越多越好,索引虽然提高了查询的效率,但是也降低了插入和更新的效率。
- insert或update时有可能会重建索引,所以建索引需要慎重考虑,视具体情况来定。
- 一个表的索引数最好不要超过5个,若太多需要考虑一些索引是否没有存在的必要。
21. 尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型
-- 反例:
king_id` varchar(20) NOT NULL COMMENT '守护者Id'
-- 正例:
`king_id` int(11) NOT NULL COMMENT '守护者Id'`
理由:
- 相对于数字型字段,字符型会降低查询和连接的性能,并会增加存储开销
22. 索引不适合建在有大量重复数据的字段上,如性别这类型数据库字段
因为SQL优化器是根据表中数据量来进行查询优化的,如果索引列有大量重复数据,Mysql查询优化器推算发现不走索引的成本更低,很可能就放弃索引了。
23. 如何字段类型是字符串,where时一定用引号括起来,否则索引失效
-- 反例
select * from user where userid =123;
-- 正例
select * from user where userid ='123';
理由:
- 为什么第一条语句未加单引号就不走索引了呢?这是因为不加单引号时,是字符串跟数字的比较,它们类型不匹配,MySQL会做隐式的类型转换,把它们转换为浮点数再做比较。