Mongodb聚合框架查询性能

时间:2020-04-03 15:09:28

标签: mongodb performance aggregation-framework

我正在使用MongoDB的聚合框架进行多个查询,以获取有关某些比赛情况的统计信息。 效果很好,但是现在我的数据库中有10万多个文档,查询速度非常慢。

每个查询需要1300毫秒到3900毫秒。所以我的整个页面大约需要12秒钟才能显示。

这是查询的代码:

// define a container for information relevant to each row
public sealed class SqlResultSetRow : IEnumerable<(string fieldName, Type fieldType, object fieldValue)>
{
    private readonly (string fieldName, Type fieldType, object fieldValue)[] m_fields;

    public int ResultSetIndex { get; }

    public SqlResultSetRow((string, Type, object)[] fields, int resultSetIndex) {
        m_fields = fields;

        ResultSetIndex = resultSetIndex;
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public IEnumerator<(string fieldName, Type fieldType, object fieldValue)> GetEnumerator() {
        var rows = m_fields;

        foreach (var row in rows) {
            yield return row;
        }
    }
    public object GetFieldName(int fieldOffset) => m_fields[fieldOffset].fieldName;
    public object GetFieldValue(int fieldOffset) => m_fields[fieldOffset].fieldValue;
}

// define a class to hold our generic method(s)
public static class SqlClientExtensions
{
    // implement a refactored version of the original method
    public static async IAsyncEnumerable<T> ProcessRows<T>(this SqlCommand command, Func<SqlResultSetRow, CancellationToken, ValueTask<T>> rowCallback, CommandBehavior commandBehavior = CommandBehavior.SequentialAccess, [EnumeratorCancellation] CancellationToken cancellationToken = default) {
        using (var dataReader = await command.ExecuteReaderAsync(commandBehavior, cancellationToken)) {
            var resultSetIndex = 0;

            do {
                var fieldCount = dataReader.FieldCount;
                var fieldNames = new string[fieldCount];
                var fieldTypes = new Type[fieldCount];

                for (var i = 0; (i < fieldCount); ++i) {
                    fieldNames[i] = dataReader.GetName(i);
                    fieldTypes[i] = dataReader.GetFieldType(i);
                }

                while (await dataReader.ReadAsync(cancellationToken)) {
                    var fields = new (string, Type, object)[fieldCount];

                    for (var i = 0; (i < fieldCount); ++i) {
                        fields[i] = (fieldNames[i], fieldTypes[i], dataReader.GetValue(i));
                    }

                    yield return await rowCallback(new SqlResultSetRow(fields, resultSetIndex), cancellationToken);
                }
            } while (await dataReader.NextResultAsync(cancellationToken));
        }
    }
}

class Program
{
    // a minimal implementation of a rowCallBack function
    public static async ValueTask<ExpandoObject> OnProcessRow(SqlResultSetRow resultSetRow, CancellationToken cancellationToken) {
        var rowValue = (new ExpandoObject() as IDictionary<string, object>);

        foreach (var field in resultSetRow) {
            rowValue[field.fieldName] = field.fieldValue;
        }

        return (rowValue as ExpandoObject);
    }

    // put everything together
    static async Task Main(string[] args) {
        try {
            var programTimeout = TimeSpan.FromMinutes(3);
            var cancellationTokenSource = new CancellationTokenSource(programTimeout);

            using (var connection = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=master;Integrated Security=True;"))
            using (var command = connection.CreateCommand()) {
                command.CommandText = "select 1 as [a];";
                command.CommandType = CommandType.Text;

                await connection.OpenAsync(cancellationToken: cancellationTokenSource.Token);

                await foreach (dynamic row in command.ProcessRows(rowCallback: OnProcessRow, cancellationToken: cancellationTokenSource.Token)) {
                    Console.WriteLine($"a: {row.a}");
                }
            }
        }
        catch (Exception e) {
            // do something with exception here

            throw;
        }
    }
}

这是我的使用方式:

class MatchRepository {
  static get inject() {
    return ['App/Models/Match']
  }

  constructor(Match) {
    this.Match = Match
  }

  /**
   * Basic matchParams used in a lot of requests
   * @param puuid of the summoner
   */
  _matchParams(puuid) {
    return {
      summoner_puuid: puuid,
      result: { $not: { $eq: 'Remake' } },
      gamemode: { $nin: [800, 810, 820, 830, 840, 850] },
      season: this.season ? this.season : { $exists: true }
    }
  }

  /**
   * Build the aggregate mongo query
   * @param {Number} puuid 
   * @param {Object} matchParams 
   * @param {Array} intermediateSteps 
   * @param {*} groupId 
   * @param {Object} groupParams 
   * @param {Array} finalSteps 
   */
  _aggregate(puuid, matchParams, intermediateSteps, groupId, groupParams, finalSteps) {
    return this.Match.query().aggregate([
      {
        $match: {
          ...this._matchParams(puuid),
          ...matchParams
        }
      },
      ...intermediateSteps,
      {
        $group: {
          _id: groupId,
          count: { $sum: 1 },
          wins: {
            $sum: {
              $cond: [{ $eq: ['$result', 'Win'] }, 1, 0]
            }
          },
          losses: {
            $sum: {
              $cond: [{ $eq: ['$result', 'Fail'] }, 1, 0]
            }
          },
          ...groupParams
        },
      },
      ...finalSteps
    ])
  }

  /**
   * Get Summoner's statistics for the N most played champions
   * @param puuid of the summoner
   * @param limit number of champions to fetch
   */
  championStats(puuid, limit = 5) {
    const groupParams = {
      champion: { $first: '$champion' },
      kills: { $sum: '$stats.kills' },
      deaths: { $sum: '$stats.deaths' },
      assists: { $sum: '$stats.assists' },
    }
    const finalSteps = [
      { $sort: { 'count': -1, 'champion.name': 1 } },
      { $limit: limit }
    ]
    return this._aggregate(puuid, {}, [], '$champion.id', groupParams, finalSteps)
  }

  /**
   * Get Summoner's statistics for all played champion classes
   * @param puuid of the summoner
   */
  championClassStats(puuid) {
    const groupId = { '$arrayElemAt': ['$champion.roles', 0] }
    return this._aggregate(puuid, {}, [], groupId, {}, [])
  }

  /**
   * Get Summoner's statistics for all played modes
   * @param puuid of the summoner
   */
  gamemodeStats(puuid) {
    return this._aggregate(puuid, {}, [], '$gamemode', {}, [])
  }

  /**
   * Get global Summoner's statistics
   * @param puuid of the summoner
   */
  globalStats(puuid) {
    const groupParams = {
      time: { $sum: '$time' },
      kills: { $sum: '$stats.kills' },
      deaths: { $sum: '$stats.deaths' },
      assists: { $sum: '$stats.assists' },
      minions: { $sum: '$stats.minions' },
      vision: { $sum: '$stats.vision' },
      kp: { $avg: '$stats.kp' },
    }
    return this._aggregate(puuid, {}, [], null, groupParams, [])
  }

  /**
   * Get Summoner's statistics for the 5 differnt roles
   * @param puuid of the summoner
   */
  roleStats(puuid) {
    const matchParams = {
      role: { $not: { $eq: 'NONE' } }
    }
    const finalSteps = [
      {
        $project: {
          role: '$_id',
          count: '$count',
          wins: '$wins',
          losses: '$losses',
        }
      }
    ]
    return this._aggregate(puuid, matchParams, [], '$role', {}, finalSteps)
  }
  /**
   * Get Summoner's played seasons
   * @param puuid of the summoner
   */
  seasons(puuid) {
    this.season = null

    return this.Match.query().aggregate([
      {
        $match: {
          ...this._matchParams(puuid),
        }
      },
      {
        $group: { _id: '$season' }
      },
    ])
  }

  /**
   * Get Summoner's mates list
   * @param puuid of the summoner
   */
  mates(puuid) {
    const intermediateSteps = [
      { $sort: { 'gameId': -1 } },
      { $unwind: '$allyTeam' },
    ]
    const groupParams = {
      account_id: { $first: '$account_id' },
      name: { $first: '$allyTeam.name' },
      mateId: { $first: '$allyTeam.account_id' },
    }
    const finalSteps = [
      {
        '$addFields': {
          'idEq': { '$eq': ['$mateId', '$account_id'] }
        }
      },
      {
        $match: {
          'idEq': false,
          'count': { $gte: 2 }
        },
      },
      { $sort: { 'count': -1, 'name': 1 } },
      { $limit: 15 },
    ]
    return this._aggregate(puuid, {}, intermediateSteps, '$allyTeam.account_id', groupParams, finalSteps)
  }
}

module.exports = MatchRepository

怎么会这么糟?我使用的是5 $ VPS,但是在数据库中有3万个文档时,速度确实很快。我不明白在“仅”增加70k文档的情况下,它如何变得如此缓慢。

0 个答案:

没有答案