Skip to content
On this page

实际需求

实际项目中,可能遇到一些 非常复杂 的 情况

比如 :按 siteCode 字段分组,其中 根据 评分 score 字段 由高到低排序, 按 20%、30%、50%的形式 分成 3 组,计算 里面 不同 vertical 的数量

解决方案

第一步、获取 所有 siteCode 个数

db.getCollection('articleCollection').aggregate([
    {
        "$match": {
            "type": {
                "$ne": "随便写的"
            },
            "vertical": {
                "$ne": "随便写的"
            },
            "score": {
                "$gte": 0
            },
            "publishTime": {
                "$gte": 1553616000000,
                "$lte": 1553702400000
            },
            "areaStr": "湖南省,长沙市"
        }
    },
    {
        "$group": {
            "_id": {
                "siteCode": "$siteCode"
            },
            "count": {
                "$sum": 1
            }
        }
    }
])

拿到了 每个 siteCode 的 个数

第二步、计算 每个 siteCode 的 高中低 各 多少个

const v1 = 0.2;
const v2 = 0.5;

const n1 = Math.floor(number * v1);
const n2 = Math.floor(number * v2) - n1;
const n3 = number - n1 - n2;

number 是每种 siteCode 的 总数

第三步、每一种 高中低 的 分组个数

const r1 = n1 > 0 ? await this.getData({ ...querys, siteCode: key, limit: n1 }) : 0;
const r2 = n2 > 0 ? await this.getData({ ...querys, siteCode: key, limit: n2, skip: n1 }) : 0;
const r3 = n3 > 0 ? await this.getData({ ...querys, siteCode: key, skip: n1 + n2 }) : 0;

第四步、getData 方法的 具体实现

    /**
     * 根据条件查询数据库
     * @param {obj} param0 传入对象
     * @param {number} param0.score_startAt 评分起始
     * @param {number} param0.score_endAt 评分结束
     * @param {string} param0.publishTime_startBy 发布时间起始
     * @param {string} param0.publishTime_endBy 发布时间结束
     * @param {string} param0.areaStr 地区省市
     * @param {string} param0.siteCode 来源代码
     * @param {obj} param0._id 分组key
     * @return {array} 查询结果
     */
    async getData({
        score_startAt,
        score_endAt,
        publishTime_startBy,
        publishTime_endBy,
        areaStr,
        siteCode,
        limit,
        skip,
        _id = {
            type: '$type',
            vertical: '$vertical'
        }
    }) {
        const searchList = [];

        const match = {
            $match: {
                type: { $ne: '随便写的' },
                vertical: { $ne: '随便写的' }
            }
        };

        if (score_startAt) {
            const key = 'score';
            const value = score_startAt;

            if (match.$match[key]) {
                match.$match[key].$gte = value;
            } else {
                match.$match[key] = { $gte: value };
            }
        }

        if (score_endAt) {
            const key = 'score';
            const value = score_endAt;

            if (match.$match[key]) {
                match.$match[key].$lt = value;
            } else {
                match.$match[key] = { $lt: value };
            }
        }

        // 如果没有传入评分起始结束值,评分条件为大于0,目的触发索引
        if (!score_startAt && !score_endAt) {
            const key = 'score';
            const value = 0;

            if (match.$match[key]) {
                match.$match[key].$gte = value;
            } else {
                match.$match[key] = { $gte: value };
            }
        }

        if (publishTime_startBy) {
            const key = 'publishTime';
            const value = moment(publishTime_startBy, 'YYYY-MM-DD HH:mm:ss').valueOf();

            if (match.$match[key]) {
                match.$match[key].$gte = value;
            } else {
                match.$match[key] = { $gte: value };
            }
        }

        if (publishTime_endBy) {
            const key = 'publishTime';
            const value = moment(publishTime_endBy, 'YYYY-MM-DD HH:mm:ss').valueOf();

            if (match.$match[key]) {
                match.$match[key].$lte = value;
            } else {
                match.$match[key] = { $lte: value };
            }
        }

        if (areaStr) {
            match.$match.areaStr = areaStr;
        }

        if (siteCode) {
            match.$match.siteCode = siteCode;
        }

        searchList.push(match);

        if (skip) {
            const $skip = { $skip: skip };
            searchList.push($skip);
        }

        if (limit) {
            const $limit = { $limit: limit };
            searchList.push($limit);
        }

        const group = {
            $group: {
                _id,
                count: { $sum: 1 }
            }
        };

        searchList.push(group);

        console.log(JSON.stringify(searchList));

        const res = await this.ctx.model.Stat.aggregate(searchList);
        return res;
    }

遇到的问题

1、数值不对

上面的 getData 是最后改好的,一开始 $skip 在 $limit 的下面

        ...
        if (limit) {
            const $limit = { $limit: limit };
            searchList.push($limit);
        }

        if (skip) {
            const $skip = { $skip: skip };
            searchList.push($skip);
        }
        ...

结果 返回长度 会是 $limit - $skip, 所以 应该先 $skip 在 $limit

2、查询速度慢

根据需求,查询数据是要 按 score 排序的

经试验,$sort 在 $match 之后 就不能 触发索引

而 $sort 放在 $group 之后,不再影响索引,但是 $sort 也没有用了,返回 数据 是没哟 排序的

最后的 解决 方案 是把 索引中 score 放到 索引中的 第一项,这样 返回 数据 默认就是 按 评分 score 排序的

{
    "score": -1,
    "publishTime": -1,
    "areaStr": -1,
    "siteCode": -1,
    "type": -1,
    "vertical": -1,
    "mediaAccountId": -1
}

这样的话,查询 条件里 就一定要有 score 字段,这样 才能 触发 索引

// 如果没有传入评分起始结束值,评分条件为大于0,目的触发索引
if (!score_startAt && !score_endAt) {
    const key = 'score';
    const value = 0;

    if (match.$match[key]) {
        match.$match[key].$gte = value;
    } else {
        match.$match[key] = { $gte: value };
    }
}

最后

其实 该 实现 方法 存在 问题,就是 每个 siteCode 都要 聚合 3 次,目前 有 十几个 siteCode,加起来 就要 聚合 几十次!

就算 每次 聚合 时间 只有 几百 毫秒,加起来 也可能 比较长

幸好 现在 只有 十几万条数据,最长 需要 20s,最短 6、7s

最理想的 方法 是 能通过 1 个 SQL 就把 结果 返回来,不过有点难 实现

后续 再想办法 优化