Dunwu Blog

大道至简,知易行难

Elasticsearch 搜索(上)

搜索简介

Elasticsearch 支持多种搜索:

  • 精确搜索(词项搜索):搜索数值、日期、IP 或字符串的精确值或范围。
  • 全文搜索:搜索非结构化文本数据并查找与查询项最匹配的文档。
  • 向量搜索:存储向量,并使用 ANN 或 KNN 搜索来查找相似的向量,从而支持 语义搜索 等场景。

可以使用 _search API 来搜索和聚合 Elasticsearch 数据流或索引中的数据。API 的 query 请求采用 DSL 语义来进行查询。

Elasticsearch 支持两种搜索方式:URI Query 和 Request Body Query(DSL)

::: details URI Query 示例

1
2
3
GET /kibana_sample_data_ecommerce/_search?q=customer_first_name:Eddie
GET /kibana*/_search?q=customer_first_name:Eddie
GET /_all/_search?q=customer_first_name:Eddie

:::

::: details Request Body Query(DSL)示例

1
2
3
4
5
6
POST /kibana_sample_data_ecommerce/_search
{
"query": {
"match_all": {}
}
}

:::

当文档存储在 Elasticsearch 中时,它会在 1 秒内近乎实时地被索引和完全搜索。

Elasticsearch 基于 Lucene 开发,并引入了分段搜索的概念。分段类似于倒排索引,但 Lucene 中的单词 index 表示“段的集合加上提交点”。提交后,将向提交点添加新分段并清除缓冲区。

位于 Elasticsearch 和磁盘之间的是文件系统缓存。内存中索引缓冲区的文档会被写入新的分段,然后写入文件系统缓存,然后才刷新到磁盘。

A Lucene index with new documents in the in-memory buffer

Lucene 允许写入和打开新分段,使其包含的文档对搜索可见,而无需执行完全提交。这是一个比提交到磁盘要轻松得多的过程,并且可以频繁地完成而不会降低性能。

The buffer contents are written to a segment, which is searchable, but is not yet committed

在 Elasticsearch 中,写入和打开新分段的这一过程称为刷新。刷新使自上次刷新以来对索引执行的所有操作都可用于搜索。

默认情况下,Elasticsearch 每秒定期刷新一次索引,但仅限于在过去 30 秒内收到一个或多个搜索请求的索引。这就是我们说 Elasticsearch 具有近实时搜索能力的原因:文档更改不会立即对搜索可见,但会在此时间范围内变得可见。

排序

在 Elasticsearch 中,默认排序是按照相关性的评分(_score进行降序排序。_score 是浮点数类型,_score 评分越高,相关性越高。评分模型的选择可以通过 similarity 参数在映射中指定。

在 5.4 版本以前,默认的相关性算法是 TF-IDF。TF 是词频(term frequency),IDF 是逆文档频率(inverse document frequency)。一个简短的解释是,一个词条出现在某个文档中的次数越多,它就越相关;但是,如果该词条出现在不同的文档的次数越多,它就越不相关。5.4 版本以后,默认的相关性算法 BM25。

此外,也可以通过 sort 自定排序规则,如:按照字段的值排序、多级排序、多值字段排序、基于 geo(地理位置)排序以及自定义脚本排序。

::: details 排序示例

单字段排序

1
2
3
4
5
6
7
8
9
10
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {}
},
"sort": [
{"order_date": {"order": "desc"}}
]
}

多字段排序

1
2
3
4
5
6
7
8
9
10
11
12
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {}
},
"sort": [
{"order_date": {"order": "desc"}},
{"_doc":{"order": "asc"}},
{"_score":{ "order": "desc"}}
]
}

:::

详情参考:Sort search results

分页

默认情况下,Elasticsearch 搜索会返回前 10 个匹配的匹配项。

Elasticsearch 支持三种分页查询方式。

  • from + size
  • search after
  • scroll

from + size

可以使用 fromsize 参数分别指定起始页和每页记录数。

当一个查询:from = 990, size = 10,会在每个分片上先获取 1000 个文档。然后,通过协调节点聚合所有结果。最后,再通过排序选取前 1000 个文档。

页数越深,占用内存越多。为了避免深分页问题,ES 默认限定最多搜索 10000 个文档,可以通过 index.max_result_window 进行设置。

::: details from + size 分页查询示例

1
2
3
4
5
6
7
8
POST /kibana_sample_data_ecommerce/_search
{
"from": 2,
"size": 5,
"query": {
"match_all": {}
}
}

:::

scroll

scroll 搜索方式类似于 RDBMS 中的游标,只允许向下翻页。每次下一页查询后,使用返回结果的 scroll id 来作为下一次翻页的标记。

scroll 在搜索初始化阶段会生成快照,后续数据的变化无法及时体现在查询结果,因此更加适合一次性批量查询或非实时数据的分页查询。

启用游标查询时,需要注意设定期望的过期时间(scroll = 1m),以降低维持游标查询窗口所需消耗的资源。

注意:Elasticsearch 官方不再建议使用 scroll 查询方式进行深分页,而是推荐使用 search_after 和时间点(PIT)一起使用。

::: details scroll 分页查询示例

1
2
3
4
5
6
7
8
9
10
POST /kibana_sample_data_ecommerce/_search?scroll=1m
{
"size": 3,
"query": {
"match": {
"currency": "EUR"
}
}
}

响应结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"_scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAmTkWRTMzNmxBYmZUbUdsdFNqMnJoTl84Zw==",
"took": 0,
"timed_out": false,
"_shards": {
// 略
},
"hits": {
"total": {
"value": 4675,
"relation": "eq"
},
"max_score": 1,
"hits": [] // 略
}
}

:::

详情参考:Paginate search results

search after

search after 搜索方式不支持指定页数,只能向下翻页;并且需要指定 sort,并保证值是唯一的。然后,可以反复使用上次结果中最后一个文档的 sort 值进行查询。

search after 实现的思路同 scroll 方式基本一致,通过记录上一次分页的位置标识,来进行下一次分页查询。相比于 scroll 方式,它的优点是可以实时获取数据的变化,解决了查询快照导致的查询结果延迟问题。

::: details search after 分页查询示例

第一次查询

1
2
3
4
5
6
7
8
9
10
11

POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {}
},
"sort": [
{"order_date": {"order": "desc"}}
]
}

响应结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"took": 2609,
"timed_out": false,
"_shards": {
// 略
},
"hits": {
"total": {
"value": 4675,
"relation": "eq"
},
"max_score": null,
"hits": [
// 略多条记录
// 最后一条记录
{
// 略
"sort": [1642893235000]
}
]
}
}

从上次查询的响应中获取 sort 值,然后将 sort 值插入 search after 数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {}
},
"search_after": [
1642893235000
],
"sort": [
{
"order_date": {
"order": "desc"
}
}
]
}

:::

限定字段

默认情况下,搜索响应中的每个点击都包含 _source,该字段保存了原始文本的 JSON 对象。有两种推荐的方法可以从搜索查询中检索所选字段:

  • 使用 fields 选项指定响应结果中返回的值。
  • 如果需要在查询时返回原始文本数据,可以使用 _source 选项。

折叠搜索结果

Elasticsearch 中,可以通过 collapse 对搜索结果进行分组,且每个分组只显示该分组的一个代表文档。

::: details collapse 查询示例

1
2
3
4
5
6
7
8
9
10
POST /kibana_sample_data_ecommerce/_search
{
"size": 10,
"query": {
"match_all": {}
},
"collapse": {
"field": "day_of_week"
}
}

响应结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
{
"took": 106,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 4675,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "kibana_sample_data_ecommerce",
"_type": "_doc",
"_id": "yZUtBX4BU8KXl1YJRBrH",
"_score": 1,
"fields": {
"day_of_week": ["Monday"]
}
},
{
"_index": "kibana_sample_data_ecommerce",
"_type": "_doc",
"_id": "ypUtBX4BU8KXl1YJRBrH",
"_score": 1,
"fields": {
"day_of_week": ["Sunday"]
}
},
{
"_index": "kibana_sample_data_ecommerce",
"_type": "_doc",
"_id": "1JUtBX4BU8KXl1YJRBrH",
"_score": 1,
"fields": {
"day_of_week": ["Tuesday"]
}
},
{
"_index": "kibana_sample_data_ecommerce",
"_type": "_doc",
"_id": "1ZUtBX4BU8KXl1YJRBrH",
"_score": 1,
"fields": {
"day_of_week": ["Wednesday"]
}
},
{
"_index": "kibana_sample_data_ecommerce",
"_type": "_doc",
"_id": "2JUtBX4BU8KXl1YJRBrH",
"_score": 1,
"fields": {
"day_of_week": ["Saturday"]
}
},
{
"_index": "kibana_sample_data_ecommerce",
"_type": "_doc",
"_id": "2ZUtBX4BU8KXl1YJRBrH",
"_score": 1,
"fields": {
"day_of_week": ["Thursday"]
}
},
{
"_index": "kibana_sample_data_ecommerce",
"_type": "_doc",
"_id": "35UtBX4BU8KXl1YJRBrI",
"_score": 1,
"fields": {
"day_of_week": ["Friday"]
}
}
]
}
}

:::

过滤搜索结果

使用带有 filter 子句的布尔查询,可以过滤搜索和聚合的结果。

使用 post_filter 可以过滤搜索的结果,但不能过滤聚合结果。

:::details filter 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /kibana_sample_data_ecommerce/_search
{
"size": 10,
"query": {
"bool": {
"filter": {
"range": {
"taxful_total_price": {
"gte": 0,
"lte": 10
}
}
}
}
}
}

:::

高亮

Elasticsearch 的高亮(highlight)可以从搜索结果中的一个或多个字段中获取突出显示的摘要,以便向用户显示查询匹配的位置。当请求突出显示(即高亮)时,响应结果的 highlight 字段中包括高亮的字段和高亮的片段。Elasticsearch 默认会用 <em></em> 标签标记关键字。

Elasticsearch 提供了三种高亮器,分别是默认的 highlighter 高亮器postings-highlighter 高亮器fast-vector-highlighter 高亮器

::: details 高亮结果示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /kibana_sample_data_ecommerce/_search
{
"size": 10,
"query": {
"match_all": {}
},
"highlight": {
"fields": {
"user": {
"pre_tags": [
"<strong>"
],
"post_tags": [
"</strong>"
]
}
}
}
}

:::

详情参考:Highlighting

分片路由搜索

Elasticsearch 可以在多个节点上的多个分片中存储索引数据的副本。在运行搜索请求时,Elasticsearch 会选择包含索引数据副本的节点,并将搜索请求转发到该节点的分片。此过程称为路由

默认情况下,Elasticsearch 使用自适应副本选择来路由搜索请求。默认情况下,自适应副本选择从所有符合条件的节点和分片中进行选择。如果要限制符合搜索请求条件的节点和分片集,可以使用 preference 查询参数。

详情参考:Search shard routing

查询规则

Elasticsearch 允许自定义查询规则来进行搜索。

详情参考:Searching with query rules

搜索模板

搜索模板是可以使用不同变量运行的存储搜索。

详情参考:Search templates

参考资料

《极客时间教程 - Elasticsearch 核心技术与实战》笔记二

第四章:深入搜索

基于词项和基于全文的搜索

基于词项的查询

Term 是表达语意的最小单位。搜索和利用统计语言模型进行自然语言处理都需要处理 Term

Term 级别查询:Term / Range / Exists / Prefix / Wildcard

在 ES 中,Term 查询,对输入不做分词。会将输入作为一个整体,在倒排索引中查找准确的词项,并且使用相关度计算公式为每个包含该词项的文档进行相关度计算。

可以通过 Constant Score 将查询转换成一个 Filtering,避免算法,并利用缓存,提高性能。

基于文本的查询

文本查询:match、match_phrase、query_string

索引和搜索时都会进行分词,查询字符串先传递到一个合适的分词器,然后生成一个供查询的词项列表。

查询时,会先对输入的查询进行分词,然后每个词项柱哥进行底层的查询,最终将结果进行合并,并为每个文档计算一个相关度分值。

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
DELETE products
PUT products
{
"settings": {
"number_of_shards": 1
}
}

POST /products/_bulk
{ "index": { "_id": 1 }}
{ "productID" : "XHDK-A-1293-#fJ3","desc":"iPhone" }
{ "index": { "_id": 2 }}
{ "productID" : "KDKE-B-9947-#kL5","desc":"iPad" }
{ "index": { "_id": 3 }}
{ "productID" : "JODL-X-1937-#pV7","desc":"MBP" }

GET /products

POST /products/_search
{
"query": {
"term": {
"desc": {
//"value": "iPhone"
"value":"iphone"
}
}
}
}

POST /products/_search
{
"query": {
"term": {
"desc.keyword": {
//"value": "iPhone"
//"value":"iphone"
}
}
}
}

POST /products/_search
{
"query": {
"term": {
"productID": {
"value": "XHDK-A-1293-#fJ3"
}
}
}
}

POST /products/_search
{
//"explain": true,
"query": {
"term": {
"productID.keyword": {
"value": "XHDK-A-1293-#fJ3"
}
}
}
}

POST /products/_search
{
"explain": true,
"query": {
"constant_score": {
"filter": {
"term": {
"productID.keyword": "XHDK-A-1293-#fJ3"
}
}

}
}
}

#设置 position_increment_gap
DELETE groups
PUT groups
{
"mappings": {
"properties": {
"names":{
"type": "text",
"position_increment_gap": 0
}
}
}
}

GET groups/_mapping

POST groups/_doc
{
"names": [ "John Water", "Water Smith"]
}

POST groups/_search
{
"query": {
"match_phrase": {
"names": {
"query": "Water Water",
"slop": 100
}
}
}
}

POST groups/_search
{
"query": {
"match_phrase": {
"names": "Water Smith"
}
}
}

结构化搜索

结构化搜索是指对结构化数据的搜索。

日期、布尔、和数字类型都是结构化的。它们都有精确的格式,可以根据这些格式进行逻辑操作,如:比较范围、判定大小。

文本也可以是结构化的。结构化的文本可以精确匹配(term)或部分匹配(prefix)

结构化结果只有是或否两个选项。

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#结构化搜索,精确匹配
DELETE products
POST /products/_bulk
{ "index": { "_id": 1 }}
{ "price" : 10,"avaliable":true,"date":"2018-01-01", "productID" : "XHDK-A-1293-#fJ3" }
{ "index": { "_id": 2 }}
{ "price" : 20,"avaliable":true,"date":"2019-01-01", "productID" : "KDKE-B-9947-#kL5" }
{ "index": { "_id": 3 }}
{ "price" : 30,"avaliable":true, "productID" : "JODL-X-1937-#pV7" }
{ "index": { "_id": 4 }}
{ "price" : 30,"avaliable":false, "productID" : "QQPX-R-3956-#aD8" }

GET products/_mapping

#对布尔值 match 查询,有算分
POST products/_search
{
"profile": "true",
"query": {
"term": {
"avaliable": true
}
}
}

#对布尔值,通过 constant score 转成 filtering,没有算分
POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"constant_score": {
"filter": {
"term": {
"avaliable": true
}
}
}
}
}

#数字类型 Term
POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"term": {
"price": 30
}
}
}

#数字类型 terms
POST products/_search
{
"query": {
"constant_score": {
"filter": {
"terms": {
"price": [
"20",
"30"
]
}
}
}
}
}

#数字 Range 查询
GET products/_search
{
"query" : {
"constant_score" : {
"filter" : {
"range" : {
"price" : {
"gte" : 20,
"lte" : 30
}
}
}
}
}
}

# 日期 range
POST products/_search
{
"query" : {
"constant_score" : {
"filter" : {
"range" : {
"date" : {
"gte" : "now-1y"
}
}
}
}
}
}

#exists 查询
POST products/_search
{
"query": {
"constant_score": {
"filter": {
"exists": {
"field": "date"
}
}
}
}
}

#处理多值字段
POST /movies/_bulk
{ "index": { "_id": 1 }}
{ "title" : "Father of the Bridge Part II","year":1995, "genre":"Comedy"}
{ "index": { "_id": 2 }}
{ "title" : "Dave","year":1993,"genre":["Comedy","Romance"] }

#处理多值字段,term 查询是包含,而不是等于
POST movies/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"genre.keyword": "Comedy"
}
}
}
}
}

#字符类型 terms
POST products/_search
{
"query": {
"constant_score": {
"filter": {
"terms": {
"productID.keyword": [
"QQPX-R-3956-#aD8",
"JODL-X-1937-#pV7"
]
}
}
}
}
}

POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"match": {
"price": 30
}
}
}

POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"term": {
"date": "2019-01-01"
}
}
}

POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"match": {
"date": "2019-01-01"
}
}
}

POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"constant_score": {
"filter": {
"term": {
"productID.keyword": "XHDK-A-1293-#fJ3"
}
}
}
}
}

POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"term": {
"productID.keyword": "XHDK-A-1293-#fJ3"
}
}
}

#对布尔数值
POST products/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"avaliable": "false"
}
}
}
}
}

POST products/_search
{
"query": {
"term": {
"avaliable": {
"value": "false"
}
}
}
}

POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"term": {
"price": {
"value": "20"
}
}
}
}

POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"match": {
"price": "20"
}
}

POST products/_search
{
"query": {
"constant_score": {
"filter": {
"bool": {
"must_not": {
"exists": {
"field": "date"
}
}
}
}
}
}
}

搜索的相关性算分

搜索的相关性打分,描述了一个文档和查询语句匹配的程度。ES 会对每个匹配查询条件的结果进行算分(_score)。

ES5 之前,默认的相关性算法是 TD-IDF;ES5 后,采用 BM25。

词频(Term Frequency,TF) - 检索词在一篇文档中出现的频率

逆文档频率(Inverse Document Frequency,IDF) - log(全部文档数/检索词出现过的文档总数),用以表示检索词在所有文档中出现的频率。

Stop Word - 词项出现频率岁高,但对相关度几乎没有用户,例如:的、the、a 之类的词。

TF-IDF 本质上就是将 TF 求和变成了加权求和。

和 TF-IDF 相比,当 TF 无限增加时, BM 25 分支会趋于一个平稳值。

Boosting 是控制相关度的一种手段。

  • boost > 1,打分的权重提升;
  • 0 < boost < 1,打分的权重降低
  • boost < 0,贡献负分

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
PUT testscore
{
"settings": {
"number_of_shards": 1
},
"mappings": {
"properties": {
"content": {
"type": "text"
}
}
}
}

PUT testscore/_bulk
{ "index": { "_id": 1 }}
{ "content":"we use Elasticsearch to power the search" }
{ "index": { "_id": 2 }}
{ "content":"we like elasticsearch" }
{ "index": { "_id": 3 }}
{ "content":"The scoring of documents is caculated by the scoring formula" }
{ "index": { "_id": 4 }}
{ "content":"you know, for search" }

POST /testscore/_search
{
//"explain": true,
"query": {
"match": {
"content":"you"
//"content": "elasticsearch"
//"content":"the"
//"content": "the elasticsearch"
}
}
}

POST testscore/_search
{
"query": {
"boosting" : {
"positive" : {
"term" : {
"content" : "elasticsearch"
}
},
"negative" : {
"term" : {
"content" : "like"
}
},
"negative_boost" : 0.2
}
}
}

POST tmdb/_search
{
"_source": [
"title",
"overview"
],
"query": {
"more_like_this": {
"fields": [
"title^10",
"overview"
],
"like": [
{
"_id": "14191"
}
],
"min_term_freq": 1,
"max_query_terms": 12
}
}
}

Query & Filtering 实现多字符串多字段查询

ES 中,有 Query 和 Filter 两种不同的 Context

  • Query - 有相关性计算
  • Filter - 没有相关性计算,可以利用缓存,性能更好

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
POST /products/_bulk
{ "index": { "_id": 1 }}
{ "price" : 10,"avaliable":true,"date":"2018-01-01", "productID" : "XHDK-A-1293-#fJ3" }
{ "index": { "_id": 2 }}
{ "price" : 20,"avaliable":true,"date":"2019-01-01", "productID" : "KDKE-B-9947-#kL5" }
{ "index": { "_id": 3 }}
{ "price" : 30,"avaliable":true, "productID" : "JODL-X-1937-#pV7" }
{ "index": { "_id": 4 }}
{ "price" : 30,"avaliable":false, "productID" : "QQPX-R-3956-#aD8" }

#基本语法
POST /products/_search
{
"query": {
"bool" : {
"must" : {
"term" : { "price" : "30" }
},
"filter": {
"term" : { "avaliable" : "true" }
},
"must_not" : {
"range" : {
"price" : { "lte" : 10 }
}
},
"should" : [
{ "term" : { "productID.keyword" : "JODL-X-1937-#pV7" } },
{ "term" : { "productID.keyword" : "XHDK-A-1293-#fJ3" } }
],
"minimum_should_match" :1
}
}
}

#改变数据模型,增加字段。解决数组包含而不是精确匹配的问题
POST /newmovies/_bulk
{ "index": { "_id": 1 }}
{ "title" : "Father of the Bridge Part II","year":1995, "genre":"Comedy","genre_count":1 }
{ "index": { "_id": 2 }}
{ "title" : "Dave","year":1993,"genre":["Comedy","Romance"],"genre_count":2 }

#must,有算分
POST /newmovies/_search
{
"query": {
"bool": {
"must": [
{"term": {"genre.keyword": {"value": "Comedy"}}},
{"term": {"genre_count": {"value": 1}}}

]
}
}
}

#Filter。不参与算分,结果的 score 是 0
POST /newmovies/_search
{
"query": {
"bool": {
"filter": [
{"term": {"genre.keyword": {"value": "Comedy"}}},
{"term": {"genre_count": {"value": 1}}}
]

}
}
}

#Filtering Context
POST _search
{
"query": {
"bool" : {

"filter": {
"term" : { "avaliable" : "true" }
},
"must_not" : {
"range" : {
"price" : { "lte" : 10 }
}
}
}
}
}

#Query Context
POST /products/_bulk
{ "index": { "_id": 1 }}
{ "price" : 10,"avaliable":true,"date":"2018-01-01", "productID" : "XHDK-A-1293-#fJ3" }
{ "index": { "_id": 2 }}
{ "price" : 20,"avaliable":true,"date":"2019-01-01", "productID" : "KDKE-B-9947-#kL5" }
{ "index": { "_id": 3 }}
{ "price" : 30,"avaliable":true, "productID" : "JODL-X-1937-#pV7" }
{ "index": { "_id": 4 }}
{ "price" : 30,"avaliable":false, "productID" : "QQPX-R-3956-#aD8" }

POST /products/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"productID.keyword": {
"value": "JODL-X-1937-#pV7"}}
},
{"term": {"avaliable": {"value": true}}
}
]
}
}
}

#嵌套,实现了 should not 逻辑
POST /products/_search
{
"query": {
"bool": {
"must": {
"term": {
"price": "30"
}
},
"should": [
{
"bool": {
"must_not": {
"term": {
"avaliable": "false"
}
}
}
}
],
"minimum_should_match": 1
}
}
}

#Controll the Precision
POST _search
{
"query": {
"bool" : {
"must" : {
"term" : { "price" : "30" }
},
"filter": {
"term" : { "avaliable" : "true" }
},
"must_not" : {
"range" : {
"price" : { "lte" : 10 }
}
},
"should" : [
{ "term" : { "productID.keyword" : "JODL-X-1937-#pV7" } },
{ "term" : { "productID.keyword" : "XHDK-A-1293-#fJ3" } }
],
"minimum_should_match" :2
}
}
}

POST /animals/_search
{
"query": {
"bool": {
"should": [
{ "term": { "text": "brown" }},
{ "term": { "text": "red" }},
{ "term": { "text": "quick" }},
{ "term": { "text": "dog" }}
]
}
}
}

POST /animals/_search
{
"query": {
"bool": {
"should": [
{ "term": { "text": "quick" }},
{ "term": { "text": "dog" }},
{
"bool":{
"should":[
{ "term": { "text": "brown" }},
{ "term": { "text": "brown" }},
]
}

}
]
}
}
}

DELETE blogs
POST /blogs/_bulk
{ "index": { "_id": 1 }}
{"title":"Apple iPad", "content":"Apple iPad,Apple iPad" }
{ "index": { "_id": 2 }}
{"title":"Apple iPad,Apple iPad", "content":"Apple iPad" }

POST blogs/_search
{
"query": {
"bool": {
"should": [
{"match": {
"title": {
"query": "apple,ipad",
"boost": 1.1
}
}},

{"match": {
"content": {
"query": "apple,ipad",
"boost":
}
}}
]
}
}
}

DELETE news
POST /news/_bulk
{ "index": { "_id": 1 }}
{ "content":"Apple Mac" }
{ "index": { "_id": 2 }}
{ "content":"Apple iPad" }
{ "index": { "_id": 3 }}
{ "content":"Apple employee like Apple Pie and Apple Juice" }

POST news/_search
{
"query": {
"bool": {
"must": {
"match":{"content":"apple"}
}
}
}
}

POST news/_search
{
"query": {
"bool": {
"must": {
"match":{"content":"apple"}
},
"must_not": {
"match":{"content":"pie"}
}
}
}
}

POST news/_search
{
"query": {
"boosting": {
"positive": {
"match": {
"content": "apple"
}
},
"negative": {
"match": {
"content": "pie"
}
},
"negative_boost": 0.5
}
}
}

单字符串多字段查询 - DisMaxQuery

Disjunction Max Query - 将评分最高的字符评分作为结果返回,满足多个字段是竞争关系的场景

对最佳字段查询进行调优:通过控制 tie_breaker 参数,引入其他字段对计算的一些影响

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
PUT /blogs/_doc/1
{
"title": "Quick brown rabbits",
"body": "Brown rabbits are commonly seen."
}

PUT /blogs/_doc/2
{
"title": "Keeping pets healthy",
"body": "My quick brown fox eats rabbits on a regular basis."
}

POST /blogs/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}

POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
]
}
}
}

POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{
"match": {
"title": "Quick pets"
}
},
{
"match": {
"body": "Quick pets"
}
}
],
"tie_breaker": 0.2
}
}
}

单字符串多字段查询 - Multi Match

场景:最佳字段、多数字段、混合字段

multi_match

best_fields 是默认类型,可以不指定

minimum_should_match 等参数可以传递到生成的 query 中

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
],
"tie_breaker": 0.2
}
}
}

POST blogs/_search
{
"query": {
"multi_match": {
"type": "best_fields",
"query": "Quick pets",
"fields": ["title","body"],
"tie_breaker": 0.2,
"minimum_should_match": "20%"
}
}
}

POST books/_search
{
"multi_match": {
"query": "Quick brown fox",
"fields": "*_title"
}
}

POST books/_search
{
"multi_match": {
"query": "Quick brown fox",
"fields": [ "*_title", "chapter_title^2" ]
}
}

DELETE /titles
PUT /titles
{
"settings": { "number_of_shards": 1 },
"mappings": {
"my_type": {
"properties": {
"title": {
"type": "string",
"analyzer": "english",
"fields": {
"std": {
"type": "string",
"analyzer": "standard"
}
}
}
}
}
}
}

PUT /titles
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "english"
}
}
}
}

POST titles/_bulk
{ "index": { "_id": 1 }}
{ "title": "My dog barks" }
{ "index": { "_id": 2 }}
{ "title": "I see a lot of barking dogs on the road " }

GET titles/_search
{
"query": {
"match": {
"title": "barking dogs"
}
}
}

DELETE /titles
PUT /titles
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "english",
"fields": {"std": {"type": "text","analyzer": "standard"}}
}
}
}
}

POST titles/_bulk
{ "index": { "_id": 1 }}
{ "title": "My dog barks" }
{ "index": { "_id": 2 }}
{ "title": "I see a lot of barking dogs on the road " }

GET /titles/_search
{
"query": {
"multi_match": {
"query": "barking dogs",
"type": "most_fields",
"fields": [ "title", "title.std" ]
}
}
}

GET /titles/_search
{
"query": {
"multi_match": {
"query": "barking dogs",
"type": "most_fields",
"fields": [
"title^10",
"title.std"
]
}
}
}

多语言及中文分词与检索

自然语言与查询 recall

处理人类自然语言时,有些情况下,尽管搜索和原文不完全匹配,但希望搜到一些内容。

可采取的优化:

  • 归一化词元
  • 抽取词根
  • 包含同义词
  • 拼写错误

混合语言、中文的分词都存在一些挑战

  • 词干提取
  • 不正确的文档频率
  • 语言识别
  • 歧义

中文分析器

  • elasticsearch-analysis-hanlp
  • elasticsearch-analysis-ik
  • elasticsearch-analysis-pinyin

SpaceJam 一个全文搜索的实例

使用 SearchTemplate 和 IndexAlias 进行查询

综合排序:Function Score Query 优化算分

ES 默认会以文档的相关度算分进行排序

可以指定一个或多个字段进行排序

使用相关度算分排序,不能满足某些特定条件

function_score 可以在查询结束后,对每一个匹配的文档进行一系列的重新算分,根据新生成的分数进行排序。提供了几种默认的计算分值的函数:

  • weight - 为每一个文档设置一个简单而不被规范化的权重
  • field_value_factor - 使用该数值来修改 _score
  • random_score - 为每一个用户使用一个不同的,随机算分结果
  • 衰减函数 - 以某个字段的值为标准,距离某个值越近,得分越高
  • script_score - 自定义脚本完全控制所需逻辑

Boost Mode

  • multiply
  • sum
  • min / max
  • replace

Max Boost 可以将算分控制在一个最大值

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
DELETE blogs
PUT /blogs/_doc/1
{
"title": "About popularity",
"content": "In this post we will talk about...",
"votes": 0
}

PUT /blogs/_doc/2
{
"title": "About popularity",
"content": "In this post we will talk about...",
"votes": 100
}

PUT /blogs/_doc/3
{
"title": "About popularity",
"content": "In this post we will talk about...",
"votes": 1000000
}

POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes"
}
}
}
}

POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes",
"modifier": "log1p"
}
}
}
}

POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes",
"modifier": "log1p" ,
"factor": 0.1
}
}
}
}

POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes",
"modifier": "log1p" ,
"factor": 0.1
},
"boost_mode": "sum",
"max_boost": 3
}
}
}

POST /blogs/_search
{
"query": {
"function_score": {
"random_score": {
"seed": 911119
}
}
}
}

Term&PhraseSuggester

自动补全与基于上下文的提示

Completion Suggester,对性能要求比较苛刻。采用了不同的数据结构,并非通过倒排索引来完成。而是将 Analyze 的数据编码成 FST 和索引一起存放。FST 会被 ES 整个加载进内存,速度很快。

精准度:completion > Phrase > Term

召回率:Term > Phrase > Completion

性能:Completion > Phrase > Term

跨集群搜索

早期版本,通过 Tribe Node 可以实现多集群访问的需求,但是还存在一定的问题,现已废弃。

ES 5.3 引入了跨集群搜索的功能。

  • 允许任何节点扮演 federated 节点,以轻量的方式,将搜索请求进行代理
  • 不需要以 Client Node 形式加入其他集群

第五章:分布式特性及分布式搜索的机制

集群分布式模型及选主与脑裂问题

分布式特性:高可用、易扩展(水平扩展,支持 PB 级数据)

ES 集群名称可以通过配置或 -E cluster.name=xxx 来设定。

ES 节点本质上就是一个 JAVA 进程。一台机器上可以运行多个 ES 进程。

每个 ES 节点都有名字,可以通过配置文件或 -E node.name=xxx 来设定。

每个 ES 节点在启动后,会分片一个 UID,保存在 data 目录下。

  • Coordinating Node - 处理请求的节点,叫 Coordinating Node(协调节点),每个节点默认都是协调节点。
  • Data Node - 保存分片数据的节点。默认就是 data node,可以设置 node.data: false 禁止成为数据节点。
  • Master Node - 负责处理创建、删除索引等请求;决定分片被分配到哪个节点;负责索引的创建与删除;维护并更新集群状态。
    • 节点启动后,默认为主节点的候选节点,可以在必要时参与选主,成为 master node。可以设置 node.master: false 禁止成为主节点候选节点。
  • 集群状态
    • 所有的节点信息
    • 所有的索引和其相关的 mapping、setting
    • 分片的路由信息
    • 每个节点上都保存了集群的状态信息
    • 只有 master 节点才能修改集群的状态信息,并负责同步给其他节点

选主过程

集群中的节点互 ping,node id 最小的会成为被选举的节点。

其他节点会加入集群,但是不承担 master 节点的角色,一旦发现被选中的主节点丢失,就会选举出新的 master。

避免脑裂

7.0 之前,minimum_master_nodes 设为 N / 2 + 1

7.0 开始,ES 自动选择以形成仲裁的节点。

分片与集群的故障转移

主分片 - 水平扩展

副本分片 - 高可用:冗余、故障转移

分片数过小:无法通过增加节点实现扩展;分片数过大:使得单个分片容量很小,导致一个节点上有过多分片,影响性能。

文档分布式存储

文档到分配的路由

1
shard = hash(_routing) % number_of_primary_shards

hash 算法确保离散

默认的 _routing 值是文档 id,可以定制 routing 数值

这也是设置 setting 中主分片数后,不能随意修改的根本原因。

分片及其生命周期

分片是 ES 中的最小工作单元。分片是一个 Lucene 的索引。

倒排索引不可变性

无需考虑并发写文件的问题,避免了锁机制带来的性能问题

一旦读入内核的文件系统缓存,便留在哪里。只要文件系统存有足够的空间,大部分请求就会直接请求内存,不会命中磁盘,提升了很大的性能

如果需要让一个新的文档可以被搜索,需要重建整个索引。

Lucene Index

在 Lucene 中,单个倒排索引文件被称为 Segment。Segment 是自包闭的,不可变更的。多个 Segment 汇总在一起,称为 Lucene 的 Index,其对应的就是 ES 中的 shard

当有新文档写入时,会生成新 Segment,查询时会同时查询所有 Segment,并且对结果汇总。Lucene 中有一个文件,用来记录所有 Segment 信息,叫做 Commit Point。

删除的文档信息,保存在 .del 文件中。

什么是 Refresh

将 Index buffer 写入 Segment 的过程叫 refresh。refresh 不执行 fsync 操作。

refresh 默认 1 秒发生一次,refresh 后,数据就可以被搜索到了。

如果系统有大量的数据写入,就会产生很多的 Segment

index buffer 被占满时,会触发 refresh,默认是 JVM 的 10%

什么是事务日志

segment 写入磁盘的过程相对耗时,借助文件系统缓存,refresh 时,现将 segment 写入缓存以开放查询

为了保证数据不丢失,所以在 index 文档时,同时写事务日志,高版本开始,事务日志默认落盘。每个分片有一个事务日志。

在 ES refresh 时,index buffer 被清空,事务日志不会清空

什么是 flush

调用 refresh,index buffer 清空并 refresh

调用 fsync,将缓存中的 segments 写入磁盘

清空事务日志

默认 30 分钟调用一次

事务日志满(512MB)

Merge

Segment 很多,需要被定期合并

ES 和 Lucene 会自动进行 Merge 操作

剖析分布式查询及相关性评分

ES 搜索分为两阶段:

  1. Query
  2. Fetch

Query 阶段

用户发出搜索请求到 ES 节点。节点收到请求后,会以协调节点的身份,在所有主副本分片中随机选择主分片数个分片,发送查询请求。

被选中的分片执行查询,进行排序。然后,每个分片都会返回 from +size 个排序后的文档 id 和排序值给协调节点。

Fetch 阶段

协调节点会将 Query 阶段从每个分片获取的排序后的文档 id 列表,进行重排序,选取 from 到 from +size 个文档的 id

以 multi get 请求的方式,到相应的分片获取详细的文档数据

潜在问题

性能问题

  • 每个分片上需要查的文档数 = from + size
  • 最终协调节点需要处理 = 主分片数 * ( from + size)
  • 深度分页

相关性算分

每个分片都要基于自己分片上的数据进行相关度计算。这会导致打分偏离的情况,尤其是数据量很少时。当文档总数很少的情况下,主分片数越多,相关性计算会越不准。

解决算分不准的方法

将主分片数设为 1;

使用 _search?search_type=dfs_query_then_fetch,消耗更多 CPU 和内存,执行性能低下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
DELETE message
PUT message
{
"settings": {
"number_of_shards": 20
}
}

GET message

POST message/_doc?routing=1
{
"content":"good"
}

POST message/_doc?routing=2
{
"content":"good morning"
}

POST message/_doc?routing=3
{
"content":"good morning everyone"
}

POST message/_search
{
"explain": true,
"query": {
"match_all": {}
}
}

POST message/_search
{
"explain": true,
"query": {
"term": {
"content": {
"value": "good"
}
}
}
}

POST message/_search?search_type=dfs_query_then_fetch
{

"query": {
"term": {
"content": {
"value": "good"
}
}
}
}

排序及 DocValues&Fielddata

默认采用相关性算分对结果进行降序排序

可以通过设定 sorting 参数,自行设定排序

如果不指定 _score,算分为 null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#单字段排序
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {

}
},
"sort": [
{"order_date": {"order": "desc"}}
]
}

#多字段排序
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {

}
},
"sort": [
{"order_date": {"order": "desc"}},
{"_doc":{"order": "asc"}},
{"_score":{ "order": "desc"}}
]
}

GET kibana_sample_data_ecommerce/_mapping

#对 text 字段进行排序。默认会报错,需打开 fielddata
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {

}
},
"sort": [
{"customer_full_name": {"order": "desc"}}
]
}

#打开 text 的 fielddata
PUT kibana_sample_data_ecommerce/_mapping
{
"properties": {
"customer_full_name" : {
"type" : "text",
"fielddata": true,
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}

#关闭 keyword 的 doc values
PUT test_keyword
PUT test_keyword/_mapping
{
"properties": {
"user_name":{
"type": "keyword",
"doc_values":false
}
}
}

DELETE test_keyword

PUT test_text
PUT test_text/_mapping
{
"properties": {
"intro":{
"type": "text",
"doc_values":true
}
}
}

DELETE test_text

DELETE temp_users
PUT temp_users
PUT temp_users/_mapping
{
"properties": {
"name":{"type": "text","fielddata": true},
"desc":{"type": "text","fielddata": true}
}
}

Post temp_users/_doc
{"name":"Jack","desc":"Jack is a good boy!","age":10}

#打开 fielddata 后,查看 docvalue_fields 数据
POST temp_users/_search
{
"docvalue_fields": [
"name","desc"
]
}

#查看整型字段的 docvalues
POST temp_users/_search
{
"docvalue_fields": [
"age"
]
}

分页与遍历-FromSize&SearchAfter&ScrollAPI

from + size

当一个查询:from = 990, size = 10,会在每个分片上先获取 1000 个文档。然后,通过协调节点聚合所有结果。最后,再通过排序选取前 1000 个文档。

页数越深,占用内存越多。为了避免深分页问题,ES 默认限定到 10000 个文档。

search after

实时获取下一页文档信息,不支持指定页数,只能向下翻页。

需要指定 sort,并保证值是唯一的

然后,可以反复使用上次结果中最后一个文档的 sort 值进行查询

scroll

创建一个快照,有新的数据写入以后,无法被查到。

每次持续后,输入上一次的 scroll id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
POST tmdb/_search
{
"from": 10000,
"size": 1,
"query": {
"match_all": {

}
}
}

#Scroll API
DELETE users

POST users/_doc
{"name":"user1","age":10}

POST users/_doc
{"name":"user2","age":11}

POST users/_doc
{"name":"user2","age":12}

POST users/_doc
{"name":"user2","age":13}

POST users/_count

POST users/_search
{
"size": 1,
"query": {
"match_all": {}
},
"sort": [
{"age": "desc"} ,
{"_id": "asc"}
]
}

POST users/_search
{
"size": 1,
"query": {
"match_all": {}
},
"search_after":
[
10,
"ZQ0vYGsBrR8X3IP75QqX"],
"sort": [
{"age": "desc"} ,
{"_id": "asc"}
]
}

#Scroll API
DELETE users
POST users/_doc
{"name":"user1","age":10}

POST users/_doc
{"name":"user2","age":20}

POST users/_doc
{"name":"user3","age":30}

POST users/_doc
{"name":"user4","age":40}

POST /users/_search?scroll=5m
{
"size": 1,
"query": {
"match_all" : {
}
}
}

POST users/_doc
{"name":"user5","age":50}
POST /_search/scroll
{
"scroll" : "1m",
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAWAWbWdoQXR2d3ZUd2kzSThwVTh4bVE0QQ=="
}

处理并发读写

采用乐观锁机制

内部版本控制:_seq_no + primary_term

外部版本控制:version + version_type=external

第六章:深入聚合分析

Bucket&Metric聚合分析及嵌套聚合

Metric(统计) - 统计计算

Bucket(分组) - 按一定规则,将文档分配到不同的桶中。

Metric 聚合

  • 单值聚合 - 只输出一个分析结果
    • min、max、avg、sum、cardinality
  • 多值聚合 - 输出多个分析结果
    • stats、extended_stats、percentile、percentile_rank、top_hits

Pipeline聚合分析

Pipeline聚合支持对聚合分析的结果,进行再次聚合分析。

Pipeline 聚合的分析结果会输出到原结果中,根据位置的不同,分为两类:

聚合的作用范围及排序

ES 聚合分析的默认作用范围是 query 的查询结果集。

同时 ES 还支持以下方式改变聚合的作用范围:

  • filter
  • post_filter
  • global

指定 order,按照 _count_key 进行排序。

聚合分析的原理及精准度问题

ES 在进行聚合分析时,协调节点会在每个分片的主分片、副分片中选一个,然后在不同分片上分别进行聚合计算,然后将每个分片的聚合结果进行汇总,返回最终结果。

由于,并非基于全量数据进行计算,所以聚合结果并非完全准确。

要解决聚合准确性问题,有两个解决方案:

  • 解决方案 1:当数据量不大时,设置 Primary Shard 为 1,这意味着在数据全集上进行聚合。
  • 解决方案 2:设置 shard_size 参数,将计算数据范围变大,进而使得 ES 的整体性能变低,精准度变高。shard_size 值的默认值是 size * 1.5 + 10

第七章:数据建模(略)

第八章:保护你的数据(略)

第九章:水平扩展 Elasticsearch 集群

第十章:生产环境中的集群运维(略)

第十一章:索引生命周期管理(略)

第十二章:用Logstash和Beats构建数据管道(略)

第十三章:用Kibana进行数据可视化分析(略)

第十四章:探索X-Pack套件(略)

参考资料

《极客时间教程 - Elasticsearch 核心技术与实战》笔记一

第一章:概述

课程介绍(略)

课程综述及学习建议(略)

Elasticsearch 概述及其发展历史

Elasticsearch 是一款基于 Lucene 的开源分布式搜索引擎。

  • 1.0(2014 年 1 月)
  • 5.0(2016 年 10 月)
    • Lucene 6.x
    • 默认打分机制从 TD-IDF 改为 BM 25
    • 支持 Keyword 类型
  • 6.0(2017 年 10 月)
    • Lucene 7.x
    • 跨集群复制
    • 索引生命周期管理
    • SQL 的支持
  • 7.0(2019 年 4 月)
    • Lucene 7.x
    • 移除 Type
    • ECK (用于支持 K8S)
    • 集群协调
    • High Level Rest Client
    • Script Score 查询

Elastic Stack 家族成员及其应用场景

Elasticsearch、Logstash、Kibana

Beats - 各种采集器

X-Pack - 商业化套件

第二章:安装上手

Elasticsearch 的安装与简单配置

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#启动单节点
bin/elasticsearch -E node.name=node0 -E cluster.name=geektime -E path.data=node0_data

#安装插件
bin/elasticsearch-plugin install analysis-icu

#查看插件
bin/elasticsearch-plugin list
#查看安装的插件
GET http://localhost:9200/_cat/plugins?v

#start multi-nodes Cluster
bin/elasticsearch -E node.name=node0 -E cluster.name=geektime -E path.data=node0_data
bin/elasticsearch -E node.name=node1 -E cluster.name=geektime -E path.data=node1_data
bin/elasticsearch -E node.name=node2 -E cluster.name=geektime -E path.data=node2_data
bin/elasticsearch -E node.name=node3 -E cluster.name=geektime -E path.data=node3_data

#查看集群
GET http://localhost:9200
#查看 nodes
GET _cat/nodes
GET _cluster/health

Kibana 的安装与界面快速浏览

1
2
3
4
5
#启动 kibana
bin/kibana

#查看插件
bin/kibana-plugin list

资料:

在 Docker 容器中运行 Elasticsearch,Kibana 和 Cerebro

Logstash 安装与导入数据

Elasticsearch 入门

基本概念 1 索引文档和 RESTAPI

基本概念:

  • Document
    • Elasticsearch 是面向文档的,文档是所有可搜索数据的最小单位。
    • Elasticsearch 中,文档会被序列化成 JSON 格式保存。无模式。
    • 每个文档都有一个唯一性 ID,如果没有指定,ES 会自动生成。
  • Field - 文档包含一组字段。每个字段有对应类型(字符串、数值、布尔、日期、二进制、范围)
    • 元数据(内置字段) - 以 _ 开头
      • _index - 文档所属索引
      • _type - 文档所属类型
      • _id - 文档的唯一 ID
      • _source - 文档的原始数据(JSON)
      • _all - 整合所有字段内容到该字段,已废弃
      • _version - 文档版本
      • _score - 相关性打分
  • Index - Document 的容器。
    • Mapping - 定义文档字段类型
    • Setting - 定义不同数据分布
  • Type - 7.0 移除 Type,每个 Index 只有一个名为 _doc 的 Type。
  • Node
  • Shard
  • Cluster

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#查看索引相关信息
GET kibana_sample_data_ecommerce

#查看索引的文档总数
GET kibana_sample_data_ecommerce/_count

#查看前 10 条文档,了解文档格式
POST kibana_sample_data_ecommerce/_search
{
}

#_cat indices API
#查看 indices
GET /_cat/indices/kibana*?v&s=index

#查看状态为绿的索引
GET /_cat/indices?v&health=green

#按照文档个数排序
GET /_cat/indices?v&s=docs.count:desc

#查看具体的字段
GET /_cat/indices/kibana*?pri&v&h=health,index,pri,rep,docs.count,mt

#How much memory is used per index?
GET /_cat/indices?v&h=i,tm&s=tm:desc

基本概念 2 - 集群、节点、分片、副本

集群的作用:高可用、可扩展

ES 集群通过集群名来区分。集群名通过配置文件或 -E cluster.name=xxx 来指定。

ES 节点通过配置文件或 -E node.name=xxx 指定。

每个 ES 节点启动后,会分配一个 UID,保存在 data 目录下

master 候选节点和 master 节点

每个节点启动后,默认就是一个 master 候选节点。候选节点可以通过选举,成为 master 节点。

集群中第一个节点启动时,会将自己选举为 master 节点。

每个节点上都保存了集群的状态,只有 master 节点才能修改集群的状态信息(通过集中式管理,保证数据一致性)。

集群状态信息:

  • 所有的节点信息
  • 所有的索引和相关 mapping、setting 信息
  • 分片的路由信息

data node 和 coordinating node

  • data node - 保存数据的节点,叫做 data node。负责保存分片数据。
  • coordinating node - 负责接受 client 请求,将请求分发到合适节点,最终把结果汇聚到一起。每个节点默认都有 coordinating node 的职责。

其他节点类型

hot & warm 节点 - 不同硬件配置的 data node,用来实现 hot & warm 架构,降低集群部署成本。

机器学习节点 - 负责跑机器学习的 Job,用来做异常检测

tribe 节点 - 连接到不同的 ES 集群

分片

主分片 - 用于水平扩展,以提升系统可承载的总数据量以及吞吐量。

  • 一个分片是一个运行 Lucene 实例
  • 主分片数在索引创建时指定,后续不允许修改,除非 reindex

副分片(副本) - 用于冗余,解决高可用的问题。

  • 副本数,可以动态调整
  • 增加副本数,可以在一定程度上提高服务的可用性,以及查询的吞吐量。

生产环境的分片数,需要提前规划:

分片数过小:

  • 无法通过增加节点实现水平扩展
  • 单个分片的数据量太大,导致数据重新分配耗时

分片数过大:

  • 影响搜索结果的相关性打分,影响统计结果的准确性
  • 单个节点上过多的分片,会导致资源浪费,同时也会影响性能
  • 7.0 开始,默认主分片数设置为 1, 解决了 over-sharding 的问题

查看集群健康状态

GET _cluster/health 有三种结果:

  • Green - 主分片和副本都正常分配
  • Yellow - 主分片全部正常分配,有副本分片未能正常分配
  • Red - 有主分片未能分配

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
get _cat/nodes?v
GET /_nodes/es7_01,es7_02
GET /_cat/nodes?v
GET /_cat/nodes?v&h=id,ip,port,v,m

GET _cluster/health
GET _cluster/health?level=shards
GET /_cluster/health/kibana_sample_data_ecommerce,kibana_sample_data_flights
GET /_cluster/health/kibana_sample_data_flights?level=shards

#### cluster state
The cluster state API allows access to metadata representing the state of the whole cluster. This includes information such as
GET /_cluster/state

#cluster get settings
GET /_cluster/settings
GET /_cluster/settings?include_defaults=true

GET _cat/shards
GET _cat/shards?h=index,shard,prirep,state,unassigned.reason

文档的基本 CRUD 和批量操作

文档的 CRUD

  • create - 创建文档,如果 ID 已存在,会失败
  • update - 增量更新文档,且文档必须已存在
  • index - 若文档不存在,则创建新文档;若文档存在,则删除现有文档,再创建新文档,同时 version+1
  • delete - DELETE <index>/_doc/1
  • read

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# create document. 自动生成 _id
POST users/_doc
{
"user" : "Mike",
"post_date" : "2019-04-15T14:12:12",
"message" : "trying out Kibana"
}

#create document. 指定 Id。如果 id 已经存在,报错
PUT users/_doc/1?op_type=create
{
"user" : "Jack",
"post_date" : "2019-05-15T14:12:12",
"message" : "trying out Elasticsearch"
}

#create document. 指定 ID 如果已经存在,就报错
PUT users/_create/1
{
"user" : "Jack",
"post_date" : "2019-05-15T14:12:12",
"message" : "trying out Elasticsearch"
}

### Get Document by ID
#Get the document by ID
GET users/_doc/1

### Index & Update
#Update 指定 ID (先删除,在写入)
GET users/_doc/1

PUT users/_doc/1
{
"user" : "Mike"
}

#GET users/_doc/1
#在原文档上增加字段
POST users/_update/1/
{
"doc": {
"post_date": "2019-05-15T14:12:12",
"message": "trying out Elasticsearch"
}
}

### Delete by Id
# 删除文档
DELETE users/_doc/1

批量写

bulk API 支持四种类型:

  • index
  • create
  • update
  • delete
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
### Bulk 操作
#执行两次,查看每次的结果

#执行第 1 次
POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test2", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }

#执行第 2 次
POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test2", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }

批量读

  • mget

  • msearch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
### mget 操作
GET /_mget
{
"docs": [
{
"_index": "test",
"_id": "1"
},
{
"_index": "test",
"_id": "2"
}
]
}

#URI 中指定 index
GET /test/_mget
{
"docs": [
{
"_id": "1"
},
{
"_id": "2"
}
]
}

GET /_mget
{
"docs": [
{
"_index": "test",
"_id": "1",
"_source": false
},
{
"_index": "test",
"_id": "2",
"_source": [
"field3",
"field4"
]
},
{
"_index": "test",
"_id": "3",
"_source": {
"include": [
"user"
],
"exclude": [
"user.location"
]
}
}
]
}

### msearch 操作
POST kibana_sample_data_ecommerce/_msearch
{}
{"query":{"match_all":{}},"size":1}
{"index":"kibana_sample_data_flights"}
{"query":{"match_all":{}},"size":2}

### 清除测试数据
#清除数据
DELETE users
DELETE test
DELETE test2

倒排索引入门

什么是正排,什么是倒排?

  • ** 正排 **:文档 ID 到文档内容和单词的关联
  • ** 倒排 **:单词到文档 ID 的关系

倒排索引含两个部分

  • ** 单词词典 ** - 记录所有文档的单词,记录单词到倒排列表的关联关系
  • ** 倒排列表 ** - 记录了单词对应的文档结合,由倒排索引项组成。

倒排索引项:

  • 文档 ID
  • 词频 TF - 单词在文档中出现的次数,用于相关性评分
  • 位置 - 单词文档中分词的位置。用于语句搜索
  • 偏移 - 记录单词的开始结束位置,实现高亮显示

要点:

  • 文档中每个字段都有自己的倒排索引
  • 可以指定某些字段不做索引

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST _analyze
{
"analyzer": "standard",
"text": "Mastering Elasticsearch"
}

POST _analyze
{
"analyzer": "standard",
"text": "Elasticsearch Server"
}

POST _analyze
{
"analyzer": "standard",
"text": "Elasticsearch Essentials"
}

通过分析器进行分词

** 分词 **:文本分析是把全文本转换一系列单词(term / token)的过程。

分析组件由如下三部分组成,它的执行顺序如下:

1
Character Filters -> Tokenizer -> Token Filters

说明:

  • Character Filters(字符过滤器) - 针对原始文本处理, 例如去除特殊字符、过了 html 标签
  • Tokenizer(分词器) - 按照策略将文本切分为单词
  • Token Filters(分词过滤器) - 对切分的单词进行加工,如:转为小写、删除 stop word、增加同义词等

ES 内置分析器:

中文分词

elasticsearch-analysis-ik

elasticsearch-thulac-plugin

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#查看不同的 analyzer 的效果
#standard
GET _analyze
{
"analyzer": "standard",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

#simpe
GET _analyze
{
"analyzer": "simple",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

GET _analyze
{
"analyzer": "stop",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

#stop
GET _analyze
{
"analyzer": "whitespace",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

#keyword
GET _analyze
{
"analyzer": "keyword",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

GET _analyze
{
"analyzer": "pattern",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

#english
GET _analyze
{
"analyzer": "english",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

POST _analyze
{
"analyzer": "icu_analyzer",
"text": "他说的确实在理”"
}

POST _analyze
{
"analyzer": "standard",
"text": "他说的确实在理”"
}

POST _analyze
{
"analyzer": "icu_analyzer",
"text": "这个苹果不大好吃"
}

SearchAPI 概览

ES Search 有两种类型:

  • URI 查询 - 在 URL 中使用查询
  • Request Body 查询 - 基于 JSON 格式的 DSL
语法 范围
/_search 集群上的所有索引
/index1/_search index1
/index1,index2/_search index1 和 index2
/index*/_search 以 index 开头的索引

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
#URI Query
GET kibana_sample_data_ecommerce/_search?q=customer_first_name:Eddie
GET kibana*/_search?q=customer_first_name:Eddie
GET /_all/_search?q=customer_first_name:Eddie

#REQUEST Body
POST kibana_sample_data_ecommerce/_search
{
"profile": true,
"query": {
"match_all": {}
}
}

URISearch 详解

使用 q 指定查询字符串(query string)

  • q - 指定查询语句,使用 Query String 语义
  • df - 默认字段
  • sort - 排序
  • from/size - 分页
  • profile - 显示查询是如何被执行的

指定字段 vs. 泛查询

  • q=title:2012 / q=2012

Term vs. Phrase

  • Beautiful Mind,等效于 Beautiful Or Mind
  • “Beautiful Mind”,等效于 Beautiful And Mind

分组与引号

  • title:(Beautiful And Mind)
  • title=”Beautiful Mind”

布尔操作

  • AND / OR / NOT 或 && / || / !
  • 必须大写
  • title:(matrix NOT reloaded)

分组

  • + 表示 must
  • - 表示 must_not
  • title:(+matrix -reloaded)

范围查询

区间表示:[] 闭区间,{} 开区间

  • year:{2019 TO 2018}
  • year:{* TO 2018}

算数符号

  • year:>2010
  • year:(>2010 && <=2018)
  • year:(+>2010 +<=2018)

通配符查询(通配符查询效率低,占用内存大,不建议使用。特别是放在最前面)

? 表示 1 个字符;* 表示任意个字符

  • title:mi?d
  • title:be*

正则表达式

  • title:[bt]oy

模糊匹配与近似查询

  • title:befutifl~1
  • title:"lord rings"~2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

#基本查询
GET /movies/_search?q=2012&df=title&sort=year:desc&from=0&size=10&timeout=1s

#带 profile
GET /movies/_search?q=2012&df=title
{
"profile":"true"
}

#泛查询,正对 _all, 所有字段
GET /movies/_search?q=2012
{
"profile":"true"
}

#指定字段
GET /movies/_search?q=title:2012&sort=year:desc&from=0&size=10&timeout=1s
{
"profile":"true"
}

# 查找美丽心灵,Mind 为泛查询
GET /movies/_search?q=title:Beautiful Mind
{
"profile":"true"
}

# 泛查询
GET /movies/_search?q=title:2012
{
"profile":"true"
}

#使用引号,Phrase 查询
GET /movies/_search?q=title:"Beautiful Mind"
{
"profile":"true"
}

#分组,Bool 查询
GET /movies/_search?q=title:(Beautiful Mind)
{
"profile":"true"
}

#布尔操作符
# 查找美丽心灵
GET /movies/_search?q=title:(Beautiful AND Mind)
{
"profile":"true"
}

# 查找美丽心灵
GET /movies/_search?q=title:(Beautiful NOT Mind)
{
"profile":"true"
}

# 查找美丽心灵
GET /movies/_search?q=title:(Beautiful %2BMind)
{
"profile":"true"
}

#范围查询 , 区间写法
GET /movies/_search?q=title:beautiful AND year:[2002 TO 2018%7D
{
"profile":"true"
}

#通配符查询
GET /movies/_search?q=title:b*
{
"profile":"true"
}

// 模糊匹配 & 近似度匹配
GET /movies/_search?q=title:beautifl~1
{
"profile":"true"
}

GET /movies/_search?q=title:"Lord Rings"~2
{
"profile":"true"
}

RequestBody 与 QueryDSL 简介

  • DSL
  • from / size(分页)
  • sort(排序)
  • _source(原文本查询)
  • script_fields(脚本)
  • match
  • match_phrase
  • simple_query_string
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
curl -XGET "http://localhost:9200/kibana_sample_data_ecommerce/_search" -H 'Content-Type: application/json' -d'
{
"query": {
"match_all": {}
}
}'

#ignore_unavailable=true,可以忽略尝试访问不存在的索引“404_idx”导致的报错
#查询 movies 分页
POST /movies,404_idx/_search?ignore_unavailable=true
{
"profile": true,
"query": {
"match_all": {}
}
}

POST /kibana_sample_data_ecommerce/_search
{
"from":10,
"size":20,
"query":{
"match_all": {}
}
}

#对日期排序
POST kibana_sample_data_ecommerce/_search
{
"sort":[{"order_date":"desc"}],
"query":{
"match_all": {}
}

}

#source filtering
POST kibana_sample_data_ecommerce/_search
{
"_source":["order_date"],
"query":{
"match_all": {}
}
}

#脚本字段
GET kibana_sample_data_ecommerce/_search
{
"script_fields": {
"new_field": {
"script": {
"lang": "painless",
"source": "doc['order_date'].value+'hello'"
}
}
},
"query": {
"match_all": {}
}
}

POST movies/_search
{
"query": {
"match": {
"title": "last christmas"
}
}
}

POST movies/_search
{
"query": {
"match": {
"title": {
"query": "last christmas",
"operator": "and"
}
}
}
}

POST movies/_search
{
"query": {
"match_phrase": {
"title":{
"query": "one love"

}
}
}
}

POST movies/_search
{
"query": {
"match_phrase": {
"title":{
"query": "one love",
"slop": 1

}
}
}
}

QueryString&SimpleQueryString 查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
PUT /users/_doc/1
{
"name":"Ruan Yiming",
"about":"java, golang, node, swift, elasticsearch"
}

PUT /users/_doc/2
{
"name":"Li Yiming",
"about":"Hadoop"
}

POST users/_search
{
"query": {
"query_string": {
"default_field": "name",
"query": "Ruan AND Yiming"
}
}
}

POST users/_search
{
"query": {
"query_string": {
"fields":["name","about"],
"query": "(Ruan AND Yiming) OR (Java AND Elasticsearch)"
}
}
}

#Simple Query 默认的 operator 是 Or
POST users/_search
{
"query": {
"simple_query_string": {
"query": "Ruan AND Yiming",
"fields": ["name"]
}
}
}

POST users/_search
{
"query": {
"simple_query_string": {
"query": "Ruan Yiming",
"fields": ["name"],
"default_operator": "AND"
}
}
}

GET /movies/_search
{
"profile": true,
"query":{
"query_string":{
"default_field": "title",
"query": "Beafiful AND Mind"
}
}
}

# 多 fields
GET /movies/_search
{
"profile": true,
"query":{
"query_string":{
"fields":[
"title",
"year"
],
"query": "2012"
}
}
}

GET /movies/_search
{
"profile":true,
"query":{
"simple_query_string":{
"query":"Beautiful +mind",
"fields":["title"]
}
}
}

DynamicMapping 和常见字段类型

什么是 Mapping

Mapping 类似数据库中 schema 的定义

Mapping 会将 JSON 文档映射成 Lucene 所需要的数据格式

一个 Mapping 属于一个索引的 Type

字段数据类型

  • 简单类型
  • Text / Keyword
  • Date
  • Integer / Floating
  • Boolean
  • Ipv4 / Ipv6
  • 复杂类型
  • 对象类型 / 嵌套类型
  • 特殊类型
  • get_point & geo_shape / percolator

什么是 Dynamic Mapping

在写入文档时,如果索引不存在,会自动创建索引

ES 会根据文档信息,自动推算出字段的类型

有时候,推算可能会不准确,当类型设置错误时,可能会导致一些功能无法正常运行。例如范围查询

能否更改 Mapping 的字段类型

Dynamic 设为 true 时,一旦有新增字段的文档写入,Mapping 也同时被更新

Dynamic 设为 false 时,Mapping 不会被更新,新增字段的数据无法被索引,但是信息会出现在 _source 中。

Dynamic 设为 stric 时,文档写入失败

对已有字段,一旦有数据写入,就不再支持修改字段的定义

如果希望改变字段类型,必须 reindex API,重建索引

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#写入文档,查看 Mapping
PUT mapping_test/_doc/1
{
"firstName":"Chan",
"lastName": "Jackie",
"loginDate":"2018-07-24T10:29:48.103Z"
}

#查看 Mapping 文件
GET mapping_test/_mapping

#Delete index
DELETE mapping_test

#dynamic mapping,推断字段的类型
PUT mapping_test/_doc/1
{
"uid" : "123",
"isVip" : false,
"isAdmin": "true",
"age":19,
"heigh":180
}

#查看 Dynamic
GET mapping_test/_mapping

#默认 Mapping 支持 dynamic,写入的文档中加入新的字段
PUT dynamic_mapping_test/_doc/1
{
"newField":"someValue"
}

#该字段可以被搜索,数据也在 _source 中出现
POST dynamic_mapping_test/_search
{
"query":{
"match":{
"newField":"someValue"
}
}
}

#修改为 dynamic false
PUT dynamic_mapping_test/_mapping
{
"dynamic": false
}

#新增 anotherField
PUT dynamic_mapping_test/_doc/10
{
"anotherField":"someValue"
}

#该字段不可以被搜索,因为 dynamic 已经被设置为 false
POST dynamic_mapping_test/_search
{
"query":{
"match":{
"anotherField":"someValue"
}
}
}

get dynamic_mapping_test/_doc/10

#修改为 strict
PUT dynamic_mapping_test/_mapping
{
"dynamic": "strict"
}

#写入数据出错,HTTP Code 400
PUT dynamic_mapping_test/_doc/12
{
"lastField":"value"
}

DELETE dynamic_mapping_test

显式 Mapping 设置与常见参数介绍

  • index - 控制当前字段是否被索引
  • index_options - 控制倒排索引记录的内容
    • docs - 记录 doc id
    • freqs - 记录 doc id 和 term freqencies
    • positions - 记录 doc id 和 term freqencies、term position
    • offsets - 记录 doc id 和 term freqencies、term position、char offsets
  • null_value - 对 null 值实现搜索,只有 keyword 类型支持
  • copy_to - _all 在 ES 7.X 被 copy_to 替代

ES 不提供专门的数组类型。但是任何字段 ,都可以包含多个相同类型的数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#设置 index 为 false
DELETE users
PUT users
{
"mappings": {
"properties": {
"firstName": {
"type": "text"
},
"lastName": {
"type": "text"
},
"mobile": {
"type": "text",
"index": false
}
}
}
}

PUT users/_doc/1
{
"firstName":"Ruan",
"lastName": "Yiming",
"mobile": "12345678"
}

POST /users/_search
{
"query": {
"match": {
"mobile":"12345678"
}
}
}

#设定 Null_value
DELETE users
PUT users
{
"mappings": {
"properties": {
"firstName": {
"type": "text"
},
"lastName": {
"type": "text"
},
"mobile": {
"type": "keyword",
"null_value": "NULL"
}
}
}
}

PUT users/_doc/1
{
"firstName":"Ruan",
"lastName": "Yiming",
"mobile": null
}

PUT users/_doc/2
{
"firstName": "Ruan2",
"lastName": "Yiming2"
}

GET users/_search
{
"query": {
"match": {
"mobile": "NULL"
}
}
}

#设置 Copy to
DELETE users
PUT users
{
"mappings": {
"properties": {
"firstName": {
"type": "text",
"copy_to": "fullName"
},
"lastName": {
"type": "text",
"copy_to": "fullName"
}
}
}
}

PUT users/_doc/1
{
"firstName": "Zhang",
"lastName": "Peng"
}

GET users/_search?q=fullName:(Zhang Peng)

POST users/_search
{
"query": {
"match": {
"fullName": {
"query": "Zhang Peng",
"operator": "and"
}
}
}
}

#数组类型
PUT users/_doc/1
{
"name":"onebird",
"interests":"reading"
}

PUT users/_doc/1
{
"name":"twobirds",
"interests":["reading","music"]
}

POST users/_search
{
"query": {
"match_all": {}
}
}

GET users/_mapping

多字段特性及 Mapping 中配置自定义 Analyzer

ES 内置的分析器无法满足需求时,可以自定义分析器,通过组合不同组件来进行定制:

  • Character Filter - html strip、mapping、pattern replace
  • Tokenizer - whitespace、standard、uax_url_email、pattern、keyword、path hierarchy
  • Token Filter - lowercase、stop、synonym

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
PUT logs/_doc/1
{
"level": "DEBUG"
}

GET /logs/_mapping

POST _analyze
{
"tokenizer":"keyword",
"char_filter":["html_strip"],
"text": "<b>hello world</b>"
}

POST _analyze
{
"tokenizer":"path_hierarchy",
"text":"/user/ymruan/a/b/c/d/e"
}

#使用 char filter 进行替换
POST _analyze
{
"tokenizer": "standard",
"char_filter": [
{
"type" : "mapping",
"mappings" : [ "- => _"]
}
],
"text": "123-456, I-test! test-990 650-555-1234"
}

# char filter 替换表情符号
POST _analyze
{
"tokenizer": "standard",
"char_filter": [
{
"type" : "mapping",
"mappings" : [ ":) => happy", ":( => sad"]
}
],
"text": ["I am felling :)", "Feeling :( today"]
}

# white space and snowball
GET _analyze
{
"tokenizer": "whitespace",
"filter": ["stop","snowball"],
"text": ["The gilrs in China are playing this game!"]
}

# whitespace 与 stop
GET _analyze
{
"tokenizer": "whitespace",
"filter": ["stop","snowball"],
"text": ["The rain in Spain falls mainly on the plain."]
}

# remove 加入 lowercase 后,The 被当成 stopword 删除
GET _analyze
{
"tokenizer": "whitespace",
"filter": ["lowercase","stop","snowball"],
"text": ["The gilrs in China are playing this game!"]
}

# 正则表达式
GET _analyze
{
"tokenizer": "standard",
"char_filter": [
{
"type": "pattern_replace",
"pattern": "http://(.*)",
"replacement": "$1"
}
],
"text": "http://www.elastic.co"
}

IndexTemplate 和 DynamicTemplate

集群上的索引会越来越多,可以根据时间周期性创建索引,例如:log-yyyyMMdd

index template - 帮助设定 mapping 和 setting,并按照一定规则,自动匹配到新创建的索引上。

  • 模板仅在一个索引被新建时,才会起作用。修改模板不会影响已创建的索引。
  • 可以设定多个索引模板,这些设置会被 merge 在一起
  • 可以指定 order,以控制模板合并过程

什么是 Dynamic Template

根据 ES 识别的数据类型,结合字段名称,来动态设定字段类型

  • 所有的字符串类型都设定成 keyword,或关闭 keyword 字段
  • is 开头的字段都设置成 boolean
  • long_ 开头的都设置成 long 类型

Dynamic Template 要点

  • Dynamic Template 是定义在某索引的 mapping 中
  • Template 有一个名称
  • 匹配规则是一个数组
  • 为匹配到字段设置 mapping
  • match_mapping_type - 匹配自动识别的字段类型,如 string、boolean 等
  • match、unmatch - 匹配字段名
  • path_match、path_unmatch

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#数字字符串被映射成 text,日期字符串被映射成日期
PUT ttemplate/_doc/1
{
"someNumber":"1",
"someDate":"2019/01/01"
}
GET ttemplate/_mapping

#Create a default template
PUT _template/template_default
{
"index_patterns": ["*"],
"order" : 0,
"version": 1,
"settings": {
"number_of_shards": 1,
"number_of_replicas":1
}
}

PUT /_template/template_test
{
"index_patterns" : ["test*"],
"order" : 1,
"settings" : {
"number_of_shards": 1,
"number_of_replicas" : 2
},
"mappings" : {
"date_detection": false,
"numeric_detection": true
}
}

#查看 template 信息
GET /_template/template_default
GET /_template/temp*

#写入新的数据,index 以 test 开头
PUT testtemplate/_doc/1
{
"someNumber":"1",
"someDate":"2019/01/01"
}
GET testtemplate/_mapping
get testtemplate/_settings

PUT testmy
{
"settings":{
"number_of_replicas":5
}
}

put testmy/_doc/1
{
"key":"value"
}

get testmy/_settings
DELETE testmy
DELETE /_template/template_default
DELETE /_template/template_test

#Dynaminc Mapping 根据类型和字段名
DELETE my_index

PUT my_index/_doc/1
{
"firstName":"Ruan",
"isVIP":"true"
}

GET my_index/_mapping
DELETE my_index
PUT my_index
{
"mappings": {
"dynamic_templates": [
{
"strings_as_boolean": {
"match_mapping_type": "string",
"match":"is*",
"mapping": {
"type": "boolean"
}
}
},
{
"strings_as_keywords": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword"
}
}
}
]
}
}

DELETE my_index
#结合路径
PUT my_index
{
"mappings": {
"dynamic_templates": [
{
"full_name": {
"path_match": "name.*",
"path_unmatch": "*.middle",
"mapping": {
"type": "text",
"copy_to": "full_name"
}
}
}
]
}
}

PUT my_index/_doc/1
{
"name": {
"first": "John",
"middle": "Winston",
"last": "Lennon"
}
}

GET my_index/_search?q=full_name:John

Elasticsearch 聚合分析简介

聚合分类:

  • Bucket - 一些字段满足特定条件的文档的集合(分组)
  • Metric - 一些数学运算,可以对文档字段进行统计分析
  • Pipeline - 对其他的聚合结果进行二次聚合
  • Matrix - 支持对多个字段的操作并提供一个结果矩阵

聚合支持嵌套

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 按照目的地进行分桶统计
GET kibana_sample_data_flights/_search
{
"size": 0,
"aggs":{
"flight_dest":{
"terms":{
"field":"DestCountry"
}
}
}
}

#查看航班目的地的统计信息,增加平均,最高最低价格
GET kibana_sample_data_flights/_search
{
"size": 0,
"aggs":{
"flight_dest":{
"terms":{
"field":"DestCountry"
},
"aggs":{
"avg_price":{
"avg":{
"field":"AvgTicketPrice"
}
},
"max_price":{
"max":{
"field":"AvgTicketPrice"
}
},
"min_price":{
"min":{
"field":"AvgTicketPrice"
}
}
}
}
}
}

#价格统计信息 + 天气信息
GET kibana_sample_data_flights/_search
{
"size": 0,
"aggs": {
"flight_dest": {
"terms": {
"field": "DestCountry"
},
"aggs": {
"stats_price": {
"stats": {
"field": "AvgTicketPrice"
}
},
"weather": {
"terms": {
"field": "DestWeather",
"size": 5
}
}
}
}
}
}

参考资料

《Elasticsearch 实战》笔记

第 1 章 Elasticsearch 介绍

  • Elasticsearch 是构建在 Apache Lucene 基础之上的开源分布式搜索引擎。
  • Elasticsearch 常见的用法是索引大规模的数据,这样可以运行全文搜索和实时数据统计。
  • Elasticsearch 提供的特性远远超越了全文搜索。例如,可以调优搜索相关性并提供搜索建议。
  • 对于数据的索引和搜索,以及集群配置的管理,都可以使用 HTTP API 的 JSON,并获得 JSON 应答。
  • 可以将 Elasticsearch 当作一个 NoSQL 的数据存储,包括了实时性搜索和分析能力。它是面向文档的,默认情况下就是可扩展的。
  • Elasticsearch 自动将数据划分为分片,在集群中的服务器上做负载均衡。这使得动态添加和移除服务器变得很容易。分片也可以复制,使得集群具有容错性。

第 2 章 深入功能

  • Elasticsearch 默认是面向文档的、可扩展的且没有固定模式(schema)的。
  • 尽管可以使用默认设置来组建集群,但在继续前行之前至少应该调整一些配置。例如, 集群的名称和堆的大小。
  • 写请求在主分片中分发,然后复制到这些主分片的副本分片。
  • 搜索在多组完整数据上轮询执行,每组数据由主分片或副本分片组成。接收搜索请求的节点将来自多份分片的部分结果进行聚合,然后将综合的结果返 回给应用程序。
  • 可以通过 HTTP 请求的 JSON 有效载荷,发送新的文档和搜索参数,然后获取 JSON 应答。

第 3 章 索引、更新和删除数据

  • 映射定义了文档中的字段,以及这些字段是如何被索引的。Elasticsearch 是无模式的,是因为映射是自动扩展的。不过在实际生产中,需要经常控制哪些被 索引,哪些被存储,以及如何存储。
  • 文档中的多数字段是核心类型,如字符串和数值。这些字段的索引方式对于 Elasticsearch 的表现以及搜索结果的相关性有着很大的影响。
  • 单一字段也可以包含多个字段或取值。我们了解了数组和多字段,它们让你在单一字段中拥有同一核心类型的多个实例。
  • 除了用于文档的字段,Elasticsearch 还提供了预定义的字段,如 _source_all。配置这些字段将修改某些你并没有显式提供给文档的数据,但是对于性能和功能都有很大影响。例如,可以决定哪些字段需要在 _all里索引。
  • 由于 Elasticsearch 在 Lucene 分段里存储数据,而分段一旦创建就不会修改,因此更新文档意味着检索现存的文档,将修改放入即将索引的新文档中,然后删除旧的索引。
  • 当 Lucene 分段异步合并时,就会移除待删的文档。这也是为什么删除整个索引要比删除单个或多个文档要快——索引删除只是意味着移除磁盘上的文件,而且无须合并。
  • 在索引、更新和删除过程中,可以使用文档版本来管理并发问题。对于更新而言,如果因为并发问题而导致更新失败了,可以告诉 Elasticsearch 自动重试。

第 4 章 搜索数据

  • 人类语言类型的查询,如 match 和 query_string 查询,对于搜索框而言是非常合适的。
  • match 查询对于全文搜索而言是核心类型,但是 query_string 查询更为灵活,也更为复杂,因为它暴露了全部的 Lucene 查询语法。
  • match 查询有多个子类型:boolean、phrase 和 phrase_prefix。主要的区别在于 boolean 匹配单独的关键词,而 phrase 考虑了多个单词在词组里的顺序。
  • 像 prefix 和 wildcard 这样的特殊查询,Elasticsearch 也是支持的。
  • 要过滤某个字段不存在的文档,请使用 missing 过滤器。
  • exists 过滤器恰恰相反,它只返回拥有指定字段值的文档。

第 5 章 分析数据

  • 分析是通过文档字段的文本,生成分词的过程。在 match 查询这样的查询中,搜索字符串会经过同样的过程,如果一篇文档的分词和搜索字符串的分词相匹配,那么它就会和搜索匹配。
  • 通过映射,每个字段都会分配一个分析器。分析器既可以在 Elasticsearch 配置或索引设置中定义,也可以是一个默认的分析器。
  • 分析器是处理的链条,由一个分词器以及若干在此分析器之前的字符过滤器、在此分词器之后的分词过滤器组成。
  • 在字符串传送到分词器之前,字符过滤器用于处理这些字符串。例如,可以使用映射字符过滤器将字符“&”转化为“and”。
  • 分词器用于将字符串切分为多个分词。例如,空白分词器将使用空格来划分单词。
  • 分词过滤器用于处理分词器所产生的分词。例如,可以使用词干提取来将单词缩减为其词根,并让搜索在该词的复数和单数形式上都可以正常运作。
  • N 元语法分词过滤器使用单词的部分来产生分词。例如,可以让每两个连续的字符生成一个分词。如果希望即使搜索字符串包含错误拼写,搜索还能奏效,那么这个就很有帮助了。
  • 侧边 N 元语法就像 N 元语法一样,但是它们只从单词的头部或结尾开始。例如,对于 “event”可以获得 e、ev 和 eve 分词。
  • 在词组级别,滑动窗口分词过滤器和 N 元语法分词过滤器相似。例如,可以使用词组里每两个连续的单词来生成分词。当用户希望提升多词匹配的相关性时,例如,在产品的简短描述中,这一点就很有帮助。

第 6 章 使用相关性进行搜索

  • 词条的频率和词条出现在文档中的次数被用于计算查询词条的得分。
  • Elasticsearch 有很多工具来定制和修改得分。
  • 重新计算部分文档的得分将会减小评分机制的影响。
  • 使用解释 API 接口来理解文档是如何被评分的。
  • Function_score 查询使用户拥有了对文档得分最终极的控制权。
  • 理解字段数据缓存,将有助于用户理解 Elasticsearch 集群是如何使用内存的。
  • 如果字段数据缓存消耗了过多的内存,可以使用像 doc_values 这样的替换方案。

第 7 章 使用聚集来探索数据

  • 通过结果文档的词条计数和统计值计算,聚集帮助用户获得查询结果的概览。
  • 聚集是 Elasticsearch 中一种新形式的切面,拥有更多类型,还可以将它们组合以获取对数据更深入的理解。
  • 主要有两种类型的聚集:桶型和度量型。
  • 度量型聚集计算一组文档上的统计值,如某个数值型字段的最小值、最大值或者平均值。
  • 某些度量型聚集通过近似算法来计算,这使得它们具有比精确聚集更好的扩展性。百分位 percentiles 和 cardinality 聚集就是如此。
  • 桶型聚集将文档放入 1 个或多个桶中,并为这些桶返回计数器。例如,某个论坛中最流行的帖子。用户可以在桶型聚集中嵌入子聚集,对于父聚集所产生的每个桶一次性地运行子聚集。比如,对于匹配每个标签的博客帖,可以使用嵌套来获得该结果集的平均评论数。
  • top_hits 聚集可以用作一种子聚集,来实现结果的分组。
  • terms 聚集通常用于发现活跃的用户、常见地址、热门的物品等场景。其他的多桶型聚集是 terms 聚集的变体,如 signif icant_terms 聚集,返回了相对于整体索引而言,查询结果集中经常出现的词。
  • range 和 date_range 聚集用于对数值和日期数据的分类。而 histogram 和 date_histogram 聚集是类似的,不过它们使用固定的间距而不是人工定义的范围。
  • 单桶型聚集,如 global, filter, filters 和 missing 聚集,可以修改用于其他聚集运行的文档集合,因为默认情况下文档集合是由查询所确定的。

第 8 章 文档间的关系

  • 对象映射,对于一对一关系最有用。
  • 嵌套文档和父子结构,处理了一对多的关系。
  • 反规范化和应用端的连接,对于多对多的关系而言最有帮助。

即使是在本地进行,连接操作仍然损害了性能。所以,通常情况下将尽量多的属性放入单个 文档是个好主意。对象映射能起到作用是因为它允许文档中存在层级结构。这里搜索和聚集就像在扁平结构的文档上一样运作。需要使用全路径来指向字段,就像 location.name。

  • 嵌套文档通常是在索引的时候进行连接,将多个 Lucene 文档放入单个分块。对于应用,分块看上去就像一篇单独的 Elasticsearch 文档。
  • _parent 字段允许你在同一索引中将一篇文档指向其父辈,也就是另一篇不同类型的文档。Elasticsearch 将使用路由来确保父辈和子辈存储在同一分片上,这样查询的时候只需 要进行本地连接。

可以使用下面的查询和过滤器来搜索嵌套和父子文档。

  • nested 查询和过滤器。
  • has_child 查询和过滤器。
  • has_parent 查询和过滤器。

第 9 章 向外扩展

  • Elasticsearch 集群是如何组建的、是如何由多个节点构成的,每个节点包含了多个索引,而每个索引又是由多个分片组成。
  • 当节点加入 Elasticsearch 集群的时候会发生什么。
  • 主节点是如何选举的。
  • 删除和停用节点。
  • 使用 __cat API 接口来了解你的集群。
  • 什么是过度分片,以及如何使用它来规划集群的未来成长。
  • 如何使用别名和路由来提升集群的灵活性和可扩展性。

第 10 章 提升性能

  • 使用 bulk 批量 API 接口将多个 index、create、update 或者 delete 操作合并到同一个请求。
  • 为了组合多个 get 或多个 search 请求,你分别可以使用多条获取或多条搜索的 API。
  • 当索引缓冲区已满、事务日志过大或上次冲刷过去太久的时候,冲刷的操作将内存中的 Lucene 分段提交到磁盘。
  • 刷新使得新的分段,无论是否冲刷,都可以用于搜索。在索引操作密集的时候,最好降低刷新的频率或者干脆关闭刷新。
  • 合并的策略可以根据分段的多少来调优。较少的分段使得搜索更快,不过合并需要花费更多的 CPU 时间。较多的分段使得合并时间更少、索引更快,但是会导致搜索变慢。
  • 优化的操作会强制合并,对于处理很多搜索请求的静态索引,这可以很好的运作。
  • 存储限流可能会使合并落后于更新,限制了索引的性能。如果你的高速的 I/O, 放宽或者取消这个限制。
  • 组合使用在 bool 过滤中的位集合过滤器和 and/or/not 过滤中的非位集合过滤器。
  • 如果你的索引是静态不变的,在分片查询缓存中缓存数量和聚集。
  • 监控 JVM 堆的使用情况,预留足够的内存空间,这样就不会遇到频繁的垃圾回收或者 OOM 错误,但是同时也要给操作系统的缓存留出一些内存。
  • 如果首次查询太慢,而且你也不介意较慢的索引过程,请使用索引预热器。
  • 如果有足够的空间存储较大的索引,请使用 N 元语法和滑动窗口而不是模糊、通配或词组查询,这会使你的搜索运行得更快。
  • 在索引之前,使用所需的数据在文档中创建新的字段,这样通常可以避免使用脚本。
  • 合适的时候,在脚本中尝试使用 Lucene 表达式、词条统计和字段数据。
  • 如果脚本不会经常变化,请参考附录 B, 学习如何在 Elasticsearch 插件中书写一个本地脚本。
  • 如果在多个分片之中没有均衡的文档频率,使用 dfs_query_then_fetcho
  • 如果不需要任何命中的文档,请使用 count 搜索类型。如果需要很多命中的文档,请使用 scan 搜索类型。

第 11 章 管理集群

  • 默认映射为索引中重复的相似映射创建提供了便利。
  • 别名允许使用单个名字查询多个索引,因此让你可以按需分割数据。
  • 集群健康的 API 提供了一个简单的方式来测量集群、节点和分片的整体健康状态。
  • 使用慢索引和慢查询的日志,有助于诊断可能影响集群性能的索引和查询操作。
  • 充分理解 JVM 虚拟机、Lucene 和 Elasticsearch 是如何分配和使用内存的,可以预防操作 系统将进程交换到磁盘。
  • 快照 API 为使用网络存储的集群提供了便捷的备份和恢复方法,资料库插件将这个功能 扩展到了共用的云服务之上。

参考资料

《极客时间教程 - MongoDB 高手课》笔记一

第一章:MongoDB 再入门

MongoDB 简介

什么是 MongoDB?

一个以 JSON 为数据模型的文档数据库。

为什么叫文档数据库?

文档来自于“JSON Document”,并非我们一般理解的 PDF,WORD 文档。

谁开发 MongDB?

上市公司 MongoDB Inc. ,总部位于美国纽约。

主要用途

  • 应用数据库,类似于 Oracle, MySQL
  • 海量数据处理,数据平台。

主要特点

  • 建模为可选

  • JSON 数据模型比较适合开发者

  • 横向扩展可以支撑很大数据量和并发

MongoDB 是免费的吗?

MongoDB 有两个发布版本:社区版和企业版。

  • 社区版是基于 SSPL,一种和 AGPL 基本类似的开源协议 。
  • 企业版是基于商业协议,需付费使用。

MongoDB vs. RDBMS

MongoDB RDBMS
数据模型 文档模型 关系模型
数据库类型 OLTP OLTP
CRUD 操作 MQL/SQL SQL
高可用 复制集 集群模式
横向扩展能力 通过原生分片完善支持 数据分区或者应用侵入式
索引支持 B-树、全文索引、地理位置索引、多键 (multikey) 索引、TTL 索引 B 树
开发难度 容易 困难
数据容量 没有理论上限 千万、亿
扩展方式 垂直扩展+水平扩展 垂直扩展

MongoDB 特色及优势

文档模型的面向对象特点

灵活:快速响应业务变化

  • 多形性:同一个集合中可以包含 不同字段(类型)的文档对象
  • 动态性:线上修改数据模式,修 改是应用与数据库均无须下线
  • 数据治理:支持使用 JSON Schema 来规范数据模式。在保证模式灵活动态的前提下,提供数据治理能力文档模型的快速开发特点

快速:最简单快速的开发方式

  • 数据库引擎只需要在一个存储区读写
  • 反范式、无关联的组织极大优化查询速度
  • 程序 API 自然,开发快速

MongoDB 优势

  • 原生的高可用和横向扩展能力
    • Replica Set – 2 to 50 个成员
    • 自恢复
    • 多中心容灾能力
    • 滚动服务 – 最小化服务终端
  • 横向扩展能力
    • 需要的时候无缝扩展
    • 应用全透明
    • 多种数据分布策略
    • 轻松支持 TB – PB 数量级

MongoDB 技术优势总结

  • JSON 结构和对象模型接近,开发代码量低
  • JSON 的动态模型意味着更容易响应新的业务需求
  • 复制集提供 99.999% 高可用
  • 分片架构支持海量数据和无缝扩容

MongoDB 基本操作

使用 insert 完成插入操作

操作格式:

1
2
db.<集合>.insertOne(<JSON 对象>)
db.<集合>.insertMany([<JSON 1>, <JSON 2>, …<JSON n>])

示例:

1
2
3
4
5
6
db.fruit.insertOne({name: "apple"})
db.fruit.insertMany([
{name: "apple"},
{name: "pear"},
{name: "orange"}
])

使用 find 查询文档

find 是 MongoDB 中查询数据的基本指令,相当于 SQL 中的 SELECT 。

find 返回的是游标。

示例:

1
2
3
4
5
6
7
8
9
db.movies.find( { "year" : 1975 } ) //单条件查询

db.movies.find( { "year" : 1989, "title" : "Batman" } ) //多条件 and 查询

db.movies.find( { $and : [ {"title" : "Batman"}, { "category" : "action" }] } ) // and 的另一种形式

db.movies.find( { $or: [{"year" : 1989}, {"title" : "Batman"}] } ) //多条件 or 查询

db.movies.find( { "title" : /^B/} ) //按正则表达式查找
查询条件对照表
SQL MQL
a = 1 {a: 1}
a <> 1 {a: {$ne: 1}}
a > 1 {a: {$gt: 1}}
a >= 1 {a: {$gte: 1}}
a < 1 {a: {$lt: 1}}
a <= 1 {a: {$lte: 1}}
查询逻辑对照表
SQL MQL
a = 1 AND b = 1 {a: 1, b: 1}{$and: [{a: 1}, {b: 1}]}
a = 1 OR b = 1 {$or: [{a: 1}, {b: 1}]}
a IS NULL {a: {$exists: false}}
a IN (1, 2, 3) {a: {$in: [1, 2, 3]}}
查询逻辑运算符
  • $lt - 存在并小于
  • $lte - 存在并小于等于
  • $gt - 存在并大于
  • $gte - 存在并大于等于
  • $ne - 不存在或存在但不等于
  • $in - 存在并在指定数组中
  • $nin - 不存在或不在指定数组中
  • $or - 匹配两个或多个条件中的一个
  • $and - 匹配全部条件

使用 find 搜索子文档

find 支持使用“field.sub_field”的形式查询子文档。假设有一个文档:

1
2
3
4
5
6
7
db.fruit.insertOne({
name: "apple",
from: {
country: "China",
province: "Guangdon"
}
})
1
2
db.fruit.find( { "from.country" : "China" } )
db.fruit.find( { "from" : {country: "China"} } )

使用 find 搜索数组

find 支持对数组中的元素进行搜索。

1
2
3
4
5
6
7
db.fruit.insert([
{ "name" : "Apple", color: ["red", "green" ] },
{ "name" : "Mango", color: ["yellow", "green"] }
])

db.fruit.find({color: "red"})
db.fruit.find({$or: [{color: "red"}, {color: "yellow"}]} )

使用 find 搜索数组中的对象

1
2
3
4
5
6
7
8
9
10
db.movies.insertOne( {
"title" : "Raiders of the Lost Ark",
"filming_locations" : [
{ "city" : "Los Angeles", "state" : "CA", "country" : "USA" },
{ "city" : "Rome", "state" : "Lazio", "country" : "Italy" },
{ "city" : "Florence", "state" : "SC", "country" : "USA" }
]
})
// 查找城市是 Rome 的记录
db.movies.find({"filming_locations.city": "Rome"})

在数组中搜索子对象的多个字段时,如果使用 $elemMatch,它表示必须是同一个 子对象满足多个条件。考虑以下两个查询:

1
2
3
4
5
6
7
8
9
10
db.getCollection('movies').find({
"filming_locations.city": "Rome",
"filming_locations.country": "USA"
})

db.getCollection('movies').find({
"filming_locations": {
$elemMatch:{"city":"Rome", "country": "USA"}
}
})

控制 find 返回的字段

  • find 可以指定只返回指定的字段;
  • _id字段必须明确指明不返回,否则默认返回;
  • 在 MongoDB 中我们称这为投影(projection);
  • db.movies.find({"category": "action"},{"_id":0, title:1})

使用 remove 删除文档

remove 命令需要配合查询条件使用;

匹配查询条件的的文档会被删除;

指定一个空文档条件会删除所有文档;

示例:

1
2
3
4
db.testcol.remove( { a : 1 } ) // 删除 a 等于 1 的记录
db.testcol.remove( { a : { $lt : 5 } } ) // 删除 a 小于 5 的记录
db.testcol.remove( { } ) // 删除所有记录
db.testcol.remove() //报错

使用 update 更新文档

Update 操作执行格式:db.<集合>.update(<查询条件>, <更新字段>)

示例:

1
2
3
4
5
6
7
db.fruit.insertMany([
{name: "apple"},
{name: "pear"},
{name: "orange"}
])

db.fruit.updateOne({name: "apple"}, {$set: {from: "China"}})

使用 updateOne 表示无论条件匹配多少条记录,始终只更新第一条;

使用 updateMany 表示条件匹配多少条就更新多少条;

updateOne/updateMany 方法要求更新条件部分必须具有以下之一,否则将报错:

  • $set/$unset

  • $push/$pushAll/$pop

  • $pull/$pullAll

  • $addToSet

使用 update 更新数组

  • $push: 增加一个对象到数组底部
  • $pushAll: 增加多个对象到数组底部
  • $pop: 从数组底部删除一个对象
  • $pull: 如果匹配指定的值,从数组中删除相应的对象
  • $pullAll: 如果匹配任意的值,从数据中删除相应的对象
  • $addToSet: 如果不存在则增加一个值到数组

使用 drop 删除集合

使用 db.<集合>.drop() 来删除一个集合

集合中的全部文档都会被删除

集合相关的索引也会被删除

1
db.collection.drop()

使用 dropDatabase 删除数据库

使用 db.dropDatabase() 来删除数据库

数据库相应文件也会被删除,磁盘空间将被释放

1
2
3
4
use tempDB
db.dropDatabase()
show collections // No collections
show dbs // The db is gone

聚合查詢

什么是 MongoDB 聚合框架

MongoDB 聚合框架(Aggregation Framework)是一个计算框架,它可以:

  • 作用在一个或几个集合上;

  • 对集合中的数据进行的一系列运算;

  • 将这些数据转化为期望的形式;

从效果而言,聚合框架相当于 SQL 查询中的:

  • GROUP BY

  • LEFT OUTER JOIN

  • AS 等

管道(Pipeline)和步骤(Stage)

整个聚合运算过程称为管道(Pipeline),它是由多个步骤(Stage)组成的, 每个管道:

  • 接受一系列文档(原始数据);
  • 每个步骤对这些文档进行一系列运算;
  • 结果文档输出给下一个步骤;

聚合计算的基本格式:

1
2
3
4
5
6
pipeline = [$stage1, $stage2, ...$stageN];

db.<COLLECTION>.aggregate(
pipeline,
{ options }
);

常见步骤:

步骤 作用 SQL 等价运算符
$match 过滤 WHERE
$project 投影 AS
$sort 排序 ORDER BY
$group 分组 GROUP BY
$skip / $limit 结果限制 SKIP / LIMIT
$lookup 左外连接 LEFT OUTER JOIN
$unwind 展开数组 N/A
$graphLookup 图搜索 N/A
$facet / $bucket 分面搜索 N/A

常见步骤中的运算符

$match $project $group
$eq/$gt/$gte/$lt/$lte
$and/$or/$not/$in
$geoWithin/$intersect
选择需要的或排除不需要的字段
$map/$reduce/$filter
$range
$multiply/$divide/$substract/$add
$year/$month/$dayOfMonth/$hour/$minute/$second
……
$sum/$avg
$push/$addToSet
$first/$last/$max/$min
……

聚合运算的使用场景

聚合查询可以用于 OLAP 和 OLTP 场景。例如:

  • OTLP - 计算
  • OLAP
    • 分析一段时间内的销售总额、均值
    • 计算一段时间内的净利润
    • 分析购买人的年龄分布
    • 分析学生成绩分布
    • 统计员工绩效

MQL 常用步骤与 SQL 对比

【示例一】

1
2
3
4
5
6
7
SELECT
FIRST_NAME AS `名`,
LAST_NAME AS `姓`
FROM Users
WHERE GENDER = '男'
SKIP 100
LIMIT 20

等价于

1
2
3
4
5
6
7
8
9
db.users.aggregate([
{$match: {gender: ’’男”}},
{$skip: 100},
{$limit: 20},
{$project: {
'名': '$first_name',
'姓': '$last_name'
}}
]);

【示例二】

1
2
3
4
5
6
SELECT DEPARTMENT,
COUNT(NULL) AS EMP_QTY
FROM Users
WHERE GENDER = '女'
GROUP BY DEPARTMENT HAVING
COUNT(*) < 10

等价于

1
2
3
4
5
6
7
8
db.users.aggregate([
{$match: {gender: '女'}},
{$group: {
_id: '$DEPARTMENT’,
emp_qty: {$sum: 1}
}},
{$match: {emp_qty: {$lt: 10}}}
]);

【示例三】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> db.students.findOne()
{
name:'张三',
score:[
{subject:'语文',score:84},
{subject:'数学',score:90},
{subject:'外语',score:69}
]
}

> db.students.aggregate([{$unwind: '$score'}])
{name: '张三', score: {subject: '语文', score: 84}}
{name: '张三', score: {subject: '数学', score: 90}}
{name: '张三', score: {subject: '外语', score: 69}}

MQL 特有步骤 $bucket

1
2
3
4
5
6
7
8
db.products.aggregate([{
$bucket:{
groupBy: "$price",
boundaries: [0,10,20,30,40],
default: "Other",
output:{"count":{$sum:1}}
}
}])

MQL 特有步骤 $facet

1
2
3
4
5
6
7
8
9
10
db.products.aggregate([{
$facet:{
price:{
$bucket:{}
},
year:{
$bucket:{}
}
}
}])

聚合查询实验

计算到目前为止的所有订单的总销售额

1
2
3
4
5
6
7
8
9
10
db.orders.aggregate([
{ $group:
{
_id: null,
total: { $sum: "$total" }
}
}
])

// 结果: // { "_id" : null, "total" : NumberDecimal("44019609") }

查询 2019 年第一季度(1 月 1 日~3 月 31 日)已完成订单(completed)的订单总金额和订单总数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
db.orders.aggregate([

// 步骤 1:匹配条件
{ $match: { status: "completed", orderDate: {
$gte: ISODate("2019-01-01"),
$lt: ISODate("2019-04-01") } } },

// 步骤二:聚合订单总金额、总运费、总数量
{ $group: {
_id: null,
total: { $sum: "$total" },
shippingFee: { $sum: "$shippingFee" },
count: { $sum: 1 } } },
{ $project: {
// 计算总金额
grandTotal: { $add: ["$total", "$shippingFee"] },
count: 1,
_id: 0 } }
])

// 结果:
// { "count" : 5875, "grandTotal" : NumberDecimal("2636376.00") }

复制集机制及原理

复制集的作用

MongoDB 复制集的主要意义在于实现服务高可用

它的现实依赖于两个方面的功能:

  • 数据写入时将数据迅速复制到另一个独立节点上
  • 在接受写入的节点发生故障时自动选举出一个新的替代节点

在实现高可用的同时,复制集实现了其他几个附加作用:

  • 数据分发:将数据从一个区域复制到另一个区域,减少另一个区域的读延迟

  • 读写分离:不同类型的压力分别在不同的节点上执行

  • 异地容灾:在数据中心故障时候快速切换到异地

典型复制集结构

一个典型的复制集由 3 个以上具有投票权的节点组成,包括:

  • 一个主节点(PRIMARY):接受写入操作和选举时投票
  • 两个(或多个)从节点(SECONDARY):复制主节点上的新数据和选举时投票
  • 不推荐使用 Arbiter(投票节点)

数据是如何复制的?

当一个修改操作,无论是插入、更新或删除,到达主节点时,它对数据的操作将被 记录下来(经过一些必要的转换),这些记录称为 oplog。

从节点通过在主节点上打开一个 tailable 游标不断获取新进入主节点的 oplog,并 在自己的数据上回放,以此保持跟主节点的数据一致。

通过选举完成故障恢复

  • 具有投票权的节点之间两两互相发送心跳;

  • 当 5 次心跳未收到时判断为节点失联;

  • 如果失联的是主节点,从节点会发起选举,选出新的主节点;

  • 如果失联的是从节点则不会产生新的选举;

  • 选举基于 RAFT 一致性算法 实现,选举成功的必要条件是大多数投票节点存活;

  • 复制集中最多可以有 50 个节点,但具有投票权的节点最多 7 个。

影响选举的因素

整个集群必须有大多数节点(N / 2 + 1)存活;

被选举为主节点的节点必须:

  • 能够与多数节点建立连接
  • 具有较新的 oplog
  • 具有较高的优先级(如果有配置)

常见选项

复制集节点有以下常见的选配项:

  • 是否具有投票权(v 参数):有则参与投票;
  • 优先级(priority 参数):优先级越高的节点越优先成为主节点。优先级为 0 的节点无法成 为主节点;
  • 隐藏(hidden 参数):复制数据,但对应用不可见。隐藏节点可以具有投票仅,但优先 级必须为 0;
  • 延迟(slaveDelay 参数):复制 n 秒之前的数据,保持与主节点的时间差。

复制集注意事项

关于硬件:

  • 因为正常的复制集节点都有可能成为主节点,它们的地位是一样的,因此硬件配置上必须一致;
  • 为了保证节点不会同时宕机,各节点使用的硬件必须具有独立性。

关于软件:

  • 复制集各节点软件版本必须一致,以避免出现不可预知的问题。

增加节点不会增加系统写性能!

MongoDB 全家桶

软件模块 描述
mongod MongoDB 数据库软件
mongo MongoDB 命令行工具,管理 MongoDB 数据库
mongos MongoDB 路由进程,分片环境下使用
mongodump / mongorestore 命令行数据库备份与恢复工具
mongoexport / mongoimport CSV/JSON 导入与导出,主要用于不同系统间数据迁移
Compass MongoDB GUI 管理工具
Ops Manager(企业版) MongoDB 集群管理软件
BI Connector(企业版) SQL 解释器 / BI 套接件
MongoDB Charts(企业版) MongoDB 可视化软件
Atlas(付费及免费) MongoDB 云托管服务,包括永久免费云数据库

第二章:从熟练到精通的开发之路

模型设计基础

数据模型

什么是数据模型?

数据模型是一组由符号、文本组成的集合,用以准确表达信息,达到有效交流、沟通 的目的。

数据模型设计的元素

实体 Entity

  • 描述业务的主要数据集合

  • 谁,什么,何时,何地,为何,如何

属性 Attribute

  • 描述实体里面的单个信息

关系 Relationship

  • 描述实体与实体之间的数据规则

  • 结构规则:1-N, N-1, N-N

  • 引用规则:电话号码不能单独存在

数据模型的三层深度:

  • 概念模型,逻辑模型,物理模型
  • 一个模型逐步细化的过程

MongoDB 文档模型设计的三个误区

  1. 不需要模型设计
  2. MongoDB 应该用一个超级大文档来组织所有数据
  3. MongoDB 不支持关联或者事务

关于 JSON 文档模型设计

文档模型设计处于是物理模型设计阶段 (PDM)

JSON 文档模型通过内嵌数组或引用字段来表示关系

文档模型设计不遵从第三范式,允许冗余。

为什么人们都说 MongoDB 是无模式?

MongoDB 同样需要概念/逻辑建模

文档模型设计的物理层结构可以和逻辑层类似

MongoDB 无模式由来: 可以省略物理建模的具体过程

关系模型 vs 文档模型

关系数据库 JSON 文档模型
模型设计层次 概念模型
逻辑模型
物理模型
概念模型
逻辑模型
模型实体 集合
模型属性 字段
模型关系 关联关系,主外键 内嵌数组,引用字段

文档模型设计之一:基础设计

建立基础文档模型

  • 根据概念模型或者业务需求推导出逻辑模型 – 找到对象
  • 列出实体之间的关系(及基数) - 明确关系
  • 套用逻辑设计原则来决定内嵌方式 – 进行建模
  • 完成基础模型构建

基础建模小结

  • 90:10 规则: 大部分时候你会使用内嵌来表示 1-1,1-N,N-N
  • 内嵌类似于预先聚合(关联)
  • 内嵌后对读操作通常有优势(减少关联)

文档模型设计之二:工况细化

场景梳理:

  • 最频繁的数据查询模式

  • 最常用的查询参数

  • 最频繁的数据写入模式

  • 读写操作的比例

  • 数据量的大小

基于内嵌的文档模型

根据业务需求

  • 使用引用来避免性能瓶颈
  • 使用冗余来优化访问性能

什么时候该使用引用方式?

  • 内嵌文档太大,数 MB 或者超过 16MB
  • 内嵌文档或数组元素会频繁修改
  • 内嵌数组元素会持续增长并且没有封顶

MongoDB 引用设计的限制

  • MongoDB 对使用引用的集合之间并无主外键检查
  • MongoDB 使用聚合框架的 $lookup 来模仿关联查询
  • $lookup 只支持 left outer join
  • $lookup 的关联目标(from)不能是分片表

文档模型设计之三:模式套用

  • 利用文档内嵌数组,将一个时间段的数据聚合到一个文档里。

  • 大量减少文档数量

  • 大量减少索引占用空间

设计模式集锦

大文档,很多字段,很多索引

列转行

模型灵活了,如何管理文档不同版本?

  • 增加一个版本号字段
  • 快速过滤掉不需要升级的文档
  • 升级时候对不同版本的文档做 不同的处理

统计网页点击流量

  • 用近似计算

业绩排名,游戏排名,商品统计等精确统计

  • 用预聚合字段
  • 模型中直接增加统计字段
  • 每次更新数据时候同时更新统计值

事务开发:写操作事务

writeConcern 决定一个写操作落到多少个节点上才算成功。writeConcern 的取值包括:

  • 0:发起写操作,不关心是否成功;

  • 1~集群最大数据节点数:写操作需要被复制到指定节点数才算成功;

  • majority:写操作需要被复制到大多数节点上才算成功。

发起写操作的程序将阻塞到写操作到达指定的节点数为止。

writeConcern 可以决定写操作到达多少个节点才算成功,journal 则定义如何才算成 功。取值包括:

  • true: 写操作落到 journal 文件中才算成功;
  • false: 写操作到达内存即算作成功。

事务开发:读操作事务之一

在读取数据的过程中我们需要关注以下两个问题:

从哪里读?——由 readPreference 来解决

什么样的数据可以读? ——由 readConcern 来解决

什么是 readPreference?

readPreference 决定使用哪一个节点来满足 正在发起的读请求。可选值包括:

  • primary: 只选择主节点;
  • primaryPreferred:优先选择主节点,如 果不可用则选择从节点;
  • secondary:只选择从节点;
  • secondaryPreferred:优先选择从节点, 如果从节点不可用则选择主节点;
  • nearest:选择最近的节点;

readPreference 场景举例

用户下订单后马上将用户转到订单详情页——primary/primaryPreferred。因为此时从节点可能还没复制到新订单;

用户查询自己下过的订单——secondary/secondaryPreferred。查询历史订单对时效性通常没有太高要求;

生成报表——secondary。报表对时效性要求不高,但资源需求大,可以在从节点单独处理,避免对线上用户造成影响;

将用户上传的图片分发到全世界,让各地用户能够就近读取——nearest。每个地区的应用选择最近的节点读取数据。

readPreference 与 Tag

readPreference 只能控制使用一类节点。Tag 则可以将节点选择控制
到一个或几个节点。考虑以下场景:

  • 一个 5 个节点的复制集;

  • 3 个节点硬件较好,专用于服务线上客户;

  • 2 个节点硬件较差,专用于生成报表;

可以使用 Tag 来达到这样的控制目的:

  • 为 3 个较好的节点打上 {purpose: “online”};

  • 为 2 个较差的节点打上 {purpose: “analyse”};

  • 在线应用读取时指定 online,报表读取时指定 reporting。

注意事项

  • 指定 readPreference 时也应注意高可用问题。例如将 readPreference 指定 primary,则发生故障转移不存在 primary 期间将没有节点可读。如果业务允许,则应选择 primaryPreferred;

  • 使用 Tag 时也会遇到同样的问题,如果只有一个节点拥有一个特定 Tag,则在这个节点失效时将无节点可读。这在有时候是期望的结果,有时候不是。例如:

    • 如果报表使用的节点失效,即使不生成报表,通常也不希望将报表负载转移到其他节点上,此时只有一个节点有报表 Tag 是合理的选择;
    • 如果线上节点失效,通常希望有替代节点,所以应该保持多个节点有同样的 Tag;
  • Tag 有时需要与优先级、选举权综合考虑。例如做报表的节点通常不会希望它成为主节点,则优先级应为 0。

事务开发:读操作事务之二

什么是 readConcern?

在 readPreference 选择了指定的节点后,readConcern 决定这个节点上的数据哪些 是可读的,类似于关系数据库的隔离级别。可选值包括:

  • available:读取所有可用的数据;
  • local:读取所有可用且属于当前分片的数据,默认设置;
  • majority:读取在大多数节点上提交完成的数据;
  • linearizable:可线性化读取文档;增强处理 majority 情况下主节点失联时候的例外情况
  • snapshot:读取最近快照中的数据;最高隔离级别,接近于 Seriazable

readConcern: local 和 available

在复制集中 local 和 available 是没有区别的。两者的区别主要体现在分片集上。考虑以下场景:

  • 一个 chunk x 正在从 shard1 向 shard2 迁移;

  • 整个迁移过程中 chunk x 中的部分数据会在 shard1 和 shard2 中同时存在,但源分片 shard1 仍然是 chunk x 的负责方:

    • 所有对 chunk x 的读写操作仍然进入 shard1;
    • config 中记录的信息 chunk x 仍然属于 shard1;
  • 此时如果读 shard2,则会体现出 local 和 available 的区别:

    • local:只取应该由 shard2 负责的数据(不包括 x);
    • available:shard2 上有什么就读什么(包括 x);

注意事项:

  • 虽然看上去总是应该选择 local,但毕竟对结果集进行过滤会造成额外消耗。在一些 无关紧要的场景(例如统计)下,也可以考虑 available;
  • MongoDB <=3.6 不支持对从节点使用 {readConcern: “local”};
  • 从主节点读取数据时默认 readConcern 是 local,从从节点读取数据时默认 readConcern 是 available(向前兼容原因)。

readConcern: majority

只读取大多数据节点上都提交了的数据。考虑如 下场景:

  • 集合中原有文档 {x: 0};
  • 将 x 值更新为 1;

如果在各节点上应用 {readConcern: “majority”} 来读取数据:

如何实现?

节点上维护多个 x 版本,MVCC 机制 MongoDB 通过维护多个快照来链接不同的版本:

  • 每个被大多数节点确认过的版本都将是一个快照;
  • 快照持续到没有人使用为止才被删除;

readConcern: majority 与脏读

MongoDB 中的回滚:

  • 写操作到达大多数节点之前都是不安全的,一旦主节点崩溃,而从节还没复制到该次操作,刚才的写操作就丢失了;

  • 把一次写操作视为一个事务,从事务的角度,可以认为事务被回滚了。

所以从分布式系统的角度来看,事务的提交被提升到了分布式集群的多个节点级别的“提交”,而不再是单个节点上的“提交”。

在可能发生回滚的前提下考虑脏读问题:

  • 如果在一次写操作到达大多数节点前读取了这个写操作,然后因为系统故障该操作回滚了,则发生了脏读问题;

使用 {readConcern: “majority”} 可以有效避免脏读

readConcern: 如何实现安全的读写分离

考虑如下场景:

向主节点写入一条数据;

立即从从节点读取这条数据。

如何保证自己能够读到刚刚写入的数据?

下述方式有可能读不到刚写入的订单

1
2
db.orders.insert({ oid: 101, sku: ”kite", q: 1})
db.orders.find({oid:101}).readPref("secondary")

使用 writeConcern + readConcern majority 来解决

1
2
db.orders.insert({ oid: 101, sku: "kiteboar", q: 1}, {writeConcern:{w: "majority”}})
db.orders.find({oid:101}).readPref(“secondary”).readConcern("majority")

readConcern: linearizable

只读取大多数节点确认过的数据。和 majority 最大差别是保证绝对的操作线性顺序 –在写操作自然时间后面的发生的读,一定可以读到之前的写

  • 只对读取单个文档时有效;
  • 可能导致非常慢的读,因此总是建议配合使用 maxTimeMS;

readConcern: snapshot

{readConcern: “snapshot”} 只在多文档事务中生效。将一个事务的 readConcern 设置为 snapshot,将保证在事务中的读:

  • 不出现脏读;

  • 不出现不可重复读;

  • 不出现幻读。

因为所有的读都将使用同一个快照,直到事务提交为止该快照才被释放。

事务开发:多文档事务

MongoDB 虽然已经在 4.2 开始全面支持了多文档事务,但并不代表大家应该毫无节制 地使用它。相反,对事务的使用原则应该是:能不用尽量不用。

通过合理地设计文档模型,可以规避绝大部分使用事务的必要性

为什么?事务 = 锁,节点协调,额外开销,性能影响

MongoDB ACID 多文档事务支持

  • Atomocity 原子性
    • 单表单文档 : 1.x 就支持
    • 复制集多表多行:4.0 复制集
    • 分片集群多表多行 4.2
  • Consistency 一致性 - writeConcern, readConcern (3.2)
  • Isolation 隔离性 - readConcern (3.2)
  • Durability 持久性 - Journal and Replication

事务的隔离级别

事务完成前,事务外的操作对该事务所做的修改不可访问

如果事务内使用 {readConcern: “snapshot”},则可以达到可重复读 Repeatable Read

事务写机制

MongoDB 的事务错误处理机制不同于关系数据库:

  • 当一个事务开始后,如果事务要修改的文档在事务外部被修改过,则事务修改这个文档时会触发 Abort 错误,因为此时的修改冲突了;

  • 这种情况下,只需要简单地重做事务就可以了;

  • 如果一个事务已经开始修改一个文档,在事务以外尝试修改同一个文档,则事务以外的修改会等待事务完成才能继续进行(write-wait.md 实验)。

Change Stream

Change Stream 是 MongoDB 用于实现变更追踪的解决方案,类似于关系数据库的触 发器,但原理不完全相同:

Change Stream 触发器
触发方式 异步 同步(事务保证)
触发位置 应用回调事件 数据库触发器
触发次数 每个订阅事件的客户端 1 次(触发器)
故障恢复 从上次断点重新触发 事务回滚

Change Stream 的实现原理

Change Stream 是基于 oplog 实现的。它在 oplog 上开启一个 tailable cursor 来追踪所有复制集上的变更操作,最终调用应用中定义的回调函数。

被追踪的变更事件主要包括:

  • insert/update/delete:插入、更新、删除;

  • drop:集合被删除;

  • rename:集合被重命名;

  • dropDatabase:数据库被删除;

  • invalidate:drop/rename/dropDatabase 将导致 invalidate 被触发,并关闭 change stream;

Change Stream 只推送已经在大多数节点上提交的变更操作。即“可重复读”的变更。 这个验证是通过 {readConcern: “majority”} 实现的。因此:

  • 未开启 majority readConcern 的集群无法使用 Change Stream;
  • 当集群无法满足 {w: “majority”} 时,不会触发 Change Stream(例如 PSA 架构 中的 S 因故障宕机)。

Change Stream 使用场景

  • 跨集群的变更复制——在源集群中订阅 Change Stream,一旦得到任何变更立即写入目标集群。

  • 微服务联动——当一个微服务变更数据库时,其他微服务得到通知并做出相应的变更。

  • 其他任何需要系统联动的场景。

注意事项

  • Change Stream 依赖于 oplog,因此中断时间不可超过 oplog 回收的最大时间窗;

  • 在执行 update 操作时,如果只更新了部分数据,那么 Change Stream 通知的也
    是增量部分;

  • 同理,删除数据时通知的仅是删除数据的 _id

参考资料

《极客时间教程 - MongoDB 高手课》笔记二

第三章:分片集群与高级运维之道

分片集群机制及原理

MongoDB 常见部署架构

TODO: 补图

为什么要使用分片集群?

——分而治之

分片如何解决?

TODO:补图

分片组件:

  • 路由节点(mongos) - 提供集群单一入口转发应用端请求选择合适数据节点进行读写合并多个数据节点的返回。无状态,建议至少 2 个。
  • 配置节点(config) - 提供集群元数据存储分片数据分布的映射。
  • 数据节点(shard) - 以复制集为单位水平扩展,最大 1024 分片。分片之间数据不重复所有分片在一起才可完整工作

分片特点:

  • 应用全透明,无特殊处理
  • 数据自动均衡
  • 动态扩容,无须下线
  • 提供三种分片方式
    • 基于范围
    • 优点:范围查询性能好
    • 缺点:数据分布不均;容易出现热点问题
    • 基于 Hash
    • 优点:数据分布均匀
    • 缺点:范围查询效率低
    • 基于 zone / tag

分片集群设计

分片的基本标准:

  • 关于数据:数据量不超过 3TB,尽可能保持在 2TB 一个片;
  • 关于索引:常用索引必须容纳进内存;

按照以上标准初步确定分片后,还需要考虑业务压力,随着压力增大,CPU、RAM、磁盘中的任何一项出现瓶颈时,都可以通过添加更多分片来解决。

合理的架构–需要多少个分片

  • A = 所需存储总量 / 单服务器可挂载容量。如:8TB / 2TB = 4
  • B = 工作集大小 / 单服务器内存容量。如:400GB / (256G * 0.6)= 3
  • C = 并发量总数 / (单服务器并发量 * 0.7)。如:30000 / (9000*0.7) = 6
  • 分片数量= max(A, B, C) = 6

关键概念

  • 片键 shard key:文档中的一个字段
  • 文档 doc :包含 shard key 的一行数据
  • 块 Chunk :包含 n 个文档
  • 分片 Shard:包含 n 个 chunk
  • 集群 Cluster:包含 n 个分片

选择合适片键

  • 取值基数(Cardinality) - 取值基数要大,因为备选值有限,不利于水平扩展
  • 取值分布 - 应尽可能均匀,以避免热点问题
  • 分散写,集中读
  • 被尽可能多的业务场景用到
  • 避免单调递增或递减的片键

足够的资源

mongos 与 config 通常消耗很少的资源,可以选择低规格虚拟机;

资源的重点在于 shard 服务器:

  • 需要足以容纳热数据索引的内存;
  • 正确创建索引后 CPU 通常不会成为瓶颈,除非涉及非常多的计算;
  • 磁盘尽量选用 SSD;

实验:分片集群搭建及扩容(略)

MongoDB 监控最佳实践

常用的监控工具及手段

  • MongoDB Ops Manager
  • Percona
  • 通用监控平台
  • 程序脚本

监控信息的来源:

  • db.serverStatus()(主要)
  • db.isMaster()(次要)
  • mongostats 命令行工具(只有部分信息)

注意:db.serverStatus() 包含的监控信息是从上次开机到现在为止的累计数据,因此不能简单使用。

serverStatus() 主要信息

  • connections: 关于连接数的信息;
  • locks: 关于 MongoDB 使用的锁情况;
  • network: 网络使用情况统计;
  • opcounters: CRUD 的执行次数统计;
  • repl: 复制集配置信息;
  • wiredTiger: 包含大量 WirdTiger 执行情况的信息:
    • block-manager: WT 数据块的读写情况;
    • session: session 使用数量;
    • concurrentTransactions: Ticket 使用情况;
  • mem: 内存使用情况;
  • metrics: 一系列性能指标统计信息;

监控报警的考量

  • 具备一定的容错机制以减少误报的发生;
  • 总结应用各指标峰值;
  • 适时调整报警阈值;
  • 留出足够的处理时间;

建议监控指标

指标 意义 获取
opcounters(操作计数器) 查询、更新、插入、删除、getmore 和其他命令的的数量。 db.serverStatus().opcounters
tickets(令牌) 对 WiredTiger 存储引擎的读/写令牌数量。令牌数量表示了可以进入存储引擎的并发操作数量。 db.serverStatus().wiredTiger.concurrentTransactions
replication lag(复制延迟) 这个指标代表了写操作到达从结点所需要的最小时间。过高的 replication lag 会减小从结点的价值并且不利于配置了写关 db.adminCommand({'replSetGetStatus': 1})
oplog window(复制时间窗) 这个指标代表 oplog 可以容纳多长时间的写操作。它表示了一个从结点可以离线多长时间仍能够追上主节点。通常建议该值应大于 24 小时为佳。 db.oplog.rs.find().sort({$natural: -1}).limit(1).next().ts -db.oplog.rs.find().sort({$natural: 1}).limit(1).next().ts
connections(连接数) 连接数应作为监控指标的一部分,因为每个连接都将消耗资源。应该计算低峰/正常/高峰时间的连接数,并制定合理的报警阈值范围。 db.serverStatus().connections
Query targeting(查询专注度) 索引键/文档扫描数量比返回的文档数量,按秒平均。如果该值比较高表示查询系需要进行很多低效的扫描来满足查询。这个情况通常代表了索引不当或缺少索引来支持查询。 var status = db.serverStatus()status.metrics.queryExecutor.scanned / status.metrics.document.returnedstatus.metrics.queryExecutor.scannedObjects / status.metrics.document.returned
Scan and Order(扫描和排序) 每秒内内存排序操作所占的平均比例。内存排序可能会十分昂贵,因为它们通常要求缓冲大量数据。如果有适当索引的情况下,内存排序是可以避免的。 var status = db.serverStatus()status.metrics.operation.scanAndOrder / status.opcounters.query
节点状态 每个节点的运行状态。如果节点状态不是 PRIMARY、SECONDARY、ARBITER 中的一个,或无法执行上述命令则报警 db.runCommand("isMaster")
dataSize(数据大小) 整个实例数据总量(压缩前) 每个 DB 执行 db.stats();
StorageSize(磁盘空间大小) 已使用的磁盘空间占总空间的百分比。

MongoDB 备份与恢复

MongoDB 的备份机制分为:

  • 延迟节点备份
  • 全量备份 + Oplog 增量

最常见的全量备份方式包括:

  • mongodump;
  • 复制数据文件;
  • 文件系统快照;

方案一:延迟节点备份

安全范围内的任意时间点状态 = 延迟从节点当前状态 + 定量重放 oplog

主节点的 oplog 时间窗 t 应满足:t >= 延迟时间 + 48 小时

方案二:全量备份加 oplog

  • 最近的 oplog 已经在 oplog.rs 集合中,因此可以在定期从集合中导出便得到了 oplog;
  • 如果主节点上的 oplog.rs 集合足够大,全量备份足够密集,自然也可以不用备份 oplog;
  • 只要有覆盖整个时间段的 oplog,就可以结合全量备份得到任意时间点的备份。

全量备份(mongodump、复制数据文件、文件系统快照) + oplog = 任意时间点备份恢复 (PIT)

复制文件全量备份注意事项

  • 必须先关闭节点才能复制,否则复制到的文件无效;
  • 也可以选择 db.fsyncLock() 锁定节点,但完成后不要忘记 db.fsyncUnlock() 解锁;
  • 可以且应该在从节点上完成;
  • 该方法实际上会暂时宕机一个从节点,所以整个过程中应注意投票节点总数。

全量备份加 oplog 注意事项–文件系统快照

  • MongoDB 支持使用文件系统快照直接获取数据文件在某一时刻的镜像;
  • 快照过程中可以不用停机;
  • 数据文件和 Journal 必须在同一个卷上;
  • 快照完成后请尽快复制文件并删除快照;

Mongodump 全量备份注意事项

  • 使用 mongodump 备份最灵活,但速度上也是最慢的;
  • mongodump 出来的数据不能表示某个个时间点,只是某个时间段

备份与恢复操作(略)

MongoDB 安全架构(略)

MongoDB 安全加固实践(略)

MongoDB 索引机制(一)

MongoDB 索引数据结构为 B-树。

B-树:基于 B 树,但是子节点数量可以超过 2 个。

MongoDB 索引类型

  • 单键索引
  • 组合索引
  • 多值索引
  • 地理位置索引
  • 全文索引
  • TTL 索引
  • 部分索引
  • 哈希索引

组合索引的最佳方式:ESR 原则

  • 精确(Equal)匹配的字段放最前面
  • 排序(Sort)条件放中间
  • 范围(Range)匹配的字段放最后面

MongoDB 索引机制(二)

MongoDB 读写性能机制

客户端请求流程图

TODO:补图

应用端-选择节点

对于复制集读操作,选择哪个节点是由 readPreference 决定的:

  • primary/primaryPreferred
  • secondary/secondaryPreferred
  • nearest

如果不希望一个远距离节点被选择,应做到以下之一

  • 将它设置为隐藏节点;
  • 通过标签(Tag)控制可选的节点;
  • 使用 nearest 方式;

数据库端-执行请求(读)

不能命中索引的搜索和内存排序是导致性能问题的最主要原因

数据库端-执行请求(写)

数据库端-合并结果

  • 如果顺序不重要则不要排序
  • 尽可能使用带片键的查询条件以减少参与查询的分片数

性能诊断工具

mongostat: 用于了解 MongoDB 运行状态的工具

mongotop: 用于了解集合压力状态的工具

mongod 日志:日志中会记录执行超过 100ms 的查询及其执行计划

高级集群设计:两地三中心

容灾级别

  • 无备源中心 - 没有灾难恢复能力,只在本地进行数据备份。
  • 本地备份+异地保存 - 本地将关键数据备份,然后送到异地保存。灾难发生后,按预定数据恢复程序恢复系统和数据。
  • 双中心主备模式 - 在异地建立一个热备份点,通过网络进行数据备份。当出现灾难时,备份站点接替主站点的业务,维护业务连续性。
  • 双中心双活 - 在相隔较远的地方分别建立两个数据中心,进行相互数据备份。当某个数据中心发生灾难时,另一个数据中心接替其工作任务。
  • 双中心双活 +异地热备 =两地三中心 - 在同城分别建立两个数据中心,进行相互数据备份。当该城市的 2 个中心同时不可用(地震/大面积停电/网络等),快速切换到异地

网络层解决方案

TODO:补图

应用层解决方案

  • 负载均衡、虚拟 IP
  • 分布式 Session
  • 使用同一套数据

数据跨中心同步

  • DBMS 跨机房基于日志同步
  • 文件系统跨机房基于存储镜像同步

多数据中心要点:

  • 正常运行状态 - 集群内一个主节点接受写,其他节点只读。
  • 主节点故障 - 主数据中心内自动切主切换时间 5-10 秒
  • 主数据中心对外网络故障或者整个数据中心不可用,主数据中心主节点自动降级。从节点升级为主节点选举时间 5-30 秒。
  • 双中心双活,分流模式 -需要跨中心写数据,同城双中心需要低延迟专线。
  • 节点数量建议要 5 个,2+2+1 模式
  • 主数据中心的两个节点要设置高一点的优先级,减少跨中心换主节点
  • 同城双中心之间的网络要保证低延迟和频宽,满足 writeConcern: Majority 的双中心写需求
  • 使用 Retryable Writes and Retryable Reads 来保证零下线时间
  • 用户需要自行处理好业务层的双中心切换

实验:搭建两地三中心集群(略)

高级集群设计:全球多写(略)

MongoDB 上线及升级(略)

第四章:企业架构师进阶之法(略)

参考资料

《高性能 MySQL》笔记

部分章节内容更偏向于 DBA 的工作,在实际的开发工作中相关性较少,直接略过。

第一章 MySQL 架构与历史

MySQL 逻辑架构

MySQL 逻辑架构分为三层:

  • 连接层 - 连接管理、认证管理
  • 核心服务层 - 缓存、解析、优化、执行
  • 存储引擎层 - 数据实际读写

并发控制

解决并发问题的最常见方式是加锁。

  • 排它锁(exclusive lock) - 也叫写锁(write lock)。锁一次只能被一个线程所持有

  • 共享锁(shared lock) - 也叫读锁(read lock)。锁可被多个线程所持有

加锁、解锁,检查锁是否已释放,都需要消耗资源,因此锁定的粒度越小,并发度越高。

MySQL 中支持多种锁粒度:

  • 表级锁(table lock) - 锁定整张表,会阻塞其他用户对该表的读写操作。
  • 行级锁(row lock) - 可以最大程度的支持并发处理。

事务

事务就是一组原子性的 SQL 查询。事务内的语句,要么全部执行成功,要么全部执行失败。

ACID

ACID 是数据库事务正确执行的四个基本要素。

  • **原子性 (Atomicity)**:一个事务被视为不可分割的最小工作单元,一个事务的所有操作要么全部提交成功,要么全部失败回滚。
  • **一致性 (Consistency)**:数据库总是从一个一致的状态到另一个一致的状态。事务没有提交,事务的修改就不会保存到数据库中。
  • **隔离性 (isolation)**:通常来说,一个事务所作的操作在最终提交之前,对其他事务来说是不可见的。
  • **持久性 (durability)**:一旦事务提交,则其所作的修改就会永久的保存到数据库中。

事务隔离级别

SQL 标准提出了四种“事务隔离级别”。事务隔离级别等级越高,越能保证数据的一致性和完整性,但是执行效率也越低。因此,设置数据库的事务隔离级别时需要做一下权衡。

事务隔离级别从低到高分别是:

  • “读未提交(read uncommitted)” - 是指,事务中的修改,即使没有提交,对其它事务也是可见的
    • 读未提交存在脏读问题。“脏读(dirty read)”是指当前事务可以读取其他事务未提交的数据。
  • “读已提交(read committed)” ** - 是指,事务提交后,其他事务才能看到它的修改**。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。
    • 读已提交解决了脏读的问题
    • 读已提交存在不可重复读问题。“不可重复读(non-repeatable read)”是指一个事务内多次读取同一数据,过程中,该数据被其他事务所修改,导致当前事务多次读取的数据可能不一致。
    • 读已提交是大多数数据库的默认事务隔离级别,如 Oracle。
  • “可重复读(repeatable read)” - 是指:保证在同一个事务中多次读取同样数据的结果是一样的
    • 可重复读解决了不可重复读问题
    • 可重复读存在幻读问题。“幻读(phantom read)”是指一个事务内多次读取同一范围的数据过程中,其他事务在该数据范围新增了数据,导致当前事务未发现新增数据。
    • 可重复读是 InnoDB 存储引擎的默认事务隔离级别
  • 串行化(serializable ) - 是指,强制事务串行执行,对读取的每一行数据都加锁,一旦出现锁冲突,必须等前面的事务释放锁。
    • 串行化解决了幻读问题。由于强制事务串行执行,自然避免了所有的并发问题。
    • 串行化策略会在读取的每一行数据上都加锁,这可能导致大量的超时和锁竞争。这对于高并发应用基本上是不可接受的,所以一般不会采用这个级别。

事务隔离级别对并发一致性问题的解决情况:

隔离级别 丢失修改 脏读 不可重复读 幻读
读未提交 ✔️️️
读已提交 ✔️️️ ✔️️️
可重复读 ✔️️️ ✔️️️ ✔️️️
可串行化 ✔️️️ ✔️️️ ✔️️️ ✔️️️

死锁

死锁是指两个或多个事务竞争同一资源,从而导致恶性循环的现象。多个事务视图以不同顺序锁定资源时,就可能会产生死锁;多个事务同时锁定同一资源时,也会产生死锁。

InnoDB 目前处理死锁的方法是将持有最少行级锁的事务进行回滚

事务日志

InnoDB 通过事务日志记录修改操作。事务日志的写入采用追加方式,因此是顺序 I/O,比随机 I/O 快很多。

事务日志持久化后,内存中被修改的数据由后台程序慢慢刷回磁盘,这称为预写日志(Write Ahead Logging,WAL)

如果数据修改以及记录到事务日志并持久化,此时系统崩溃,存储引擎可以在系统重启之后自动恢复数据。

MySQL 中的事务

MySQL 提供了两种事务存储引擎:InnoDB 和 NDB CLuster。

MySQL 默认采用自动提交模式(AUTOCOMMIT)。即如果不显式的声明一个事务,MySQL 会把每一个查询都当作一个事务来操作。

可以通过设置 AUTOCOMMIT 来启用或禁用自动提交模式。

可以通过执行 SET TRANSACTION ISOLATION LEVEL 来设置事务隔离级别。

InnoDB 采用两阶段锁定协议,在事务执行过程中,随时都可以执行锁定,锁只有在执行 COMMIT 或者 ROLLBACK 时才会释放,并且所有的锁都在一瞬间释放。

InnoDB 也支持通过特定语句显示加锁:

1
2
3
4
5
// 先在表上加上 IS 锁,然后对读取的记录加 S 锁
select ... lock in share mode;

// 当前读:先在表上加上 IX 锁,然后对读取的记录加 X 锁
select ... for update;

多版本并发控制

可以将 MVCC 视为行级锁的一个变种,它在很多情况下避免了加锁,因此开销更低。

MVCC 是通过保存数据在某个时刻的快照来实现的。也就是说,不管执行多久,每个事务看到的数据是一致的。根据事务开始时间不同, 每个事务对同一张表,同一时刻看到的数据可能是不一样的。

不同存储引擎实现 MVCC 的方式有所不同,典型的有乐观并发控制和悲观并发控制。

InnoDB 的 MVCC 是通过在每行记录后面保存两个隐藏列来实现。一个列保存了行的创建时间,一个是保存了过期时间。当然存储的不是实际的时间,而是系统版本号(system version number),每开始一个新事务,系统版本号都会自动递增。事务开始时刻的系统版本号作为事务的版本号,用来和查询到的每行记录的版本号作比较。

  • Select - InnoDB 会根据这两个条件来查询:
    • 只查找版本号小于或者等于当前事务的数据行,这样可以保证事务读取到的数据要么是在事务开始前就存在的,要么是自己插入或者修改的。
    • 行的删除版本要么未定义,要么大于当前事务的版本号,这样可以保证读取到的数据在事务开始之前没有被删除。
  • Insert - InnoDB 为新插入的每一行数据保存当前的系统版本号为行版本号。
  • Delete - InnoDB 为删除的每一行保存当前的版本号为行删除标识。
  • Update - InnoDB 为插入一条新纪录,保存当前系统版本号为行版本号,同时保存当前系统的版本号到原来的行为行删除标识。

MVCC 只在可重复读和读已提交两个隔离级别下工作。

MySQL 的存储引擎

Mysql 将每个数据库保存为数据目录下的一个子目录。建表时,MySQL 会在数据库子目录下创建一个和表同名的 .frm 文件保存表的定义。因为 MySQL 使用文件系统的目录和文件来保存数据库和表的定义,大小写敏感性和具体的平台密切相关:在 Windows 中,大小写不敏感;在 Linux 中,大小写敏感。

Mysql 常见存储引擎

  • InnoDB - 默认事务引擎。
  • MyISAM - Mysql 5.1 及之前的默认引擎。
  • Archive
  • Memory
  • NDB

第二章 MySQL 基准测试(略)

第三章 服务器性能剖析(略)

第四章 Schema 与数据类型优化

数据类型

整数类型

整数类型有可选的 UNSIGNED 属性,标识不允许负值,大致可以使正数的上限提高一倍。

类型 大小 作用
TINYINT 1 字节 小整数值
SMALLINT 2 字节 大整数值
MEDIUMINT 3 字节 大整数值
INT 4 字节 大整数值
BIGINT 8 字节 极大整数值

浮点数类型

FLOATDOUBLE 分别使用 4 个字节、8 个字节存储空间,它们支持使用标准的浮点运算进行近似计算,存在丢失精度的可能。

DECIMAL 类型用于存储精确的小数,支持精确计算,但是计算代价高。只有在需要对小数进行精确计算时,才应该使用 DECIMAL,例如财务数据。此外,当数据量较大时,可以考虑使用 BIGINT 代替 DECIMAL,将需要存储的货币单位乘以需要精确的倍数即可。

类型 大小 用途
FLOAT 4 字节 单精度浮点数值
DOUBLE 8 字节 双精度浮点数值
DECIMAL 精确的小数值

字符串类型

VARCHAR 类型用于存储可变长字符串。

CHAR 类型是定长字符串。

与 CHAR 和 VARCHAR 类似的类型还有 BINARY 和 VARBINARY,它们存储的是二进制字符串。

类型 大小 用途
CHAR 0-255 字节 定长字符串
VARCHAR 0-65535 字节 变长字符串

BLOB 和 TEXT

BLOBTEXT 都用于存储很大的数据,分别采用二进制和字符串方式存储。

类型 大小 用途
TINYBLOB 0-255 字节 不超过 255 个字符的二进制字符串
TINYTEXT 0-255 字节 短文本字符串
BLOB 0-65 535 字节 二进制形式的长文本数据
TEXT 0-65 535 字节 长文本数据
MEDIUMBLOB 0-16 777 215 字节 二进制形式的中等长度文本数据
MEDIUMTEXT 0-16 777 215 字节 中等长度文本数据
LONGBLOB 0-4 294 967 295 字节 二进制形式的极大文本数据
LONGTEXT 0-4 294 967 295 字节 极大文本数据

日期和时间类型

类型 大小 格式 作用 备注
DATE 3 字节 YYYY-MM-DD 日期值
TIME 3 字节 HH:MM:SS 时间值或持续时间
YEAR 1 字节 YYYY 年份值
DATETIME 8 字节 YYYY-MM-DD hh:mm:ss 混合日期和时间值 有效时间范围为 1000-01-01 00:00:00 到 9999-12-31 23:59:59
TIMESTAMP 4 字节 YYYY-MM-DD hh:mm:ss 混合日期和时间值,时间戳 有效时间范围为 1970-01-01 00:00:01 到 2038-01-19 03:14:07

特殊类型

  • ENUM - 枚举类型,用于存储单一值,可以选择一个预定义的集合。
  • SET - 集合类型,用于存储多个值,可以选择多个预定义的集合。

Schema 设计简单规则

  • 尽量避免过度设计,例如会导致极其复杂查询的 schema 设计,或者有很多列的表设计。
  • 使用小而简单的合适数据类型,除非真实数据模型中有确切的需要,否则应该尽可能地避免使用 NULL 值。
  • 尽量使用相同的数据类型存储相似或相关的值,尤其是要在关联条件中使用的列。
  • 注意可变长字符串,其在临时表和排序时可能导致悲观的按最大长度分配内存。
  • 尽量使用整型定义标识列。
  • 避免使用 MySQL 已经遗弃的特性,例如制定浮点数的精度,或者整数的显示宽度。
  • 小心使用 ENUM 和 SET,虽然它们用起来很方便,但是不要滥用,否则有时候会变成陷阱,最好避免使用 BIT。

范式意味着不存储冗余数据,但往往需要多关联查询,增加了查询的复杂度;反范式意味着存储冗余数据,但是减少了关联查询。在实际应用中,范式和反范式应当混合使用。

ALTER TABLE 如果操作的是大表,需要耗费大量时间。一般的操作是:用新结构创建一张空表,从旧表查出所有数据插入新表,然后删除旧表。

有两种替代方案:

  • 在一台不提供服务的机器上执行 ALTER TABLE 操作,然后和提供服务的主库进行切换。
  • 影子拷贝:创建一张新表,然后通过重命名和删表操作交换两张表。

第五章 创建高性能的索引

索引是存储引擎用于快速找到记录的一种数据结构。

索引优化应该是对查询性能优化最有效的手段了。

索引基础

索引可以包含一个或多个列的值。如果索引包含多个列,那么列的顺序也十分重要,因为 MySQL 只能高效地使用索引的最左前缀列。

B-Tree 索引

大多数 MySQL 引擎都支持 B-Tree 索引。存储引擎以不同的方式使用 B-Tree 索引,性能也各有不同,各有优劣。例如,MyISAM 使用前缀压缩技术使得索引更小,但 InnoDB 则按照原数据格式进行存储。再如 MyISAM 索引通过数据的物理位置引用被索引的行,而 InnoDB 则根据主键引用被索引的行。

B-Tree 通常意味着所有的值都是按顺序存储的,并且每一个叶子页到根的距离相同。

B-Tree 索引从索引的根节点开始进行搜索。根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下层查找。通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值的上限和下限。最终存储引擎要么是找到对应的值,要么该记录不存在。

叶子节点比较特别,它们的指针指向的是被索引的数据,而不是其他的节点页。在根节点和叶子节点之间可能有很多层节点页。树的深度和表的大小直接相关。

B-Tree 对索引列是顺序组织存储的,所以很适合查找范围数据。

假设有如下数据表:

1
2
3
4
5
6
7
CREATE TABLE People(
last_name varchar(50) not null,
first_name varchar(50) not null,
dob date not null,
gender enum('m','f')not null,
key(last_name, first_name, dob)
);

对于表中的每一行数据,索引中包含了 last_name、 first_name 和 dob 列的值。

请注意,索引对多个值进行排序的依据是 CREATE TABLE 语句中定义索引时列的顺序。看一下最后两个条目,两个人的姓和名都-样,则根据他们的出生日期来排列顺序。

可以使用 B-Tree 索引的查询类型。B-Tree 索引适用于全键值、键值范围或键前缀查找。

其中键前缀查找只适用于根据最左前缀的查找生。前面所述的索引对如下类型的查询有效。

  • 全值匹配 - 全值匹配指的是和索引中的所有列进行匹配,例如前面提到的索引可用于查找姓名为 Cuba Allen、出生于 1960-01-01 的人。
  • 匹配最左前缀 - 前面提到的索引可用于查找所有姓为 Allen 的人,即只使用索引的第一列。
  • 匹配列前缀 - 也可以只匹配某–列的值的开头部分。例如前面提到的索引可用于查找所有以 J 开头的姓的人。这里也只使用了索引的第一列。
  • 匹配范围值 - 例如前面提到的索引可用于查找姓在 Allen 和 Barrymore 之间的人。这里也只使用了索引的第一列。
  • 精确匹配某一列并范围匹配另外一列 - 前面提到的索引也可用于查找所有姓为 Allen, 并且名字是字母 K 开头的人。即第一列 last_ name 全匹配,第二列 first_name 范围匹配。
  • 只访问索引的查询 - B-Tree 通常可以支持“只访问索引的查询”,即查询只需要访问索引,而无须访问数据行。也叫做覆盖索引。

因为索引树中的节点是有序的,所以除了按值查找外,索引还可以用于查询中的排序操作。

B-Tree 索引的限制:

  • 如果不是按照索引的最左列开始查找,则无法使用索引。例如上面例子中的索引无法用于查找名字为 Bill 的人,也无法查找某个特定生日的人,因为这两列都不是最左数据列。类似地,也无法查找姓氏以某个字母结尾的人。
  • 不能跳过索引中的列。也就是说,前面所述的索引无法用于查找姓为 Smith 并且在某个特定日期出生的人。如果不指定名 (first_name),则 MySQL 只能使用索引的第一列。
  • 如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找。例如有查询 WHERE last_name=' Smith' AND first_name LIKE 'J%' AND dob = '1976-12-23' ,这个查询只能使用索引的前两列,因为这里 LIKE 是一个范围条件(但是服务器可以把其余列用于其他目的)。如果范围查询列值的数量有限,那么可以通过使用多个等于条件来代替范围条件。

哈希索引

哈希索引 (hashindex) 基于哈希表实现,只有精确匹配索引所有列的查询才有效。

对于每一行数据,存储引擎都会对所有的索引列计算-一个哈希码 (hash code), 哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。

如果多个列的哈希值相同,索引会以链表的方式存放多个记录指针到同一个哈希条目中。

哈希索引的限制:

  • 哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能的影响并不明显。

  • 哈希索引数据并不是按照索引值顺序存储的,所以也就无法用于排序

  • 哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。例如,在数据列 (A,B) 上建立哈希索引,如果查询只有数据列 A, 则无法使用该索引。

  • 哈希索引只支持等值比较查询,包括 =IN()<=> (注意 <><=> 是不同的操作)。也不支持任何范围查询,例如 WHERE price > 100

  • 访问哈希索引的数据非常快,除非有很多哈希冲突(不同的索引列值却有相同的哈希值)。当出现哈希冲突的时候,存储引擎必须遍历链表中所有的行指针,逐行进行比较,直到找到所有符合条件的行。

  • 如果哈希冲突很多的话,一些索引维护操作的代价也会很高。例如,如果在某个选择性很低(哈希冲突很多)的列上建立哈希索引,那么当从表中删除一行时,存储引擎需要遍历对应哈希值的链表中的每一行,找到并删除对应行的引用,冲突越多,代价越大。

空间数据索引 (R-Tree)

MyISAM 表支持空间索引,可以用作地理数据存储。和 B-Tree 索引不同,这类索引无须前缀查询。空间索引会从所有维度来索引数据。

查询时,可以有效地使用任意维度来组合查询。必须使用 MySQL 的 GIS 相关函数如 MBRCONTAINS() 等来维护数据。MySQL 的 GIS 支持并不完善,所以大部分人都不会使用这个特性。开源关系数据库系统中对 GIS 的解决方案做得比较好的是 PostgreSQL 的 PostGIS

全文索引

全文索引是一种特殊类型的索引,它查找的是文本中的关键词,而不是直接比较索引中的值。

全文搜索和其他几类索引的匹配方式完全不一样。它有许多需要注意的细节,如停用词、词干和复数、布尔搜索等。

全文索引更类似于搜索引擎做的事情,而不是简单的 WHERE 条件匹配。

在相同的列上同时创建全文索引和基于值的 B-Tree 索引不会有冲突,全文索引适用于 MATCH AGAINST 操作,而不是普通的 WHERE 条件操作。

索引的优点

索引有以下优点:

  1. 索引大大减少了服务器需要扫描的数据量。
  2. 索引可以帮助服务器避免排序和临时表。
  3. 索引可以将随机 I/O 变为顺序 I/O。

索引是最好的解决方案吗?

  • 对于非常小的表,大部分情况下简单的全表扫描更高效。

  • 对于中到大型的表,索引就非常有效。

  • 但对于特大型的表,建立和使用索引的代价将随之增长。这种情况下,则需要一种技术可以直接区分出查询需要的一组数据,而不是一条记录一条记录地匹配。例如可以使用分区技术。

  • 如果表的数量特别多,可以建立一个元数据信息表,用来查询需要用到的某些特性。例如执行那些需要聚合多个应用分布在多个表的数据的查询,则需要记录。哪个用户的信息存储在哪个表中”的元数据,这样在查询时就可以直接忽略那些不包含指定用户信息的表。对于大型系统,这是一个常用的技巧。

高性能的索引策略

正确地创建和使用索引是实现高性能查询的基础。

独立的列

独立的列是指索引列不能是表达式的一部分,也不能是函数的参数。

下面两个例子都无法使用索引:

1
2
SELECT actor_ id FROM sakila.actor WHERE actor_id + 1 = 5;
SELECT ... WHERE TO_DAYS(CURRENT_DATE) - TO_ DAYS(date_col) <= 10;

前缀索引和索引选择性

有时候需要索引很长的字符列,这会让索引变得大且慢。一种策略是,可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率。但这样也会降低索引的选择性。索引的选择性是指,不重复的索引值和总记录数的比值。索引的选择性越高则查询效率越高。

对于 BLOB、TEXT 或者很长的 VARCHAR 类型的列,必须使用前缀索引,因为 MySQL 不允许索引这些列的完整长度

前缀应该足够长,以使得前缀索引的选择性接近于索引整个列。通常来说,选择性能够接近 0.03,基本上就可用了。

计算前缀索引选择性的示例

1
2
3
4
5
6
SELECT COUNT(DISTINCT LEFT (city, 3)) / COUNT(*) AS sel3,
COUNT(DISTINCT LEFT (city, 4)) / COUNT(*) AS sel4,
COUNT(DISTINCT LEFT (city, 5)) / COUNT(*) AS sel5,
COUNT(DISTINCT LEFT (city, 6)) / COUNT(*) AS se16,
COUNT(DISTINCT LEFT (city, 7)) / COUNT(*) AS sel7,
FROM sakila.city demo;

多列索引

在多个列上建立独立的单列索引大部分情况下并不能提高 MySQL 的查询性能。

例如,表 film_actor 在字段 film_id 和 actor_id 上各有一个单列索引。但对于下面这个查询 WHERE 条件,这两个单列索引都不是好的选择:

1
2
SELECT film_id, actor_id FROM sakila.film_actor
WHERE actor_id = 10 or film_id = 1;

选择合适的索引列顺序

正确的顺序依赖于使用该索引的查询,并且同时需要考虑如何更好地满足排序和分组的需要。

如何选择索引的列顺序:

  • 将选择性最高的列放到索引最前列。
  • 可能需要根据那些运行频率最高的查询来调整索引列的顺序,让这种情况下索引的选择性最高。
1
SELECT * FROM payment WHERE staff.id = 2 AND customer._id = 584;

是应该创建一个 (staffid, customer id) 索引还是应该颠倒一下顺序?

可以跑一些查询来确定在这个表中值的分布情况,并确定哪个列的选择性更高。

聚簇索引

聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。聚簇表示数据行和相邻的键值紧凑地存储在一起。因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引

具体的细节依赖于其实现方式,在 InnoDB 中,数据行实际上存放在索引的叶子页 (leaf page) 中。

聚簇索引的优点:

  • 可以把相关数据保存在一起,访问数据时,可以减少磁盘 I/O。
  • 数据访问更快。聚簇索引将索引和数据保存在同一个 B-Tree 中,因此从聚簇索引中获取数据通常比在非聚簇索引中查找要快。
  • 使用覆盖索引扫描的查询可以直接使用页节点中的主键值

聚簇索引的缺点:

  • 聚簇数据最大限度地提高了 I/O 密集型应用的性能,但如果数据全部都放在内存中,则访问的顺序就没那么重要了,聚簇索引也就没什么优势了。
  • 插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到 InnoDB 表中速度最快的方式。但如果不是按照主键顺序加载数据,那么在加载完成后最好使用 OPTIMIZE TABLE 命令重新组织一下表。
  • 更新聚簇索引列的代价很高,因为会强制 InnoDB 将每个被更新的行移动到新的位置。
  • 基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临页分裂 (page split) 的问题。当行的主键值要求必须将这一行插人到某个已满的页中时,存储引擎会将该页分裂成两个页面来容纳该行,这就是一次页分裂操作。页分裂会导致表占用更多的磁盘空间。
  • 聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候。
  • 二级索引 (非聚簇索引)可能比想象的要更大,因为在二级索引的叶子节点包含了引用行的主键列。
  • 二级索引访问需要两次索引查找,而不是一次。(回表)

InnoDB 和 MyISAM 的数据分布对比

MyISAM 存储引擎采用非聚簇索引存储数据,而 InnoDB 存储引擎采用聚簇索引存储数据。

来看下 MyISAM 和 InnoDB 是如何存储下面的表:

1
2
3
4
5
6
CREATE TABLE layout_test (
col1 int NOT NULL,
col2 int NOT NULL,
PRIMARY KEY(col1),
KEY(col2),
);

对于 MyISAM,其数据分布比较简单,按照数据插入的顺序存储在磁盘上。对于每一行数据,都是一个行号,从 0 开始递增。由于行是定长的,所以 MyISAM 可以从表的开头跳过所需的字节找到需要的行(有点类似于数组)。如下图:

MyISAM 使用主键索引查找数据时,在 B+Tree 的叶子节点除了存储索引键之外,还保存了每个键所处的行指针(可以理解为行号)。当找到某个索引键对应的行指针后,就能定位到它对应的数据。如下图:

对于 MyISAM 的二级索引,它的存储方式跟主键索引没有什么区别,如下图:

所以对于 MyISAM 来讲,主键索引和其它索引在存储结构上并没有什么区别。主键索引就是一个名为 PRIMARY 的惟一非空索引

对于 InnoDB 来讲,主键索引是聚簇的,也就是主键索引就是表,所以不像 MyISAM 那样需要独立的行存储。 聚簇索引的每个叶子节点都包含了主键值、事务 ID、用于事务和 MVCC 的回滚指针以及所有剩余列(这个例子中是 col2)。对于 InnoDB 的主键索引,数据分布如下图:

InnoDB 的二级索引和聚簇索引区别比较大,它的二级索引的叶子节点存储的不是”行指针”,而是主键值。存储主键值带来的好处是,InnoDB 在移动行时无须更新二级索引的这个指针。如下图:

由于 InnoDB 是通过主键聚集数据,所以使用 InnoDB 时,一定要指定主键,如果没有定义主键,InnoDB 会选择一个惟一的非空索引代替,如果没有这样的索引,InnoDB 会隐式定义一个主键来作为聚簇索引。

由于聚簇索引插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到 InnoDB 表中速度最快的方式,所以通常我们都使用一个递增 ID 作为主键。

最后,我们使用一个比较抽象的图,对比一下聚簇和非聚簇的数据分布:

覆盖索引

如果一个索引包含所有需要查询的字段的值,我们就称之为“ 覆盖索引”。覆盖索引能极大地提高性能。

  • 索引条目通常远小于数据行大小,所以如果只需要读取索引,那 MySQL 就会极大地减少数据访问量。
  • 因为索引是按照列值顺序存储的(至少在单个页内是如此),所以对于 I/O 密集型的范围查询会比随机从磁盘读取每一行数据的 I/O 要少得多。
  • 一些存储引擎如 MyISAM 在内存中只缓存索引,数据则依赖于操作系统来缓存,因此要访问数据需要一次系统调用。
  • InnoDB 的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询。

覆盖索引必须要存储索引列的值,而哈希索引、空间索引和全文索引等都不存储索引列的值,所以 MySQL 只能使用 B-Tree 索引做覆盖索引。

使用索引扫描来做排序

如果 EXPLAIN 出来的 type 列的值为 index, 则说明 MySQL 使用了索引扫描来做排序(不要和 Extra 列的 Using index 搞混淆了)。

MySQL 可以使用同一个索引既满足排序,又用于查找行。只有当索引的列顺序和 ORDER BY 子句的顺序完全一致,并且所有列的排序方向都一样时,MySQL 才能够使用索引来对结果做排序。

索引和锁

InnoDB 只有在访问行的时候才会对其加锁,而索引能够减少 InnoDB 访问的行数,从而减少锁的数量。

第六章 查询性能优化

为什么查询速度会慢

慢查询基础:优化数据访问

重构查询的方式

查询执行的基础

MySQL 查询优化器的局限性

查询优化器的提示(hint)

优化特定类型的查询

案例学习

第七章 MySQL 高级特性(略)

第八章 优化服务器设置(略)

第九章 操作系统和硬件优化(略)

第十章 复制

复杂概述

配置复制

复制的原理

复制拓扑

复制和容量规划

复制管理和维护

复制的问题和解决方案

复制有多快

MySQL 复制的高级特性

其他复制技术

第十一章 可扩展的 MySQL(略)

第十二章 高可用性(略)

第十三章 云端的 MySQL(略)

第十四章 应用层优化(略)

第十五章 备份与恢复(略)

第十六章 MySQL 用户工具(略)

参考资料

《MongoDB 权威指南》笔记二

第 5 章 索引

索引是用于提升查询效率的一种存储结构。

索引简介

在 MongoDB 中,不使用索引的查询称为集合扫描,这意味要扫描所有数据。

创建索引

1
db.users.createIndex({"username" : 1})

创建复合索引

如果查询中有多个排序方向或者查询条件中有多个键,那么复合索引会非常有用。

1
db.users.createIndex({"age" : 1, "username" : 1})

explain

何时不使用索引

索引类型

索引管理

第 6 章 特殊的索引和集合类型

地理空间索引

全文搜索索引

固定集合

TTL 索引

使用 GridFS 存储文件

第 7 章 聚合框架

管道、阶段和可调参数

阶段入门:常见操作

表达式

$project

$unwind

数组表达式

累加器

分组简介

将聚合管道结果写入集合中

第 8 章 事务

第 9 章 应用程序设计

第 10 章 创建副本集

如何设计副本集

在 MongoDB 中,副本集``是一组服务器,其中一个是用于处理写操作的主节点(primary),还有多个用于保存主节点的数据副本的从节点(secondary)。如果主节点崩溃了,则从节点会从其中选取出一个新的主节点。

副本集中的停止运行或不可用的节点数只要不到一半,副本集整体都是可以对外提供服务的。——这是为什么呢?

MongoDB 的故障转移能力基于 Raft 共识协议实现。该协议的核心在于,当主节点出现故障时,集群中半数以上的节点所认可的节点才能当选为新的主节点。假设一个集群中有 5 个节点,其中 3 个节点不可用,2 个节点可以正常工作。这 2 个节点不能达到副本集大多数的要求(至少需要 3 个节点),因此它们无法选举出一个主节点。如果这 2 个节点中有一个是主节点,那么当它注意到无法得到大多数节点支持时,就会从主节点上退位。几秒后,副本集中会包含 2 个从节点和 3 个无法访问的节点。

为什么剩下的 2 个节点不能选举出主节点?问题在于:其他 3 个节点实际上可能没有崩溃,而是因为网络故障而不可达。在这种情况下,左侧的 3 个节点将选举出一个主节点,因为它们可以达到副本集节点中的大多数(5 个节点中的 3 个)。在网络分区的情况下,我们不希望两边的网络各自选举出一个主节点,因为那样的话副本集就拥有 2 个主节点了。如果 2 个主节点都可以写入数据,那么整个副本集的数据就会发生混乱,这就是所谓的“脑裂”。

有两种推荐的集群模式:

  • 将“大多数”成员放在一个数据中心。如果有一个主数据中心,而且你希望副本集的主节点总是位于主数据中心,那么这是一个比较好的配置。只要主数据中心正常运转,就会有一个主节点。不过,如果主数据中心不可用了,那么备份数据中心的成员将无法选举出主节点。
  • 在两个数据中心各自放置数量相等的成员,在第三个地方放置一个用于打破僵局的副本集成员。如果两个数据中心“同等”重要,那么这种配置会比较好,因为任意一个数据中心的服务器都可以找到另一台服务器以达到“大多数”。不过,这样就需要将服务器分散到 3 个地方。

如何进行选举

当一个从节点无法与主节点连通时,它就会联系并请求其他的副本集成员将自己选举为主节点。其他成员会做几项健全性检查:它们能否连接到一个主节点,而这个主节点是发起选举的节点无法连接到的?这个发起选举的从节点是否有最新数据?有没有其他更高优先级的成员可以被选举为主节点?

MongoDB 在其 3.2 版本中引入了 Raft 共识协议。

副本集成员相互间每隔两秒发送一次心跳(heartbeat,也就是 ping)。如果某个成员在 10秒内没有反馈心跳,则其他成员会将该不良成员标记为无法访问。选举算法将尽“最大努力”尝试让具有最高优先权的从节点发起选举。成员优先权会影响选举的时机和结果。优先级高的从节点要比优先级低的从节点更快地发起选举,而且也更有可能成为主节点。然而,低优先级的从节点也可能短暂地被选为主节点,即使还存在一个可用的高优先级从节点。副本集成员会继续发起选举直到可用的最高优先级成员被选为主节点。

就所有能连接到的成员,被选为主节点的成员必须拥有最新的复制数据。严格地说,所有的操作都必须比任何一个成员的操作都要高,因此所有的操作都必须比任何一个成员的操作都要晚。

第 11 章 副本集的组成

同步

复制是指在多台服务器上保持相同的数据副本。MongoDB 实现此功能的方式是保存操作日志(oplog),其中包含了主节点执行的每一次写操作。oplog 是存在于主节点 local 数据库中的一个固定集合。从节点通过查询此集合以获取需要复制的操作。

每个从节点都维护着自己的 oplog,用来记录它从主节点复制的每个操作。

如果一个从节点由于某种原因而停止运行,那么当它重新启动后,就会从 oplog 中的最后一个操作开始同步。由于这些操作是先应用到数据上然后再写入 oplog,因此从节点可能会重复已经应用到其数据上的操作。MongoDB 在设计时就考虑到了这种情况:将 oplog 中的同一个操作执行多次与只执行一次效果是一样的。oplog 中的每个操作都是幂等的。也就是说,无论对目标数据集应用一次还是多次,oplog 操作都会产生相同的结果。

心跳

每个成员需要知道其他成员的状态:谁是主节点?谁可以作为同步源?谁停止运行了?为了维护副本集的最新视图,所有成员每隔两秒会向副本集的其他成员发送一个心跳请求。

心跳请求用于检查每个成员的状态。

心跳的一个最重要的功能是让主节点知道自己是否满足副本集“大多数”的条件。如果主节点不再得到“大多数”节点的支持,它就会降级,成为一个从节点。

选举

当一个成员无法访问到主节点时,便会申请选举。申请选举的成员会向其所能访问到的所有成员发出通知。如果这个成员不适合作为主节点,那么其他成员会知道原因:可能这个成员的数据落后于副本集,或者已经有一个主节点在申请选举,而那个失败的成员无法访问到此节点。在这些情况下,其他成员将投票反对该成员的申请。

如果申请选举的成员从副本集中获得了大多数选票,该成员将转换到 PRIMARY 状态。

回滚

如果主节点执行一个写操作之后停止了运行,而从节点还没来得及复制此操作,那么新选举出的主节点可能会丢失这个写操作。

第 12 章 从应用程序连接副本集

第 13 章 管理

第 14 章 分片简介

MongoDB 支持自动分片。

MongoDB 通过 mongos 进程来进行分片的路由寻址。mongos 维护着一个“目录”,指明了哪个分片包含哪些数据。mongos 知道哪些数据在哪个分片上,可以将请求转发到适当的分片。如果有对请求的响应,mongos 会收集它们,并在必要时进行合并,然后再发送回应用程序。

在对集合进行分片时,需要选择一个片键(shard key)。片键是 MongoDB 用来拆分数据的一个或几个字段。

包含片键并可以发送到单个分片或分片子集的查询称为定向查询(targeted query)。必须发送到所有分片的查询称为分散–收集查询(scatter-gather query),也称为广播查询:mongos会将查询分散到所有分片,然后再从各个分片收集结果。

第 15 章 配置分片

均衡器

均衡器负责数据的迁移。它会定期检查分片之间是否存在不均衡(一个分片的块数量达到阈值),如果存在,就会对块进行迁移。

MongoDB 3.4 以后,均衡器位于配置服务器副本集的主节点成员上;MongoDB 3.4 及以前,每个 mongos 会偶尔扮演均衡器角色。

第 16 章 选择片键

第 17 章 分片管理

第 18 章 了解应用程序的动态

第 19 章 MongoDB 安全介绍

第 20 章 持久性

第 21 章 在生产环境中设置 MongoDB

第 22 章 监控 MongoDB

第 23 章 备份

第 24 章 部署 MongoDB

参考资料

《MongoDB 权威指南》笔记一

第 1 章 MongoDB 简介

MongoDB 简介

MongoDB 是一个分布式文档数据库,由 C++ 语言编写。

面向文档

面向文档的数据库使用更灵活的“文档”模型取代了“行”的概念。通过嵌入文档和数组,面向文档的方式可以仅用一条记录来表示复杂的层次关系。

MongoDB 中也没有预定义模式(predefined schema):文档键值的类型和大小不是固定的。由于没有固定的模式,因此按需添加或删除字段变得更容易。

综上,MongoDB 支持结构化、半结构化数据模型,可以动态响应结构变化

功能丰富

MongoDB 提供了丰富的功能:

  • 索引 - MongoDB 支持通用的二级索引,并提供唯一索引、复合索引、地理空间索引及全文索引功能。此外,它还支持在不同层次结构(如嵌套文档和数组)上建立二级索引。
  • 聚合 - MongoDB 提供了一种基于数据处理管道的聚合框架。
  • 特殊的集合和索引类型 - MongoDB 支持有限生命周期(TTL)集合,适用于保存将在特定时间过期的数据,比如会话和固定大小的集合,以及用于保存最近的数据(日志)。MongoDB 还支持部分索引,可以仅对符合某个条件的文档创建索引,以提高效率并减少所需的存储空间。
  • 文件存储 - 针对大文件及文件元数据的存储,MongoDB 使用了一种非常易用的协议。

分布式

MongoDB 作为分布式存储,自然也具备了分布式的一般特性:

  • 通过副本机制提供高可用
  • 通过分片提供扩容能力

第 2 章 入门指南

文档是 MongoDB 中的基本数据单元,可以粗略地认为其相当于关系数据库管理系统中的行(但表达力要强得多)。

类似地,集合可以被看作具有动态模式的表。

一个 MongoDB 实例可以拥有多个独立的数据库,每个数据库都拥有自己的集合。

每个文档都有一个特殊的键 “_id”,其在所属的集合中是唯一的。

MongoDB 自带了一个简单但功能强大的工具:mongo shell。mongo shell 对管理 MongoDB 实例和使用 MongoDB 的查询语言操作数据提供了内置的支持。它也是一个功能齐全的 JavaScript 解释器,用户可以根据需求创建或加载自己的脚本。

文档

文档是一组有序键值的集合。

文档中的值不仅仅是“二进制大对象”,它们可以是几种不同的数据类型之一(甚至可以是一个完整的嵌入文档)。

文档中的键是字符串类型。除了少数例外的情况,可以使用任意 UTF-8 字符作为键。

键中不能含有 \0(空字符)。这个字符用于表示一个键的结束。

.$ 是特殊字符,只能在某些特定情况下使用。

MongoDB 会区分类型和大小写。

下面这两个文档是不同的:

1
2
{"count" : 5}
{"count" : "5"}

下面这两个文档也不同:

1
2
{"count" : 5}
{"Count" : 5}

需要注意,MongoDB 中的文档不能包含重复的键。例如,下面这个文档是不合法的。

1
{ "greeting": "Hello, world!", "greeting": "Hello, MongoDB!" }

集合

集合就是一组文档。如果将文档比作关系数据库中的行,那么一个集合就相当于一张表。

集合具有动态模式的特性。这意味着一个集合中的文档可以具有任意数量的不同“形状”。例如,以下两个文档可以存储在同一个集合中:

1
2
{"greeting" : "Hello, world!", "views": 3}
{"signoff": "Good night, and good luck"}

集合由其名称进行标识。集合名称可以是任意 UTF-8 字符串,但有以下限制。

  • 集合名称不能是空字符串(””)。
  • 集合名称不能含有 \0(空字符),因为这个字符用于表示一个集合名称的结束。
  • 集合名称不能以 system. 开头,该前缀是为内部集合保留的。例如,system.users 集合中保存着数据库的用户,system.namespaces 集合中保存着有关数据库所有集合的信息。
  • 用户创建的集合名称中不应包含保留字符 $。许多驱动程序确实支持在集合名称中使用 $,这是因为某些由系统生成的集合会包含它,但除非你要访问的是这些集合之一,否则不应在名称中使用 $ 字符。

使用 . 字符分隔不同命名空间的子集合是一种组织集合的惯例。例如,有一个具有博客功能的应用程序,可能包含名为 blog.posts 和名为 blog.authors 的集合。这只是一种组织管理的方式,blog 集合(它甚至不必存在)与其“子集合”之间没有任何关系。

数据库

MongoDB 使用集合对文档进行分组,使用数据库对集合进行分组。一个 MongoDB 实例可以承载多个数据库,每个数据库有零个或多个集合。

数据库按照名称进行标识的。数据库名称可以是任意 UTF-8 字符串,但有以下限制:

  • 数据库名称不能是空字符串(””)。
  • 数据库名称不能包含 /\."*<>:|?$、单一的空格以及 \0(空字符),基本上只能使用 ASCII 字母和数字。
  • 数据库名称区分大小写。
  • 数据库名称的长度限制为 64 字节。

MongoDB 使用 WiredTiger 存储引擎之前,数据库名称会对应文件系统中的文件名。尽管现在已经不这样处理了,但之前的许多限制遗留了下来。

此外,还有一些数据库名称是保留的。这些数据库可以被访问,但它们具有特殊的语义。具体如下。

  • adminadmin 数据库会在身份验证和授权时被使用。此外,某些管理操作需要访问此数据库。
  • local:在副本集中,local 用于存储复制过程中所使用的数据,而 local 数据库本身不会被复制。
  • config:MongoDB 的分片集群会使用 config 数据库存储关于每个分片的信息。通过将数据库名称与该库中的集合名称连接起来,可以获得一个完全限定的集合名称,称为命名空间

启动 MongoDB

启动 MongoDB 的方式:

  • Unix 系统 - 执行 mongod
  • Windows 系统 - 执行 mongod.exe

如果没有指定参数,则 mongod 会使用默认的数据目录 /data/db/。如果数据目录不存在或不可写,那么服务器端将无法启动。因此在启动 MongoDB 之前,创建数据目录(如 mkdir -p /data/db/)并确保对该目录有写权限非常重要。

默认情况下,MongoDB 会监听 27017 端口上的套接字连接。如果端口不可用,那么服务器将无法启动。

MongoDB Shell

MongoDB 内置了 MongoDB Shell 工具来提供命令行交互工具。

要启动 shell,可以执行 mongo 文件。

【示例】MongoDB Shell 基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 查看有哪些数据库
> show dbs
admin 0.000GB
config 0.000GB
fc_open_core 0.000GB
local 0.000GB
spring-tutorial 0.000GB
test 0.919GB

# 切换到 test 数据库
> use test
switched to db test

# 插入文档
> db.user.insertOne({ name: "dunwu", sex: 'man' })
{
"acknowledged" : true,
"insertedId" : ObjectId("670a281a2647017bf5f42962")
}

# 查询文档
}
> db.user.find()
{ "_id" : ObjectId("670a281a2647017bf5f42962"), "name" : "dunwu", "sex" : "man" }

# 更新文档
> db.user.updateOne({ name: "dunwu" }, { $set: { age: 30 } })
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }

# 删除文档
> db.user.deleteOne({ name: "dunwu" })
{ "acknowledged" : true, "deletedCount" : 1 }

# 退出 MongoDB Shell
> quit()

数据类型

MongoDB 中的文档可以被认为是“类似于 JSON”的形式。

MongoDB 基本数据类型如下:

null - null 类型用于表示空值或不存在的字段。

1
{ "x": null }

布尔类型 - 布尔类型的值可以为 true 或者 false。

1
{ "x": true }

数值类型 - shell 默认使用 64 位的浮点数来表示数值类型。因此,下面的数值在 shell 中看起来是“正常”的:

1
2
{"x" : 3.14}
{"x" : 3}

对于整数,可以使用 NumberInt 或 NumberLong 类,它们分别表示 4 字节和 8 字节的有符号整数。

1
2
{"x" : NumberInt("3")}
{"x" : NumberLong("3")}

字符串类型 - 任何 UTF-8 字符串都可以使用字符串类型来表示。

1
{ "x": "foobar" }

日期类型 - MongoDB 会将日期存储为 64 位整数,表示自 Unix 纪元(1970 年 1 月 1 日)以来的毫秒数,不包含时区信息。

1
{"x" : new Date()}

正则表达式 - 查询时可以使用正则表达式,语法与 JavaScript 的正则表达式语法相同。

1
{"x" : /foobar/i}

数组类型 - 集合或者列表可以表示为数组。

1
{ "x": ["a", "b", "c"] }

内嵌文档 - 文档可以嵌套其他文档,此时被嵌套的文档就成了父文档的值。

1
{ "x": { "foo": "bar" } }

Object ID - Object ID 是一个 12 字节的 ID,是文档的唯一标识。MongoDB 中存储的每个文档都必须有一个 “_id” 键。”_id” 的值可以是任何类型,但其默认为 ObjectId。在单个集合中,每个文档的 “_id” 值都必须是唯一的,以确保集合中的每个文档都可以被唯一标识。

1
{"x" : ObjectId()}

ObjectId 占用了 12 字节的存储空间,可以用 24 个十六进制数字组成的字符串来表示。

1
2
0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |  9  |  10  |  11
时间戳 | 随机值 | 计数器(起始值随机)

ObjectId 的前 4 字节是从 Unix 纪元开始以秒为单位的时间戳。这提供了一些有用的属性。时间戳与接下来的 5 字节(稍后会介绍)组合在一起,在秒级别的粒度上提供了唯一性。

二进制数据 - 二进制数据是任意字节的字符串,不能通过 shell 操作。如果要将非 UTF-8 字符串存入数据库,那么使用二进制数据是唯一的方法。

代码 - MongoDB 还可以在查询和文档中存储任意的 JavaScript 代码:

1
{"x" : function() { /* ... */ }}

最后,还有一些类型主要在内部使用(或被其他类型取代)。这些将根据需要在文中特别说明

使用 MongoDB shell(略)

第 3 章 创建、更新和删除文档

插入文档

insertOne

insertOne 方法用于插入单条文档

insertOne 方法语法如下:

1
db.collection.insertOne(document, options)
  • document - 要插入的单个文档。
  • options(可选) - 一个可选参数对象,可以包含 writeConcernbypassDocumentValidation 等。

【示例】向 movies 集合中插入一条文档

1
> db.movies.insertOne({"title" : "Stand by Me"})

insertMany

insertMany 方法用于批量插入文档

insertMany 方法语法如下:

1
db.collection.insertMany(documents, options)
  • documents - 要插入的文档数组。
  • options(可选) - 一个可选参数对象,可以包含 orderedwriteConcernbypassDocumentValidation 等。
1
> db.movies.insertMany([{"title" : "Ghostbusters"},{"title" : "E.T."},{"title" : "Blade Runner"}]);

在当前版本中,MongoDB 能够接受的最大消息长度是 48MB,因此在单次批量插入中能够插入的文档是有限制的。如果尝试插入超过 48MB 的数据,则多数驱动程序会将这个批量插入请求拆分为多个 48MB 的批量插入请求。

MongoDB 会对要插入的数据进行最基本的检查:检查文档的基本结构,如果不存在 “_id” 字段,则自动添加一个。

删除文档

deleteOne

deleteOne 方法用于删除单条文档

1
2
3
4
5
6
7
8
9
10
11
12
13
> db.movies.find()
{ "_id" : ObjectId("670a31a206fe06538fb4d138"), "title" : "Stand by Me" }
{ "_id" : ObjectId("670a31ab06fe06538fb4d139"), "title" : "Ghostbusters" }
{ "_id" : ObjectId("670a31ab06fe06538fb4d13a"), "title" : "E.T." }
{ "_id" : ObjectId("670a31ab06fe06538fb4d13b"), "title" : "Blade Runner" }

> db.movies.deleteOne({"_id" : ObjectId("670a31a206fe06538fb4d138")})
{ "acknowledged" : true, "deletedCount" : 1 }

> db.movies.find()
{ "_id" : ObjectId("670a31ab06fe06538fb4d139"), "title" : "Ghostbusters" }
{ "_id" : ObjectId("670a31ab06fe06538fb4d13a"), "title" : "E.T." }
{ "_id" : ObjectId("670a31ab06fe06538fb4d13b"), "title" : "Blade Runner" }

deleteMany

deleteMany 方法用于删除满足筛选条件的所有文档

1
2
3
4
5
6
7
8
9
10
11
12
13
> db.movies.find()
{ "_id" : 0, "title" : "Top Gun", "year" : 1986 }
{ "_id" : 1, "title" : "Back to the Future", "year" : 1985 }
{ "_id" : 3, "title" : "Sixteen Candles", "year" : 1984 }
{ "_id" : 4, "title" : "The Terminator", "year" : 1984 }
{ "_id" : 5, "title" : "Scarface", "year" : 1983 }

> db.movies.deleteMany({"year" : 1984}){ "acknowledged" : true, "deletedCount" : 2 }

> db.movies.find()
{ "_id" : 0, "title" : "Top Gun", "year" : 1986 }
{ "_id" : 1, "title" : "Back to the Future", "year" : 1985 }
{ "_id" : 5, "title" : "Scarface", "year" : 1983 }

可以使用 deleteMany删除集合中的所有文档

1
2
3
4
5
6
7
8
9
10
11
12
> db.movies.find()
{ "_id" : 0, "titl
e" : "Top Gun", "year" : 1986 }{ "_id" : 1, "titl
e" : "Back to the Future", "year" : 1985 }{ "_id" : 3, "titl
e" : "Sixteen Candles", "year" : 1984 }{ "_id" : 4, "titl
e" : "The Terminator", "year" : 1984 }{ "_id" : 5, "titl
e" : "Scarface", "year" : 1983 }

> db.movies.deleteMany({})
{ "acknowledged" :true, "deletedCount" : 5 }

> db.movies.find()

更新文档

replaceOne

replaceOne 方法用于将新文档完全替换匹配的文档。这对于进行大规模模式迁移的场景非常有用。

1
db.collection.replaceOne(filter, replacement, options)
  • filter - 用于查找文档的查询条件。
  • replacement - 新的文档,将替换旧的文档。
  • options - 可选参数对象,如 upsert 等。

【示例】replaceOne 示例

1
2
3
4
db.collection.replaceOne(
{ name: "Tom" }, // 过滤条件
{ name: "Tom", age: 18 } // 新文档
);

updateOne

updateOne 方法用于更新单条文档

updateOne 方法语法如下:

1
db.collection.updateOne(filter, update, options)
  • filter - 用于查找文档的查询条件。
  • update - 指定更新操作的文档或更新操作符。
  • options - 可选参数对象,如 upsertarrayFilters 等。

【示例】updateOne 示例

1
2
3
4
5
db.collection.updateOne(
{ name: "Tom" }, // 过滤条件
{ $set: { age: 19 } }, // 更新操作
{ upsert: false } // 可选参数
);

updateMany

updateMany 方法用于批量更新文档

updateMany 方法语法如下:

1
db.collection.updateMany(filter, update, options)
  • filter - 用于查找文档的查询条件。
  • update - 指定更新操作的文档或更新操作符。
  • options - 可选参数对象,如 upsertarrayFilters 等。

【示例】updateMany 示例

1
2
3
4
5
db.collection.updateMany(
{ age: { $lt: 30 } }, // 过滤条件
{ $set: { status: "active" } }, // 更新操作
{ upsert: false } // 可选参数
);

更新运算符

“$set” 用来设置一个字段的值。如果这个字段不存在,则创建该字段。

1
2
3
4
5
6
7
8
9
// 使用 "$set" 来修改值
> db.users.updateOne({"name" : "joe"},{"$set" : {"favorite book" : "Green Eggs and Ham"}})

// 使用 "$set" 来修改键的类型
// 将 "favorite book" 键的值更改为一个数组
> db.users.updateOne({"name" : "joe"},{"$set" : {"favorite book" : "Green Eggs and Ham"}})

// 如果用户发现自己其实不爱读书,则可以用 "$unset" 将这个键完全删除
> db.users.updateOne({"name" : "joe"},{"$unset" : {"favorite book" : 1}})

“$inc” 运算符可以用来修改已存在的键值或者在该键不存在时创建它。对于更新分析数据、因果关系、投票或者其他有数值变化的地方,使用这个会非常方便。

1
> db.games.updateOne({"game":"pinball","user":"joe"},{"$inc":{"score":50}})

如果数组已存在,”$push” 就会将元素添加到数组末尾;如果数组不存在,则会创建一个新的数组

1
> db.blog.posts.updateOne({"title":"A blog post"},{"$push":{"comments":{"name":"joe","email":"joe@example.com","content":"nice post."}}})

如果将数组视为队列或者栈,那么可以使用 “$pop”从任意一端删除元素{"$pop":{"key":1}} 会从数组末尾删除一个元素,{"$pop":{"key":-1}} 则会从头部删除它。

“$pull” 用于删除与给定条件匹配的数组元素

1
> db.lists.updateOne({}, {"$pull" : {"todo" : "laundry"}})

upsert

upsert 是一种特殊类型的更新。如果找不到与筛选条件相匹配的文档,则会以这个条件和更新文档为基础来创建一个新文档;如果找到了匹配的文档,则进行正常的更新。

1
> db.users.updateOne({"rep" : 25}, {"$inc" : {"rep" : 3}}, {"upsert" : true})

第 4 章 查询

find 简介

MongoDB 中使用 find 方法来进行查询。查询就是返回集合中文档的一个子集,子集的范围从 0 个文档到整个集合。要返回哪些文档由 find 的第一个参数决定,该参数是一个用于指定查询条件的文档。

find 的语法格式如下:

1
db.collection.find(query, projection)
  • query - 用于查找文档的查询条件。默认为 {},即匹配所有文档。
  • projection(可选) - 指定返回结果中包含或排除的字段。

【示例】查找示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 查找所有文档
> db.collection.find()

// 按条件查找文档
> db.collection.find({ age: { $gt: 25 } })

// 按条件查找文档,并只返回指定字段
> db.collection.find(
{ age: { $gt: 25 } },
{ name: 1, age: 1, _id: 0 }
)

// 以格式化的方式来显示所有文档
> db.collection.find().pretty()

查询条件

比较操作符

操作符 描述 示例
$eq 等于 { age: { $eq: 25 } }
$ne 不等于 { age: { $ne: 25 } }
$gt 大于 { age: { $gt: 25 } }
$gte 大于等于 { age: { $gte: 25 } }
$lt 小于 { age: { $lt: 25 } }
$lte 小于等于 { age: { $lte: 25 } }
$in 在指定的数组中 { age: { $in: [25, 30, 35] } }
$nin 不在指定的数组中 { age: { $nin: [25, 30, 35] } }

【示例】查找年龄大于 25 且城市为 “New York” 的文档:

1
db.collection.find({ age: { $gt: 25 }, city: "New York" });

逻辑操作符

操作符 描述 示例
$and 逻辑与,符合所有条件 { $and: [ { age: { $gt: 25 } }, { city: "New York" } ] }
$or 逻辑或,符合任意条件 { $or: [ { age: { $lt: 25 } }, { city: "New York" } ] }
$not 取反,不符合条件 { age: { $not: { $gt: 25 } } }
$nor 逻辑与非,均不符合条件 { $nor: [ { age: { $gt: 25 } }, { city: "New York" } ] }

【示例】查找年龄大于 25 或城市为 “New York” 的文档:

1
db.collection.find({ $or: [ { age: { $gt: 25 } }, { city: "New York" } ] });

元素操作符

操作符 描述 示例
$exists 字段是否存在 { age: { $exists: true } }
$type 字段的 BSON 类型 { age: { $type: "int" } }

【示例】查找包含 age 字段的文档:

1
db.myCollection.find({ age: { $exists: true } });

数组操作符

操作符 描述 示例
$all 数组包含所有指定的元素 { tags: { $all: ["red", "blue"] } }
$elemMatch 数组中的元素匹配指定条件 { results: { $elemMatch: { score: { $gt: 80, $lt: 85 } } } }
$size 数组的长度等于指定值 { tags: { $size: 3 } }
slice 返回一个数组键中元素的子集

查询数组元素的方式与查询标量值相同。

1
2
3
4
// 插入数组列表
db.food.insertOne({"fruit" : ["apple", "banana", "peach"]}
// 查找数组中的 banana 元素
db.food.find({"fruit" : "banana"})

如果需要通过多个元素来匹配数组,那么可以使用 “$all”。

1
2
3
4
5
6
db.food.insertOne({"_id" : 1, "fruit" : ["apple", "banana", "peach"]})
db.food.insertOne({"_id" : 2, "fruit" : ["apple", "kumquat", "orange"]})
db.food.insertOne({"_id" : 3, "fruit" : ["cherry", "banana", "apple"]})

// 查询同时包含元素 "apple" 和 "banana" 的文档
db.food.find({fruit : {$all : ["apple", "banana"]}})

查询特定长度的数组

1
db.food.find({"fruit" : {"$size" : 3}})

其他操作符

还有一些其他操作符如下:

操作符 描述 示例
$regex 匹配正则表达式 { name: { $regex: /^A/ } }
$text 进行文本搜索 { $text: { $search: "coffee" } }
$where 使用 JavaScript 表达式进行条件过滤 { $where: "this.age > 25" }

查找名字以 “A” 开头的文档:

1
db.myCollection.find({ name: { $regex: /^A/ } })

特定类型查询

null

null 会匹配值为 null 的文档以及缺少这个键的所有文档

1
> db.c.find({"z" : null})

如果仅想匹配键值为 null 的文档,则需要检查该键的值是否为 null,并且通过 “$exists” 条件确认该键已存在。

1
db.c.find({"z" : {"$eq" : null, "$exists" : true}})

正则表达式

$regex” 可以在查询中为字符串的模式匹配提供正则表达式功能。正则表达式对于灵活的字符串匹配非常有用。

1
> db.users.find( {"name" : {"$regex" : /joe/i } })

MongoDB 会使用 Perl 兼容的正则表达式(PCRE)库来对正则表达式进行匹配。任何 PCRE 支持的正则表达式语法都能被 MongoDB 接受。在查询中使用正则表达式之前,最好先在 JavaScript shell 中检查一下语法,这样可以确保匹配与预想的一致。

查询内嵌文档

假设文档形式如下:

1
2
3
4
5
6
7
{
"name": {
"first": "Joe",
"last": "Schmoe"
},
"age": 45
}

查询 first 属性为 Joe,last 属性为 Schmoe 的文档:

1
db.people.find({"name.first" : "Joe", "name.last" : "Schmoe"})

$where 查询

$where 允许你在查询中执行任意的 JavaScript 代码。这样就能在查询中做大部分事情了。除非绝对必要,否则不应该使用 “$where” 查询:它们比常规查询慢得多。

1
2
3
4
5
6
7
8
9
10
db.foo.find({"$where" : function () {
for (var current in this) {
for (var other in this) {
if (current != other && this[current] == this[other]) {
return true;
}
}
}
return false;
}});

游标

数据库会使用游标返回 find 的执行结果。

1
2
3
4
var cursor = db.people.find();
cursor.forEach(function(x) {
print(x.name);
});

limit() 方法用于限制查询结果返回的文档数量。

1
db.collection.find().limit(10);

skip() 方法用于跳过指定数量的文档,从而实现分页或分批查询。

1
2
// 跳过前 10 个文档,返回接下来的 10 个文档
db.collection.find().skip(10).limit(10);

sort() 方法可以通过参数指定排序的字段。

1
2
// 先按 age 字段升序排序,再按 createdAt 字段降序排序
db.myCollection.find().sort({ age: 1, createdAt: -1 });

参考资料

《SQL 必知必会》笔记

第 1 课 了解 SQL

数据库基础

  • 数据库(database) - 保存有组织的数据的容器(通常是一个文件或一组文件)。
  • 表(table) - 某种特定类型数据的结构化清单。
  • 模式 - 关于数据库和表的布局及特性的信息。
  • 列(column) - 表中的一个字段。所有表都是由一个或多个列组成的。
  • 数据类型 - 所允许的数据的类型。每个表列都有相应的数据类型,它限制(或允许)该列中存储的数据。
  • 行(row) - 表中的一个记录。
  • 主键(primary key) - 一列(或一组列),其值能够唯一标识表中每一行。表中的任何列都可以作为主键,只要它满足以下条件:
    • 任意两行都不具有相同的主键值;
    • 每一行都必须具有一个主键值(主键列不允许 NULL 值);
    • 主键列中的值不允许修改或更新;
    • 主键值不能重用(如果某行从表中删除,它的主键不能赋给以后的新行)。

什么是 SQL

SQL 是 Structured Query Language(结构化查询语言)的缩写。SQL 是一种专门用来与数据库沟通的语言。

第 2 课 检索数据

作为 SQL 组成部分的保留字。关键字不能用作表或列的名字。

检索单列

1
2
SELECT prod_name
FROM Products;

检索多列

1
2
SELECT prod_id, prod_name, prod_price
FROM Products;

检索所有列

1
2
SELECT *
FROM Products;

检索去重

1
2
SELECT DISTINCT vend_id
FROM Products;

限制数量

检索 TOP5 数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- SQL Server 和 Access
SELECT TOP 5 prod_name
FROM Products;

-- DB2
SELECT prod_name
FROM Products
FETCH FIRST 5 ROWS ONLY;

-- Oracle
SELECT prod_name
FROM Products
WHERE ROWNUM <=5;

-- MySQL、MariaDB、PostgreSQL 或者 SQLite
SELECT prod_name
FROM Products
LIMIT 5;
-- 检索从第 5 行起的 5 行数据
SELECT prod_name
FROM Products
LIMIT 5 OFFSET 5;
-- MySQL 和 MariaDB 中,上面的示例可以简化如下
SELECT prod_name
FROM Products
LIMIT 5, 5;

使用注释

1
2
3
4
5
6
7
8
9
10
11
SELECT prod_name -- 这是一条注释
FROM Products;

# 这是一条注释
SELECT prod_name
FROM Products;

/* SELECT prod_name, vend_id
FROM Products; */
SELECT prod_name
FROM Products;

第 3 课 排序检索数据

SQL 语句由子句构成,有些子句是必需的,有些则是可选的。一个子句通常由一个关键字加上所提供的数据组成。例如,SELECT 语句中的 FROM 子句。

ORDER BY 子句取一个或多个列的名字,据此对输出进行排序。ORDER BY 支持两种排序方式:ASC(升序) 和 DESC(降序)。

按单列排序:

1
2
3
SELECT prod_name
FROM Products
ORDER BY prod_name;

按多列排序:

1
2
3
SELECT prod_id, prod_price, prod_name
FROM Products
ORDER BY prod_price DESC, prod_name;

按列位置排序(不推荐):

1
2
3
SELECT prod_id, prod_price, prod_name
FROM Products
ORDER BY 2, 3;

指定排序方向

1
2
3
SELECT prod_id, prod_price, prod_name
FROM Products
ORDER BY prod_price DESC;

第 4 课 过滤数据

只检索所需数据需要指定搜索条件(search criteria),搜索条件也称为过滤条件(filter condition)。

在 SELECT 语句中,数据根据 WHERE 子句中指定的搜索条件进行过滤。

1
2
3
SELECT prod_name, prod_price
FROM Products
WHERE prod_price = 3.49;

检索所有价格小于 10 美元的产品。

1
2
3
SELECT prod_name, prod_price
FROM Products
WHERE prod_price < 10;

检索所有不是供应商 DLL01 制造的产品

1
2
3
4
5
6
7
8
9
-- 下面两条查询语句作用相同

SELECT vend_id, prod_name
FROM Products
WHERE vend_id <> 'DLL01';

SELECT vend_id, prod_name
FROM Products
WHERE vend_id != 'DLL01';

检索价格在 5 美元和 10 美元之间的所有产品

1
2
3
SELECT prod_name, prod_price
FROM Products
WHERE prod_price BETWEEN 5 AND 10;

检索所有没有邮件地址的顾客

1
2
3
SELECT cust_name
FROM CUSTOMERS
WHERE cust_email IS NULL;

第 5 课 高级数据过滤

  • AND - AND 用来表示检索满足所有给定条件的行。
  • OR - OR 用来表示检索匹配任一给定条件的行。

组合 WHERE 子句

检索由供应商 DLL01 制造且价格小于等于 4 美元的所有产品的名称和价格

1
2
3
SELECT prod_id, prod_price, prod_name
FROM Products
WHERE vend_id = 'DLL01' AND prod_price <= 4;

检索由供应商 DLL01 或供应商 BRS01 制造的所有产品的名称和价格

1
2
3
SELECT prod_name, prod_price
FROM Products
WHERE vend_id = 'DLL01' OR vend_id = 'BRS01';

WHERE 子句可以包含任意数目的 AND 和 OR 操作符。允许两者结合以进行复杂、高级的过滤。

SQL 在处理 OR 操作符前,优先处理 AND 操作符。

下面的示例中,SQL 会理解为由供应商 BRS01 制造的价格为 10 美元以上的所有产品,以及由供应商 DLL01 制造的所有产品,而不管其价格如何。

1
2
3
4
SELECT prod_name, prod_price
FROM Products
WHERE vend_id = 'DLL01' OR vend_id = 'BRS01'
AND prod_price >= 10;

任何时候使用具有 AND 和 OR 操作符的 WHERE 子句,都应该使用圆括号明确地分组操作符。

1
2
3
4
SELECT prod_name, prod_price
FROM Products
WHERE (vend_id = 'DLL01' OR vend_id = 'BRS01')
AND prod_price >= 10;

IN 操作符

IN 操作符用来指定条件范围,范围中的每个条件都可以进行匹配。IN 取一组由逗号分隔、括在圆括号中的合法值。

1
2
3
4
SELECT prod_name, prod_price
FROM Products
WHERE vend_id IN ( 'DLL01', 'BRS01' )
ORDER BY prod_name;

和下面的示例作用相同

1
2
3
4
SELECT prod_name, prod_price
FROM Products
WHERE vend_id = 'DLL01' OR vend_id = 'BRS01'
ORDER BY prod_name;

为什么要使用 IN 操作符?其优点如下。

  • 在有很多合法选项时,IN 操作符的语法更清楚,更直观。
  • 在与其他 AND 和 OR 操作符组合使用 IN 时,求值顺序更容易管理。
  • IN 操作符一般比一组 OR 操作符执行得更快。
  • IN 的最大优点是可以包含其他 SELECT 语句,能够更动态地建立 HERE 子句。

NOT 操作符

NOT 用来否定其后条件的关键字。

检索除 DLL01 之外的所有供应商制造的产品

1
2
3
4
SELECT prod_name
FROM Products
WHERE NOT vend_id = 'DLL01'
ORDER BY prod_name;

和下面的示例作用相同

1
2
3
4
SELECT prod_name
FROM Products
WHERE vend_id <> 'DLL01'
ORDER BY prod_name;

第 6 课 用通配符进行过滤

通配符(wildcard)用来匹配值的一部分的特殊字符。

搜索模式(search pattern)由字面值、通配符或两者组合构成的搜索条件。

在搜索子句中使用通配符,必须使用 LIKE 操作符。LIKE 指示 DBMS,后跟的搜索模式利用通配符匹配而不是简单的相等匹配进行比较。

百分号(%)通配符

%表示任何字符出现任意次数。

检索所有产品名以 Fish 开头的产品

1
2
3
SELECT prod_id, prod_name
FROM Products
WHERE prod_name LIKE 'Fish%';

匹配任何位置上包含文本 bean bag 的值,
不论它之前或之后出现什么字符。

检索产品名中包含 bean bag 的产品

1
2
3
SELECT prod_id, prod_name
FROM Products
WHERE prod_name LIKE '%bean bag%';

检索产品名中以 F 开头,y 结尾的产品

1
2
3
SELECT prod_name
FROM Products
WHERE prod_name LIKE 'F%y';

下划线(_)通配符

下划线(_)的用途与%一样,但它只匹配单个字符。

1
2
3
SELECT prod_id, prod_name
FROM Products
WHERE prod_name LIKE '__ inch teddy bear';

方括号([ ])通配符

方括号([])通配符用来指定一个字符集,它必须匹配指定位置(通配符的位置)的一个字符。

说明:并不是所有 DBMS 都支持用来创建集合的 []。只有微软的 Access 和 SQL Server 支持集合。

找出所有名字以 J 或 M 开头的联系人:

1
2
3
4
SELECT cust_contact
FROM Customers
WHERE cust_contact LIKE '[JM]%'
ORDER BY cust_contact;

第 7 课 创建计算字段

拼接字段

拼接字符串值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- Access 和 SQL Server
SELECT vend_name + ' (' + vend_country + ')'
FROM Vendors
ORDER BY vend_name;

-- DB2、Oracle、PostgreSQL、SQLite 和 Open Office Base
SELECT vend_name || ' (' || vend_country || ')'
FROM Vendors
ORDER BY vend_name;

-- MySQL 或 MariaDB
SELECT Concat(vend_name, ' (', vend_country, ')')
FROM Vendors
ORDER BY vend_name;

去除字符串中的空格

1
2
3
4
5
6
7
8
9
-- Access 和 SQL Server
SELECT RTRIM(vend_name) + ' (' + RTRIM(vend_country) + ')'
FROM Vendors
ORDER BY vend_name;

-- DB2、Oracle、PostgreSQL、SQLite 和 Open Office Base
SELECT RTRIM(vend_name) || ' (' || RTRIM(vend_country) || ')'
FROM Vendors
ORDER BY vend_name;

别名

使用别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- Access 和 SQL Server
SELECT RTRIM(vend_name) + ' (' + RTRIM(vend_country) + ')'
AS vend_title
FROM Vendors
ORDER BY vend_name;

-- DB2、Oracle、PostgreSQL、SQLite 和 Open Office Base
SELECT RTRIM(vend_name) || ' (' || RTRIM(vend_country) || ')'
AS vend_title
FROM Vendors
ORDER BY vend_name;

-- MySQL 和 MariaDB
SELECT Concat(vend_name, ' (', vend_country, ')')
AS vend_title
FROM Vendors
ORDER BY vend_name;

执行算术计算

汇总物品的价格(单价乘以订购数量):

1
2
3
4
5
6
SELECT prod_id,
quantity,
item_price,
quantity*item_price AS expanded_price
FROM OrderItems
WHERE order_num = 20008;

第 8 课 使用函数处理数据

大多数 SQL 实现支持以下类型的函数:

  • 算术函数
  • 文本处理函数
  • 时间处理函数
  • 聚合函数
  • 返回 DBMS 正使用的特殊信息(如返回用户登录信息)的系统函数

文本处理函数

函数 说明
LEFT()(或使用子字符串函数) 返回字符串左边的字符
LENGTH()(也使用 DATALENGTH() 或 LEN()) 返回字符串的长度
LOWER()(Access 使用 LCASE()) 将字符串转换为小写
LTRIM() 去掉字符串左边的空格
RIGHT()(或使用子字符串函数) 返回字符串右边的字符
RTRIM() 去掉字符串右边的空格
SOUNDEX() 返回字符串的 SOUNDEX 值
UPPER()(Access 使用 UCASE()) 将字符串转换为大写

UPPER() 将文本转换为大写

1
2
3
SELECT vend_name, UPPER(vend_name) AS vend_name_upcase
FROM Vendors
ORDER BY vend_name;

日期和时间处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- SQL Server
SELECT order_num
FROM Orders
WHERE DATEPART(yy, order_date) = 2012;

-- Access
SELECT order_num
FROM Orders
WHERE DATEPART('yyyy', order_date) = 2012;

-- PostgreSQL
SELECT order_num
FROM Orders
WHERE DATE_PART('year', order_date) = 2012;

-- Oracle
SELECT order_num
FROM Orders
WHERE to_number(to_char(order_date, 'YYYY')) = 2012;

-- MySQL 和 MariaDB
SELECT order_num
FROM Orders
WHERE YEAR(order_date) = 2012;

数值处理函数

函数 说明
ABS() 返回一个数的绝对值
COS() 返回一个角度的余弦
EXP() 返回一个数的指数值
PI() 返回圆周率
SIN() 返回一个角度的正弦
SQRT() 返回一个数的平方根
TAN() 返回一个角度的正切

第 9 课 汇总数据

聚集函数(aggregate function)对某些行运行的函数,计算并返回一个值。

函数 说明
AVG() 返回某列的平均值
COUNT() 返回某列的行数
MAX() 返回某列的最大值
MIN() 返回某列的最小值
SUM() 返回某列值之和

AVG() 通过对表中行数计数并计算其列值之和,求得该列的平均值。

使用 AVG() 返回 Products 表中所有产品的平均价格:

1
2
SELECT AVG(prod_price) AS avg_price
FROM Products;

COUNT() 函数进行计数。可利用 COUNT() 确定表中行的数目或符合特定条件的行的数目。

返回 Customers 表中顾客的总数:

1
2
SELECT COUNT(*) AS num_cust
FROM Customers;

只对具有电子邮件地址的客户计数:

1
2
SELECT COUNT(cust_email) AS num_cust
FROM Customers;

MAX() 返回指定列中的最大值。

返回 Products 表中最贵物品的价格:

1
2
SELECT MAX(prod_price) AS max_price
FROM Products;

MIN() 返回指定列的最小值。

返回 Products 表中最便宜物品的价格

1
2
SELECT MIN(prod_price) AS min_price
FROM Products;

SUM() 用来返回指定列值的和(总计)。

返回订单中所有物品数量之和

1
2
3
SELECT SUM(quantity) AS items_ordered
FROM OrderItems
WHERE order_num = 20005;

组合聚集函数

1
2
3
4
5
SELECT COUNT(*) AS num_items,
MIN(prod_price) AS price_min,
MAX(prod_price) AS price_max,
AVG(prod_price) AS price_avg
FROM Products;

第 10 课 分组数据

分组是使用 SELECT 语句的 GROUP BY 子句建立的。

1
2
3
SELECT vend_id, COUNT(*) AS num_prods
FROM Products
GROUP BY vend_id;

GROUP BY 要点:

  • GROUP BY 子句可以包含任意数目的列,因而可以对分组进行嵌套,更细致地进行数据分组。
  • 如果在 GROUP BY 子句中嵌套了分组,数据将在最后指定的分组上进行汇总。换句话说,在建立分组时,指定的所有列都一起计算(所以不能从个别的列取回数据)。
  • GROUP BY 子句中列出的每一列都必须是检索列或有效的表达式(但不能是聚集函数)。如果在 SELECT 中使用表达式,则必须在 GROUP BY 子句中指定相同的表达式。不能使用别名。
  • 大多数 SQL 实现不允许 GROUP BY 列带有长度可变的数据类型(如文本或备注型字段)。
  • 除聚集计算语句外,SELECT 语句中的每一列都必须在 GROUP BY 子句中给出。
  • 如果分组列中包含具有 NULL 值的行,则 NULL 将作为一个分组返回。如果列中有多行 NULL 值,它们将分为一组。
  • GROUP BY 子句必须出现在 WHERE 子句之后,ORDER BY 子句之前。

HAVING 要点:

HAVING 非常类似于 WHERE。唯一的差别是,WHERE 过滤行,而 HAVING 过滤分组。

过滤两个以上订单的分组

1
2
3
4
SELECT cust_id, COUNT(*) AS orders
FROM Orders
GROUP BY cust_id
HAVING COUNT(*) >= 2;

列出具有两个以上产品且其价格大于等于 4 的供应商:

1
2
3
4
5
SELECT vend_id, COUNT(*) AS num_prods
FROM Products
WHERE prod_price >= 4
GROUP BY vend_id
HAVING COUNT(*) >= 2;

检索包含三个或更多物品的订单号和订购物品的数目:

1
2
3
4
SELECT order_num, COUNT(*) AS items
FROM orderitems
GROUP BY order_num
HAVING COUNT(*) >= 3;

要按订购物品的数目排序输出,需要添加 ORDER BY 子句

1
2
3
4
5
SELECT order_num, COUNT(*) AS items
FROM orderitems
GROUP BY order_num
HAVING COUNT(*) >= 3
ORDER BY items, order_num;

在 SELECT 语句中使用时必须遵循的次序:

1
SELECT -> FROM -> WHERE -> GROUP BY -> HAVING -> ORDER BY

第 11 课 使用子查询

子查询(subquery),即嵌套在其他查询中的查询。

假如需要列出订购物品 RGAN01 的所有顾客,应该怎样检索?下面列出具体的步骤。

(1) 检索包含物品 RGAN01 的所有订单的编号。

1
2
3
SELECT order_num
FROM OrderItems
WHERE prod_id = 'RGAN01';

输出

1
2
3
4
order_num
-----------
20007
20008

(2) 检索具有前一步骤列出的订单编号的所有顾客的 ID。

1
2
3
SELECT cust_id
FROM Orders
WHERE order_num IN (20007,20008);

输出

1
2
3
4
cust_id
----------
1000000004
1000000005

(3) 检索前一步骤返回的所有顾客 ID 的顾客信息。

1
2
3
SELECT cust_name, cust_contact
FROM Customers
WHERE cust_id IN ('1000000004','1000000005');

现在,结合这两个查询,把第一个查询(返回订单号的那一个)变为子查询。

1
2
3
4
5
SELECT cust_id
FROM orders
WHERE order_num IN (SELECT order_num
FROM orderitems
WHERE prod_id = 'RGAN01');

再进一步结合第三个查询

1
2
3
4
5
6
7
SELECT cust_name, cust_contact
FROM customers
WHERE cust_id IN (SELECT cust_id
FROM orders
WHERE order_num IN (SELECT order_num
FROM orderitems
WHERE prod_id = 'RGAN01'));

第 12 课 联结表

笛卡尔积 - 由没有联结条件的表关系返回的结果为笛卡儿积。检索出的行的数目将是第一个表中的行数乘以第二个表中的行数。

内联结

1
2
3
SELECT vend_name, prod_name, prod_price
FROM vendors INNER JOIN products
ON vendors.vend_id = products.vend_id;

联结多个表

下面两个 SQL 等价:

1
2
3
4
5
6
7
8
9
10
11
SELECT cust_name, cust_contact
FROM customers, orders, orderitems
WHERE customers.cust_id = orders.cust_id AND orderitems.order_num = orders.order_num AND prod_id = 'RGAN01';

SELECT cust_name, cust_contact
FROM customers
WHERE cust_id IN (SELECT cust_id
FROM orders
WHERE order_num IN (SELECT order_num
FROM orderitems
WHERE prod_id = 'RGAN01'));

第 13 课 创建高级联结

自联结

给与 Jim Jones 同一公司的所有顾客发送一封信件:

1
2
3
4
5
6
7
8
9
10
11
-- 子查询方式
SELECT cust_id, cust_name, cust_contact
FROM customers
WHERE cust_name = (SELECT cust_name
FROM customers
WHERE cust_contact = 'Jim Jones');

-- 自联结方式
SELECT c1.cust_id, c1.cust_name, c1.cust_contact
FROM customers AS c1, customers AS c2
WHERE c1.cust_name = c2.cust_name AND c2.cust_contact = 'Jim Jones';

自然联结

1
2
3
SELECT c.*, o.order_num, o.order_date, oi.prod_id, oi.quantity, oi.item_price
FROM customers AS c, orders AS o, orderitems AS oi
WHERE c.cust_id = o.cust_id AND oi.order_num = o.order_num AND prod_id = 'RGAN01';

左外联结

1
2
3
4
5
6
7
8
9
SELECT customers.cust_id, orders.order_num
FROM customers
INNER JOIN orders
ON customers.cust_id = orders.cust_id;

SELECT customers.cust_id, orders.order_num
FROM customers
LEFT OUTER JOIN orders
ON customers.cust_id = orders.cust_id;

右外联结

1
2
3
4
SELECT customers.cust_id, orders.order_num
FROM customers
RIGHT OUTER JOIN orders
ON orders.cust_id = customers.cust_id;

全外联结

1
2
3
4
SELECT customers.cust_id, orders.order_num
FROM orders
FULL OUTER JOIN customers
ON orders.cust_id = customers.cust_id;

注意:Access、MariaDB、MySQL、Open Office Base 和 SQLite 不支持 FULLOUTER JOIN 语法。

使用带聚集函数的联结

1
2
3
4
5
6
SELECT customers.cust_id,
COUNT(orders.order_num) AS num_ord
FROM customers
INNER JOIN orders
ON customers.cust_id = orders.cust_id
GROUP BY customers.cust_id;

第 14 课 组合查询

主要有两种情况需要使用组合查询:

  • 在一个查询中从不同的表返回结构数据;
  • 对一个表执行多个查询,按一个查询返回数据。

把 Illinois、Indiana、Michigan 等州的缩写传递给 IN 子句,检索出这些州的所有行

1
2
3
SELECT cust_name, cust_contact, cust_email
FROM Customers
WHERE cust_state IN ('IL','IN','MI');

找出所有 Fun4All

1
2
3
SELECT cust_name, cust_contact, cust_email
FROM Customers
WHERE cust_name = 'Fun4All';

组合这两条语句

1
2
3
4
5
6
7
SELECT cust_name, cust_contact, cust_email
FROM customers
WHERE cust_state IN ('IL', 'IN', 'MI')
UNION
SELECT cust_name, cust_contact, cust_email
FROM customers
WHERE cust_name = 'Fun4All';

UNION 默认从查询结果集中自动去除了重复的行;如果想返回所有的匹配行,可使用 UNION ALL

1
2
3
4
5
6
7
SELECT cust_name, cust_contact, cust_email
FROM customers
WHERE cust_state IN ('IL', 'IN', 'MI')
UNION ALL
SELECT cust_name, cust_contact, cust_email
FROM customers
WHERE cust_name = 'Fun4All';

第 15 课 插入数据

插入完整的行

1
2
3
4
5
6
-- 下面两条 SQL 等价
INSERT INTO Customers
VALUES ('1000000006', 'Toy Land', '123 Any Street', 'New York', 'NY', '11111', 'USA', NULL, NULL);

INSERT INTO Customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email)
VALUES ('1000000006', 'Toy Land', '123 Any Street', 'New York', 'NY','11111', 'USA', NULL, NULL);

插入行的一部分

1
2
INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country)
VALUES ('1000000006', 'Toy Land', '123 Any Street', 'New York', 'NY', '11111', 'USA');

插入某些查询的结果

1
2
3
INSERT INTO Customers(cust_id, cust_contact, cust_email, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country)
SELECT cust_id, cust_contact, cust_email, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country
FROM CustNew;

从一个表复制到另一个表

1
2
3
4
5
6
7
SELECT *
INTO CustCopy
FROM Customers;

-- MariaDB、MySQL、Oracle、PostgreSQL 和 SQLite
CREATE TABLE CustCopy AS
SELECT * FROM Customers;

第 16 课 更新和删除数据

更新单列

更新客户 1000000005 的电子邮件地址

1
2
3
UPDATE Customers
SET cust_email = 'kim@thetoystore.com'
WHERE cust_id = '1000000005';

更新多列

1
2
3
UPDATE customers
SET cust_contact = 'Sam Roberts', cust_email = 'sam@toyland.com'
WHERE cust_id = '1000000006';

从表中删除特定的行

1
2
DELETE FROM Customers
WHERE cust_id = '1000000006';

更新和删除的指导原则

  • 除非确实打算更新和删除每一行,否则绝对不要使用不带 WHERE 子句的 UPDATE 或 DELETE 语句。
  • 保证每个表都有主键,尽可能像 WHERE 子句那样使用它(可以指定各主键、多个值或值的范围)。
  • 在 UPDATE 或 DELETE 语句使用 WHERE 子句前,应该先用 SELECT 进行测试,保证它过滤的是正确的记录,以防编写的 WHERE 子句不正确。
  • 使用强制实施引用完整性的数据库,这样 DBMS 将不允许删除其数据与其他表相关联的行。
  • 有的 DBMS 允许数据库管理员施加约束,防止执行不带 WHERE 子句的 UPDATE 或 DELETE 语句。如果所采用的 DBMS 支持这个特性,应该使用它。

第 17 课 创建和操纵表

创建表

利用 CREATE TABLE 创建表,必须给出下列信息:

  • 新表的名字,在关键字 CREATE TABLE 之后给出;
  • 表列的名字和定义,用逗号分隔;
  • 有的 DBMS 还要求指定表的位置。
1
2
3
4
5
6
7
CREATE TABLE products (
prod_id CHAR(10) NOT NULL,
vend_id CHAR(10) NOT NULL,
prod_name CHAR(254) NOT NULL,
prod_price DECIMAL(8, 2) NOT NULL,
prod_desc VARCHAR(1000) NULL
);

更新表

添加列:

1
2
ALTER TABLE Vendors
ADD vend_phone CHAR(20);

删除列:

1
2
ALTER TABLE Vendors
DROP COLUMN vend_phone;

删除表

1
DROP TABLE CustCopy;

第 18 课 使用视图

视图是虚拟的表。与包含数据的表不一样,视图只包含使用时动态检索数据的查询。

视图的一些常见应用

重用 SQL 语句

  • 简化复杂的 SQL 操作。在编写查询后,可以方便地重用它而不必知道其基本查询细节。
  • 使用表的一部分而不是整个表。
  • 保护数据。可以授予用户访问表的特定部分的权限,而不是整个表的访问权限。
  • 更改数据格式和表示。视图可返回与底层表的表示和格式不同的数据。

创建视图

创建一个名为 ProductCustomers 的视图,它联结三个表,返回已订购了任意产品的所有顾客的列表。

1
2
3
4
5
CREATE VIEW ProductCustomers AS
SELECT cust_name, cust_contact, prod_id
FROM Customers, Orders, OrderItems
WHERE Customers.cust_id = Orders.cust_id
AND OrderItems.order_num = Orders.order_num;

检索订购了产品 RGAN01 的顾客

1
2
3
SELECT cust_name, cust_contact
FROM ProductCustomers
WHERE prod_id = 'RGAN01';

第 19 课 使用存储过程

创建存储过程

对邮件发送清单中具有邮件地址的顾客进行计数

1
2
3
4
5
6
7
8
9
10
11
12
CREATE PROCEDURE MailingListCount (
ListCount OUT INTEGER
) IS
v_rows INTEGER;

BEGIN
SELECT COUNT(*)
INTO v_rows
FROM customers
WHERE NOT cust_email IS NULL;
ListCount := v_rows;
END;

第 20 课 管理事务处理

使用事务处理(transaction processing),通过确保成批的 SQL 操作要么完全执行,要么完全不执行,来维护数据库的完整性。

  • 事务(transaction)指一组 SQL 语句;
  • 回退(rollback)指撤销指定 SQL 语句的过程;
  • 提交(commit)指将未存储的 SQL 语句结果写入数据库表;
  • 保留点(savepoint)指事务处理中设置的临时占位符(placeholder),可以对它发布回退(与回退整个事务处理不同)。

事务开始结束标记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- SQL Server
BEGIN TRANSACTION
...
COMMIT TRANSACTION

-- MariaDB 和 MySQL
SET TRANSACTION
...

-- Oracle
SET TRANSACTION
...

-- PostgreSQL
BEGIN
...

SQL 的 ROLLBACK 命令用来回退(撤销)SQL 语句

1
2
DELETE FROM Orders;
ROLLBACK;

一般的 SQL 语句都是针对数据库表直接执行和编写的。这就是所谓的隐式提交(implicit commit),即提交(写或保存)操作是自动进行的。

在事务处理块中,提交不会隐式进行。进行明确的提交,使用 COMMIT 语句。

1
2
3
4
BEGIN TRANSACTION
DELETE OrderItems WHERE order_num = 12345
DELETE Orders WHERE order_num = 12345
COMMIT TRANSACTION

要支持回退部分事务,必须在事务处理块中的合适位置放置占位符。这样,如果需要回退,可以回退到某个占位符。在 SQL 中,这些占位符称为保留点。

1
2
3
4
5
6
7
8
-- MariaDB、MySQL 和 Oracle
SAVEPOINT delete1;
...
ROLLBACK TO delete1;

-- SQL Server
SAVE TRANSACTION delete1;
ROLLBACK TRANSACTION delete1;

第 21 课 使用游标

SQL 检索操作返回一组称为结果集的行,这组返回的行都是与 SQL 语句相匹配的行(零行或多行)。简单地使用 SELECT 语句,没有办法得到第一行、下一行或前 10 行。

有时,需要在检索出来的行中前进或后退一行或多行,这就是游标的用途所在。游标(cursor)是一个存储在 DBMS 服务器上的数据库查询,它不是一条 SELECT 语句,而是被该语句检索出来的结果集。

游标要点

  • 能够标记游标为只读,使数据能读取,但不能更新和删除。
  • 能控制可以执行的定向操作(向前、向后、第一、最后、绝对位置、相对位置等)。
  • 能标记某些列为可编辑的,某些列为不可编辑的。
  • 规定范围,使游标对创建它的特定请求(如存储过程)或对所有请求可访问。
  • 指示 DBMS 对检索出的数据(而不是指出表中活动数据)进行复制,使数据在游标打开和访问期间不变化。

使用 DECLARE 语句创建游标,这条语句在不同的 DBMS 中有所不同。DECLARE 命名游标,并定义相应的 SELECT 语句,根据需要带 WHERE 和其他子句。

1
2
3
4
5
6
7
8
9
10
11
-- DB2、MariaDB、MySQL 和 SQL Server
DECLARE CustCursor CURSOR
FOR
SELECT * FROM Customers
WHERE cust_email IS NULL

-- Oracle 和 PostgreSQL
DECLARE CURSOR CustCursor
IS
SELECT * FROM Customers
WHERE cust_email IS NULL

使用 OPEN CURSOR 语句打开游标,在大多数 DBMS 中的语法相同:

1
OPEN CURSOR CustCursor

关闭游标

1
CLOSE CustCursor

第 22 课 高级 SQL 特性

约束

约束(constraint)管理如何插入或处理数据库数据的规则。

DBMS 通过在数据库表上施加约束来实施引用完整性。大多数约束是在表定义中定义的,用 CREATE TABLE 或 ALTER TABLE 语句。

主键是一种特殊的约束,用来保证一列(或一组列)中的值是唯一的,而且永不改动。换句话说,表中的一列(或多个列)的值唯一标识表中的每一行。

表中任意列只要满足以下条件,都可以用于主键

  • 任意两行的主键值都不相同。
  • 每行都具有一个主键值(即列中不允许 NULL 值)。
  • 包含主键值的列从不修改或更新。
  • 主键值不能重用。如果从表中删除某一行,其主键值不分配给新行。

创建表时指定主键

1
2
3
4
5
6
7
8
9
CREATE TABLE vendors (
vend_id CHAR(10) NOT NULL PRIMARY KEY,
vend_name CHAR(50) NOT NULL,
vend_address CHAR(50) NULL,
vend_city CHAR(50) NULL,
vend_state CHAR(5) NULL,
vend_zip CHAR(10) NULL,
vend_country CHAR(50) NULL
);

更新表时指定主键

1
2
ALTER TABLE Vendors
ADD CONSTRAINT PRIMARY KEY (vend_id);

外键是表中的一列,其值必须列在另一表的主键中。

创建表时指定外键

cust_id 中的任何值都必须是 Customers 表的 cust_id 中的值

1
2
3
4
5
CREATE TABLE Orders (
order_num INTEGER NOT NULL PRIMARY KEY,
order_date DATETIME NOT NULL,
cust_id CHAR(10) NOT NULL REFERENCES customers(cust_id)
);

更新表时指定外键

1
2
3
ALTER TABLE Orders
ADD CONSTRAINT
FOREIGN KEY (cust_id) REFERENCES Customers (cust_id)

唯一约束用来保证一列(或一组列)中的数据是唯一的。它们类似于主键,但存在以下重要区别。

  • 表可包含多个唯一约束,但每个表只允许一个主键。
  • 唯一约束列可包含 NULL 值。
  • 唯一约束列可修改或更新。
  • 唯一约束列的值可重复使用。
  • 与主键不一样,唯一约束不能用来定义外键。

检查约束用来保证一列(或一组列)中的数据满足一组指定的条件。检查约束的常见用途有以下几点。

  • 检查最小或最大值。例如,防止 0 个物品的订单(即使 0 是合法的数)。
  • 指定范围。例如,保证发货日期大于等于今天的日期,但不超过今天起一年后的日期。
  • 只允许特定的值。例如,在性别字段中只允许 M 或 F。

利用这个约束,任何插入(或更新)的行都会被检查,保证 quantity 大于 0。

1
2
3
4
5
6
7
CREATE TABLE OrderItems (
order_num INTEGER NOT NULL,
order_item INTEGER NOT NULL,
prod_id CHAR(10) NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
item_price MONEY NOT NULL
);

检查名为 gender 的列只包含 M 或 F,可编写如下的 ALTER TABLE 语句:

1
ADD CONSTRAINT CHECK (gender LIKE '[MF]')

索引

索引用来排序数据以加快搜索和排序操作的速度。

1
2
CREATE INDEX prod_name_ind
ON Products (prod_name);

触发器

触发器是特殊的存储过程,它在特定的数据库活动发生时自动执行。触发器可以与特定表上的 INSERT、UPDATE 和 DELETE 操作(或组合)相关联。

触发器的一些常见用途

  • 保证数据一致。例如,在 INSERT 或 UPDATE 操作中将所有州名转换为大写。
  • 基于某个表的变动在其他表上执行活动。例如,每当更新或删除一行时将审计跟踪记录写入某个日志表。
  • 进行额外的验证并根据需要回退数据。例如,保证某个顾客的可用资金不超限定,如果已经超出,则阻塞插入。
  • 计算计算列的值或更新时间戳。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- SQL Server
CREATE TRIGGER customer_state
ON Customers
FOR INSERT, UPDATE
AS
UPDATE Customers
SET cust_state = Upper(cust_state)
WHERE Customers.cust_id = inserted.cust_id;

-- Oracle 和 PostgreSQL
CREATE TRIGGER customer_state
AFTER INSERT OR UPDATE
FOR EACH ROW
BEGIN
UPDATE Customers
SET cust_state = Upper(cust_state)
WHERE Customers.cust_id = :OLD.cust_id
END;

数据库安全

安全性使用 SQL 的 GRANT 和 REVOKE 语句来管理。

参考资料