Bootstrap

ElasticSearch与Mysql对比(ElasticSearch常用方法大全,持续更新)

ElasticSearch是一个开源的搜索引擎,它可以被下面这样准确的形容:

  • 一个分布式的实时文档存储,每个字段可以被索引与搜索
  • 一个分布式实时分析搜索引擎
  • 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据

个人理解:

ES的特点就是搜索快;

插入ES的每一条数据可以理解为一个json报文,每个json报文都有一个唯一的id,每次插入就是新增一条json入库;

如果插入时不指定id,则id会自动生成一个新的;

如果插入时指定id,则使用指定的id,并会覆盖之前使用这个id的数据。

ES插入数据前,需要创建索引,ES插入数据时,必须指定索引名;

如果索引存在,则会把数据插入指定索引;

如果索引不存在,则会自动新建一个索引,索引字段为json数据中的字段名。

个人理解,索引就类似数据库中的表。

在此通过对比的方式总结下ES常用方法,从创建索引(表)、插入数据(insert)、删除数据(delete)、修改数据(update)、查询数据(select)等几方面总结。

特别说明:文章所有内容基于ElasticSerch 7.6.2版本;

ES代码样例是在Kibana的Dev Tools中运行的,或者是在Java中运行的。

一、创建索引(表)

1.ES创建索引

ES创建索引,个人感觉就像sql创建数据库中的表。

下方创建一个名称叫exam_data的索引,properties中的为映射(列)名。

ES的创建索引语句例如(POST请求):

PUT exam_data
{
  "settings":{
    "index":{
      "number_of_shards": "1",
      "number_of_replicas": "1"
    }
  },
  
  "mappings": {
    "properties": {
      "userId":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},
      "startTime": {"type":"long"},
      "commitTime": {"type":"long"},
      "duration": {"type":"double"},
      "examStartTime":{"type":"long"},
      "examEndTime":{"type":"long"},
      "examId":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},
      "examName":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},
      "examStyleId":{"type":"long"},
      "examStyleName":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},
      "isPass":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},
      "passScore": {"type":"double"},
      "score": {"type":"double"},
      "times": {"type":"long"},
      "examCreater":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},
      "examCreateTime": {"type":"long"},
      "company":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},
      "com":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},
      "branch":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},
      "brc":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},
      "aracde":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},
      "ara":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},
      "chnl":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},
      "sequence":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},
      "memo":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}}
    }
  }
}

说明:

这个ES索引(ES表)是用来记录用户考试成绩等相关信息的,用户每完成一次考试,后台就会推到ES一条数据。

字段说明:


userId:用户id

startTime:开始考试时间

commitTime:结束考试时间

duration:考试用时

examStartTime:考试开始时间(可能持续一周)

examEndTime:考试结束时间(可能持续一周)

examId:考试id

examName:考试名称

examStyleId:考试规则id(网段限制等)

examSytleName:考试规则名称(网段限制等)

isPass:是否通过考试

passScore:考试通过所需分数

score:考试得分

times:考试次数

examCreater:考试创建人

examCreateTime:考试创建时间

company:一级公司id

com:一级公司名称

branch:二级公司id

brc:二级公司名称

aracde:三级公司id

ara:三级公司名称

chnl:渠道

sequence:队列id(全局唯一id,每一条考试记录有一个id)

memo:备注

2.对应的建表sql,这里写一个简单的例子。(与上方不完全对应,主要记录下大概用法)

create table 'exam_data' (
'id' bigint(20) unsigned not null auto increment,
'sequence' bigint(20) unsigned not null comment '队列id',
'userName' varchar(60) character set utf8mb4 collate utf8mb4_general_ci null default null comment '用户名',
'type' tinyint(3) unsigned null default null,
'start_time' datetime null default null comment '开始时间',
'end_time' datetime null default null comment '结束时间',

'create_time' datetime null default current_timestamp comment '创建时间,自动更新',
'update_time' timestamp null default current_timestamp on update current_timestamp comment '更新时间,自动更新',

primary key ('id'),
index 'sequence' ('sequence') using btree
)
engine=InnoDB
default character set=ut8mb4 collate=utf8mb4_general_ci
comment='考试记录表'
auto_increment=12398
row_format=dynamic
;

备注:

create_time与update_time是自动更新的,插入数据时可以不用管这两个字段;

当数据要记录这两个时间时,比较方便;甚至可以在旧表中直接新增这两个字段,而不用管代码逻辑。

二、索引结构的修改

1.增加索引字段

(1)sql

alter table exam_data add abc varchar(256) default '123'

(2)ES

下方代码在exam_data索引中增加了一个字段abc,并设置了keyword属性。

PUT exam_data/_mappings
{
  "properties": {
    "abc": {
      "type": "text",
      "fields": {
         "keyword": {
            "type": "keyword",
            "ignore_above": 256
         }
      }
    }
  }
}

2.删除索引字段

(1)sql

alter table exam_data drop column abc

(2)ES

好像不能,需要新建一个索引,然后把数据导过去

3.修改索引字段

(1)sql

语法是有,但是自己测试发现报错:

ALTER TABLE exam_data ALTER COLUMN abc varchar(233)

(2)ES

好像不能,需要新建一个索引,然后把数据导过去

4.重新创建索引、导入旧数据的具体流程

参考:https://www.cnblogs.com/Rawls/p/10300639.html

https://www.cnblogs.com/huangxiufen/p/12461191.html

涉及到3个索引,待修改结构的旧索引exam_data、中间索引exam_data_1、新索引exam_data

(1)把旧索引连同结构与数据复制到中间索引(相当于改名字)

POST _reindex
{
   "source": {
     "index": "exam_data"
   },
   "dest": {
     "index": "exam_data_1"
   }
}

(2)删除旧索引

delete exam_data

(3)执行创建新索引exam_data的语句

(4)从中间索引把数据导入新索引

POST _reindex
{
   "source": {
     "index": "exam_data_1"
   },
   "dest": {
     "index": "exam_data"
   }
}

(5)删除中间索引

delete exam_data_1

三、插入数据

1.ES插入数据样例

例如插入数据到索引exam_data中。

(1)使用put请求,根据索引名与id插入数据

PUT exam_data/_doc/1
{
  "id123":"123",
  "examId":"abc",
  "score":"100"
}

说明:

exam_data是索引名(表名),_doc是固定写法,1是指定这条json入库后的id为1;

这条json的字段可以不按照索引中设置的映射字段,例如id123并不在创建索引时索引的映射字段(列)中,也不影响之后根据id123搜索出该条数据;

不过这条json会算在索引exam_data下,当删除该索引时,这条json也会被删除。

(2)使用post请求,根据索引名插入数据

POST exam_data/_doc
{
  "id123":"123",
  "examId":"abc",
  "score":"100"
}

说明:

使用post请求插入数据,不用指定id,插入后会自动生成一个唯一id。

2.sql插入数据样例,简单对比用

insert into mytable(id,name) values('1','abc')

3.Java使用ES插入数据样例

(1)插入数据操作

//注入esclient对象
@Autowired
@Qualifier("restHighLevelClient")
private RestHighLevelClient esClient;

public void insertTOES() {
  //待插入数据
  String data = "{\"id\":\"abc\",\"name\":\"abc\"}";
  //这里指定ES的索引名
  IndexRequest request = new IndexRequest("exam_data");
  //这里指定插入ES的数据的id,也可以不写
  //request.id("1");
  //填充数据
  request.source(data,XContentType.JSON);
  
  //发送请求并获取返回值,esclient是注入的javabean,见下个代码块的创建代码
  IndexResponse index = esclient.index(request,RequestOptions.DEFAULT);
  //如果插入成功,会返回id;如果指定了id,则值与上方指定的id相同
  System.out.println(index.getId());
}

(2)esclient对象初始化方法

@Configuration
public class ElasticsearchRestClientConfig {
  
  //从yml中获取配置信息
  @Value("${es.host}")
  private String host;
  @Value("${es.port}")
  private String port;
  @Value("${es.connTimeout}")
  private String connTimeout;
  @Value("${es.socketTimeout}")
  private String socketTimeout;
  @Value("${es.connectionRequestTimeout}")
  private String connectionRequestTimeout;
  @Value("${es.username}")
  private String username;
  @Value("${es.password}")
  private String password;

  //把这个bean转入spring容器,之后就能@Autowired获取到了
  @Bean
  public RestHighLevelClient restHighLevelClient(){
    BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
    credentialsProvider.setCredentials(AuthScope.ANY,new UsernamePasswordCredentials(username,password));

    RestClientBuilder builder = RestClient.builder(new HttpHost(host,port))
      .setRequestConfigCallback(requestconfigBuilder -> requestConfigBuilder
        .setConnectTimeout(connTimeout)
        .setSocketTimeout(socketTimeout)
        .setConnectionRequestTimeout(connectionRequestTimeout)
      );
    return new RestHighLevelClient(builder);
  }
}

(3)相关application.yml

#es相关参数,名称不固定,自己用的
es: 
  host: 10.123.123.123
  port: 9200
  scheme: http
  connTimeout: 5000000
  socketTimeout: 3000000
  connectionRequestTimeout: 500
  #用户名与密码,ES允许不设置用户名密码登录,不过不安全;所以最好还是设置一个
  username: abc
  password: abc

四、删除数据

1.ES删除数据

(1)根据查询条件删除

POST exam_data/_delete_by_query
{
  "query": {
    "term": {
      "examId": "abc"
    }
  }
}

说明:

exam_data是索引名,_delete_by_query是固定写法;

该语句删除所有examId等于abc的数据。

(2)根据id删除

DELETE exam_data/_doc/1

该语句根据每条数据的唯一id删除数据。

删除了id为1的数据。

2.sql删除数据样例

delete from exam_data where id = '1'

五、修改数据

1.ES修改数据

(1)先找到目标数据的id,然后新增一条相同id的数据,即可覆盖掉旧数据

(2)先找到目标数据的id,然后进行修改

POST exam_data/_update/1
{
  "doc": {
    "examId":"abcdefg"
  }
}

这个语句将id为1的数据的examId修改为abcdefg.

(3)按照查询条件进行修改

POST exam_data/_update_by_query
{
  "query":{
    "term":{
      "examId":"abcdefg"
    }
  },
  "script":{
    "source": "ctx._source['concact']=\"ccc\""
  }
}

这个语句,将把examId为abcdefg的所有数据的concact字段修改为ccc;如果没有concact字段,则会新增该字段并设置为ccc;

_update_by_query为固定写法(更新by查询),ctx._source为固定写法(指向当前要被更新的文档)。

2.sql修改数据

update exam_data e set e.concact = 'ccc' where e.examId = 'abcdefg'

六、查询数据

1.ES查询数据

GET exam_data/_search
{
  "query":{
    "bool": {
      "must" : [
        {
          "term": {
             "examId.keyword": {
                "value": "abcdefg"
             }
          }
        }
      ],
      "adjust_pure_negative": true,
      "boost": 1
    }
  }
}

这个语句可以匹配到所有examId为abcdefg的数据,返回结果默认返回10条(设置方法不在此处讲解),返回的hits字段是匹配数量,可以知道究竟有多少条数据符合此条件。

2.sql查询数据

select * from exam_data where examId = 'abcdefg'

七、ES复杂场景查询样例

首先,用到了自己封装的2个公共方法,如下:

●CommonUtil.getCommonBoolQueryBuilder(),这个方法是获取查询条件对象BoolQueryBuilder用的;其中有一些设置条件的方法,可以参考。

public static BoolQueryBuilder getCommonBoolQueryBuilder(RestHighLevelClient esclient, BoolSearchVo data, String startTime, String endTime) throws Exception{
  //所有查询条件都设置在这个bean中
  BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

  //设置查询考试时长小于2小时的数据;大于2小时的舍弃,为错误数据
  //数据已转为分钟,所以查询duration小于120的
  boolQueryBuilder.must(QueryBuilders.rangeQuery("duration").lt(2*60));
  
  //根据考试名称或ID,模糊查询,如果有重复,则选取最新的一场考试的数据
  String examNameOrId = data.getExamNameOrId();
  if(StringUtils.isNotEmpty(examNameOrId)){
    //先根据考试名称或ID,模糊查询出最新一场考试的ID
    //getLatestByIdOrName在下面的代码块中,可以页面搜索找到
    String examId = getLatestByIdOrName(esclient, examNameOrId);
    if(StringUtils.isNotEmpty(examId)){
      boolQueryBuilder.must(QueryBuilders.termQuery("examId.keyword",examId));
    }else{
      boolQueryBuilder.must(QueryBuilders.termQuery("examId.keyword",examNameOrId));
    }
  }

  //根据机构填写查询条件
  //机构有3级,用_分割,多选时逗号拼接,机构可能的形式为
  //1,2_1,3_4_5
  String instituteId = data.getInstituteId();
  if(StringUtils.isNotEmpty(instituteId)){
    String instituteIds = instituteId.split(",");
    //这个if也许不用写,多余了...
    if(instituteIds.length == 1){
      if(instituteIds.split("_").length == 1){
        boolQueryBuilder.must(QueryBuilders.termQuery("company.keyword",instituteId));
      }else if(instituteIds.split("_").length == 2){
        String company = instituteIds.split("_")[0];
        String branch = instituteIds.split("_")[1];
        boolQueryBuilder.must(QueryBuilders.termQuery("company.keyword",company));
        boolQueryBuilder.must(QueryBuilders.termQuery("branch.keyword",branch));
      }else if(instituteIds.split("_").length == 3){
        //本来应该是拆开查询company,branch,aracde的
        //不过数据中有一个字段是这样类型的,如3_4_5
        //如果某些人只属于一级公司或二级公司,会是1_*_*,*_*_*
        boolQueryBuilder.must(QueryBuilders.termQuery("instituteId.keyword",instituteId));
      }
    }
    else{
      //如果是多选查询,用bool嵌套的方法
      //注意,必须嵌套,直接用should查的结果有问题,不行
      BoolQueryBuilder boolShould = QueryBuilders.boolQuery();
      //1,a_1,_b_1_2
      for(int i=0; i<instituteIds.length; i++){
        //company,branch,aracde
        String[] cba = instituteIds[i].split("_");
        if(cba.length == 1 || cba.length == 2){
          //ES的正则,查询开头为某格式的,例如开头为1_的或2_3_的
          boolShould.should(QueryBuilders.wildcardQuery("instituteId.keyword",instituteIds[i]+"_*"));
        }
        else if(cba.length == 3){
          boolShould.should(QueryBuilders.termQuery("instituteId.keyword",instituteIds[i]));
        }
      }
      //放入查询bean,实现嵌套bool
      boolQeuryBuilder.must(boolShould);
    }
  }

  //根据渠道筛选,除了机构,考生还可能属于某个渠道
  //渠道为1-9,a-z,单个字符,逗号拼接
  //1,2,a,b,3
  //使用filter与termsQuery实现
  String chnl = data.getChnl();
  if(StringUtils.isNotEmpty(chnl)){
    String[] chnls=chnl.spilt(",");
    boolQueryBuilder.filter(QueryBuilders.termsQuery("chnl.keyword",chnls));
  }

  //根据考生提交日期筛选
  if(StringUtils.isNotEmpty(startTime) && StringUtils.isNotEmpty(endTime) {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    long st = sdf.parse(startTime).getTme();
    //结束日期是当日0时0分0秒,因此要加成23时59分59秒
    long et = sdf.parse(endTime).getTime()+(24*60*60*1000-1);
    
    boolQueryBuilder.must(QueryBuilders.rangeQuery("commitTime").from(st).to(et));
  }

  return boolQueryBuilder;
}

●CommonUtil.getSearchResponse(),用来获取返回结果:

//公用搜索方法
public static SearchResponse getSearchResponse(RestHighLevelClient esclient, BoolQueryBuilder boolQueryBuilder, AggregationBuilder aggregationBulder) throws Exception {
  //要存入的索引名(可以理解为表名)
  SearchRequest request = new SearchRequest("exam_data");
  
  SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
  //放入查询条件对象,这个一般都有值,因此没有判空
  searchSourceBuilder.query(boolQueryBuilder);
  //放入聚合函数,进行了判空
  if(aggregationBuilder != null) {
    searchSourceBuilder.aggregation(aggregationBuilder);
  }
  
  request.source(searchSourceBuilder);

  //这个日志中打印出来的语句可以放到kibana运行
  log.debug("ES请求报文:"+ request.toString());

  SearchResponse response = esclient.search(request, RequestOptions.DEFAULT);

  //这个可以看到ES返回的json报文
  log.debug("ES响应:"+ response.toString());

  return response;
}

1.根据id或名称,模糊查询数据,如果有多条重复,选取最新的一条。

分两步,先模糊查询,获得最新的id,然后根据此id获取其余数据。(ES一步到位较难,因此使用这个方法)

ES使用嵌套bool、aggregations聚合实现。

(1)对应sql:

select id from exam_data where (examName like '%考试%' or examId like "%考试%") order by create_time desc limit 1
select * from exam_data where id = '查询得到的id'

(2)对应ES:分两步,第一步,根据传入的值模糊查询,得到最新一条的id。

GET exam_data/_search
{
  "query": {
    "bool": {
      "must": [
        "bool": {
          "should": [
             {
               "match_phrase_prefix":{
                  "examName": {
                     "query": "考试",
                     "slop": 0,
                     "max_expansions": 50,
                     "boost": 1
                  }
               }
             },
             {
               "match_phrase_prefix":{
                  "examId": {
                     "query": "考试",
                     "slop": 0,
                     "max_expansions": 50,
                     "boost": 1
                  }
               }
             }
          ],
          "adjust_pure_negative": true,
          "boost": 1
        }
      ],
      "adjust_pure_negative": true,
      "boost": 1
    }
  },
  "aggregations": {
     "examStartTime": {
       "terms": {
         "field": "examStartTime",
         "size": 1,
         "min_doc_count": 1,
         "shard_min_doc_count": 0,
         "show_term_doc_count_error": false,
         "order":{
            "_key": "desc"
         }
       },
       "aggregations": {
          "examId": {
            "terms": {
              "field":"examId.keyword",
              "size":10,
              "min_doc_count": 1,
              "shard_min_doc_count": 0,
              "show_term_doc_count_error": false,
              "order":[
                {
                  "_count": "desc"
                },
                {
                  "_key": "asc"
                }
              ]
            }
          }
       }

     }
  }
}

说明:

只用2个should不能实现需求,必须使用bool嵌套should才行,如上方使用了2个bool。

order中的_key:desc可以按key的降序排序,例如examStartTime(long)的降序;再有size:1,就是最大的了。

第一次聚合是为了按时间聚合、降序、得到时间最新的数据;第二次聚合是为了得到examId;只用第一次聚合的话,ES返回的内容中只有examStartTime和聚合数,得不到examId,所以要用第二次聚合。

ES第二步,根据id查询目标数据,如果为空也没关系,查询到0条就可以:

GET exam_data/_search
{
  "query": {
    "bool": {
      "must": [
        {
           "term": {
             "examId.keyword":{
                 "value": "得到的id",
                 "boost": 1
             }
           }
        }
      ],
      "adjust_pure_negative": true,
      "boost": 1
    }
  },
  "aggregations": {
          "zrs": {
            "cardinality": {
              "field":"userId.keyword"
           }
         }
   }
}

说明:ES中,如果要获取某条数据的某个信息,最好还是使用聚合aggregations,聚合某字段,然后从桶里找值,而不是从hits中找,因为hits返回的有时候不准确。

上方的语句,筛选条件为某个指定的examId,然后用cardinality对结果的userId去重,可以从返回的结果桶中得到考试人数。

(3)对应java,功能为根据考试id或考试名称,获取到最新的考试的考试人数:

先获取最新的考试ID:

//获取最新的考试ID
private static String getLatestIdByIdOrName(String examNameOrId, RestHighLevelClient esclient) throws Exception{

String examNameOrId = "考试";

//模糊查询ES,获取最新的考试id
String[] result = new String[1];
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
BoolQueryBuilder boolShould = QueryBuilders.boolQuery();
boolShould.should(QueryBuilders.matchPhrasePrefixQuery("examName",examNameOrId));
boolShould.should(QueryBuilders.matchPhrasePrefixQuery("examId",examNameOrId));
boolQueryBuilder.must(boolShould);

//聚合,按时间逆序,选1条就够
TermsAggregationBuilder termsTime = AggregationBuilder.terms("examStartTime").field("examStartTime").order(InternalOrder.key(false).size(1));
//聚合,用来获取examId的聚合
TermsAggregationBuilder termsId = AggregationBuilder.terms("examId").field("examId.keyword");
termsTime.subAggregation(termsId);

//从结果找到examId, getSearchResponse是自己封装的方法,见下
SearchResponse searchResponse = getSearchResponse(esclient, boolQueryBuilder, termsTime);
ParsedLongTerms parsedExamStartTime = searchResponse.getAggregations().get("examStartTime");
parsedExamStartTime.getBuckets().forEach( bucket -> {
  ParsedLongTerms parseExamId = bucket.getAggregations().get("examId");
  parseExamId.getBuckets().forEach( bucket2 -> {
    //因为设置了size为1,所以正常情况下只循环一次
    result[0] = bucket2.getKeyAsString();
  });
});
return result[0];
}

然后根据id,按userId聚合,查询最新一场考试的考试人数:

//返回最新一场考试的考试总人数
public String searchZrs(BoolSearchVo data) throws Exception {
  //使用cardinality按userId去重
  //zrs是自己起的桶名
  //userId是数据的字段,需要加keyword
  CardinalityAggregationBuilder aggregationBuilder = AggregationBuilders.cardinality("zrs").field("userId.keyword");
  //获取公共查询的参数
  BoolQueryBuilder bqb = CommonUtil.getCommonBoolQueryBuilder(esclient, data, data.getStartTime(), data.getEndTime());
  //获取返回结果
  SearchResponse searchResponse = CommonUtil.getSearchResponse(esclient, bqb, aggregationBuilder);

  //获取总人数
  Cardinality valueCount = searchResponse.getAggregations().get("zrs");
  String count = String.valueOf(valueCount.getValue());

  return count;
}

2.查询某个数字字段的和

(1)sql样例

select sum(a.duration) from exam_data a where DATE_FORMAT(a.commit_time,'%Y-%m-%d') between '2021-01-01' and '2021-02-01'

(2)ES样例,使用聚合sum

GET exam_data/_search
{
  "query": {
    "bool": {
      "must": [
        {
           "range": {
             "commitTime":{
                 "from": "1624896000000",
                 "to": "1624996000000",
                 "include_lower":true,
                 "include_upper":true,
                 "boost": 1
             }
           }
        }
      ],
      "adjust_pure_negative": true,
      "boost": 1
    }
  },
  "aggregations": {
          "kssc": {
            "sum": {
              "field":"duration"
           }
         }
   }
}

(3)java样例

//考试时长
public String searchkssc(BoolSearchVo data) throws Exception {
  //聚合sum
  SumAggregationBuilder aggregationBuilder = AggregationBuilders.sum("kssc").field("duration");
  //获取公共查询的参数
  BoolQueryBuilder bqb = CommonUtil.getCommonBoolQueryBuilder(esclient, data, data.getStartTime(), data.getEndTime());
  //获取返回结果
  SearchResponse searchResponse = CommonUtil.getSearchResponse(esclient, bqb, aggregationBuilder);

  //获取考试时长
  Sum valueCount = searchResponse.getAggregations().get("kssc");
  //从分钟转为小时
  double value = valueCount.getValue();
  //保留2位小数,四舍五入
  String duration = new BigDecimal(vaule/60).setScale(2,RoundingMode.HALF_UP).toString();

  return duration;
}

3.按某个字段排序、查询前三名数据

具体功能:按company分组,查询考试次数,取前三名;并查询对应的考试人数(按userId去重后的数量)。

(1)sql样例:

按company分组,查询考试次数,取前三名:

select company,count(*) from exam_data group by company order by count(*) desc limit 0,3

limit 3或limit 0,3

得到company后,再查询对应的考试人数(按userId去重)

select count(*) from exam_data group by user_id where company = 'A';
select count(*) from exam_data group by user_id where company = 'B';
select count(*) from exam_data group by user_id where company = 'C';

然后就能得到A,B,C的考试次数与考试人数。

(2)ES样例:

注意,与sql查询的顺序不太一样,一个ES就够:

按company分组,按桶的数量逆序取前3名,得到了前三名的company,在第一层桶的key中;

然后从第一层桶的doc_count中,就能获得考试次数;

然后按userId分组,是第二层桶,获得第二层桶buckets[]数组的长度,就是考试人数(相当于按userId去重了)。

GET exam_data/_search
{
  "query": {
    "bool": {
      "must": [
        {
           "range": {
             "commitTime":{
                 "from": "1624896000000",
                 "to": "1624996000000",
                 "include_lower":true,
                 "include_upper":true,
                 "boost": 1
             }
           }
        }
      ],
      "must_not": [
        {
          "term": {
            "company.keyword" : {
              "value": "*",
              "boost": 1
            }
          }
        }
      ],
      "adjust_pure_negative": true,
      "boost": 1
    }
  },
  "aggregations": {
          "fgsGroup": {
            "terms": {
              "field":"company.keyword",
              "size": 3,
              "min_doc_count": 1,
              "shard_min_doc_count": 0,
              "show_term_doc_count_error": false,
              "order": [
                {
                  "_count": "desc"
                },{
                  "_key": "asc"
                }
              ]
           },
           "aggregations": {
             "userIdGroup": {
                "terms": {
                  "field":"userId.keyword",
                  "size": 10,
                  "min_doc_count": 1,
                  "shard_min_doc_count": 0,
                  "show_term_doc_count_error": false,
                  "order": [
                    {
                      "_count": "desc"
                    },{
                      "_key": "asc"
                    }
                  ]
               }
             }
           }

         }
   }
}

(3)java样例:

//考试排行,返回考试次数前三名list,每个list的bean中有公司名、考试次数、考试人数
public ArrayList<RankVo> searchkszph(BoolSearchVo data) throws Exception {
  //首先根据分公司分组,取前三名,就是按考试次数排名的前三名
  TermsAggregationBuilder tb1 = AggregationBuilders.terms("fgsGroup").field("company.keyword").order(InternalOrder.count(false)).size(3);
  //然后按userId分组
  TermsAggregationBuilder tb2 = AggregationBuilders.terms("userIdGroup").field("userId.keyword");
  //tb2为tb1的子桶
  tb1.subAggregation(tb2);
  //获取公共查询的参数
  BoolQueryBuilder bqb = CommonUtil.getCommonBoolQueryBuilder(esclient, data, data.getStartTime(), data.getEndTime());
  //排除分公司为*的数据
  bqb.mustNot(QueryBuilders.termQuery("company.keyword","*"));

  //获取返回结果
  SearchResponse searchResponse = CommonUtil.getSearchResponse(esclient, bqb, tb1);

  ArrayList<RankVo> returnList = new ArrayList<>();

  //以下省略,从searchResponse中遍历桶获得结果即可
  //可以打断点查看返回的string是什么样的
  //第一层桶buckets[]的key是company
  //第一层桶buckets[]的doc_count是考试次数
  //第二层桶buckets[]的数组长度是考试人数(人数的意思就是次数按userId去重)
  
  return returnList;
}

4.filter与terms实现匹配多个条件

例如,匹配渠道chnl是1或2或3的

(1)sql样例

select count(*) from exam_data where chnl in ('1','2','3')

(2)ES样例

GET exam_data/_search
{
  "query": {
    "bool": {
      "filter": [
        {
           "terms": {
             "chnl.keyword":[
                 "1",
                 "2",
                 "3"
             ],
             "boost": 1
           }
        }
      ],
      "adjust_pure_negative": true,
      "boost": 1
    }
  }

}

(3)java样例(详情见上方类似的代码块):

  //根据渠道筛选,除了机构,考生还可能属于某个渠道
  //渠道为1-9,a-z,单个字符,逗号拼接
  //1,2,a,b,3

  //使用filter与termsQuery实现,注意是terms Query

  String chnl = data.getChnl();
  if(StringUtils.isNotEmpty(chnl)){
    String[] chnls=chnl.spilt(",");
    boolQueryBuilder.filter(QueryBuilders.termsQuery("chnl.keyword",chnls));
  }

5.bool嵌套实现匹配多个条件

例如,查询

(company是a的) 或 (company是b、branch是1的) 或 (company是c、branch是2、aracde是3的)

这样一组数据。

(1)sql样例

select * from exam_data where  (company in (a,b,c)) or 
( concat(company,'_',branch) in ('d_1','e_2') ) or
( concat(company,'_',branch,'_',aracde) in ('f_2_3','g_4_5') ) 

(2)ES样例

说明:

注意ES的wildcard正则表达式与普通的不太一样;

a_*的意思是匹配以a_开头的所有数据;

b_1_*是匹配以b_1_开头的所有数据;

term就是匹配完全相等的数据了;

报文结构:bool->must->bool->should,即为bool嵌套。

可以匹配company为

GET exam_data/_search
{
  "query": {
    "bool": {
      "must": [
       {
        "bool": {
          "should": [
             {
               "wildcard":{
                 "instituteId.keyword": {
                    "wildcard": "a_*",
                    "boost": 1
                 }
               }
             },
             {
               "wildcard":{
                 "instituteId.keyword": {
                    "wildcard": "b_1_*",
                    "boost": 1
                 }
               }
             },
             {
               "term":{
                 "instituteId.keyword": {
                    "value": "E_3_5",
                    "boost": 1
                 }
               }
             },
             {
               "term":{
                 "instituteId.keyword": {
                    "value": "F_5_6",
                    "boost": 1
                 }
               }
             }
             
          ],
          "adjust_pure_negative": true,
          "boost": 1
         }
       }
      ],
      "adjust_pure_negative": true,
      "boost": 1
    }
  }

}

(3)java样例(详情见上方相似代码块)

//根据机构填写查询条件
  //机构有3级,用_分割,多选时逗号拼接,机构可能的形式为
  //1,2_1,3_4_5
  String instituteId = data.getInstituteId();
  if(StringUtils.isNotEmpty(instituteId)){
    String instituteIds = instituteId.split(",");
    //这个if也许不用写,多余了...
    if(instituteIds.length == 1){
      if(instituteIds.split("_").length == 1){
        boolQueryBuilder.must(QueryBuilders.termQuery("company.keyword",instituteId));
      }else if(instituteIds.split("_").length == 2){
        String company = instituteIds.split("_")[0];
        String branch = instituteIds.split("_")[1];
        boolQueryBuilder.must(QueryBuilders.termQuery("company.keyword",company));
        boolQueryBuilder.must(QueryBuilders.termQuery("branch.keyword",branch));
      }else if(instituteIds.split("_").length == 3){
        //本来应该是拆开查询company,branch,aracde的
        //不过数据中有一个字段是这样类型的,如3_4_5
        //如果某些人只属于一级公司或二级公司,会是1_*_*,*_*_*
        boolQueryBuilder.must(QueryBuilders.termQuery("instituteId.keyword",instituteId));
      }
    }
    else{
      //如果是多选查询,用bool嵌套的方法
      //注意,必须嵌套,直接用should查的结果有问题,不行
      BoolQueryBuilder boolShould = QueryBuilders.boolQuery();
      //1,a_1,_b_1_2
      for(int i=0; i<instituteIds.length; i++){
        //company,branch,aracde
        String[] cba = instituteIds[i].split("_");
        if(cba.length == 1 || cba.length == 2){
          //ES的正则,查询开头为某格式的,例如开头为1_的或2_3_的
          boolShould.should(QueryBuilders.wildcardQuery("instituteId.keyword",instituteIds[i]+"_*"));
        }
        else if(cba.length == 3){
          boolShould.should(QueryBuilders.termQuery("instituteId.keyword",instituteIds[i]));
        }
      }
      //放入查询bean,实现嵌套bool
      boolQeuryBuilder.must(boolShould);
    }
  }

6.聚合嵌套实现查询多个字段的值

例如查询公司ID company与公司中文名称com;

在ES中,一个聚合只能查询一个字段,因此要聚合嵌套,才能查询其余字段

(1)sql样例:

select company,com from exam_data group by company,com

(2)ES样例:

GET exam_data/_search
{
  "query": {
    "bool": {
      "must": [
        {
           "range": {
             "commitTime":{
                 "from": "1624896000000",
                 "to": "1624996000000",
                 "include_lower":true,
                 "include_upper":true,
                 "boost": 1
             }
           }
        }
      ],
      "adjust_pure_negative": true,
      "boost": 1
    }
  },
  "aggregations": {
          "fgsGroup": {
            "terms": {
              "field":"company.keyword",
              "size": 99999,
              "min_doc_count": 1,
              "shard_min_doc_count": 0,
              "show_term_doc_count_error": false,
              "order": [
                {
                  "_count": "desc"
                },{
                  "_key": "asc"
                }
              ]
           },
           "aggregations": {
             "userIdGroup": {
                "terms": {
                  "field":"com.keyword",
                  "size": 99999,
                  "min_doc_count": 1,
                  "shard_min_doc_count": 0,
                  "show_term_doc_count_error": false,
                  "order": [
                    {
                      "_count": "desc"
                    },{
                      "_key": "asc"
                    }
                  ]
               }
             }
           }

         }
   }
}

这样得到的返回结果,是先按company聚合、第一个桶的key即是company的值;

然后按com聚合,第二个桶的key即是com的值;

因为一个company对应一个com,因此每个company桶中的子桶、com的数量正常情况下也是一个;

然后java中遍历、获取返回结果,就能得到这两个字段的值了。

(3)java样例:

  //准备获取company的值,首先根据company分组
  TermsAggregationBuilder tb1 = AggregationBuilders.terms("fgsGroup").field("company.keyword").order(InternalOrder.count(false)).size(3);
  //准备获取com的值,然后根据com分组
  TermsAggregationBuilder tb2 = AggregationBuilders.terms("userIdGroup").field("com.keyword");
  //tb2为tb1的子桶
  tb1.subAggregation(tb2);

  //返回结果遍历桶即可,可参考上方代码,此处省略
  ......

八、其它ES常用操作

1.清空数据及索引

(1)类似sql的drop table:

drop table exam_data

(2)清空数据及索引的ES命令如下:

delete 索引名称

2.只清空数据

(1)类似sql的delete命令,不加条件即是所有数据:

delete from exam_data 

(2)ES样例:

POST /exam_data/_delete_by_query?pretty
{
    "query": {
        "match_all": {
        }
    }
}

3.复制索引及数据

(1)sql样例

CREATE TABLE 新表 SELECT * FROM 旧表

(2)ES样例

POST _reindex{
        "source": {
            "index": "exam_data" //待复制索引
        },
        "dest": {
            "index": "new_exam_data"//目标索引
        }
    }

4.仅复制索引

(1)sql样例

CREATE TABLE 新表 SELECT * FROM 旧表 WHERE 1=2

(2)ES样例,暂时没有找到;

下方的代码可以把_id为1的数据与索引结构复制到new_exam_data;

只有1条数据,复制完了后手动清空索引数据吧。

如果没有_id为1的数据,则复制失败,索引也不会被创建,所以应该选择一个存在的id。

POST _reindex{
        "source": {
            "index": "exam_data", //待复制索引
            "query": {
              "match": {
                 "_id": "1"
              }
            }
        },
        "dest": {
            "index": "new_exam_data"//目标索引
        }
    }

九、其它相关笔记,ES条件查询、ES聚合等样例

1.需要注意的点

●matchPhrasePrefixQuery查询text类型字段时,不能加keyword,否则会报错

●termQuery、termsQuery、wildcardQuery查询text类型字段时,应该加keyword,不加的话查询结果会不准确,会漏查部分数据

●rangeQuery查询时,不能加keyword,加上的话会查询不到数据

●创建索引时,如果是text类型的字段,最好加上keyword,如下:

"examId":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above": 256}}}

●查询或聚合text字段类型时,应该加keyword,不加的话返回结果会不准确

●查询或聚合非text字段类型时,不用加keyword

2.多条件查询三级公司的sql写法

●需求:公司有三个级别,company,branch,aracde

要求前端使用一个字段查询,可以多选,因此实际参数可能的类型是:

a,b_1,c_2_3

●误区:之前认为sql太复杂,因此使用了$进行sql拼接,结果被查出来sql注入的问题了;后来又想了想,还是可以用#实现的;也是在java中处理、然后sql中拼接字段。

看来,前端传入参数决定sql的时候,一定不能用$,必须想办法用#,否则会sql注入的。

●实现:

@Select({

  "<script>",
  "select commit_time as group_date, count(*) as exam_count  ",
  
  "where 1=1 ",
  "<if test='instituteId !=null and instituteId != \"\" and instituteId!=undefined '> ",
  "and ( ",
  "1=2 ",
  
  "<if test='instituteId1 !=null and instituteId1 != \"\" and instituteId1!=undefined '> ",
  "or e.company in ",
  "<foreach collection='instituteId1' item='iid1' open='(' separator=',' close=')' >",
  "#{iid1}",
  "</foreach>",
  "</if>", 

    "<if test='instituteId2 !=null and instituteId2 != \"\" and instituteId2!=undefined '> ",
  "or CONCAT(e.company,'_',e.branch) in ",
  "<foreach collection='instituteId2' item='iid2' open='(' separator=',' close=')' >",
  "#{iid2}",
  "</foreach>",
  "</if>", 

    "<if test='instituteId3 !=null and instituteId3 != \"\" and instituteId3!=undefined '> ",
  "or CONCAT(e.company,'_',e.branch,'_',e.aracde) in ",
  "<foreach collection='instituteId3' item='iid3' open='(' separator=',' close=')' >",
  "#{iid3}",
  "</foreach>",
  "</if>", 

  ")</if>",

  "<if test='flag == 1'>"
  "and CONCAT(e.company,'_',e.branch)!='A_60' ",
  "</if>",

  "group by DATE_FORMAT(e.commit_time,'%Y-%m-%d') ",
  "order by group_date ",
  "</script>"
})
@Results({
@Result(column = "group_date",property="date",jdbcType=JdbcType.VARCHAR),
@Result(column = "exam_count",property="value",jdbcType=JdbcType.VARCHAR)
})
List<Map> getExamCountByParamGroupByDay(Map map);

说明:

●1=1是为了之后拼接and不会报错

●1=2是为了之后拼接or不会报错

●test是一个=,test内的条件是==,或!=

●前端传来的instituteId可能是a,b_1,c_2_3,所以需要java中进行分类(java代码省略),没有_的放入instituteId1,有一个_的放入instituteId2,有2个_的放入instituteId3,再对应外部and、内部or、有concat的sql即可实现

●注意foreach用的参数需要是数组类型

3.ES的两种排序:

(1)按照桶中数据的数量排序,相当于group by后,按count排序,如下:

//根据company分组,取前三名,就是按考试次数排名的前三名
//count是桶中数据的数量,false是逆序
  TermsAggregationBuilder tb1 = AggregationBuilders.terms("fgsGroup").field("company.keyword").order(InternalOrder.count(false)).size(3);

(2)按桶字段值排序,例如时间字段,可以选择最新/旧的数据,如下:

//按时间逆序,获取最新一条数据;examStartTime是long类型,不加keyword
//false是逆序
  TermsAggregationBuilder tbTime = AggregationBuilders.terms("mytime").field("examStartTime").order(InternalOrder.key(false)).size(1);

4.ES查询返回数据不准确问题与解决方法

ES在使用terms聚合查询时,由于多分片取数据的关系,返回数据可能不准确。

因此,在对数据准确性要求高、对响应速度要求不高的情况下,可以调高请求报文中的shard_size与size参数,让返回数据更准确;

需要注意的是shard_size与size都需要调高,只调高一个是不够的。

ES代码如下(仅展示aggregations的terms部分):

"aggregations": {
  "my_group_name": {
    "terms": {
      "field": "company.keyword",
      "size": 99999,
      "shard_size": 99999,
      "min_doc_count": 1,
      "shard_min_doc_count": 0,
      "show_term_doc_count_error": false,
      "order": [
        {
          "_count": "desc"
        },{
          "_key": "asc"
        }
      ]
    }
  }
}

java代码如下:

//递归设置terms聚合的size与shard_size
public static void useMaxSize(AggregationBuilder ab){
  if(ab != null) {
    if(ab instanceof TermsAggregationBuilder) {
      ab.size(Integer.MAX_VALUE);
      ab.shardSize(Integer.MAX_VALUE);
    }
    Collection<AggregationBuilder> subs = ab.getSubAggregations();
    for(AggregationBuilder subAb : subs) {
      useMaxSize(subAb);
    }
  }
}

十、开发中遇到的问题与解决方法

本次开发了一个数据统计系统(test-cockpit),用到了ES;每个考生考完一次考试,就会把消息推送给ES;然后有一个前端页面可以展示按要求从ES查询到的数据,例如考试数、考试人数、考试次数、考试时长等。

遇到的问题与解决方法如下:

1.swagger,在后端使用这个jar包后,可以访问指定网页,得到后端现有的controller的url与出参入参,便于前端查看;

springboot项目中,默认访问的网页是:http://localhost:8080/swagger-ui.html

(或:http://localhost:8080/swagger/index.html)

2.kafka不用在控制台手动创建队列,直接使用即可;但是使用新队列时,需要生产者先向该队列发送一条消息,然后消费者才能启动成功;如果消费者先启动并监听新队列,会报错(队列不存在导致):

nested exception is java.lang.IllegalStateException: Topic(s) [test-my_kafka_Topic] is/are not present and missingTopicFatal is true

如果必须先启动消费者,那要在yml的kafka配置中增加:

missing-topics-fatal: false

3.将父类bean转换为子类bean的方法

(1)利用json

String jsonStr = JSONObject.toJSONString(fatherBean);
ChildBean cb = JSONObject.parseObject(jsonStr, ChildBean.class);

(2)使用BeanUtils

BeanUtils.copyProperties(fatherBean, childBean);

4.输出流输出的数据不正确问题

使用输出流输出excel文件时,用到了ServletOutputStream,发现使用该对象输出时,必须使用3个参数的输出方法,输出内容才正确;不能使用1个参数的输出方法,输出的内容打不开。如下:

FileInputStream inputStream = null;
ServletOutputStream outputStream = null;

try{

//假设从硬盘读excel,准备返回给用户
inputStream = new FileInputStream("D://abc.xls");
int l;
//jar包中就是1024,所以这个没问题
byte[]i = new byte[1024];
//这个response是controller中的ServletHttpResponse
outputStream = response.getOutputStream();
while((l = inputStream.read(i)) != -1){
  //这里必须是三个参数,输出才正确;否则输出的excel打不开
  outputStream.write(i,0,l);
  
  //这个不行
  //outputStream.write(i);
}
outputStream.flush();
logger.info("导出成功");
return "导出成功";

}catch(Exception e){
  logger.error("导出失败!",e);
  return "导出失败,请稍后再试!";
}

//标准关流方法
finally {

if(outputStream != null){
  try{
    outputStream.close();
  }catch(IOException e){
    logger.error("outputStream关闭失败");
  }
}
if(inputStream != null){
  try{
    inputStream.close();
  }catch(IOException e){
    logger.error("inputStream关闭失败");
  }
}

}

5.获得机构树的一种方法

(1)数据库表中,存储着三级的机构树,例如:

idcompanycombranchbrcaracdeara
19北京分公司NULLNULLNULL
239北京分公司15朝阳NULLNULL
249北京分公司16昌平NULLNULL
339北京分公司16昌平102阳高中路支公司
349北京分公司16昌平103曹路支公司

想要获得机构树层级的map,首先要在sql中按规则查询,然后java里处理;

(2)sql如下:

select * from 
(select * from company where branch ="" and aracde ="NULL" order by company asc) a union
(select * from company where branch !="" and aracde ="NULL" order by branch asc) b union
(select * from company where branch !="" and aracde !="NULL" order by aracde asc) c
 

这个sql可以查询出一级分公司、二级分公司、三级分公司,然后用union,把结果集按顺序拼起来,最后返回一个List<Company>对象。(Company是javabean,对应数据库的company表)

(3)java如下:

public List selectCompanyTree(){
  //通过respository,执行sql,获取结果
  List<Company> companys = companyRespository.findCompanyTree();
  //准备返回的机构树
  ArrayList<InstituteVo> treeList = new ArrayList<>();
  //准备一个map
  HashMap<String,InstituteVo> instituteVoMap = new HashMap<>();

  //开始处理返回结果,返回结果有序,第一部分为一级,第二部分为二级,第三部分为三级
  for(Company c : companys){
    //如果是一级
    if(StringUtils.isEmpty(String.valueOf(c.getBranch()).trim()) && "NULL".equals(c.getAracde())){
      //先判断map中是否存在,如果不存在才处理,如果存在,说明处理过了,不做处理
      if(instituteVoMap.get(c.getCompany()) == null){
        instituteVo level1Vo = new instituteVo(c.getCompany(),c.getCom(),new ArrayList<>());
        //存入map
        instituteVoMap.put(c.getCompany(),level1Vo);
        //存入返回的list
        treeList.add(level1Vo);
      }
    }
    //如果是二级
    else if(StringUtils.isNotEmpty(String.valueOf(c.getBranch()).trim()) && "NULL".equals(c.getAracde())){
      //先判断map中是否存在,如果不存在才处理,如果存在,说明处理过了,不做处理
      if(instituteVoMap.get(c.getCompany()+"_"+c.getBranch()) == null){
        instituteVo level2Vo = new instituteVo(c.getCompany()+"_"+c.getBranch(),c.getBrc(),new ArrayList<>());
        //获取一级的对象
        instituteVo level1Vo = instituteVoMap.get(c.getCompany());
        //如果一级对象还没有处理,那就特殊处理,创建一个
        if(level1Vo == null){
          instituteVo level1Vo = new instituteVo(c.getCompany(),c.getCom(),new ArrayList<>());
          //存入map
          instituteVoMap.put(c.getCompany(),level1Vo);
          //存入返回的list
          treeList.add(level1Vo);
        }
        //装入sublist
        level1Vo.getChildren().add(level2Vo);
        //存入map
        instituteVoMap.put(c.getCompany()+"_"+c.getBranch(),level2Vo);
      }
    }
    //如果是三级
    else if(StringUtils.isNotEmpty(String.valueOf(c.getBranch()).trim()) && !"NULL".equals(c.getAracde())){
      instituteVo level3Vo = new instituteVo(c.getCompany()+"_"+c.getBranch()+"_"+c.getAracde(),c.getAra(),null);
      //找到二级的对象
      instituteVo level2Vo = instituteVoMap.get(c.getCompany()+"_"c.getBranch());
      //如果二级对象还没有处理,就特殊处理
      if(level2Vo == null){
          level2Vo = new instituteVo(c.getCompany()+"_"+c.getBranch(),c.getBrc(),new ArrayList<>());
          //找到一级的对象
          instituteVo level1Vo = instituteVoMap.get(c.getCompany());
          //如果一级对象还没有处理,那就特殊处理,创建一个
        if(level1Vo == null){
          instituteVo level1Vo = new instituteVo(c.getCompany(),c.getCom(),new ArrayList<>());
          //存入map
          instituteVoMap.put(c.getCompany(),level1Vo);
          //存入返回的list
          treeList.add(level1Vo);
        }
        //存入map
        instituteVoMap.put(c.getCompany()+"_"+c.getBranch(),level2Vo);
        //把二级存入一级的子list
        level1Vo.getChildren().add(level2Vo);
      }
      //把三级存入二级的子list
      level2Vo.getChildren().add(level3Vo);

      }else {
        logger.error("未知级别的机构"+c.toString());
      }
    }
    //写一个全部,当做最高级别的树
    ArrayList<InstituteVo> resultList = new ArrayList<>();
    InstituteVo masterVo = new InstituteVo("","全部",treeList);
    resultList.add(masterVo);
    
    return resultList;
  }
}

总结:

通过sql的union,把一级、二级、三级机构树按顺序排好;然后在java中,使用一个map缓存机构对象,把一级、二级、三级对象都存入map中,然后把三级对象存入对应的二级对象的子链中(list)、把二级对象存入对应的一级对象的子链中(list);

然后把一级对象装入一个list返回,或者在外层再套一个"全部"机构后、装入list返回。

6.sql按天导出数据,没有的补充0

如果sql直接按天导出数据,【group by DATE_FORMAT(t.create_time,'%Y-%m-%d')】,当某天有数据时,没有问题;当某天没有数据时,就没有那行;不符合要求。

可以用以下方式实现:

(1)使用sql创建一个包含每天的结果集,结果集只有一列,为每天的数据,如

date
2021-07-01
2021-07-02
......

sql如下:

select date from (
  select ADDDATE('1970-01-01', t4.i*10000 + t3.i*1000 + t2.i*100 + t1.i*10 + t0.i) date from
  (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t0,
  (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t1,
  (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t2,
  (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t3,
  (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t4
) v
where date between '2021-07-01' and '2021-07-30'

这句sql可以查询出列名为date、数据为2021-07-01到2021-07-30的结果集;

这句sql中用逗号连接t0,t1,t2,t3,t4,得到笛卡尔积,然后用ADDDATE方法,算出每行的日期并转为【年-月-日】格式(每行的日期差1天,adddate('1970-01-01',1)会得到1970-01-02);

最后使用where得到所要的范围。

(2)使用上方的sql,left join 查询出数据的结果,使用ifnull为没有的赋值为0,即可得到所需的数据;如下:

select date_table.date as final_date, ifnull(data_table.group_count, 0) as final_count 
from

(
select date from (
  select ADDDATE('1970-01-01', t4.i*10000 + t3.i*1000 + t2.i*100 + t1.i*10 + t0.i) date from
  (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t0,
  (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t1,
  (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t2,
  (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t3,
  (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t4
) v
where date between '2021-07-01' and '2021-07-30'
) date_table

left join 

(
select date_format(a.create_time,'%Y-%m-%d') as group_date, count(*) as group_count 
from exam_data a 
group by date_format(a.create_time,'%Y-%m-%d')
) data_table

on date_table.date = data_table.group_date
order by final_date

这句sql先查询出日期列结果集、数据按天分组后的结果集,然后用left join关联、没有的补0,最后得到想要的结果。

最后的order by是必要的,为了让结果有序,如果不加,有时候顺序会乱。

7.java按天导出数据,没有的补充0

以下是java代码:

//某天没有数据的,补0
public static void fillDate(String startTime, String endTime, ArrayList<MyBean> result) throws Exception{

  //假设传入参数的样式
  startTime = "2021-07-01";
  endTime = "2022-08-08";
  MyBean my1 = new MyBean("2021-07-01","5");
  MyBean my2 = new MyBean("2021-07-03","3");
  MyBean my3 = new MyBean("2021-07-05","1");
  //该list中的数据需要是有序的,日期从小到大
  result = new ArrayList<>();
  result.add(my1);
  result.add(my2);
  result.add(my3);

  //准备补从开始日期到结束日期、没有数据的为0
  //例如new MyBean("2021-07-02","0")然后装入list等

  if(StringUtils.isNotEmpty(startTime) && StringUtils.isNotEmpty(endTime)){
    //准备返回的list
    ArrayList<MyBean> newResult = new ArrayList<>();
    
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    Date startDate = sdf.parse(startTime);
    Date endDate = sdf.parse(endTime);

    //借助calendar,从开始日期,每次加1,进行补
    Calendar c = Calendar.getInstance();
    c.setTime(startDate);
    
    Iterator<MyBean> iterator = result.iterator();
    MyBean fromListBean = null;
    if(iterator.hasNext()){
      fromListBean = iterator.next();
    }
    //当没有到最后一天时
    while(c.getTime().getTime() <= endDate.getTime()){
      //如果list为空
      if(fromListBean == null){
        MyBean bean = new MyBean();
        bean.setDate(sdf.format(c.getTime()));
        bean.setValue("0");
        newResult.add(bean);
      }
      else{
        String date = fromListBean.getDate();
        String compareDate = sdf.format(c.getTime());
        //如果日期相等
        if(StringUtils.equals(compareDate,date)){
          newResult.add(fromListVo);
          //然后指向下一个元素,如果没有了,设为null
          if(iterator.hasNext()){
            fromListBean = iterator.next();
          }else{
            fromListBean = null;
          }
        }
        //如果不相等,newBean,补0
        else{
          MyBean bean = new MyBean();
          bean.setDate(sdf.format(c.getTime()));
          bean.setValue("0");
          newResult.add(bean);
        }
      }
      //加一天
      c.add(Calendar.DAY_OF_MONTH, 1);
    }
    //使用新的list; 没有写return,所以这样写
    result.clear();
    result.addAll(newResult);

  }
}

8.sql条件查询!=的问题

sql中,使用where a.flag !='0',期望得到flag不是0的所有结果,但是并不行,flag=null也不是0,却被去掉了;所以应该使用where ifnull(a.flag,1)!='0',才能得到flag不是0的所有结果;

或者使用where if(a.flag is null,'1',a.flag) != '0',也可以得到flag不是0的所有结果。

9.sql时间相减问题

sql中的dateTime等时间类型不能直接相减,结果不对;需要使用TIME_TO_SEC()转为秒然后再减。

10.ES匹配*的问题

如果某个字段的值恰好为*,那么以下查询会有问题:

GET exam_data/_search
{
  "query":{
    "bool":{
      "must_not": [
        {
          "term": {
            "examId": {
              "value": "*"
            }
          }
        }
      ]
    }
  }
}

这条语句想查询所有examId不为*的数据,但是会不起作用,查询出所有数据。

同样,以下查询也有问题:

GET exam_data/_search
{
  "query":{
    "bool":{
      "must": [
        {
          "term": {
            "examId": {
              "value": "*"
            }
          }
        }
      ]
    }
  }
}

该语句想查询所有examId为*的数据,但是也有问题,会查询到0条数据。

解决方法:

将examId替换为examId.keyword,才能实现想要的效果。

;