Skip to content

查看生成的 SQL ​

概述 ​

EF Core 将 LINQ 查询转换为 SQL 语句发送到数据库。查看和分析这些生成的 SQL 是性能优化和问题排查的关键技能。

为什么需要查看 SQL? ​

场景说明
性能优化识别 N+1 查询、全表扫描等问题
正确性验证确保查询逻辑符合预期
调试问题定位参数类型不匹配、索引未使用等问题
学习工具理解 EF Core 如何翻译 LINQ
代码审查验证查询效率,避免潜在问题

方法一: 日志记录(LogTo) ​

基础配置 ​

csharp
// Program.cs
using Microsoft.Extensions.Logging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString);
    
    // ✅ 启用日志记录
    options.LogTo(Console.WriteLine, LogLevel.Information);
});

var app = builder.Build();

过滤特定类别 ​

csharp
// 仅记录 SQL 命令
options.LogTo(
    Console.WriteLine,
    new[] { DbLoggerCategory.Database.Command.Name },  // 仅 SQL 命令
    LogLevel.Information);

// 记录查询和命令
options.LogTo(
    Console.WriteLine,
    new[] 
    { 
        DbLoggerCategory.Database.Command.Name,
        DbLoggerCategory.Query.Name 
    },
    LogLevel.Information);

输出到文件 ​

csharp
// 输出到文件
var logFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ef-sql.log");

options.LogTo(
    message => File.AppendAllText(logFilePath, message + Environment.NewLine),
    LogLevel.Information);

示例输出 ​

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (12ms) [Parameters=[@__price_0='?' (DbType = Decimal)], CommandType='Text', CommandTimeout='30']
      SELECT [p].[Id], [p].[Name], [p].[Price]
      FROM [Products] AS [p]
      WHERE [p].[Price] > @__price_0

方法二: ToQueryString ​

基础用法 ​

csharp
// 直接查看生成的 SQL
var query = context.Products
    .Where(p => p.Price > 100)
    .OrderBy(p => p.Name);

string sql = query.ToQueryString();

Console.WriteLine(sql);
/*
输出:
SELECT [p].[Id], [p].[Name], [p].[Price], [p].[CategoryId]
FROM [Products] AS [p]
WHERE [p].[Price] > 100.0
ORDER BY [p].[Name]
*/

复杂查询 ​

csharp
// 包含 Include 的查询
var query = context.Orders
    .Include(o => o.Customer)
    .Include(o => o.OrderItems)
        .ThenInclude(oi => oi.Product)
    .Where(o => o.OrderDate >= DateTime.UtcNow.AddDays(-30))
    .OrderByDescending(o => o.TotalAmount);

string sql = query.ToQueryString();
Console.WriteLine(sql);

聚合查询 ​

csharp
var query = context.Products
    .GroupBy(p => p.CategoryId)
    .Select(g => new 
    { 
        CategoryId = g.Key,
        Count = g.Count(),
        AvgPrice = g.Average(p => p.Price)
    });

string sql = query.ToQueryString();
Console.WriteLine(sql);
/*
输出:
SELECT [p].[CategoryId] AS [CategoryId], 
       COUNT(*) AS [Count], 
       AVG([p].[Price]) AS [AvgPrice]
FROM [Products] AS [p]
GROUP BY [p].[CategoryId]
*/

限制 ​

csharp
// ⚠️ ToQueryString() 不支持某些操作

// ❌ 不支持 FindAsync
var product = await context.Products.FindAsync(1);  // 不能使用 ToQueryString

// ❌ 不支持 FromSqlRaw
var products = context.Products.FromSqlRaw("EXEC GetProducts");  // 不能链式调用

// ✅ 适用于 LINQ 查询
var products = context.Products.Where(p => p.Price > 100);
string sql = products.ToQueryString();  // ✅ 可以

方法三: SQL Server Profiler ​

使用 SQL Server Management Studio ​

步骤:
1. 打开 SSMS
2. 工具 -> SQL Server Profiler
3. 连接到数据库
4. 选择模板: "Standard" 或 "TSQL"
5. 运行追踪
6. 执行应用操作
7. 查看捕获的 SQL 语句

使用 Azure Data Studio ​

步骤:
1. 安装 "Profiler" 扩展
2. 连接到数据库
3. 点击 "New Profiler Session"
4. 执行应用操作
5. 实时查看 SQL 语句

XEvent Profiler(轻量级) ​

sql
-- 创建扩展事件会话
CREATE EVENT SESSION [EFCore_Tracing] ON SERVER
ADD EVENT sqlserver.rpc_completed(
    ACTION(sqlserver.client_app_name, sqlserver.database_name)
    WHERE ([sqlserver].[like_i_sql_unicode_string]([sqlserver].[client_app_name], N'%EFCore%'))
),
ADD EVENT sqlserver.sql_batch_completed(
    ACTION(sqlserver.client_app_name, sqlserver.database_name)
    WHERE ([sqlserver].[like_i_sql_unicode_string]([sqlserver].[client_app_name], N'%EFCore%'))
)
ADD TARGET package0.ring_buffer
WITH (MAX_MEMORY = 4096 KB, EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS);

-- 启动会话
ALTER EVENT SESSION [EFCore_Tracing] ON SERVER STATE = START;

-- 停止会话
ALTER EVENT SESSION [EFCore_Tracing] ON SERVER STATE = STOP;

-- 删除会话
DROP EVENT SESSION [EFCore_Tracing] ON SERVER;

方法四: MiniProfiler ​

安装 ​

bash
dotnet add package MiniProfiler.EntityFrameworkCore
dotnet add package MiniProfiler.AspNetCore.Mvc

配置 ​

csharp
// Program.cs
using StackExchange.Profiling;

var builder = WebApplication.CreateBuilder(args);

// 添加 MiniProfiler
builder.Services.AddMiniProfiler(options =>
{
    options.RouteBasePath = "/profiler";
    options.ColorScheme = ColorScheme.Dark;
    options.PopupRenderPosition = RenderPosition.Right;
    options.ShowControls = true;
})
.AddEntityFramework();  // 集成 EF Core

var app = builder.Build();

// 使用中间件
app.UseMiniProfiler();

使用 ​

访问应用时:
1. 页面右上角会出现 MiniProfiler 浮窗
2. 显示请求耗时和 SQL 数量
3. 点击查看详情:
   - 每个 SQL 语句
   - 执行时间
   - 参数值
   - 调用堆栈

自定义监控 ​

csharp
public class ProductService
{
    private readonly AppDbContext _context;

    public async Task<List<Product>> GetExpensiveProducts()
    {
        using (MiniProfiler.Current.Step("GetExpensiveProducts"))
        {
            return await _context.Products
                .Where(p => p.Price > 1000)
                .ToListAsync();
        }
    }
}

方法五: 拦截器 ​

创建 SQL 拦截器 ​

csharp
// SqlInterceptor.cs
using Microsoft.EntityFrameworkCore.Diagnostics;

public class SqlInterceptor : DbCommandInterceptor
{
    private readonly ILogger<SqlInterceptor> _logger;

    public SqlInterceptor(ILogger<SqlInterceptor> logger)
    {
        _logger = logger;
    }

    public override InterceptionResult<int> NonQueryExecuting(
        DbCommand command,
        CommandEventData eventData,
        InterceptionResult<int> result)
    {
        LogCommand(command, "NonQueryExecuting");
        return base.NonQueryExecuting(command, eventData, result);
    }

    public override InterceptionResult<object> ScalarExecuting(
        DbCommand command,
        CommandEventData eventData,
        InterceptionResult<object> result)
    {
        LogCommand(command, "ScalarExecuting");
        return base.ScalarExecuting(command, eventData, result);
    }

    public override ValueTask<InterceptionResult<object>> ReaderExecutingAsync(
        DbCommand command,
        CommandEventData eventData,
        InterceptionResult<object> result,
        CancellationToken cancellationToken = default)
    {
        LogCommand(command, "ReaderExecutingAsync");
        return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
    }

    private void LogCommand(DbCommand command, string eventType)
    {
        var parameters = string.Join(", ", 
            command.Parameters.Cast<DbParameter>()
                .Select(p => $"{p.ParameterName} = {p.Value}"));
        
        _logger.LogInformation("""
            [{EventType}] 
            Command: {CommandText}
            Parameters: {Parameters}
            Timeout: {Timeout}s
            """,
            eventType,
            command.CommandText,
            parameters,
            command.CommandTimeout);
    }
}

注册拦截器 ​

csharp
// Program.cs
builder.Services.AddSingleton<SqlInterceptor>();

builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
    options.UseSqlServer(connectionString);
    options.AddInterceptors(sp.GetRequiredService<SqlInterceptor>());
});

示例输出 ​

info: SqlInterceptor[0]
      [ReaderExecutingAsync] 
      Command: SELECT [p].[Id], [p].[Name], [p].[Price]
               FROM [Products] AS [p]
               WHERE [p].[Price] > @__price_0
      Parameters: @__price_0 = 100
      Timeout: 30s

实际案例分析 ​

案例 1: N+1 查询问题 ​

csharp
// ❌ 错误的代码
var orders = await context.Orders.ToListAsync();

foreach (var order in orders)
{
    // ⚠️ 每次循环都执行一次查询 (N+1 问题)
    order.Customer = await context.Customers
        .FindAsync(order.CustomerId);
}

// 生成的 SQL (假设 100 个订单):
// SELECT * FROM Orders                    -- 1 次
// SELECT * FROM Customers WHERE Id = @p1  -- 执行 100 次!
// SELECT * FROM Customers WHERE Id = @p2
// ...

// ✅ 修复: 使用 Include
var orders = await context.Orders
    .Include(o => o.Customer)
    .ToListAsync();

// 生成的 SQL:
// SELECT [o].[Id], [o].[CustomerId], [c].[Id], [c].[Name]
// FROM [Orders] AS [o]
// INNER JOIN [Customers] AS [c] ON [o].[CustomerId] = [c].[Id]

案例 2: 客户端评估警告 ​

csharp
// ⚠️ EF Core 8+ 会警告
var query = context.Products
    .Where(p => IsExpensive(p.Price));  // ⚠️ 无法转换为 SQL

bool IsExpensive(decimal price)
{
    return price > 1000;
}

// 日志输出:
// warn: Microsoft.EntityFrameworkCore.Query[20503]
//       The LINQ expression 'where IsExpensive([p].Price)' could not be translated.
//       Additional information: Translation of method 'Program.IsExpensive' failed.
//       Either rewrite the query in a form that can be translated, or use 'AsEnumerable()'
//       to explicitly evaluate on client.

// ✅ 修复: 使用可翻译的表达式
var query = context.Products
    .Where(p => p.Price > 1000);  // ✅ 可以转换为 SQL

案例 3: 参数嗅探问题 ​

csharp
// 问题: 相同的查询,不同参数导致性能差异
var searchTerm = "Laptop";

var products = await context.Products
    .Where(p => p.Name.Contains(searchTerm))  // ⚠️ 生成 LIKE '%Laptop%'
    .ToListAsync();

// 生成的 SQL:
// EXEC sp_executesql N'SELECT [p].[Id], [p].[Name] 
//                      FROM [Products] AS [p] 
//                      WHERE [p].[Name] LIKE @__searchTerm_0',
//                    N'@__searchTerm_0 nvarchar(4000)',
//                    @__searchTerm_0=N'%Laptop%'

// 性能问题:
// - LIKE '%xxx%' 无法使用索引
// - 大数据量时全表扫描

// ✅ 修复: 使用全文索引或调整查询
var products = await context.Products
    .Where(p => EF.Functions.FreeText(p.Name, searchTerm))  // ✅ 使用全文索引
    .ToListAsync();

案例 4: 隐式类型转换 ​

csharp
// ⚠️ 类型不匹配导致索引失效
long customerId = 123;

var order = await context.Orders
    .FirstOrDefaultAsync(o => o.CustomerId == customerId);

// CustomerId 在数据库中是 int,但 C# 中是 long
// 生成的 SQL:
// WHERE [o].[CustomerId] = @__customerId_0  -- 参数类型: bigint

// 问题: SQL Server 需要将 int 转换为 bigint,可能导致索引失效

// ✅ 修复: 使用正确的类型
int customerId = 123;  // 与数据库类型匹配

var order = await context.Orders
    .FirstOrDefaultAsync(o => o.CustomerId == customerId);

// 生成的 SQL:
// WHERE [o].[CustomerId] = @__customerId_0  -- 参数类型: int ✅

高级技巧 ​

格式化 SQL 输出 ​

csharp
// 美化 SQL 输出
public static class QueryExtensions
{
    public static string ToFormattedQueryString(this IQueryable query)
    {
        var sql = query.ToQueryString();
        
        // 简单格式化(可以使用专业的 SQL 格式化库)
        return sql
            .Replace("SELECT", "\nSELECT")
            .Replace("FROM", "\nFROM")
            .Replace("WHERE", "\nWHERE")
            .Replace("ORDER BY", "\nORDER BY")
            .Replace("GROUP BY", "\nGROUP BY")
            .Replace("INNER JOIN", "\nINNER JOIN")
            .Replace("LEFT JOIN", "\nLEFT JOIN")
            .Trim();
    }
}

// 使用
var query = context.Products.Where(p => p.Price > 100);
Console.WriteLine(query.ToFormattedQueryString());
/*
输出:
SELECT [p].[Id], [p].[Name], [p].[Price]
FROM [Products] AS [p]
WHERE [p].[Price] > 100.0
*/

记录慢查询 ​

csharp
public class SlowQueryInterceptor : DbCommandInterceptor
{
    private readonly ILogger<SlowQueryInterceptor> _logger;
    private readonly TimeSpan _threshold = TimeSpan.FromSeconds(1);

    public SlowQueryInterceptor(ILogger<SlowQueryInterceptor> logger)
    {
        _logger = logger;
    }

    public override async ValueTask<InterceptionResult<object>> ReaderExecutingAsync(
        DbCommand command,
        CommandEventData eventData,
        InterceptionResult<object> result,
        CancellationToken cancellationToken = default)
    {
        var stopwatch = Stopwatch.StartNew();
        
        var interceptorResult = await base.ReaderExecutingAsync(
            command, eventData, result, cancellationToken);
        
        stopwatch.Stop();
        
        if (stopwatch.Elapsed > _threshold)
        {
            _logger.LogWarning("""
                SLOW QUERY DETECTED ({Duration}ms)
                SQL: {Sql}
                Parameters: {Parameters}
                """,
                stopwatch.ElapsedMilliseconds,
                command.CommandText,
                string.Join(", ", command.Parameters.Cast<DbParameter>()
                    .Select(p => $"{p.ParameterName}={p.Value}")));
        }
        
        return interceptorResult;
    }
}

比较不同 LINQ 生成的 SQL ​

csharp
// 方式 1: 使用 Where + FirstOrDefault
var query1 = context.Products
    .Where(p => p.Price > 100)
    .OrderBy(p => p.Name)
    .FirstOrDefault();

Console.WriteLine(query1.ToQueryString());

// 方式 2: 直接使用 FirstOrDefault
var query2 = context.Products
    .Where(p => p.Price > 100)
    .OrderBy(p => p.Name)
    .Take(1)
    .FirstOrDefault();

Console.WriteLine(query2.ToQueryString());

// 对比两种方式的 SQL,选择更优的

性能分析清单 ​

检查项 ​

  • [ ] 查询计划: 是否使用了预期的索引?
  • [ ] JOIN 数量: 是否有多余的 JOIN?
  • [ ] SELECT 列: 是否选择了不必要的列?
  • [ ] WHERE 条件: 是否正确过滤?
  • [ ] 参数化: 查询是否参数化(避免 SQL 注入)?
  • [ ] N+1 问题: 是否有循环查询?
  • [ ] 客户端评估: 是否有查询在客户端执行?
  • [ ] 超时设置: CommandTimeout 是否合理?

常见警告信号 ​

信号可能的问题解决方案
LIKE '%xxx%'无法使用索引使用前缀搜索或全文索引
多个小查询N+1 问题使用 Include 或批量加载
SELECT *选择了不必要的列使用投影查询
客户端方法调用无法翻译重写为可翻译的 LINQ
嵌套子查询性能差重构为 JOIN 或拆分查询

最佳实践 ​

✅ 推荐做法 ​

1. 开发环境始终记录 SQL ​

csharp
if (builder.Environment.IsDevelopment())
{
    options.LogTo(Console.WriteLine, LogLevel.Information);
    options.EnableSensitiveDataLogging();  // 显示参数值
}

2. 生产环境使用采样 ​

csharp
// 仅记录 1% 的查询,避免性能影响
var random = new Random();

options.LogTo(
    message =>
    {
        if (random.Next(100) < 1)  // 1% 采样
        {
            _logger.LogInformation(message);
        }
    },
    LogLevel.Information);

3. 定期审查慢查询 ​

csharp
// 每周运行慢查询分析
public class WeeklyQueryReview : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // 等待到下周一
            await WaitForNextMonday(stoppingToken);
            
            // 分析慢查询日志
            var slowQueries = AnalyzeSlowQueries();
            
            // 发送报告
            await SendReport(slowQueries);
        }
    }
}

❌ 避免的错误 ​

1. 生产环境启用敏感日志 ​

csharp
// ❌ 错误: 泄漏敏感数据
options.EnableSensitiveDataLogging();  // 在生产环境!

// ✅ 正确: 仅开发环境
if (builder.Environment.IsDevelopment())
{
    options.EnableSensitiveDataLogging();
}

2. 忘记关闭详细日志 ​

csharp
// ❌ 错误: 所有环境都记录
options.LogTo(Console.WriteLine, LogLevel.Debug);

// ✅ 正确: 根据配置
if (configuration.GetValue<bool>("Logging:EnableDetailedSql"))
{
    options.LogTo(Console.WriteLine, LogLevel.Information);
}

总结 ​

工具选择指南 ​

场景推荐工具
快速查看单个查询ToQueryString()
开发时实时监控LogTo
生产环境问题排查拦截器 + 日志
性能分析MiniProfiler
数据库端监控SQL Profiler
审计所有查询拦截器

核心要点 ​

  1. 开发阶段: 始终开启 SQL 日志
  2. 性能优化: 重点关注慢查询和 N+1 问题
  3. 生产环境: 谨慎记录,避免性能影响
  4. 定期审查: 建立查询审查流程
  5. 学习工具: 通过 SQL 输出理解 EF Core 翻译机制

基于 MIT 许可发布