mongo添加索引及索引相关方法
目录
-
索引的类型和属性
- 单键索引
- 普通单键索引
- 索引子文档字段
- 唯一索引
- 复合索引
- 多键值索引
- 过期索引
- 哈希索引
- 地理位置索引
- 文本索引
- 单键索引
-
索引操作方法
- 查看现有索引
- 列出数据库的所有索引
- 删除索引
- 重建索引
- 创建索引的参数
-
索引规则
-
查询优化器
-
何时查询计划缓存才会变呢
-
联合索引的优化
-
聚合管道的优化
-
最期望看到的查询组合
-
最不期望看到的查询组合
-
最左前缀原则
-
效率极低的操作符
-
-
explain
- 介绍
- queryPlanner返回结果的意义
- executionStats返回结构的意义
- stage的类型的意义
-
运维命令
一.索引的类型和属性
1.单键索引
①普通单键索引
MongoDB 支持文档集合中任何字段的索引,在默认情况下,所有集合在 _id 字段上都有一个索引,应用程序和用户可以添加额外的索引来支持重要的查询操作
对于单字段索引和排序操作,索引键的排序顺序(即升序或降序)无关紧要,因为 MongoDB 可以在任意方向上遍历索引。
创建单键索引的语法结构如下:
# 1 为升序,-1 为降序
db.collection.createlndex ( { key: 1 } )
以下示例为插入一个文档,并在 score 键上创建索引,具体步骤如下:
db.records.insert(
{
"score" : 1034,
"location" : { state: "NY", city: "New York"}
}
)
db.records.createTndex( { score: 1 } )
使用 score 字段进行查询,再使用 explain() 函数,可以查看查询过程:
db.records.find({score:1034}).explain()
②索引子文档字段(或者"内嵌索引")
{
"address": {
"city": "Los Angeles",
"state": "California",
"pincode": "123"
},
"tags": [
"music",
"cricket",
"blogs"
],
"name": "Tom Benzamin"
}
假设我们需要通过city、state、pincode字段来检索文档,由于这些字段是子文档的字段,所以我们需要对子文档建立索引。
为子文档的city字段创建索引,命令如下:
db.users.ensureIndex({"address.city":1})
对嵌套文档本身“address”建立索引,与对嵌套文档的某个字段(address.city)建立索引是完全不相同的。
对整个文档建立索引,只有在使用文档完整匹配时才会使用到这个索引,例如建立了这样一个索引db.personInfos.createIndex({“address”:1}),那么只有使用db.personInfos.find({“address”:{“pincode”:”xxx”,”city”:”xxx”,""state":"xxx"}})这种完整匹配时才会使用到这个索引,使用db.personInfos.find({“address.city”:”xxx”})是不会使用到该索引的。
③唯一索引
唯一索引是索引具有的一种属性,让索引具备唯一性,确保这张表中,该条索引数据不会重复出现。在每一次insert和update操作时,都会进行索引的唯一性校验,保证该索引的字段组合在表中唯一。
db.containers.createIndex({name: 1},{unique:true, background: true})
db.packages.createIndex({ appId: 1, version: 1 },{unique:true, background: true})
Mongo提供两种建索引的方式foreground和background。
前台操作,它会阻塞用户对数据的读写操作直到index构建完毕;
后台模式,不阻塞数据读写操作,独立的后台线程异步构建索引,此时仍然允许对数据的读写操作。
创建索引时一定要写{background: true}
创建索引时一定要写{background: true}
创建索引时一定要写{background: true}MongoDB中是只有库级锁的,创建索引时要添加参数{background: true}。
2.复合索引
MongoDB 支持复合索引,其中复合索引结构包含多个字段
复合索引可以支持在多个字段上进行的匹配查询,语法结构如下:
db.collection.createIndex ({ <key1> : <type>, <key2> : <type2>, ...})
需要注意的是,在建立复合索引的时候一定要注意顺序的问题,顺序不同将导致查询的结果也不相同。
如下语句创建复合索引:
db.records.createIndex ({ "score": 1, "location.state": 1 })
查看复合索引的查询计划的语法如下:
db.records.find({score:1034, "location.state" : "NY"}).explain()
3.多键值索引(或者"数组索引")
若要为包含数组的字段建立索引,MongoDB 会为数组中的每个元素创建索引键。这些多键值索引支持对数组字段的高效查询
建多键值索引的语法如下:
db.collecttion.createlndex( { <key>: < 1 or -1 > })
需要注意的是,如果集合中包含多个待索引字段是数组,则无法创建复合多键索引。
以下示例代码展示插入文档,并创建多键值索引:
db.survey.insert ({item : "ABC", ratings: [ 2, 5, 9 ]})
db.survey.createIndex({ratings:1})
db.survey.find({ratings:2}).explain()
对数组建立索引的代价是非常高的,他实际上是会对数组中的每一项都单独建立索引,就相当于假设数组中有十项,那么就会在原基础上,多出十倍的索引大小。如果有一百个一千个呢?
所以在mongo中是禁止对两个数组添加复合索引的,对两个数组添加索引那么索引大小将是爆炸增长,所以谨记在心。
4.过期索引(TTL)
可以针对某个时间字段,指定文档的过期时间(经过指定时间后过期 或 在某个时间点过期)
5.哈希索引(Hashed Index)
是指按照某个字段的hash值来建立索引,hash索引只能满足字段完全匹配的查询,不能满足范围查询等
6.地理位置索引(Geospatial Index)
能很好的解决一些场景,比如『查找附近的美食』、『查找附近的加油站』等
7.文本索引(Text Index)
能解决快速文本查找的需求,比如,日志平台,相对日志关键词查找,如果通过正则来查找的话效率极低,这时就可以通过文本索引的形式来进行查找
二.索引操作方法
1.查看现有索引
若要返回集合上所有索引的列表,则需使用驱动程序的 db.collection.getlndexes() 方法或类似方法。
例如,可使用如下方法查看 records 集合上的所有索引:
db.records.getIndexes()
2.列出数据库的所有索引
若要列出数据库中所有集合的所有索引,则需在 MongoDB 的 Shell 客户端中进行以下操作:
db.getCollectionNames().forEach(function(collection){
indexes = db[collection].getIndexes();
print("Indexes for " + collection + ":" );
printjson(indexes);
});
3.删除索引
MongoDB 提供的两种从集合中删除索引的方法如下:
# 删除单个索引
db.collection.dropIndex("")
# 删除集合的全部索引
db.collection.dropIndexes()
若要删除特定索引,则可使用该 db.collection.droplndex() 方法。
例如,以下操作将删除集合中 score 字段的升序索引:
db.records.dropIndex ({ "score" : 1 }) //升序降序不能错,如果为-1,则提示无索引
还可以使用 db.collection.droplndexes() 删除除 _id 索引之外的所有索引。
例如,以下命令将从 records 集合中删除所有索引:
db.records.dropIndexes()
4.重建索引
db.myCollection.reIndex()
db.runCommand( { reIndex : 'myCollection' } )
通常这是不必要的,但是在集合的大小变动很大及集合在磁盘空间上占用很多空间时重建索引才有用。对于大数据量的集合来说,重建索引可能会很慢。
MongoDB中索引是大小写敏感的。
5.创建索引的参数
参数 | 类型 | 描述 |
---|---|---|
background | Boolean | 建索引过程会阻塞其它数据库操作,background可指定以后台方式创建索引,即增加 "background" 可选参数。 "background" 默认值为false。 |
unique | Boolean | 建立的索引是否唯一。指定为true创建唯一索引。默认值为false. |
name | string | 索引的名称。如果未指定,MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。 |
dropDups | Boolean | 3.0+版本已废弃。在建立唯一索引时是否删除重复记录,指定 true 创建唯一索引。默认值为 false. |
sparse | Boolean | 对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为true的话,在索引字段中不会查询出不包含对应字段的文档.。默认值为 false. |
expireAfterSeconds | integer | 指定一个以秒为单位的数值,完成 TTL设定,设定集合的生存时间。 |
v | index version | 索引的版本号。默认的索引版本取决于mongod创建索引时运行的版本。 |
weights | document | 索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。 |
default_language | string | 对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语 |
language_override | string | 对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的language,默认值为 language. |
三.索引规则
1.查询优化器
Mongo自带了一个查询优化器会为我们选择最合适的查询方案。
如果一个索引能够精确匹配一个查询,那么查询优化器就会使用这个索引。
如果不能精确匹配呢?可能会有几个索引都适合你的查询,那MongoDB是怎样选择的呢?
- MongoDB的查询计划会将多个索引并行的去执行,最先返回第101个结果的就是胜者,其他查询计划都会被终止,执行优胜的查询计划;
- 这个查询计划会被缓存,接下来相同的查询条件都会使用它;
2.何时查询计划缓存才会变呢?
- 在计划评估之后表发生了比较大的数据波动,查询优化器就会重新挑选可行的查询计划
- 建立索引时
- 每执行1000次查询之后,查询优化器就会重新评估查询计划
3.联合索引的优化
当你查询条件的顺序和你索引的顺序不一致的话,mongo会自动的调整查询顺序,保证你可以使用上索引。
例如:你的查询条件是(a,c,b)但是你的索引是(a,b,c)mongo会自动将你的查询条件调整为abc,寻找最优解。
4.聚合管道的优化
- 如果管道中不需要使用一个完整的文档的全部字段的话,管道不会将多余字段进行传递
- $sort 和 $limit 合并,在内存中只会维护limit个数量的文档,不需要将所有的文档维护在内存中,大大降低内存中sort的压力
然而管道中的索引使用情况是极其不佳的,在管道中,只有在管道最开始时的match sort可以使用到索引,一旦发生过project投射,group分组,lookup表关联,unwind打散等操作后,就完全无法使用索引。
5.最期望看到的查询组合
- Fetch+IDHACK
- Fetch+ixscan
- Limit+(Fetch+ixscan)
- PROJECTION+ixscan
6. 最不期望看到的查询组合
- COLLSCAN(全表扫)
- SORT(使用sort但是无index)
- COUNTSCAN****(不使用索引进行count)
7. 最左前缀原则
假定索引(a,b,c) 它可能满足的查询如下:
1. a
2. a,b
3. a,b,c
4. a,c [该组合只能用a部分]
5. a, c, b [cb在查询时会被优化换位置]
显然,最左前缀的核心是查询条件字段必须含有索引第一个字段
最左值尽可能用最精确过滤性最好的值,不要用那种可能会用于范围模糊查询,用于排序的字段
8. 效率极低的操作符
- \(where和\)exists:这两个操作符,完全不能使用索引。
- \(ne和\)not:通常来说取反和不等于,可以使用索引,但是效率极低,不是很有效,往往也会退化成扫描全表。
- $nin:不包含,这个操作符也总是会全表扫描
- 对于管道中的索引,也很容易出现意外,只有在管道最开始时的match sort可以使用到索引,一旦发生过project投射,group分组,lookup表关联,unwind打散等操作后,就完全无法使用索引。
四.explain
执行explain
db.union_recipe.find({"name" : /.*鸡.*/i,"foodTags.text":"鲁菜"}).explain("executionStats")
查询出来的计划
{
"queryPlanner": {
"plannerVersion": NumberInt("1"),
"namespace": "iof_prod_recipe.union_recipe",
"indexFilterSet": false,
"parsedQuery": {
"$and": [
{
"foodTags.text": {
"$eq": "鲁菜"
}
},
{
"name": {
"$regex": ".*鸡.*",
"$options": "i"
}
}
]
},
"winningPlan": {
# 根据内层阶段树查到的索引去抓取完整的文档
"stage": "FETCH",
"filter": {
"name": {
"$regex": ".*鸡.*",
"$options": "i"
}
},
# 每个阶段将自己的查询结果传递给父阶段树,所以从里往外读Explain
"inputStage": {
# IXSCAN该阶段使用了索引进行扫描
"stage": "IXSCAN",
# 使用了 foodTags.text: -1 这条索引
"keyPattern": {
"foodTags.text": -1
},
"indexName": "foodTags.text_-1",
"isMultiKey": true,
"multiKeyPaths": {
"foodTags.text": [
"foodTags"
]
},
"isUnique": false,
"isSparse": false,
"isPartial": false,
"indexVersion": NumberInt("2"),
"direction": "forward",
"indexBounds": {
"foodTags.text": [
"[\"鲁菜\", \"鲁菜\"]"
]
}
}
},
"rejectedPlans": [
{
"stage": "FETCH",
"filter": {
"foodTags.text": {
"$eq": "鲁菜"
}
},
"inputStage": {
"stage": "IXSCAN",
"filter": {
"name": {
"$regex": ".*鸡.*",
"$options": "i"
}
},
"keyPattern": {
"name": 1
},
"indexName": "name_1",
"isMultiKey": false,
"multiKeyPaths": {
"name": [ ]
},
"isUnique": false,
"isSparse": false,
"isPartial": false,
"indexVersion": NumberInt("2"),
"direction": "forward",
"indexBounds": {
"name": [
"[\"\", {})",
"[/.*鸡.*/i, /.*鸡.*/i]"
]
}
}
}
]
},
"executionStats": {
"executionSuccess": true,
"nReturned": NumberInt("49"),
"executionTimeMillis": NumberInt("2"),
"totalKeysExamined": NumberInt("300"),
"totalDocsExamined": NumberInt("300"),
"executionStages": {
"stage": "FETCH",
"filter": {
"name": {
"$regex": ".*鸡.*",
"$options": "i"
}
},
"nReturned": NumberInt("49"),
"executionTimeMillisEstimate": NumberInt("0"),
"works": NumberInt("302"),
"advanced": NumberInt("49"),
"needTime": NumberInt("251"),
"needYield": NumberInt("0"),
"saveState": NumberInt("5"),
"restoreState": NumberInt("5"),
"isEOF": NumberInt("1"),
"invalidates": NumberInt("0"),
"docsExamined": NumberInt("300"),
"alreadyHasObj": NumberInt("0"),
"inputStage": {
"stage": "IXSCAN",
"nReturned": NumberInt("300"),
"executionTimeMillisEstimate": NumberInt("0"),
"works": NumberInt("301"),
"advanced": NumberInt("300"),
"needTime": NumberInt("0"),
"needYield": NumberInt("0"),
"saveState": NumberInt("5"),
"restoreState": NumberInt("5"),
"isEOF": NumberInt("1"),
"invalidates": NumberInt("0"),
"keyPattern": {
"foodTags.text": -1
},
"indexName": "foodTags.text_-1",
"isMultiKey": true,
"multiKeyPaths": {
"foodTags.text": [
"foodTags"
]
},
"isUnique": false,
"isSparse": false,
"isPartial": false,
"indexVersion": NumberInt("2"),
"direction": "forward",
"indexBounds": {
"foodTags.text": [
"[\"鲁菜\", \"鲁菜\"]"
]
},
"keysExamined": NumberInt("300"),
"seeks": NumberInt("1"),
"dupsTested": NumberInt("300"),
"dupsDropped": NumberInt("0"),
"seenInvalidated": NumberInt("0")
}
}
},
"ok": 1,
"operationTime": Timestamp(1598602456, 1),
"$clusterTime": {
"clusterTime": Timestamp(1598602456, 1),
"signature": {
"hash": BinData(0, "/t+ZhDHuT6EtZMFyqmesvq9Rlfk="),
"keyId": NumberLong("6838110804550615041")
}
}
}
1.介绍
queryPlanner:查询计划的选择器,首先进行查询分析,最终选择一个winningPlan,是explain返回的默认层面。
executionStats:为执行统计层面,返回winningPlan的统计结果
allPlansExecution:为返回所有执行计划的统计,包括rejectedPlan
所以:我们在查询优化的时候,只需要关注queryPlanner, executionStats即可,因为queryPlanner为我们选择出了winningPlan, 而executionStats为我们统计了winningPlan的所有关键数据。
2.queryPlanner返回结果的意义
explain.queryPlanner: queryPlanner的返回
explain.queryPlanner.namespace:该值返回的是该query所查询的表
explain.queryPlanner.indexFilterSet:针对该query是否有indexfilter
explain.queryPlanner.winningPlan:查询优化器针对该query所返回的最优执行计划的详细内容。
explain.queryPlanner.winningPlan.stage:最优执行计划的stage,这里返回是FETCH,可以理解为通过返回的index位置去检索具体的文档(stage有数个模式,将在后文中进行详解)。
Explain.queryPlanner.winningPlan.inputStage:用来描述子stage,并且为其父stage提供文档和索引关键字。
explain.queryPlanner.winningPlan.stage的child stage,此处是IXSCAN,表示进行的是index scanning。
explain.queryPlanner.winningPlan.keyPattern:所扫描的index内容,此处是did:1,status:1,modify_time: -1与scid : 1
explain.queryPlanner.winningPlan.indexName:winning plan所选用的index。
explain.queryPlanner.winningPlan.isMultiKey是否是Multikey,此处返回是false,如果索引建立在array上,此处将是true。
explain.queryPlanner.winningPlan.direction:此query的查询顺序,此处是forward,如果用了.sort({modify_time:-1})将显示backward。
explain.queryPlanner.winningPlan.indexBounds:winningplan所扫描的索引范围,如果没有制定范围就是[MaxKey, MinKey],这主要是直接定位到mongodb的chunck中去查找数据,加快数据读取。
explain.queryPlanner.rejectedPlans:其他执行计划(非最优而被查询优化器reject的)的详细返回,其中具体信息与winningPlan的返回中意义相同,故不在此赘述。
3.executionStats返回结构的意义
executionStats.executionSuccess:是否执行成功
executionStats.nReturned:满足查询条件的文档个数,即查询的返回条数
executionStats.executionTimeMillis:整体执行时间
executionStats.totalKeysExamined:索引整体扫描的文档个数,和早起版本的nscanned 是一样的
executionStats.totalDocsExamined:document扫描个数, 和早期版本中的nscannedObjects 是一样的
executionStats.executionStages:整个winningPlan执行树的详细信息,一个executionStages包含一个或者多个inputStages
executionStats.executionStages.stage:这里是FETCH去扫描对于documents,后面会专门用来解释大部分查询使用到的各种stage的意思
executionStats.executionStages.nReturned:由于是FETCH,所以这里该值与executionStats.nReturned一致
executionStats.executionStages.docsExamined:与executionStats.totalDocsExamined一致executionStats.inputStage中的与上述理解方式相同
explain.executionStats.executionStages.works:被查询执行阶段所操作的“工作单元(work units)”数。
explain.executionStats.executionStages.advanced:优先返回给父stage的中间结果集中文档个数
explain.executionStats.executionStages.isEOF:查询执行是否已经到了数据流的末尾
这些值的初始值都是0。Works的 值当isEOF为1时要比nReturned大1, isEOF为0是相同。
explain 结果将查询计划以阶段树的形式呈现。
每个阶段将其结果(文档或索引键)传递给父节点。
中间节点操纵由子节点产生的文档或索引键。
根节点是MongoDB从中派生结果集的最后阶段。
4.stage的类型的意义
COLLSCAN :全表扫描
IXSCAN:索引扫描
FETCH::根据索引去检索指定document
SHARD_MERGE:各个分片返回数据进行merge
SORT:表明在内存中进行了排序(与前期版本的scanAndOrder:true一致)
SORT_MERGE:表明在内存中进行了排序后再合并
LIMIT:使用limit限制返回数
SKIP:使用skip进行跳过
IDHACK:针对_id进行查询
SHARDING_FILTER:通过mongos对分片数据进行查询
COUNT:利用db.coll.count()之类进行count运算
COUNTSCAN:count不使用用Index进行count时的stage返回
COUNT_SCAN:count使用了Index进行count时的stage返回
SUBPLA:未使用到索引的$or查询的stage返回
TEXT:使用全文索引进行查询时候的stage返回
五.常用操作
1.分析MongoDB数据库正在执行的请求
db.currentOp()
{
"desc" : "conn632530",
"threadId" : "140298196924160",
"connectionId" : 632530,
"client" : "11.192.159.236:57052",
"active" : true,
"opid" : 1008837885,
"secs_running" : 0,
"microsecs_running" : NumberLong(70),
"op" : "update",
"ns" : "mygame.players",
"query" : {
"uid" : NumberLong(31577677)
},
"numYields" : 0,
"locks" : {
"Global" : "w",
"Database" : "w",
"Collection" : "w"
},
....
},
字段 | 返回值说明 |
---|---|
client | 该请求是由哪个客户端发起的。 |
opid | 操作的唯一标识符。说明 如果有需要,可以通过db.killOp(opid)直接终止该操作。 |
secs_running | 表示该操作已经执行的时间,单位为秒。如果该字段返回的值特别大,需要查看请求是否合理。 |
microsecs_running | 表示该操作已经执行的时间,单位为微秒。如果该字段返回的值特别大,需要查看请求是否合理。 |
ns | 该操作目标集合。 |
op | 表示操作的类型。通常是查询、插入、更新、删除中的一种。 |
locks | 跟锁相关的信息,详情请参见并发介绍,本文不做详细介绍。 |
如果发现有异常的请求,您可以找到该请求对应的opid,执行db.killOp(opid)
终止该请求。
2.查看该数据下的慢请求日志
db.system.profile.find().pretty();
分析慢请求日志,查找引起MongoDB CPU使用率升高的原因。查看到该请求进行了全表扫描
{
"op" : "query",
"ns" : "123.testCollection",
"command" : {
"find" : "testCollection",
"filter" : {
"name" : "zhangsan"
},
"$db" : "123"
},
"keysExamined" : 0,
"docsExamined" : 11000000,
"cursorExhausted" : true,
"numYield" : 85977,
"nreturned" : 0,
"locks" : {
"Global" : {
"acquireCount" : {
"r" : NumberLong(85978)
}
},
"Database" : {
"acquireCount" : {
"r" : NumberLong(85978)
}
},
"Collection" : {
"acquireCount" : {
"r" : NumberLong(85978)
}
}
},
"responseLength" : 232,
"protocol" : "op_command",
"millis" : 19428,
"planSummary" : "COLLSCAN",
"execStats" : {
"stage" : "COLLSCAN",
"filter" : {
"name" : {
"$eq" : "zhangsan"
}
},
"nReturned" : 0,
"executionTimeMillisEstimate" : 18233,
"works" : 11000002,
"advanced" : 0,
"needTime" : 11000001,
"needYield" : 0,
"saveState" : 85977,
"restoreState" : 85977,
"isEOF" : 1,
"invalidates" : 0,
"direction" : "forward",
....in"
}
],
"user" : "root@admin"
}
通常在慢请求日志中,您需要重点关注以下几点。
-
全表扫描(关键字: COLLSCAN、 docsExamined )
全集合(表)扫描COLLSCAN 。
当一个操作请求(如查询、更新、删除等)需要全表扫描时,将非常占用CPU资源。在查看慢请求日志时发现COLLSCAN关键字,很可能是这些查询占用了CPU资源。说明:
如果这种请求比较频繁,建议对查询的字段建立索引的方式来优化。
通过查看docsExamined的值,可以查看到一个查询扫描了多少文档。该值越大,请求所占用的CPU开销越大。 -
不合理的索引(关键字: IXSCAN、keysExamined )
说明:
索引不是越多越好,索引过多会影响写入、更新的性能。
如果您的应用偏向于写操作,索引可能会影响性能。
通过查看keysExamined字段,可以查看到 一个使用了索引的查询,扫描了多少条索引。该值越大,CPU开销越大。
如果索引建立的不太合理,或者是匹配的结果很多。这样即使使用索引,请求开销也不会优化很多,执行的速度也会很慢。
-
大量数据排序(关键字: SORT、hasSortStage )
当查询请求里包含排序的时候, system.profile 集合里的hasSortStage字段会为 true 。
如果排序无法通 过索引满足,MongoDB会在查询结果中进行排序。
而排序这个动作将非常消耗CPU资源,这种情况需要对经常排序的字段建立索引的方式进行优化。
说明 当您在system.profile集合里发现SORT关键字时,可以考虑通过索引来优化排序。
-
其他还有诸如建立索引、aggregation(遍历、查询、更新、排序等动作的组合) 等操作也可能非常耗CPU资源,但本质上也是上述几种场景。
站在巨人肩膀上摘苹果
http://c.biancheng.net/view/6558.html
https://docs.mongodb.org/v3.0/reference/explain-results/
https://zhuanlan.zhihu.com/p/77971681
https://www.jianshu.com/p/2b09821a365d
https://www.imooc.com/article/285899
https://blog.csdn.net/weixin_33446857/article/details/83085018
https://www.runoob.com/mongodb/mongodb-advanced-indexing.html
https://www.runoob.com/mongodb/mongodb-indexing.html
目录 返回
首页