应使用 whereNotIn() 实现 Eloquent 的“except”语义,即通过数据库层筛选排除指定 ID 列表的记录,避免 PHP 内存过滤;需确保字段已建索引,子查询须防 NULL 并显式指定表与字段,不可混用 whereDoesntHave()。

用 whereNotIn() 实现 Eloquent 的“except”语义
PHP 中没有原生的 except() 方法,Eloquent 也不提供直接排除一批模型的查询构造器。所谓“差集”,本质是「查出不在某个 ID 列表里的记录」,最稳妥的做法就是用 whereNotIn()。
常见错误是试图用 reject() 或 diff() 在 PHP 数组层面操作结果集——这会导致全量查库再内存过滤,严重浪费资源、丢失分页和索引优势。
- 必须先明确你要排除的是哪些
id(或其他唯一字段),且该字段已建立数据库索引 - 若排除条件涉及关联字段(如
user_id),确保该字段也已加索引,否则whereNotIn()会变慢 - MySQL 对
whereNotIn()子查询有坑:如果子查询返回NULL,整条语句结果为空 —— 所以务必在子查询中加->whereNotNull('id') - 示例:
$excludedIds = [1, 5, 12];<br>$posts = Post::whereNotIn('id', $excludedIds)->get();
用子查询做动态 except:比如排除某用户已读文章
当“要排除的集合”本身来自另一张表(如 reads 表),就不能硬编码 ID 列表,得用子查询。Laravel 支持闭包式子查询传入 whereNotIn(),但要注意写法。
错误写法:whereNotIn('id', Read::pluck('post_id')) —— 这会先执行 pluck(),把全部 post_id 拉到 PHP 内存,可能超内存或触发 MySQL 的 IN 参数长度限制(尤其 MySQL 5.7 默认 max_allowed_packet=4MB)。
立即学习“PHP免费学习笔记(深入)”;
- 正确做法是传一个闭包:
Post::whereNotIn('id', function ($query) {<br> $query->select('post_id')<br> ->from('reads')<br> ->where('user_id', auth()->id());<br>}) - 注意:子查询里不能用 Eloquent 模型方法(如
Read::where(...)->pluck()),必须用select()+from()显式指定,否则生成的 SQL 会多一层嵌套,影响优化器判断 - 如果子查询需 join 或复杂条件,建议先验证生成的 SQL(用
toSql())是否可走索引
whereDoesntHave() 不是 except 的替代品
有人看到「排除有某种关联的记录」就直觉用 whereDoesntHave(),但它语义不同:它排除的是「不存在关联记录」的主模型,而不是「排除掉某些已知 ID 的模型」。
例如:想排除 ID 为 [3,7,9] 的文章 → 必须用 whereNotIn('id', [...]);而 whereDoesntHave('author') 是找那些 author_id 为 NULL 或对应作者被删了的文章,完全不是一回事。
-
whereDoesntHave()底层是NOT EXISTS子查询,适合关系存在性判断 -
whereNotIn()是NOT IN,适合精确 ID 排除 - 混用会导致查不到数据却无报错,调试时容易误判为数据问题,其实是逻辑写错了
大数据量下避免 whereNotIn() 性能断崖
当要排除的 ID 数量超过几千个,MySQL 的 NOT IN 可能变慢,尤其配合 ORDER BY + LIMIT 分页时,优化器常放弃索引。
这不是 Laravel 的锅,是 SQL 本身特性。这时得换策略:
- 把要排除的 ID 写入临时表(如
temp_excludes),然后用LEFT JOIN ... IS NULL替代whereNotIn() - 或者反向思考:先查出要保留的 ID(用
whereIn()+ 合理条件缩小范围),再基于这些 ID 查详情 - 绝对不要在生产环境对上万 ID 调用
whereNotIn('id', $hugeArray)—— PHP 内存、SQL 解析、网络传输全会吃紧
真正难的从来不是写哪行代码,而是想清楚“这个排除动作,数据库能不能用索引快速完成”。



















