: \ q: w7 I1 N
mysql索引和explain的详解索引原理分析
; f5 Q% @9 {8 @# h6 R( m9 E* z1 ?* E+ P) V# v2 y6 K$ s
索引存储结构( S( p+ |" K7 b0 W0 I; j4 t7 g7 I* X
索引是在存储引擎中实现的,也就是说不同的存储引擎,会使使用不同的索引
9 X, z) Z% h6 c7 EMyISAM和InnoDB存储引擎:只支持B+ TREE索引, 也不能够更换3 X0 W2 ~' `8 P7 B+ ^! ~& n6 H
MEMORY/HEAP存储引擎:支持HASH和BTREE索引( [* X8 Q/ [+ a
" _& l5 Z* s) \/ B& A* B
B树图示5 N4 |* W! ^, v* A
) X$ A( [4 ^5 k# P6 V' P, PB树是为了磁盘或其它存储设备设计的一种多叉(下面你会看到,相对于二叉,B树每个内结点有多个分支,即多叉)平衡查找树。 多叉平衡。2 @5 \) q3 H7 u7 B
' e* A) L& Y: C' B/ V
: H2 j) G: ~; B( |4 n9 t- r! v4 o) L$ f/ v& W4 p& l. s
0 [7 U9 _8 T# Y, O# [+ Y
B树和B+树的区别:# {2 l# u1 V9 g
B树和B+树的最大区别在于非叶子节点是否存储数据的问题% l8 v1 {1 K: ~0 a
% h) L+ {' o7 L1 n* \+ E7 G" G
在结构上:
, v& u0 l! m$ H8 [* N4 t* p(1) B树是非也只节点和叶子节点都会存储数据。
. g3 S- m: ]& P( \' _% _(2) B+树只有叶子节点才会存储数据,而且数据都是在一行上,而且这些数据都是指针指向的,也是有顺序的。; y! i7 f8 [. | w& Z# E: o
: B' X I& T/ M4 e6 T2 Q在性能上:& `/ N A& e% C$ [3 T
(1)对于B-树相对于B+数据,B-Tree因为非叶子结点也保存具体数据,所以在查找某个关键字的时候找到即可返回。而B+Tree所有的数据都在叶子结点,每次查找都得到叶子结点。所以在同样高度的B-Tree和B+Tree中,B-Tree查找某个关键字的效率更高。B-Tree在单条数据读写有着更强的性能。( f- [1 C; _8 Y0 p
(2)但由于B+Tree所有的数据都在叶子结点,并且结点之间有指针连接,在找大于某个关键字或者小于某个关键字的数据的时候,B+Tree只需要找到该关键字然后沿着链表遍历就可以了,而B-Tree还需要遍历该关键字结点的根结点去搜索。这个也决定当连表查询的时候mysql比起mongo有显著的优势。更重要的是由于B-Tree的每个结点(这里的结点可以理解为一个数据页)都存储主键+实际数据,而B+Tree非叶子结点只存储关键字信息,而每个页的大小有限是有限的,所以同一页能存储的B-Tree的数据会比B+Tree存储的更少。这样同样总量的数据,B-Tree的深度会更大,增大查询时的磁盘I/O次数,进而影响查询效率。: C p+ G5 b# i$ a+ y2 s! {& Y
H2 @6 ^1 O: m. o/ e6 n- G
聚集索引(MyISAM)6 X8 A# y2 T1 ?
B+树叶节点只会存储数据行(数据文件)的指针,简单来说数据和索引不在一起,就是聚集# n# v6 {4 l$ n d2 B6 ~
索引。6 j6 l0 j! G, f H0 F: ^
聚集索引包含主键索引和辅助索引都会存储数据指针的值。
" @+ P3 @8 e; \. h1 R4 u+ H9 g
9 q4 W& |- J) A( V8 I$ f3 F
' [% e# F7 G. a8 f! K7 G, \) o* c, w0 x1 y5 z4 ]
辅助索引(次要索引)
/ Z! r; X2 J( {2 j7 a; Q/ U8 G; O在 MyISAM 中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,1 U* u) @/ V2 x/ }0 l7 r) [7 _
而辅助索引的 key 可以重复。如果我们在 Col2 上建立一个辅助索引,则此索引的结构如下图所示4 D+ ~4 O, u1 c9 Z# c2 ~
: [# i2 {% d5 ^& L& l) o同样也是一颗 B+Tree,叶子节点中保存数据记录的地址。因此,MyISAM 中索引检索的算法为首先按照B+Tree 搜索算法搜索索引,如果指定的 Key 存在,则取出其data 域的值,然后以 data 域的值为地址,读取相应数据记录。% U' I8 x0 ?5 |6 i. X
; r$ {9 U- W. x2 ]; b4 z聚集索引(InnoDB)3 C4 B" s, }- a/ I, `- O
' z% m' Q. H% z. R主键索引(聚集索引)的叶子节点会存储数据行,也就是说数据和索引是在一起,这就是聚集索引。
2 x7 N, d& S: x辅助索引只会存储主键值
6 C& D# z, N5 ^' W如果没有没有主键,则使用唯一索引建立聚集索引;如果没有唯一索引,MySQL会按照一定规则创建聚集索引。5 {2 j# y s5 Z
0 N8 O) Y. h/ p6 E l/ G% V4 a" G主键索引
) l* h$ N/ F' a! w" \! Y: ]3 d1.InnoDB 要求表必须有主键(MyISAM 可以没有),如果没有显式指定,则 MySQL系统会自动选择一个可以2 @7 _& S8 _4 e% k
唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL 自动为 InnoDB 表生成一个隐含字段作为主键,类型为长整形。# j8 g$ }5 h3 ^
* p3 c$ Y6 ~" F L3 N9 l( q
5 a- |! t1 V1 J4 a+ M2 O- L" Y' {, e& v& t' c0 m
5 @9 h) |- |/ `% q3 q0 N- m6 v9 D
上图是 InnoDB 主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为 InnoDB 的数据文件本身要按主键聚集。5 \4 Q/ C! E! ]* f2 [8 t8 i1 }3 D+ h
3 n# e% l0 Z: W( L3 M0 R' g4 n* c
4 u% |5 W4 e; S; l4 O1 G
$ j9 v; x9 G& s8 p
9 w) S8 G# k5 m. S( i: y! O) S
3 a3 E% K, |& H% {( _. f; D: C% T
G8 g! o% O; A/ P* `: ~mysql创建索引的时候和用法与索引息息相关,要建立合适的索引和理解一些索引的执行计划,就需要认识索引的结构。% o7 E: N8 ] J" _& ]
% x6 w! u1 _, F3 ~explain的详解' a. {# `, N/ e, e* m1 d4 C7 v
( q$ j# b* \- x1 J5 e
参数说明:" h. w6 _1 M# z4 b- G" C3 {
explain后会出现十列数据,下面将介绍这下面的十列数据。
9 g3 L# a+ g- U V" P { g f- h: I! a4 f- m
id、select_type、table、type、possible_keys、key、key_len、ref、rows、Extra
$ \3 O* k A& f' q2 j" U( z! t, _
. ^9 k6 V1 z, T( @2 ~8 ?' T先附上案例表:
4 w* f, H! Z7 g# Q
J$ P" L! o* d! s ~8 PCREATE TABLE `taddr` (
4 q7 S) R! R* f1 V, w `id` int(11) NOT NULL AUTO_INCREMENT,
: [) C W7 b# K1 P1 \ `country` varchar(100) DEFAULT '',
- X0 D! h. D$ E/ c( s5 k2 I5 _ `province` varchar(100) DEFAULT '',: a+ O" ^: i( Q
PRIMARY KEY (`id`)
. i/ m: L% q! o* A) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8, z* J" ~. u/ N0 W
( _0 O' v _: }; ]CREATE TABLE `user` (; y; i+ f; N9 Y7 i
`id` int(11) NOT NULL AUTO_INCREMENT,
# k8 `2 Y0 ^) H& E `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
% c Y" h- N) Q5 [2 ^4 T `password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,- n; \. N2 a! l3 L
`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
0 H6 `, h! W/ a9 \$ o' F8 B `addr_id` int(11) NULL DEFAULT NULL,
& K5 l* V$ U6 y+ Q0 Q& U4 g1 m" c) X PRIMARY KEY (`id`) USING BTREE,; E3 h' \! h% V& S, o5 C9 T2 K
INDEX `addr_id`(`addr_id`) USING BTREE% u! w8 A% B! {6 z$ i) Q. L
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;, t f$ Z9 V, Y5 ]6 g
, I! f% y8 j% O% ]" ]2 @5 O
2 f( W% H! g' r/ ~' d+ D8 gCREATE TABLE `type_time` ( B8 a/ m! |" x+ v
`id` int(11) NOT NULL AUTO_INCREMENT,
" e5 T. v! l" @3 A% O5 I! D `time` varchar(255) DEFAULT '[]', g! p% K( D( F; z% m6 {. Y
`name` varchar(100) DEFAULT '',
7 i3 ]' e0 u" Y E PRIMARY KEY (`id`),' n# \5 t% {. F- |: l, B) c0 t
INDEX `name_time_index`(`name`,`time`) USING BTREE
6 z" e e* V( w, P/ g. z0 D( E. ^) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf81 X3 v0 h" o0 i( T% c$ p
; `! I/ c- I6 u) A9 @
一、id
( F+ G. k# k( R5 \$ L$ F7 @ R每个 SELECT语句都会自动分配的一个唯一标识符.* B! t1 F2 x% O' ], K. M+ U
表示查询中操作表的顺序,有三种情况:
, ~- d! Y% ]; o/ Y% sid相同:执行顺序由上到下& F! q; l& a+ E. Y6 l, Y* m& g1 w: N
id不同:如果是子查询,id号会自增,id越大,优先级越高。
/ u7 l+ ~6 Q o) U2 `id相同的不同的同时存在
1 [1 Q% U1 g- s6 |1 ^- Nid列为null的就表示这是一个结果集,不需要使用它来进行查询。# q+ s8 I0 ]( W- {
8 Z6 X. R7 x+ Y2 ~& O二、select_type1 n- J& b2 L+ \7 f$ D$ p
+ ^3 @& M5 z* ]- r/ [+ ?# v
查询类型,主要用于区别普通查询、联合查询(union、union all)、子查询等复杂查询3 ~: h( G1 _& E) O
; Q+ R1 j) L0 u2 w) @2.1、simple! ~+ C; S4 ]4 d6 Q" b+ Z
表示不需要union操作或者不包含子查询的简单select查询。有连接查询时,外层的查询为simple9 _5 Z) _8 k) e" K2 \
5 {: z4 S+ v, i/ d
EXPLAIN select * from user
* S; o" `5 y7 m* n! Z3 y! o( D. g4 m. n8 n- s8 n* ?
* z# ~0 N o: E
5 B2 E# u, R+ G9 G$ p. x
EXPLAIN select u.id,u.addr_id,a.* from user u inner join taddr a on u.addr_id=a.id
8 a# v0 G$ N! j" _/ c% ~
% c g- M4 ]% [$ M W0 q% v3 l
+ F$ L& r9 C1 j$ f @$ U4 y
2.2 primary7 N0 {& r5 N* Z
一个需要union操作或者含有子查询的select,位于最外层的单位查询的select_type为primary。2 o, y/ H+ o8 ?+ \) o: h0 s' x5 ^2 e
+ S, \1 G& a( s6 B. Z: wexplain select * from taddr t inner join (" q6 K) @+ |' i7 M
select addr_id from user ) u on t.id=u.addr_id
2 ?; I8 V$ ?4 l, U3 }0 C0 C
1 X/ A9 K# _5 W, ?: l9 {
explain select * from user u where u.addr_id =1q
. f2 H# p. A) S/ E/ P3 }union all: c( e. ^9 A* U; f
select * from user u where u.addr_id =2
1 w0 y, h. ^7 R+ g
0 i4 P' N: S" h. r- T
, b! B8 d/ }8 S2.3 subquery
$ }5 I- v9 X1 n' B. k7 Z8 Q( u( d除了from字句中包含的一查询外,其他地方出现的子查询都可能是subquery) V$ Q+ l3 N$ n* I' @
1 J1 r% } r! J6 H
2.4 dependent subquery0 h+ T% i& @, L" n$ x
1 U1 \9 ? j, z6 u# A# H2 n
与dependent union类似,表示这个subquery的查询要受到外部表查询的影响4 F4 }% u! U N
0 h+ F! n1 W8 B1 yexplain select u.name,(select t.province from taddr t where u.addr_id=t.id) from user u
7 M Y5 P& `: \# ~1 b
3 P2 \' p# p. {/ Q* H
2.5 union" X- u" ]9 v- K; M) Y) P
union连接的两个select查询,第⼀个查询是PRIMARY,除了第一个表外,第二个以后的表select_type都是union
f# {4 M$ Y, m& _2 H
1 _+ b; K/ Y6 t. E' H三、table" q* f% W" n7 _" D6 W
显示的查询表名,如果查询使用了别名,那么这里显示的是别名
4 ]1 k. P+ [; `如果不涉及对数据表的操作,那么这显示为null# k+ o) L4 A0 q5 H7 k! J4 ^
如果显示为尖括号括起来的就表示这个是临时表,后边的N就是执行计划中的id,表示结果来自于这个查询产生。2 }) u* i8 P/ b
如果是尖括号括起来的<union M,N>,与类似,也是一个临时表,表示这个结果来自于union查询的id为M,N的结果集。0 k) S, q$ [6 ^" w9 Y4 \( t
- h' W! Q) R. o8 }0 I1 h3 @四、type
6 T- P4 D* i; `
7 I! K# r" P1 t: w$ b依次从好到差:
, A! e+ }2 X0 }5 X3 }9 S) @$ ~system,const,eq_ref,ref,fulltext,ref_or_null,unique_subquery,& K6 t( C9 L# \
index_subquery,range,index_merge,index,ALL& q0 m( ]3 g3 p* b" A5 N
. S+ I3 v* B, M6 Q% a4 t
除了all之外,其他的type都可以使⽤到索引,除了index_merge之外,其他的type只可以用到一个索引
$ c& z0 V$ t3 K- ~5 N: V4 ?, |$ Z/ k$ Z
4、1 system+ `4 K# e; A9 N
表中只有一行数据或者是空表。. |# I$ b& O2 v4 O8 p( l7 }
( E" Z: [- F$ H2 i, E6 G1 V
4、2const5 l) K3 u$ K4 ^1 O% n+ n; g
使用唯一索引或者主键,返回记录一定是1行记录的等值where条件时,通常type是const。其他数据库也叫做唯一索引扫描。8 `7 j' _! h6 r3 M/ j
# R; x0 A& ]7 ^4 ^- Q- b n( e; j4、3 eq_ref9 G8 D9 ^: K; w- ?/ r
关键字:连接字段主键或者唯一性索引。. C% L; c+ Z" [% x
此类型通常出现在多表的 join 查询, 表示对于前表的每一个结果, 都只能匹配到后表的一行结果. 并且查询的比较较操作通常是 ‘=’, 查询效率较高.
, Y1 Y* u/ L6 M# v7 |% U- ]+ n9 D: A3 n' G( _2 D8 ?& q5 K
EXPLAIN select u.id,u.addr_id,a.* from user u inner join taddr a on u.addr_id=a.id! b, J$ F, m2 m! C
) z1 B& ^0 l" h' _$ Q& s! c' |: g8 O# ` R
4 _+ Q; r8 p8 ^9 ^6 f7 m4 o
6 K/ B$ G: c+ \$ l3 i. z9 N7 E! {8 C/ }6 f( m( C
4、4 ref
, f7 _9 b* V( ^, i* k) a针对非唯一性索引,使用等值(=)查询非主键。或者是使用了最左前缀规则索引的查询。 EXPLAIN select u.id,u.addr_id,a.* from taddr a left join user u on u.addr_id=a.id
- P, r- h0 c2 t5 Z
& k$ S$ v4 E3 r; T3 ]4.5 fulltext
8 T; c3 ^* S; r8 R7 _( }4 O全文索引检索,要注意,全文索引的优先级很高,若全高索引和普通索引同时存在时,mysql不管代价,优先选择使用全文索引
% {0 @6 u4 ?) k6 y6 ]4 H" a8 H Y: T
O% X9 w6 m; m/ W4、6 unique_subquery7 ~2 c" l8 {9 b* s
用于where中的in形式子查询,子查询返回不重复值唯一值$ }+ y% y/ ]7 e, ~. O8 ^' d
: x0 S$ n/ ]+ M0 k/ X0 g
4、7 index_subquery
) _! R+ z0 ^5 I1 J4 W' R, n用于in形式子查询使用到了辅助索引或者in常数列表,子查询可能返回重复值,可以使用索引将子查询去重。6 X1 s; |3 Y. d
$ {4 V! [3 Y( n9 `; w1 @
4、8 range6 [" i2 O8 @; h! g% v% P
索引范围扫描,常用于使用>,<,is null,between ,in ,like等运算符的查询中。
. c0 t$ \7 ~ ]8 o
" G9 X ^2 ~5 l7 z& p" x$ A7 uexplain select * from type_time a inner join (
+ f7 s+ ^( R5 ]' bselect id from type_time where name =‘2’ and time in (‘2’,‘3’,‘4’) ) b on a.id=b.id% l7 X" B8 D. M( t
" K9 v! Y; K) U$ H$ E9 l) Y
# {& L; h, Q) u5 L
: ^; ^( P! h M F+ G7 D/ }8 H1 A) ?7 ~
4、9 index
* O% X, q: |3 @8 r6 s7 U键字:条件是出现在索引树中的节点的。可能没有完全匹配索引。" j/ f$ w& F$ y% g0 z4 K7 _
索引全表扫描,把索引从头到尾扫一遍,常用于使用索引列就可以处理不需要读取数据文件的查询、可以使使用索引排序或者分组的查询。+ F P0 ?# ^ o
7 x) P9 N# Z; p* k6 c, |
explain select * from user group by addr_id
$ ]5 H# | H3 U/ q
- I- G% Y" s7 ]4 \/ w) o% {
6 S. n/ }3 h8 @8 i7 T- j; w5 Z/ W, q: J( u7 H
6 s) r* v! y3 Y6 }% a. c8 q) Y5 V! i9 E- m) T, Y7 I* S
explain select addr_id from user6 i. |+ j! s( F5 j9 h4 y
4 }& |9 x- F7 u6 P/ S4 Y
2 i+ Q1 V5 t' H
S* I0 ~7 R9 E8 U) F3 p: q* j4 l. `3 W! A0 u1 O
4、10 all1 L0 P9 D+ }2 Q. S9 e: j; t
这个就是全表扫描数据文件,然后再在server层进行过滤返回符合要求的记录。3 V) S1 u5 O& l b6 o! r. H2 x: I
1 h- p/ p1 b4 h6 P& z
五、possible_keys/ X- v5 j+ H4 U; Q: g3 u
1 s1 f5 K) \* G# W$ ?
此次查询中可能选用的索引,一个或多个
: w4 W7 d1 C3 j& n) a8 h4 u D" f- R( S- c2 c# P
六、key0 \3 W3 |0 N& g% t4 S7 k# r
查询真正使使用到的索引,select_type为index_merge时,这里可能出现两个以上的索引,其他的select_type这里只会出现一个。
( k2 \+ O- h. a/ F$ w' e- U+ `0 J) Q' ]4 S5 g
七、key_len4 d K+ n3 [( ]+ w4 q' Y
* k1 n; i t9 U9 _: s用于处理查询的索引长度度,如果是单列索引,那就整个索引长度算进去,如果是多列索引,那么查
& d' `8 Q& H9 R6 {$ W9 `询不一定都能使用到所有的列,具体使用到了多少个列的索引,这里就会计算进去,没有使用到的,这里不会计算进去。留意下这个列的值,算下你的多列索引总长度就知道有没有使用到所有的列了。' _/ {; v; H4 P1 X' x
另外,key_len只计算where条件用到的索引长度,而排序和分组就算使用到了索引,也不会计算到key_len中。
* ?' \ I- o. V3 i; T" {explain select id from type_time where name =‘2’ 用到长度303
4 i* M/ Y, d1 r+ q* u" f+ _
! c/ V) t" j8 {" ~; T( ~
$ R: _6 y: J2 n. {% t, I4 Yexplain select id from type_time where name =‘2’ and time in (‘2’,‘3’,‘4’) 用到长度 1071' U# [; X3 ]( m! r* a
! n) ~ V. t8 [9 a" u( ?
; p+ y2 X- e$ {; S
) v/ A3 C( ]. W8 f9 E八、ref, A, q8 z; c3 H( u
如果是使用的常数等值查询,这里会显示const
9 F9 ]* V0 ?# L& c$ \3 E如果是连接查询,被驱动表的执行计划这里会显示驱动表的关联字段
5 @ V+ ]* |+ A( X如果是条件使用了表达式或者函数,或者条件列发生了内部隐式转换,这里可能显示为func
7 I9 {6 P L2 R
# c M! T" H" y1 B& [7 w; r9 Q4 Y九、rows: f2 U3 t2 M4 X; {9 |$ G" @
这里是执行计划中估算的扫描行数,不是精确值(InnoDB不是精确的值,MyISAM是精确的值,主要原因是InnoDB使用了MVCC并发机制)- M5 ?# j6 Y0 Z1 Z4 J& r; W
9 j( h2 W+ U* [$ q) c+ @
十、extra2 u; h6 Q1 ^# a# f5 b4 X. D; m% w( Q
这个列包含不适合在其他列中显示但十分重要的额外的信息,其中比较常见有一些:
c- y0 U5 D7 e' ]; ~
$ }% Y1 p. W& a. [. Z. |10、1 using temporary/ m- h6 r5 Y. ]
表示使用了临时表存储中间结果。, b0 U8 D( `: l2 e% ]: C5 m5 L
MySQL在对查询结果order by和group by时使用临时表7 I. T% o6 }7 Z6 @7 ^
临时表可以是内存临时表和磁盘临时表,执行计划中看不出来,需要查看status变量,
0 R* A* k- `( O0 }+ z4 vused_tmp_table,used_tmp_disk_table才能看出来。( \' o$ w) c) [8 r
9 M' `8 F# S6 w) `5 B
explain select * from user u inner join taddr t on u.addr_id=t.id GROUP BY t.id
! q. l! C% o2 v& l# r$ U1 D5 C1 T' L6 }( F$ l! t
0 Y* U7 I; O" W% I
2 D: F" y* m/ U- I* y+ [8 ?10、2 using filesort C* p2 Q/ d5 p0 f# f" j
排序时无法使用到索引时,就会出现这个。常用于order by和group by语句中; K/ ^0 O; f0 b1 ^% n9 V8 i( @
9 B4 ~. }0 C* @4 k说明MySQL会使用个外部的索引排序,而不是按照索引顺序进行读取。; E0 o8 T- @4 j4 f
MySQL中无法利索引索引完成的排序操作称为“文件排序“7 t$ l2 t E6 D+ p3 R1 [
# [: r. c* x6 o$ t( ~* H10、3 using index
/ ]/ ?# B2 r. A查询时不需要回表查询,直接通过索引就可以获取查询的数据。
) p& o N8 m7 a. p9 V% s* }% g表示相应的SELECT查询中使用到了覆盖索引(Covering Index),避免回表访问数据行,效率不$ J. q& T1 n$ B2 U
错。+ U5 a$ d4 g& M. F4 }) z; n
如果同时出现Using Where ,说明索引被用来执行查找索引键值
( U ` F+ Y9 n5 I) Q* t @* L3 O# ~如果没有同时出现Using Where ,表明索引用来读取数据来执行查找动作。* F! W8 }# ?$ ` O( }1 q# w$ s4 _
% z8 W' K9 b( T: c, [
这里对索引的原理和explain做了一些介绍,需要索引需要建立之后对其改变查询方式可能会更能深刻理解 InnoDB 使用覆盖索引和非覆盖索引造成区别。这也是建立索引和使用sql需要特别考虑的问题。* x" @3 z: t) p2 I& @! n+ e
————————————————
3 x4 b2 X5 X0 ]: s1 A1 S- T版权声明:本文为CSDN博主「筏镜」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。' R% M, ^" W( X
原文链接:https://blog.csdn.net/fajing_feiyue/article/details/105616629- ^0 z: h$ w' Q& Z4 i% z. d
2 N* \+ Y y6 r2 s! g
) r$ v) ]6 N' @1 B
|