Appearance
查看生成的 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 |
| 审计所有查询 | 拦截器 |
核心要点
- 开发阶段: 始终开启 SQL 日志
- 性能优化: 重点关注慢查询和 N+1 问题
- 生产环境: 谨慎记录,避免性能影响
- 定期审查: 建立查询审查流程
- 学习工具: 通过 SQL 输出理解 EF Core 翻译机制