团队 code review 时,一位同事把 count(*)改成了 count(1),说这样性能更好。真的是这样吗?今天通过源码和实测数据,把这个问题说透。
本文基于 MySQL 8.0.28 版本测试,不同版本的优化器行为可能有差异
三种 count 方式的本质区别
先看看这三种写法在 MySQL 中到底做了什么:
java
// 模拟MySQL处理count的伪代码
public class CountProcessor {
// count(*) 的处理逻辑
public long countStar(Table table) {
long count = 0;
for (Row row : table.getAllRows()) {
// 只要行存在就计数,不管字段值
count++;
}
return count;
}
// count(1) 的处理逻辑
public long countOne(Table table) {
long count = 0;
for (Row row : table.getAllRows()) {
// MySQL优化器会把count(1)转换为count(*)
count++;
}
return count;
}
// count(字段) 的处理逻辑
public long countField(Table table, String fieldName) {
long count = 0;
for (Row row : table.getAllRows()) {
Object value = row.getField(fieldName);
// 关键:只统计非NULL值
if (value != null) {
count++;
}
}
return count;
}
}

性能测试:用数据说话
创建测试环境,准备 1000 万条数据,多轮测试取平均值:
java
@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);
}
}
}
基准测试结果(基于 AWS RDS MySQL 8.0.28)
测试环境:
- 实例规格:db.r6g.xlarge (4 vCPU, 32 GiB RAM)
- 存储:1000 GiB GP3 SSD
- 测试数据:1000 万条记录
场景 | COUNT(*) | COUNT(1) | COUNT(主键) | COUNT(索引列) | COUNT(非索引列) |
---|---|---|---|---|---|
冷查询 | 2.13s | 2.15s | 2.31s | 2.45s | 8.76s |
热查询 | 0.18s | 0.18s | 0.21s | 0.23s | 7.89s |
并行查询(4 线程) | 0.58s | 0.59s | 0.65s | 0.71s | 2.34s |
结论:COUNT(*)和 COUNT(1)性能基本一致,COUNT(非索引列)性能最差。
存储引擎差异与索引统计
不同存储引擎对 COUNT 的处理方式差异很大:
java
@Component
@Slf4j
public class StorageEngineCountAnalyzer {
@Autowired
private JdbcTemplate jdbcTemplate;
public void analyzeEngineSpecificBehavior() {
// 查看表的存储引擎和统计信息
String engineQuery = """
SELECT
t.table_name,
t.engine,
t.table_rows,
t.avg_row_length,
t.data_length,
t.index_length
FROM information_schema.tables t
WHERE t.table_schema = DATABASE()
AND t.table_name IN ('users', 'orders', 'products')
""";
List<Map<String, Object>> results = jdbcTemplate.queryForList(engineQuery);
results.forEach(row -> {
String tableName = (String) row.get("table_name");
String engine = (String) row.get("engine");
Long approximateRows = (Long) row.get("table_rows");
log.info("表: {}, 引擎: {}, 近似行数: {}", tableName, engine, approximateRows);
// 不同引擎的COUNT行为
switch (engine) {
case "MyISAM":
log.info("MyISAM引擎维护了准确的行数,COUNT(*)是O(1)操作");
break;
case "InnoDB":
log.info("InnoDB引擎需要扫描索引统计行数,COUNT(*)是O(n)操作");
updateIndexStatistics(tableName);
break;
case "Memory":
log.info("Memory引擎类似MyISAM,维护行数元数据");
break;
}
});
}
/**
* 更新表的索引统计信息
*/
public void updateIndexStatistics(String tableName) {
try {
// 更新索引统计信息
jdbcTemplate.execute("ANALYZE TABLE " + tableName);
// 查看索引基数和选择性
String sql = """
SELECT
s.index_name,
s.cardinality,
t.table_rows,
ROUND((s.cardinality / t.table_rows * 100), 2) as selectivity_percent
FROM information_schema.statistics s
JOIN information_schema.tables t
ON s.table_schema = t.table_schema
AND s.table_name = t.table_name
WHERE s.table_schema = DATABASE()
AND s.table_name = ?
AND s.seq_in_index = 1
ORDER BY s.cardinality DESC
""";
List<Map<String, Object>> stats = jdbcTemplate.queryForList(sql, tableName);
stats.forEach(stat ->
log.info("索引: {}, 基数: {}, 选择性: {}%",
stat.get("index_name"),
stat.get("cardinality"),
stat.get("selectivity_percent")
)
);
} catch (Exception e) {
log.error("更新表统计信息失败: {}", tableName, e);
}
}
}
执行计划成本分析与监控
通过 EXPLAIN 和性能监控深入分析 COUNT 的执行成本:
java
@Component
@Slf4j
public class CountCostAnalyzer {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private MeterRegistry meterRegistry;
public void analyzeCountPlans() {
String[] queries = {
"SELECT COUNT(*) FROM user_test",
"SELECT COUNT(1) FROM user_test",
"SELECT COUNT(id) FROM user_test",
"SELECT COUNT(username) FROM user_test",
"SELECT COUNT(email) FROM user_test"
};
for (String query : queries) {
analyzeQueryWithMetrics(query);
}
}
private void analyzeQueryWithMetrics(String query) {
Timer.Sample sample = Timer.Sample.start(meterRegistry);
try {
log.info("分析查询: {}", query);
// EXPLAIN分析
List<Map<String, Object>> explainResult = jdbcTemplate.queryForList("EXPLAIN " + query);
for (Map<String, Object> row : explainResult) {
log.info("type: {}, key: {}, rows: {}, Extra: {}",
row.get("type"),
row.get("key"),
row.get("rows"),
row.get("Extra")
);
}
// 使用会话级profiling
analyzeCostProfile(query);
// 记录成功指标
meterRegistry.counter("mysql.count.success", "query", extractQueryType(query))
.increment();
} catch (Exception e) {
log.error("查询分析失败: {}", query, e);
meterRegistry.counter("mysql.count.error", "query", extractQueryType(query))
.increment();
} finally {
sample.stop(Timer.builder("mysql.count.duration")
.tag("query", extractQueryType(query))
.register(meterRegistry));
}
}
public void analyzeCostProfile(String query) {
jdbcTemplate.execute((ConnectionCallback<Void>) connection -> {
try (Statement stmt = connection.createStatement()) {
stmt.execute("SET profiling = 1");
// 执行查询
jdbcTemplate.queryForObject(query, Long.class);
// 获取profile信息
try (ResultSet rs = stmt.executeQuery("SHOW PROFILE")) {
while (rs.next()) {
double duration = rs.getDouble("Duration");
if (duration > 0.001) {
log.info("步骤: {}, 耗时: {}s",
rs.getString("Status"),
duration
);
}
}
}
} catch (SQLException e) {
log.error("Profile分析失败", e);
} finally {
// 确保关闭profiling
try {
connection.createStatement().execute("SET profiling = 0");
} catch (SQLException e) {
log.warn("关闭profiling失败", e);
}
}
return null;
});
}
private String extractQueryType(String query) {
if (query.contains("COUNT(*)")) return "count_star";
if (query.contains("COUNT(1)")) return "count_one";
if (query.contains("COUNT(id)")) return "count_id";
if (query.contains("COUNT(email)")) return "count_email";
return "count_other";
}
}
生产环境实战场景
场景 1:高并发计数器实现(使用 Caffeine 缓存)
java
@Component
@Slf4j
public class ConcurrentCountManager {
// 使用Caffeine缓存,更安全高效
private final Cache<String, Long> countCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats()
.build();
@Autowired
private JdbcTemplate jdbcTemplate;
public long getCount(String tableName) {
return countCache.get(tableName, this::queryCount);
}
private long queryCount(String tableName) {
// 验证表名防止SQL注入
if (!isValidTableName(tableName)) {
throw new IllegalArgumentException("Invalid table name: " + tableName);
}
Long count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM " + tableName, Long.class
);
return count != null ? count : 0L;
}
private boolean isValidTableName(String tableName) {
return tableName != null && tableName.matches("^[a-zA-Z_][a-zA-Z0-9_]*$");
}
@Scheduled(fixedDelay = 60000)
public void logCacheStats() {
CacheStats stats = countCache.stats();
log.info("缓存统计 - 命中率: {}, 加载次数: {}, 驱逐次数: {}",
String.format("%.2f%%", stats.hitRate() * 100),
stats.loadCount(),
stats.evictionCount()
);
}
}
场景 2:分库分表场景的 COUNT 优化
java
@Service
@Slf4j
public class ShardingCountService {
@Autowired
private List<DataSource> shardDataSources;
@Autowired
private ExecutorService executorService;
/**
* 分库分表场景下的COUNT优化
*/
public long getShardedCount(String logicalTableName) {
CompletableFuture<Long>[] futures = new CompletableFuture[shardDataSources.size()];
for (int i = 0; i < shardDataSources.size(); i++) {
final int shardIndex = i;
futures[i] = CompletableFuture.supplyAsync(() -> {
JdbcTemplate shardJdbcTemplate = new JdbcTemplate(shardDataSources.get(shardIndex));
String physicalTable = logicalTableName + "_" + shardIndex;
try {
Long count = shardJdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM " + physicalTable,
Long.class
);
log.debug("分片 {} 统计结果: {}", shardIndex, count);
return count != null ? count : 0L;
} catch (Exception e) {
log.error("分片 {} 统计失败", shardIndex, e);
return 0L;
}
}, executorService);
}
// 汇总所有分片结果
return CompletableFuture.allOf(futures)
.thenApply(v -> Arrays.stream(futures)
.mapToLong(f -> f.join())
.sum()
)
.join();
}
}
场景 3:COUNT 策略选择
java
@Service
@Slf4j
public class SmartCountStrategySelector {
@Autowired
private CountOptimizationConfig config;
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private ConcurrentCountManager countManager;
@Autowired
private RedisTemplate<String, Long> redisTemplate;
/**
* 选择最优的COUNT策略
*/
public long getOptimizedCount(String tableName, CountContext context) {
// 1. 评估表大小
long approximateSize = getApproximateTableSize(tableName);
// 2. 根据上下文选择策略
CountOptimizationConfig.CountStrategy strategy = selectStrategy(
approximateSize,
context
);
log.debug("表 {} 选择策略: {}, 预估大小: {}",
tableName, strategy, approximateSize);
// 3. 执行对应策略
return switch (strategy) {
case DIRECT -> directCount(tableName);
case CACHED -> countManager.getCount(tableName);
case APPROXIMATE -> approximateCount(tableName);
case COUNTER_TABLE -> counterTableCount(tableName);
};
}
private CountOptimizationConfig.CountStrategy selectStrategy(
long tableSize,
CountContext context) {
// 高精度要求
if (context.isHighAccuracy()) {
return tableSize < 1_000_000 ?
CountOptimizationConfig.CountStrategy.DIRECT :
CountOptimizationConfig.CountStrategy.COUNTER_TABLE;
}
// 高频查询
if (context.getQueryFrequency() > 100) {
return CountOptimizationConfig.CountStrategy.CACHED;
}
// 默认策略
return config.selectStrategy(tableSize);
}
private long getApproximateTableSize(String tableName) {
String sql = """
SELECT table_rows
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = ?
""";
Long size = jdbcTemplate.queryForObject(sql, Long.class, tableName);
return size != null ? size : 0L;
}
private long directCount(String tableName) {
Long count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM " + tableName, Long.class
);
return count != null ? count : 0L;
}
private long approximateCount(String tableName) {
return getApproximateTableSize(tableName);
}
private long counterTableCount(String tableName) {
String sql = "SELECT count_value FROM table_counters WHERE table_name = ?";
Long count = jdbcTemplate.queryForObject(sql, Long.class, tableName);
return count != null ? count : 0L;
}
@Data
@Builder
public static class CountContext {
private boolean highAccuracy;
private int queryFrequency; // 每分钟查询次数
private boolean realTime;
private long maxLatencyMs;
}
}
场景 4:带熔断器的 COUNT 服务
java
@Component
@Slf4j
public class CountCircuitBreaker {
private final CircuitBreaker circuitBreaker;
@Autowired
private RedisTemplate<String, Long> redisTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
public CountCircuitBreaker() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.slidingWindowSize(10)
.build();
this.circuitBreaker = CircuitBreaker.of("countService", config);
circuitBreaker.getEventPublisher()
.onStateTransition(event ->
log.warn("熔断器状态变更: {} -> {}",
event.getStateTransition().getFromState(),
event.getStateTransition().getToState())
);
}
public long getCountWithFallback(String tableName, Supplier<Long> countSupplier) {
return circuitBreaker.executeSupplier(() -> {
log.debug("执行COUNT查询: {}", tableName);
return countSupplier.get();
}, throwable -> {
log.error("COUNT查询失败,使用降级策略: {}", tableName, throwable);
return getFallbackCount(tableName);
});
}
private long getFallbackCount(String tableName) {
// 降级策略优先级
// 1. 尝试从Redis缓存获取
String cacheKey = "count:fallback:" + tableName;
Long cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
log.info("使用Redis缓存值作为降级: {} = {}", tableName, cached);
return cached;
}
// 2. 使用information_schema近似值
try {
String sql = """
SELECT table_rows
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = ?
""";
Long approximate = jdbcTemplate.queryForObject(sql, Long.class, tableName);
if (approximate != null) {
log.info("使用近似值作为降级: {} ≈ {}", tableName, approximate);
// 缓存近似值
redisTemplate.opsForValue().set(cacheKey, approximate, Duration.ofMinutes(10));
return approximate;
}
} catch (Exception e) {
log.error("获取近似值失败", e);
}
// 3. 返回-1表示无法获取
log.warn("所有降级策略失败: {}", tableName);
return -1L;
}
}
COUNT 优化配置类
java
@Configuration
@ConfigurationProperties(prefix = "mysql.count.optimization")
@Data
@Validated
public class CountOptimizationConfig {
@NotNull
@Min(1)
private Integer cacheSize = 1000;
@NotNull
@Min(1)
private Integer cacheExpireMinutes = 5;
@NotNull
private Boolean enableParallelQuery = true;
@NotNull
@Min(1)
@Max(16)
private Integer parallelThreads = 4;
@NotNull
private Boolean enableApproximateCount = true;
@NotNull
@Min(0)
@Max(100)
private Integer approximateThresholdPercent = 5;
/**
* 根据表大小自动选择COUNT策略
*/
public CountStrategy selectStrategy(long tableSize) {
if (tableSize < 10_000) {
return CountStrategy.DIRECT;
} else if (tableSize < 1_000_000) {
return CountStrategy.CACHED;
} else if (enableApproximateCount) {
return CountStrategy.APPROXIMATE;
} else {
return CountStrategy.COUNTER_TABLE;
}
}
public enum CountStrategy {
DIRECT, // 直接查询
CACHED, // 使用缓存
APPROXIMATE, // 近似值
COUNTER_TABLE // 计数器表
}
}
MySQL 8.0 新特性对 COUNT 的影响
java
@Component
@Slf4j
public class MySQL8CountOptimizer {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* MySQL 8.0 并行查询配置
*/
@PostConstruct
public void configureParallelQuery() {
// 检查MySQL版本
String version = jdbcTemplate.queryForObject(
"SELECT VERSION()", String.class
);
if (version != null && version.startsWith("8.")) {
log.info("MySQL 8.0 detected, configuring parallel query");
// 预热查询计划缓存
warmupQueryPlanCache();
}
}
/**
* 预热查询计划缓存
*/
private void warmupQueryPlanCache() {
List<String> criticalTables = Arrays.asList("users", "orders", "products");
criticalTables.forEach(table -> {
try {
// 预热不同类型的COUNT查询
jdbcTemplate.queryForObject("SELECT COUNT(*) FROM " + table, Long.class);
// 预热带条件的COUNT
jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM " + table + " WHERE id > ?",
Long.class,
0
);
log.info("查询计划缓存预热完成: {}", table);
} catch (Exception e) {
log.warn("查询计划缓存预热失败: {}", table, e);
}
});
}
public void testParallelQueryPerformance() {
StopWatch watch = new StopWatch();
// 禁用并行
watch.start("COUNT without parallel");
jdbcTemplate.execute("SET SESSION innodb_parallel_read_threads = 1");
Long count1 = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM large_table", Long.class
);
watch.stop();
// 启用并行
watch.start("COUNT with parallel");
jdbcTemplate.execute("SET SESSION innodb_parallel_read_threads = 4");
Long count2 = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM large_table", Long.class
);
watch.stop();
log.info("并行查询性能对比:\n{}", watch.prettyPrint());
}
}
决策流程图

时间复杂度分析
操作类型 | MyISAM | InnoDB (无 WHERE) | InnoDB (有 WHERE) |
---|---|---|---|
COUNT(*) | O(1) | O(n) - 扫描最小索引 | O(log n + m) - m 为结果集大小 |
COUNT(indexed_column) | O(1) | O(n) - 扫描列索引 | O(log n + m) |
COUNT(non_indexed_column) | O(n) | O(n) - 全表扫描 | O(n) |
COUNT(DISTINCT column) | O(n log n) | O(n log n) | O(n log n) |
注:n 为表的总行数
最佳实践
- 统计总行数 :使用
COUNT(*)
,语义清晰,性能最优 - 统计非空值 :使用
COUNT(column)
,注意 NULL 值影响 - 大表优化:考虑缓存、近似值或分片统计
- 避免在循环中 COUNT:使用 GROUP BY 一次查询
- 注意存储引擎差异:MyISAM 的 COUNT(*)是 O(1),InnoDB 是 O(n)
- 监控慢查询:设置合理的 slow_query_log 阈值
- 分页优化:避免每次都 COUNT,考虑游标分页
- 安全防护:永远不要直接拼接 SQL,使用参数化查询
- MySQL 8.0 优化:启用并行查询提升大表 COUNT 性能
- 生产环境策略:组合使用计数器表、缓存和异步更新
- 高可用保障:使用熔断器防止雪崩,提供降级方案
- 分库分表:并行统计各分片,注意连接池大小
总结
对比维度 | COUNT(*) | COUNT(1) | COUNT(字段) | COUNT(DISTINCT) |
---|---|---|---|---|
统计内容 | 所有行 | 所有行 | 非 NULL 值 | 去重后的非 NULL 值 |
性能表现 | 最优(InnoDB 会选择最小索引) | 与 COUNT(*)相同 | 取决于字段是否有索引 | 需要排序去重,最慢 |
使用场景 | 统计总行数 | 无特殊优势,不推荐 | 统计字段完整性 | 统计唯一值数量 |
NULL 处理 | 包含 NULL 行 | 包含 NULL 行 | 排除 NULL 值 | 排除 NULL 值 |
优化器处理 | 标准处理 | 转换为 COUNT(*) | 需要检查字段值 | 需要去重操作 |
推荐指数 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐(特定场景) | ⭐⭐⭐(必要时使用) |