冒泡排序、插入排序、选择排序这三种排序算法,它们的时间复杂度都是 O(n²),比较高,适合小规模数据的排序。归并排序和快速排序时间复杂度为 O(nlogn),适合大规模的数据排序。

归并排序和快速排序都用到了分治思想。

归并排序

归并排序的原理

如果要排序一个数组,先从中间的位置将数组分割成两个子数组,然后再对这两个子数组从中间位置分割,如此直到分割之后的子数组只有一个元素。然后将最终分割的数组按大小顺序合并到一个新的数组中,如此递归地合并直到所有子数组合并完毕,就可以得到一个有序的数组了。

例子

假设要对下面这组数据利用归并排序方法进行排序:

1
$arr = [3, 5, 100, 6, 78];

第一次分割成下面两个数组:

1
$arr1 = [3, 5, 100]; $arr2 = [6, 78];

第二次分割:

1
2
3
4
// $arr1 划分成
[3] [5, 100]
// $arr2 划分成
[6] [78]

按递归的思想会先处理 $arr1 分割出来的两个数组:

[3] 里面只有一个元素,无需再分割,[5, 100] 还需要再次分割成 [5][100],分割完成就开始合并了,合并的时候,需要创建一个大小等于两个需要合并的数组大小总和的数组 temp,然后将小的元素先放入 temp,如此直到所有元素都放进了 temp 之后,这次合并操作就完成了,就得到了一个有序的数组 temp

所以 $arr1 最终合并得到:

1
[3, 5, 100]

$arr2 最终合并得到:

1
[6, 78]

再将 $arr1$arr2 处理的结果合并,也就是将 [3, 5, 100][6, 78] 合并,即可得到:

1
[3, 5, 6, 78, 100]

PHP 实现

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
function merge_sort($arr, $p, $r)
{
if ($p >= $r) return [$arr[$r]];

$q = intval(($p + $r) / 2);

$arr1 = merge_sort($arr, $p, $q);
$arr2 = merge_sort($arr, $q + 1, $r);

return merge($arr1, $arr2);
}

function merge($arr1, $arr2)
{
$arr = [];
$n1 = count($arr1);
$n2 = count($arr2);

$k = 0;
$i = 0;
$j = 0;
while ($i < $n1 && $j < $n2) {
if ($arr1[$i] <= $arr2[$j]) {
$arr[$k++] = $arr1[$i++];
} else {
$arr[$k++] = $arr2[$j++];
}
}

while ($i < $n1) {
$arr[$k++] = $arr1[$i++];
}

while ($j < $n2) {
$arr[$k++] = $arr2[$j++];
}

return $arr;
}

算法分析

  1. 归并排序是否稳定?

merge 实现里面对相等的元素,我们可以将 $arr1 的元素先合并到 $arr,这样即可实现排序算法的稳定性。

  1. 时间复杂度是多少?

归并排序的排序过程中不会受原始数据的有序度影响,都是固定的不断拆分成两个数组,直到最终拆分到只有一个元素的时候进行合并,小的在前面,大的在后面。也就是说,归并排序的时间复杂度是固定的 O(nlogn),非常稳定。

  1. 空间复杂度是多少?

在将原始数据一分为二的时候,我们不需要开辟额外的空间来存储数据,但是在合并的过程中,我们需要开辟一个临时数组来存储排序后的数据,然后将这个数组覆盖掉原始数组的那一段范围的数据。

但由于某一时刻 CPU 中最多只有一个合并的操作在进行,所以最大的空间复杂度为 O(n)

快速排序

快速排序原理

快速排序的过程中,我们会先从原始数组中随机选一个数 pivot,以这个数为中点,将剩余的其他 n-1 个数划分为两个部分,小于 pivot 的交换到 pivot 的左边,大于 pivot 的交换到 pivot 的右边。然后再对左边和右边两部分的子数组递归进行此操作,直到最后只有一个元素为止,这个时候我们的整个数组就排序完毕了。

例子

利用快速排序对以下数组进行排序:

1
$arr = [5, 1, 7, 8, 6];
  1. 选择最后一个数(6)作为中点,小于 6 的交换到 $arr 的左边,中间为 6,大于 6 的交换到 $arr 的右边。

  2. 经过上一步的交换后,$arr[5, 1, 6, 8, 7],这个时候 6 其实就是最终排序完的位置了。

  3. 再对 6 左边和右边的子数组 [5, 1][8, 7] 执行第一步操作,如此直到子数组只有一个元素,结束排序。

我们可以发现,快速排序的过程,其实就是一次次地确定最终排序数组中间元素的过程。

PHP 实现

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
function quick_sort(&$arr, $p, $r)
{
if ($p >= $r) return;

$q = partition($arr, $p, $r);

quick_sort($arr, $p, $q - 1);
quick_sort($arr, $q + 1, $r);
}

$array = [1, 5, 3, 7, 9, 4, 20, 100, 55, 67, 66];

// 分区+排序
function partition(&$arr, $p, $r)
{
// 返回分区点
$pivot = $arr[$r];

$i = $p;
for ($j = $p; $j < $r - 1; $j++) { // $r - 1 的值为 pivot,中间值
if ($arr[$j] < $pivot) {
$temp = $arr[$i];
$arr[$i] = $arr[$j];
$arr[$j] = $temp;
$i++;
}
}

// $i 永远不大于 $j,$i <= $j
// $i == $j 的情况下不交换位置
// 情况一:只有一个数,不影响
// 情况二:数组里面全部都是小于 $pivot 的元素,这种情况不需要再互换了
if ($i < $j) {
$arr[$r] = $arr[$i];
$arr[$i] = $pivot;
}

return $i;
}

quick_sort($array, 0, count($array) - 1);
var_export($array);

算法分析

  1. 快速排序是否是稳定的排序算法?

由于快速排序的过程中,会发生元素前后的交换,所以不能保证稳定性,所以是不稳定的排序算法。

  1. 快速排序的时间复杂度是多少?

平均时间复杂度是 O(nlogn),但可能比这个大

  1. 快速排序的空间复杂度是多少?

快速排序是原地排序算法,排序过程不需要开辟新的空间来处理。

归并排序和快速排序对比

  1. 归并排序是稳定的排序算法,快速排序不是稳定的排序算法

  2. 归并排序的过程需要开辟临时的空间来进行合并操作,快速排序是原地排序算法,归并排序的空间复杂度太高

  3. 归并排序的时间复杂度是固定的,快速排序的时间复杂度会受实际数据有序度影响

  4. 实际使用中,快速排序用得更多,因为是原地排序算法,但是不能保证排序结果的稳定性

冒泡排序

冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。

例子

假设要对一组数据排序(数组):4,5,6,3,2,1

冒泡过程(由小到大排序,每次冒泡实质就是拿出最大的元素放到数组末尾):

第一次冒泡:

1、拿 4 和 5 对比,4 < 5 所以不做元素互换操作,此时数组为 4,5,6,3,2,1

2、拿 5 和 6 对比,5 < 6 所以不做元素互换操作,此时数组为 4,5,6,3,2,1

3、拿 6 和 3 对比,6 > 3 所以将 6 和 3 位置互换,此时数组为 4,5,3,6,2,1

4、拿 6 和 2 对比,6 > 2 所以将 6 和 2 位置互换,此时数组为 4,5,3,2,6,1

5、拿 6 和 1 对比,6 > 1 所以将 6 和 1 位置互换,此时数组为 4,5,3,2,1,6

经过这次冒泡,元素 6 的位置就是最终排序后的位置了。

第二次冒泡的过程中,因为数组最后一个元素已经是最大的元素,因此我们只需要对开头的 5 个元素排序即可。

第二次冒泡,此时数组为 4,5,3,2,1,6 :

1、拿 4 和 5 对比,4 < 5 所以不做元素互换操作,此时数组为 4,5,3,2,1,6

2、拿 5 和 3 对比,5 > 3 所以交换这两个元素的位置,此时数组为 4,3,5,2,1,6

3、拿 5 和 2 对比,5 > 2 所以交换这两个元素的位置,此时数组为 4,3,2,5,1,6

4、拿 5 和 1 对比,5 > 1 所以交换这两个元素的位置,此时数组为 4,3,2,1,5,6

到此,第二次冒泡结束(因为数组末尾的元素在第一次冒泡的时候已经确定的了),经过这次冒泡,倒数第二个元素也确定了下来。

我们一共有 6 个元素,所以还需要进行 4 次冒泡操作才能得到最后的排序结果。

冒泡排序优化

因为我们在冒泡过程中,实际上是对比相邻的两个元素,如果 $arr[$i] > $arr[$i + 1],那么交换这两个元素的位置。

也就是说,如果一次冒泡过程中,完全没有发生数据交换,说明数组里面的数组都是从小到大有序的。这个时候我们就可以直接退出冒泡过程了。

PHP 实现

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
$array = [1, 5, 3, 7, 9, 4, 20, 100, 55, 67, 66];

function bubble_sort($arr)
{
$n = count($arr);
if ($n == 1) return $arr;

// 是否有发生数据交换操作,如果一次冒泡中没有发生数据交换操作说明已经有序,可以直接退出冒泡过程
$flag = true;

// 冒泡操作
for ($i = 0; $i < $n; $i++) {
// $i 是已经排好序的元素个数,-1 是要拿 $arr[$j] 和 $arr[$j + 1] 比较,这样只要到前一个元素就可以处>理完全部数据
for ($j = 0; $j < $n - $i - 1; $j++) {
if ($arr[$j] > $arr[$j + 1]) {
$temp = $arr[$j];
$arr[$j] = $arr[$j + 1];
$arr[$j + 1] = $temp;
$flag = false;
}
}

if (!$flag) break;
}

return $arr;
}

var_export(bubble_sort($array));

问题

  1. 冒泡排序是否是原地排序算法?

冒泡的过程中,只有相邻数据位置互换,不需要开辟新的空间来进行操作,所以是原地排序算法。

  1. 冒泡排序是否是稳定的算法?

冒泡过程中,我们只在 $arr[$j] > $arr[$j + 1] 的时候,才互换位置,相等的两个元素,位置不会变动,所以是稳定的算法。

  1. 冒泡排序的时间复杂度是多少?

最好的情况下,数组已经有序,只需要进行一次冒泡即可,时间复杂度是 O(n)

最坏的情况下,完全逆序,冒泡过程总遍历次数为 (n - 1) + (n - 2) + ... + 1,也就是 n*(n-1)/2,所以时间复杂度是 O(n²)

插入排序

先看一个简单的问题。一个有序的数组,我们往里面添加一个新的数据后,如何继续保持数据有序呢?很简单,我们只要遍历数组,找到数据应该插入的位置将其插入即可。我们可以通过这种方法保持集合中的数据一直有序。

插入排序的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。

在插入排序中,我们会将数据列表划分为两个部分,一部分是有序区间,另一部分是无序区间。插入排序的过程就是,遍历无序区间里面的每一个元素,在有序区间里面遍历找到一个合适的位置进行插入,直到无序区间的数据全部插入到有序区间里面,这个时候数据列表就完全有序了。

初始状态下,一般会将第一个元素 $arr[0] 视作有序区间,然后 $arr[1...n-1] 为无序区间,我们会遍历 $arr[1...n-1] 中的每一个元素,拿来插入到有序区间中,直到遍历完无序区间的全部数据。

例子

假设要对一组数据排序(数组):4,5,6,3,2,1,按顺序排序。

开始的时候,我们的有序区间是 $arr[0...0],无序区间是 $arr[1...n-1]

第一次遍历,我们拿到无序区间的第一个元素 5($arr[1]),将其与 4 对比,5 > 4,所以位置不变,因为有序区间只有一个元素,所以此次遍历结束。但是此时有序区间已经是 $arr[0...1],无序区间是 $arr[2...n-1]

第二次遍历,我们拿到无序区间的第一个元素 6($arr[2]),我们将其与有序区间的最后一个元素,也就是 5 对比,发现 6 比 5 大,是有序区间最大的元素,所以此次遍历结束。但是此时有序区间已经是 $arr[0...2],无序区间是 $arr[3...n-1]

我们这里是从有序区间的最后一个元素开始对比,但从有序区间第一个元素开始对比也是可以的。只是操作上稍有不同,如果从第一个元素开始对比,在我们找到合适的位置的时候,需要先将有序区间剩余的元素往后挪动即可。但是需要注意这种情况下对结果稳定性的影响,也就是说,这种情况下,我们比较的时候需要使用 >=<= 这种包含相等情况的也进行位置互换操作。这样才能保证排序结果的稳定性。

如此遍历,直到无序区间的元素被全部遍历完毕。

PHP 实现

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
function insertion_sort($arr)
{
$n = count($arr);
if ($n <= 1) return $arr;

// 0~$i-1 为有序区间
// $i~$n-1 为无序区间
for ($i = 1; $i < $n; $i++) {
$value = $arr[$i];
// 查找插入的位置
for ($j = $i - 1; $j >= 0; $j--) {
if ($arr[$j] > $value) {
$arr[$j + 1] = $arr[$j]; // 数据移动
} else {
break;
}
}
// 到这里的时候说明: 1. $arr[$j] <= $value(找到了插入的位置) 或 2. $j = -1 (循环终止,$value 是目前有序区间最小的数)
$arr[$j + 1] = $value; // 插入数据
}

return $arr;
}

var_export(insertion_sort($array));

问题

  1. 插入排序是原地排序算法吗?

在插入排序的过程中,我们并不需要额外的存储空间,所以空间复杂度为 O(1),也就是说,插入排序是一个原地排序算法。

  1. 插入排序是稳定的排序算法吗?

在插入排序中,对于值相同的元素,我们并不交换位置,可以保持原有的前后顺序不变,所以是稳定的排序算法。

  1. 插入排序的时间复杂度是多少?

最好的情况下,如果数据已经是有序的,我们并不需要移动任何数据,每次只需要比较一次就能确定插入的位置。这种情况下,时间复杂度是 O(n)。注意这里是从尾到头遍历已经有序的数据。

最坏的情况下,数据是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,需要移动数据的次数为 O(n²),所以最坏情况时间复杂度为 O(n²)

我们在数组中插入一个数据的平均时间复杂度是 O(n)。所以,对于插入排序来说,每次插入操作都相当于在数组中插入一个数据,循环执行 n 次插入操作,所以平均时间复杂度为 O(n²)。

选择排序

选择排序和插入排序有点类似,在排序的过程中,数据也被划分成有序区间和无序区间。但是选择排序是每次从未排序的区间里面选择最小的元素,将其放到已排序区间的末尾。

PHP 实现

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
function selection_sort($arr)
{
$n = count($arr);
if ($n <= 1) return $arr;

for ($i = 0; $i < $n - 1; $i++) {
// 假设一开始最小的元素都是未排序区间的第一个元素,如果有更小的,则替换
$min = $arr[$i];
$minIndex = $i;
// 选择最小元素,获取其值和下标
for ($j = $i; $j < $n; $j++) {
if ($arr[$j] < $min) {
$min = $arr[$j];
$minIndex = $j;
}
}
// 将最小的元素跟未排序区间的第一个元素交换
$arr[$minIndex] = $arr[$i];
$arr[$i] = $min;
}

return $arr;
}

var_export(selection_sort($array));

问题

  1. 选择排序是原地排序算法吗?

在选择排序的过程中,只需要两个变量来存储最小元素的值和最小元素的下标,不需要其他额外的空间,所以是原地排序算法。

  1. 选择排序是稳定的算法吗?

由于每次选择排序会将最小元素跟未排序区间第一个元素互换,这个互换过程会改变原有数据的顺序,所以是不稳定的算法。

  1. 选择排序的时间复杂度是多少?

选择排序所需时间为 n + (n - 1) + ... + 1,即 n(n + 1)/2,所以时间复杂度是 O(n²)

冒泡和插入时间复杂度一样,为什么插入排序更受欢迎?

因为每一次循环过程中,冒泡排序需要 3 个赋值操作,而插入排序只需要一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 冒泡排序中数据的交换操作:
if (a[j] > a[j + 1]) { // 交换
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
flag = true;
}

// 插入排序中数据的移动操作
if (a[j] > value) {
a[j + 1] = a[j]; // 数据移动
} else {
break;
}

所以,虽然冒泡排序和插入排序在时间复杂度上是一样的,都是 O(n²),但是如果我们希望把性能优化做到机智,那肯定首选插入排序。插入排序的算法思路也有很大的优化空间 => 希尔排序。

小结

冒泡排序、插入排序、选择排序对比

是否是原地排序? 是否稳定? 最好 最坏 平均
冒泡排序 O(n) O(n²) O(n²)
插入排序 O(n) O(n²) O(n²)
选择排序 O(n²) O(n²) O(n²)

这三种时间复杂度为 O(n²) 的排序算法中,冒泡排序、选择排序,可能就纯粹停留在理论的层面了,学习的目的也只是为了开拓思维,实际开发中应用并不多,但是插入排序还是挺有用的。有些编程语言的排序函数实现原理会用到插入排序算法。

这三种排序算法,实现代码都非常简单,对于小规模数据的排序,用起来非常高效。但是在大规模数据排序的时候,这个时间复杂度还是稍微有点高,所以更倾向于使用时间复杂度为 O(nlogn) 的排序算法。

在未安装之前替换

  1. 替换 brew 默认源
1
export HOMEBREW_BREW_GIT_REMOTE=https://mirrors.ustc.edu.cn/brew.git
  1. 替换 homebrew/homebrew-core 默认源(中科大源):
1
export HOMEBREW_CORE_GIT_REMOTE=https://mirrors.ustc.edu.cn/homebrew-core.git

如果想下次启动的时候,还使用这两个源,需要做以下修改:

1
2
3
4
5
6
7
替换brew.git:
cd "$(brew --repo)"
git remote set-url origin https://mirrors.ustc.edu.cn/brew.git

替换homebrew-core.git:
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://mirrors.ustc.edu.cn/homebrew-core.git

参考链接:

https://lug.ustc.edu.cn/wiki/mirrors/help/brew.git

本文用的 MySQL 是阿里云的 RDS,一主一从,主 1 核,从 2 核。配置很低,这不是本文讨论内容,不多说为什么。

背景

在很长一段时间的上线流程中,我们都要跑一个脚本(Laravel artisan 命令),用来对所有用户的权限做一些可能的更新操作。这个过程需要很长的时间,30w 左右的客户,19 个进程跑这些脚本短则 12 小时左右,多则一天以上。

影响

运行这个脚本的过程中,整个网站响应速度都慢了很多。查看服务器 CPU、内存 负载,没有什么压力,最后发现是 MySQL 从库 CPU 负载一直 100%(2 核的配置)。

分析

  1. 大家会说,很明显啊,升级一下 MySQL 从库,让它读得快一点就好了啊。我开始也是这么想的,从 2 核升到 8 核后,发现虽然快了,但是没有快 4 倍,后来去看主库发现,这个时候主库的 CPU 100% 了(原来平稳 30% 左右)。

  2. 现在发现了主库满载,我也想到了可以考虑升级一下主库,但是主库是包年包月的,一升就要按之前付费的时间来补足这些费用,但这些我决定不了。所以暂时放弃这个想法了。

本地测试

上一次上线的时候,也是发现了这个问题,上一次个人是想从代码层面找到优化的地方的(比如,会不会有嵌套的循环等,这个可以用 xhprof),后来把这个给忘了。

发现升级 MySQL 服务器的想法失败了,想起了自己之前写过的一个记录 SQL 语句的工具,便想着可以看看是不是有什么 SQL 执行太久了。

  1. 本地测试发现,一个脚本执行下来,有 200 个 SQL 语句,其中有很多是可以修改为批量操作的。因为 Laravel 模型的 createMany 是将关联数据一条一条插入的,所以将 createMany 修改为 insert 同时插入多条记录。

  2. 我们模型里面还用了 spatie/laravel-activitylog 这个包,这就导致,上面的批量语句有多少条,最终产生的 SQL 语句是这个数量的两倍。考虑到这个包记录的那些数据从来就没有用过,就把相关模型记录变更的操作禁用掉了

更新后的效果

把这两百个 SQL 缩减为 17 个之后,信心满满地将代码更新上去,失望地发现,感觉一点都没有快(Laravel horizon 面板 job 数量减少还是很缓慢)。然后接手这个功能的人告诉我,其实真实场景是写的情况是很少的,大部分是读语句。

本地测试的时候,打印的语句好像并没有非常慢的,但是有几个 80ms 左右的查询,个人觉得这是一个比较正常的。但事实证做优化的时候不能太感性,优化应该是有某些指标、方向指导的。

记录生成的 SQL 语句

既然去掉了 90% 的 SQL 语句都没有效,那就可能是一些语句在测试环境和生产的执行效率差别太大(生产数据多一些)。所以就写了下面的代码:

1
2
3
4
5
6
7
8
9
10
11
$cache = \Cache::get('permission');
if (!$cache) {
enable_mySQL_log();
enable_mongo_log();
\Cache::put('permission', 1, 120);
}
// ... 这里省略业务代码 ...
if (!$cache) {
disable_mySQL_log();
disable_mongo_log();
}

这里的 enable_mySQL_log 几个函数是自定义用来记录 SQL 语句的,使用 Cache 的目的是为了只记录一次(一次就够)。

从记录的语句中,发现了有两条语句都是用了 300ms 以上,而其他语句平均时间是 3ms。但这两个语句所在的表数据不是很多(20w左右),而且只是一个简单的 join 加一些条件筛选。

到这里原因其实已经很明显了,把这两条 SQL 语句拿去 explain 一下,答案没有让我失望,type: All

接下来的事情就很简单了,给涉及的那张表加了两个字段的独立索引,再去看 SQL 语句,300ms 降到了平均水平 3ms。

原因

MySQL join 表的时候,关联表的关联字段如果没有索引的话,会导致全表扫描。

回到测试环境加上索引后测试发现,原来 80ms 的语句,现在只需要 10ms 左右。

其他可以优化的地方

因为我们的脚本是扫描全表做处理的,所以可以考虑将里面的大部分查询转换为批量查询,查询到结果之后再进行业务逻辑处理。

总而言之,可以批量就不要分开操作。

反思总结

  1. 优化的时候如果找不到方向,不妨可以看看产生的 SQL 语句,可能只是某个表忘记加索引了。

  2. 关于 spatie/laravel-activitylog,虽然我们从来没有用过,但是它一直在记录,感觉如果从来不需要去看模型变更记录的话,还不如不要。

  3. 关于定位系统性能问题,上一次上线的时候,虽然想从代码着手,但是并没有发现有什么特别影响性能的地方。经过这次经历发现,发现系统有性能的时候,靠谱的做法是先定位到哪里产生了性能的问题。因为这个系统从开始到现在一直很多地方都有一些性能问题,另外一方面觉得那一部分业务负责,所以觉得慢是正常的。这些都是非常感性的想法,非常有害的,这会导致我们做不出正确的判断。

正确的做法是,从各方面去定位原因,比如应用服务器负载、数据库服务器负载,如果有某一个方面到达了瓶颈的话,马上升级并不是一个明智的做法,我们需要明确的知道是不是服务器不够用,还是代码写得有问题,如果最终确定的确是服务器支撑不住的话,再考虑升级。因为有些严重的性能问题,带来的性能下降是指数级的,单靠升级服务器要付出很大的成本。

如果我们遇到问题就凭感觉来断定的话,往往导致走很多弯路(走就算了,更坏的结果是到最后问题也还是没有解决)。

  1. 那些你觉得理所当然的未必就是理所当然的。这个问题从出现到解决经历的周期说实话非常长了,接手它的人和大家其实一致觉得,是因为里面业务逻辑复杂,可能需要的查询很多,处理很多东西。但事实证明,事情不像表明看到的那样。

nested 类型是一种特殊的对象 object 数据类型,允许对象数组彼此独立地进行索引和查询。

对象数组如何扁平化

内部对象 object 字段的数组不能像我们期望的那样工作。Lucene 没有内部对象的概念,所以 Elasticsearch 将对象层次结构扁平化为一个字段名称和值简单列表。例如,以下文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
curl -XPUT 'localhost:9200/my_index/my_type/1?pretty' -H 'Content-type: application/json' -d '
{
"group": "fans",
"user": [
{
"first": "John",
"last": "Smith"
},
{
"first": "Alice",
"last": "White"
}
]
}
'

说明

user 字段被动态的添加为 object 类型的字段。

在内部其转换成一个看起来像下面这样的文档:

1
2
3
4
5
{
"group": "fans",
"user.first": ["alice", "john"],
"user.last": ["smith", "white"]
}

user.firstuser.last 字段被扁平化为多值字段,并且 alice 和 white 之间的关联已经丢失。本文档将错误地匹配 user.first 为 alice 和 user.last 为 smith 的查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
curl -XGET 'localhost:9200/my_index/_search?pretty' -H 'Content-Type: application/json' -d '
{
"query": {
"bool": {
"must": [
{
"match": {
"user.first": "Alice"
}
},
{
"match": {
"user.last": "Smith"
}
}
]
}
}
}
'

对对象数组使用嵌套字段

如果需要索引对象数组并维护数组中每个对象的独立性,则应使用 nested 数据类型而不是 object 数据类型。在内部,嵌套对象将数组中的每个对象作为单独的隐藏文档进行索引,这意味着每个嵌套对象都可以使用嵌套查询 nested query 独立于其他对象进行查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
curl -XPUT 'localhost:9200/my_index?pretty' -H 'Content-Type: application/json' -d '
{
"mappings": {
"my_type": {
"properties": {
"user": {
"type": "nested"
}
}
}
}
}
'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
curl -XPUT 'localhost:9200/my_index/my_type/1?pretty' -H 'Content-Type: application/json' -d '
{
"group": "fans",
"user": [
{
"first": "John",
"last": "Smith"
},
{
"first": "Alice",
"last": "White"
}
]
}
'

说明:

user 字段映射为 nested 类型,而不是默认的 object 类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
curl -XGET 'localhost:9200/my_index/_search?pretty' -H 'Content-Type: application/json' -d '
{
"query": {
"nested": {
"path": "user",
"query": {
"bool": {
"must": [
{
"match": { "user.first": "Alice"}
},
{
"match": { "user.last": "Smith" }
}
]
}
}
}
}
}
'

说明:

此查询得不到匹配,是因为 Alice 和 Smith 不在同一个嵌套对象中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
curl -XGET 'localhost:9200/my_index/_search?pretty' -H 'Content-Type: application/json' -d '
{
"query": {
"nested": {
"path": "user",
"query": {
"bool": {
"must": [
{ "match": { "user.first": "Alice" } },
{ "match": { "user.last": "White" } }
]
}
},
"inner_hits": {
"highlight": {
"fields": {
"user.first": {}
}
}
}
}
}
}
'

说明:

此查询得到匹配,是因为 Alice 和 White 位于同一个嵌套对象中。

inner_hits 允许我们突出显示匹配的嵌套文档。

嵌套文档可以:

  • 使用 nested 查询进行查询
  • 使用 nested 和 reverse_nested 聚合进行分析
  • 使用 nested 排序进行排序
  • 使用 nested_inner_hits 进行检索与突出显示

嵌套字段参数

嵌套字段接受以下参数:

  • dynamic:是否将新属性动态添加到现有的嵌套对象。共有 true(默认),false 和 strict 三种参数。
  • include_in_all:(_all 字段已经废弃了)
  • properties:嵌套对象中的字段,可以是任何数据类型,包括嵌套。新的属性可能会添加到现有的嵌套对象。

备注:

类型映射(type mapping)、对象字段和嵌套字段包含的子字段,称之为属性 properties。这些属性可以为任意数据类型,包括 object 和 nested。属性可以通过以下方式加入:

  • 当在创建索引时显式定义它们
  • 当使用 PUT mapping API 添加或更新映射类型时显式地定义它们
  • 当索引包含新字段的文档时动态的加入

重要:

由于嵌套文档作为单独的文档进行索引,因此只能在 nested 查询,nested/reverse_nested 聚合或者 nested_inner_hits 的范围内进行访问。

限制嵌套字段的个数

索引一个拥有 100 个嵌套字段的文档,相当于索引了 101 个文档,因为每一个嵌套文档都被索引为一个独立的文档。为了防止不明确的映射,每个索引可以定义的嵌套字段的数量已被限制为 50 个。

多嵌套查询方式:

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
{
"query": {
"bool": {
"must": [
{
"nested": {
"path": [
"ajxx"
],
"query": {
"bool": {
"must": [
{
"match": {
"ajxx.ajzt": "破案"
}
},
{
"range": {
"ajxx.sasj": {
"gte": "2017-01-01 12:10:10",
"lte": "2017-01-02 12:10-40"
}
}
}
],
"should": [
{
"query_string": {
"query": "20170316盗窃案"
}
}
]
}
}
}
}
]
}
}
}

查询字段名称的模糊匹配编辑 字段名称可以用模糊匹配的方式给出:任何与模糊式正则匹配的字段都会被包括在搜索条件中。

1
2
3
4
5
6
{
"multi_match": {
"query": "结果",
"fields": ["*", "*_title"]
}
}

当这些子字段出现数值类型的时候,就会报异常了,解决方法是加入 lenient 字段。

1
2
3
4
5
6
7
8
9
10
11
12
{
"type": "parse_exception",
"reason": "failed to parse date field [XXX] with format [yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis]"
}

{
"multi_match": {
"query": "结果",
"lenient": "true",
"fields": ["*"]
}
}

利用 multi_match 嵌套全文检索

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
"include_in_parent": true,
"include_in_root": true,
{
"query": {
"bool": {
"must": [
{
"match": {
"ajztmc": "立案"
}
},
{
"match": {
"zjasl": "刑事"
}
},
{
"range": {
"lasj": {
"gte": "2015-01-01 12:10:10",
"lte": "2016-01-01 12:10:40"
}
}
},
{
"nested": {
"path": [
"rqxxx"
],
"query": {
"bool": {
"must": [
{
"match": {
"rqxx.baqmc": "办案区名称"
}
}
]
}
}
}
},
{
"nested": {
"path": [
"saryxx"
],
"query": {
"bool": {
"must": [
{
"match": {
"abc": "嫌疑人"
}
},
{
"match": {
"def": "女"
}
}
]
}
}
}
},
{
"nested": {
"path": [
"wp"
],
"query": {
"bool": {
"must": [
{
"match": {
"wp.wpzlnc": "赃物"
}
},
{
"match": {
"wp.wpztmc": "物品入库"
}
}
]
}
}
}
},
{
"multi_match": {
"query": "男",
"lenient",
"fields": [
"*"
]
}
}
]
}
},
"from": 0,
"size": 100,
"sort": {
"zxxgsj": {
"order": "desc"
}
}
}
0%