我的 C# 查询代码明明只写了一遍,数据库却被查了好几次、结果还前后对不上,我对着 LINQ 的延迟执行排查了大半天的复盘

在一个 .NET 后台服务里我写了段统计代码:从数据库查出一批订单,然后统计总数、求和金额、再取前几条,一个 LINQ 查询"复用"三次,自觉很 DRY。上线后却两个怪事:数据库监控显示那条查询每次执行竟被打了三遍;统计的总数和求和明细条数偶尔对不上。盯着这段"明明只写了一次查询"的代码百思不得其解,排查大半天才撞上 LINQ 最经典也最阴险的特性——延迟执行(deferred execution)。原来 var query = Where().Select() 这行只是构建了"查询的配方"、并没真正执行;后面的 Count()/Sum()/ToList() 这些终结操作,每一次都会把整个查询从头重跑一遍(对 EF 就是重发一次 SQL),我做了三次终结操作 DB 就被查了三遍;而三次发生在不同时刻,期间数据变了结果自然对不上。这篇从延迟执行原理、该物化时 ToList/分清 IQueryable 与 IEnumerable/别在循环重复枚举的正解、延迟+闭包捕获的另一个坑、操作符执行时机速查、延迟 vs 立即取舍、决策图与铁律,到附上一段在谓词里埋打印、亲眼看见延迟执行的实验代码。核心领悟:简洁的语法常掩盖背后不简单的机制,对自动/隐式/惰性/智能的特性要追问它背后到底做了什么、何时做、做几次;理解黑盒最好的方法是写个小实验让它显形。

我的 C# 查询代码明明只写了一遍,数据库却被查了好几次、结果还前后对不上,我对着 LINQ 的延迟执行排查了大半天的复盘

那是我在一个 .NET 后台服务里写的一段数据统计代码。逻辑很简单:从数据库查出一批符合条件的订单,然后统计总数、求和金额、再取前几条展示。我自觉写得清清爽爽,一个 LINQ 查询复用三次,DRY 得很。可上线后两个问题让我懵了:一是数据库监控显示,这段代码每次执行,那条查询居然被打了三遍;二是更诡异的——统计出来的"总数"和"求和用的明细条数",偶尔对不上。我盯着那段"明明只写了一次查询"的代码,百思不得其解。排查了大半天,才终于撞上了 LINQ 那个最经典、也最阴险的特性:延迟执行(deferred execution)。这篇就把这场"查询被偷偷执行多次"的事故,从头复盘一遍。

故障现场:写一次的查询,跑了三次

先看现场。问题就藏在这段看似无害、还挺"优雅"的统计代码里:

// 我的统计代码: 一个查询, "复用"三次 —— 看起来很 DRY
public OrderStats GetStats(DateTime since)
{
    // query 是一个 IEnumerable, 我以为它"就是查出来的结果"
    var query = _dbContext.Orders
        .Where(o => o.CreatedAt >= since && o.Status == "paid")
        .Select(o => new Order { Id = o.Id, Amount = o.Amount });
        // ↑ 我以为执行到这, 数据库已经查完、结果存在 query 里了

    // 然后我"复用" query 三次:
    int count = query.Count();                 // ← 第1次遍历
    decimal total = query.Sum(o => o.Amount);   // ← 第2次遍历
    var top5 = query.Take(5).ToList();          // ← 第3次遍历

    return new OrderStats { Count = count, Total = total, Top5 = top5 };
}

// 数据库日志(一次 GetStats 调用):
//   SELECT ... FROM Orders WHERE CreatedAt >= @p AND Status = 'paid'   -- Count
//   SELECT ... FROM Orders WHERE CreatedAt >= @p AND Status = 'paid'   -- Sum
//   SELECT ... FROM Orders WHERE CreatedAt >= @p AND Status = 'paid'   -- Take
//   ↑ 同一个 query, 数据库被查了 3 遍!

// 现象拼图:
//   - 我以为: var query = ... 这行就把数据查出来了, 存进了 query。
//   - 实际上: LINQ 是"延迟执行"的! query 只是一个"查询的描述/配方",
//             并没有真正执行。
//   - 每次对 query 做 Count()/Sum()/ToList() 这种"终结操作", 才会
//     真正去执行一次查询(对 EF 来说就是发一次 SQL)。
//   - 我"复用"了 3 次 query → 数据库被查了 3 次。
//   - 更坑: 如果这3次查询之间, 数据库的数据变了(别的事务插入/更新了订单),
//           那 Count 和 Sum 看到的就是"不同时刻的数据" → 结果对不上!

看到数据库日志里同一条 SQL 整整齐齐打了三遍时,我才恍然大悟。我一直以为 var query = _dbContext.Orders.Where(...).Select(...) 这行执行完,数据就已经查出来、装进 query 变量里了。可事实是:LINQ 是"延迟执行"的——query 此刻只是一个"查询的描述"(一份配方),并没有真正去查真正触发执行的,是后面的 Count()Sum()ToList() 这些"终结操作"——我对 query 做了三次终结操作,数据库就被实打实地查了三遍。而那个"结果对不上"的诡异问题也有了答案:三次查询发生在不同的时刻,如果期间有别的事务改动了订单数据,那 CountSum 看到的就是"不同时刻的数据快照",自然对不上

第一件事:搞懂什么是 LINQ 的延迟执行

要解决它,得先真正理解 LINQ 的延迟执行(deferred execution)到底是怎么回事。

LINQ 延迟执行(deferred execution)详解

# 核心概念: LINQ 查询分两个阶段
#   阶段1: 定义查询(构建"配方")—— 不执行!
#     var query = list.Where(x => x > 5).Select(x => x * 2);
#     这行只是"描述了要做什么", 一行数据都没处理。
#   阶段2: 执行查询(枚举"配方")—— 此时才真正干活
#     当你"遍历" query 时, 才会真正执行。

# 哪些操作会"触发执行"(终结操作 / immediate)?
#   - foreach 遍历
#   - ToList() / ToArray() / ToDictionary()  (物化成集合)
#   - Count() / Sum() / Average() / Min() / Max()  (聚合)
#   - First() / Single() / Any() / All()  (取值/判断)
#   → 这些会"立即"把查询跑一遍, 得到结果。

# 哪些操作是"延迟的"(deferred, 只是接着构建配方)?
#   - Where / Select / OrderBy / Take / Skip / GroupBy / Join ...
#   → 这些返回的还是 IEnumerable/IQueryable, 只是在配方上"再加一步",
#     并不执行。

# 关键后果:
#   1. 每次"触发执行", 都会把整个查询【从头重新跑一遍】。
#      → 对 IEnumerable(内存): 重新遍历+重新计算。
#      → 对 IQueryable(EF/数据库): 重新发一次 SQL!
#   2. 查询用到的"外部变量", 是在【执行时】才求值的(闭包捕获),
#      不是定义时。变量后来变了, 结果也跟着变。

# 为什么这么设计? —— 好处是"组合"和"按需"
#   - 可以一步步组合查询条件, 最后才执行(灵活)。
#   - 不取就不算(惰性), 配合 Take 等能避免查全部(高效)。
#   - 但代价: 不理解它, 就会"无意中重复执行" 或 "结果不符预期"。

# 核心: LINQ 查询是"延迟执行"的 —— 定义时只构建配方, 遍历/聚合/ToList
#   等终结操作才真正执行, 且每次终结都从头重跑一遍(IQueryable 会重发SQL)。

原来,LINQ 的查询执行,是个"两阶段"的过程。阶段一:定义查询——var query = list.Where(...).Select(...) 这样的代码,只是"描述了要做什么"(构建一份配方),一行数据都没处理阶段二:执行查询——只有当你遍历它时,才真正干活。关键在于分清两类操作:终结操作(foreachToListCountSumFirstAny 等)会"立即"把查询跑一遍;而延迟操作(WhereSelectOrderByTake 等)只是在配方上"再加一步",并不执行由此带来两个关键后果:一是每次触发执行,都会把整个查询从头重跑一遍(对内存集合是重新遍历计算,对 EF 是重新发一次 SQL!);二是查询用到的外部变量,是在执行时才求值的(闭包捕获)这套设计本是为了"组合的灵活"和"按需的惰性",但代价是:不理解它,就会像我一样"无意中重复执行"或得到"不符预期的结果"

第二件事:正解——该物化时就物化,理解执行时机

搞懂了原理,正解就清晰了:需要多次使用结果时,用 ToList() 一次性物化;真正需要惰性/流式时才保留延迟;分清 IEnumerable 与 IQueryable

// ====== 正解一: 要多次用结果, 就先 ToList() 物化一次 ======
public OrderStats GetStats(DateTime since)
{
    // ✓ ToList() 立即执行查询一次, 把结果装进内存 List
    var orders = _dbContext.Orders
        .Where(o => o.CreatedAt >= since && o.Status == "paid")
        .Select(o => new Order { Id = o.Id, Amount = o.Amount })
        .ToList();   // ← 关键! 数据库只查这一次, 结果固定下来

    // 后面都是对"内存里的 List"操作, 不再碰数据库:
    int count = orders.Count;                  // List.Count, O(1), 不查库
    decimal total = orders.Sum(o => o.Amount);  // 内存求和, 不查库
    var top5 = orders.Take(5).ToList();         // 内存取前5, 不查库

    return new OrderStats { Count = count, Total = total, Top5 = top5 };
    // → 数据库只查 1 次; 三个统计基于"同一份数据快照", 结果一致。
}

// ====== 正解二: 真需要惰性/流式时, 才保留延迟(别盲目都 ToList)======
// 如果数据量巨大、只想边读边处理、不想全部加载进内存:
foreach (var item in hugeQuery)   // 延迟执行: 一条条流式拉取, 内存友好
{
    Process(item);
}
// → 这种"只遍历一次 + 大数据量"的场景, 延迟执行(不 ToList)反而是对的。

// ====== 正解三: 分清 IEnumerable vs IQueryable ======
// IQueryable (EF, query 还没离开数据库):
//   .Where(...) 会被翻译成 SQL 的 WHERE, 在【数据库端】过滤(高效)。
IQueryable<Order> q1 = _dbContext.Orders.Where(o => o.Amount > 100);
//   → SELECT ... WHERE Amount > 100  (数据库只返回符合的)

// IEnumerable (已经 ToList/AsEnumerable, 数据在内存):
//   .Where(...) 在【内存端】用 LINQ-to-Objects 过滤。
IEnumerable<Order> q2 = _dbContext.Orders.ToList().Where(o => o.Amount > 100);
//   → 先 SELECT * 全表拉进内存(!), 再在内存过滤 → 浪费!
// ★ 坑: 过早 ToList()/AsEnumerable() 会让后续过滤在内存做, 把全表拉回来。
//   原则: 让数据库能做的过滤/排序/分页, 尽量在 IQueryable 阶段做完, 最后才物化。

// ====== 正解四: 警惕"在循环里枚举延迟查询" ======
foreach (var id in ids)
    if (query.Any(o => o.Id == id))   // ✗ 每次循环都重新执行 query!
        ...
// → 应先 var list = query.ToList(); 再在 list 上 Any。

// 核心: 多次用结果就 ToList 物化一次(只查一遍、快照一致); 大数据流式才保留延迟;
//   让过滤排序分页在 IQueryable(数据库端)做完再物化; 别在循环里重复枚举延迟查询。

修复的核心,是"理解执行时机,在'该物化'和'该惰性'之间做对选择"正解一:要多次用结果,就先 ToList() 物化一次——它立即执行查询一次、把结果装进内存 List,后续的 Count/Sum/Take 全在内存上操作,数据库只查一次、三个统计基于同一份快照、结果一致。这就是我这次该用的解法。正解二:真需要惰性/流式时才保留延迟——数据量巨大、只想边读边处理时,foreach 延迟执行反而内存友好(别盲目都 ToList)。正解三:分清 IQueryableIEnumerable——前者(EF)的 Where 被翻译成 SQL 在数据库端过滤;后者(已物化)的 Where内存端过滤。过早 ToList 会把全表拉回内存再过滤,极其浪费;原则是让过滤/排序/分页尽量在 IQueryable 阶段(数据库端)做完,最后才物化正解四:警惕在循环里枚举延迟查询(每次循环都重新执行)。归根结底:多次用就 ToList 物化一次;大数据流式才保留延迟;过滤排序分页在数据库端做完再物化;别在循环里重复枚举。

第三件事:延迟执行的另一个坑——闭包捕获

排查时我还顺带发现,延迟执行还和"闭包捕获变量"勾连在一起,藏着另一个经典坑。

// 延迟执行 + 闭包捕获: 查询用的"外部变量", 是【执行时】才求值的

// ✗ 坑1: 在循环里构建查询, 捕获了循环变量
var queries = new List<IEnumerable<int>>();
for (int i = 0; i < 3; i++)
{
    queries.Add(numbers.Where(n => n > i));   // 捕获的是变量 i 本身
}
// 等到后面真正枚举这些 query 时, 循环早已结束, i = 3
// → 三个 query 用的都是 i=3! (而不是预期的 0,1,2)
//   (C# 5+ 的 foreach 变量已修复此问题, 但 for 循环变量仍有此坑)

// ✓ 修法: 用局部变量"快照"当前值
for (int i = 0; i < 3; i++)
{
    int local = i;                            // 每次循环新建一个局部变量
    queries.Add(numbers.Where(n => n > local));// 捕获各自的 local
}

// ✗ 坑2: 定义查询后, 改了外部变量, 查询结果跟着变
int threshold = 10;
var q = numbers.Where(n => n > threshold);     // 还没执行
threshold = 100;                              // 改了阈值
var result = q.ToList();                       // 此刻才执行 → 用的是 100!
// → result 是 n > 100 的, 不是 n > 10 的(很多人以为是 10)。

// ✓ 修法: 要么定义后立刻 ToList, 要么别在执行前改捕获的变量。

// 核心: 延迟查询捕获的外部变量在"执行时"才求值, 不是定义时;
//   循环里构建查询要用局部变量快照, 定义后改变量会改变查询结果。

这个发现让我对延迟执行的理解又深了一层:它不只关乎"何时查",还关乎"用的是什么时候的变量值"。坑一:在循环里构建查询、捕获了循环变量——延迟查询捕获的是变量本身,等到后面真正枚举时,循环早已结束,所有 query 用的都是循环结束后的最终值(比如 for 里的 i=3)。修法是用局部变量快照当前值(int local = i)。坑二:定义查询后改了外部变量,查询结果跟着变——var q = numbers.Where(n => n > threshold) 定义后改了 threshold,等 ToList 执行时用的是改后的新值。它们共同的根源是:延迟查询捕获的外部变量,是在"执行时"才求值的,不是"定义时"归根结底:循环里构建查询要用局部变量快照;定义后别在执行前改捕获的变量,否则会悄悄改变查询结果。下面这张图,是这次延迟执行导致重复查询的成因与解法:

第四件事:LINQ 操作符执行时机速查

这次踩坑后,我把常用 LINQ 操作符按"延迟"还是"立即执行"分了类,写代码时心里就有数了。

类别 操作符 说明
延迟(构建配方) Where / Select / OrderBy 只加一步,不执行
延迟(构建配方) Take / Skip / GroupBy / Join 返回 IEnumerable,继续延迟
立即(触发执行) ToList / ToArray / ToDictionary 物化成集合,执行一次
立即(触发执行) Count / Sum / Average / Min / Max 聚合,遍历一次
立即(触发执行) First / Single / Last / ElementAt 取单个元素,执行
立即(触发执行) Any / All / Contains 判断,执行
立即遍历 foreach 每次 foreach 都执行一遍

这张表,把"哪些会执行、哪些不会"彻底分清了。记忆的诀窍是:返回"还是一个序列(IEnumerable/IQueryable)"的,基本是延迟的(它只是接着搭配方);返回"一个具体结果(集合 List、数字 Count、单个元素 First、布尔 Any)"的,基本是立即执行的(它要算出那个结果就必须跑一遍)它给我的启发是:判断一个 LINQ 操作"会不会触发查询",看它的返回类型最准——返回序列 → 延迟;返回具体值/集合 → 立即这个简单的判据,让我以后写链式 LINQ 时,能一眼看出"这条链在哪一步才真正访问数据库",从而有意识地控制"执行发生在哪里、发生几次"很多 LINQ 的性能问题和诡异 bug,本质都是"没意识到某一步悄悄触发了执行";而看懂返回类型,就是看懂执行时机的钥匙。

第五件事:延迟 vs 立即,各自的适用场景

这次事故让我明白,延迟和立即没有绝对的好坏,关键是用对场景。我把两者的取舍整理了一下。

维度 保留延迟(不物化) 立即物化(ToList)
多次使用结果 ✗ 每次都重新执行 ✓ 只执行一次,复用
结果一致性 ✗ 多次执行可能不一致 ✓ 固定快照,一致
大数据量流式 ✓ 边读边处理,省内存 ✗ 全部加载进内存
只用一次/只取部分 ✓ 配合 Take 不查全部 △ 可能多查
继续在DB端过滤 ✓ IQueryable 翻译成SQL ✗ 物化后只能内存过滤
可读性/可预测性 △ 执行时机隐蔽 ✓ 执行点明确

这张表,让我对"到底该不该 ToList"有了清晰的判断。核心结论是:要多次使用结果、要结果一致、要执行点明确 → 立即物化(ToList);要处理大数据流、只取部分、还要继续在数据库端过滤 → 保留延迟它给我的最大启发是:延迟执行是一把双刃剑——用对了它高效又灵活(惰性、可组合、流式),用错了它就重复执行、结果飘忽;而用对的前提,是你清楚地知道"执行到底发生在何时、几次"更深一层,这让我意识到:很多语言/框架提供的"聪明特性"(延迟执行、隐式转换、自动装箱、响应式……),都是"方便"和"隐蔽"的结合体;它们把一些操作藏在了语法背后,替你自动完成,这在你理解它时是便利,在你不理解它时就是陷阱所以面对任何"帮你自动做了什么"的特性,最该做的,是搞清楚"它到底在背后替我做了什么、什么时候做的"——把隐蔽的变明确,便利才不会反噬成 bug。

第六件事:写 LINQ 查询时,我现在的决策习惯

现在写 LINQ,我不再 var query = ... 然后随手乱用,而是按这张图先想清楚"它什么时候执行、执行几次":

这张图的精髓,是"写 LINQ 时,主动想清楚执行时机和次数"第一问永远是 "这个结果会被用几次":用多次就立即 ToList 物化一次(避免重复执行);只用一次再看数据量,大就保留延迟流式读、不大随意。然后:物化前,尽量把能在数据库端做的 Where/OrderBy/分页都做完,最后才 ToList(别把全表拉回内存再过滤)。过程中始终警惕"在循环里重复枚举延迟查询"而最后一步是我现在的硬习惯:查数据库日志,确认 SQL 真的只发了预期的次数这套习惯,让我写 LINQ 时,从"写完就以为对了"变成了"清楚它何时执行、执行几次"——核心始终是:LINQ 查询是延迟执行的,写的时候就要在脑子里"运行"一遍它的执行时机。

我立下的几条规矩

这场"查询被偷偷执行三次"的事故,换来了我写 LINQ/EF 时,刻进骨子里的几条铁律:

  1. LINQ 是延迟执行的。定义查询只是建配方,终结操作(ToList/Count/Sum/foreach)才真正执行。
  2. 结果要用多次,先 ToList 物化一次。避免重复执行、保证多个统计基于同一快照。
  3. 大数据流式才保留延迟。别盲目 ToList 把海量数据全拉进内存。
  4. 过滤排序分页尽量在 IQueryable 数据库端做完再物化。别过早 ToList 把全表拉回内存过滤。
  5. 别在循环里重复枚举延迟查询。每次循环都会重新执行整个查询。
  6. 注意闭包捕获。延迟查询的外部变量在执行时才求值,循环里用局部变量快照。
  7. 看返回类型判断执行时机。返回序列=延迟,返回具体值/集合=立即;改完查 DB 日志验证 SQL 次数。

附:一段能直观"看见"延迟执行的实验代码

口说无凭。下面这段代码,通过在查询里埋一个打印,让你亲眼看见"定义查询时不执行、每次枚举才执行"的全过程:

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 2, 3, 4, 5 };

        Console.WriteLine("--- 1. 定义查询(注意: 此时没有任何打印!)---");
        var query = numbers.Where(n =>
        {
            Console.WriteLine($"  正在过滤: {n}");   // 埋一个打印, 看它何时被调用
            return n > 2;
        });
        Console.WriteLine("--- 查询已定义, 但上面的'正在过滤'一行都没打印 ---");
        Console.WriteLine("    → 证明: 定义查询时, 并没有执行!\n");

        Console.WriteLine("--- 2. 第一次枚举 (Count) ---");
        int c = query.Count();            // 此刻才执行 → 打印 5 次"正在过滤"
        Console.WriteLine($"    Count = {c}\n");

        Console.WriteLine("--- 3. 第二次枚举 (ToList) ---");
        var list = query.ToList();        // 又执行一遍 → 又打印 5 次"正在过滤"
        Console.WriteLine($"    List = [{string.Join(",", list)}]\n");

        Console.WriteLine("--- 4. 对比: 先 ToList 物化, 再多次用 ---");
        var materialized = numbers.Where(n =>
        {
            Console.WriteLine($"  (物化版)正在过滤: {n}");
            return n > 2;
        }).ToList();                      // 只执行这一次
        int c2 = materialized.Count;      // 内存操作, 不再触发"正在过滤"
        var l2 = materialized.ToList();   // 内存操作, 不再触发"正在过滤"
        Console.WriteLine($"    → 物化后, '正在过滤'只打印了一轮! Count={c2}");
    }
}

/* 输出(节选), 一眼看清延迟执行:
   --- 1. 定义查询(注意: 此时没有任何打印!)---
   --- 查询已定义, 但上面的'正在过滤'一行都没打印 ---
       → 证明: 定义查询时, 并没有执行!
   --- 2. 第一次枚举 (Count) ---
     正在过滤: 1 ... 正在过滤: 5     ← 这里才执行!
   --- 3. 第二次枚举 (ToList) ---
     正在过滤: 1 ... 正在过滤: 5     ← 又执行一遍!(重复)
   --- 4. 对比: 先 ToList 物化, 再多次用 ---
     (物化版)正在过滤: 1 ... 5       ← 只这一轮, 后续Count/ToList都不再触发
*/

// 核心: 定义查询不打印(未执行), 每次Count/ToList都重新打印一轮(重复执行);
//   先ToList物化后, 过滤只发生一次, 后续都是内存操作。亲眼所见, 胜过千言。

这段实验代码,把"延迟执行"这个抽象概念,变成了肉眼可见的打印序列它的精妙之处,在于Where 的谓词里埋了一行 Console.WriteLine——这样,"过滤操作每被执行一次,就打印一行",于是查询到底什么时候、执行了几次,全都暴露在输出里跑一遍你会清清楚楚地看到:定义查询那一刻,一行"正在过滤"都没有(证明没执行);第一次 Count 打印了一轮、第二次 ToList 又打印了一轮(证明每次枚举都重新执行);而先 ToList 物化后,"正在过滤"只打印了一轮,后续的 Count/ToList 都不再触发(证明物化后只执行一次)这,正是我想用这段代码,留给每个学 LINQ 的人的最后一课:对于任何"行为隐蔽、难以捉摸"的特性,最好的理解方式,不是反复读文档,而是动手写一段能"把它的隐蔽行为显形"的实验代码就像这里"在谓词里埋打印",一个小小的探针,就让"看不见的执行时机"变得一目了然把抽象的机制,转化成可观测的现象——这是程序员理解一切"黑盒"最有力的手段;眼见为实,胜过千言万语,也胜过想当然的猜测。从今往后,每当我对某个特性的行为拿不准,我都会想起这段代码教我的方法:别猜,写个小实验,让它自己告诉你答案。

写在最后

回头看,这场由 LINQ 延迟执行引发的、查询被偷偷跑三遍的事故,真正教给我的,远不止"记得 ToList"这一个技巧。它让我对编程里一类极其普遍的现象,有了警觉:很多语言和框架,都热衷于提供"聪明的、帮你自动做事"的特性——它们把某些操作的"真实发生时机"或"真实代价",巧妙地隐藏在了简洁的语法背后LINQ 的延迟执行就是典型:一行 var query = ...,看起来人畜无害、像个普通的赋值,背后却藏着"它还没执行、且每次用都会重新执行"这个天大的玄机。我栽就栽在,把这个"看起来像普通变量"的东西,当成了"已经算好的结果"这让我领悟到一个朴素却深刻的道理:当一段代码"看起来在做 A",我们一定要搞清楚它"实际上在做什么、什么时候做、做几次";尤其是那些"语法很简洁、行为很智能"的特性——简洁的语法,往往掩盖了背后不简单的机制真正的掌握,不是"会用这个语法",而是"看穿这个语法背后,到底发生了什么";对"自动、隐式、惰性、智能"这类词保持一份清醒的好奇,去追问它们背后的真实行为,是写出可预测、高性能代码的必修课这,是我用一次"查询跑三遍"的事故,换来的、关于 LINQ、也关于"看穿语法糖"的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次写下 var query = ... 时,多问一句"它到底什么时候、会执行几次",那我对着那三条一模一样的 SQL 日志熬的这大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我给系统加了 Redis 缓存本以为高枕无忧,大促零点却被缓存集体失效瞬间打垮了数据库,我对着缓存雪崩、击穿、穿透排查了大半天的复盘

2026-6-2 5:49:33

技术教程

我用 as 把接口返回的数据断言成了想要的类型,编译器一声不吭全绿,上线后却在访问属性时疯狂报 undefined,我对着类型断言排查了大半天的复盘

2026-6-2 6:00:16

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索