@Slf4j
@SpringBootTest
@TestPropertySource(properties = {
"spring.datasource.hikari.maximum-pool-size=5",
"spring.datasource.hikari.minimum-idle=1"
})
public class CountPerformanceTest {
private static final DecimalFormat df = new DecimalFormat("#.##");
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private PlatformTransactionManager transactionManager;
@BeforeEach
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void setupTestData() {
try {
// 创建测试表
jdbcTemplate.execute("""
CREATE TABLE IF NOT EXISTS user_test (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100),
age INT,
city VARCHAR(50),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_email (email),
INDEX idx_city (city)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""");
// 使用事务批量插入
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.execute(status -> {
insertTestDataInBatches();
return null;
});
// 更新统计信息
jdbcTemplate.execute("ANALYZE TABLE user_test");
log.info("测试数据准备完成,表统计信息已更新");
} catch (Exception e) {
log.error("准备测试数据失败", e);
throw new RuntimeException("测试数据初始化失败", e);
}
}
private void insertTestDataInBatches() {
List<Object[]> batchArgs = new ArrayList<>();
String[] cities = {"北京", "上海", "广州", "深圳", "杭州"};
for (int i = 1; i <= 10_000_000; i++) {
String username = "user_" + i;
String email = (i % 10 == 0) ? null : "user" + i + "@test.com";
Integer age = (i % 20 == 0) ? null : 20 + (i % 50);
String city = (i % 15 == 0) ? null : cities[i % cities.length];
batchArgs.add(new Object[]{username, email, age, city});
if (i % 10000 == 0) {
jdbcTemplate.batchUpdate(
"INSERT INTO user_test (username, email, age, city) VALUES (?, ?, ?, ?)",
batchArgs
);
batchArgs.clear();
if (i % 100000 == 0) {
log.info("已插入 {} 条数据", i);
}
}
}
}
@Test
public void testCountPerformance() {
StopWatch stopWatch = new StopWatch("COUNT性能测试");
try {
warmUp();
// 多次测试取平均值
int testRounds = 5;
Map<String, List<Long>> timings = new HashMap<>();
for (int round = 0; round < testRounds; round++) {
log.info("===== 第 {} 轮测试 =====", round + 1);
performSingleRound(stopWatch, timings, round + 1);
// 轮次间隔,避免缓存影响
Thread.sleep(1000);
}
// 计算并输出平均值
logAverageTimings(timings);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("测试被中断", e);
} finally {
if (stopWatch.isRunning()) {
stopWatch.stop();
}
log.info("性能测试总结:\n{}", stopWatch.prettyPrint());
}
}
private void performSingleRound(StopWatch stopWatch, Map<String, List<Long>> timings, int round) {
// 测试count(*)
stopWatch.start("COUNT(*) - Round " + round);
Long countStar = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM user_test", Long.class);
stopWatch.stop();
long time = stopWatch.getLastTaskTimeMillis();
timings.computeIfAbsent("COUNT(*)", k -> new ArrayList<>()).add(time);
log.info("COUNT(*) 结果: {}, 耗时: {} ms", countStar != null ? countStar : "null", time);
// 测试count(1)
stopWatch.start("COUNT(1) - Round " + round);
Long countOne = jdbcTemplate.queryForObject("SELECT COUNT(1) FROM user_test", Long.class);
stopWatch.stop();
time = stopWatch.getLastTaskTimeMillis();
timings.computeIfAbsent("COUNT(1)", k -> new ArrayList<>()).add(time);
log.info("COUNT(1) 结果: {}, 耗时: {} ms", countOne != null ? countOne : "null", time);
// 测试count(主键)
stopWatch.start("COUNT(id) - Round " + round);
Long countId = jdbcTemplate.queryForObject("SELECT COUNT(id) FROM user_test", Long.class);
stopWatch.stop();
time = stopWatch.getLastTaskTimeMillis();
timings.computeIfAbsent("COUNT(id)", k -> new ArrayList<>()).add(time);
log.info("COUNT(id) 结果: {}, 耗时: {} ms", countId != null ? countId : "null", time);
// 测试count(索引列)
stopWatch.start("COUNT(username) - Round " + round);
Long countUsername = jdbcTemplate.queryForObject("SELECT COUNT(username) FROM user_test", Long.class);
stopWatch.stop();
time = stopWatch.getLastTaskTimeMillis();
timings.computeIfAbsent("COUNT(username)", k -> new ArrayList<>()).add(time);
log.info("COUNT(username) 结果: {}, 耗时: {} ms", countUsername != null ? countUsername : "null", time);
// 测试count(非索引列)
stopWatch.start("COUNT(age) - Round " + round);
Long countAge = jdbcTemplate.queryForObject("SELECT COUNT(age) FROM user_test", Long.class);
stopWatch.stop();
time = stopWatch.getLastTaskTimeMillis();
timings.computeIfAbsent("COUNT(age)", k -> new ArrayList<>()).add(time);
log.info("COUNT(age) 结果: {}, 耗时: {} ms", countAge != null ? countAge : "null", time);
}
private void logAverageTimings(Map<String, List<Long>> timings) {
log.info("\n===== 性能测试平均值 =====");
timings.forEach((query, times) -> {
double average = times.stream()
.mapToLong(Long::longValue)
.average()
.orElse(0.0);
log.info("{} 平均耗时: {} ms", query, df.format(average));
});
}
private void warmUp() {
log.info("开始预热查询...");
for (int i = 0; i < 5; i++) {
jdbcTemplate.queryForObject("SELECT COUNT(*) FROM user_test", Long.class);
}
}
@AfterEach
public void cleanup() {
try {
jdbcTemplate.execute("DROP TABLE IF EXISTS user_test");
log.info("测试表清理完成");
} catch (Exception e) {
log.error("清理测试数据失败", e);
}
}
}