无尘阁日记

无尘阁日记

你看到 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() 做的事情是:

  1. 执行 SQL,得到所有行(2410 行)

  2. 按主模型(这里是 TJDomainSubjectsSub)的主键(比如 s.Id)来构建模型数组

  3. 同一个主键(同一个 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,不再做“主模型去重”。

所以会有两种可能:

  1. 你当前这段实际执行的代码并没有真正调用到 asArray() 的查询(比如你调试的是另一个 $query

  2. 你们项目里有自定义的 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 个。

六、总结一下真正的“坑点”

  1. SQL 行数 = 2410,是“明细行”的数量(科目×sheet 的组合)

  2. ActiveRecord 的 all() 返回的是“主模型数量”(这里是科目 TJDomainSubjectsSub),
    对同一个主键的多行会被合并成 1 个模型 → 只剩 40 个科目

  3. createCommand()->queryAll() 是原始 DB 行数,不受 AR 的去重逻辑影响

  4. 如果你想拿明细级别的 2410 条记录,就不要期望用“科目 AR”直接拿到 2410 个模型,
    而是:

    • 要么用 Query/createCommand()

    • 要么换成以 sheet 为主模型的 AR 查询

    • 或排查项目里是否有自定义 ActiveQuery 在 all()/populate() 中再做二次压缩

所以,你现在遇到的是 Yii ActiveRecord 的设计特性 + 自身主表选错/预期不一致的问题
不是 SQL 和数据库不一致的 bug。