索引优化策略
本文档详细介绍 Ape-Volo-Admin 项目的数据库索引设计、查询优化实践及监控方法。
📖 目录
- 索引设计 - 各类索引的 SqlSugar 特性用法
- 查询优化示例 - 操作日志表的性能对比(100+ 倍提升)
- 索引使用情况 - MySQL/SQL Server 监控 SQL 与优化建议
- 最佳实践 - 索引设计原则与常见问题
📈 索引优化策略
1. 索引设计
1.1 主键索引
csharp
/// <summary>
/// 主键索引(聚集索引,决定数据物理存储顺序)
/// </summary>
public class User : BaseEntity
{
[SugarColumn(IsPrimaryKey = true)]
public long Id { get; set; }
}1.2 唯一索引
csharp
/// <summary>
/// 唯一索引(保证字段唯一性,提高查询性能)
/// </summary>
[SugarIndex("unique_{table}_UserName", nameof(UserName), OrderByType.Asc, true)]
public class User : BaseEntity
{
public string UserName { get; set; }
}1.3 普通索引
csharp
/// <summary>
/// 普通索引(加速查询,不保证唯一性)
/// </summary>
[SugarIndex("index_{table}_CreateBy", nameof(CreateBy), OrderByType.Asc)]
[SugarIndex("index_{table}_IsDeleted", nameof(IsDeleted), OrderByType.Asc)]
public class BaseEntity : RootKey<long>, ICreateByEntity, ISoftDeletedEntity
{
public string CreateBy { get; set; }
public bool IsDeleted { get; set; }
}1.4 复合索引
csharp
/// <summary>
/// 复合索引(多字段联合查询,顺序很重要)
/// </summary>
[SugarIndex("index_{table}_Email_Enabled", nameof(Email), OrderByType.Asc, nameof(Enabled), OrderByType.Asc)]
public class User : BaseEntity
{
public string Email { get; set; }
public bool Enabled { get; set; }
}1.5 覆盖索引(Include)
csharp
/// <summary>
/// 覆盖索引(索引包含查询所需的全部字段,避免回表)
/// 语法:{include:字段1,字段2}
/// </summary>
[SugarIndex("index_{table}_UserId{include:Name,Email}", nameof(UserId), OrderByType.Asc)]
public class Order : BaseEntity
{
public long UserId { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}2. 查询优化示例
以操作日志表为例,展示索引优化前后的性能差异。
2.1 实体定义
优化前(无索引):
csharp
/// <summary>
/// 操作日志(优化前:无索引)
/// </summary>
[LogDataBase]
[SplitTable(SplitType.Month)]
[SugarTable($@"{"log_operate"}_{{year}}{{month}}{{day}}", IsDisabledUpdateAll = true)]
public class OperateLog : BaseEntity
{
public string? Area { get; set; }
public string? Controller { get; set; }
public string? Action { get; set; }
public string? Method { get; set; }
public string? Description { get; set; }
public string? RequestUrl { get; set; }
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
public string? RequestParameters { get; set; }
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
public string? ResponseData { get; set; }
[SugarColumn(DefaultValue = "0")]
public long ExecutionDuration { get; set; }
public string? RequestIp { get; set; }
public string? IpAddress { get; set; }
public string? UserAgent { get; set; }
public string? OperatingSystem { get; set; }
public string? DeviceType { get; set; }
public string? BrowserName { get; set; }
public string? Version { get; set; }
[SplitField]
public new DateTime CreateTime { get; set; }
}优化后(添加索引):
csharp
/// <summary>
/// 操作日志(优化后:添加索引)
/// </summary>
[LogDataBase]
[SplitTable(SplitType.Month)]
[SugarTable($@"{"log_operate"}_{{year}}{{month}}{{day}}", IsDisabledUpdateAll = true)]
// 添加复合索引:按创建人 + 创建时间范围查询
[SugarIndex("index_{table}_CreateBy_CreateTime", nameof(CreateBy), OrderByType.Asc, nameof(CreateTime), OrderByType.Desc)]
// 添加单字段索引:按操作类型(Controller + Action)查询
[SugarIndex("index_{table}_Controller", nameof(Controller), OrderByType.Asc)]
[SugarIndex("index_{table}_Action", nameof(Action), OrderByType.Asc)]
// 添加慢查询索引:执行耗时超过阈值
[SugarIndex("index_{table}_ExecutionDuration", nameof(ExecutionDuration), OrderByType.Desc)]
// 添加 IP 查询索引
[SugarIndex("index_{table}_RequestIp", nameof(RequestIp), OrderByType.Asc)]
public class OperateLog : BaseEntity
{
public string? Area { get; set; }
public string? Controller { get; set; }
public string? Action { get; set; }
public string? Method { get; set; }
public string? Description { get; set; }
public string? RequestUrl { get; set; }
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
public string? RequestParameters { get; set; }
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
public string? ResponseData { get; set; }
[SugarColumn(DefaultValue = "0")]
public long ExecutionDuration { get; set; }
public string? RequestIp { get; set; }
public string? IpAddress { get; set; }
public string? UserAgent { get; set; }
public string? OperatingSystem { get; set; }
public string? DeviceType { get; set; }
public string? BrowserName { get; set; }
public string? Version { get; set; }
[SplitField]
public new DateTime CreateTime { get; set; }
}2.2 查询场景对比
场景一:按用户查询指定时间范围的操作记录
csharp
// 优化前:全表扫描
var logs = await db.Queryable<OperateLog>()
.Where(x => x.CreateBy == "admin"
&& x.CreateTime >= startDate
&& x.CreateTime <= endDate)
.OrderBy(x => x.CreateTime, OrderByType.Desc)
.ToPageListAsync(1, 20);
// 执行计划(优化前):
// Table Scan on log_operate_202501 (cost=0..50000 rows=100000)
// Filter: CreateBy='admin' AND CreateTime BETWEEN '2025-01-01' AND '2025-01-31'
// 扫描行数:100,000 | 耗时:~850mscsharp
// 优化后:使用复合索引 index_{table}_CreateBy_CreateTime
var logs = await db.Queryable<OperateLog>()
.Where(x => x.CreateBy == "admin"
&& x.CreateTime >= startDate
&& x.CreateTime <= endDate)
.OrderBy(x => x.CreateTime, OrderByType.Desc)
.ToPageListAsync(1, 20);
// 执行计划(优化后):
// Index Range Scan on index_log_operate_202501_CreateBy_CreateTime (cost=0..120 rows=200)
// Index Cond: CreateBy='admin' AND CreateTime BETWEEN '2025-01-01' AND '2025-01-31'
// 扫描行数:200 | 耗时:~8ms
// 性能提升:106倍场景二:查询慢 SQL(执行时长超过 1000ms)
csharp
// 优化前:全表扫描
var slowLogs = await db.Queryable<OperateLog>()
.Where(x => x.ExecutionDuration > 1000)
.OrderBy(x => x.ExecutionDuration, OrderByType.Desc)
.Take(50)
.ToListAsync();
// 执行计划(优化前):
// Table Scan + Sort (cost=0..50000 rows=100000)
// 扫描行数:100,000 | 耗时:~920mscsharp
// 优化后:使用索引 index_{table}_ExecutionDuration
var slowLogs = await db.Queryable<OperateLog>()
.Where(x => x.ExecutionDuration > 1000)
.OrderBy(x => x.ExecutionDuration, OrderByType.Desc)
.Take(50)
.ToListAsync();
// 执行计划(优化后):
// Index Range Scan on index_log_operate_202501_ExecutionDuration (cost=0..80 rows=150)
// Index Cond: ExecutionDuration > 1000
// 扫描行数:150 | 耗时:~6ms
// 性能提升:153倍场景三:按 IP 统计访问频次
csharp
// 优化前:全表扫描 + 分组
var ipStats = await db.Queryable<OperateLog>()
.GroupBy(x => x.RequestIp)
.Select(x => new
{
Ip = x.RequestIp,
Count = SqlFunc.AggregateCount(x.RequestIp)
})
.OrderBy(x => x.Count, OrderByType.Desc)
.ToListAsync();
// 执行计划(优化前):
// Table Scan + HashAggregate + Sort (cost=0..70000)
// 扫描行数:100,000 | 耗时:~1200mscsharp
// 优化后:使用索引 index_{table}_RequestIp
var ipStats = await db.Queryable<OperateLog>()
.GroupBy(x => x.RequestIp)
.Select(x => new
{
Ip = x.RequestIp,
Count = SqlFunc.AggregateCount(x.RequestIp)
})
.OrderBy(x => x.Count, OrderByType.Desc)
.ToListAsync();
// 执行计划(优化后):
// Index Scan on index_log_operate_202501_RequestIp + GroupAggregate + Sort (cost=0..8000)
// 扫描行数:100,000(索引) | 耗时:~95ms
// 性能提升:12倍(分组场景仍需全索引扫描,但避免回表)2.3 性能对比总结
| 查询场景 | 优化前耗时 | 优化后耗时 | 性能提升 | 关键索引 |
|---|---|---|---|---|
| 按用户+时间范围查询 | ~850ms | ~8ms | 106 倍 | CreateBy + CreateTime 复合索引 |
| 慢 SQL 排查 | ~920ms | ~6ms | 153 倍 | ExecutionDuration 降序索引 |
| IP 访问统计 | ~1200ms | ~95ms | 12 倍 | RequestIp 索引 |
2.4 索引设计要点
- 复合索引顺序:高选择性字段(如
CreateBy)优先,范围查询字段(如CreateTime)放后。 - 覆盖索引:对只需返回索引列的查询,可避免回表(如 IP 统计)。
- 分表场景:索引随分表自动创建(
{table}占位符会替换为实际表名)。 - 维护成本:索引会增加写入开销,避免对"大文本字段"(如
RequestParameters)建索引。
2.5 验证方式
MySQL 执行计划:
sql
-- 查看查询执行计划
EXPLAIN SELECT * FROM log_operate_202501
WHERE CreateBy = 'admin' AND CreateTime BETWEEN '2025-01-01' AND '2025-01-31';
-- 输出关键字段:
-- type: ALL(全表扫描,需优化) | range/ref(使用索引,已优化)
-- key: 使用的索引名称
-- rows: 扫描行数估算SQL Server 执行计划:
sql
-- 开启统计信息
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
-- 执行查询
SELECT * FROM log_operate_202501
WHERE CreateBy = 'admin' AND CreateTime BETWEEN '2025-01-01' AND '2025-01-31';
-- 查看图形化执行计划(推荐)
-- 在 SSMS 中按 Ctrl+M 或点击"包括实际的执行计划"
-- 关键指标:
-- Table Scan(表扫描) → 需要添加索引
-- Index Seek(索引查找) → 索引使用正常
-- Index Scan(索引扫描) → 可能需要优化查询条件性能对比验证:
sql
-- MySQL: 查看查询耗时
SELECT SQL_NO_CACHE * FROM log_operate_202501
WHERE CreateBy = 'admin'
LIMIT 1000;
-- SQL Server: 查看 IO 统计
-- 逻辑读取次数(Logical reads)越低越好
-- 物理读取次数(Physical reads)越低越好3. 索引使用情况
在生产环境中,定期监控索引使用情况可以帮助识别冗余索引(浪费存储空间)和缺失索引(性能瓶颈)。以下提供不同数据库的监控 SQL 及结果解读。
3.1 MySQL 索引监控
查看未使用的索引:
sql
-- 从 MySQL 5.6+ 开始支持
SELECT
t.TABLE_SCHEMA AS '数据库',
t.TABLE_NAME AS '表名',
t.INDEX_NAME AS '索引名',
CASE t.NON_UNIQUE
WHEN 0 THEN '唯一索引'
ELSE '普通索引'
END AS '索引类型',
GROUP_CONCAT(t.COLUMN_NAME ORDER BY t.SEQ_IN_INDEX) AS '索引字段'
FROM information_schema.STATISTICS t
LEFT JOIN performance_schema.table_io_waits_summary_by_index_usage i
ON t.TABLE_SCHEMA = i.OBJECT_SCHEMA
AND t.TABLE_NAME = i.OBJECT_NAME
AND t.INDEX_NAME = i.INDEX_NAME
WHERE t.TABLE_SCHEMA = 'ape_volo_admin_main' -- 替换为实际数据库名
AND i.INDEX_NAME IS NULL -- 从未被使用过
AND t.INDEX_NAME != 'PRIMARY' -- 排除主键
GROUP BY t.TABLE_SCHEMA, t.TABLE_NAME, t.INDEX_NAME, t.NON_UNIQUE
ORDER BY t.TABLE_NAME, t.INDEX_NAME;查看索引使用频率:
sql
SELECT
OBJECT_SCHEMA AS '数据库',
OBJECT_NAME AS '表名',
INDEX_NAME AS '索引名',
COUNT_READ AS '读取次数',
COUNT_WRITE AS '写入次数',
COUNT_FETCH AS '查询扫描次数',
COUNT_INSERT AS '插入触发次数',
COUNT_UPDATE AS '更新触发次数',
COUNT_DELETE AS '删除触发次数',
ROUND(COUNT_READ / (COUNT_WRITE + COUNT_READ) * 100, 2) AS '读取占比(%)'
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA = 'ape_volo_admin_main'
AND INDEX_NAME IS NOT NULL
ORDER BY COUNT_READ DESC
LIMIT 50;3.2 SQL Server 索引监控
查看未使用的索引:
sql
-- 查询自数据库启动以来从未被查询使用的索引
SELECT
DB_NAME() AS '数据库',
OBJECT_NAME(i.object_id) AS '表名',
i.name AS '索引名',
CASE i.type
WHEN 1 THEN '聚集索引'
WHEN 2 THEN '非聚集索引'
ELSE '其他类型'
END AS '索引类型',
s.user_seeks AS '用户查找次数',
s.user_scans AS '用户扫描次数',
s.user_lookups AS '用户查询次数',
s.user_updates AS '用户更新次数'
FROM sys.indexes i
LEFT JOIN sys.dm_db_index_usage_stats s
ON i.object_id = s.object_id
AND i.index_id = s.index_id
AND s.database_id = DB_ID()
WHERE OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
AND i.type_desc <> 'HEAP'
AND i.is_primary_key = 0
AND i.is_unique_constraint = 0
AND s.index_id IS NULL -- 从未被使用
ORDER BY OBJECT_NAME(i.object_id), i.name;查看索引使用频率与收益:
sql
SELECT
OBJECT_NAME(s.object_id) AS '表名',
i.name AS '索引名',
s.user_seeks AS '查找次数',
s.user_scans AS '扫描次数',
s.user_lookups AS '查询次数',
s.user_updates AS '更新次数',
(s.user_seeks + s.user_scans + s.user_lookups) AS '总读取次数',
s.user_updates AS '总写入次数',
CASE
WHEN s.user_updates > 0 THEN
ROUND((s.user_seeks + s.user_scans + s.user_lookups) * 1.0 / s.user_updates, 2)
ELSE 999999
END AS '读写比'
FROM sys.dm_db_index_usage_stats s
INNER JOIN sys.indexes i
ON s.object_id = i.object_id
AND s.index_id = i.index_id
WHERE s.database_id = DB_ID()
AND OBJECTPROPERTY(s.object_id, 'IsUserTable') = 1
ORDER BY (s.user_seeks + s.user_scans + s.user_lookups) DESC;3.3 结果解读
| 指标 | 说明 | 优化建议 |
|---|---|---|
| 读取次数 | 索引被查询使用的总次数(COUNT_READ / user_seeks + user_scans + user_lookups) | 读取次数高的索引应保留,低于 100 次/天可考虑删除 |
| 写入次数 | 索引因 INSERT/UPDATE/DELETE 被更新的次数 | 写入次数远大于读取次数的索引(读写比 < 1:10)慎重 |
| 读写比 | 读取次数 / 写入次数 | 比值 > 10:1 说明索引高效;< 1:1 可能是冗余索引 |
| 扫描次数 | 全表扫描或大范围扫描(COUNT_FETCH / user_scans) | 频繁扫描说明缺少合适的索引,需检查查询条件 |
| 未使用 | 自数据库启动或监控重置后从未被查询使用(INDEX_NAME IS NULL / s.index_id IS NULL) | 确认业务逻辑后可安全删除,释放存储空间 |
| 唯一性 | 唯一索引(NON_UNIQUE = 0)可提高选择性 | 对高重复率字段(如 IsDeleted)避免唯一索引 |
| 聚集索引 | SQL Server 中每表只有一个聚集索引(通常是主键),决定数据物理存储顺序 | 优先用于范围查询或排序字段(如 CreateTime) |
| 覆盖索引 | 索引包含查询所需的全部字段,避免回表(MySQL 的 include / SQL Server 的 INCLUDE 列) | 对频繁查询的字段组合建议使用覆盖索引 |
3.4 优化建议
定期清理冗余索引:
- 运行未使用索引查询,记录 3-6 个月未使用的索引。
- 确认业务逻辑后删除(备份脚本以便回滚)。
- 示例:
DROP INDEX index_user_LastLoginTime ON user;
识别缺失索引(SQL Server):
sql-- 查看系统建议的缺失索引 SELECT d.statement AS '表名', gs.avg_user_impact AS '平均影响(%)', gs.user_seeks AS '查找次数', gs.user_scans AS '扫描次数', d.equality_columns AS '等值查询列', d.inequality_columns AS '范围查询列', d.included_columns AS '包含列', 'CREATE INDEX IX_' + OBJECT_NAME(d.object_id) + '_' + REPLACE(REPLACE(COALESCE(d.equality_columns, '') + COALESCE(d.inequality_columns, ''), ', ', '_'), '[', '') + ']' + ' ON ' + d.statement + ' (' + COALESCE(d.equality_columns, '') + CASE WHEN d.equality_columns IS NOT NULL AND d.inequality_columns IS NOT NULL THEN ', ' ELSE '' END + COALESCE(d.inequality_columns, '') + ')' + CASE WHEN d.included_columns IS NOT NULL THEN ' INCLUDE (' + d.included_columns + ')' ELSE '' END AS '建议SQL' FROM sys.dm_db_missing_index_details d INNER JOIN sys.dm_db_missing_index_groups g ON d.index_handle = g.index_handle INNER JOIN sys.dm_db_missing_index_group_stats gs ON g.index_group_handle = gs.group_handle WHERE d.database_id = DB_ID() ORDER BY gs.avg_user_impact * gs.user_seeks DESC;监控慢查询:
MySQL 慢查询配置:
sql-- 开启慢查询日志 SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 2; -- 超过2秒的查询记录 SET GLOBAL log_queries_not_using_indexes = 'ON'; -- 记录未使用索引的查询 -- 查看慢查询日志路径 SHOW VARIABLES LIKE 'slow_query_log_file'; -- 查看当前配置 SHOW VARIABLES LIKE '%slow_query%'; SHOW VARIABLES LIKE 'long_query_time';慢查询统计分析:
sql-- 查看最慢的10条查询(按平均耗时排序) SELECT SCHEMA_NAME AS '数据库', LEFT(DIGEST_TEXT, 100) AS 'SQL摘要', COUNT_STAR AS '执行次数', ROUND(AVG_TIMER_WAIT/1000000000, 2) AS '平均耗时(秒)', ROUND(MAX_TIMER_WAIT/1000000000, 2) AS '最大耗时(秒)', ROUND(SUM_TIMER_WAIT/1000000000, 2) AS '总耗时(秒)', ROUND(SUM_ROWS_EXAMINED/COUNT_STAR, 0) AS '平均扫描行数', ROUND(SUM_ROWS_SENT/COUNT_STAR, 0) AS '平均返回行数' FROM performance_schema.events_statements_summary_by_digest WHERE SCHEMA_NAME = 'ape_volo_admin_main' -- 替换为实际数据库名 AND DIGEST_TEXT IS NOT NULL ORDER BY AVG_TIMER_WAIT DESC LIMIT 10; -- 查看未使用索引的查询 SELECT SCHEMA_NAME AS '数据库', LEFT(DIGEST_TEXT, 150) AS 'SQL摘要', COUNT_STAR AS '执行次数', ROUND(AVG_TIMER_WAIT/1000000000, 2) AS '平均耗时(秒)', ROUND(SUM_ROWS_EXAMINED/COUNT_STAR, 0) AS '平均扫描行数' FROM performance_schema.events_statements_summary_by_digest WHERE SCHEMA_NAME = 'ape_volo_admin_main' AND SUM_NO_INDEX_USED > 0 -- 未使用索引 ORDER BY COUNT_STAR DESC LIMIT 10; -- 查看全表扫描次数最多的查询 SELECT SCHEMA_NAME AS '数据库', LEFT(DIGEST_TEXT, 150) AS 'SQL摘要', COUNT_STAR AS '执行次数', SUM_NO_INDEX_USED AS '未使用索引次数', SUM_NO_GOOD_INDEX_USED AS '未使用好索引次数', ROUND(AVG_TIMER_WAIT/1000000000, 2) AS '平均耗时(秒)' FROM performance_schema.events_statements_summary_by_digest WHERE SCHEMA_NAME = 'ape_volo_admin_main' AND (SUM_NO_INDEX_USED > 0 OR SUM_NO_GOOD_INDEX_USED > 0) ORDER BY SUM_NO_INDEX_USED DESC LIMIT 10;慢查询日志分析工具:
bash# 使用 mysqldumpslow 分析慢查询日志(Linux/Mac) mysqldumpslow -s at -t 10 /var/log/mysql/slow-query.log # 参数说明: # -s at: 按平均查询时间排序 # -s c: 按查询次数排序 # -s r: 按返回记录数排序 # -t 10: 显示前10条 # Windows PowerShell 查看慢查询日志 Get-Content "C:\ProgramData\MySQL\MySQL Server 8.0\Data\slow-query.log" -Tail 50优化建议:
- 平均扫描行数 > 10000:需要添加索引
- 执行次数高 + 未使用索引:优先优化对象
- 总耗时高的查询:对系统整体影响大,重点关注
分表场景:
- 对按月分表的日志表(如
log_operate_{yyyyMM}),确保每个分表自动创建索引。 - 定期归档历史分表,避免索引维护开销累积。
- 对按月分表的日志表(如
写入密集表:
- 对日志表等高写入场景,索引数量不宜超过 5 个。
- 优先保留高查询频率索引(如
CreateBy + CreateTime),删除低频索引。
4. 最佳实践
4.1 索引设计原则
| 原则 | 说明 | 示例 |
|---|---|---|
| 选择性优先 | 为高选择性字段(唯一值多)建索引,低选择性(如性别)不适合建索引 | ✅ UserName、Email / ❌ Gender |
| 复合索引顺序 | 等值查询字段在前,范围查询字段在后;高选择性字段在前 | ✅ (CreateBy, CreateTime) / ❌ 反过来 |
| 最左前缀 | 复合索引 (A, B, C) 可以支持 A、(A, B)、(A, B, C) 三种查询 | 索引 (Email, Enabled) 可查 Email |
| 覆盖索引 | 查询字段全部包含在索引中,避免回表 | {include:Name,Age} 语法 |
| 避免冗余 | 索引 (A, B) 存在时,单独的索引 A 是冗余的(最左前缀已覆盖) | 删除单独的 A 索引 |
| 控制数量 | 单表索引不超过 5-7 个(写入密集表 ≤ 5 个) | 日志表最多 5 个索引 |
| 避免大字段 | 不对 TEXT/BLOB/长字符串建索引(影响性能和存储) | ❌ RequestParameters 字段 |
4.2 常见场景索引策略
4.2.1 日志表(高写入场景)
csharp
/// 推荐索引:
/// 1. 复合索引:CreateBy + CreateTime(按用户+时间查询)
/// 2. 单字段索引:ExecutionDuration(慢 SQL 排查)
/// 3. 单字段索引:RequestIp(IP 统计)
/// 避免:对大文本字段(RequestParameters/ResponseData)建索引
[SugarIndex("index_{table}_CreateBy_CreateTime", nameof(CreateBy), OrderByType.Asc, nameof(CreateTime), OrderByType.Desc)]
[SugarIndex("index_{table}_ExecutionDuration", nameof(ExecutionDuration), OrderByType.Desc)]
[SugarIndex("index_{table}_RequestIp", nameof(RequestIp), OrderByType.Asc)]
public class OperateLog : BaseEntity { ... }4.2.2 业务表(读多写少)
csharp
/// 推荐索引:
/// 1. 唯一索引:用户名/邮箱(登录验证)
/// 2. 复合索引:部门ID + 状态(常见筛选条件)
/// 3. 覆盖索引:列表查询常用字段
[SugarIndex("unique_{table}_UserName", nameof(UserName), OrderByType.Asc, true)]
[SugarIndex("index_{table}_DeptId_Enabled", nameof(DeptId), OrderByType.Asc, nameof(Enabled), OrderByType.Asc)]
[SugarIndex("index_{table}_Email{include:NickName,Phone}", nameof(Email), OrderByType.Asc)]
public class User : BaseEntity { ... }4.2.3 分表场景
csharp
/// 按月分表的表,索引会自动随分表创建
/// {table} 占位符会替换为实际表名(如 log_operate_202501)
[SplitTable(SplitType.Month)]
[SugarTable($@"{"log_operate"}_{{year}}{{month}}", IsDisabledUpdateAll = true)]
[SugarIndex("index_{table}_CreateTime", nameof(CreateTime), OrderByType.Desc)]
public class OperateLog : BaseEntity
{
[SplitField] // 分表字段
public new DateTime CreateTime { get; set; }
}4.3 常见问题
Q1: 为什么加了索引查询还是很慢?
- 原因:
- 索引列参与了函数计算(如
WHERE YEAR(CreateTime) = 2025) - 使用了
OR条件跨多个字段(索引失效) - 隐式类型转换(如字符串字段用数字查询)
- 数据分布导致优化器选择全表扫描(如查询返回 > 30% 数据)
- 索引列参与了函数计算(如
- 解决:
- 改为
WHERE CreateTime >= '2025-01-01' AND CreateTime < '2026-01-01' - 拆分
OR为多个查询或使用UNION - 确保查询参数类型与字段一致
- 增加其他过滤条件减少返回行数
- 改为
Q2: 索引越多越好吗?
- 答案:否。每个索引都会增加写入开销(
INSERT/UPDATE/DELETE时需维护索引)。 - 建议:
- 写入密集表(日志表):≤ 5 个索引
- 普通业务表:≤ 7 个索引
- 定期清理未使用的索引
Q3: 什么时候用复合索引,什么时候用多个单字段索引?
- 复合索引:查询条件经常同时使用多个字段(如
WHERE CreateBy = ? AND CreateTime BETWEEN ? AND ?) - 单字段索引:字段独立查询(如
WHERE Email = ?) - 误区:复合索引
(A, B)不能替代(B)的查询(不满足最左前缀)
Q4: 分表后索引如何管理?
- 自动创建:SqlSugar 的
SplitTable会自动为每个分表创建索引({table}占位符替换) - 历史表归档:定期将历史分表迁移到归档库,删除生产库的历史分表(包括索引)
- 监控:使用索引使用情况 SQL 检查各分表索引是否有效
Q5: 如何平衡索引与写入性能?
- 策略:
- 优先为高频查询建索引(读写比 > 10:1)
- 批量写入时临时禁用非必要索引(备份/恢复场景)
- 异步写入日志表(Redis 队列 + 后台消费,参考 操作日志文档)
- 使用分区表代替过多索引(时间范围查询)
📚 相关文档
- SqlSugar 扩展配置 - ORM 配置、分表、过滤器
- 操作日志 - 日志采集与异步写入策略
- 多租户配置 - 租户隔离与动态路由

