dimanche 20 mai 2012

Fonctions postgresql, heuristiques

Reprenons l'exemple d'hier, mais cette fois-ci avec 2 tables de 10M lignes chacune, et une requête un tout petit peu plus rigolote, puisqu'elle limite la jointure aux 10 premières lignes de la première table. Pas de surprises, le planificateur de requêtes voit que la limite permet d'aller chercher les éléments de la deuxième table via leur index, et propose une requête efficace, avec un temps d'exécution de 12 ms.

create table data1(a integer not null);
insert into data1 select generate_series(1, 10000000);
create index data1_idx on data1(a);
create table data2(a integer not null);
insert into data2 select generate_series(1, 10000000);
create index data2_idx on data2(a);

select *
from (select * from data1 limit 10) d1
join data2 d2 on d1.a = d2.a

Soyons un peu plus pervers, et prenons maintenant la limite depuis une autre table, empêchant ainsi le planificateur de voir que d1 peut être en grande partie ignorée. Ouch! Le plan en prend un coup, et le temps d'exécution explose à presque 4 secondes.

create table data_limit(l integer not null);
insert into data_limit values(10);

select *
from (select * from data1 limit (select * from data_limit)) d1
join data2 d2 on d1.a = d2.a

Si l'on sait que notre limite va être la plupart du temps petite, comment peut-on aider notre planificateur à choisir la bonne requête? Une possibilité est de passer par une fonction, et de lui passer le "hint" qui va bien, c'est à dire en indiquant le nombre de lignes auxquelles l'on s'attend via le mot-clé "rows" (notez que par défaut, Postgres suppose que la fonction renvoie 1000 lignes). Et tout d'un coup, le plan redevient raisonnable, et la requête rapide:

create function get_limited_data1()
returns table(a integer)
rows 10
as
$$
 select * from data1 limit (select * from data_limit);
$$ language sql;

select *
from get_limited_data1() d1
join data2 d2 on d1.a = d2.a

Il peut être utile de passer par une fonction pour ajuster les heuristiques du planificateur de requêtes.

Attention cependant! Si la fonction est inlinée, le planificateur perdra ses heuristiques, et pourra revenir à un plan moins efficace:

create function get_limited_data1()
returns table(a integer)
stable
rows 10
as
$$
 select * from data1 limit (select * from data_limit);
$$ language sql;

select *
from get_limited_data1() d1
join data2 d2 on d1.a = d2.a

Forcer la volatilité la plus haute permet d'empêcher l'inline de la fonction, et donc de forcer les heuristiques du planificateur.

Fonctions postgresql, inlining

Regardons d'un peu plus près les fonctions Postgres et leurs conséquences sur les performances. Créons deux tables d'entiers munies chacune d'un index, l'une faisant 2M de lignes, et l'autre seulement 10.

create table data1(a integer not null);
insert into data1 select generate_series(1, 10000000, 5);
create index data1_idx on data1(a);
create table data2(a integer not null);
insert into data2 select generate_series(1, 10);
create index data2_idx on data2(a);

Joignons les 2 tables, et oh, miracle, le planificateur de requête décide d'utiliser l'index sur la grande table. Temps d'exécution, 12 ms.

select *
from data1 d1 join data2 d2 on d1.a = d2.a;

Maintenant, utilisons plutôt des fonctions, qui retournent le contenu de la table.

create function get_data1() 
returns table(a integer)
as
$$
 select * from data1;
$$ language sql;

create function get_data2()
returns table(a integer)
as
$$
 select * from data2;
$$ language sql;

select *
from get_data1() d1 join get_data2() d2 on d1.a = d2.a;

Le plan est beaucoup moins sympathique: le planificateur ayant perdu toute information sur les index disponibles ne peut que faire une jointure bourrine, pour une exécution qui dure 2.3 secondes. Comment arranger cela?

Une fonction Postgres possède un certain nombre d'attributs qu'il est possible d'ajuster pour donner plus d'information au planificateur de tâches. L'un de ces attributs est la volatilité de la fonction. Par défaut, toute fonction est volatile, ce qui empêche le planificateur de faire aucune optimisation, mais une fonction ne contenant que des "select" peut être passé à "stable", ce qui permet au planificateur de l'inliner. Le résultat est sans appel:

create function get_data1() 
returns table(a integer)
stable
as
$$
 select * from data1;
$$ language sql;

create function get_data2()
returns table(a integer)
stable
as
$$
 select * from data2;
$$ language sql;

select *
from get_data1() d1 join get_data2() d2 on d1.a = d2.a;

L'on est revenu au premier plan, car le planificateur a tout inliné et a donc pu utiliser sa connaissance des index pour revenir au temps d'exécution initial.

Il est la plupart du temps extrêmement utile d'attribuer la bonne volatilité à une fonction afin de permettre au planificateur de requête de l'optimiser au mieux.

mercredi 9 mai 2012

Mon nouveau téléphone

C'est avec une petite larme au coin de l’œil que j'abandonne mon Nokia 6500c. C'était une brave bête, stylée, indestructible, et plutôt ergonomique, bien qu'un poil buggée. Je ne saute qu'à moitié vers le smartphone, puisque je suis maintenant l'heureux utilisateur d'un C3 "Touch & Type", qui comme son nom l'indique est muni à la fois d'un clavier et d'un écran tactile.

Au final, et après un petit peu de personnalisation, je retrouve toutes mes marques.

Cependant, y transférer mes SMS n'a pas été de tout repos. Laissez moi vous conter un peu tout ça.

Tout d'abord, je décide d'utiliser l'option Bluetooth. Il me faut m'y reprendre 3 ou 4 fois avant de comprendre comment configurer chaque téléphone pour qu'ils acceptent de se causer, mais ça fini par passer. Mais ce n'était que partie remise, puisqu'au milieu de la (longue) transmission, il plante. Je réessaye, il plante encore.

Prochaine étape: passer par le PC. Certes, mais où est la suite Nokia pour Linux? Nulle part, on dirait. Malgré les demandes répétées de quelques barbus irréductibles, Nokia ne fournit qu'une solution Windows, et les quelques applications OpenSource qui prétendent y arriver refusent obstinément de découvrir mon téléphone.

Ça fait mal, mais bon, rebootons, et installons l'OVI suite. Ah, et puis les drivers Nokia qui doivent être téléchargés depuis Windows Update. Ah, et puis il faut rebooter la machine. Ah, et puis ne faut pas brancher le téléphone trop tôt, sinon l'application est perdue. Au moment où je m'apprêtais à sacrifier un poulet, ça se connecte enfin. Youpie! Je synchronise, et au bout d'un autre (long) moment, mes contacts, mes photos et mes SMS sont sur la machine. J'y branche alors mon nouveau téléphone, et je synchronise. Malheureusement, c'est là que l'on ne s'est pas bien compris, avec la machine: elle a juste téléchargé les moitié de messages qui étaient déjà sur le téléphone, conséquence de la copie Bluetooth partielle, et m'a pourri ma boite en passant la moitié des messages à la date d'aujourd'hui.

J'ai bien ensuite perdu 2 heures à tenter de nettoyer pour resynchroniser. Il devait se souvenir de la date du dernier message téléchargé, parce qu'il ne voulait plus rien savoir et ne transmettait plus aucune donnée. Finalement, j'ai découvert qu'il était possible de faire du "glisser-déposer" pour forcer la transmission des messages vers la machine, puis de recommencer pour renvoyer vers le nouveau téléphone... Mais ce mode prend encore plus de temps, au rythme de 2 ou 3 messages à la seconde.

Finalement, tout y est. Les messages multimédia apparaissent au début au lieu de venir dans l'ordre alphabétique, mais je survivrai. Pour moi, c'est la conséquence d'applications trop intelligentes: je n'arrive pas à comprendre à l'avance les opérations que le système va effectuer, et quand je me plante, je dois alors me battre pour retrouver les fonctionnalités de base (copie ça ici) qui me permettent de réparer... Voyons cependant le bon côté des choses: l'OVI suite permet d'exporter ses SMS au format CSV. J'ai l'intention de tout coller dans une base de données, et de tirer quelques statistiques d'une inutilité crasse. Vous aurez été prévenus!