Il arrive parfois que nous devions rechercher des données associées à un ensemble de clés, jusqu'à ce que nous atteignions le nombre total d'enregistrements souhaité..
Un exemple trĂšs « vivant » serait de sortir les 20 tĂąches les plus anciennes, figurant dans la liste des employĂ©s (par exemple, au sein d'un mĂȘme dĂ©partement). Ce type de demande est assez frĂ©quent pour divers tableaux de bord de gestion prĂ©sentant des rĂ©sumĂ©s des domaines de travail.

Dans cet article, nous examinerons l'implĂ©mentation sur PostgreSQL d'une solution « naĂŻve » Ă ce type de problĂšme, d'un algorithme « plus intelligent » et d'un algorithme complexe « en boucle » en SQL avec une condition de sortie basĂ©e sur les donnĂ©es trouvĂ©es,qui peut ĂȘtre utile tant pour le dĂ©veloppement personnel que pour l'application dans d'autres cas similaires.
Prenons un jeu de données d'exemple provenant de . Afin que les enregistrements affichés ne « sautent » pas d'une fois à l'autre en cas de valeurs de tri identiques, nous allons étendre l'index thématique par l'ajout de la clé primaire,. Cela lui conférera immédiatement son unicité et garantira l'unité de l'ordre de tri :
CREATE INDEX ON task(owner_id, task_date, id);
-- et nous allons supprimer l'ancien
DROP INDEX task_owner_id_task_date_idx;Tel qu'on l'entend, tel qu'on l'écrit.
Pour commencer, esquissons la variante de requĂȘte la plus simple, en passant les ID des exĂ©cutants :
SELECT
*
FROM
task
WHERE
owner_id = ANY('{1,2,4,8,16,32,64,128,256,512}'::integer[])
ORDER BY
task_date, id
LIMIT 20; 
C'est un peu triste â nous avons demandĂ© seulement 20 enregistrements, mais l'Index Scan nous a renvoyĂ© 960 lignes, qui ont ensuite dĂ» ĂȘtre triĂ©es⊠Essayons de lire un peu moins.
unnest + ARRAY
La premiĂšre rĂ©flexion qui nous aidera â si nous avons besoin de juste 20 enregistrements triĂ©s, alors il suffit de lire pas plus de 20 triĂ©s dans le mĂȘme ordre pour chaque clĂ©. Heureusement, nous disposons d'un index appropriĂ© (owner_id, task_date, id). Nous utiliserons le mĂȘme mĂ©canisme d'extraction et de « dĂ©ploiement en colonnes »
de l'enregistrement complet de la table , comme dans. Nous appliquerons également le repli en tableau en utilisant la fonction ARRAY() WITH T AS ( SELECT unnest(ARRAY( SELECT t FROM task t WHERE owner_id = unnest ORDER BY task_date, id LIMIT 20 -- nous limitons ici... )) r FROM unnest('{1,2,4,8,16,32,64,128,256,512}'::integer[]) ) SELECT (r).* FROM T ORDER BY (r).task_date, (r).id LIMIT 20; -- ... et ici - aussi:
Oh, c'est dĂ©jĂ beaucoup mieux ! 
40 % plus rapide, et 4,5 fois moins de donnĂ©es ont dĂ» ĂȘtre lues. La matĂ©rialisation des enregistrements de table via CTE
Je souligne quedans certains cas dans certains cas essayer de travailler directement avec les champs d'enregistrement aprĂšs leur recherche dans une sous-requĂȘte, sans les « envelopper » dans un CTE peut conduire Ă une « multiplication » de l'InitPlan proportionnellement au nombre de ces champs :
SELECT
((
SELECT
t
FROM
task t
WHERE
owner_id = 1
ORDER BY
task_date, id
LIMIT 1
).*);Résultat (coût=4.77..4.78 lignes=1 largeur=16) (temps réel=0.063..0.063 lignes=1 boucles=1)
Tampons : coup partagé=16
InitPlan 1 (retourne $0)
-> Limite (coût=0.42..1.19 lignes=1 largeur=48) (temps réel=0.031..0.032 lignes=1 boucles=1)
Tampons : coup partagé=4
-> Recherche d'index utilisant task_owner_id_task_date_id_idx sur la tùche t (coût=0.42..387.57 lignes=500 largeur=48) (temps réel=0.030..0.030 lignes=1 boucles=1)
Condition d'index : (owner_id = 1)
Tampons : coup partagé=4
InitPlan 2 (retourne $1)
-> Limite (coût=0.42..1.19 lignes=1 largeur=48) (temps réel=0.008..0.009 lignes=1 boucles=1)
Tampons : coup partagé=4
-> Recherche d'index utilisant task_owner_id_task_date_id_idx sur la tùche t_1 (coût=0.42..387.57 lignes=500 largeur=48) (temps réel=0.008..0.008 lignes=1 boucles=1)
Condition d'index : (owner_id = 1)
Tampons : coup partagé=4
InitPlan 3 (retourne $2)
-> Limite (coût=0.42..1.19 lignes=1 largeur=48) (temps réel=0.008..0.008 lignes=1 boucles=1)
Tampons : coup partagé=4
-> Recherche d'index utilisant task_owner_id_task_date_id_idx sur la tùche t_2 (coût=0.42..387.57 lignes=500 largeur=48) (temps réel=0.008..0.008 lignes=1 boucles=1)
Condition d'index : (owner_id = 1)
Tampons : coup partagé=4
InitPlan 4 (retourne $3)
-> Limite (coût=0.42..1.19 lignes=1 largeur=48) (temps réel=0.009..0.009 lignes=1 boucles=1)
Tampons : coup partagé=4
-> Recherche d'index utilisant task_owner_id_task_date_id_idx sur la tùche t_3 (coût=0.42..387.57 lignes=500 largeur=48) (temps réel=0.009..0.009 lignes=1 boucles=1)
Condition d'index : (owner_id = 1)
Tampons : coup partagé=4
Le mĂȘme enregistrement a Ă©tĂ© « recherchĂ© » 4 fois⊠Jusqu'Ă PostgreSQL 11, ce comportement se produisait rĂ©guliĂšrement, et la solution consistait à « envelopper » dans un CTE, ce qui est une limite claire pour l'optimiseur dans ces versions.
Accumulateur récursif
Dans la version prĂ©cĂ©dente, nous avons lu au total 200 lignes pour obtenir les 20 nĂ©cessaires. Ce n'est plus 960, mais encore moins â est-ce possible ?
Essayons de profiter de notre connaissance selon laquelle nous avons seulement besoin de 20 enregistrements. Autrement dit, nous allons itérer la lecture des données uniquement jusqu'à atteindre le nombre dont nous avons besoin.
Ătape 1 : liste de dĂ©part
Il est Ă©vident que notre liste « cible » de 20 enregistrements doit commencer par les « premiers » enregistrements selon l'un de nos clĂ©s owner_id. Donc, trouvons d'abord ces « tout premiers » selon chacune des clĂ©s et ajoutons-les Ă la liste, en les triant dans l'ordre que nous souhaitons â (task_date, id).

Ătape 2 : trouver les « enregistrements suivants »
Maintenant, si nous prenons le premier enregistrement de notre liste et commençons à « avancer » dans l'index en conservant la clé owner_id, tous les enregistrements trouvés seront précisément les suivants dans le résultat de la sélection. Bien sûr, seulement tant que nous n'avons pas franchi la clé d'application deuxiÚme enregistrement dans la liste.
Si nous avons franchi le deuxiĂšme enregistrement, alors le dernier enregistrement lu doit ĂȘtre ajoutĂ© Ă la liste Ă la place du premier (avec le mĂȘme owner_id), aprĂšs quoi la liste est Ă nouveau triĂ©e.

C'est-à -dire que nous avons toujours une seule entrée par clé dans la liste (si les enregistrements sont épuisés et que nous n'avons pas franchi, le premier enregistrement de la liste disparaßt simplement et rien n'est ajouté), et ils sont toujours triés dans l'ordre croissant de la clé d'application (task_date, id).

Ătape 3 : filtrer et « dĂ©plier » les enregistrements
Dans certaines lignes de notre sĂ©lection rĂ©cursive, certains enregistrements rv sont dupliquĂ©s â d'abord nous trouvons ceux qui « franchissent la frontiĂšre du 2e enregistrement de la liste », puis les plaçons comme 1er de la liste. Donc, la premiĂšre apparition doit ĂȘtre filtrĂ©e.
La terrible requĂȘte finale
AVEC RĂCURSIF T COMME (
-- #1 : nous insérons les "premiers" enregistrements par rapport à chaque clé de l'ensemble
AVEC wrap COMME ( -- "matérialisons" les enregistrements pour éviter que l'accÚs aux champs provoque une multiplicité d'InitPlan/SubPlan
AVEC T COMME (
SĂLECTIONNER
(
SĂLECTIONNER
r
DE
task r
OĂ
owner_id = unnest
COMMANDER PAR
task_date, id
LIMIT 1
) r
DE
unnest('{1,2,4,8,16,32,64,128,256,512}'::integer[])
)
SĂLECTIONNER
array_agg(r ORDER BY (r).task_date, (r).id) liste -- nous trions la liste dans l'ordre souhaité
DE
T
)
SĂLECTIONNER
liste
, liste[1] rv
, FAUX not_cross
, 0 taille
DE
wrap
UNION ALL
-- #2 : nous lisons les enregistrements du 1er ordre de la clé jusqu'à ce que nous dépassions l'enregistrement du 2Úme
SĂLECTIONNER
CAS
-- si rien n'est trouvé pour la clé du 1er enregistrement
QUAND X._r EST NON DISTINCT DE NULL ALORS
T.liste[2:] -- nous l'éliminons de la liste
-- si nous N'AVONS PAS croisé la clé appliquée du 2Úme enregistrement
QUAND X.not_cross ALORS
T.liste -- simplement nous continuons la mĂȘme liste sans modifications
-- si le 2Úme enregistrement n'est déjà plus dans la liste
QUAND T.liste[2] EST NULL ALORS
-- simplement renvoyer une liste vide
'{}'
-- nous re-trions le dictionnaire en supprimant le 1er enregistrement et en ajoutant le dernier trouvé
SINON (
SĂLECTIONNER
coalesce(T.liste[2] || array_agg(r ORDER BY (r).task_date, (r).id), '{}')
DE
unnest(T.liste[3:] || X._r) r
)
FIN
, X._r
, X.not_cross
, T.taille + X.not_cross::integer
DE
T
, LATERAL(
AVEC wrap COMME ( -- "matérialisons" l'enregistrement
SĂLECTIONNER
CAS
-- si nous avons en fait "dépassé" le 2Úme enregistrement
QUAND PAS T.not_cross
-- alors l'enregistrement nécessaire est le premier de la liste
ALORS T.liste[1]
SINON ( -- si nous n'avons pas croisé, alors la clé est restée comme dans l'enregistrement précédent - elle s'en inspire
SĂLECTIONNER
_r
DE
task _r
OĂ
owner_id = (rv).owner_id ET
(task_date, id) > ((rv).task_date, (rv).id)
COMMANDER PAR
task_date, id
LIMIT 1
)
FIN _r
)
SĂLECTIONNER
_r
, CAS
-- si le 2Úme enregistrement n'est déjà plus dans la liste, mais que nous avons trouvé quelque chose
QUAND liste[2] EST NULL ET _r EST DISTINCT DE NULL ALORS
VRAI
SINON -- rien trouvé ou "dépassé"
coalesce(((_r).task_date, (_r).id) < ((liste[2]).task_date, (liste[2]).id), FAUX)
FIN not_cross
DE
wrap
) X
OĂ
T.taille < 20 ET -- nous limitons ici la quantité
T.liste EST DISTINCT DE '{}' -- ou jusqu'à ce que la liste soit épuisée
)
-- #3 : "développons" les enregistrements - l'ordre est garanti par la construction
SĂLECTIONNER
(rv).*
DE
T
OĂ
not_cross; -- nous prenons uniquement les enregistrements "non croisĂ©s" 
Ainsi, nous avons Ă©changĂ© 50 % des lectures de donnĂ©es contre 20 % du temps d'exĂ©cutionC'est-Ă -dire que si vous avez des raisons de penser que la lecture pourrait ĂȘtre longue (par exemple, les donnĂ©es ne sont souvent pas en cache et doivent ĂȘtre rĂ©cupĂ©rĂ©es sur le disque), vous pouvez ainsi dĂ©pendre moins de la lecture.
Dans tous les cas, le temps d'exĂ©cution s'est avĂ©rĂ© meilleur que dans la premiĂšre version « naĂŻve ». Mais lequel de ces 3 variantes utiliser â c'est Ă vous de choisir.
Source : habr.com
