Exploration SQL
(lecture : 7 minutes)
Article technique aujourdâhui. Lorsque jâai dĂ©veloppĂ© la page des profils de Flus, jâai fait face Ă un bug qui mâa bien occupĂ©. Pas le genre de bug trĂšs grave, mais suffisamment pĂ©nible pour que je veuille le corriger.
Contexte : un profil liste les six liens publiĂ©s le plus rĂ©cemment par une personne. Pour quâun lien soit publiĂ©, il doit faire partie dâune collection publique et ne pas ĂȘtre masquĂ©. Un lien peut faire partie de plusieurs collections et une collection peut contenir plusieurs liens ; on a donc une relation « many-to-many ». La date de publication est associĂ©e Ă une collection ; il peut donc y avoir plusieurs dates de publication pour un mĂȘme lien. Mon problĂšme : les liens publiĂ©s dans plusieurs collections pouvaient apparaĂźtre plusieurs fois.
Je vais vous illustrer tout ça par des requĂȘtes SQL. Pour simplifier le problĂšme, on va considĂ©rer que toutes les collections sont publiques et les liens tous visibles.
CREATE TABLE links (
id SERIAL PRIMARY KEY,
url TEXT NOT NULL
);
CREATE TABLE collections (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE links_to_collections (
link_id INTEGER NOT NULL REFERENCES links,
collection_id INTEGER NOT NULL REFERENCES collections,
published_at TIMESTAMP NOT NULL
);
INSERT INTO links (url)
VALUES ('flus.fr'),
('marienfressinaud.fr'),
('framasoft.org');
INSERT INTO collections (name)
VALUES ('Mes favoris'),
('Mes partages');
INSERT INTO links_to_collections (link_id, collection_id, published_at)
VALUES (1, 1, '2022-01-17'),
(2, 2, '2022-01-18'),
(3, 2, '2022-01-19'),
(1, 2, '2022-01-20');
Que fait-on dans ces quelques lignes de SQL ? On commence par créer les trois tables qui vont contenir les données (links, collections et la table faisant la jonction links_to_collections). On insÚre ensuite trois liens et deux collections. Enfin, on fait le lien entre les liens et les collections :
- flus.fr dans « Mes favoris » le 17 janvier ;
- marienfressinaud.fr dans « Mes partages » le 18 janvier ;
- framasoft.org dans « Mes partages » le 19 janvier ;
- enfin, à nouveau flus.fr dans « Mes partages » le 20 janvier.
Commençons par récupérer les trois liens les plus récents avec leur date de publication :
SELECT l.id, l.url, lc.published_at
FROM links l
INNER JOIN links_to_collections lc
ON lc.link_id = l.id
ORDER BY lc.published_at DESC, l.id
LIMIT 3;
Cette requĂȘte nous retourne le rĂ©sultat suivant :
| id | url | published_at |
|---|---|---|
| 1 | flus.fr | 2022-01-20 |
| 3 | framasoft.org | 2022-01-19 |
| 2 | marienfressinaud.fr | 2022-01-18 |
On a donc les lignes que lâon souhaite. Seulement, quâarrive-t-il si on change la limite de trois Ă six ?
| id | url | published_at |
|---|---|---|
| 1 | flus.fr | 2022-01-20 |
| 3 | framasoft.org | 2022-01-19 |
| 2 | marienfressinaud.fr | 2022-01-18 |
| 1 | flus.fr | 2022-01-17 |
Mince, flus.fr apparaĂźt dĂ©sormais deux fois car il se trouve dans deux collections, avec deux dates de publication diffĂ©rentes. On ne le veut quâune seule fois Ă sa date la plus rĂ©cente ! Essayons dâajouter une clause DISTINCT :
SELECT DISTINCT l.id, l.url, lc.published_at
FROM links l
INNER JOIN links_to_collections lc
ON lc.link_id = l.id
ORDER BY lc.published_at DESC, l.id
LIMIT 6;
| id | url | published_at |
|---|---|---|
| 1 | flus.fr | 2022-01-20 |
| 3 | framasoft.org | 2022-01-19 |
| 2 | marienfressinaud.fr | 2022-01-18 |
| 1 | flus.fr | 2022-01-17 |
MĂȘme rĂ©sultat : normal, DISTINCT sâapplique Ă toutes les colonnes de la sĂ©lection, published_at inclus. Heureusement, PostgreSQL propose une autre syntaxe :
SELECT DISTINCT ON (l.id) l.id, l.url, lc.published_at
FROM links l
INNER JOIN links_to_collections lc
ON lc.link_id = l.id
ORDER BY lc.published_at DESC, l.id
LIMIT 6;
AĂŻe, cette fois on obtient une erreur : « ERROR: SELECT DISTINCT ON expressions must match initial ORDER BY expressions ». Elle indique que la colonne du DISTINCT ON (l.id) doit se trouver Ă gauche dans la liste de lâORDER BY. Mais si lâid se trouve Ă gauche, nos liens ne seront plus triĂ©s dans le bon ordre !
Ă ce niveau-lĂ , jâarrive au bout de ma crĂ©ativitĂ© en termes de SQL et il me faut faire appel Ă StackOverflow. Heureusement, je ne suis pas le premier Ă mâĂȘtre posĂ© la question. La piste Ă©tait bonne1, mais il nous faut maintenant dĂ©placer tout ça dans une sous-requĂȘte qui sera triĂ©e Ă nouveau :
SELECT l2.* FROM (
SELECT DISTINCT ON (l.id) l.id, l.url, lc.published_at
FROM links l
INNER JOIN links_to_collections lc
ON lc.link_id = l.id
ORDER BY l.id, lc.published_at DESC
) l2
ORDER BY l2.published_at DESC, l2.id
LIMIT 6;
EtâŠ
| id | url | published_at |
|---|---|---|
| 1 | flus.fr | 2022-01-20 |
| 3 | framasoft.org | 2022-01-19 |
| 2 | marienfressinaud.fr | 2022-01-18 |
Tadam ! Câest ce quâil nous fallait !
Maintenant ajoutez à ça toute la complexitĂ© dâun vrai systĂšme, avec le calcul du nombre de commentaires par lien, lâĂ©tat de lecture pour lâutilisateur courant, dâautres trucs Ă calculer dynamiquement, etc. et vous obtenez une requĂȘte des plus agrĂ©ables (non). NâhĂ©sitez pas Ă me faire savoir si vous avez une solution plus Ă©lĂ©gante ! đ
Mise à jour du 12 février.
Heureusement, jâai des copains plus malins que moi et qui apprĂ©cient particuliĂšrement le SQL. Merci Ă François pour cette solution bien meilleure Ă base de MAX â pour rĂ©cupĂ©rer le published_at le plus rĂ©cent â et de GROUP BY, tout simplement.
SELECT l.id, l.url, MAX(lc.published_at) AS published_at
FROM links l
INNER JOIN links_to_collections lc
ON lc.link_id = l.id
GROUP BY l.id
ORDER BY published_at DESC, l.id
LIMIT 6;
-
Notez que la rĂ©ponse StackOverflow propose une seconde approche qui nâest pas propre Ă PostgreSQL. Je la trouve personnellement un poil moins facile Ă relire, donc je suis restĂ© avec la solution prĂ©sentĂ©e dans cet article. Ce ne sera pas forcĂ©ment votre cas ! ↩