MongoDB中的计算分组字段

13

在这个来自MongoDB文档的示例中,我如何使用MongoTemplate编写查询?

db.sales.aggregate(
   [
      {
        $group : {
           _id : { month: { $month: "$date" }, day: { $dayOfMonth: "$date" }, year: { $year: "$date" } },
           totalPrice: { $sum: { $multiply: [ "$price", "$quantity" ] } },
           averageQuantity: { $avg: "$quantity" },
           count: { $sum: 1 }
        }
      }
   ]
)

一般来说,我该如何按计算字段分组?

2个回答

20

实际上,你可以先使用"project"完成这样的操作,但对我来说,在此之前需要$project阶段有点不太直观:

你实际上可以像这样首先使用 "project",但是对我来说,在此之前要求一个$project阶段有点不太直观:

    Aggregation agg = newAggregation(
        project("quantity")
            .andExpression("dayOfMonth(date)").as("day")
            .andExpression("month(date)").as("month")
            .andExpression("year(date)").as("year")
            .andExpression("price * quantity").as("totalAmount"),
        group(fields().and("day").and("month").and("year"))
            .avg("quantity").as("averavgeQuantity")
            .sum("totalAmount").as("totalAmount")
            .count().as("count")
    );

就像我说的那样,这看起来不符合直觉,因为你应该只需要在$group阶段下声明所有内容,但是助手似乎不能按照这种方式工作。序列化结果有点奇怪(用数组包装了日期操作符参数),但它似乎可以工作。但仍然需要使用两个管道阶段而不是一个。

这个有什么问题呢?好吧,通过分离“project”部分,处理整个管道中的所有文档以获得计算字段,这意味着它必须在进入组阶段之前通过所有东西。

运行两种形式的查询可以明显地看到处理时间上的差异。使用单独的project阶段,在我的硬件上执行所需的时间是在所有字段在“group”操作期间计算的查询所需时间的三倍。

因此似乎目前唯一正确的构建方法是自己构建管道对象:

    ApplicationContext ctx =
            new AnnotationConfigApplicationContext(SpringMongoConfig.class);
    MongoOperations mongoOperation = (MongoOperations) ctx.getBean("mongoTemplate");

    BasicDBList pipeline = new BasicDBList();
    String[] multiplier = { "$price", "$quantity" };

    pipeline.add(
        new BasicDBObject("$group",
            new BasicDBObject("_id",
                new BasicDBObject("month", new BasicDBObject("$month", "$date"))
                    .append("day", new BasicDBObject("$dayOfMonth", "$date"))
                    .append("year", new BasicDBObject("$year", "$date"))
            )
            .append("totalPrice", new BasicDBObject(
                "$sum", new BasicDBObject(
                    "$multiply", multiplier
                )
            ))
            .append("averageQuantity", new BasicDBObject("$avg", "$quantity"))
            .append("count",new BasicDBObject("$sum",1))
        )
    );

    BasicDBObject aggregation = new BasicDBObject("aggregate","collection")
        .append("pipeline",pipeline);

    System.out.println(aggregation);

    CommandResult commandResult = mongoOperation.executeCommand(aggregation);

如果你觉得这些都太简洁了,那么你可以使用 JSON 源并解析它。但是当然,它必须是有效的 JSON:

    String json = "[" +
        "{ \"$group\": { "+
            "\"_id\": { " +
                "\"month\": { \"$month\": \"$date\" }, " +
                "\"day\": { \"$dayOfMonth\":\"$date\" }, " +
                "\"year\": { \"$year\": \"$date\" } " +
            "}, " +
            "\"totalPrice\": { \"$sum\": { \"$multiply\": [ \"$price\", \"$quantity\" ] } }, " +
            "\"averageQuantity\": { \"$avg\": \"$quantity\" }, " +
            "\"count\": { \"$sum\": 1 } " +
        "}}" +
    "]";

    BasicDBList pipeline = (BasicDBList)com.mongodb.util.JSON.parse(json);

1
感谢Neil抽出时间回答这个问题。实际上,我更喜欢你的第一个解决方案。可能是因为我更熟悉关系型数据库,对管道框架不太熟悉。 - Navin Viswanath
@NavinViswanath 分阶段进行这项操作是不好的。这样会增加数据的额外传递,从而需要更多的时间。在我的样本测试中,需要三倍的时间。我在答案中进一步扩展了这个问题,因为这对理解很有用。 - Neil Lunn
我明白了。再次感谢你,尼尔。关于我正在编写的查询,它与这个非常相似,我在'project'之前有一个'match'。我猜那是另一个阶段,虽然'match'应该会显著过滤掉进入'project'阶段的文档。 - Navin Viswanath

0
另一个选择是使用自定义聚合操作类,可以像这样定义:
public class CustomAggregationOperation implements AggregationOperation {
    private DBObject operation;

    public CustomAggregationOperation (DBObject operation) {
        this.operation = operation;
    }

    @Override
    public DBObject toDBObject(AggregationOperationContext context) {
        return context.getMappedObject(operation);
    }
}

然后在管道中实现它,就像这样:

Aggregation aggregation = newAggregation(
    new CustomAggregationOperation(
        new BasicDBObject(
            "$group",
            new BasicDBObject("_id",
                new BasicDBObject("day", new BasicDBObject("$dayOfMonth", "$date" ))
                .append("month", new BasicDBObject("$month", "$date"))
                .append("year", new BasicDBObject("$year", "$date"))
            )
            .append("totalPrice", new BasicDBObject(
                "$sum", new BasicDBObject(
                    "$multiply", Arrays.asList("$price","$quantity")
                )
            ))
            .append("averageQuantity", new BasicDBObject("$avg", "$quantity"))
            .append("count",new BasicDBObject("$sum",1))
        )
    )
)

这基本上是一个与现有的管道助手使用的接口一致的接口,但它直接采用DBObject定义以在管道构建中返回,而不是使用其他助手方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接