Chargement…

SQL: pourquoi LIMIT ne vous sauvera pas

SQL: pourquoi LIMIT ne vous sauvera pas

GitHub vient de reconstruire son API d'historique des stars. Et derrière l'annonce produit, il y a surtout une leçon SQL que beaucoup de devs n'ont toujours pas intégrée.

2 511 requêtes pour tracer une simple courbe

Pour afficher la courbe de stars d'un repo, des outils comme star-history.com n'avaient qu'une option : paginer l'endpoint stargazers, 100 utilisateurs par page, garder les dates et jeter les identités.

Pour react/react et ses 251 000 stars, ça donnait environ 2 511 requêtes.

Et ce n'était que la partie visible du problème :

  • les listes de stargazers étaient scrapées en masse pour alimenter des campagnes de spam et de phishing
  • un bug de pagination faisait compter toutes les stars du repo à chaque requête, juste pour calculer le nombre de pages
  • sur une seule semaine, près de 37 000 de ces COUNT ont été loggés comme lents (environ 3 secondes en moyenne), et plus de 1 000 ont été tués par les garde-fous de la base

Je trouve ce dernier point savoureux.

Un COUNT(*) sur toute la table à chaque page, c'est le genre d'erreur que je vois dans des back-offices de PME. Pas chez GitHub.

Mais LIMIT ne rend pas la requête moins chère

L'idée évidente, c'était de renvoyer des agrégats plutôt que des personnes : grouper les stars par jour, trier, paginer avec LIMIT et OFFSET.

Sauf que ça ne change quasiment rien au coût. Je m'explique.

En SQL, l'ordre logique d'exécution n'est pas l'ordre dans lequel vous écrivez la requête :

  1. FROM et WHERE : la base sélectionne les lignes
  2. GROUP BY : elle construit tous les groupes
  3. ORDER BY : elle trie tous les groupes
  4. LIMIT / OFFSET : elle découpe le résultat

LIMIT arrive en dernier. Il réduit ce que vous recevez, pas ce que la base a traité.

Et dès qu'il y a un GROUP BY suivi d'un ORDER BY sur l'agrégat, le moteur ne peut pas s'arrêter en route : il doit avoir tous les groupes en main pour savoir lesquels sont les 7 premiers.

OFFSET aggrave le tout, car les lignes sautées sont quand même lues, puis jetées.

-- Version naïve : toutes les stars du repo sont lues, groupées et triées
-- avant que LIMIT ne garde 7 lignes
SELECT DATE(created_at) AS jour, COUNT(*) AS stars
FROM stars
WHERE repository_id = :repo_id
GROUP BY jour
ORDER BY jour DESC
LIMIT 7 OFFSET 210;

-- Version GitHub : le filtre temporel sur un index (repository_id, created_at)
-- réduit les lignes AVANT l'agrégation
SELECT DATE(created_at) AS jour, COUNT(*) AS stars
FROM stars
WHERE repository_id = :repo_id
  AND created_at >= :debut_fenetre
  AND created_at < :fin_fenetre
GROUP BY jour
ORDER BY jour;

Petite nuance quand même : un LIMIT peut être très efficace quand le tri suit un index et qu'il n'y a aucune agrégation. Le moteur lit l'index dans l'ordre et s'arrête après N lignes.

Mais dès que vous groupez ou triez sur une valeur calculée, cette sortie anticipée disparaît.

Résultat côté GitHub : avec l'approche agrégat + LIMIT, lire tout l'historique de react/react en 24 requêtes aurait coûté environ 6 millions de lignes examinées. Chaque requête retraitait les 251 000 stars.

Donc on filtre avant de compter

La version livrée pagine par semaines calendaires. Chaque page est traduite en plage de dates sur created_at, indexée, et la base ne groupe que les lignes de cette plage.

Les chiffres parlent d'eux-mêmes :

  • jusqu'à 30 semaines de comptes quotidiens par requête
  • environ 24 requêtes pour react/react au lieu de 2 511
  • environ 251 000 lignes lues au total, soit chaque star lue une seule fois
  • plus aucune identité dans la réponse

Et en creusant, ils ont trouvé le même anti-pattern sur la page stargazers de github.com. Passée sur le compteur en cache, elle est descendue de plusieurs centaines de requêtes lentes par semaine à zéro.

C'est exactement la logique du keyset pagination : vous ne demandez pas "la page 30", vous demandez "ce qui se trouve entre telle et telle borne". L'index fait le travail, la base ne lit que ce dont elle a besoin.

star-history-avant-apres.svg

FAQ

Pourquoi l'ancienne API d'historique des stars posait un problème de sécurité ?

Elle exposait les identités des utilisateurs ayant starré un repo, données qui étaient scrapées en masse pour alimenter des campagnes de spam et de phishing. La nouvelle version ne renvoie plus que des agrégats temporels, sans aucune identité.

Pourquoi ajouter un LIMIT ne suffit pas à accélérer une requête SQL ?

Parce que LIMIT s'applique en dernier dans l'ordre d'exécution logique d'une requête, après le GROUP BY et le ORDER BY. Si la base doit trier des groupes agrégés, elle doit d'abord tous les calculer avant de pouvoir n'en garder que quelques-uns.

Dans quel cas un LIMIT reste-t-il réellement efficace ?

Quand le tri suit directement un index existant et qu'il n'y a aucune agrégation à calculer. Le moteur peut alors lire l'index dans l'ordre et s'arrêter dès qu'il a trouvé le nombre de lignes demandé.

Quelle technique GitHub a utilisée pour résoudre ce problème de pagination ?

Ils paginent par semaines calendaires en convertissant chaque page en une plage de dates filtrée sur un index, avant l'agrégation. C'est le principe du keyset pagination : on demande les lignes entre deux bornes précises plutôt qu'une page numérotée avec OFFSET.

Quel gain concret cette refonte a-t-elle apporté pour un repo comme react/react ?

Le nombre de requêtes est passé d'environ 2511 à seulement 24, et le volume de lignes lues est tombé à 251000, soit chaque star lue une seule fois au lieu d'être recomptée à chaque page. La même page sur github.com est passée de plusieurs centaines de requêtes lentes par semaine à zéro.

Cette nouvelle approche élimine-t-elle tout risque de requête lente ?

Non, GitHub reconnaît qu'une semaine anormalement dense, par exemple un repo qui explose sur Hacker News, peut encore produire une requête lourde puisque la plage temporelle borne la période mais pas le volume de stars à l'intérieur. C'est une limite assumée plutôt qu'un problème résolu à 100 pour cent.


Poursuivre la lecture

AWS rachète DuckLabs (DuckDB) News
#aws#duckdb#analytics

AWS rachète DuckLabs (DuckDB)

Amazon rachète DuckLabs, l'équipe derrière DuckDB. Licence MIT intacte grâce à la fondation, mais le rachat change-t-il vraiment la donne pour ce moteur analytique embarqué ?

Alexandre P.
Nocodb: entre airtable et une base de données News
#nocodb#postgres#airtable

Nocodb: entre airtable et une base de données

NocoDB s'installe en une commande pour remplacer Airtable, mais la licence a changé début 2026. On regarde ce que ça change vraiment avant de le déployer chez un client.

Alexandre P.