Tous les
Essayons avec
CREATE TABLE PhonesPersons (person INTEGER NOT NULL, -- Not UNIQUE
-- since a person may have several phone numbers
phone TEXT NOT NULL -- Not UNIQUE since
-- a phone can be used by several persons
);
puis je peuple cette table pour des tests avec un million d'entrées (le script
% time psql --pset pager essais -c \
"SELECT person FROM PhonesPersons WHERE phone = '+33 8116243'"
person
--------
997313
(1 row)
psql [...] 0.06s user 0.01s system 14% cpu 0.506 total
En effet, sans index, PostgreSQL a dû parcourir toutes les
données. Cherchons des explications :
essais=> EXPLAIN SELECT person FROM PhonesPersons WHERE phone = '+33 8116243';
QUERY PLAN
-----------------------------------------------------------------
Seq Scan on phonespersons (cost=0.00..18064.00 rows=1 width=4)
Filter: (phone = '+33 8116243'::text)
(2 rows)
C'est confirmé, la requête a pris du temps car il a fallu un balayage
séquentiel (
psql [...] 0.04s user 0.02s system 97% cpu 0.070 total
Le temps écoulé est beaucoup plus court (70 ms au lieu de 500). En effet, l'utilisation de
l'index permet à PostgreSQL d'être plus efficace :
essais=> EXPLAIN SELECT person FROM PhonesPersons WHERE phone = '+33 8116243';
QUERY PLAN
-------------------------------------------------------------------------------
Index Scan using phone_idx on phonespersons (cost=0.00..8.41 rows=1 width=4)
Index Cond: (phone = '+33 8116243'::text)
(2 rows)
Bien, cela, c'était pour le cas où on cherchait un numéro de
téléphone spécifique. Mais si on cherche une partie du numéro, par
exemple uniquement les
essais=> SELECT person FROM PhonesPersons WHERE phone LIKE '+33%';
person
---------
651229
829130
...
mais elle n'utilise pas l'index et est donc très lente :
essais=> EXPLAIN SELECT person FROM PhonesPersons WHERE phone LIKE '+33%';
QUERY PLAN
-------------------------------------------------------------------
Seq Scan on phonespersons (cost=0.00..18064.00 rows=163 width=4)
Filter: (phone ~~ '+33%'::text)
(2 rows)
On peut convaincre
essais=> EXPLAIN SELECT person FROM PhonesPersons WHERE phone LIKE '+33%';
QUERY PLAN
----------------------------------------------------------------------------
Bitmap Heap Scan on phonespersons (cost=6.06..569.94 rows=163 width=4)
Filter: (phone ~~ '+33%'::text)
-> Bitmap Index Scan on phone_idx (cost=0.00..6.02 rows=163 width=0)
Index Cond: ((phone ~>=~ '+33'::text) AND (phone ~<~ '+35'::text))
(4 rows)
Notez que cela ne marcherait pas si le joker (le caractère
Mais, ici, je prends une autre méthode, reposant sur les fonctions.
L'idée est de créer un index pour une sous-chaîne. Pour simplifier,
au début, on va supposer une sous-chaîne de longueur fixe, les trois
premiers caractères, avec la fonction
essais=> SELECT person FROM PhonesPersons WHERE substr(phone, 1, 3) = '+33';
person
---------
651229
829130
...
Cette requête n'utilise pas l'index qui existe, car on ne donne au
SGBD qu'une sous-chaîne :
essais=> EXPLAIN SELECT person FROM PhonesPersons WHERE substr(phone, 1, 3) = '+33';
QUERY PLAN
--------------------------------------------------------------------
Seq Scan on phonespersons (cost=0.00..20564.00 rows=5000 width=4)
Filter: (substr(phone, 1, 3) = '+33'::text)
(2 rows)
POur arranger cela, créeons un index pour la fonction
essais=> EXPLAIN SELECT person FROM PhonesPersons WHERE substr(phone, 1, 3) = '+33';
QUERY PLAN
----------------------------------------------------------------------------------
Bitmap Heap Scan on phonespersons (cost=83.09..5808.11 rows=5000 width=4)
Recheck Cond: (substr(phone, 1, 3) = '+33'::text)
-> Bitmap Index Scan on countrycode_idx (cost=0.00..81.84 rows=5000 width=0)
Index Cond: (substr(phone, 1, 3) = '+33'::text)
(4 rows)
Est-ce vraiment plus rapide ? Sans index :
% time psql --pset pager essais -c \
"SELECT person FROM PhonesPersons WHERE substr(phone, 1, 3) = '+33'"
...
psql [...] 0.05s user 0.01s system 5% cpu 1.036 total
Avec index :
psql [...] 0.06s user 0.01s system 87% cpu 0.073 total
Donc, oui, l'index est réellement efficace, 73 milli-secondes
d'attente au lieu d'une seconde entière.
Ici, le cas était simple car la sous-chaîne était de longueur
fixe. Pour essayer avec des longueurs variables, abandonnons les
numéros de téléphone pour les adresses de
CREATE TABLE EmailsPersons (person INTEGER NOT NULL,
email TEXT NOT NULL
);
et créeons une fonction SQL pour extraire le
-- Extracts the TLD from a domain name (or an email address)
CREATE OR REPLACE FUNCTION tld(TEXT) RETURNS TEXT IMMUTABLE AS
'
DECLARE
first_dot INTEGER;
rest TEXT;
BEGIN
first_dot = strpos($1, ''.'');
rest = substr($1, first_dot+1);
IF strpos(rest, ''.'') = 0 THEN
RETURN rest;
ELSE
RETURN last_label(rest);
END IF;
END;
'
LANGUAGE PLPGSQL;
On peut l'utiliser :
essais=> SELECT email, tld(email) FROM EmailsPersons WHERE person = 147676;
email | tld
-------------------------------------+-----
0jdm8k13r3ek4rxs2tqo7-t@khl4u6sf.de | de
owqwtrs8o2o20s8ri3sb@g4eoas.fr | fr
(2 rows)
Cette requête n'utilise pas d'index :
essais=> EXPLAIN SELECT email FROM EmailsPersons WHERE tld(email) = 'de';
QUERY PLAN
----------------------------------------------------------------------
Seq Scan on emailspersons (cost=0.00..271292.00 rows=5000 width=38)
Filter: (tld(email) = 'de'::text)
(2 rows)
Mais on peut créer un index sur cette fonction avec
essais=> EXPLAIN SELECT email FROM EmailsPersons WHERE tld(email) = 'de';
QUERY PLAN
-----------------------------------------------------------------------------
Bitmap Heap Scan on emailspersons (cost=87.34..9201.36 rows=5000 width=38)
Recheck Cond: (tld(email) = 'de'::text)
-> Bitmap Index Scan on tld_idx (cost=0.00..86.09 rows=5000 width=0)
Index Cond: (tld(email) = 'de'::text)
(4 rows)
Attention toutefois en testant l'utilisation de l'index avec
Pour les deux exemples que j'ai utilisé, où les données sont
stockées sous forme de texte alors qu'elles sont en fait structurées,
une solution meilleure serait peut-être de créer un type
spécifique. C'est ce que fait par exemple
À noter que les index de PostgreSQL ne marchent apparemment pas sur
les requêtes de non-existence. Si on cherche les adresses dans le TLD
Un contournement simple est de transformer le test « différent de
CHAÎNE » en « strictement supérieur ou inférieur à CHAÎNE ». L'index
est alors bien utilisé. Essayons avec le préfixe téléphonique
espagnol, sur une base où ils sont nombreux, de façon à ce que la
négation ne sélectionne pas trop de tuples (car, sinon,
rappelez-vous, PostgreSQL peut décider de ne pas utiliser l'index) :
EXPLAIN SELECT person FROM PhonesPersons WHERE substr(phone, 1, 3) <> '+34'; QUERY PLAN
--------------------------------------------------------------------
Seq Scan on phonespersons (cost=0.00..20564.00 rows=9333 width=4)
Filter: (substr(phone, 1, 3) <> '+34'::text)
(2 rows)
essais=> EXPLAIN SELECT person FROM PhonesPersons WHERE substr(phone, 1, 3) > '+34' OR substr(phone, 1, 3) < '+34';
QUERY PLAN
----------------------------------------------------------------------------------------------
Bitmap Heap Scan on phonespersons (cost=163.34..6109.12 rows=9319 width=4)
Recheck Cond: ((substr(phone, 1, 3) > '+34'::text) OR (substr(phone, 1, 3) < '+34'::text))
-> BitmapOr (cost=163.34..163.34 rows=9333 width=0)
-> Bitmap Index Scan on areacode_idx (cost=0.00..124.28 rows=7459 width=0)
Index Cond: (substr(phone, 1, 3) > '+34'::text)
-> Bitmap Index Scan on areacode_idx (cost=0.00..34.40 rows=1874 width=0)
Index Cond: (substr(phone, 1, 3) < '+34'::text)
(7 rows)
]]>
Cela ne marche qu'avec certains
types d'index, ceux qui sont ordonnés
(
Merci à Mark Byers, Guillaume Lelarge, Dimitri Fontaine, Mathieu Arnold, araqnid et Thomas Reiss pour leur aide érudite.