MongoDB

Wer hat gewonnen? Ranking in der Aggregation Pipeline

Die $setWindowFields-Stage verfügt über verschiedene Möglichkeiten, die Position eines Werts im Vergleich zu anderen Dokumenten zu bestimmen. Ausgehend von folgenden Demodaten werfen wir einen Blick auf die Unterschiede:

[{
  "ts": ISODate("2023-09-20T00:00:00.000Z"),
  "value": 100
},
{
  "ts": ISODate("2023-09-21T00:00:00.000Z"),
  "value": 150
},
{
  "ts": ISODate("2023-09-24T00:00:00.000Z"),
  "value": 50
},
{
  "ts": ISODate("2023-09-26T00:00:00.000Z"),
  "value": 10
},
{
  "ts": ISODate("2023-09-27T00:00:00.000Z"),
  "value": 100
}]

Wichtig ist beim Ranking die Sortierung der Dokumente; während in anderen Fällen oft der Zeitstempel verwendet wird, wird beim Ranking nach dem Platzierungskriterium sortiert:

[
  {
    $setWindowFields: {
      partitionBy: null,
      sortBy: { value: -1 },
      output: {
        position: {
          $documentNumber: {},
        },
      },
    },
  },
]

Obiges Beispiel sortiert die Dokumente absteigend nach dem Wert und verwendet die $documentNumber-Funktion, um die Dokumente zu nummerieren. Aufgrund der Einfachheit der Funktion werden auch keine weiteren Parameter angegeben. Bei mehrfach vorkommenden Werten wird die Nummerierung einfach fortgesetzt.

Dies führt zu folgendem Ergebnis

[{
  "ts": ISODate("2023-09-21T00:00:00.000Z"),
  "value": 150,
  "position": 1
},
{
  "ts": ISODate("2023-09-20T00:00:00.000Z"),
  "value": 100,
  "position": 2
},
{
  "ts": ISODate("2023-09-27T00:00:00.000Z"),
  "value": 100,
  "position": 3
},
{
  "ts": ISODate("2023-09-24T00:00:00.000Z"),
  "value": 50,
  "position": 4
},
{
  "ts": ISODate("2023-09-26T00:00:00.000Z"),
  "value": 10,
  "position": 5
}]

$rank und $denseRank

Will man eine Platzvergabe, bei denen gleiche Werte zur gleichen Platzierung führen, kann man die Funktionen $rank oder $denseRank nutzen. Beide Funktionen vergeben für gleiche Werte die gleiche Platzierung. Der Unterschied ist, wie die Nummerierung anschließend fortgesetzt wird:

  • $rank lässt anschließend Lücken.
  • $denseRank setzt die Nummerierung anschließend ohne Lücken fort.

In einer Aggregation-Pipeline werden die Funktionen folgendermaßen eingesetzt ($rank könnte ohne weiteres durch $denseRank ersetzt werden, falls gewünscht):

[
  {
    $setWindowFields: {
      partitionBy: null,
      sortBy: { value: -1 },
      output: {
        position: {
          $rank: {},
        },
      },
    },
  },
]

Das Ergebnis bei Einsatz von $rank ist folgendes:

[{
  "ts": ISODate("2023-09-21T00:00:00.000Z"),
  "value": 150,
  "position": 1
},
{
  "ts": ISODate("2023-09-20T00:00:00.000Z"),
  "value": 100,
  "position": 2
},
{
  "ts": ISODate("2023-09-27T00:00:00.000Z"),
  "value": 100,
  "position": 2
},
{
  "ts": ISODate("2023-09-24T00:00:00.000Z"),
  "value": 50,
  "position": 4
},
{
  "ts": ISODate("2023-09-26T00:00:00.000Z"),
  "value": 10,
  "position": 5
}]

$denseRank hingegen vergibt die Position 3 und 4 im Anschluss an die beiden zweiten Plätze:

[{
  "ts": ISODate("2023-09-21T00:00:00.000Z"),
  "value": 150,
  "position": 1
},
{
  "ts": ISODate("2023-09-20T00:00:00.000Z"),
  "value": 100,
  "position": 2
},
{
  "ts": ISODate("2023-09-27T00:00:00.000Z"),
  "value": 100,
  "position": 2
},
{
  "ts": ISODate("2023-09-24T00:00:00.000Z"),
  "value": 50,
  "position": 3
},
{
  "ts": ISODate("2023-09-26T00:00:00.000Z"),
  "value": 10,
  "position": 4
}]

Und welcher ist der schlechteste Wert?

Mit obiger Pipeline sind wir in der Lage, die Position eines Werts zu bestimmen. Die Bestimmung des besten Werts ist also kein Problem. Wollen wir zusätzlich markieren können, wenn ein Wert der schlechteste war, dann können wir das durch die folgende Ergänzung der Aggregation Pipeline erreichen:

[
  {
    $setWindowFields: {
      partitionBy: null,
      sortBy: { value: -1 },
      output: {
        position: {
          $denseRank: {},
        },
        worst: {
          $last: "$value",
          window: {
            documents: ["unbounded", "unbounded"],
          },
        },
      },
    },
  },
  {
    $set: {
      isWorst: {
        $eq: ["$value", "$worst"],
      },
    },
  },
  {
    $unset: "worst"
  }
]

Die Pipeline bestimmt in der $setWindowFields-Stage neben der Platzierung über $denseRank auch den schlechtesten Wert. In der folgenden $set-Stage wird anschließend der aktuelle Wert mit dem schlechtesten verglichen und das Ergebnis im isWorst-Feld abgespeichert. Dieser Ansatz funktioniert auch in Szenarien, in denen es mehrere Dokumente mit den schlechtesten Werten gibt und führt zu folgender Ausgabe:

[{
  "ts": ISODate("2023-09-21T00:00:00.000Z"),
  "value": 150,
  "position": 1,
  "isWorst": false
},
{
  "ts": ISODate("2023-09-20T00:00:00.000Z"),
  "value": 100,
  "position": 2,
  "isWorst": false
},
{
  "ts": ISODate("2023-09-27T00:00:00.000Z"),
  "value": 100,
  "position": 2,
  "isWorst": false
},
{
  "ts": ISODate("2023-09-24T00:00:00.000Z"),
  "value": 50,
  "position": 3,
  "isWorst": false
},
{
  "ts": ISODate("2023-09-26T00:00:00.000Z"),
  "value": 10,
  "position": 4,
  "isWorst": true
}]

Fazit

Die obigen Beispiele zeigen, dass das Ranking von Werten in einer Gruppe von Dokumenten mit $setWindowFields äußerst einfach ist. Man kann die obigen Stages selbstverständlich noch mit passenden Partitions-Einstellungen versehen, um die Ranking jeweils innerhalb einer Gruppe von Dokumenten vorzunehmen.


MongoDB NoSql MongoDB Driver for C#
Markus Wildgruber
Markus Wildgruber

Geschäftsführer

  • CloudArchitect
  • DeveloperCoach
  • DotNet
  • MongoDB
  • Angular