自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

詳解如何優(yōu)雅實現(xiàn)先分組再組內(nèi)排序取數(shù)據(jù)解決方案

數(shù)據(jù)庫 其他數(shù)據(jù)庫
分組后組內(nèi)排序取第一條記錄的操作,是 SQL 查詢中的高頻需求。推薦優(yōu)先使用窗口函數(shù) ROW_NUMBER(),其次是子查詢方法,并根據(jù)實際業(yè)務(wù)場景選擇合適的實現(xiàn)方式。

1.背景

之前有一段時間,老碰到一個需求業(yè)務(wù)邏輯場景:先對數(shù)據(jù)進行分組,然后在每組數(shù)據(jù)內(nèi)按某種規(guī)則排序,最后取出每組的第一條記錄,當(dāng)然也不一定就是取第一條數(shù)據(jù),也有可能是取前兩條數(shù)據(jù)等等,看具體業(yè)務(wù)邏輯需要,總的來說是一種在數(shù)據(jù)庫查詢中常見的業(yè)務(wù)需求。本文將詳細(xì)從使用MySQL, elasticsearch等方式總結(jié)這種操作的實現(xiàn)方案、使用場景以及優(yōu)化技巧。

2.準(zhǔn)備工作

所謂萬事俱備只欠東風(fēng),要研究這個查詢場景就得先準(zhǔn)備好數(shù)據(jù),還是使用一貫的用戶表User:

CREATE TABLE`tb_user` (
`id`bigint(20) NOTNULL AUTO_INCREMENT COMMENT'主鍵',
`user_no`varchar(255) NOTNULLCOMMENT'編號',
`name`varchar(255) DEFAULTNULLCOMMENT'昵稱',
`email`varchar(255) DEFAULTNULLCOMMENT'郵箱',
`phone`varchar(255) NOTNULLCOMMENT'手機號',
`gender`tinyint(4) NOTNULLDEFAULT'0'COMMENT'性別  0:男生   1:女生',
`birthday`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'出生日期',
`is_delete`tinyint(4) NOTNULLDEFAULT'0'COMMENT'刪除標(biāo)志 0:否  1:是',
`create_time` datetime DEFAULTNULLCOMMENT'創(chuàng)建時間',
`update_time` datetime DEFAULTNULLCOMMENT'更新時間',
`create_by`bigint(20) DEFAULTNULLCOMMENT'創(chuàng)建人',
`update_by`bigint(20) DEFAULTNULLCOMMENT'更新人',
`address`varchar(1024) DEFAULTNULLCOMMENT'地址',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5201011DEFAULTCHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

這里我插入了500多萬條數(shù)據(jù),為啥是500w條數(shù)據(jù)?因為我有個小目標(biāo)先掙個500w,哈哈,開個玩笑跑題了~~~,其實是我個人認(rèn)為單表500w條數(shù)據(jù)不多也不少,正好可以驗證一些SQL性能差異。

圖片圖片

插入都是比較真實的模擬數(shù)據(jù),至于怎么插入這么多數(shù)據(jù),你可以寫寫存儲過程,或者使用代碼腳本插入,想使用代碼插入的,可以看看之前我們總結(jié)的文章:最近做百萬級數(shù)據(jù)性能壓測,來看看人家如何使用MyBatis 優(yōu)雅批量插入數(shù)據(jù),從80s優(yōu)化到1s?。?!

這里我使用的數(shù)據(jù)庫配置是:阿里云的云數(shù)據(jù)庫RDS-MySQL 2核4GB,阿里云上可以免費使用3個月,有需要的可以去申請

數(shù)據(jù)已到位,我們開始分析先分組、再組內(nèi)排序取數(shù)據(jù)實現(xiàn)方式,基于上面的數(shù)據(jù):我們根據(jù)名字查詢出每個名字最近插入的個人信息

3.MySQL實現(xiàn)

數(shù)據(jù)庫查詢,一條SQL搞定,永遠是我們在實現(xiàn)功能需求邏輯的第一追求,當(dāng)然只是首選也不一定必選,要考慮性能問題,凡事不能一概而論,這就像算法的好壞需要從空間和時間兩個維度去考量一個道理。言歸正傳,我們看看數(shù)據(jù)庫MySQL查詢層面有哪些實現(xiàn)方式

3.1 使用窗口函數(shù) ROW_NUMBER()

窗口函數(shù)是解決該問題的首選方法,既簡潔又高效

SELECT
 * 
FROM
 ( SELECTid, NAME, birthday, ROW_NUMBER () OVER ( PARTITIONBYNAMEORDERBYidDESC ) AS row_num FROM tb_user
   wherenamein('徐千云', '李億石')
 ) AS u 
WHERE
 u.row_num=1

查詢結(jié)果:

圖片圖片

耗時:1.547s  注意窗口函數(shù)要 MySQL 8.0 及以上版本才有哦。

3.2 使用子查詢和JOIN關(guān)聯(lián)查詢

話不多說直接上SQL:

SELECT id, name, birthday from tb_user 
where id in (SELECT MAX(id) from tb_user where name in('徐千云', '李億石') group by name)

耗時:3.687s   明顯比使用窗口函數(shù)要慢的多

換一種寫法:

SELECT id, name, birthday from tb_user u
INNER JOIN (SELECT MAX(id) max_id from tb_user where name in('徐千云', '李億石') group by name) as t
on u.id=t.max_id

耗時:1.418s  明顯比子查詢快很多,這就是大量數(shù)據(jù)下,不同的SQL查詢性能差別是挺大的

3.3 優(yōu)化點

上面的查詢最快的都要1s多,這算是慢查詢了肯定要優(yōu)化,直接加索引

ALTER TABLE `db_test`.`tb_user` 
ADD INDEX `idx_name`(`name`) USING BTREE;

500w條的數(shù)據(jù)表,加索引會需要小一會兒。加完索引再次分別執(zhí)行上面的SQL語句,執(zhí)行結(jié)果耗時如下:

窗口函數(shù):0.026s        子查詢:2.229s         JOIN關(guān)聯(lián)查詢:0.014s

從結(jié)果上來看,執(zhí)行速度明顯變快了,效果可謂是立竿見影

如果我們查詢不需要出生日期birthday,以最慢的子查詢?yōu)槔?/p>

SELECT id,name from tb_user 
where id in (SELECT MAX(id) from tb_user where name in('徐千云', '李億石') group by name)

耗時:1.77s,也有算提升,因為這個可以使用覆蓋索引,建設(shè)了回表的次數(shù)。

3.4 討論

也不一定查詢每個名字最新插入的數(shù)據(jù),可能是查詢每個名字年紀(jì)最小的第一條數(shù)據(jù),這時候窗口函數(shù)查詢?nèi)缦拢?/p>

SELECT
 id, name, birthday
FROM
 ( SELECTid, name,birthday, ROW_NUMBER () OVER ( PARTITIONBYNAMEORDERBY birthday DESC ) AS row_num FROM tb_user
   wherenamein('徐千云', '李億石')
 ) AS u 
WHERE
 u.row_num =1

耗時:0.30s  挺快的

使用子查詢:

SELECT id, name, birthday from tb_user u1
where birthday = (select max(birthday) from tb_user u2 where u2.name in('徐千云', '李億石') and u1.`name`=u2.`name`)

直接查詢不出來~~~

項目推薦:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企業(yè)級系統(tǒng)架構(gòu)底層框架封裝,解決業(yè)務(wù)開發(fā)時常見的非功能性需求,防止重復(fù)造輪子,方便業(yè)務(wù)快速開發(fā)和企業(yè)技術(shù)棧框架統(tǒng)一管理。引入組件化的思想實現(xiàn)高內(nèi)聚低耦合并且高度可配置化,做到可插拔。嚴(yán)格控制包依賴和統(tǒng)一版本管理,做到最少化依賴。注重代碼規(guī)范和注釋,非常適合個人學(xué)習(xí)和企業(yè)使用

Github地址:https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent

4.elasticsearch實現(xiàn)

數(shù)據(jù)異構(gòu)是解決數(shù)據(jù)量大的一大常見方式,我們經(jīng)常使用elasticsearch來緩解MySQL數(shù)據(jù)庫的查詢壓力,來應(yīng)對海量數(shù)據(jù)的復(fù)雜查詢,那接下來我們就來看看基于elasticsearch怎么實現(xiàn)先分組再組內(nèi)排序取數(shù)據(jù)

在 Elasticsearch 中,可以使用 terms 聚合來實現(xiàn)分組,再結(jié)合 top_hits 聚合實現(xiàn)組內(nèi)排序并取每組的第一條數(shù)據(jù)。terms 聚合用于對某個字段進行分組,而 top_hits 聚合用于在每個分組內(nèi)獲取指定數(shù)量的文檔并按某種順序排序。

查詢每個名字年紀(jì)最小的第一條數(shù)據(jù):

GET user_info/_search
{
"size": 0,
"query": {
    "bool": {
      "filter": [
        { "terms": { "name": ["徐千云", "李億石"] }},
      ]
    }
  },
"aggs": {
    "group_by_name": {
      "terms": {
        "field": "name",
        "size": 1000
      },
      "aggs": {
        "latest_user": {
          "top_hits": {
            "sort": [
              { "birthday": { "order": "desc" }}
            ],
            "_source": ["id", "name", "org_id", "birthday"],
            "size": 1
          }
        }
      }
    }
  }
}
  • size: 0:在根查詢中設(shè)置 size 為 0,表示不返回頂層文檔,只返回聚合結(jié)果。
  • query 部分:使用 bool 查詢,限定 name 在["徐千云", "李億石"]內(nèi)。
  • terms 聚合 (group_by_name):按 name 分組,size 設(shè)置為較大值(如 1000)以確保返回盡可能多的分組。
  • top_hits 聚合 (latest_user):在每個 name 分組內(nèi),按 birthday 字段降序排序,并只返回每組的第一個文檔(即年齡最小的 的文檔)。指定 _source 字段過濾以返回所需字段。

terms 聚合的 size 限制了返回分組的數(shù)量。若需要更多分組,可以增大 size 或使用 composite 聚合。對于較大數(shù)據(jù)集,此查詢可能較慢,因為需要對分組中的每個文檔進行排序,直接看代碼:

public Integer aggregateResults(UserQueryDTO dto, Consumer<List<UserInfo>> consumer) {
        finalint pageSize = 500;
        Integer total = 0;
        Map<String, Object> afterKey = null;
        while (true) {
            // 創(chuàng)建 SearchRequest
            SearchRequest searchRequest = new SearchRequest(getAliasName());
            // 構(gòu)建查詢條件
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
            if (StringUtils.isNotBlank(dto.getTypes())) {
                boolQueryBuilder.filter(QueryBuilders.termsQuery("name", dto.getNames));
            }
            // Composite聚合實現(xiàn)并分頁
            CompositeAggregationBuilder compositeAggregation = AggregationBuilders
                    .composite("group_by_name",
                            Lists.newArrayList( new TermsValuesSourceBuilder("name").field("name")))
                    .size(pageSize);
            // 設(shè)置分頁的 afterKey
            compositeAggregation.aggregateAfter(afterKey);
            // 添加top_hits子聚合,size=1獲取每個分組中的最新文檔
            TopHitsAggregationBuilder topHitsAggregation = AggregationBuilders
                    .topHits("latest_user")
                    .sort("birthday", SortOrder.DESC)
                    .fetchSource(new String[]{"id", "name", "birthday"}, null)
                    .size(1);
            // 添加子聚合到 composite 聚合
            compositeAggregation.subAggregation(topHitsAggregation);
            // 構(gòu)建 SearchSourceBuilder size=0不返回文檔,只返回聚合結(jié)果
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder()
                    .size(0)
                    .query(boolQueryBuilder)
                    .aggregation(compositeAggregation);

            // 設(shè)置搜索請求,執(zhí)行搜索
            searchRequest.source(searchSourceBuilder);
            SearchResponse response = search(searchRequest);

            // 處理分頁聚合結(jié)果
            List<String> dataList = new ArrayList<>();
            ParsedComposite compositeAgg = response.getAggregations().get("group_by_name");
            // 遍歷當(dāng)前頁的分組結(jié)果
            compositeAgg.getBuckets().forEach(bucket -> {
                ParsedTopHits topHits = bucket.getAggregations().get("latest_user");
                SearchHit[] hits = topHits.getHits().getHits();
                for (SearchHit hit : hits) {
                   dataList.add(hit.getSourceAsString());
                }
            });
            List<UserInfo> results = convert(dataList, UserInfo.class);
            // 回調(diào)
            consumer.accept(results);
            total = total + results.size();
            // 更新 afterKey,為下一頁查詢準(zhǔn)備, 如果afterKey為空,說明已經(jīng)查詢到最后一頁
            afterKey = compositeAgg.afterKey();
            if (afterKey == null) {
                break;
            }
        }
        return total;
    }

afterKey 參數(shù):在 CompositeAggregationBuilder 中的 aggregateAfter(afterKey) 設(shè)置為上一次查詢的 afterKey,用于實現(xiàn)分頁。

分頁循環(huán):每次查詢一頁數(shù)據(jù),更新 afterKey,直到 afterKey 為 null,表示已達到最后一頁。

TopHitsAggregationBuilder:按 id 降序排序并返回每組中的最新文檔,用于獲取分組的最新記錄。

不知道你是否有注意到方法aggregateResults()有一個回調(diào)函數(shù)參數(shù)Consumer<List<UserInfo>> consumer,當(dāng)我們在操作大批量數(shù)據(jù)時,不可能一次性查出所有數(shù)據(jù)來處理,性能扛不住,只能分批分批查詢,如果分頁查詢出來放到一個集合最后在處理,內(nèi)存也支撐不住,這時候我們只在分頁查詢數(shù)據(jù)的同時處理相關(guān)數(shù)據(jù),比如分頁每頁200條查詢出200個名字年紀(jì)最小的人,并把他的更新時間更新為當(dāng)前,這樣就可以寫一個更新更新時間的方法,傳入aggregateResults(),等分頁查詢出用戶之后,根據(jù)用戶id回調(diào)更新即可。

5.總結(jié)

分組后組內(nèi)排序取第一條記錄的操作,是 SQL 查詢中的高頻需求。推薦優(yōu)先使用窗口函數(shù) ROW_NUMBER(),其次是子查詢方法,并根據(jù)實際業(yè)務(wù)場景選擇合適的實現(xiàn)方式。同時,結(jié)合索引和字段優(yōu)化,可以顯著提升查詢性能。當(dāng)然這是針對大批量查詢的情況下,如果你這是查詢一兩個姓名的最小年紀(jì)這種,你完全可以查詢出所有數(shù)據(jù),在Java代碼邏輯中利用內(nèi)存分組之后再排序,取相應(yīng)數(shù)據(jù)即可。

責(zé)任編輯:武曉燕 來源: Shepherd進階筆記
相關(guān)推薦

2010-07-09 09:24:37

SQL Server分

2010-05-07 16:30:01

Oracle數(shù)據(jù)集成

2010-02-23 14:56:18

WCF Bug

2009-12-16 13:39:27

Ruby元編程

2013-03-01 10:42:12

LTE3G通信網(wǎng)絡(luò)技術(shù)

2009-12-02 13:39:34

SAP認(rèn)證Novell

2022-03-01 09:31:06

JWTSession跨域

2018-11-12 11:12:46

2011-07-29 10:21:03

iPad 橫豎屏 切換

2009-11-04 16:03:59

2023-03-05 18:23:38

分布式ID節(jié)點

2024-04-01 09:24:39

2020-05-14 14:57:48

MySQLExcel排序

2013-07-30 11:18:59

SAP大數(shù)據(jù)解決方案

2013-10-18 15:27:30

微軟大數(shù)據(jù)微軟

2009-12-29 16:07:19

ADO類型

2010-06-28 15:55:23

數(shù)據(jù)泄漏DLP數(shù)據(jù)保護

2019-11-11 17:34:16

前端開發(fā)技術(shù)

2009-10-12 16:29:25

有線網(wǎng)絡(luò)布線解決方案

2009-11-05 09:42:44

智能小區(qū)寬帶接入
點贊
收藏

51CTO技術(shù)棧公眾號