很多时候我们希望看到 LINQ 最终生成的 SQL 语句,拿到 SQL Server 中执行,用执行计划工具分析性能瓶颈。
方案一:VS 插件(Linq to SQL Debug Visualizer)
注意:这个插件是十年前写的,目前已无人维护,最新 VS(如 VS2015)未测试是否可用。
- 下载地址:https://weblogs.asp.net/scottgu/linq-to-sql-debug-visualizer
- 安装方式:解压后找到
bin/debug/SqlServerQueryVisualizer.dll,复制到:
D:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Packages\DebuggerCode language: CSS (css)
调试时 hover 在 IQueryable 变量上即可查看生成的 SQL。
方案二:db.Log 输出(适合控制台/窗体程序)
using (var db = new DataClasses1DataContext())
{
db.Log = Console.Out; // 直接输出到控制台
var q = from s in db.Student
where s.age > 12
select s;
var l = q.ToList();
}Code language: JavaScript (javascript)
局限:
Console.Out只适合控制台和窗体程序,不适合 ASP.NET Web 程序。
改进:输出到文件
using (var db = new DataClasses1DataContext())
{
StreamWriter sw = new StreamWriter(
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "log.txt"));
db.Log = sw;
var q = from s in db.Student
where s.age > 12
select s;
var l = q.ToList();
sw.Flush();
sw.Close();
}Code language: JavaScript (javascript)
问题:
db.Log输出的 SQL 带参数占位符(@p0),不能直接在 SQL Server 中执行。
方案三:GetCommand 生成可执行的 SQL 文件(改进版)
将参数声明为 DECLARE 变量,拼成可直接执行的 .sql 文件:
StreamWriter sw = new StreamWriter(
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "log.sql"));
DbCommand com = _db.GetCommand(t); // t 是 IQueryable 对象
foreach (DbParameter item in com.Parameters)
{
sw.WriteLine(string.Format(
"declare {0} {1} set {0} = {2}",
item.ParameterName,
item.DbType == DbType.Int32
? "int"
: (new DbType[] { DbType.AnsiString, DbType.Guid }.Contains(item.DbType)
? "varchar(100)"
: item.DbType.ToString()),
new DbType[] { DbType.Int32, DbType.Decimal }.Contains(item.DbType)
? item.Value.ToString()
: "'" + item.Value + "'"
));
}
sw.Write(com.CommandText);
sw.Flush();
sw.Close();Code language: PHP (php)
生成的文件内容类似:
declare @p0 int set @p0 = 12
SELECT [t0].[ID], [t0].[Name], [t0].[age]
FROM [Student] AS [t0]
WHERE [t0].[age] > @p0Code language: PHP (php)
可以直接在 SQL Server Management Studio 中执行。
方案四:直接替换参数
将参数占位符直接替换为实际值,生成纯 SQL,方便选取某一段单独执行:
StreamWriter sw = new StreamWriter(
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "log.sql"));
DbCommand com = _db.GetCommand(t); // t 是 IQueryable 对象
var sql = com.CommandText;
var noQuote = new DbType[] { DbType.Int32, DbType.Decimal }; // 不带单引号的类型
// 从后往前替换,避免 @p0 和 @p0_1 互相干扰
for (int i = com.Parameters.Count - 1; i >= 0; i--)
{
var item = com.Parameters[i];
sql = sql.Replace(
item.ParameterName,
noQuote.Contains(item.DbType)
? item.Value.ToString() + " "
: "'" + item.Value + "'"
);
}
sw.Write(sql);
sw.Flush();
sw.Close();Code language: JavaScript (javascript)
生成的 SQL:
SELECT [t0].[ID], [t0].[Name], [t0].[age]
FROM [Student] AS [t0]
WHERE [t0].[age] > 12Code language: CSS (css)
优点: 可以直接复制某一段 SQL 到 SQL Server 中执行,配合执行计划工具分析哪些地方耗时。
用 SQL Server 执行计划分析性能
拿到可执行的 SQL 后,在 SQL Server Management Studio 中:
- 点击 “包括实际的执行计划”(Ctrl + M)
- 执行 SQL
- 查看执行计划面板,找出:
- 表扫描(Table Scan) → 缺少索引
- 键查找(Key Lookup) → 需要覆盖索引
- 高开销操作 → 优化方向
给表连接字段增加索引
执行计划中发现表扫描时,给连接字段加索引:
-- 给外键字段加索引(最常见)
CREATE INDEX IX_Student_ClassID ON Student(ClassID);
-- 给经常作为条件的字段加索引
CREATE INDEX IX_Student_Name ON Student(Name);
-- 复合索引(多条件查询)
CREATE INDEX IX_Student_ClassID_Name ON Student(ClassID, Name);
| 场景 | 建议 |
|---|---|
JOIN 字段 | 必须加索引 |
WHERE 频繁过滤的字段 | 加索引 |
ORDER BY 字段 | 加索引可避免排序 |
| 低区分度字段(如 gender) | 不建议单独加索引 |
四种方案对比
| 方案 | 适用场景 | 能否直接执行 | 维护状态 |
|---|---|---|---|
| VS 插件 Debug Visualizer | 调试时快速查看 | 需手动复制 | 已停止维护 |
db.Log = Console.Out | 控制台/窗体 | 带参数不能直接执行 | 可用 |
db.Log 写文件 | 所有程序 | 带参数不能直接执行 | 可用 |
GetCommand + 参数声明 | 所有程序 | 可直接执行 | 可用 |
GetCommand + 直接替换值 | 所有程序 | 可直接执行,最方便 | 推荐 |
Previous: 表关联使用拼接字符串的