You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Mongoose关联双集合聚合求和结果异常问题求助

MongoDB聚合查询统计数值异常问题解决

集合Schema定义

Entity

const Entity = new Schema(
  {
    name: {
      type: String,
      required: true,
    },
    type: {
      type: String,
      required: true,
      enum: ["owner", "customer", "supplier"],
    },
  }
);

Account

const Account = new Schema(
  {
    name: {
      type: String,
      required: true,
    },
    phone: {
      type: String,
      default: "+964",
    },
  }
);

PR

const PR = new Schema(
  {
    number: {
      type: Number,
      default: 0,
    },
    customer: {
      type: Schema.Types.ObjectId,
      ref: "Entity",
    },
    owner: {
      type: Schema.Types.ObjectId,
      ref: "Entity",
    },
    account: {
      type: Schema.Types.ObjectId,
      ref: "Account",
    },
    items: [item.schema],
    total_usd: {
      type: Number,
      default: 0,
    },
    total_iqd: {
      type: Number,
      default: 0,
    },
  }
);

Invoice

const Invoice = new Schema(
  {
    number: {
      type: Number,
      index: true,
      default: 0,
    },
    customer: {
      type: Schema.Types.ObjectId,
      ref: "Entity",
    },
    owner: {
      type: Schema.Types.ObjectId,
      ref: "Entity",
    },
    account: {
      type: Schema.Types.ObjectId,
      ref: "Account",
    },
    total_usd: {
      type: Number,
      default: 0,
    },
    total_iqd: {
      type: Number,
      default: 0,
    },
  }
);

需求说明

按状态、年份和account分组,统计PR和Invoice集合中total_usd与total_iqd的总和。

现有聚合查询代码

await entityModel
  .aggregate([
    { $match: { _id: new mongoose.Types.ObjectId(req.query._id) } },
    {
      $lookup: {
        from: "invoices",
        localField: "_id",
        foreignField: "customer",
        as: "invoices",
      },
    },
    { $unwind: "$invoices" },
    {
      $lookup: {
        from: "accounts",
        localField: "invoices.account",
        foreignField: "_id",
        pipeline: [{ $project: { _id: 0, name: 1 } }],
        as: "invoices.account",
      },
    },
    { $unwind: "$invoices.account" },
    {
      $lookup: {
        from: "prs",
        localField: "_id",
        foreignField: "customer",
        as: "prs",
      },
    },
    { $unwind: "$prs" },
    {
      $lookup: {
        from: "accounts",
        localField: "prs.account",
        foreignField: "_id",
        pipeline: [{ $project: { _id: 0, name: 1 } }],
        as: "prs.account",
      },
    },
    { $unwind: "$prs.account" },
    {
      $group: {
        _id: {
          inv_status: "$invoices.status",
          inv_account: "$invoices.account",
          inv_year: { $year: "$invoices.created_at" },
          pr_status: "$prs.status",
          pr_account: "$prs.account",
          pr_year: { $year: "$prs.created_at" },
        },
        inv_sum_usd: { $sum: "$invoices.total_usd" },
        inv_sum_iqd: { $sum: "$invoices.total_iqd" },
        pr_sum_usd: { $sum: "$prs.total_usd" },
        pr_sum_iqd: { $sum: "$prs.total_iqd" },
      },
    },
  ])
  .then((result) => {
    res.send(result);
  });

错误查询结果

[
    {
        "_id": {
            "inv_status": "approved",
            "inv_account": {
                "name": "FIIQ"
            },
            "inv_year": 2023,
            "pr_status": "approved",
            "pr_account": {
                "name": "FIIX"
            },
            "pr_year": 2023
        },
        "inv_sum_usd": 480000,
        "inv_sum_iqd": 0,
        "pr_sum_usd": 514440,
        "pr_sum_iqd": 0
    },
    {
        "_id": {
            "inv_status": "approved",
            "inv_account": {
                "name": "FIIQ"
            },
            "inv_year": 2023,
            "pr_status": "approved",
            "pr_account": {
                "name": "FIIQ"
            },
            "pr_year": 2023
        },
        "inv_sum_usd": 144000,
        "inv_sum_iqd": 0,
        "pr_sum_usd": 40700,
        "pr_sum_iqd": 14256000
    },
    {
        "_id": {
            "inv_status": "approved",
            "inv_account": {
                "name": "FIIX"
            },
            "inv_year": 2023,
            "pr_status": "approved",
            "pr_account": {
                "name": "FIIX"
            },
            "pr_year": 2023
        },
        "inv_sum_usd": 2551740,
        "inv_sum_iqd": 0,
        "pr_sum_usd": 2829420,
        "pr_sum_iqd": 0
    },
    {
        "_id": {
            "inv_status": "approved",
            "inv_account": {
                "name": "FIIX"
            },
            "inv_year": 2023,
            "pr_status": "approved",
            "pr_account": {
                "name": "FIIQ"
            },
            "pr_year": 2023
        },
        "inv_sum_usd": 765522,
        "inv_sum_iqd": 0,
        "pr_sum_usd": 223850,
        "pr_sum_iqd": 78408000
    }
]

问题原因及修复方案

问题根源

原查询先对invoices执行$unwind,再关联prs并执行$unwind,导致笛卡尔积:每个Invoice会和所有关联的PR进行组合,最终求和时同一Invoice/PR的数值被重复计算,结果远大于实际值。

修复方案

分别对Invoice和PR集合在$lookup阶段完成分组统计,避免笛卡尔积。具体步骤:

  1. 匹配目标Entity;
  2. 用$lookup关联invoices,在pipeline内完成按状态、年份、account的分组求和,并关联Account名称;
  3. 用$lookup关联prs,同样在pipeline内完成分组求和与Account关联;
  4. 最后整理输出格式。

修复后的代码

await entityModel
  .aggregate([
    { $match: { _id: new mongoose.Types.ObjectId(req.query._id) } },
    // 处理Invoice的分组统计
    {
      $lookup: {
        from: "invoices",
        let: { entityId: "$_id" },
        pipeline: [
          { $match: { $expr: { $eq: ["$customer", "$$entityId"] } } },
          // 关联Account获取名称
          {
            $lookup: {
              from: "accounts",
              localField: "account",
              foreignField: "_id",
              as: "account_info",
              pipeline: [{ $project: { name: 1 } }]
            }
          },
          { $unwind: "$account_info" },
          // 按状态、年份、account分组求和
          {
            $group: {
              _id: {
                status: "$status",
                year: { $year: "$created_at" },
                account: "$account_info.name"
              },
              sum_usd: { $sum: "$total_usd" },
              sum_iqd: { $sum: "$total_iqd" }
            }
          },
          // 重命名字段方便识别
          {
            $project: {
              _id: 0,
              type: "invoice",
              status: "$_id.status",
              year: "$_id.year",
              account: "$_id.account",
              total_usd: "$sum_usd",
              total_iqd: "$sum_iqd"
            }
          }
        ],
        as: "invoice_stats"
      }
    },
    // 处理PR的分组统计
    {
      $lookup: {
        from: "prs",
        let: { entityId: "$_id" },
        pipeline: [
          { $match: { $expr: { $eq: ["$customer", "$$entityId"] } } },
          {
            $lookup: {
              from: "accounts",
              localField: "account",
              foreignField: "_id",
              as: "account_info",
              pipeline: [{ $project: { name: 1 } }]
            }
          },
          { $unwind: "$account_info" },
          {
            $group: {
              _id: {
                status: "$status",
                year: { $year: "$created_at" },
                account: "$account_info.name"
              },
              sum_usd: { $sum: "$total_usd" },
              sum_iqd: { $sum: "$total_iqd" }
            }
          },
          {
            $project: {
              _id: 0,
              type: "pr",
              status: "$_id.status",
              year: "$_id.year",
              account: "$_id.account",
              total_usd: "$sum_usd",
              total_iqd: "$sum_iqd"
            }
          }
        ],
        as: "pr_stats"
      }
    },
    // 合并两个统计结果数组
    { $project: { combined: { $concatArrays: ["$invoice_stats", "$pr_stats"] } } },
    { $unwind: "$combined" },
    // 按状态、年份、account、类型分组
    {
      $group: {
        _id: {
          type: "$combined.type",
          status: "$combined.status",
          year: "$combined.year",
          account: "$combined.account"
        },
        total_usd: { $sum: "$combined.total_usd" },
        total_iqd: { $sum: "$combined.total_iqd" }
      }
    },
    // 整理输出格式
    {
      $project: {
        _id: 0,
        document_type: "$_id.type",
        status: "$_id.status",
        year: "$_id.year",
        account: "$_id.account",
        total_usd: 1,
        total_iqd: 1
      }
    }
  ])
  .then((result) => {
    res.send(result);
  });

说明

  • 该方案在$lookup的pipeline内完成子集合的分组统计,彻底避免了笛卡尔积;
  • 最终结果会按文档类型(invoice/pr)、状态、年份、account展示统计总和,数值与实际数据一致;
  • 若需要将Invoice和PR的同维度数据合并,可调整最后一步的$group逻辑。

内容的提问来源于stack exchange,提问作者Mohammed Riyadh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 04:54:51