大字段查询性能优化终极方案

场景诊断

cs 复制代码
 var query = _logRequestRepository
  .WhereIf(!string.IsNullOrWhiteSpace(input.ApiMethod), x => x.ApiMethod.Contains(input.ApiMethod))
  .WhereIf(!string.IsNullOrWhiteSpace(input.SysCode), x => x.SysCode.Contains(input.SysCode))
  .WhereIf(input.Status.HasValue, x => x.Status == input.Status)
  .WhereIf(!string.IsNullOrWhiteSpace(input.TrayBarcode), x => x.ParamJson.Contains(input.TrayBarcode))
  .OrderBy(!string.IsNullOrWhiteSpace(input.Sorting) ? input.Sorting : "RequestTime desc").Take(100);

PARAM_JSON是大字段varchar(max)类型 超长
这种查询方式ParamJson.Contains(input.TrayBarcode)典型的无法走普通索引的查询

三种查询方式的性能排序与核心差距

查询方式 效率排名 典型QPS(参考) P95延迟 索引利用能力 内存开销
原生SQL/ADO.NET/Dapper 第1名 1842+ 58ms 完全可控(可强制走索引) 极低
SqlSugar 第2名 约1200-1500(估算) 80-100ms 较优(需手动优化) 中等
EF Core 第3名 312 394ms 可能失效(生成低效SQL) 极高

原生SQL(最快) > SqlSugar(中等) > EF Core(最慢)

⚡ 一、效率最快的终极方案:Dapper + 全文索引

1.1 SQL Server端:创建全文索引

sql 复制代码
-- 第1步:检查是否启用全文索引(若未启用,SQL Server服务配置需安装)
EXEC sp_fulltext_database 'enable';
GO

-- 第2步:创建全文目录(存储索引的容器)
CREATE FULLTEXT CATALOG FT_Catalog_LogRequest WITH ACCENT_SENSITIVITY = OFF;
GO

-- 第3步:在目标表上创建全文索引
-- 关键前提:表必须有一个单列、唯一、非空的索引(通常是主键)
CREATE FULLTEXT INDEX ON dbo.LogRequests(
    ParamJson       -- 你要模糊查询的大字段
        LANGUAGE 2052,  -- 简体中文分词,大幅提升中文匹配精度
    ApiMethod,      -- 其他需要全文检索的字段可一并加入
    SysCode
)
KEY INDEX PK_LogRequests   -- 替换为你的实际主键索引名
ON FT_Catalog_LogRequest
WITH CHANGE_TRACKING AUTO;  -- 自动跟踪数据变更
GO

-- 第4步:为其他过滤字段创建B-Tree索引(加速Status、时间排序等)
CREATE NONCLUSTERED INDEX IX_LogRequests_Status_RequestTime 
ON dbo.LogRequests (Status, RequestTime DESC) 
INCLUDE (ApiMethod, SysCode);  -- 覆盖索引,避免回表

传统LIKE '%xxx%'必然导致全表扫描,无论怎么优化SQL都无效

全文索引将大文本拆分为词库,查询时走倒排索引,复杂度从O(n)降至O(log n)

1.2 应用端代码:Dapper原生SQL(最高效调用)

cs 复制代码
 public virtual async Task<List<LogRequestDto>> GetLogRequestListDapper(GetLogRequestListInput input)
 {
     StringBuilder sb = new StringBuilder();
     sb.Append($@"SELECT
                 	Id,
                     API_METHOD AS ApiMethod,
                     SYS_CODE AS SysCode,
                     STATUS AS STATUS,
                     REQUEST_TIME AS RequestTime,
                     PARAM_JSON AS ParamJson,
                     RESPONSE_TIME AS ResponseTime 
             FROM
                 LOG_REQUEST WITH ( NOLOCK ) WHERE 1=1");

     if (!input.ApiMethod.IsNullOrWhiteSpace())
     {
         sb.Append(@$" AND API_METHOD = '{input.ApiMethod}' ");
     }
     if (!input.SysCode.IsNullOrWhiteSpace())
     {
         sb.Append(@$" AND SYS_CODE = '{input.SysCode}' ");
     }
     if (input.Status.HasValue)
     {
         sb.Append(@$" AND STATUS = '{input.Status}' ");
     }
     if (!string.IsNullOrWhiteSpace(input.TrayBarcode))
     {
         sb.Append(@$" AND CONTAINS(PARAM_JSON, '{input.TrayBarcode}') ");
     }
     List<LogRequestDto> logRequestDtos = new List<LogRequestDto>();

     var connection = await GetDbConnectionAsync();
     var trans = await GetDbTransactionAsync();
     var dtoList = await connection.QueryAsync<LogRequestDto>(sb.ToString(), null, transaction: trans, commandTimeout: 0);
     logRequestDtos = dtoList.ToList();
     return logRequestDtos;
 }
cs 复制代码
        public async Task<PagedResultDto<LogRequestDto>> QueryPageListAsync(GetLogRequestListInput input)
        {

            if (!input.TrayBarcode.IsNullOrEmpty())
            {
                var list = await _logRequestDapper.GetLogRequestListDapper(input);
                var logRequests = list.OrderByDescending(x => x.RequestTime).ToList();
                var totalCount = list.Count();
                var logRequestDtos = logRequests.Skip(input.SkipCount).Take(input.MaxResultCount).ToList();
                //var query = _logRequestRepository
                // .WhereIf(!string.IsNullOrWhiteSpace(input.ApiMethod), x => x.ApiMethod.Contains(input.ApiMethod))
                // .WhereIf(!string.IsNullOrWhiteSpace(input.SysCode), x => x.SysCode.Contains(input.SysCode))
                // .WhereIf(input.Status.HasValue, x => x.Status == input.Status)
                // .WhereIf(!string.IsNullOrWhiteSpace(input.TrayBarcode), x => x.ParamJson.Contains(input.TrayBarcode))
                // .OrderBy(!string.IsNullOrWhiteSpace(input.Sorting) ? input.Sorting : "RequestTime desc").Take(100);
                //var count = await query.AsNoTracking().CountAsync();
                //var queryList = await query.PageBy(input).AsNoTracking().ToListAsync();

                //var logRequestDtos = ObjectMapper.Map<List<LogRequest>, List<LogRequestDto>>(queryList);
                logRequestDtos = logRequestDtos.Select(_ =>
                {
                    if (_.RequestTime.HasValue && _.ResponseTime.HasValue)
                    {
                        _.SpendTime = (_.ResponseTime.Value - _.RequestTime.Value).TotalMilliseconds;
                    }
                    return _;
                }).ToList();

                return new PagedResultDto<LogRequestDto>(totalCount, logRequestDtos);


        }
            else
            {

                var query = _logRequestRepository
                   .WhereIf(!string.IsNullOrWhiteSpace(input.ApiMethod), x => x.ApiMethod.Contains(input.ApiMethod))
                   .WhereIf(!string.IsNullOrWhiteSpace(input.SysCode), x => x.SysCode.Contains(input.SysCode))
                   .WhereIf(input.Status.HasValue, x => x.Status == input.Status)
                   .OrderBy(!string.IsNullOrWhiteSpace(input.Sorting) ? input.Sorting : "RequestTime desc");
        var count = await query.AsNoTracking().CountAsync();
        var queryList = await query.PageBy(input).AsNoTracking().ToListAsync();

        var logRequestDtos = ObjectMapper.Map<List<LogRequest>, List<LogRequestDto>>(queryList);
        logRequestDtos = logRequestDtos.Select(_ =>
                {
                    if (_.RequestTime.HasValue && _.ResponseTime.HasValue)
                    {
                        _.SpendTime = (_.ResponseTime.Value - _.RequestTime.Value).TotalMilliseconds;
                    }
                    return _;
                }).ToList();

return new PagedResultDto<LogRequestDto>(count, logRequestDtos);
            }

        }

此代码的杀手级优势:

  • 查询缓存命中率极高 :Dapper对完全相同SQL结构+参数化 的查询缓存执行计划,第二次起微秒级响应

  • 内存零浪费:只select需要的列,大字段ParamJson仅在全文中索引,返回时不额外占用内存。

  • 索引友好LIKE 'str%'可走B-Tree索引,CONTAINS走全文索引,Status=走普通索引。每种查询都有索引支撑

⚖️ 二、为什么不用EF Core或SqlSugar?(性能差距根源)

维度 Dapper + 原生SQL(推荐) SqlSugar EF Core 性能差距原因
大字段LIKE 全文索引(CONTAINS) 只能LIKE '%x%',全表扫描 只能LIKE '%x%',全表扫描 10-100倍。索引存在与不存在的区别
内存开销 仅传输投影字段 投影需手动Select 易漏掉Select,拉取整个实体 5-10倍。EF默认SELECT *风险
执行计划缓存 100%参数化,缓存命中 参数化良好 动态SQL易生成字面量,缓存失效 3-6倍。Dapper缓存命中96μs vs EF首次数百ms
索引提示/强制索引 完全支持(WITH INDEX) 不支持 不支持LINQ,必须FromSqlRaw 关键能力缺失。无法干预错误执行计划

结论 :SqlSugar和EF Core在"大字段模糊查询"这个特定场景下,由于无法在LINQ层面调用CONTAINS ,注定只能走全表扫描。这不是ORM快慢的问题,是功能缺失导致索引无法使用 的问题。只有原生SQL能让你用上全文索引。

相关推荐
Nyarlathotep011320 小时前
SQL的事务控制
sql·mysql
晨星shine2 天前
GC、Dispose、Unmanaged Resource 和 Managed Resource
后端·c#
NineData2 天前
NineData智能数据管理平台新功能发布|2026年1-2月
数据库·sql·数据分析
用户298698530142 天前
.NET 文档自动化:Spire.Doc 设置奇偶页页眉/页脚的最佳实践
后端·c#·.net
用户3667462526742 天前
接口文档汇总 - 2.设备状态管理
c#
用户3667462526742 天前
接口文档汇总 - 3.PLC通信管理
c#
阿里云大数据AI技术3 天前
用 SQL 调大模型?Hologres + 百炼,让数据开发直接“对话”AI
sql·llm
Ray Liang3 天前
用六边形架构与整洁架构对比是伪命题?
java·python·c#·架构设计
Scout-leaf6 天前
WPF新手村教程(三)—— 路由事件
c#·wpf
用户298698530146 天前
程序员效率工具:Spire.Doc如何助你一键搞定Word表格排版
后端·c#·.net