Общий способ - это, конечно, иметь агрегацию базы данных.Если вы хотите, чтобы данные находились в «недельном диапазоне», то есть несколько способов сделать это, в зависимости от того, какой подход вы на самом деле хотите для своего случая.
Группировка по неделям ISO
Просто продемонстрировав для «месяца мая» пример, вы получите что-то вроде:
startdate = datetime(2018,5,1)
enddate = datetime(2018,6,1)
result = db.sales.aggregate([
{ '$match': { 'date': { '$gte': startdate, '$lt': enddate } } },
{ '$group': {
'_id': {
'year': { '$year': '$date' },
'week': { '$isoWeek': '$date' }
},
'totalQty': { '$sum': '$qty' },
'count': { '$sum': 1 }
}},
{ '$sort': { '_id': 1 } }
])
Это довольно простой вызов с использованием $year
и $isoWeek
или, возможно, даже операторы $week
в зависимости от того, что фактически поддерживает ваша версия MongoDB.Все, что вам нужно сделать, это указать их в ключе группировки _id
$group
, а затем выбрать другие аккумуляторы, например $sum
, в зависимости от того, что вам действительно нужно «накапливать» вэта группировка.
$week
и $isoWeek
просто немного отличаются, когда последний более соответствует функциям isoweek
библиотека для Python и аналогичные вещи для других языков.В общем, вы можете просто настроить неделю с двумя, добавив 1
.См. Документацию для получения более подробной информации.
В этом случае вы можете при желании просто позволить базе данных выполнить работу по «агрегации», а затем получить «желаемые даты» на основе выходных данных.т.е. для python вы можете преобразовать результат с datetime
значениями, соответствующими каждой неделе, как:
result = list(result)
for item in result:
item.update({
'start': datetime.combine(
Week(item['_id']['year'],item['_id']['week']).monday(),
datetime.min.time()
),
'end': datetime.combine(
Week(item['_id']['year'],item['_id']['week']).sunday(),
datetime.max.time()
)
})
item.pop('_id',None)
Группировать по самоопределению
Если придерживаться стандартов ISO не для вас, тоальтернативный подход состоит в том, чтобы определить свои собственные «интервалы», для которых нужно накапливать «группировку».Основным инструментом с MongoDB здесь является $bucket
и предварительная обработка небольшого списка:
cuts = [startdate]
date = startdate
while ( date < enddate ):
date = date + timedelta(days=7)
if ( date > enddate ):
date = enddate
cuts.append(date)
alternate = db.sales.aggregate([
{ '$match': { 'date': { '$gte': startdate, '$lt': enddate } } },
{ '$bucket': {
'groupBy': '$date',
'boundaries': cuts,
'output': {
'totalQty': { '$sum': '$qty' },
'count': { '$sum': 1 }
}
}},
{ '$project': {
'_id': 0,
'start': '$_id',
'end': {
'$cond': {
'if': {
'$gt': [
{ '$add': ['$_id', (1000 * 60 * 60 * 24 * 7) - 1] },
enddate
]
},
'then': { '$add': [ enddate, -1 ] },
'else': {
'$add': ['$_id', (1000 * 60 * 60 * 24 * 7) - 1]
}
}
},
'totalQty': 1,
'count': 1
}}
])
Вместо использования определенных функций, таких как $week
или$isoWeek
, вместо этого мы определяем «интервалы в 7 дней» с заданной даты начала запроса и создаем массив этих интервалов, всегда, конечно, заканчивая «максимальным» значением из диапазона данных
Этот list
затем задается в качестве аргумента для этапа агрегации $bucket
для его опции "boundaries"
.На самом деле это просто список значений, который сообщает оператору, что накапливать «до» для каждой «группировки».
Фактически, оператор на самом деле является просто «сокращенной» реализацией * 1066Оператор агрегации * на стадии конвейера $group
.Оба этих оператора требуют MongoDB 3.4, но вы можете сделать то же самое, используя $cond
в пределах $group
, но просто вложив каждое условие else
для каждой "границы"значение.Это возможно, но только немного сложнее, и вы все равно должны использовать MongoDB 3.4 как минимальную версию к настоящему моменту.
Если вы обнаружите, что действительно должны, используя $cond
в$group
добавлен к приведенным ниже примерам, показывающим, как по существу преобразовать тот же самый список cuts
в такое утверждение, и это означает, что вы можете по существу делать то же самое вплоть до MongoDB 2.2, гдебыла введена структура агрегирования.
Примеры
В качестве полного примера вы можете рассмотреть следующий листинг, который вставляет случайные данные за месяц, а затем запускает оба представленных варианта агрегации:
from random import randint
from datetime import datetime, timedelta, date
from isoweek import Week
from pymongo import MongoClient
from bson.json_util import dumps, JSONOptions
import bson.json_util
client = MongoClient()
db = client.test
db.sales.delete_many({})
startdate = datetime(2018,5,1)
enddate = datetime(2018,6,1)
currdate = startdate
batch = []
while ( currdate < enddate ):
currdate = currdate + timedelta(hours=randint(1,24))
if ( currdate > enddate ):
currdate = enddate
qty = randint(1,100);
if ( currdate < enddate ):
batch.append({ 'date': currdate, 'qty': qty })
if ( len(batch) >= 1000 ):
db.sales.insert_many(batch)
batch = []
if ( len(batch) > 0):
db.sales.insert_many(batch)
batch = []
result = db.sales.aggregate([
{ '$match': { 'date': { '$gte': startdate, '$lt': enddate } } },
{ '$group': {
'_id': {
'year': { '$year': '$date' },
'week': { '$isoWeek': '$date' }
},
'totalQty': { '$sum': '$qty' },
'count': { '$sum': 1 }
}},
{ '$sort': { '_id': 1 } }
])
result = list(result)
for item in result:
item.update({
'start': datetime.combine(
Week(item['_id']['year'],item['_id']['week']).monday(),
datetime.min.time()
),
'end': datetime.combine(
Week(item['_id']['year'],item['_id']['week']).sunday(),
datetime.max.time()
)
})
item.pop('_id',None)
print("Week grouping")
print(
dumps(result,indent=2,
json_options=JSONOptions(datetime_representation=2)))
cuts = [startdate]
date = startdate
while ( date < enddate ):
date = date + timedelta(days=7)
if ( date > enddate ):
date = enddate
cuts.append(date)
alternate = db.sales.aggregate([
{ '$match': { 'date': { '$gte': startdate, '$lt': enddate } } },
{ '$bucket': {
'groupBy': '$date',
'boundaries': cuts,
'output': {
'totalQty': { '$sum': '$qty' },
'count': { '$sum': 1 }
}
}},
{ '$project': {
'_id': 0,
'start': '$_id',
'end': {
'$cond': {
'if': {
'$gt': [
{ '$add': ['$_id', (1000 * 60 * 60 * 24 * 7) - 1] },
enddate
]
},
'then': { '$add': [ enddate, -1 ] },
'else': {
'$add': ['$_id', (1000 * 60 * 60 * 24 * 7) - 1]
}
}
},
'totalQty': 1,
'count': 1
}}
])
alternate = list(alternate)
print("Bucket grouping")
print(
dumps(alternate,indent=2,
json_options=JSONOptions(datetime_representation=2)))
cuts = [startdate]
date = startdate
while ( date < enddate ):
date = date + timedelta(days=7)
if ( date > enddate ):
date = enddate
if ( date < enddate ):
cuts.append(date)
stack = []
for i in range(len(cuts)-1,0,-1):
rec = {
'$cond': [
{ '$lt': [ '$date', cuts[i] ] },
cuts[i-1]
]
}
if ( len(stack) == 0 ):
rec['$cond'].append(cuts[i])
else:
lval = stack.pop()
rec['$cond'].append(lval)
stack.append(rec)
pipeline = [
{ '$match': { 'date': { '$gt': startdate, '$lt': enddate } } },
{ '$group': {
'_id': stack[0],
'totalQty': { '$sum': '$qty' },
'count': { '$sum': 1 }
}},
{ '$sort': { '_id': 1 } },
{ '$project': {
'_id': 0,
'start': '$_id',
'end': {
'$cond': {
'if': {
'$gt': [
{ '$add': [ '$_id', ( 1000 * 60 * 60 * 24 * 7 ) - 1 ] },
enddate
]
},
'then': { '$add': [ enddate, -1 ] },
'else': {
'$add': [ '$_id', ( 1000 * 60 * 60 * 24 * 7 ) - 1 ]
}
}
},
'totalQty': 1,
'count': 1
}}
]
#print(
# dumps(pipeline,indent=2,
# json_options=JSONOptions(datetime_representation=2)))
older = db.sales.aggregate(pipeline)
older = list(older)
print("Cond Group")
print(
dumps(older,indent=2,
json_options=JSONOptions(datetime_representation=2)))
С выводом:
Week grouping
[
{
"totalQty": 449,
"count": 9,
"start": {
"$date": "2018-04-30T00:00:00Z"
},
"end": {
"$date": "2018-05-06T23:59:59.999Z"
}
},
{
"totalQty": 734,
"count": 14,
"start": {
"$date": "2018-05-07T00:00:00Z"
},
"end": {
"$date": "2018-05-13T23:59:59.999Z"
}
},
{
"totalQty": 686,
"count": 14,
"start": {
"$date": "2018-05-14T00:00:00Z"
},
"end": {
"$date": "2018-05-20T23:59:59.999Z"
}
},
{
"totalQty": 592,
"count": 12,
"start": {
"$date": "2018-05-21T00:00:00Z"
},
"end": {
"$date": "2018-05-27T23:59:59.999Z"
}
},
{
"totalQty": 205,
"count": 6,
"start": {
"$date": "2018-05-28T00:00:00Z"
},
"end": {
"$date": "2018-06-03T23:59:59.999Z"
}
}
]
Bucket grouping
[
{
"totalQty": 489,
"count": 11,
"start": {
"$date": "2018-05-01T00:00:00Z"
},
"end": {
"$date": "2018-05-07T23:59:59.999Z"
}
},
{
"totalQty": 751,
"count": 13,
"start": {
"$date": "2018-05-08T00:00:00Z"
},
"end": {
"$date": "2018-05-14T23:59:59.999Z"
}
},
{
"totalQty": 750,
"count": 15,
"start": {
"$date": "2018-05-15T00:00:00Z"
},
"end": {
"$date": "2018-05-21T23:59:59.999Z"
}
},
{
"totalQty": 493,
"count": 11,
"start": {
"$date": "2018-05-22T00:00:00Z"
},
"end": {
"$date": "2018-05-28T23:59:59.999Z"
}
},
{
"totalQty": 183,
"count": 5,
"start": {
"$date": "2018-05-29T00:00:00Z"
},
"end": {
"$date": "2018-05-31T23:59:59.999Z"
}
}
]
Cond Group
[
{
"totalQty": 489,
"count": 11,
"start": {
"$date": "2018-05-01T00:00:00Z"
},
"end": {
"$date": "2018-05-07T23:59:59.999Z"
}
},
{
"totalQty": 751,
"count": 13,
"start": {
"$date": "2018-05-08T00:00:00Z"
},
"end": {
"$date": "2018-05-14T23:59:59.999Z"
}
},
{
"totalQty": 750,
"count": 15,
"start": {
"$date": "2018-05-15T00:00:00Z"
},
"end": {
"$date": "2018-05-21T23:59:59.999Z"
}
},
{
"totalQty": 493,
"count": 11,
"start": {
"$date": "2018-05-22T00:00:00Z"
},
"end": {
"$date": "2018-05-28T23:59:59.999Z"
}
},
{
"totalQty": 183,
"count": 5,
"start": {
"$date": "2018-05-29T00:00:00Z"
},
"end": {
"$date": "2018-05-31T23:59:59.999Z"
}
}
]
Необязательная демонстрация JavaScript
Поскольку некоторые из вышеприведенных подходов довольно "питонны", то для более широкой аудитории мозгов JavaScriptобщий для предмета будет выглядеть так:
const { Schema } = mongoose = require('mongoose');
const moment = require('moment');
const uri = 'mongodb://localhost/test';
mongoose.Promise = global.Promise;
//mongoose.set('debug',true);
const saleSchema = new Schema({
date: Date,
qty: Number
})
const Sale = mongoose.model('Sale', saleSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
let start = new Date("2018-05-01");
let end = new Date("2018-06-01");
let date = new Date(start.valueOf());
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
let batch = [];
while ( date.valueOf() < end.valueOf() ) {
let hour = Math.floor(Math.random() * 24) + 1;
date = new Date(date.valueOf() + (1000 * 60 * 60 * hour));
if ( date > end )
date = end;
let qty = Math.floor(Math.random() * 100) + 1;
if (date < end)
batch.push({ date, qty });
if (batch.length >= 1000) {
await Sale.insertMany(batch);
batch = [];
}
}
if (batch.length > 0) {
await Sale.insertMany(batch);
batch = [];
}
let result = await Sale.aggregate([
{ "$match": { "date": { "$gte": start, "$lt": end } } },
{ "$group": {
"_id": {
"year": { "$year": "$date" },
"week": { "$isoWeek": "$date" }
},
"totalQty": { "$sum": "$qty" },
"count": { "$sum": 1 }
}},
{ "$sort": { "_id": 1 } }
]);
result = result.map(({ _id: { year, week }, ...r }) =>
({
start: moment.utc([year]).isoWeek(week).startOf('isoWeek').toDate(),
end: moment.utc([year]).isoWeek(week).endOf('isoWeek').toDate(),
...r
})
);
log({ name: 'ISO group', result });
let cuts = [start];
date = start;
while ( date.valueOf() < end.valueOf() ) {
date = new Date(date.valueOf() + ( 1000 * 60 * 60 * 24 * 7 ));
if ( date.valueOf() > end.valueOf() ) date = end;
cuts.push(date);
}
let alternate = await Sale.aggregate([
{ "$match": { "date": { "$gte": start, "$lt": end } } },
{ "$bucket": {
"groupBy": "$date",
"boundaries": cuts,
"output": {
"totalQty": { "$sum": "$qty" },
"count": { "$sum": 1 }
}
}},
{ "$addFields": {
"_id": "$$REMOVE",
"start": "$_id",
"end": {
"$cond": {
"if": {
"$gt": [
{ "$add": [ "$_id", ( 1000 * 60 * 60 * 24 * 7 ) - 1 ] },
end
]
},
"then": { "$add": [ end, -1 ] },
"else": {
"$add": [ "$_id", ( 1000 * 60 * 60 * 24 * 7 ) - 1 ]
}
}
}
}}
]);
log({ name: "Bucket group", result: alternate });
cuts = [start];
date = start;
while ( date.valueOf() < end.valueOf() ) {
date = new Date(date.valueOf() + ( 1000 * 60 * 60 * 24 * 7 ));
if ( date.valueOf() > end.valueOf() ) date = end;
if ( date.valueOf() < end.valueOf() )
cuts.push(date);
}
let stack = [];
for ( let i = cuts.length - 1; i > 0; i-- ) {
let rec = {
"$cond": [
{ "$lt": [ "$date", cuts[i] ] },
cuts[i-1]
]
};
if ( stack.length === 0 ) {
rec['$cond'].push(cuts[i])
} else {
let lval = stack.pop();
rec['$cond'].push(lval);
}
stack.push(rec);
}
let pipeline = [
{ "$group": {
"_id": stack[0],
"totalQty": { "$sum": "$qty" },
"count": { "$sum": 1 }
}},
{ "$sort": { "_id": 1 } },
{ "$project": {
"_id": 0,
"start": "$_id",
"end": {
"$cond": {
"if": {
"$gt": [
{ "$add": [ "$_id", ( 1000 * 60 * 60 * 24 * 7 ) - 1 ] },
end
]
},
"then": { "$add": [ end, -1 ] },
"else": {
"$add": [ "$_id", ( 1000 * 60 * 60 * 24 * 7 ) - 1 ]
}
}
},
"totalQty": 1,
"count": 1
}}
];
let older = await Sale.aggregate(pipeline);
log({ name: "Cond group", result: older });
mongoose.disconnect();
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})()
и аналогичный вывод, конечно:
{
"name": "ISO group",
"result": [
{
"start": "2018-04-30T00:00:00.000Z",
"end": "2018-05-06T23:59:59.999Z",
"totalQty": 576,
"count": 10
},
{
"start": "2018-05-07T00:00:00.000Z",
"end": "2018-05-13T23:59:59.999Z",
"totalQty": 707,
"count": 11
},
{
"start": "2018-05-14T00:00:00.000Z",
"end": "2018-05-20T23:59:59.999Z",
"totalQty": 656,
"count": 12
},
{
"start": "2018-05-21T00:00:00.000Z",
"end": "2018-05-27T23:59:59.999Z",
"totalQty": 829,
"count": 16
},
{
"start": "2018-05-28T00:00:00.000Z",
"end": "2018-06-03T23:59:59.999Z",
"totalQty": 239,
"count": 6
}
]
}
{
"name": "Bucket group",
"result": [
{
"totalQty": 666,
"count": 11,
"start": "2018-05-01T00:00:00.000Z",
"end": "2018-05-07T23:59:59.999Z"
},
{
"totalQty": 727,
"count": 12,
"start": "2018-05-08T00:00:00.000Z",
"end": "2018-05-14T23:59:59.999Z"
},
{
"totalQty": 647,
"count": 12,
"start": "2018-05-15T00:00:00.000Z",
"end": "2018-05-21T23:59:59.999Z"
},
{
"totalQty": 743,
"count": 15,
"start": "2018-05-22T00:00:00.000Z",
"end": "2018-05-28T23:59:59.999Z"
},
{
"totalQty": 224,
"count": 5,
"start": "2018-05-29T00:00:00.000Z",
"end": "2018-05-31T23:59:59.999Z"
}
]
}
{
"name": "Cond group",
"result": [
{
"totalQty": 666,
"count": 11,
"start": "2018-05-01T00:00:00.000Z",
"end": "2018-05-07T23:59:59.999Z"
},
{
"totalQty": 727,
"count": 12,
"start": "2018-05-08T00:00:00.000Z",
"end": "2018-05-14T23:59:59.999Z"
},
{
"totalQty": 647,
"count": 12,
"start": "2018-05-15T00:00:00.000Z",
"end": "2018-05-21T23:59:59.999Z"
},
{
"totalQty": 743,
"count": 15,
"start": "2018-05-22T00:00:00.000Z",
"end": "2018-05-28T23:59:59.999Z"
},
{
"totalQty": 224,
"count": 5,
"start": "2018-05-29T00:00:00.000Z",
"end": "2018-05-31T23:59:59.999Z"
}
]
}