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

场景诊断

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能让你用上全文索引。

相关推荐
SQL必知必会2 小时前
SQL 优化技术精要:让查询飞起来
数据库·sql
少云清2 小时前
【安全测试】5_应用服务器安全性测试 _SQL注入和文件上传漏洞
数据库·sql·安全性测试
SQL必知必会13 小时前
SQL 窗口帧:ROWS vs RANGE 深度解析
数据库·sql·性能优化
游乐码14 小时前
c#变长关键字和参数默认值
学习·c#
一个天蝎座 白勺 程序猿15 小时前
破译JSON密码:KingbaseES全场景JSON数据处理实战指南
数据库·sql·json·kingbasees·金仓数据库
全栈小515 小时前
【C#】合理使用DeepSeek相关AI应用为我们提供强有力的开发工具,在.net core 6.0框架下使用JsonNode动态解析json字符串,如何正确使用单问号和双问号做好空值处理
人工智能·c#·json·.netcore·deepseek
wearegogog12315 小时前
基于C#的TCP/IP通信客户端与服务器
服务器·tcp/ip·c#
SQL必知必会15 小时前
使用 SQL 进行 RFM 客户细分分析
大数据·数据库·sql
麦聪聊数据15 小时前
如何用 B/S 架构解决混合云环境下的数据库连接碎片化难题?
运维·数据库·sql·安全·架构