<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      百萬數據easyExcel導出優化

      百萬數據easyExcel導出優化

      依賴

              <dependency>
                  <groupId>com.baomidou</groupId>
                  <artifactId>mybatis-plus-boot-starter</artifactId>
                  <version>3.5.2</version>
              </dependency>
      

      postgresql

      背景

      支付記錄導出!

      導出某時間點之后的百萬數據

      -- ----------------------------
      -- 表 4:支付記錄表(t_payment_record)
      -- ----------------------------
      CREATE TABLE IF NOT EXISTS t_payment_record (
                                                      id VARCHAR(64) PRIMARY KEY NOT NULL,  -- 支付訂單號(業務側生成的outTradeNo)
                                                      item_id VARCHAR(64),                  -- 繳費項目ID
                                                      item_name VARCHAR(255),               -- 訂單標題(繳費項目名稱)
                                                      description VARCHAR(500),            -- 新增字段:項目詳細說明
                                                      person_id VARCHAR(64),                -- 支付人員ID
                                                      person_name VARCHAR(64),              -- 支付人員姓名
                                                      amount NUMERIC(19,4),                 -- 支付金額(結算后金額)
                                                      status VARCHAR(50),                   -- 支付狀態(PENDING:待支付, SUCCESS:成功, FAILED:失敗)
                                                    
                                                      pay_user_app_ip VARCHAR(45),          -- 支付用戶終端IP(IPv4/IPv6)
                                                      user_agent VARCHAR(512),              -- 用戶瀏覽器代理信息
                                                      pay_success_url VARCHAR(1024),        -- 支付成功跳轉URL
                                                      create_time TIMESTAMP WITH TIME ZONE,  -- 記錄創建時間
                                                      update_time TIMESTAMP WITH TIME ZONE,  -- 記錄更新時間(狀態變更時更新)
                                                      school_class VARCHAR(255),            -- 學校班級信息(數據庫存組織路徑名稱,前端解析展示)
                                                      original_amount NUMERIC(19,4),        -- 項目原金額(未減免前的金額)
                                                      reduction_item_details TEXT,          -- 減免項目詳情(JSON數組格式,包含ID、名稱、金額)
                                                      error_msg VARCHAR(1024)
      );
      

      方案三 游標分頁導出(需某字段邏輯有序)推薦

      3.1核心思路

      create_time + id聯合索引 + 游標分頁 + 生產者 - 消費者模式”

      摒棄offset分頁,基于 “有序字段游標” 直接定位下一頁起點(如自增主鍵id、創建時間create_time),徹底避免掃描前序無關數據。

      采用生產者-消費者模式 與 多線程并發處理 的組合方案

      查詢-寫入解耦:通過隊列解耦兩個階段,充分利用數據庫和磁盤 I/O 的并行處理能力

      3.4若依賴有序字段不唯一

      當前場景的核心限制:id是 UUID 字符串(無序,無法作為游標字段),但create_time是有序的時間戳(可作為游標字段)。因此,方案需基于create_time實現游標分頁,結合id解決create_time重復問題,同時通過聯合索引優化查詢效率。

      1. 索引設計與數據查詢邏輯
      索引

      由于id是 UUID(無序),create_time是有序時間戳(可能重復),需創建聯合索引確保分頁有序性和查詢效率

      聯合索引: 百萬占73M

      -- 聯合索引:(create_time, id)
      -- 作用:1. 利用create_time的有序性作為游標基礎;2. 用id解決create_time重復時的排序唯一性
      CREATE INDEX idx_payment_create_time_id ON t_payment_record (create_time, id);
      

      覆蓋索引:百萬占154M 但提升不大

      create index idx_payment_fix_end on
      t_payment_record (create_time, id, item_name, person_id, person_name, amount, status);
      
      數據查詢邏輯

      基于create_timeid作為游標條件

      普通查詢

      -- 上一頁最后一條記錄的create_time = #{lastCreateTime},id = #{lastId}
      select * 
      from t_payment_record 
      where 
        -- 核心條件:時間在后 或 時間相同但id在后(解決時間重復)
        (create_time > #{lastCreateTime}) 
        or (create_time = #{lastCreateTime} and id > #{lastId})
      order by create_time asc, id asc  -- 與聯合索引順序一致,確保索引生效
      limit #{batchSize};  -- 批量大小(如1000)
      

      查詢某時間點之后

              select create_time, id, item_name, person_id, person_name, amount, status
              from t_payment_record
              where create_time >= #{lastCreateTime}
                and (
                  create_time > #{lastCreateTime}
                      or (
                      create_time = #{lastCreateTime}
                          and (
                          #{lastId,jdbcType=VARCHAR} is null
                              or id > #{lastId,jdbcType=VARCHAR}
                          )
                      )
                  )
              order by create_time asc, id asc
              limit #{batchSize}
      

      會先按照 create_time 排序,對于 create_time 完全相同的記錄,會進一步按照 id 進行字典序排

      序(UUID 本質是字符串,按字符串比較規則排序)

      外層create_time >= #{lastCreateTime}:劃定總查詢范圍

      不能去掉外層, 索引利用效率會下降:PostgreSQL 的查詢優化器在解析create_time >= ... and (...)時,能更明確地定位到聯合索引(create_time, id)create_time >= lastCreateTime的范圍,掃描效率更高;而缺少外層條件時,優化器可能需要更復雜的判斷,影響索引使用。 實測去掉特別慢 直接400多秒了

      {lastId,jdbcType=VARCHAR}
      postgresql對null參數的類型敏感 需明確參數類型

      2.本地性能實測

      不加索引 258秒 -》》 聯合索引 119秒 ->> 覆蓋索引 104秒

      分頁大小1000 ->>>2000: 86秒 ->>>>2300:83秒->>>>2500:85秒(提升已不大)

      僅聯合索引 Index Scan

      Limit  (cost=0.55..8.58 rows=1 width=124)
        ->  Index Scan using idx_payment_create_time_id on t_payment_record  (cost=0.55..8.58 rows=1 width=124)
              Index Cond: (create_time >= '2025-09-05 19:19:57.19+08'::timestamp with time zone)
              Filter: ((create_time > '2025-09-05 19:19:57.19+08'::timestamp with time zone) OR ((create_time = '2025-09-05 19:19:57.19+08'::timestamp with time zone) AND ((id)::text > 'MOCK_36472235-4553-461f-b682-a61cb7ca8b80'::text)))
      

      覆蓋索引 Index Only Scan

      Limit  (cost=0.55..8.58 rows=1 width=124)
        ->  Index Only Scan using idx_payment_fix_end on t_payment_record  (cost=0.55..8.58 rows=1 width=124)
              Index Cond: (create_time >= '2025-09-05 19:19:57.19+08'::timestamp with time zone)
              Filter: ((create_time > '2025-09-05 19:19:57.19+08'::timestamp with time zone) OR ((create_time = '2025-09-05 19:19:57.19+08'::timestamp with time zone) AND ((id)::text > 'MOCK_36472235-4553-461f-b682-a61cb7ca8b80'::text)))
      
      3. Java 代碼改造(生產者 - 消費者模式)

      沿用原方案的 “生產者 - 消費者 + 阻塞隊列” 模式(避免內存溢出),但需適配create_timeid作為游標字段,核心改造如下:

      (1)實體類定義
      // Excel導出DTO(按需映射,避免直接用實體類)
      @Data
      public class PaymentRecordExcel {
          @ExcelProperty("支付訂單號")
          private String id;
          @ExcelProperty("支付時間")
          private String createTime;  // 格式化后的字符串(如"yyyy-MM-dd HH:mm:ss")
      
          /**
           * 訂單標題(繳費項目名稱)
           */
          @TableField(value = "item_name")
          private String itemName;
      
          /**
           * 身份證
           */
          @ExcelProperty("身份證")
          private String personId;
      
          /**
           * 支付人員姓名
           */
          @ExcelProperty("支付人員姓名")
          private String personName;
      
          @ExcelProperty("金額")
          private BigDecimal amount;
      
          /**
           * 支付狀態(PENDING:待支付, SUCCESS:成功, FAILED:失敗)
           */
          @ExcelProperty("支付狀態")
          private String status;
      }
      
      (2)Mapper 接口與 XML(分頁查詢)
          /**
           * 查詢指定時間及之后的數據,處理時間重復時的ID續查
           * @param lastCreateTime 起始時間(包含)
           * @param lastId 起始ID(首次查詢為null,續查為上一批最后一個ID)
           * @param batchSize 批量大小
           */
          List<TPaymentRecord> listByCursor(
                  @Param("lastCreateTime") Date lastCreateTime,
                  @Param("lastId") String lastId,
                  @Param("batchSize") int batchSize
          );
      
          <select id="listByCursor" resultType="com.yuvision.dvsa.entity.TPaymentRecord">
              select id, create_time, item_name, person_id, person_name, amount, status
              from t_payment_record
              where
                  create_time >= #{lastCreateTime}  -- 包含起始時間及之后的數據
                and (
                  create_time > #{lastCreateTime}  -- 時間在起始時間之后,不限制ID
                      or (
                      create_time = #{lastCreateTime}  -- 時間等于起始時間
                          and (
                          #{lastId,jdbcType=VARCHAR} is null  -- 首次查詢:包含所有同時間的記錄
                              or id > #{lastId,jdbcType=VARCHAR}  -- 續查:只包含ID大于上一批最后一個ID的記錄
                          )
                      )
                  )
              order by create_time asc, id asc  -- 與聯合索引順序一致
              limit #{batchSize}
          </select>
      
      (3)核心導出邏輯(生產者 - 消費者模式)
      /**
           * 基于create_time游標分頁的百萬級數據導出
           * 導出指定時間及之后的所有支付記錄(優化:lastCreateTime從參數傳入,非null)
           *
           * @param lastCreateTime 起始時間(包含該時間)
           * @param lastId         起始ID(可選,用于分頁續查,首次查詢傳null)
           * @return 導出文件路徑
           */
          public void exportMillionData(Date lastCreateTime, String lastId) throws InterruptedException {
              // 校驗參數:lastCreateTime不可為null
              if (lastCreateTime == null) {
                  throw new IllegalArgumentException("lastCreateTime不能為空");
              }
              // 1. 初始化參數
              String filePath = "D://temp//" + System.currentTimeMillis() + ".xlsx";
              //分頁大小1000 ->>>2000:  86秒 ->>>>2300:83秒->>>>2500:85秒(提升已不大)
              int batchSize = 2300;  // 批量大小(可根據測試調整,建議1000-2000)
              BlockingQueue<List<TPaymentRecord>> queue = new ArrayBlockingQueue<>(10);  // 緩沖隊列(避免內存溢出)
              ExecutorService executor = Executors.newFixedThreadPool(2);  // 1個生產者+1個消費者
              // 關鍵:CountDownLatch計數=1,用于等待消費者完成所有寫入
              CountDownLatch allDoneLatch = new CountDownLatch(1);
      
              // 游標跟蹤:從傳入的lastCreateTime和lastId開始
              AtomicReference<Date> currentCreateTime = new AtomicReference<>(lastCreateTime);
              AtomicReference<String> currentId = new AtomicReference<>(lastId);
      //        // 2. 游標跟蹤(上一頁最后一條記錄的createTime和id,初始為null)
      //        AtomicReference<Date> lastCreateTime = new AtomicReference<>(null);
      //        AtomicReference<String> lastId = new AtomicReference<>(null);
      
              long startTime = System.currentTimeMillis();
      
      
              // 3. 生產者線程:查詢數據并放入隊列
              executor.submit(() -> {
                  try {
                      int batchNum = 0;
                      while (true) {
                          // 3.1 調用mapper查詢下一批數據
                          List<TPaymentRecord> data = paymentRecordMapper.listByCursor(
                                  currentCreateTime.get(),
                                  currentId.get(),
                                  batchSize
                          );
      
                          // 3.2 若查詢結果為空,說明數據已全部導出,退出循環
                          if (data == null || data.isEmpty()) {
                              // 數據查詢完畢,放入空數組作為結束標記
                              queue.put(new ArrayList<>());
                              log.info("所有數據查詢完成,共導出{}批", batchNum);
                              break;
                          }
      
                          // 3.3 更新游標(記錄當前批次最后一條數據的createTime和id)
                          TPaymentRecord lastRecord = data.get(data.size() - 1);
                          currentCreateTime.set(lastRecord.getCreateTime());
                          currentId.set(lastRecord.getId());
      
                          // 3.4 將數據放入隊列(阻塞等待,避免OOM)
                          queue.put(data);
                          batchNum++;
                          log.info("生產第{}批數據,大小:{},最后一條時間:{}",
                                  batchNum, data.size(), lastRecord.getCreateTime());
                      }
                  } catch (Exception e) {
                      log.error("生產者查詢數據異常", e);
                      // 異常時也放入結束標記,避免消費者卡死
                      try {
                          queue.put(new ArrayList<>());
                      } catch (InterruptedException ex) {
                          Thread.currentThread().interrupt();
                      }
                  }
              });
      
      
              // 4. 消費者線程:從隊列取數據并寫入Excel
              executor.submit(() -> {
                  // 4.1 初始化EasyExcel寫入器
                  try (ExcelWriter excelWriter = EasyExcel.write(filePath, PaymentRecordExcel.class).build();
                  ) {
                      WriteSheet writeSheet = EasyExcel.writerSheet("支付記錄").build();
                      int writeBatchNum = 0;
                      log.info("消費者開始寫入Excel...");
      
                      // 4.2 循環從隊列取數據,直到生產者完成且隊列空
                      while (true) {
      
                          // 從隊列取數據(超時5秒,避免無限阻塞)
                          List<TPaymentRecord> data = queue.poll(5, TimeUnit.SECONDS);
      
                          if (data == null || data.isEmpty()) {
                              // 收到結束標記,退出循環
                              log.info("消費者完成,共寫入{}批數據", writeBatchNum);
                              break;
                          }
      
                          // 轉換為ExcelDTO(格式化時間等)
                          List<PaymentRecordExcel> excelData = data.stream().map(record -> {
                              PaymentRecordExcel excel = new PaymentRecordExcel();
                              BeanUtil.copyProperties(record, excel);
      
                              Date createTime = record.getCreateTime();
                              excel.setCreateTime(DateUtil.format(createTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
                              return excel;
                          }).collect(Collectors.toList());
      
                          // 寫入Excel
                          excelWriter.write(excelData, writeSheet);
                          writeBatchNum++;
                          log.info("寫入第{}批數據,大小:{}", writeBatchNum, data.size());
                      }
      
                      log.info("Excel寫入完成,文件路徑:{}", filePath);
                  } catch (Exception e) {
                      log.error("Excel寫入異常", e);
                  } finally {
                      // 無論成功失敗,都通知主線程“所有寫入完成”
                      allDoneLatch.countDown();
                  }
              });
      
      
              // 5. 等待所有任務完成,關閉資源
              //
              allDoneLatch.await();  // 等待生產者完成
              long endTime = System.currentTimeMillis();
              log.info("導出完成,總耗時:{}秒", (endTime - startTime) / 1000);
      
              executor.shutdown();
              if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
                  log.warn("線程池未正常關閉,強制終止");
                  executor.shutdownNow();
              }
      
              // 睡眠幾秒的作用是跑測試用例時候,以免最后一次的countdown喚醒主線程后,excelWriter還沒有來得及關閉程序就退出了
              Thread.sleep(10000);
      
              log.info("已結束");
          }
      

      mysql

      通過easyExcel 導出1,000,001條數據

      需要考慮以下問題

      1.需要分頁,如果一次性導出,內存會溢出,需要分頁批量導出。

      2.批量導出,如果采用分頁查詢,需要考慮是否出現深分頁帶來的性能問題。

      3.考慮采用多線程處理

      4.采用阿里巴巴開源的EasyExcel組件生成excel文件,且EasyExcel不支持多線程導出

      方案一 單線程分頁導出(基礎方案)

      核心思路

      通過分頁查詢(LIMIT #{offset}, #{size})批量獲取數據,逐批寫入 Excel,避免一次性加載全部數據導致內存溢出。

      適用場景

      適用于數據量較小(非深分頁)的場景,或作為性能基準測試(用于定位瓶頸)

      本地性能實測

      ...
      數據庫查詢耗時830豪秒
      寫入excel耗時11毫秒
      第982次寫入1000條數據成功
      數據庫查詢耗時751豪秒
      寫入excel耗時11毫秒
      ...
      生成excel總耗時464秒 db耗時406843毫秒 excel寫入耗時10025毫秒
      

      瓶頸分析:數據庫 IO 耗時占比極高(約 87.7%),因分頁查詢未利用索引,導致全表掃描

      核心sql

      select t.* 
      from t_eeg_record_mock t 
      limit #{offset}, #{size}
      
      id select_type table partitions type possible_keys key key_len ref rows filtered Extra
      1 SIMPLE t ALL 1008210 100.0

      (注:type=ALL 表示全表掃描,無索引可用,隨offset增大,掃描行數激增)

      方案二 覆蓋索引優化分頁導出

      2.1核心思路

      數據庫采用 “子查詢先獲取主鍵 ID,再關聯查詢全字段” 的方式,利用主鍵索引減少無效數據掃描,優化深分頁性能。

      采用生產者-消費者模式 與 多線程并發處理 的組合方案

      查詢-寫入解耦:通過隊列解耦兩個階段,充分利用數據庫和磁盤 I/O 的并行處理能力

      2.2使用場景

      1. 業務必須支持 “隨機跳頁”,無法接受方案三的 “只能順序分頁”
      2. 有序字段不穩定或不存在

      2.3本地性能實測

      生成excel總耗時109秒
      

      2.4核心sql

      子查詢先通過主鍵索引查id,再關聯獲取,避免掃描大量無關行

              select t.*
              from t_eeg_record_mock t,
                   (select id from t_eeg_record_mock limit #{offset}, #{size}) t2
              where t.id = t2.id
              order by t.id asc
      

      優化原理

      1. 子查詢僅掃描id(主鍵,通常包含在索引中),減少字段讀取的 IO 成本;
      2. 主查詢通過id = t2.id利用主鍵索引快速定位全字段,避免全表掃描。
      id select_type table partitions type possible_keys key key_len ref rows filtered Extra
      1 PRIMARY ALL 2000 100.0 Using temporary; Using filesort
      1 PRIMARY t eq_ref PRIMARY PRIMARY 4 t2.id 1 100.0
      2 DERIVED t_eeg_record_mock index idx_create_time 6 1008210 100.0 Using index

      (注:子查詢通過idx_create_time索引(含主鍵id)實現覆蓋掃描,避免全表讀取)

      方案三 游標分頁導出(需某字段邏輯有序)推薦

      3.1核心思路

      摒棄offset分頁,基于 “有序字段游標” 直接定位下一頁起點(如自增主鍵id、創建時間create_time),徹底避免掃描前序無關數據。

      采用生產者-消費者模式 與 多線程并發處理 的組合方案

      查詢-寫入解耦:通過隊列解耦兩個階段,充分利用數據庫和磁盤 I/O 的并行處理能力

      3.2適用場景

      • 數據量極大,存在深分頁問題(如導出百萬級數據),需要避免傳統limit offsetoffset過大導致的全表掃描效率問題;
      • 分頁字段是 “有序的”(字段值本身可比較大小,如時間、自增 ID 等),無論插入順序如何,只要字段值邏輯上有先后順序即可!
      • 不需要 “跳頁” 查詢(如只能從第 1 頁→第 2 頁→…→第 N 頁,無法直接跳到第 100 頁),因為游標分頁依賴上一頁的最后一條數據定位下一頁;
      • 字段允許存在重復值(需結合唯一字段如主鍵,通過聯合索引保證排序唯一性)。

      3.3若依賴有序字段唯一(如主鍵id)

      3.3.1本地性能實測
      生成excel總耗時11秒
      
      3.3.2核心sql
      select t.*
      from t_eeg_record_mock t
      where t.id > #{lastId}  -- 上一頁最后一條數據的id
      order by id asc
      limit #{size};
      
      3.3.3代碼
      /**
           * 使用id分頁優化,前提條件id是有序的,50萬條數據大約5秒
           *  100萬約11秒
           * @throws InterruptedException
           */
          public void generateExcelAsyncAndIdMax() throws InterruptedException {
      
              BlockingQueue<List<EegRecordMock>> queue = new ArrayBlockingQueue<>(10);// 添加緩沖隊列
              String filePath = "D://temp//" + System.currentTimeMillis() + ".xlsx";
              ExecutorService executorService = Executors.newFixedThreadPool(2);
              //批量大小 可根據測試結果調整
              int batchNum0 = 1000;
              int total0 = (int) this.count();
              int num = total0 / batchNum0;
              num = total0 % batchNum0 == 0 ? num : num + 1;
              CountDownLatch countDownLatch = new CountDownLatch(num);
              long startTime = System.currentTimeMillis();
      
              executorService.submit(() -> {
                  try {
                      int batchNum = batchNum0;
                      int total = total0;
                      int current = 1;
      
                      AtomicInteger index = new AtomicInteger(0);
                      //數據庫以2為起始id  idIndex為2-1=1
                      AtomicReference<Integer> idIndex = new AtomicReference<>(1);
      
                      log.info("開始數據生產任務,總數據量:{}", total); // 規范日志級別
                      while (total > 0) {
                          int offset = (current - 1) * batchNum;
                          batchNum = total > batchNum ? batchNum : total;
                          int size = batchNum;
                          int maxId = idIndex.get();
                          List<EegRecordMock> data = eegRecordMockMapper.listBetweenId(maxId, size);
                          //取本批次最后一條記錄的id作為下次查詢起點
                          idIndex.set(data.get(data.size() - 1).getId());
                          try {
                              queue.put(data);
                              log.info("生產第{}批次數據,最大ID:{},批次大小:{}",
                                      index.incrementAndGet(), maxId, data.size()); // 結構化日志
                          } catch (InterruptedException e) {
                              log.error("數據放入隊列中斷異常,剩余數據量:{}", total, e);
                              Thread.currentThread().interrupt(); // 重置中斷狀態
                          }
      
                          total -= batchNum;
                          current++;
                      }
                      log.info("數據生產任務完成"); // 規范日志級別
                  } catch (Exception e) {
                      log.error("生產線程發生未預期異常,異常信息:{}", e.getMessage(), e);
                  }
              });
      
              executorService.submit(() -> {
      
                  try (ExcelWriter excelWriter = EasyExcel.write(filePath, EegRecordMockExcel.class).build()) {
                      WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
                      log.info("開始Excel寫入任務--------");
                      try {
                          while (countDownLatch.getCount() > 0) {
                              List<EegRecordMock> data = queue.poll(5, TimeUnit.SECONDS); // 添加超時機制
                              if (data == null) {
                                  log.error("獲取數據超時,剩余批次:{}", countDownLatch.getCount());
                                  break;
                              }
                              if (!data.isEmpty()) {
                                  excelWriter.write(data, writeSheet);
                                  log.debug("成功寫入第{}批次,數據量:{}",
                                          countDownLatch.getCount(), data.size()); // 調試級別日志
                              }else{
                                  log.warn("data為空");
                              }
                              countDownLatch.countDown();
      
                          }
                          log.info("結束Excel寫入任務--------");
      
                      } catch (InterruptedException e) {
                          log.error("數據消費中斷異常", e);
                          Thread.currentThread().interrupt(); // 重置中斷狀態
                      }
                  } catch (Exception e) {
                      log.error("Excel寫入失敗,文件路徑:{},異常信息:{}", filePath, e.getMessage(), e);
                  }
      
      
              });
      
      
              countDownLatch.await();
              long endTime = System.currentTimeMillis();
              log.warn("生成excel總耗時{}秒", (endTime - startTime) / 1000);
      
              // 添加線程池關閉
              executorService.shutdown();//優雅關閉
              if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {//等待10秒
                  log.warn("線程池未正常關閉,強制終止");
                  executorService.shutdownNow();
              }
      
              // 睡眠三秒的作用是跑測試用例時候,以免最后一次的countdown喚醒主線程后,excelWriter還沒有來得及關閉程序就退出了
              Thread.sleep(10000);
      
              log.info("已結束");
          }
      
          <select id="listBetweenId" resultType="com.dake.excel.demo.dao.entity.EegRecordMock">
              select t.*
              from t_eeg_record_mock t
              where t.id > #{id}
              order by id asc
              limit #{size}
          </select>
      
      @TableName("t_eeg_record_mock")
      @Data
      public class EegRecordMock implements Serializable {
      
          private static final long serialVersionUID = 1L;
      
          /**
           * 主鍵id
           */
          @TableId(type = IdType.AUTO)
          private Integer id;
      
          /**
           * 唯一標識uuid
           */
          private String uuid;
      
          /**
           * 用戶id
           */
          private Integer userId;
      
          /**
           * 實驗設置表id
           */
          private Integer algoId;
      
          /**
           * 設備名稱
           */
          private String deviceName;
      
          /**
           * 系統生成數據時間
           */
          private LocalDateTime createTime;
      
      }
      
      @ContentRowHeight(20)
      @HeadRowHeight(35)
      @ColumnWidth(20)
      @Data
      public class EegRecordMockExcel implements Serializable {
      
          private static final long serialVersionUID = 1L;
      
      
          @ExcelProperty({"腦電主數據id"})
          private Integer id;
      
          @ExcelProperty({"uuid"})
          private String uuid;
      
          /**
           * 用戶id
           */
          @ExcelProperty("用戶id")
          private Integer userId;
      
          /**
           * 算法id
           */
          @ExcelProperty("算法id")
          private Integer algoId;
      
          /**
           * 設備名稱
           */
          @ExcelProperty("設備名稱")
          private String deviceName;
      
          /**
           * 系統生成數據時間
           */
          @ExcelProperty("創建時間")
          private LocalDateTime createTime;
      
      
      }
      

      3.4若依賴有序字段不唯一

      若分頁依賴有序字段不唯一(如非主鍵: order by create_time),需確保排序字段有索引,然后結合主鍵保證唯一性創建聯合索引,如 order by create_time, id

      比如導出支付記錄, id為uuid, 需要按時間先后展示

      3.4.1本地性能實測
      3.4.2優化步驟

      1 創建聯合索引:(確保排序字段在前,主鍵在后,保證唯一性)

      對于 create_time 完全相同的記錄,會進一步按照 id 進行字典序排序

      create index idx_create_time_id on t_eeg_record_mock(create_time, id);
      

      2 基于排序字段的游標分頁:(基于上一頁末尾的create_timeid

      -- 上一頁最后一條數據的 create_time = last_time,id = last_id
      select * 
      from t_eeg_record_mock 
      where create_time > #{last_time} 
         or (create_time = #{last_time} and id > #{last_id})  -- 處理create_time相同的情況
      order by create_time, id 
      limit #{size};
      

      通過聯合索引實現 “范圍 + 精確匹配”,既保證排序一致性,又避免掃描無關數據。

      3.4.3代碼
      posted @ 2025-09-05 21:43  星空與滄海  閱讀(22)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 久久精品天天中文字幕人妻| 强奷乱码中文字幕| 国产成人永久免费av在线| 久久亚洲国产成人精品性色| 1精品啪国产在线观看免费牛牛| 色视频不卡一区二区三区| 大伊香蕉在线精品视频75| 精品一卡2卡三卡4卡乱码精品视频| 伊人久久久av老熟妇色| 天天做天天爱夜夜爽女人爽| 日本一区二区三区专线| 国产精品精品一区二区三| 国产精品久久久国产盗摄| 在线观看中文字幕国产码| 国产无遮挡又黄又大又爽| 亚洲免费福利在线视频| 日本高清一区免费中文视频| 亚洲国产av区一区二| 搡老熟女老女人一区二区| 亚洲中文字幕无码av永久| 国产精品男女午夜福利片| 欧美国产日韩久久mv| 精品国产欧美一区二区三区在线| 日本黄色三级一区二区三区| 亚洲欧洲∨国产一区二区三区 | 国内揄拍国内精品少妇国语| 亚洲成av人片乱码色午夜| 丰满少妇内射一区| 中文字幕人妻有码久视频| 久久精品人妻无码专区| 精品人妻系列无码一区二区三区| 日日碰狠狠添天天爽五月婷| 国产成人a在线观看视频免费| 国产精品538一区二区在线| 久久精品国产91精品亚洲| 亚洲精品无码久久千人斩| 亚洲综合色丁香婷婷六月图片 | 国产一区二区三区精品综合| 中国少妇人妻xxxxx| 苍井空一区二区三区在线观看| 国产激情艳情在线看视频|