資料統計查詢最佳化
當前專案中存在的問題
當前的資料統計模組中,營業額統計、使用者統計和訂單統計這三個介面的在業務層中的執行流程如下:
- 根據前端傳來的起止日期計算期間每一天的日期並存入日期集合。
- 遍歷日期集合得到每一天的日期,將該日期處理後再查詢資料庫中當天滿足條件的資料。
- 將每次查詢的結果進行處理後存入相應的結果集合。
- 將結果集合進行封裝後返回。
在第二步中,查詢的日期有幾天,就會查詢幾次資料庫,同時每次查詢的資料量有很小。而比起資料庫的查詢操作本身,與資料庫的連線非常耗費時間。這種做法無疑回導致效能大幅下降。
解決方案
每個介面都只與資料庫進行一次連線,在這一次連線中查詢出所有需要的資料,然後在Java中進行處理並返回。具體步驟如下:
- 根據前端傳來的起止日期計算期間每一天的日期並存入日期集合。
- 查詢起止日期範圍內所有滿足條件的資料。
- 將查詢到的資料進行處理並封裝返回。
程式碼開發
營業額統計
- 在ReportServiceImpl中修改getTurnoverStatistics方法:
@Override
public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
List<LocalDate> dateList = getDateList(begin, end); //當前集合用於存放從begin到end範圍內每天的日期
BigDecimal[] turnoverList = new BigDecimal[dateList.size()]; //當前集合用於存放從begin到end範圍內每天的營業額
Arrays.fill(turnoverList, BigDecimal.ZERO); //初始化營業額陣列
//查詢從begin到end範圍內狀態為已完成的所有訂單資料
Map map = new HashMap<>();
map.put("begin", LocalDateTime.of(begin, LocalTime.MIN));
map.put("end", LocalDateTime.of(end, LocalTime.MAX));
map.put("status", Orders.COMPLETED);
List<Orders> ordersList = orderMapper.getByMap(map);
//把查出來的訂單的營業額加上
for (Orders orders : ordersList) {
LocalDate orderTime = orders.getOrderTime().toLocalDate();
int period = Period.between(begin, orderTime).getDays();
turnoverList[period] = turnoverList[period].add(orders.getAmount());
}
//將集合轉換為字串
String dateListString = StringUtils.join(dateList, ",");
String turnoverListString = StringUtils.join(turnoverList, ",");
//構造TurnoverReportVO並返回
TurnoverReportVO turnoverReportVO = TurnoverReportVO.builder()
.dateList(dateListString)
.turnoverList(turnoverListString)
.build();
return turnoverReportVO;
}
- 在OrderMapper介面中宣告getByMap方法:
List<Orders> getByMap(Map map);
- 在OrderMapper.xml中編寫getByMap方法的SQL語句:
<select id="getByMap" resultType="com.sky.entity.Orders">
select * from orders
<where>
<if test="begin != null">
and order_time > #{begin}
</if>
<if test="end != null">
and order_time < #{end}
</if>
<if test="status != null">
and status = #{status}
</if>
</where> order by order_time asc
</select>
使用者統計
- 在ReportServiceImpl中修改getUserStatistics方法:
@Override
public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {
List<LocalDate> dateList = getDateList(begin,end); //當前集合用於存放從begin到end範圍內每天的日期
int[] totalUserList = new int[dateList.size()]; //當前集合用於存放從begin到end範圍內每天的總使用者數
int[] newUserList = new int[dateList.size()]; //當前集合用於存放從begin到end範圍內每天新增的使用者數
//查詢註冊時間在begin到end範圍內的所有使用者資料
List<User> userList = userMapper.getByBeginAndEndTime(
LocalDateTime.of(begin, LocalTime.MIN),
LocalDateTime.of(end, LocalTime.MAX));
//根據查詢到的使用者資料計算每天的新增使用者數
for (User user : userList) {
LocalDate createTime = user.getCreateTime().toLocalDate();
int period = Period.between(begin, createTime).getDays();
newUserList[period]++;
}
//查詢begin時間之前的總使用者數
Map map = new HashMap<>();
map.put("end", LocalDateTime.of(begin, LocalTime.MIN));
Integer totalUser = userMapper.countByMap(map);
//計算每天的總使用者數
totalUserList[0] = totalUser + newUserList[0];
for (int i = 1; i < dateList.size(); i++) {
totalUserList[i] = totalUserList[i - 1] + newUserList[i];
}
//將集合轉換為字串
String dateListString = StringUtils.join(dateList, ",");
String totalUserListString = StringUtils.join(totalUserList, ',');
String newUserListString = StringUtils.join(newUserList, ',');
//構造UserReportVO並返回
UserReportVO userReportVO = UserReportVO.builder()
.dateList(dateListString)
.totalUserList(totalUserListString)
.newUserList(newUserListString)
.build();
return userReportVO;
}
- 在OrderMapper介面中宣告getByBeginAndEndTime方法:
List<User> getByBeginAndEndTime(LocalDateTime begin, LocalDateTime end);
- 在OrderMapper.xml中編寫getByBeginAndEndTime方法的SQL語句:
<select id="getByBeginAndEndTime" resultType="com.sky.entity.User">
select * from user
<where>
<if test="begin != null">
and create_time > #{begin}
</if>
<if test="end != null">
and create_time < #{end}
</if>
</where>
</select>
訂單統計
- 在ReportServiceImpl中修改getOrdersStatistics方法:
@Override
public OrderReportVO getOrdersStatistics(LocalDate begin, LocalDate end) {
List<LocalDate> dateList = getDateList(begin, end); //當前集合用於存放從begin到end範圍內每天的日期
int[] orderCountList = new int[dateList.size()]; //當前集合用於存放從begin到end範圍內每天的訂單數
int[] validOrderCountList = new int[dateList.size()]; //當前集合用於存放從begin到end範圍內每天的有效訂單數
//查詢從begin到end範圍內的所有訂單資料
Map map = new HashMap<>();
map.put("begin", LocalDateTime.of(begin, LocalTime.MIN));
map.put("end", LocalDateTime.of(end, LocalTime.MAX));
List<Orders> ordersList = orderMapper.getByMap(map);
//把查出來的訂單數以及已完成的訂單數加上
for (Orders orders : ordersList) {
LocalDate orderTime = orders.getOrderTime().toLocalDate();
int period = Period.between(begin, orderTime).getDays();
orderCountList[period]++;
if (orders.getStatus().equals(Orders.COMPLETED)) {
validOrderCountList[period]++;
}
}
Integer totalOrderCount = Arrays.stream(orderCountList).reduce(Integer::sum).getAsInt(); //計算訂單總數
Integer validOrderCount = Arrays.stream(validOrderCountList).reduce(Integer::sum).getAsInt(); //計算有效訂單總數
Double orderCompletionRate = totalOrderCount == 0 ? 0.0 : validOrderCount.doubleValue() / totalOrderCount; //計算訂單完成率
//將集合轉換為字串
String dateListString = StringUtils.join(dateList, ",");
String orderCountListString = StringUtils.join(orderCountList, ',');
String validOrderCountListString = StringUtils.join(validOrderCountList, ',');
//構造OrderReportVO並返回
OrderReportVO orderReportVO = OrderReportVO.builder()
.dateList(dateListString)
.orderCountList(orderCountListString)
.validOrderCountList(validOrderCountListString)
.totalOrderCount(totalOrderCount)
.validOrderCount(validOrderCount)
.orderCompletionRate(orderCompletionRate)
.build();
return orderReportVO;
}
功能測試
- 透過介面文件測試方法的執行時間(測試時查詢近30日資料),如下表所示:
介面 | 最佳化前執行時間 | 最佳化後執行時間 |
---|---|---|
營業額統計 | 98ms | 37ms |
使用者統計 | 117ms | 4ms |
訂單統計 | 112ms | 9ms |
- 透過前後端聯調測試驗證了方法的正確性。