Skip to content

索引优化策略

本文档详细介绍 Ape-Volo-Admin 项目的数据库索引设计、查询优化实践及监控方法。


📖 目录


📈 索引优化策略

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 | 耗时:~850ms
csharp
// 优化后:使用复合索引 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 | 耗时:~920ms
csharp
// 优化后:使用索引 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 | 耗时:~1200ms
csharp
// 优化后:使用索引 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~8ms106 倍CreateBy + CreateTime 复合索引
慢 SQL 排查~920ms~6ms153 倍ExecutionDuration 降序索引
IP 访问统计~1200ms~95ms12 倍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 优化建议

  1. 定期清理冗余索引

    • 运行未使用索引查询,记录 3-6 个月未使用的索引。
    • 确认业务逻辑后删除(备份脚本以便回滚)。
    • 示例:DROP INDEX index_user_LastLoginTime ON user;
  2. 识别缺失索引(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;
  3. 监控慢查询

    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:需要添加索引
    • 执行次数高 + 未使用索引:优先优化对象
    • 总耗时高的查询:对系统整体影响大,重点关注
  4. 分表场景

    • 对按月分表的日志表(如 log_operate_{yyyyMM}),确保每个分表自动创建索引。
    • 定期归档历史分表,避免索引维护开销累积。
  5. 写入密集表

    • 对日志表等高写入场景,索引数量不宜超过 5 个。
    • 优先保留高查询频率索引(如 CreateBy + CreateTime),删除低频索引。

4. 最佳实践

4.1 索引设计原则

原则说明示例
选择性优先为高选择性字段(唯一值多)建索引,低选择性(如性别)不适合建索引UserNameEmail / ❌ 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: 为什么加了索引查询还是很慢?

  • 原因
    1. 索引列参与了函数计算(如 WHERE YEAR(CreateTime) = 2025
    2. 使用了 OR 条件跨多个字段(索引失效)
    3. 隐式类型转换(如字符串字段用数字查询)
    4. 数据分布导致优化器选择全表扫描(如查询返回 > 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: 如何平衡索引与写入性能?

  • 策略
    1. 优先为高频查询建索引(读写比 > 10:1)
    2. 批量写入时临时禁用非必要索引(备份/恢复场景)
    3. 异步写入日志表(Redis 队列 + 后台消费,参考 操作日志文档
    4. 使用分区表代替过多索引(时间范围查询)

📚 相关文档

版权所有 © 2021-2026 ApeVolo-Team