成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

詳解如何優雅實現先分組再組內排序取數據解決方案

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

1.背景

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

2.準備工作

所謂萬事俱備只欠東風,要研究這個查詢場景就得先準備好數據,還是使用一貫的用戶表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'刪除標志 0:否  1:是',
`create_time` datetime DEFAULTNULLCOMMENT'創建時間',
`update_time` datetime DEFAULTNULLCOMMENT'更新時間',
`create_by`bigint(20) DEFAULTNULLCOMMENT'創建人',
`update_by`bigint(20) DEFAULTNULLCOMMENT'更新人',
`address`varchar(1024) DEFAULTNULLCOMMENT'地址',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5201011DEFAULTCHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

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

圖片圖片

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

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

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

3.MySQL實現

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

3.1 使用窗口函數 ROW_NUMBER()

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

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

查詢結果:

圖片圖片

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

3.2 使用子查詢和JOIN關聯查詢

話不多說直接上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   明顯比使用窗口函數要慢的多

換一種寫法:

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  明顯比子查詢快很多,這就是大量數據下,不同的SQL查詢性能差別是挺大的

3.3 優化點

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

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

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

窗口函數:0.026s        子查詢:2.229s         JOIN關聯查詢:0.014s

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

如果我們查詢不需要出生日期birthday,以最慢的子查詢為例:

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

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

3.4 討論

也不一定查詢每個名字最新插入的數據,可能是查詢每個名字年紀最小的第一條數據,這時候窗口函數查詢如下:

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企業級系統架構底層框架封裝,解決業務開發時常見的非功能性需求,防止重復造輪子,方便業務快速開發和企業技術棧框架統一管理。引入組件化的思想實現高內聚低耦合并且高度可配置化,做到可插拔。嚴格控制包依賴和統一版本管理,做到最少化依賴。注重代碼規范和注釋,非常適合個人學習和企業使用

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

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

4.elasticsearch實現

數據異構是解決數據量大的一大常見方式,我們經常使用elasticsearch來緩解MySQL數據庫的查詢壓力,來應對海量數據的復雜查詢,那接下來我們就來看看基于elasticsearch怎么實現先分組再組內排序取數據

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

查詢每個名字年紀最小的第一條數據:

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:在根查詢中設置 size 為 0,表示不返回頂層文檔,只返回聚合結果。
  • query 部分:使用 bool 查詢,限定 name 在["徐千云", "李億石"]內。
  • terms 聚合 (group_by_name):按 name 分組,size 設置為較大值(如 1000)以確保返回盡可能多的分組。
  • top_hits 聚合 (latest_user):在每個 name 分組內,按 birthday 字段降序排序,并只返回每組的第一個文檔(即年齡最小的 的文檔)。指定 _source 字段過濾以返回所需字段。

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

public Integer aggregateResults(UserQueryDTO dto, Consumer<List<UserInfo>> consumer) {
        finalint pageSize = 500;
        Integer total = 0;
        Map<String, Object> afterKey = null;
        while (true) {
            // 創建 SearchRequest
            SearchRequest searchRequest = new SearchRequest(getAliasName());
            // 構建查詢條件
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
            if (StringUtils.isNotBlank(dto.getTypes())) {
                boolQueryBuilder.filter(QueryBuilders.termsQuery("name", dto.getNames));
            }
            // Composite聚合實現并分頁
            CompositeAggregationBuilder compositeAggregation = AggregationBuilders
                    .composite("group_by_name",
                            Lists.newArrayList( new TermsValuesSourceBuilder("name").field("name")))
                    .size(pageSize);
            // 設置分頁的 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);
            // 構建 SearchSourceBuilder size=0不返回文檔,只返回聚合結果
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder()
                    .size(0)
                    .query(boolQueryBuilder)
                    .aggregation(compositeAggregation);

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

            // 處理分頁聚合結果
            List<String> dataList = new ArrayList<>();
            ParsedComposite compositeAgg = response.getAggregations().get("group_by_name");
            // 遍歷當前頁的分組結果
            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);
            // 回調
            consumer.accept(results);
            total = total + results.size();
            // 更新 afterKey,為下一頁查詢準備, 如果afterKey為空,說明已經查詢到最后一頁
            afterKey = compositeAgg.afterKey();
            if (afterKey == null) {
                break;
            }
        }
        return total;
    }

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

分頁循環:每次查詢一頁數據,更新 afterKey,直到 afterKey 為 null,表示已達到最后一頁。

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

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

5.總結

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

責任編輯:武曉燕 來源: Shepherd進階筆記
相關推薦

2010-07-09 09:24:37

SQL Server分

2010-05-07 16:30:01

Oracle數據集成

2010-02-23 14:56:18

WCF Bug

2009-12-16 13:39:27

Ruby元編程

2013-03-01 10:42:12

LTE3G通信網絡技術

2009-12-02 13:39:34

SAP認證Novell

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節點

2013-07-30 11:18:59

SAP大數據解決方案

2013-10-18 15:27:30

微軟大數據微軟

2022-03-01 09:31:06

JWTSession跨域

2009-12-29 16:07:19

ADO類型

2024-04-01 09:24:39

2010-06-28 15:55:23

數據泄漏DLP數據保護

2020-05-14 14:57:48

MySQLExcel排序

2009-12-22 15:50:11

2019-11-11 17:34:16

前端開發技術

2009-10-12 16:29:25

有線網絡布線解決方案
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 欧美一区中文字幕 | 一区二区三区在线看 | 伊人欧美视频 | 国产精品毛片无码 | 亚州午夜精品 | 国产精品久久久久久久久久久久 | 麻豆一区一区三区四区 | 精品国产一区二区在线 | 国产精品久久久亚洲 | 欧美色视频免费 | 午夜一级做a爰片久久毛片 精品综合 | a级免费黄色片 | 欧美黄色片| 97久久精品午夜一区二区 | 亚洲成人综合在线 | 在线免费观看日本视频 | 欧美日韩一区二区三区四区五区 | 亚洲精品丝袜日韩 | 久久久久99 | 国产亚洲精品久久久优势 | 国产一区在线免费观看 | 欧美精品在线播放 | 欧美亚洲免费 | 亚洲精品乱码久久久久久9色 | 国产成人精品免费视频大全最热 | 中文字幕一区二区三区乱码在线 | 神马久久春色视频 | 午夜精品一区二区三区在线视频 | 日韩精品999| 久久精品伊人 | 久草视 | av永久 | 中文字幕亚洲一区 | 亚洲激精日韩激精欧美精品 | 国产午夜精品一区二区三区四区 | 亚洲人在线 | 免费一区 | 一区二区三区免费 | 国产精品视频久久久久久 | 亚洲色视频 | 韩日一区二区 |