我正在使用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文档的情况下,它如何变得如此缓慢。