Requêtes de pagination ‘Top-N’

On a souvent besoin d’afficher la première page d’un résultat (par exemple les 10 premières lignes) et éventuellement les pages suivantes avec des requêtes indépendantes. En attendant la 12c (très bientôt) qui implémentera la syntaxe standard qui commence à se normaliser sur tous les SGBD (OFFSET … FETCH FIRST/NEXT … ROWS) il y a plusieurs techniques possibles mais toutes n’ont pas les mêmes performances.

La démo des différentes possibilités avec plans d’exécution: demo

Déjà commençons avec deux mauvaises idées:

  • Ne pas mettre d’ORDER BY en espérant que le résultat revient trié.
    Sans ORDER BY, l’ordre du résultat n’est pas prédictible. Même s’il on passe par un index. Il faut mettre la clause ORDER BY et le tri sera évité par Oracle si l’index correspondant le permet.
  • Faire une requête normale (avec ORDER BY) et ne limiter le résultat qu’au moment du Fetch.
    Le résultat sera juste, mais les performances moins bonne que si l’optimiseur connait à l’avance le nombre de lignes dont on a besoin.(cf. plans)

rownum
La technique la plus ancienne est de faire le ORDER BY dans une sous-requêtes, puis limiter le résultat avec ROWNUM:
Exemple:
les 10 premiers enregistrements d’un contrat:

select * from (select * from TEST where contract_id=500 order by start_validity  asc) where rownum<=10;

C’est la solution qui fonctionne depuis longtemps et probablement la plus utilisée (par Hibernate par exemple).

Mais la doc 11gR2 précise cependant: The ROW_NUMBER built-in SQL function provides superior support for ordering the results of a query

row_number()
Avec la fonction analytique row_number() on compte les lignes dans une sous-requête puis on limite le résultat.

select /*+ FIRST_ROWS */ * from (select test.*,row_number()over(order by start_validity) rn from test where contract_id=500) where rn<=10  order by start_validity;

A noter:

– je lance la requête en FIRST ROWS car je je souhaite afficher rapidement ces 10 premières lignes. Avec ROWNUM, c’était implicite – le CBO passait en optimisation FIRST_ROWS(10). L’accès par INDEX RANGE SCAN est donc probablement préférable.

– j’ai utilisé FIRST_ROWS et non FIRST_ROWS(10) car sinon dans mon exemple, FULL TABLE SCAN était choisi (peut être un bug en 11.2.0.3). La bonne technique devrait être FIRST_ROWS(n).

– j’ai un index sur (contract_id,start_validity) qui peut être utilisé pour éviter un tri car ce sont des NUMBER. Avec des VARCHAR2 le plan d’exécution peut dépendre des paramètres NLS car l’ordre de l’index (binary) n’est peut-être pas le même que celui de la langue.

Le conseil: tester et vérifier les plans d’exécution et le volume nécessaire au tri sur des données représentatives afin de valider les hints et indexes.