
本文介绍如何在 Laravel 中基于 document_number 分组,精准筛选出每组记录中的最新版本(如 Version 2、Version 3),或排除最新版仅保留历史版本,涵盖原生 SQL 优化、Eloquent 实现及关键注意事项。
本文介绍如何在 laravel 中基于 `document_number` 分组,精准筛选出每组记录中的最新版本(如 version 2、version 3),或排除最新版仅保留历史版本,涵盖原生 sql 优化、eloquent 实现及关键注意事项。
在文档管理系统、合同版本控制或审计日志等场景中,常需对具备复合唯一约束(如 document_number + version)的数据表进行“每文档取最新版”或“仅取历史旧版”的查询。Laravel 默认不提供开箱即用的“分组取极值记录”能力,需结合数据库特性与 Eloquent 灵活组合实现。
✅ 获取每个文档的最新版本(Latest Version per Document)
核心思路是:按 document_number 分组,并在每组内选取 version 字段的最大值。但需注意:若 version 是形如 "Version 1" 的字符串,直接 MAX(version) 将按字典序比较("Version 10"
推荐使用 Laravel 的 DB::raw() 配合数据库函数安全提取版本号:
use Illuminate\Support\Facades\DB;
use App\Models\Document;
$latestVersions = Document::select('document_number')
->selectRaw('MAX(CAST(SUBSTRING_INDEX(version, " ", -1) AS UNSIGNED)) as version_number')
->groupBy('document_number')
->get()
->map(function ($item) {
return [
'document_number' => $item->document_number,
'version' => 'Version ' . $item->version_number,
];
});✅ 说明:SUBSTRING_INDEX(version, " ", -1) 提取 version 字段最后一个空格后的数字(兼容 "v1"、"1.2"、"Version 5" 等格式),CAST(... AS UNSIGNED) 确保数值比较;最终通过 map() 组装为可读的 "Version X" 格式。
若需返回完整模型实例(而不仅是字段),可采用子查询关联方式(更健壮,支持 Eloquent 关系与访问器):
$latestSubquery = Document::select('document_number')
->selectRaw('MAX(CAST(SUBSTRING_INDEX(version, " ", -1) AS UNSIGNED)) as max_version_num')
->groupBy('document_number');
$latestDocuments = Document::whereIn([
'document_number',
DB::raw('CAST(SUBSTRING_INDEX(version, " ", -1) AS UNSIGNED)')
], function ($query) use ($latestSubquery) {
$query->from(DB::raw("({$latestSubquery->toSql()}) as latest"))
->select('document_number', 'max_version_num')
->setBindings($latestSubquery->getBindings());
})->get();? 获取所有历史版本(排除每文档最新版)
只需先查出各文档最新版本号,再用 whereNotIn() 排除即可:
// 步骤1:获取各 document_number 对应的最新 version 数字
$latestVersionNumbers = Document::select('document_number')
->selectRaw('MAX(CAST(SUBSTRING_INDEX(version, " ", -1) AS UNSIGNED)) as max_ver')
->groupBy('document_number')
->pluck('max_ver', 'document_number'); // ['ABCDoc1' => 2, 'ABCDoc2' => 3]
// 步骤2:构造 where 子句,排除最新版
$historicalDocs = Document::whereExists(function ($query) use ($latestVersionNumbers) {
$query->select(DB::raw(1))
->from('documents as d2')
->whereColumn('d2.document_number', 'documents.document_number')
->whereRaw('CAST(SUBSTRING_INDEX(d2.version, " ", -1) AS UNSIGNED) < ?', [
DB::raw('CAST(SUBSTRING_INDEX(documents.version, " ", -1) AS UNSIGNED)')
]);
})->get();更简洁的替代方案(适用于中小数据量):
$historicalDocs = Document::whereNotIn('id', function ($query) use ($latestVersionNumbers) {
$query->select('id')
->from('documents')
->whereIn('document_number', $latestVersionNumbers->keys())
->whereColumn(
DB::raw('CAST(SUBSTRING_INDEX(version, " ", -1) AS UNSIGNED)'),
$latestVersionNumbers->values()->toArray()
);
})->get();⚠️ 关键注意事项
- 版本号格式一致性至关重要:上述 SUBSTRING_INDEX(version, " ", -1) 假设版本字符串以空格分隔且数字在末尾。若实际格式为 v1.0.2 或 2024-001,请改用正则表达式(MySQL 8.0+ 支持 REGEXP_SUBSTR)或应用层解析。
-
性能优化建议:为 document_number 和 version 字段添加联合索引(即使已有唯一索引,GROUP BY 查询仍可受益):
Schema::table('documents', function (Blueprint $table) { $table->index(['document_number', 'version']); }); - 避免 N+1 问题:当需加载关联模型(如 createdBy 用户)时,务必使用 with() 预加载,而非在循环中调用关系。
- 迁移至 Laravel 11+? 可考虑封装为 Eloquent Scope 或自定义 Builder 方法,提升复用性与可测试性。
通过合理利用数据库聚合能力与 Laravel 的查询构建器,无需引入第三方包即可优雅解决多版本模型的分组极值查询需求——既保持性能,又维持代码清晰度与可维护性。










