常见 SQL 语句在 ES 中如何使用?

452 阅读8分钟

「这是我参与2022首次更文挑战的第13天,活动详情查看:2022首次更文挑战」。

1 前言

ES 在对比关系型数据库中,检索数据方面有着极大的优势,但是常常我们还是喜欢先思考 SQL 是如何实现的,再思考 ElasticSearch 是怎么实现的,所以本篇文章将会介绍一下常见的 SQL ,ElasticSearch 要怎么实现这些常见 SQL。(这里检索方式由于 SQL 的是 Like,对应 ElasticSearch 检索方式很多,过段时间另起一篇介绍检索方式,下面主要介绍 SQL 的 group by 等基本操作)。

1.1 基本语法

由于下面涉及到主要是 ElasticSearch 聚合的语法。首先回顾一下语法

"aggregations":{   ## 和 Query 同级的关键词
	"aggregation_name_1":{  ## 自定义聚合查询名字
		"aggregation_type":{	## 聚合类型,Metric或者其他
			## aggregation_body 聚合需要的查询体
		},
		"meta" :{  ## 子聚合,不是必须
			## meta_data_body
		},
		"aggregations": { ## 子聚合,不是必须
		    ## sub_aggregation
		}
	},
	## 可以包含多个同级聚合,不是必须
	"aggregation_name_2":{  ## 自定义聚合查询名字
		## ......
	}
}

注意 aggregations 是和我们平常 ES query 查询是同级的,并且如果是聚合,建议分页的 size 设置为 0。

ElasticSearch 聚合有三种主要分类

image.png

2 实战

2.1 准备

以一张积木表为例,如下:

CREATE TABLE `ji_mu` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '积木名称',
  `color` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '积木颜色',
  `size` int DEFAULT NULL COMMENT '积木大小',
  `shape` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '积木形状',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

相应的数据

INSERT INTO ji_mu (`name`,color,size,shape) VALUES ('red-01','red','5',"正方形"); 
INSERT INTO ji_mu (`name`,color,size,shape) VALUES ('white-02','white','6',"正方形"); 
INSERT INTO ji_mu (`name`,color,size,shape) VALUES ('black-03','black','8',"圆形"); 
INSERT INTO ji_mu (`name`,color,size,shape) VALUES ('yellow-04','yellow','1',"圆形"); 
INSERT INTO ji_mu (`name`,color,size,shape) VALUES ('red-05','red','3',"圆形"); 
INSERT INTO ji_mu (`name`,color,size,shape) VALUES ('white-06','white','6',"三角形"); 
INSERT INTO ji_mu (`name`,color,size,shape) VALUES ('black-07','black','8',"三角形"); 
INSERT INTO ji_mu (`name`,color,size,shape) VALUES ('red-08','red','5',"正方形"); 
INSERT INTO ji_mu (`name`,color,size,shape) VALUES ('white-09','white','6',"正方形"); 
INSERT INTO ji_mu (`name`,color,size,shape) VALUES ('yellow-10','yellow','8',"圆形"); 
INSERT INTO ji_mu (`name`,color,size,shape) VALUES ('blue-11','blue','8',"长方形"); 
INSERT INTO ji_mu (`name`,color,size,shape) VALUES ('red-12','red','8',"三角形");
INSERT INTO ji_mu (`name`,color,size,shape) VALUES ('red-13','red','5',"三角形"); 
INSERT INTO ji_mu (`name`,color,size,shape) VALUES ('black-14','black','1',"椭圆"); 

同时建立 ElasticSearch 中的 Mapping 结构

PUT ji_mu
{
  "mappings": {
    "properties": {
      "shape": {
        "type": "keyword"
      },
      "color": {
        "type": "keyword"
      },
      "size": {
        "type": "integer"
      },
      "name": {
        "type": "keyword"
      }
    }
  }
}

数据和 MySQL 一样

POST ji_mu/_bulk
{"index":{"_id":1}}
{"size":5, "name":"red-01", "shape":"正方形", "color":"red"}
{"index":{"_id":2}}
{"size":6, "name":"white-02","shape":"正方形", "color":"white"}
{"index":{"_id":3}}
{"size":8, "name":"black-03","shape":"圆形", "color":"black"}
{"index":{"_id":4}}
{"size":1, "name":"yellow-04","shape":"圆形", "color":"yellow"}
{"index":{"_id":5}}
{"size":3, "name":"red-05","shape":"圆形", "color":"red"}
{"index":{"_id":6}}
{"size":6, "name":"white-06","shape":"三角形", "color":"white"}
{"index":{"_id":7}}
{"size":8, "name":"black-07","shape":"三角形", "color":"black"}
{"index":{"_id":8}}
{"size":5, "name":"red-08","shape":"正方形", "color":"red"}
{"index":{"_id":9}}
{"size":6, "name":"white-09","shape":"正方形", "color":"white"}
{"index":{"_id":10}}
{"size":8, "name":"yellow-10","shape":"圆形", "color":"yellow"}
{"index":{"_id":11}}
{"size":8, "name":"blue-11","shape":"长方形", "color":"blue"}
{"index":{"_id":12}}
{"size":8, "name":"red-12","shape":"三角形", "color":"red"}
{"index":{"_id":13}}
{"size":5, "name":"red-13","shape":"三角形", "color":"red"}
{"index":{"_id":14}}
{"size":1, "name":"black-13","shape":"椭圆", "color":"black"}

2.2 测试

2.2.1 案例一

需求:查询积木表中,按照形状分类,并且统计每个形状颜色数量且数量大于 1,最后取 top 2 个。SQL 语句如下

SELECT shape,COUNT(DISTINCT color) color_count 
FROM ji_mu 
GROUP BY shape 
HAVING color_count > 1 
ORDER BY color_count DESC LIMIT 2;

这条 SQL 很简单,首先使用 GROUP BY shape 进行形状分组,同时计算每个形状的颜色数量,由于同一形状颜色有些一样,需要去重,所以 COUNT(DISTINCT color) color_count,然后使用 HAVING color_count > 1 方式来过滤,要求颜色数量大于 1 ,最后使用 ORDER BY color_count DESC LIMIT 倒序并取出前 2 名。

那么怎么用 ElasticSearch 如何完成这个需求呢?

2.2.1.1 Group By

首先看 group by, ElasticSearch 对应的就是 Buckets 聚合的 terms,意思是分桶聚合

POST ji_mu/_search
{
  "size": 0,
  "aggs": {
    "shape_terms": {   //类似 group by 分组
      "terms": {       // 分桶聚合
        "field": "shape",  // 字段
        "size": 10
      }
    }
  }
}

结果大概如下,aggregations 下就是各个桶中的结果

{
  ......
  "aggregations" : {
    "shape_terms" : {  // 自定义的聚合的名称
      ......
      "buckets" : [
        {
          "key" : "三角形",   // 类似 group by 的字段
          "doc_count" : 4   // 桶中的数量,类似 count
        },
        {
          "key" : "圆形",
          "doc_count" : 4
        },
        {
          "key" : "正方形",
          "doc_count" : 4
        },
        {
          "key" : "椭圆",
          "doc_count" : 1
        },
        {
          "key" : "长方形",
          "doc_count" : 1
        }
      ]
    }
  }
}

2.2.1.2 DISTINCT

由于需要对颜色去重,在 ElasticSearch 可以使用指标类聚合 cardinality。它可以达到数据去重效果并统计。

DSL 语句:

POST ji_mu/_search
{
  "size": 0,
  "aggs": {
    "shape_terms": {  // 分桶聚合,类似 group by
      "terms": {
        "field": "shape",
        "size": 10
      },
      "aggs": {
        "color_count": {  // 在分桶聚合基础上,使用指标聚合求出颜色数量
          "cardinality": {
            "field": "color"
          }
        }
      }
    }
  }
}

结果如下

{
  ......
  "aggregations" : {
    ......
    "shape_terms" : { // 自定义的聚合名称
      "buckets" : [
        {
          "key" : "三角形",
          "doc_count" : 4,
          "color_count" : { // 自定义的聚合名称
            "value" : 3     // 每个桶颜色的数量
          }
        },
        {
          "key" : "圆形",
          "doc_count" : 4,
          "color_count" : {
            "value" : 3
          }
        },
        {
          "key" : "正方形",
          "doc_count" : 4,
          "color_count" : {
            "value" : 2
          }
        },
        {
          "key" : "椭圆",
          "doc_count" : 1,
          "color_count" : {
            "value" : 1
          }
        },
        {
          "key" : "长方形",
          "doc_count" : 1,
          "color_count" : {
            "value" : 1
          }
        }
      ]
    }
  }
}

2.2.1.3 Having & Order by

由于我们需要二次的进行过滤,只取颜色数量大于 1 的,在 ElasticSearch 中可以使用管道聚合类型的 bucket_selector 来完成,管道聚合是可以从其他聚合类型当作输入来源,然后二次过滤

POST ji_mu/_search
{
  "size": 0,
  "aggs": {
    "shape_terms": {  // 按照形状分组
      "terms": {   // 桶聚合类型
        "field": "shape", // 字段
        "size": 10
      },
      "aggs": {
        "color_count": {  // 获取颜色数量
          "cardinality": {  // 指标聚合类型
            "field": "color"  // 指定字段
          }
        },
        "color_count_filter":{   // having 过滤
          "bucket_selector": {  // 管道聚合类型
            "buckets_path": {
              "count":"color_count"  // 指定管道路径为上面的 color_count 指标聚合,并赋值给 count
            },
            "script": "params.count > 1"  // 取路径赋值的 count 字段,过滤只有 count 大于 1 的
          }
        }
      }
    }
  }
}

结果如下,可以看到结果中,椭圆和长方形不见了。

{
    ......
  "aggregations" : {
    "shape_terms" : {  // 自定义的聚合名称
      ......
      "buckets" : [ 
        {
          "key" : "三角形",  
          "doc_count" : 4, // 相当于 count
          "color_count" : { // 自定义的聚合名称
            "value" : 3
          }
        },
        {
          "key" : "圆形",
          "doc_count" : 4,
          "color_count" : {
            "value" : 3
          }
        },
        {
          "key" : "正方形",
          "doc_count" : 4,
          "color_count" : {
            "value" : 2
          }
        }
      ]
    }
  }
}

最后为了将结果倒排和取前 2个。在 ElasticSearch 中可以继续采用管道聚合类型的 bucket_sort。进行倒排和 limit 2。

POST ji_mu/_search
{
  "size": 0,
  "aggs": {
    "shape_terms": {  // 按照形状分组
      "terms": {   // 桶聚合类型
        "field": "shape", // 字段
        "size": 10
      },
      "aggs": {
        "color_count": {  // 获取颜色数量
          "cardinality": {  // 指标聚合类型
            "field": "color"  // 指定字段
          }
        },
        "color_count_filter":{   // having 过滤
          "bucket_selector": {  // 管道聚合类型
            "buckets_path": {
              "count":"color_count"  // 指定管道路径为上面的 color_count 指标聚合,并赋值给 count
            },
            "script": "params.count > 1"  // 取路径赋值的 count 字段,过滤只有 count 大于 1 的
          }
        },
        "color_count_sort":{  
          "bucket_sort": {  // 管道聚合类型
            "sort": {"color_count":"desc"}, // 指定排序的字段为上面 color_count 的指标聚合
            "from": 0,  // 相当于 limit
            "size": 2
          }
        }
      }
    }
  }
}

结果如下:正方形就不见了

{
    ......
  "aggregations" : {
    "shape_terms" : {  // 自定义的聚合名称
      ......
      "buckets" : [ 
        {
          "key" : "三角形",  
          "doc_count" : 4, // 相当于 count
          "color_count" : { // 相当于 DISTINCT
            "value" : 3
          }
        },
        {
          "key" : "圆形",
          "doc_count" : 4,
          "color_count" : {
            "value" : 3
          }
        }
      ]
    }
  }
}

2.2.2 案例二

需求:求出每个 color 颜色中 size 最大的一条记录。

SELECT * FROM ji_mu
WHERE (color,size) IN (
    SELECT color,MAX(size) size
    FROM ji_mu 
    GROUP BY color 
)

首先从子查询中分析,通过 GROUP BY color 分组,区别出每种颜色,而后计算出每组颜色取 size 的最大值 MAX(size) ,而后当初一个子查询,进行 in 操作,如果某条记录同时满足 color 和 size,则返回详细信息。

那么如何用 ElasticSearch 如何完成这个需求呢?

2.2.2.1 Group By

依然是 group by,和前面案例类似分桶聚合

POST ji_mu/_search
{
  "size": 0,
  "aggs": {
    "shape_terms": {
      "terms": {
        "field": "shape",
        "size": 10
      }
    }
  }
}

2.2.2.2 MAX

Max 函数,对应的 ElasticSearch 中对应的是指标聚合 Max,同样求出每个的最大值。

POST ji_mu/_search
{
  "size": 0,
  "aggs": {
    "shape_terms": {  // 分桶聚合
      "terms": {
        "field": "shape",
        "size": 10
      },
      "aggs": {
        "max_info": {  // 在分桶聚合的基础上,求每个桶最大值
          "max": {
            "field": "size"
          }
        }
      }
    }
  }
}

结果如下:

{
  ......
  "aggregations" : {
    "shape_terms" : {   // 自定义聚合名称
      ......
      "buckets" : [
        {
          "key" : "三角形",
          "doc_count" : 4,
          "max_info" : {    // 自定义聚合名称
            "value" : 8.0    // 每个桶中的最大,相当于 Max()
          }
        },
        {
          "key" : "圆形",
          "doc_count" : 4,
          "max_info" : {
            "value" : 8.0
          }
        },
        {
          "key" : "正方形",
          "doc_count" : 4,
          "max_info" : {
            "value" : 6.0
          }
        },
        {
          "key" : "椭圆",
          "doc_count" : 1,
          "max_info" : {
            "value" : 1.0
          }
        },
        {
          "key" : "长方形",
          "doc_count" : 1,
          "max_info" : {
            "value" : 8.0
          }
        }
      ]
    }
  }
}

到了这一步,in 要怎么操作呢?在 SQL 中是为了获取详细信息所以采用了 in 这种操作,然而在 ElasticSearch 可以另辟路径,使用分桶聚合类型中的 top_hits,该聚合可以返回聚合结果前几条,对结果进行排序,这样 Max 聚合就不需要了。

POST ji_mu/_search
{
  "size": 0,
  "aggs": {
    "color_terms": {   // 分桶聚合
      "terms": {
        "field": "color",
        "size": 10
      },
      "aggs": {
        "top_info": {   
          "top_hits": {
            "size": 1,  // 取每个桶的 top 1 数据
            "sort": [
              {
                "size": "desc" // 对每个桶中的数据进行倒序
              }
            ]
          }
        }
      }
    }
  }
}

结果如下:

{
  ......
  "aggregations" : {
    "color_terms" : {   // 根据 color 进行了分桶
      ......
      "buckets" : [
        {
          "key" : "red",
          "doc_count" : 5,
          "top_info" : {
            "hits" : {
              "total" : {
                "value" : 5,
                "relation" : "eq"
              },
              "max_score" : null,
              "hits" : [   // 桶中第一条的详细数据
                {
                  "_index" : "ji_mu",
                  "_type" : "_doc",
                  "_id" : "12",
                  "_score" : null,
                  "_source" : {
                    "size" : 8,
                    "name" : "red-12",
                    "shape" : "三角形",
                    "color" : "red"
                  },
                  "sort" : [
                    8
                  ]
                }
              ]
            }
          }
        },
        {
          "key" : "black",
          "doc_count" : 3,
          "top_info" : {
            "hits" : {
              "total" : {
                "value" : 3,
                "relation" : "eq"
              },
              "max_score" : null,
              "hits" : [   // 桶中第一条的详细数据
                {
                  "_index" : "ji_mu",
                  "_type" : "_doc",
                  "_id" : "3",
                  "_score" : null,
                  "_source" : {
                    "size" : 8,
                    "name" : "black-03",
                    "shape" : "圆形",
                    "color" : "black"
                  },
                  "sort" : [
                    8
                  ]
                }
              ]
            }
          }
        },
        ......
      ]
    }
  }
}

结尾

至此我们使用 ElasticSearch 完成了文中提到的两个案例,可以看到越来越复杂,并且有些地方和 SQL 方式是不大一样的,但是也并不是不能解决。所以得对 ElasticSearch 多加熟悉,才能对这些需求游刃有余。都看到这里了,喜欢点个赞呗 ^V^。