你看到 40 条,是因为 Yii 把剩下 2370 条都“合并掉”了
2025-11-14
问:
$subjects = TJDomainSubjectsSub::find()
->alias('s')
->leftJoin(TJDomainSheetsSub::tableName() . ' as sh', 's.SheetId = sh.Id and' . ' sh.ProjectId = ' .
$params['ProjectId'] . ' and sh.ValidMethod = ' . $params['EevaluationMethodType'] . ' and sh.IsDeleted = 0')
->where([
's.ProjectId' => $params['ProjectId'],
's.ValidMethod' => $params['EevaluationMethodType'],
's.IsDeleted' => 0,
])
->andWhere(['not', ['sh.SheetType' => 3]]) // Normal=1,OpenFromSubject=2,TemplateSheet=3
->select('sh.*,s.Id as SubjectId,s.Name as SubjectName,s.FromSubjectId as SourceSubjectId')
->asArray()
->all();
为何这个语句我在数据库执行的数量是2410条。但是打印的结果只有40条?
答:
先把关键结论说在前面:
你的这条 SQL 在数据库里返回 2410 行没问题,
但通过TJDomainSubjectsSub::find()查的时候,只会给你 40 个“科目模型”,
这是因为 ActiveRecord 把同一个主模型(同一个 s.Id)的重复行合并掉了,
所以你看到的是 40,而不是 2410。
createCommand()->queryAll()是“原生行数(2410)”,
ActiveRecord 是“去重后的主表记录数(40)”。
这个行为在 Yii 官方论坛也有人遇到过,query->count() 和 count(query->all()) 不一致,本质就是JOIN 导致主表重复行,AR 填充模型时按主键去重。(Yii Framework Forum)
一、为什么 SQL 是 2410,而 AR 只有 40?
你打印出来的 SQL 是:
SELECT `sh`.*, `s`.`Id` AS `SubjectId`, `s`.`Name` AS `SubjectName`, `s`.`FromSubjectId` AS `SourceSubjectId` FROM `TJ_Domain_Subjects` `s` INNER JOIN `TJ_Domain_Sheets` `sh` ON s.SheetId = sh.Id WHERE ((`s`.`ProjectId`='1292') AND (`s`.`ValidMethod`='1') AND (`s`.`IsDeleted`=0)) AND ((`sh`.`ProjectId`='1292') AND (`sh`.`ValidMethod`='1') AND (`sh`.`IsDeleted`=0)) AND (`sh`.`SheetType` <> 3)
这条 SQL 的主表是 TJ_Domain_Subjects(别名 s),TJ_Domain_Sheets(sh)是被 join 进来的。
如果一个科目 s.Id 对应很多条 sheet(满足你这些条件),
那么在结果表里就会出现:
s.Id = 1 + shA s.Id = 1 + shB s.Id = 1 + shC ...
也就是说:
2410 ≈ 若干科目 × 每个科目对应的多条 sheet
很有可能:一共只有 40 个不同的 s.Id,每个科目下面很多 sheet 组合起来就是 2410 行
你可以直接在数据库里跑一条验证一下(强烈建议你跑一下):
SELECT COUNT(*) AS total_rows, COUNT(DISTINCT s.Id) AS distinct_subjects FROM `TJ_Domain_Subjects` `s` INNER JOIN `TJ_Domain_Sheets` `sh` ON s.SheetId = sh.Id WHERE ((`s`.`ProjectId`='1292') AND (`s`.`ValidMethod`='1') AND (`s`.`IsDeleted`=0)) AND ((`sh`.`ProjectId`='1292') AND (`sh`.`ValidMethod`='1') AND (`sh`.`IsDeleted`=0)) AND (`sh`.`SheetType` <> 3);
大概率会得到类似:
total_rows = 2410 distinct_subjects = 40
也就是:2410 行数据实际上只有 40 个不同的科目 Id。
二、ActiveRecord 为啥会“自动变成 40 行”?
关键点在于:你是用 TJDomainSubjectsSub::find() 开的查询。
对 Yii 来说,这是一个“科目模型的查询”,s 是主表,sh 是附带 join 的表。
在 非 asArray 的情况下,ActiveQuery::all() 做的事情是:
执行 SQL,得到所有行(2410 行)
按主模型(这里是
TJDomainSubjectsSub)的主键(比如s.Id)来构建模型数组同一个主键(同一个 s.Id)的多行,只会生成一个模型对象
这正是官方论坛里那种现象:query->count() 统计的是“SQL 原始行数”,而 count(query->all()) 是“去掉重复主模型之后的模型数量”。(Yii Framework Forum)
所以:
你的 SQL 行数 = 2410(包含重复科目)
ActiveRecord 的模型数 = 40(每个科目只保留一个模型)
如果你这时打印 $subjects[0],会发现每个元素其实是“一个科目模型”(或者一行数组,视 asArray 而定),不会有 2410 行那么细。
这不是 bug,是 Yii 的设计:ActiveRecord 的查询结果本质是“主模型集合”,不是“原始行集合”。
三、那为啥 createCommand()->queryAll() 又是 2410?
因为:
Yii::$app->db->createCommand($sql)->queryAll();
这条是最原始的 DB 层调用,它根本不管 ActiveRecord、不管主键、不管模型,
就是把 SQL 的 2410 行原封不动返回数组给你,所以你看到的是 2410。
而 TJDomainSubjectsSub::find()->...->all() 走的是 AR 流程,如上所说,会按主键合并。
四、那 asArray() 为什么没救?
理论上:
->asArray()->all()
对于普通查询应该是直接返回 $rows,不再做“主模型去重”。
所以会有两种可能:
你当前这段实际执行的代码并没有真正调用到 asArray() 的查询(比如你调试的是另一个
$query)你们项目里有自定义的 ActiveQuery / 基类,把
all()或populate()重写了,在 asArray 的情况下也做了压缩
建议你在链上加这样一段调试一下(你应该已经部分做了,但补充 asArray 的检查):
$query = TJDomainSubjectsSub::find()
->alias('s')
->innerJoin('TJ_Domain_Sheets sh', 's.SheetId = sh.Id')
->where([...])
->andWhere([...])
->andWhere(['<>', 'sh.SheetType', 3])
->select('sh.*, s.Id AS SubjectId, s.Name AS SubjectName, s.FromSubjectId AS SourceSubjectId')
->asArray();
Yii::error('query class = ' . get_class($query), 'debug');
Yii::error('asArray=' . var_export($query->asArray, true)
. ' distinct=' . var_export($query->distinct, true)
. ' groupBy=' . var_export($query->groupBy, true)
. ' indexBy=' . var_export($query->indexBy, true),
'debug');
$rows = $query->createCommand()->queryAll();
Yii::error('raw rows = ' . count($rows), 'debug');
$subjects = $query->all();
Yii::error('ar rows = ' . count($subjects), 'debug');如果
raw rows= 2410 而ar rows= 40,并且asArray=true,那几乎可以确定:
你们项目里有自定义 ActiveQuery / 基类,在all()里对 asArray 的结果又做了一次“压缩”(比如按 SubjectId 再ArrayHelper::index())。如果
asArray=false,那就是你当前这个链上 没成功打开 asArray 模式,走的是模型填充流程,按主键合并成 40 个科目。
五、那我想要“2410 条明细”应该怎么查?
你现在这个需求,本质是:“我要所有 科目×Sheet 的组合行,并且 SheetType≠3”,
不是“我要 40 个科目对象”。
比较稳妥的几种写法:
方案 1:继续用 SQL / Query,不用 ActiveRecord
既然你已经验证 createCommand()->queryAll() 是对的,
那直接封装成 Query 就行:
use yii\db\Query; $rows = (new Query()) ->from(['s' => 'TJ_Domain_Subjects']) ->innerJoin(['sh' => 'TJ_Domain_Sheets'], 's.SheetId = sh.Id') ->where([ 's.ProjectId' => $params['ProjectId'], 's.ValidMethod' => $params['EevaluationMethodType'], 's.IsDeleted' => 0, 'sh.ProjectId' => $params['ProjectId'], 'sh.ValidMethod' => $params['EevaluationMethodType'], 'sh.IsDeleted' => 0, ]) ->andWhere(['<>', 'sh.SheetType', 3]) ->select([ 'sh.*', 'SubjectId' => 's.Id', 'SubjectName' => 's.Name', 'SourceSubjectId'=> 's.FromSubjectId', ]) ->all(); // 这里的 count($rows) 一定是 2410
方案 2:把“主模型”换成 Sheet
既然你从“Sheet”角度看,每一行就是一个 sheet+subject 的组合,那可以让 AR 主模型换成 TJDomainSheetsSub:
$rows = TJDomainSheetsSub::find()
->alias('sh')
->innerJoin(TJDomainSubjectsSub::tableName() . ' s', 's.SheetId = sh.Id')
->where([
's.ProjectId' => $params['ProjectId'],
's.ValidMethod' => $params['EevaluationMethodType'],
's.IsDeleted' => 0,
'sh.ProjectId' => $params['ProjectId'],
'sh.ValidMethod' => $params['EevaluationMethodType'],
'sh.IsDeleted' => 0,
])
->andWhere(['<>', 'sh.SheetType', 3])
->select('sh.*, s.Id AS SubjectId, s.Name AS SubjectName, s.FromSubjectId AS SourceSubjectId')
->asArray()
->all();如果一个 sheet 只对应一个 subject,那这里用 AR 也不会被“按主键合并”成 40 个。
六、总结一下真正的“坑点”
SQL 行数 = 2410,是“明细行”的数量(科目×sheet 的组合)
ActiveRecord 的 all() 返回的是“主模型数量”(这里是科目 TJDomainSubjectsSub),
对同一个主键的多行会被合并成 1 个模型 → 只剩 40 个科目createCommand()->queryAll()是原始 DB 行数,不受 AR 的去重逻辑影响如果你想拿明细级别的 2410 条记录,就不要期望用“科目 AR”直接拿到 2410 个模型,
而是:要么用
Query/createCommand()要么换成以 sheet 为主模型的 AR 查询
或排查项目里是否有自定义 ActiveQuery 在
all()/populate()中再做二次压缩
所以,你现在遇到的是 Yii ActiveRecord 的设计特性 + 自身主表选错/预期不一致的问题,
不是 SQL 和数据库不一致的 bug。
发表评论: