Design von Paginierungsseiten in API

Es kann für eine API schwierig sein, alle Ergebnisse einer Abfrage zurückzugeben, insbesondere wenn es Tausende davon gibt. Dies führt zu einer Belastung des Servers, des Clients und des Netzwerks und ist häufig nicht erforderlich. Deshalb haben wir uns eine Paginierung ausgedacht.



Die übliche Art des Paging ist der Versatz oder die Seitenzahl. Sie stellen eine Anfrage wie folgt:



GET /api/products?page=10
{"items": [...100 products]}
      
      





und dann das:



GET /api/products?page=11
{"items": [...another 100 products]}
      
      





Bei einer einfachen Verschiebung stellt sich heraus ?offset=1000



und ?offset=1100



- die gleichen Eier, nur im Profil. Hier gehen wir entweder direkt zur SQL-Abfrage des Typs OFFSET 1000 LIMIT 100



oder multiplizieren mit der Seitengröße (Wert LIMIT



). Dies ist ohnehin keine optimale Lösung, da jede Datenbank diese 1000 Zeilen überspringen muss. Und um sie zu überspringen, müssen Sie sie identifizieren. Es spielt keine Rolle, ob es sich um PostgreSQL, ElasticSearch oder MongoDB handelt, es muss sie bestellen, neu berechnen und wegwerfen.



Dies ist unnötige Arbeit. Es wiederholt sich jedoch immer wieder, da dieses Design einfach zu implementieren ist. Sie ordnen Ihre API direkt einer Datenbankanforderung zu.



Was ist dann zu tun? Wir konnten sehen, wie die Datenbanken funktionieren! Sie haben das Konzept eines Cursors - es ist ein Zeiger auf eine Zeichenfolge. So können Sie die Datenbank kann sagen : „Gib mir 100 Zeilen nach hinten dies .“ Eine solche Abfrage ist für die Datenbank viel praktischer, da mit hoher Wahrscheinlichkeit eine Zeile durch ein Feld mit einem Index identifiziert wird. Und Sie müssen diese Zeilen nicht abrufen und überspringen, sondern gehen direkt daran vorbei.



Beispiel:



GET /api/products
{"items": [...100 products],
 "cursor": "qWe"}
      
      





Die API gibt eine (undurchsichtige) Zeichenfolge zurück, mit der die nächste Seite abgerufen werden kann:



GET /api/products?cursor=qWe
{"items": [...100 products],
 "cursor": "qWr"}
      
      





In Bezug auf die Implementierung gibt es viele Möglichkeiten. In der Regel haben Sie einige Abfragekriterien, z. B. eine Produkt-ID. In diesem Fall codieren Sie es mit einem reversiblen Algorithmus (z. B. Hash-IDs ). Und wenn Sie eine Abfrage mit einem Cursor erhalten, dekodieren Sie sie und generieren eine Abfrage wie WHERE id > :cursor LIMIT 100



.



Kleiner Leistungsvergleich. Hier ist das Ergebnis des Offsets: Und hier ist das Ergebnis der Operation : Eine Differenz von mehreren Größenordnungen! Die tatsächlichen Zahlen hängen natürlich von der Größe der Tabelle, der Filter und der Speicherimplementierung ab. Hier ist ein großartiger Artikel



=# explain analyze select id from product offset 10000 limit 100;

QUERY PLAN

---------------------------------------------------------------------------------------------------------------------------------

Limit (cost=1114.26..1125.40 rows=100 width=4) (actual time=39.431..39.561 rows=100 loops=1)

-> Seq Scan on product (cost=0.00..1274406.22 rows=11437243 width=4) (actual time=0.015..39.123 rows=10100 loops=1)

Planning Time: 0.117 ms

Execution Time: 39.589 ms








where







=# explain analyze select id from product where id > 10000 limit 100;

QUERY PLAN

------------------------------------------------------------------------------------------------------------------------------

Limit (cost=0.00..11.40 rows=100 width=4) (actual time=0.016..0.067 rows=100 loops=1)

-> Seq Scan on product (cost=0.00..1302999.32 rows=11429082 width=4) (actual time=0.015..0.052 rows=100 loops=1)

Filter: (id > 10000)

Planning Time: 0.164 ms

Execution Time: 0.094 ms








Weitere technische Details finden Sie auf Folie 42 für einen Leistungsvergleich.





Natürlich fragt niemand Produkte nach ID ab - sie werden normalerweise nach Relevanz abgefragt (und dann nach ID als entscheidendem Parameter ). In der realen Welt erfordert die Auswahl einer Lösung die Betrachtung spezifischer Daten. Anfragen können nach Kennung sortiert werden (da sie monoton ansteigen). Auf diese Weise können auch Artikel aus der Liste zukünftiger Einkäufe sortiert werden - zum Zeitpunkt der Erstellung der Liste. In unserem Fall werden die Produkte von ElasticSearch geladen, das natürlich einen solchen Cursor unterstützt.



Der Nachteil ist, dass Sie mit der zustandslosen API keinen Link zur vorherigen Seite erstellen können. Bei der Benutzerpaginierung führt kein Weg an diesem Problem vorbei. Wenn es also wichtig ist, Schaltflächen für die vorherige / nächste Seite und "Direkt zu Seite 10 gehen" zu haben, müssen Sie die alte Methode verwenden. In anderen Fällen kann die By-Cursor-Methode die Leistung erheblich verbessern, insbesondere bei sehr großen Tabellen mit sehr tiefer Paginierung.



All Articles