Affichage des articles dont le libellé est sql. Afficher tous les articles
Affichage des articles dont le libellé est sql. Afficher tous les articles

vendredi 18 août 2017

Postgres et performances de l'indexage

Je suis en train d'effectuer un changement de types sur des grosses tables partitionnées (jusqu'à 1 TB par groupe de tables), et je découvre quelques aspects intéressants des performances de ce genre d'opérations. Aujourd'hui, parlons des performances de l'indexage.

Pour illustrer, voyons ce script qui créé une table avec 3 colonnes d'entiers, bytea et texte. Ajoutons 10.000.000 de lignes, et indexons chaque colonne à son tour.

create table t(f1 int not null, f2 bytea not null, f3 text not null);

insert into t
select i, i::text::bytea, i::text
from generate_series(1, 10000000) i;

create index t1 on t(f1);
create index t2 on t(f2);
create index t3 on t(f3);

Voyons à quelle vitesse chaque opération d'indexage s'est effectuée.

EntierByteaTexte
7 s18 s40 s

Alors, qu'indexer une colonne d'entiers soit plus rapide qu'une colonne de texte, ça ne me paraît pas choquant. En revanche, que le texte prenne plus de 2 fois le temps du binaire, voilà qui est surprenant !

Je n'ai pas d’explication à ce phénomène. Cependant, l'on notera que la différence principale entre le binaire et le texte est la présence d'un encodage. Celui-ci a certainement un impact lors de la création de l'index. Peut-être que Postgres revérifie que chaque chaîne est conforme à l'encodage ?

La bonne nouvelle, c'est qu'une fois l'index créé, le scan est aussi rapide pour les deux colonnes. Pas de quoi s'inquiéter, donc, mais à savoir lorsque l'on a besoin d'indexer quelques milliards de lignes avant Lundi matin.

Justement, parlons-en. Comment donc mettre à jour et indexer notre table efficacement ? Une approche qui fonctionne bien sur les machines multi-cœur est de supprimer l'héritage pour chaque table enfant, supprimer les index, et mettre à jour la table. Une fois l'opération complétée pour chaque table enfant, l'on peut recréer les index en parallèle : en effet, il est possible de créer des index en même temps sur différentes tables, et l'opération étant principalement limitée en CPU, l'on se retrouvera avec un temps divisé d'autant. Puis, l'on peut rattacher chaque sous-table à la table parent, et c'est reparti. L'on pensera à analyser les tables, car la mise à jour d'un type de colonne efface les statistiques.

jeudi 18 juin 2015

Ubuntu Touch - Transférer ses SMS - Phase 1: reconnaissance (suite)

Reprenons, et regardons de plus près les deux tables restantes, thread_participants et text_events.

thread_participants

ChampTypeCommentaire
accountIdchaîneClé vers la table "threads"
threadIdchaîneClé vers la table "threads"
typeentierClé vers la table "threads"
participantIdchaîneLe nom du participant à ce fil de discussions. Pour les SMS simples, c'est simplement notre correspondant. De ce que je vois des données, utiliser la même valeur que le threadId fonctionne

text_events

ChampTypeCommentaire
accountIdchaîneClé vers la table "threads"
threadIdchaîneClé vers la table "threads"
eventIdchaîneProbablement un identifiant unique pour le message. Dans mes données, c'est à une valeur qui ressemble à "/ril_0/message_" suivi d'une chaîne qui ressemble furieusement à un hash pour les messages envoyés, et à une date au format ISO (par exemple 2015-06-01T22:35:52+0100-1) pour les messages reçus
senderIdchaîneL'envoyeur, à "self" pour les messages envoyés, et au numéro de tel de l'envoyeur pour les messages reçus
timestampdatetimeLa date et l'heure auxquelles le message a été reçu
newEventbooléenAucune idée. Toujours à faux dans mes données.
messagechaîneLe message proprement dit
messageTypeentierLe type de message, manifestement toujours à 0
messageStatusentierManifestement là pour indiquer si le message a été lu, envoyé, etc. Les bonnes valeurs semblent être 4 pour les messages envoyés, et 0 pour les messages reçus
readTimestampdatetimeLa date et l'heure auxquelles le message a été lu. Peut probablement être mis à la même valeur que timestamp pour les messages reçus, et à null pour les messages envoyés.
subjectchaîneLe sujet du message, manifestement non-existant pour les SMS (peut-être là pour les MMS?). Doit probablement être mis à null pour les messages envoyés et à la chaîne vide pour les messages reçus

Maintenant, il va me falloir passer à la deuxième phase: massage de mes données en CSV pour pouvoir les insérer dans la base et retrouver mes SMS.

mercredi 10 juin 2015

Ubuntu Touch - Transférer ses SMS - Phase 1: reconnaissance

Les SMS sur un Ubuntu Phone sont situés dans une base de données sqlite à l'emplacement ~/.local/share/history-service/history.sqlite. En théorie, pour importer ses SMS, il suffit de les ajouter à la base dans le format qui va bien. Téléchargeons donc la base et jetons un coup d'oeil:

$ sqlite3 history.sqlite
sqlite> .schema
CREATE TABLE schema_version (
    version int
);
CREATE TABLE threads (
    accountId varchar(255),
    threadId varchar(255),
    type tinyint,
    lastEventId varchar(255),
    lastEventTimestamp datetime,
    count int,
    unreadCount int
);
CREATE TABLE thread_participants (
    accountId varchar(255),
    threadId varchar(255),
    type tinyint,
    participantId varchar(255)
);
CREATE TABLE text_events (
    accountId varchar(255),
    threadId varchar(255),
    eventId varchar(255),
    senderId varchar(255),
    timestamp datetime,
    newEvent bool,
    message varchar(512),
    messageType tinyint,
    messageStatus tinyint,
    readTimestamp datetime,
    subject varchar(256)
);

Rien de très méchant à priori (j'ai laissé la partie du schéma qui décrit les "triggers" qui mettent à jour certaines dates, ainsi que les voice_events, correspondant aux conversations téléphoniques, et aux text_event_attachments, correspondant aux pièces jointes, j'imagine pour les MMS). L'on comprend qu'il y a un certain nombre de "threads", qui regroupent un certain nombre de "thread_participants", et que l'on peut ensuite ajouter chaque message dans text_event. Voici ce que j'ai réussi à en comprendre de la première table d'après les données déjà présentes après quelques appels et quelques SMS:

threads

ChampTypeCommentaire
accountIdchaîneUn nom de compte, probablement une constante pour tous les téléphones. Chez moi, c'est systématiquement la valeur "ofono/ofono/account0"
threadIdchaîneL'identifiant du thread. C'est apparemment le numéro de téléphone de son correspondant, sauf quand il s'agit de correspondants nommés, comme "Ikea" ou "Orange"
typeentierUne valeur qui peut être 0 ou 1. En regardant les numéros de téléphone associés, je pense que cela indique s'il s'agit d'un thread de message, avec le code 0, ou d'un historique d'appel, avec un 1. Pour l'import des SMS, on mettra 0 partout.
lastEventIdchaîneC'est un identifiant assez moche, qui contient soit un numéro de téléphone, soit une date, suivi d'une date et d'une heure. J'imagine que c'est le dernier eventId de la table text_events, et qu'il est mis à jour via une trigger
lastEventTimestampdate et heurela date et l'heure du dernier message
countentierLe nombre de messages dans le thread
unreadCountentierLe nombre de messages non lus. Pour l'import, un 0 sera bien suffisant.

Suite au prochain numéro, car il est temps d'aller se coucher!

mercredi 14 novembre 2012

Où l'on reparle des heuristiques

J'ai eu l'occasion récemment de me remettre aux heuristiques des fonctions Postgresql, que j'avais mentionnées dans un post précédent

Voyons le problème: l'on a un certain nombre de valeurs que l'on veut rechercher dans une grande table. On insère donc ces valeurs dans une table temporaire, et l'on fait une jointure. En fonction du nombre de valeurs dans la table temporaire, le planificateur de tâches va soit utiliser l'index, soit ordonner la table temporaire et faire un merge join. Sauf que depuis l'application, l'on prépare le plan à la première exécution, et l'on a donc aucune certitude sur le plan choisi. Pire, souvent les premières requêtes chargent un grand nombre de valeurs, et optimisent donc le plan pour ce cas, alors qu'ensuite le système se stabilise sur de toutes petites recherches, qui prennent donc beaucoup plus de temps.

create table data(i integer not null);
insert into data select generate_series(1, 1000000);
create index data_idx on data(i);

create temp table search(i integer not null);
insert into search select generate_series(1, 1000);
analyze search;
select d.i
from data d
join search s on d.i = s.i
truncate search;
insert into search select generate_series(1, 1);
analyze search;
select d.i
from data d
join search s on d.i = s.i

L'on pourrait empêcher la préparation du plan, ce qui oblige l'application à différencier entre les requêtes que l'on peut préparer et les autres. Mais il y a aussi une solution: doucement orienter le planificateur de requêtes vers le plan le plus efficace en moyenne, à l'aide d'une fonction et de hints bien choisis.

create or replace function get_search() 
returns table(i integer) 
rows 1 
volatile as
$$
 select * from search;
$$ language sql;

truncate search;
insert into search select generate_series(1, 100000);
analyze search;
select d.i
from data d
join get_search() s on d.i = s.i

L'on aura beau mettre autant de lignes que l'on veut dans la table search, le plan passera toujours par l'index de la grande table:

dimanche 4 novembre 2012

Postgresql - Arguments nommés

Je ne m'en étais même pas rendu compte: Postgresql 9.2 permet maintenant d'utiliser des paramètres nommés au sein de ses fonctions, en place des paramètres positionnels. C'est à dire qu'avant, il fallait écrire:

create function myadd(int, int) returns int as
$$
 select $1 + $2;
$$ language sql;

Mais maintenant, l'on peut écrire:

create function myadd(x int, y int) returns int as
$$
 select x + y;
$$ language sql;

Ce qui est quand même nettement plus lisible, non?

mardi 30 octobre 2012

Un coalesce en colonnes

Ça faisait longtemps que l'on avait pas fait de SQL, non? Alors, petit exercice. Soit une table contenant pour chaque date le nom d'un champ qui change de valeur, et sa nouvelle valeur. Comment transformer cette approche historique en approche par état, où l'on voit à chaque instant la valeur de chaque champ?

Tout d'abord, créons la table.

create table history(t serial, field text, value integer);
insert into history values(default, 'a', 1);
insert into history values(default, 'b', 12);
insert into history values(default, 'a', 3);
insert into history values(default, 'c', 101);
insert into history values(default, 'b', 12);
insert into history values(default, 'b', 14);
insert into history values(default, 'a', 3);
insert into history values(default, 'a', 1);
insert into history values(default, 'c', 102);
insert into history values(default, 'a', 2);
insert into history values(default, 'b', 13);

Pour visualiser la valeur de chaque champ, on donnera à chaque champ sa colonne, pour l'associer à l'ensemble des moments qui nous intéressent. Une jointure gauche s'impose donc:

select m.t, a.value as a, b.value as b, c.value as c
from history m
left outer join history a on m.t = a.t and a.field = 'a'
left outer join history b on m.t = b.t and b.field = 'b'
left outer join history c on m.t = c.t and c.field = 'c'
order by m.t

tabc
11
212
33
4101
512
614
73
81
9102
102
1113

C'est à peu près ça, sauf qu'il faut maintenant remplir les vides avec la dernière valeur disponible. Une sorte de coalesce, mais vertical.

Les sous-requêtes viennent rapidement à notre aide: pour chaque champ, l'on veut récupérer la dernière valeur en ordonnant par le temps, ce qui est facile grâce à limit. Voici donc:

select
 m.t,
 (select value from history h 
  where h.t <= m.t 
  and field = 'a' 
  and value is not null 
  order by h.t 
  desc limit 1) as a,
 (select value from history h 
  where h.t <= m.t 
  and field = 'b' 
  and value is not null 
  order by h.t desc 
  limit 1) as b,
 (select value from history h 
  where h.t <= m.t 
  and field = 'c' 
  and value is not null 
  order by h.t desc 
  limit 1) as c
from history m

tabc
11
2112
3312
4312101
5312101
6314101
7314101
8114101
9114102
10214102
11213102

Byzance! Mais arrêtons nous un instant: n'est-ce pas terriblement inefficace d'avoir des sous-requêtes dans le select? Peut-on trouver une autre solution, en utilisant par exemple des window functions? Ça vaut la peine d'essayer. Je ne crois pas qu'il existe de window function qui remplisse exactement ce rôle, mais une approche possible consiste à commencer par écrire la requête qui a un t donné donne le dernier t disponible pour le champ, en demandant à la window function d'aller chercher le max(t) pour un champ donné. La fenêtre par défaut allant jusqu'à la ligne courante, et les entrées nulles de notre jointure gauche étant par définition plus petites que le temps précédent, ça marche plutôt bien:

select 
 m.t, 
 max(a.t) over (order by m.t) as a,
 max(b.t) over (order by m.t) as b,
 max(c.t) over (order by m.t) as c
from history m
left outer join history a on m.t = a.t and a.field = 'a'
left outer join history b on m.t = b.t and b.field = 'b'
left outer join history c on m.t = c.t and c.field = 'c'
order by m.t

tabc
11
212
332
4324
5354
6364
7764
8864
9869
101069
1110119

Une fois que le temps attendu est disponible dans sa colonne, il est trivial d'obtenir la solution en joignant la table ainsi créée avec la table "history", pour chaque champ, afin d'en retrouver la valeur. La requête a pris du poids:

select h.t, a.value, b.value, c.value
from
 (select 
  m.t, 
  max(a.t) over (order by m.t) as a,
  max(b.t) over (order by m.t) as b,
  max(c.t) over (order by m.t) as c
 from history m
 left outer join history a on m.t = a.t and a.field = 'a'
 left outer join history b on m.t = b.t and b.field = 'b'
 left outer join history c on m.t = c.t and c.field = 'c'
 order by m.t) as h
left outer join history a on h.a = a.t and a.field = 'a'
left outer join history b on h.b = b.t and b.field = 'b'
left outer join history c on h.c = c.t and c.field = 'c'
order by t

Alors, est-ce plus rapide que l'approche par sous-requêtes dans le select? Malheureusement, non... Je m'attendais à une grosse différence, mais le planificateur de requête est loin d'être bête, et avec les bons index, les deux approches se valent, avec un léger avantage pour l'approche par sous-requête.

En cadeau bonus, cependant, le plan de notre dernière requête:

jeudi 23 août 2012

Postgresql - Méthodes de classes

J'ai lu avec intérêt aujourd'hui un article décrivant Postgres en tant que système de base de données relationnelle-objet. Était évoquée notamment une fonctionnalité que je ne connaissais pas: une fonction sur une table peut être appelée directement sur la table via un '.' au lieu d'avoir à utiliser l'appel usuel avec parenthèses. Démonstration:

create table resultats(
 equipe text not null,
 victoires integer not null,
 nuls integer not null,
 defaites integer not null);

insert into resultats values('Troyes', 3, 0, 1);
insert into resultats values('Sète', 1, 2, 0);

create function total(resultats) returns integer as
$$
 select $1.victoires + $1.nuls + $1.defaites;
$$ language sql;

create function points(resultats) returns integer as
$$
 select $1.victoires * 3 + $1.nuls * 2 + $1.defaites;
$$ language sql;

Si l'on veut visualiser le nombre total de matchs et les points de chaque équipe, l'on peut utiliser la requête suivante:

select r.equipe, total(r), points(r) from resultats r;

Mais il est également possible de l'écrire comme ça:

select r.equipe, r.total, r.points from resultats r;

En dehors de l'aspect purement syntactique de la chose, c'est aussi une manière de considérer les fonctions total et points comme étant des méthodes sur la classe resultats.

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.

jeudi 15 mars 2012

DB2 ou Sybase?

C'est le choix cornélien qui me tombe dessus au boulot.

D'un côté, DB2, que nous utilisons depuis fort longtemps, mais dont les performances sont plutôt mauvaises (rien à voir à priori avec le moteur en lui même, c'est plutôt une histoire de performances du stockage réseau), à tel point que nous avons peut-être aujourd'hui perdu un important client, excédé de nos problèmes de performances. Ajoutons également que les DBAs nous facturent en interne une petite fortune, et l'on comprend pourquoi l'on songe à bouger.

De l'autre, Sybase, qui a l'énorme avantage de coûter quelque chose comme 20 fois moins au bas mot, grâce à la ristourne négociée par le département en échange d'un achat massif de licenses. Les performances pourraient être meilleures (?), mais les fonctionnalités semblent moins à la hauteur. L'absence de requêtes récursives est un problème qui pourrait être particulièrement difficile à émuler, et complexifier de fait la couche applicative. L'exécution de requêtes concurrentes pourrait être moins efficace. Et enfin, je n'ai toujours pas réussi à faire fonctionner leur bibliothèque ODBC en 64 bits.

Je n'aurais pas eu d'hésitations à proposer Postgresql, mais malheureusement ce n'est pas une option dans notre monde merveilleux. Tant pis, on fera avec.

jeudi 1 mars 2012

Analysez vos tables

D'abord, créons une table raisonnablement grosse, avec un bel index créé implicitement par la clé primaire.


drop table if exists numbers;
create table numbers(n integer primary key);
insert into numbers select generate_series(1, 100000);

Si l'on veut chercher une valeur en particulier, l'on voit que le planificateur de tâches décide très raisonnablement de passer par l'index:

select * from numbers where n = 1297



Là où ça devient intéressant, c'est lorsque l'on passe par une table temporaire, parce que l'on peut vouloir chercher par exemple plusieurs valeurs à la fois:

create temporary table test(n integer not null);
insert into test values(1297);

Regardons le plan...

select * from numbers n join test t on n.n = t.n;

Stupeur, le planificateur décide de hacher l'ensemble de la table "numbers", au lieu d'utiliser l'index.


En effet: le planificateur, ne connaissant pas la taille de la table temporaire, suppose par défaut qu'elle fait 1000 lignes, et par conséquent choisit un plan non optimal.

Heureusement, il est possible de lui demander d'analyser la table afin de mettre à jour ses statistiques.

Ansi:

analyze test;
select * from numbers n join test t on n.n = t.n;

nous donne enfin le plan recherché.


L'on peut ainsi dépenser quelques millisecondes au sein de la transaction pour mettre à jour ses statistiques, et sauver ainsi potentiellement plusieurs secondes en permettant au planificateur de mieux choisir.

mercredi 8 juin 2011

Postgresql et encore plus de requêtes récursives

L'on m'a posé récemment un intéressant problème de base de données qui pourrait se traduire ainsi: étant donné la liste d'employés suivante, déterminer le premier directeur auquel se rattache chaque employé:


create table employees(
id integer not null,
name text not null,
level text not null,
manager_id integer null);

insert into employees values
(1, 'Anne', 'CEO', null),
(2, 'Barbara', 'Director', 1),
(3, 'Carole', 'Director', 2),
(4, 'Delphine', 'Director', 2),
(5, 'Eleonore', 'Supervisor', 2),
(6, 'Fanny', 'Supervisor', 3),
(7, 'Gwen', 'Supervisor', 5),
(8, 'Henriette', 'Supervisor', 7),
(9, 'Isabelle', 'Employee', 5),
(10, 'Juliette', 'Employee', 6),
(11, 'Kariane', 'Employee', 5),
(12, 'Lucie', 'Employee', 4);

La première étape est de pondre une requête récursive, nous donnant pour chaque employé la liste de ses chefs. L'on tiendra à jour également la distance hiérarchique:

with recursive managers(employee_id, manager_id, depth) as(
select id, manager_id, 1 from employees
union
select m.employee_id, e.manager_id, m.depth + 1
from managers m
join employees e on m.manager_id = e.id)
select
e1.id,
e1.name,
m.depth,
e2.id,
e2.name,
e2.level
from managers m
join employees e1 on m.employee_id = e1.id
join employees e2 on m.manager_id = e2.id

Cette requête retourne les résultats suivants:

2,Barbara,1,1,Anne,CEO
3,Carole,1,2,Barbara,Director
4,Delphine,1,2,Barbara,Director
5,Eleonore,1,2,Barbara,Director
6,Fanny,1,3,Carole,Director
7,Gwen,1,5,Eleonore,Supervisor
8,Henriette,1,7,Gwen,Supervisor
9,Isabelle,1,5,Eleonore,Supervisor
10,Juliette,1,6,Fanny,Supervisor
11,Kariane,1,5,Eleonore,Supervisor
12,Lucie,1,4,Delphine,Director
3,Carole,2,1,Anne,CEO
4,Delphine,2,1,Anne,CEO
5,Eleonore,2,1,Anne,CEO
6,Fanny,2,2,Barbara,Director
7,Gwen,2,2,Barbara,Director
8,Henriette,2,5,Eleonore,Supervisor
9,Isabelle,2,2,Barbara,Director
10,Juliette,2,3,Carole,Director
11,Kariane,2,2,Barbara,Director
12,Lucie,2,2,Barbara,Director
6,Fanny,3,1,Anne,CEO
7,Gwen,3,1,Anne,CEO
8,Henriette,3,2,Barbara,Director
9,Isabelle,3,1,Anne,CEO
10,Juliette,3,2,Barbara,Director
11,Kariane,3,1,Anne,CEO
12,Lucie,3,1,Anne,CEO
8,Henriette,4,1,Anne,CEO
10,Juliette,4,1,Anne,CEO


On approche. Prenons donc seulement le cas où un chef est un directeur, et voyons ce que cela donne:

with recursive managers(employee_id, manager_id, depth) as(
select id, manager_id, 1 from employees
union
select m.employee_id, e.manager_id, m.depth + 1
from managers m
join employees e on m.manager_id = e.id)
select
e1.id,
e1.name,
m.depth,
e2.id,
e2.name,
e2.level
from managers m
join employees e1 on m.employee_id = e1.id
join employees e2 on m.manager_id = e2.id
where e2.level = 'Director'

3,Carole,1,2,Barbara,Director
4,Delphine,1,2,Barbara,Director
5,Eleonore,1,2,Barbara,Director
6,Fanny,2,2,Barbara,Director
6,Fanny,1,3,Carole,Director
7,Gwen,2,2,Barbara,Director
8,Henriette,3,2,Barbara,Director
9,Isabelle,2,2,Barbara,Director
10,Juliette,3,2,Barbara,Director
10,Juliette,2,3,Carole,Director
11,Kariane,2,2,Barbara,Director
12,Lucie,2,2,Barbara,Director
12,Lucie,1,4,Delphine,Director

Mieux! Mais comme parfois un directeur dépend d'un autre directeur, les pauvres Fanny, Juliette et Lucie sont en double. Il faut donc ne récupérer pour un employé que le cas où la profondeur est la plus basse. Cela donne des requêtes plutôt ésotériques en vieux SQL. Mais une fois de plus, la modernité nous tend les bras, et les window function permettent de résoudre le problème (presque) immédiatement. Prenons donc la fonction rank(), qui retourne le rang d'une ligne sur une certaine partition. Nous cherchons donc le rang d'une ligne d'employé, partitionné sur cet employé, et ordonné par la profondeur:

with recursive managers(employee_id, manager_id, depth) as(
select id, manager_id, 1 from employees
union
select m.employee_id, e.manager_id, m.depth + 1
from managers m
join employees e on m.manager_id = e.id)
select
e1.name as employee_name,
e2.name as director_name,
rank() over (partition by e1.name order by depth)
from managers m
join employees e1 on m.employee_id = e1.id
join employees e2 on m.manager_id = e2.id
where e2.level = 'Director'

Carole,Barbara,1
Delphine,Barbara,1
Eleonore,Barbara,1
Fanny,Carole,1
Fanny,Barbara,2
Gwen,Barbara,1
Henriette,Barbara,1
Isabelle,Barbara,1
Juliette,Carole,1
Juliette,Barbara,2
Kariane,Barbara,1
Lucie,Delphine,1
Lucie,Barbara,2

On y est presque. Nous voilà donc avec le rang basé sur la profondeur, par nom. Lucie dépend donc d'abord de Delphine, et ensuite de Barbara. Maintenant, obtenir le résultat final est trivial:

select employee_name, director_name
from
(
with recursive managers(employee_id, manager_id, depth) as(
select id, manager_id, 1 from employees
union
select m.employee_id, e.manager_id, m.depth + 1
from managers m
join employees e on m.manager_id = e.id)
select
e1.name as employee_name,
e2.name as director_name,
rank() over (partition by e1.name order by depth)
from managers m
join employees e1 on m.employee_id = e1.id
join employees e2 on m.manager_id = e2.id
where e2.level = 'Director'
) t
where rank = 1

Carole,Barbara
Delphine,Barbara
Eleonore,Barbara
Fanny,Carole
Gwen,Barbara
Henriette,Barbara
Isabelle,Barbara
Juliette,Carole
Kariane,Barbara
Lucie,Delphine

La requête n'est certes pas tout à fait basique, mais elle est relativement claire grâce à la séparation entre la partie récursive d'abord, la window fonction ensuite, et enfin la sur-requête finale pour présenter les résultats. Sans ces outils, l'on aurait eu une panoplie de sous-requêtes se joignant elles-mêmes, pour un résultat bien moins propre, et bien moins efficace.

mardi 22 mars 2011

Être consistent...

Rhaaa, IBM! Ils viennent de confirmer après 3 jours d'arrachage de cheveux qu'ils ne supportent pas une fonctionnalité qui semble pourtant basique.

DB2, ou plutôt son API C, répondant au doux nom de CLI, fournit un mode très efficace exécuter plusieurs fois une même requête. L'idée est de créer un tableau, de "binder" ce tableau à une colonne (via un appel à SQLBindParameter), puis d'exécuter la requête préparée. Tout est envoyé en une fois à la base, ce qui évite l'effet ping-pong entre le client et le serveur. Par exemple, une requête "insert into mytable values(?)" permet d'insérer des milliers de lignes en un seul appel.

Nous codons donc une belle couche d'abstraction en C++, et continuons joyeusement à coder.

Jusqu'au jour où je découvre que le binding par colonnes ne fonctionne pas avec les procédures stockées. Dans le genre, je créé une procédure qui fait un bête insert dans mytable, et j'appelle "call myproc(?)". Pire, DB2 va exécuter la procédure, mais seulement sur le premier élément, ce qui cause un beau bordel dans la base. La documentation est peu claire sur ce point précis, la spec ODBC dit que ça devrait marcher. Eh ben non.

Reprenons: Une fonctionnalité qui semble pourtant aller de soi manque, ce manquement est mal documenté, et au lieu d'envoyer un message d'erreur clair, seule une partie de la requête tourne, causant une corruption des données. Peut-être que la raison fondamentale est que insert, update et delete supportent un vrai mode d'insertion par tableau, mais dans ce cas pourquoi ne pas au moins tenter d'être consistent, et pour le moteur de base de données de revenir dans un mode ligne par ligne dans le cas des procédures stockées? J'ai du mal à penser qu'IBM n'a pas les moyens humains de fournir quelque chose qui semble aussi trivial.

La solution, c'est d'utiliser le binding par valeur pour toute requête contenant une procédure stockée, mais c'est difficile à abstraire, puisqu'il faut soit laisser à l'utilisateur le choix (et donc la chance de se tromper), soit tenter d'analyser la requête pour détecter si elle est optimisable. En l'occurence, j'ai choisi la sale mais raisonnablement triviale solution d'optimiser si le premier mot de la requête est "insert". Et le code de la couche d'abstraction devient plus moche car il doit supporter deux types de bindings, et donc deux chemins de code, avec les presque duplications et l'augmentation des risques de se bugs qui vont avec. Beurk.

vendredi 27 août 2010

Postgresql et window functions

Les window functions de Postgresql permettent d'écrire des requêtes bien plus avancées, et ainsi de résoudre de nombreux problèmes complexes en pur SQL. Par exemple, prenons la table et les données suivantes:


create table historical(
id integer not null,
valid_from timestamp not null,
valid_to timestamp not null);

insert into historical values(1, '17-04-2010', '19-04-2010');
insert into historical values(1, '19-04-2010', '20-04-2010');
insert into historical values(1, '20-04-2010', '03-05-2010');
insert into historical values(1, '07-05-2010', '08-05-2010');
insert into historical values(1, '08-05-2010', '14-05-2010');
insert into historical values(2, '17-04-2010', '18-04-2010');
insert into historical values(2, '20-04-2010', '21-04-2010');
insert into historical values(2, '21-04-2010', '24-04-2010');
insert into historical values(2, '25-04-2010', '27-04-2010');
insert into historical values(2, '30-04-2010', '02-05-2010');
insert into historical values(2, '02-05-2010', '05-05-2010');


Pour une raison ou pour une autre, les données historiques ont été ajoutées sans essayer de les combiner avec l'existant, et l'on se retrouve donc avec bien plus de lignes que nécessaire dans la base. Ansi, les entrées du 17 au 19, du 19 au 20 et du 20 au 3 pourraient être représentées par une seule ligne, valide du 17 au 3. La question est: est-il possible d'écrire une requête SQL qui renvoie les données compactées?

Avec les window functions, oui!

L'idée est d'abord d'utiliser la fonction "lag", qui renvoie la valeur dans la table un certain nombre de lignes avant ou après, afin de comparer chaque ligne à la précédente. Si elle est différente, retournons 1, sinon, retournons 0:

select
id,
valid_from,
valid_to,
case when id = lag(id, 1)
over (order by id, valid_from, valid_to)
and valid_from = lag(valid_to, 1)
over (order by id, valid_from, valid_to)
then 0 else 1 end as agg
from historical
order by id, valid_from, valid_to

Cette requête nous renvoie une colonne supplémentaire, "agg", qui est à 0 lorsque la ligne pourrait être combinée avec la précédente.

1 2010-04-17 00:00:00 2010-04-19 00:00:00 1
1 2010-04-19 00:00:00 2010-04-20 00:00:00 0
1 2010-04-20 00:00:00 2010-05-03 00:00:00 0
1 2010-05-07 00:00:00 2010-05-08 00:00:00 1
1 2010-05-08 00:00:00 2010-05-14 00:00:00 0
2 2010-04-17 00:00:00 2010-04-18 00:00:00 1
2 2010-04-20 00:00:00 2010-04-21 00:00:00 1
2 2010-04-21 00:00:00 2010-04-24 00:00:00 0
2 2010-04-25 00:00:00 2010-04-27 00:00:00 1
2 2010-04-30 00:00:00 2010-05-02 00:00:00 1
2 2010-05-02 00:00:00 2010-05-05 00:00:00 0

Ceci fait, appelons maintenant la fonction "sum", pour renvoyer la somme courante des "agg", ce qui fournit un rang:

select id, valid_from, valid_to,
sum(agg) over (order by id, valid_from, valid_to) as ranked
from
(select
id,
valid_from,
valid_to,
case when id = lag(id, 1)
over (order by id, valid_from, valid_to)
and valid_from = lag(valid_to, 1)
over (order by id, valid_from, valid_to)
then 0 else 1 end as agg
from historical
order by id, valid_from, valid_to) as agg_aggregated
order by id, valid_from, valid_to

Cette requête retourne maintenant un rang identique pour chaque ligne pouvant être combinée.

1 2010-04-17 00:00:00 2010-04-19 00:00:00 1
1 2010-04-19 00:00:00 2010-04-20 00:00:00 1
1 2010-04-20 00:00:00 2010-05-03 00:00:00 1
1 2010-05-07 00:00:00 2010-05-08 00:00:00 2
1 2010-05-08 00:00:00 2010-05-14 00:00:00 2
2 2010-04-17 00:00:00 2010-04-18 00:00:00 3
2 2010-04-20 00:00:00 2010-04-21 00:00:00 4
2 2010-04-21 00:00:00 2010-04-24 00:00:00 4
2 2010-04-25 00:00:00 2010-04-27 00:00:00 5
2 2010-04-30 00:00:00 2010-05-02 00:00:00 6
2 2010-05-02 00:00:00 2010-05-05 00:00:00 6

Il est maintenant trivial d'agréger les lignes ayant le même rang, et de retourner le plus petit "from" et le plus grand "to".

select
min(id) as id,
min(valid_from) as valid_from,
max(valid_to) as valid_to
from
(select id, valid_from, valid_to,
sum(agg) over (order by id, valid_from, valid_to) as ranked
from
(select
id,
valid_from,
valid_to,
case when id = lag(id, 1)
over (order by id, valid_from, valid_to)
and valid_from = lag(valid_to, 1)
over (order by id, valid_from, valid_to)
then 0 else 1 end as agg
from historical
order by id, valid_from, valid_to) as agg_aggregated
order by id, valid_from, valid_to) as agg_ranked
group by ranked
order by id, valid_from

Le résultat est celui attendu:

1 2010-04-17 00:00:00 2010-05-03 00:00:00
1 2010-05-07 00:00:00 2010-05-14 00:00:00
2 2010-04-17 00:00:00 2010-04-18 00:00:00
2 2010-04-20 00:00:00 2010-04-24 00:00:00
2 2010-04-25 00:00:00 2010-04-27 00:00:00
2 2010-04-30 00:00:00 2010-05-05 00:00:00


Postgres peut ainsi combiner un million de lignes à la minute sur un PC de bureau, alors que les solutions alternatives allant du chargement et de la combinaison dans un programme séparé, à la solution itérative (mal) implémentée via une extension dans une BDD moins costaude, peuvent être de 5 à 1000 fois plus lents. Yay!

dimanche 9 mai 2010

Variadic templates

Ça a été chaud, mais j'ai enfin réussi à faire quelque chose d'utile avec mes variadic templates. J'ai templatisé le code de base de données pour laisser le compilo générer les classes de transactions à ma place.

En utilisant le système de classes de transactions de pqxx, l'on se retrouve à écrire beaucoup de classes qui se ressemblent beaucoup. L'on a un constructeur dans lequel on passe ses paramètres d'entrée et de sortie, l'opérateur parenthèses qui effectue l'appel en lui-même. J'y rajoute généralement la préparation de ma requête. Un exemple parmi d'autres:


class CheckAccount : public pqxx::transactor<pqxx::transaction<> >
{
public:
CheckAccount(const boost::shared_ptr<Logger> & logger,
const std::string & login,
const std::string & password,
bool & success,
UserId & userId):
m_logger(logger),
m_login(login),
m_password(password),
m_success(success),
m_userId(userId)
{
}

void operator()(argument_type & T)
{
TimedLogger t(m_logger, "Executed \"" + getRequest() + "\"");
pqxx::result result = T.prepared(getPlan())(m_login)(m_password).exec();
if(result.size() == 1)
{
m_success = true;
m_userId = UserId(result.at(0).at(0).as<uint32_t>());
}
else
{
m_success = false;
}
}

static std::string getPlan()
{
return "checkAccount_plan";
}

static std::string getRequest()
{
return "select checkAccount($1, $2)";
}

static void prepare(const boost::shared_ptr<pqxx::connection> & dbConn)
{
dbConn->prepare(getPlan(), getRequest())
("varchar(255)", pqxx::prepare::treat_string)
("varchar(255)", pqxx::prepare::treat_string);
}

private:
boost::shared_ptr<Logger> m_logger;
std::string m_login;
std::string m_password;
bool & m_success;
UserId & m_userId;
};


Le problème, c'est que c'est très verbeux, et qu'en écrire 2 ou 3, ça passe encore, mais des dizaines, ça enlève tout le fun du code.

J'ai donc profité des variadic templates pour tenter de réduire un petit peu la chose. L'idée est de créer une classe policy, qui va contenir les informations importantes de la requête:


class CheckAccount
{
public:
static std::string getPlan()
{
return "checkAccount_plan";
}

static std::string getRequest()
{
return "select checkAccount($1, $2)";
}

typedef std::tuple<std::string, std::string> InputT;
typedef std::tuple<int> OutputT;
};


Cette classe étant définie, on l'utilise pour créer la classe requête attendue, et on la lance.


typedef Request<CheckAccount> CheckAccountRequest;
CheckAccountRequest::prepare(dbConn);
dbConn->perform(CheckAccountRequest(std::make_tuple(login, password), result));


La difficulté est de correctement gérer la préparation d'une part, et l'appel d'autre part. Voici à quoi ressemble la classe de requête:


template<class POLICY>
class Request : public pqxx::transactor<pqxx::transaction<> >
{
public:
Request(const typename POLICY::InputT & input,
typename POLICY::OutputT & output):
m_input(input),
m_output(output)
{
}

void operator()(argument_type & T)
{
pqxx::prepare::invocation invocation = T.prepared(POLICY::getPlan());
internals::call(invocation, m_input);
invocation.exec();
}

static void prepare(const boost::shared_ptr<pqxx::connection> & dbConn)
{
internals::prepare(dbConn,
POLICY::getPlan(),
POLICY::getRequest(),
typename POLICY::InputT());
}

private:
const typename POLICY::InputT & m_input;
typename POLICY::OutputT & m_output;
};


Toute la logique se trouve dans les fonctions templates "prepare" et "call". Regardons donc la préparation:


template<typename ... Args>
class Prepare;

template<typename T, typename ... Args>
class Prepare<T, Args...>
{
public:
void operator()(const pqxx::prepare::declaration & declaration)
{
Prepare<Args...>()(declaration
(SqlType<T>::getSqlType(),
SqlType<T>::getParamTreatment()));
}
};

template<>
class Prepare<>
{
public:
void operator()(const pqxx::prepare::declaration & declaration)
{
}
};

template<typename ... Args>
void prepare(const boost::shared_ptr<pqxx::connection> & dbConn,
const std::string & plan,
const std::string & request,
const std::tuple<Args...> & )
{
Prepare<Args...>()(dbConn->prepare(plan, request));
}


L'on appelle récursivement Prepare, en dépaquetant le template variadique au fur et à mesure. A chaque appel, l'on utilise une classe utilitaire SqlType qui renvoie le nom SQL et traitement à effectuer sur le paramètre en fonction du type passé (par exemple, std::string s'appelle text en SQL, et nécessite d'être échappé).

Le code déroule donc le tuple, et utilise le type de tête pour gérer la préparation.

Jetez un coup d'oeil dans le <a href="http://adh.svn.sourceforge.net/viewvc/adh/framework/trunk/utils/Db.h?view=markup">dépôt subversion</a> sur la manière de gérer la partie "call": plutôt que de dérouler le tuple, ma récursion se fait sur un paramètre template entier qui correspond à l'index de l'élément du tuple qui m'intéresse.

Malheureusement, C++0x est encore très expérimental, et cela se sent: il faut beaucoup tripoter pour que les choses marchent, et la bibliothèque standard est assez pauvre sur les nouveaux types (pas de "get_head" et "get_tail" pour std::tuple, par exemple).

Mais je ne regrette pas de m'y être mis. Je n'ai plus qu'à gérer les paramètres de sortie, et à moi les requêtes solides et typesafe en un clin d'œil!

dimanche 7 février 2010

Créer une table coûte cher...

Pour mon serveur de logs, j'ai utilisé le système suivant pour écrire un message dans la base Postgresql:

  • Ouvrir la transaction

  • Créer une table temporaire qui sera détruite automatiquement à la fin de la transaction (on commit drop)

  • Remplir la table avec les éléments de mon message

  • Appeler la fonction qui va cŕeer les entrées dans les tables, à partir de la table temporaire

  • Fermer la transaction, causant la destruction de la table temporaire.

Pour tester un peu tout ceci, j'ai envoyé 10000 messages de log d'un coup. Grosse déception, les performances étaient vraiment ridicules, à environ 60 messages par seconde.

J'ai ensuite réessayé en changeant le mécanisme pour la table temporaire: cette fois-ci, je créé la table une seule fois, au tout début, et je dis à Postgres de se contenter de tronquer la table à la fin de la transaction (on commit delete rows).

Surprise, la base de données s'en trouve beaucoup mieux, et pédale à 600 transactions par seconde!

Autant qu'il soit agréable qu'il ait été si simple de décupler ainsi les performances, je ne suis pas tout à fait satisfait par cette solution. Le problème principal est de gérer les reconnections: comment détecter que la table a disparu, et comment la recréer?

J'ai donc changé mon code de manière à appeler, pour chaque transaction, une fonction qui va détecter si la table existe déjà, et si non la créé. Vivement les blocks anonymes (DO $$ ... $$), mais ce sera pour Postgresql 8.5 (ou plutôt, Postgresql 9.0, puisque c'est ainsi que la prochaine version s'appelle désormais).

Verdict: 400 transactions par secondes. Et une routine qui sera imperméable aux déconnections. Ça me va!

samedi 23 janvier 2010

Taille d'une base de données Postgresql

C'est pourtant facile!


select datname, pg_size_pretty(pg_database_size(datname))
from pg_database

lundi 31 août 2009

Premiers pas avec Ocsigen

Je voulais m'y mettre depuis longtemps, à ce serveur web. Or, justement, j'avais un petit projet en tête pour me délasser d'AdH: un système de documentation.


Ocsigen est donc un serveur web écrit en Ocaml, qui charge des modules basés sur le framework Eliom. Ocsigen est performant, mais surtout force un typage extrêmement fort du HTML en lui-même, et des liens entre les différents services. Enfin, l'on pourra réutiliser tout le code Ocaml existant, à condition de le faire rentrer dans le modèle de thread, basé sur une bibliothèque de threads coopératifs, Lwt.

Le gros problème d'Ocsigen, c'est qu'en dehors de la documentation, point de salut, ou si peu.

La documentation en elle-même est complète, mais manque furieusement d'exemples si l'on sort un tant soit peu des sentiers battus, et il faut pas mal d'efforts pour commencer à comprendre la manière dont les différents éléments s'enchaînent. L'inférence de types de nos amis Hindley et Milner vient heureusement à notre secours, et il serait une très mauvaise idée d'essayer d'écrire les interfaces avec des types aussi biscornus.

La communauté, elle, est inexistante (ou du moins, très bien cachée), et les sites publics qui tournent sous Ocsigen sont rares. Heureusement, un Wiki a été écrit pour Ocsigen pour le Google Summer of Code, ce qui aide beaucoup à voir comment, par exemple, utiliser Postgresql à travers Lwt.

Il est particulièrement intéressant d'écrire des pages à travers Ocsigen, c'est une manière d'appréhender la programmation Web tout à fait différente. On finit par se faire aux types ésotériques, et j'ai rapidement écrit quelques fonctions pour faciliter l'utilisation d'éléments récurrents.

Mon système de documentation, répondant au doux nom de Médoc (Mes docs :) ), avance tout gentiment, en quelques jours j'ai déjà une base de données (avec chiffrage des données les plus sensibles!), un outil en ligne de commande pour y sauver les documents, et une page web qui permet d'effectuer des recherches et d'afficher les documents. En voici une petite capture:



La page affiche tous les documents correspondant à un terme, en se basant sur le "Full Text Search" de Postgresql. L'on affiche également les miniatures de chaque page, cliquables et qui mènent au document grandeur nature.

Un peu de CSS sera nécessaire pour rendre l'ensemble plus joli, mais le système est déjà utile, ce qui est un bon début!

vendredi 28 août 2009

Postgresql Full Text Search - Planquez ces chiffres!

Admettons que l'on veuille sauver des textes cryptés dans la colonne d'une table Postgresql. Rien de plus facile en utilisant les fonctions pgp_sym_encrypt et pgp_sym_decrypt. Jusque là, tout va bien.

Mais admettons maintenant que l'on veuille également pouvoir faire des recherches rapides sur ces textes en utilisant le Full Text Search. L'on rajoute une colonne de type tsvector, et l'on parse le texte là dedans. Patatras, votre vilain espion peut maintenant recréer le document en regardant ce champ, qui, lui, ne peut pas être crypté!

L'on peut bien sûr monter ses données sur une partition cryptée. Mais c'est compliqué, et pas facile à installer chez l'utilisateur, sans compter que tout est en clair tant que la machine est allumée.

Cependant, admettons encore (ça fait beaucoup, je sais!) que les données sensibles soient plutôt des nombres (codes secrets, identification, numéros de compte, sommes d'argent). Sans ces nombres, les documents deviennent plutôt inoffensifs. Et de plus, il est peu probable que l'on aie besoin de ces nombres pour faire des recherches. C'est parfois utile, certes ("quels sont les documents qui contiennent mon numéro de compte?"), mais on pourra s'en passer.

Ça tombe bien, il est possible de créer une nouvelle configuration qui escamotera ce genre de données. Il suffit d'enlever la correspondance entre les types de tokens et le parseur.

Ajoutons donc la configuration french_safe:

create text search configuration french_safe ( copy = pg_catalog.french );
alter text search configuration french_safe drop mapping for int;
alter text search configuration french_safe drop mapping for uint;
alter text search configuration french_safe drop mapping for float;
alter text search configuration french_safe drop mapping for numword;

Alors que de la configuration de base, tout un tas de données sensibles auraient pu être extraites:


select * from to_tsvector(
'french',
'mon code secret est le 12345 et j''ai 217.45 € sur mon compte')
=> "'12345':6 '217.45':10 'cod':2 'compt':13 'secret':3"


la nouvelle configuration, elle, garde nos petits secrets bien au chaud!

select * from to_tsvector(
'french_safe',
'mon code secret est le 12345 et j''ai 217.45 € sur mon compte')
=> "'cod':2 'compt':11 'secret':3"


Faites donc un peu tourner des phrases sous ts_debug, et voyez comment les nombres et les mélanges chiffres lettres (mots de passes!) sont gentiment effacés.

lundi 10 août 2009

Postgresql 8.4 et les requêtes récursives

La fonctionnalité de Postgres 8.4 que je préfère, c'est les requêtes récursives. Voilà vraiment quelque chose qui va permettre de résoudre bon nombre de problèmes de manière beaucoup plus élégante, et notamment tout ce qui concerne les arbres, les graphes, et les hiérarchies de toutes sortes.

Voici par exemple un arbre où chaque élément pointe vers son parent.


create table h(entry int not null, parent int null);

insert into h values(1, null);
insert into h values(2, 1);
insert into h values(3, 1);
insert into h values(4, 1);
insert into h values(5, 1);
insert into h values(6, 2);
insert into h values(7, 2);
insert into h values(8, 6);
insert into h values(9, 8);
insert into h values(10, 4);


Maintenant, répondons à la question: Quels sont tous les éléments enfants de l'élément 2? Avec le SQL récursif, c'est trivial:


with recursive deep(n) as(
select entry from h where entry = 2
union
select entry from h join deep on h.parent = deep.n
)
select * from deep;


Le premier élément de l'union est l'amorce de la récursion: l'on démarre donc avec l'élément 2. Le second élément est la récursion en elle-même: l'on retourne l'ensemble des éléments qui sont parents de l'élément n.

La documentation contient quelques exemples beaucoup plus compliqués, où l'on explore des graphes cycliques, avec un SQL qui devient franchement ésotérique. Mais pour des cas simples, c'est un très bon moyen de se débarrasser des couches de pl/pgsql ou autres, et de revenir en pur SQL, ce qui pourrait entre autres améliorer les performances en permettant au planificateur de travailler directement sur toutes les tables.

A propos de planificateur, le nouveau pgadmin3 (malheureusement pas encore dans Squeeze, mais cela ne saurait tarder) montre de bien jolies choses sur ces requêtes récursives. Voici donc l'explain de ma requête.



En faisant quelques tests supplémentaires, je me suis rendu compte que Postgres utilisait gentiment les index qu'on lui donnait sur les colonnes "entry" et "parent", ce qui permettait de gagner environ 25% de temps sur de très grosses requêtes (arbre de 100 000 éléments). Elle est pas belle, la vie?