) \% z+ z& a2 u0 b$ H, ~8 l hmysql索引和explain的详解索引原理分析
3 g3 i! {! w% W) O" I1 J- D
" b, B" x, i2 P; ^* _索引存储结构
3 f; m1 q6 U3 B* ]/ t1 G, V3 x索引是在存储引擎中实现的,也就是说不同的存储引擎,会使使用不同的索引1 U' t6 I* e6 ^( z8 {* f4 p$ {' X& W( l
MyISAM和InnoDB存储引擎:只支持B+ TREE索引, 也不能够更换
# _. f4 F$ h& S# HMEMORY/HEAP存储引擎:支持HASH和BTREE索引4 t- e! D, _6 z4 u0 X1 Z& E
! K9 ?3 v$ K3 s j- I) MB树图示! [0 h: }* d( h; L. s
b# o7 T4 i& b) C: ~) p
B树是为了磁盘或其它存储设备设计的一种多叉(下面你会看到,相对于二叉,B树每个内结点有多个分支,即多叉)平衡查找树。 多叉平衡。3 T& k0 d' `; g* o* V& P+ y
4 C0 s% z( G4 P; `
, o. @% e% s k9 Z3 I# x
$ K' [* `0 T1 M0 w4 R8 z
3 s' _. O: ~9 B/ Q0 s0 hB树和B+树的区别:4 Y" Y2 K2 J! E4 o
B树和B+树的最大区别在于非叶子节点是否存储数据的问题
) G( d5 a) p8 }1 y) u
5 \9 w9 ~1 g/ l5 X在结构上:7 Z7 `1 Q3 ]8 s6 t* t- [* f4 @
(1) B树是非也只节点和叶子节点都会存储数据。
2 `5 D1 J8 k0 E% t(2) B+树只有叶子节点才会存储数据,而且数据都是在一行上,而且这些数据都是指针指向的,也是有顺序的。4 O) d( Z1 {/ x- n! O) F! n
2 @7 {9 q7 h( B1 T/ N
在性能上:( A% _7 u# J G$ I3 U
(1)对于B-树相对于B+数据,B-Tree因为非叶子结点也保存具体数据,所以在查找某个关键字的时候找到即可返回。而B+Tree所有的数据都在叶子结点,每次查找都得到叶子结点。所以在同样高度的B-Tree和B+Tree中,B-Tree查找某个关键字的效率更高。B-Tree在单条数据读写有着更强的性能。( b. o8 Y2 g% O% c& P! H
(2)但由于B+Tree所有的数据都在叶子结点,并且结点之间有指针连接,在找大于某个关键字或者小于某个关键字的数据的时候,B+Tree只需要找到该关键字然后沿着链表遍历就可以了,而B-Tree还需要遍历该关键字结点的根结点去搜索。这个也决定当连表查询的时候mysql比起mongo有显著的优势。更重要的是由于B-Tree的每个结点(这里的结点可以理解为一个数据页)都存储主键+实际数据,而B+Tree非叶子结点只存储关键字信息,而每个页的大小有限是有限的,所以同一页能存储的B-Tree的数据会比B+Tree存储的更少。这样同样总量的数据,B-Tree的深度会更大,增大查询时的磁盘I/O次数,进而影响查询效率。
" O5 W( |/ }2 k, [4 D6 C9 [
/ I( g6 a/ `4 z! R聚集索引(MyISAM)
6 d- U( `' z" V( J" T. TB+树叶节点只会存储数据行(数据文件)的指针,简单来说数据和索引不在一起,就是聚集
% h% n2 O( {4 c索引。$ y' ]% z% P7 s0 T: C* _" m: f: E: `
聚集索引包含主键索引和辅助索引都会存储数据指针的值。
Y8 f7 ]' [8 y/ h+ I9 R6 L* ~% ~* o G
* k% Z2 D4 R% l6 `# y
H F" |" n4 d9 P' ^$ P
辅助索引(次要索引)
Q$ n* _- p$ k: q9 W% k/ t; v在 MyISAM 中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,
- o5 c& i$ B2 C" A1 A2 k: V而辅助索引的 key 可以重复。如果我们在 Col2 上建立一个辅助索引,则此索引的结构如下图所示# S0 A$ x9 F0 Z) ~7 c
- K, L2 ~: N& R" K( u8 Y同样也是一颗 B+Tree,叶子节点中保存数据记录的地址。因此,MyISAM 中索引检索的算法为首先按照B+Tree 搜索算法搜索索引,如果指定的 Key 存在,则取出其data 域的值,然后以 data 域的值为地址,读取相应数据记录。
7 |4 p" n! T, q6 b7 V
; W" S2 \- a5 ?# \8 x聚集索引(InnoDB)- m' y0 \8 q7 o
1 o6 o0 H5 s# |, N, h
主键索引(聚集索引)的叶子节点会存储数据行,也就是说数据和索引是在一起,这就是聚集索引。
, N3 C0 P2 [5 o6 B辅助索引只会存储主键值* ~- W" t! U: J8 x! P& p8 f& E) {
如果没有没有主键,则使用唯一索引建立聚集索引;如果没有唯一索引,MySQL会按照一定规则创建聚集索引。/ q, q2 A' u: |" x
) g6 Y8 }% G" Z! g7 B主键索引' T' q: l, W+ g G' F- m: R0 W# \
1.InnoDB 要求表必须有主键(MyISAM 可以没有),如果没有显式指定,则 MySQL系统会自动选择一个可以( n2 \( X6 B# G
唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL 自动为 InnoDB 表生成一个隐含字段作为主键,类型为长整形。1 q8 @8 r" I, i) h- t" b
/ O, i* V8 \. o1 F- ~4 u* w) K
7 _% N* Y" U. Q/ q# C4 P1 } U. d6 W- S
' m) g+ J' f& y: _8 Q; y上图是 InnoDB 主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为 InnoDB 的数据文件本身要按主键聚集。
% e1 G/ _" S3 j. p( y
$ `( J0 l4 h5 }% j; z' v0 M. w
: h5 z3 a: l) H, G2 D! b
, M) p- ~1 f% j4 T8 v3 d M4 t+ ~
5 p: x. p8 x9 t- E, ^, n# ?
& P- r* \# q. U [0 T* y& r+ ]7 \+ J4 b7 n- S( p' r2 B
mysql创建索引的时候和用法与索引息息相关,要建立合适的索引和理解一些索引的执行计划,就需要认识索引的结构。" |. }0 `7 T" F8 b" Y, _) m& s
( e- `% W) d' R# i2 vexplain的详解
' @# `: O0 E/ Y+ E- p! q J: z- h; f/ \; U# k2 V- p
参数说明:
1 g, h O! Y0 D' F, vexplain后会出现十列数据,下面将介绍这下面的十列数据。
) i- x" [0 W+ h* b; B! E
9 J' F; j7 H, B' Hid、select_type、table、type、possible_keys、key、key_len、ref、rows、Extra/ x% X: V- v1 V! U ^) i6 _( s
4 T# D0 b+ O0 b# j, i7 a2 R5 i/ q! J先附上案例表:2 G" r1 i8 \! l; ~" L
/ w# z% `! F2 _: z! ~& w) x
CREATE TABLE `taddr` (
! r Z) i, R4 ?4 w5 n: c `id` int(11) NOT NULL AUTO_INCREMENT,! L, Z7 l- z( S1 X" J
`country` varchar(100) DEFAULT '',9 H' N3 J" k2 d) X- C& ^7 L
`province` varchar(100) DEFAULT '',
9 A7 o9 {1 J) y E6 _ PRIMARY KEY (`id`)+ n9 p; r( E9 Z5 ?& K/ }9 U
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
- ^7 k9 Q' w8 p0 J1 q; [; y$ K
9 `+ r# B7 r: Y8 |7 t. qCREATE TABLE `user` (
+ r/ {/ b/ j2 K1 G `id` int(11) NOT NULL AUTO_INCREMENT,
0 F. t) D) R" ?3 Y- [ V `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
; H9 f, A d; G4 j5 u0 S. z `password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,# i7 n( X, D( b- j- t" s0 c
`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,/ f/ l; `' {9 n9 b
`addr_id` int(11) NULL DEFAULT NULL,# X" a! \" s2 B9 {8 N
PRIMARY KEY (`id`) USING BTREE,
( \8 `7 a% f9 D: D: Z# W u! L6 e INDEX `addr_id`(`addr_id`) USING BTREE; w2 \) c/ F9 [9 L
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
: {. B; h2 t" n5 x9 D' X
- K( h: ]3 R( E1 g
# k5 _2 K: k9 m( H$ CCREATE TABLE `type_time` ( |* l+ Z9 U! O' s' o" L4 n$ |+ y- W# g
`id` int(11) NOT NULL AUTO_INCREMENT,
) P# {0 k9 ~: t* D& l- Z, k' p3 M `time` varchar(255) DEFAULT '[]',
2 Q+ _6 u' m& D3 e) w# t# y `name` varchar(100) DEFAULT '',
" O5 O8 Q$ c1 @# X+ h, u PRIMARY KEY (`id`),
0 y9 \" O- b: s( }' A0 h- o INDEX `name_time_index`(`name`,`time`) USING BTREE' ^2 N( X! @% O( a4 G8 J) K
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf83 N7 m5 A. e# g/ W( b
* @+ P1 y& |" ~6 R$ Y
一、id% Q0 e$ j) L( G; ` v
每个 SELECT语句都会自动分配的一个唯一标识符.
2 P; U% q! R/ Y- x表示查询中操作表的顺序,有三种情况:
) F) S2 }/ t; }! t& Y3 F# Oid相同:执行顺序由上到下
! w; `6 y4 ~5 \5 Zid不同:如果是子查询,id号会自增,id越大,优先级越高。
; \! P9 ~+ p) \$ Did相同的不同的同时存在: P( G' W9 R5 k
id列为null的就表示这是一个结果集,不需要使用它来进行查询。
q& R2 R1 k; j! O" R: P& H J
0 k4 N3 ~* ^% D3 ]7 c; s0 C# `二、select_type' {& j$ ~5 [- g+ ^3 f
6 A6 b- z; o ^) y/ t4 U查询类型,主要用于区别普通查询、联合查询(union、union all)、子查询等复杂查询5 H$ S+ ?4 D9 f2 `
' k# Y; G/ l* p4 h5 c. f2.1、simple
4 U: n+ Z& S! o( ?" X3 {: E表示不需要union操作或者不包含子查询的简单select查询。有连接查询时,外层的查询为simple8 k2 l9 Q' G0 d
# S0 j4 c( ]) {) |1 r( _
EXPLAIN select * from user/ e r: j3 f# N% B
% B( o4 i0 n b8 K& D
" \. ?1 U" y1 Q4 Q
1 r2 D' t/ F6 b9 e- X( d# g% ~EXPLAIN select u.id,u.addr_id,a.* from user u inner join taddr a on u.addr_id=a.id
3 X2 O% K% e& E0 d3 F
6 j; W0 ~' q E+ p9 g
5 }/ x* |2 \3 @7 {( @ f2.2 primary$ Y' E) c% ` t+ M3 w( o
一个需要union操作或者含有子查询的select,位于最外层的单位查询的select_type为primary。3 h( K7 R1 x+ L, a( ^
7 @+ Z; l, I& r
explain select * from taddr t inner join (
4 B; E5 _) O8 \% yselect addr_id from user ) u on t.id=u.addr_id* A7 w `0 t5 L- T$ a9 h
! o/ v4 P4 c( }: wexplain select * from user u where u.addr_id =1q! I. f& }) P/ x) _# @ `
union all1 {9 W4 D1 Z) ~$ i8 w O) J
select * from user u where u.addr_id =2
* P7 w' A! Z" L1 O ?2 O4 [
% f, A/ J" f- p0 c5 a
) u* R2 Z, P8 |' ?2 a2.3 subquery! q( p% b& V, R- m9 v0 c
除了from字句中包含的一查询外,其他地方出现的子查询都可能是subquery& X. s% ?7 ^6 y: O9 U2 l6 b
5 \, R W9 y" \; Q
2.4 dependent subquery
/ w; C p4 E$ V) R% P/ M6 u9 u
与dependent union类似,表示这个subquery的查询要受到外部表查询的影响6 ~/ V" A$ o/ W! A- c
; p4 h! F& A& h4 C
explain select u.name,(select t.province from taddr t where u.addr_id=t.id) from user u
2 R ], O, g: }) i8 B( d, L: i
3 X+ V. I9 o0 E+ a6 w3 r% D2.5 union
; [# R/ b) Y9 r l7 tunion连接的两个select查询,第⼀个查询是PRIMARY,除了第一个表外,第二个以后的表select_type都是union
8 c5 q+ H4 Y+ p v/ o/ t; s4 D' W6 n6 F; V
三、table
' w9 [/ T& }) a0 [. P: Y9 P( ^& F显示的查询表名,如果查询使用了别名,那么这里显示的是别名1 Q2 f2 K$ o1 V% W+ B
如果不涉及对数据表的操作,那么这显示为null! j H6 t" |1 ~) h8 j6 u0 A
如果显示为尖括号括起来的就表示这个是临时表,后边的N就是执行计划中的id,表示结果来自于这个查询产生。- H% `" b8 \: M' e
如果是尖括号括起来的<union M,N>,与类似,也是一个临时表,表示这个结果来自于union查询的id为M,N的结果集。& V1 C) R7 E+ L( Q, k {' b' N
* k; n5 K) S7 N5 N$ l* h' i四、type& ]1 T2 c0 x2 Q% p
) H) b g" l3 v/ Z6 b
依次从好到差:
3 h/ ?1 W" [. e* Esystem,const,eq_ref,ref,fulltext,ref_or_null,unique_subquery,! ~9 |, U9 A$ Q3 n9 a* ^
index_subquery,range,index_merge,index,ALL
% T( r' m3 a! {8 k7 a" z/ _5 S# F7 E3 d g9 i( v3 O
除了all之外,其他的type都可以使⽤到索引,除了index_merge之外,其他的type只可以用到一个索引
1 c A( \5 d# [7 v2 j
- q+ a/ a* i$ l" |% c4、1 system
0 E2 C# @" K9 \+ I p2 p) B! Q表中只有一行数据或者是空表。% l! y$ d: d! [
! X1 }* D( g4 M) x6 u" v9 @4、2const
! K3 _. ]) f* L5 T+ K/ A. R* `使用唯一索引或者主键,返回记录一定是1行记录的等值where条件时,通常type是const。其他数据库也叫做唯一索引扫描。8 L7 l* Z5 d( a/ C1 X3 a
7 ]& h3 k/ V* m8 X
4、3 eq_ref
; W# \5 r- O1 q! N2 n关键字:连接字段主键或者唯一性索引。
) a2 M4 @1 e& V+ B6 x此类型通常出现在多表的 join 查询, 表示对于前表的每一个结果, 都只能匹配到后表的一行结果. 并且查询的比较较操作通常是 ‘=’, 查询效率较高.
. W! a- P( y2 A/ W8 Z" E. {5 }. G$ G! W G- \) b: V# x2 O6 e- o) r
EXPLAIN select u.id,u.addr_id,a.* from user u inner join taddr a on u.addr_id=a.id' V3 }- c& A# o. i; R; w; Y2 i! ~
) ?" c) p4 f* N, _, v3 h; z( ?3 k3 X1 j5 q3 K" ]1 V; @4 }
# M1 n6 c% T4 h- f1 y0 L, g
# _8 ^& P1 r- V
) w! b1 u/ Y9 ^9 n- A# f
4、4 ref
! @) w, U) g, n: K5 N' s( X针对非唯一性索引,使用等值(=)查询非主键。或者是使用了最左前缀规则索引的查询。 EXPLAIN select u.id,u.addr_id,a.* from taddr a left join user u on u.addr_id=a.id
- q2 b+ L0 A+ j$ _* ?: C% y8 v. T9 L& Y. _9 }' |9 a8 N3 z
4.5 fulltext
4 X6 L/ y: X k) e4 O全文索引检索,要注意,全文索引的优先级很高,若全高索引和普通索引同时存在时,mysql不管代价,优先选择使用全文索引
+ U" ~) t/ X( j8 v7 J/ @% B9 w- A- P4 I( w( l \% E/ K
4、6 unique_subquery ?4 d' \2 F6 _$ ]$ _; b
用于where中的in形式子查询,子查询返回不重复值唯一值
) \3 K3 k6 h' H! q+ A1 S x
6 B+ Y. |! y3 J( r0 ]4、7 index_subquery: O2 C2 I3 l- O7 W4 `) `
用于in形式子查询使用到了辅助索引或者in常数列表,子查询可能返回重复值,可以使用索引将子查询去重。3 k3 c7 `- v( c! N$ Y3 x8 u! A9 r
- g0 ?" }- l% H7 t4、8 range
$ M v4 i% J- e9 W" l+ q索引范围扫描,常用于使用>,<,is null,between ,in ,like等运算符的查询中。
, H( Z& q( D7 M1 E V3 H
* n2 H5 k: ?* P0 }" L3 ?explain select * from type_time a inner join (
/ ~- R8 r- g+ {- Qselect id from type_time where name =‘2’ and time in (‘2’,‘3’,‘4’) ) b on a.id=b.id
( _+ j$ l( D& n* u! u' p5 p
& k$ B+ J3 s* F* k9 N: p+ c$ h
, ~$ f. j* z0 z) E* C6 `
5 } ^1 A8 o3 n/ e- B
& j; d0 w0 B4 {* L* a( J& |2 q
4、9 index
) u+ a ^9 C0 L* A: d5 _键字:条件是出现在索引树中的节点的。可能没有完全匹配索引。
! s2 b& H9 p N/ y* f: w9 j索引全表扫描,把索引从头到尾扫一遍,常用于使用索引列就可以处理不需要读取数据文件的查询、可以使使用索引排序或者分组的查询。
6 B4 a) I$ c A8 j0 K9 Y' c7 T
" _5 o/ L* X9 K, Fexplain select * from user group by addr_id; l* f4 f" ]0 t
: {( v5 G% G& V9 Y
' t' [; ?* p8 m! `* M) e; t: p w }- K1 O- p9 ?' e
. y+ A7 Y: Y7 _8 X' e
" u6 X5 x/ x: k, l) I/ C& @explain select addr_id from user
, z# q i) P0 A% w
+ s! [% j) C ~2 b1 C
- d& Y2 ?6 X7 A$ y# v
5 ~' J5 X0 U- k! H! _/ H* M, k' H' x4 n' L
4、10 all
6 _6 i* J# x' R; o$ r这个就是全表扫描数据文件,然后再在server层进行过滤返回符合要求的记录。
9 t0 G( N0 a' }! a& p$ P8 n8 c% A$ x* ?( ]
五、possible_keys. Y# N" @0 K8 I: B n+ i9 u
7 `$ A- l$ k/ {& P7 p此次查询中可能选用的索引,一个或多个( [; h5 ~. ~& z+ s7 @" f
9 g5 ~% w2 N7 p+ [3 v) X0 S六、key. y8 ^3 \" l$ R5 V: N% a* \
查询真正使使用到的索引,select_type为index_merge时,这里可能出现两个以上的索引,其他的select_type这里只会出现一个。
- H' I9 ?: `3 g$ V
+ j' e( f0 U4 f8 g- b8 z" u% r& v七、key_len
9 l1 `) M) }- S/ w+ w+ O& P8 W% v7 s
8 R( ?" Z" D' x用于处理查询的索引长度度,如果是单列索引,那就整个索引长度算进去,如果是多列索引,那么查
0 S' u6 n* {; U* J5 P询不一定都能使用到所有的列,具体使用到了多少个列的索引,这里就会计算进去,没有使用到的,这里不会计算进去。留意下这个列的值,算下你的多列索引总长度就知道有没有使用到所有的列了。7 w! c% N. D+ [3 @& Q4 z
另外,key_len只计算where条件用到的索引长度,而排序和分组就算使用到了索引,也不会计算到key_len中。
' N7 L1 k: Z' \explain select id from type_time where name =‘2’ 用到长度303
8 C) x3 T4 c, Z3 @: m y' p* y# ~: Y% Q+ p+ X& a
, n, U7 y+ i+ n6 ^7 E8 ?' mexplain select id from type_time where name =‘2’ and time in (‘2’,‘3’,‘4’) 用到长度 1071
6 n0 x' n" T$ m" P& J$ y# `# _, I5 R5 Z/ j8 K
" k9 c6 \1 W. G( p
( `# S. E: v+ M0 D, x& X# v八、ref
% c; f6 z! c) K) e2 W" l如果是使用的常数等值查询,这里会显示const
5 z9 E, M' ]; V& {+ \如果是连接查询,被驱动表的执行计划这里会显示驱动表的关联字段
8 Y" F' ~8 u$ p, V: ]: S) L如果是条件使用了表达式或者函数,或者条件列发生了内部隐式转换,这里可能显示为func# }( f2 [: |; F
1 Y. D# x) `; h2 n4 i
九、rows9 _6 X. I4 f! [+ F; J2 ?2 A1 i
这里是执行计划中估算的扫描行数,不是精确值(InnoDB不是精确的值,MyISAM是精确的值,主要原因是InnoDB使用了MVCC并发机制)
7 y5 i$ r2 |& z; E( X
2 Q# F: j4 j: ]; w& t十、extra8 U& H3 w9 T, q( A
这个列包含不适合在其他列中显示但十分重要的额外的信息,其中比较常见有一些:
; Y, \6 H0 b$ l- w2 G
$ e, T, H" p3 u4 A( z" @10、1 using temporary. H! Y, W" m8 r- a
表示使用了临时表存储中间结果。5 T" T }/ I( \5 d& [7 b8 D
MySQL在对查询结果order by和group by时使用临时表
& ^+ `; N+ _& Z- ~临时表可以是内存临时表和磁盘临时表,执行计划中看不出来,需要查看status变量,2 B2 C+ W4 T; U
used_tmp_table,used_tmp_disk_table才能看出来。4 R& v( U# x8 r& z+ ~2 K
5 R) m1 |: \1 \6 X; v
explain select * from user u inner join taddr t on u.addr_id=t.id GROUP BY t.id
* g3 K( [# {3 V; h7 g7 }+ h' v
: U; a" S$ r& o! G
9 m1 h/ [& b( ]3 }7 y% L& Z) E/ O2 ]
10、2 using filesort
- |/ y& n) K' X, |$ E! h3 ~ f排序时无法使用到索引时,就会出现这个。常用于order by和group by语句中
" T4 k ?4 [4 K, I5 _7 ]! P: W8 i8 m8 o$ M& y$ V' q: y1 I
说明MySQL会使用个外部的索引排序,而不是按照索引顺序进行读取。
( w# R- |' _! q5 ~( @MySQL中无法利索引索引完成的排序操作称为“文件排序“; d5 j7 L; v. Y: Q$ G+ T" P
* q1 r% F. V6 Y
10、3 using index1 E# ^' _" t* t( \
查询时不需要回表查询,直接通过索引就可以获取查询的数据。
9 C9 U& O b% M9 A6 s, F表示相应的SELECT查询中使用到了覆盖索引(Covering Index),避免回表访问数据行,效率不
# V3 x9 }6 o, a( R9 I4 T错。* v0 V' P) z. [" @% d; O
如果同时出现Using Where ,说明索引被用来执行查找索引键值
: r: Q, x3 |9 c0 J; \2 ~如果没有同时出现Using Where ,表明索引用来读取数据来执行查找动作。2 ~7 Q) k& e* L$ [ i
5 f3 w" ]$ d6 w# t
这里对索引的原理和explain做了一些介绍,需要索引需要建立之后对其改变查询方式可能会更能深刻理解 InnoDB 使用覆盖索引和非覆盖索引造成区别。这也是建立索引和使用sql需要特别考虑的问题。
/ a2 r6 a3 [9 e+ ~————————————————/ O$ }% ]& I6 [$ r
版权声明:本文为CSDN博主「筏镜」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。9 c, G6 s4 M! R ?! T6 v0 r$ F
原文链接:https://blog.csdn.net/fajing_feiyue/article/details/105616629
$ S% F9 H0 I$ C- o* I( r: F$ S* }
5 w. L* z% y9 C* @, v0 Q w, Y |