Nouveautés du planificateur de requêtes de Postgres 16
(citusdata.com)- PostgreSQL 16 ajoute 10 améliorations au planificateur/optimiseur de requêtes, élargissant le choix des plans d’exécution pour
DISTINCT, les agrégations, les jointures, les fonctions de fenêtre et les requêtes sur tables partitionnées SELECT DISTINCT, les agrégations avecORDER BY/DISTINCTet les traitements aprèsMerge Joinexploitent plus activement les entrées partiellement triées, ce qui permet de produire les résultats avec moins de mémoire qu’un tri complet- La prise en charge de Memoize dans
UNION ALL, deRight Anti Joinet des jointures par hachage parallèles pour les jointuresFULL/RIGHTvise à réduire le coût des recherches répétées et de la création de grandes tables de hachage - Les fonctions de fenêtre évitent les traitements
RANGEinutiles et réduisent lesWindowAggqui devaient s’exécuter jusqu’au bout ; certaines fonctions peuvent désormais s’arrêter plus tôt selon les conditions - Toutes ces améliorations sont activées par défaut ; il est donc utile de comparer les
EXPLAINet les temps d’exécution de vos workloads réels avant et après une mise à niveau vers PostgreSQL 16
Périmètre des améliorations du planificateur dans PostgreSQL 16
- PostgreSQL 16 introduit plusieurs améliorations dans le planificateur de requêtes, permettant à de nombreuses requêtes SQL de s’exécuter plus rapidement que dans les versions précédentes de PostgreSQL
- Les améliorations du planificateur incluses dans les notes de version de PG16 sont expliquées plus en détail, avec des comparaisons de sorties
EXPLAINentre PG15 et PG16 ainsi que des exemples de tests reproductibles - Ici, le planificateur correspond au composant couramment appelé optimiseur dans d’autres bases de données relationnelles
Optimisation du tri et de DISTINCT
-
Utilisation d’Incremental Sort dans
SELECT DISTINCT- Incremental Sort a été ajouté pour la première fois dans PostgreSQL 13 ; lorsque les résultats sont déjà triés selon les premières colonnes, il réduit le coût en ne triant que les colonnes restantes
- Le planificateur de PostgreSQL 16 prend aussi en compte Incremental Sort pour les requêtes
SELECT DISTINCT - Par exemple, s’il existe un index btree sur la colonne
aet qu’un ordrea, best nécessaire, il est possible d’obtenir via l’index un résultat trié selona, puis de ne trier quebà chaque changement de valeur dea - Dans le quicksort de PostgreSQL, trier plusieurs petits groupes peut être plus efficace que trier un seul grand groupe
- Dans l’exemple de requête, PG15 utilisait
HashAggregateet un scan séquentiel, tandis que PG16 choisit l’indexdistinct_test_a_idxetIncremental Sort Presorted Key: adans la sortie de PG16 signifie que l’entrée déjà triée selonaa été exploitée- La méthode par hachage de PG15 a déversé environ 30 Mo sur disque, tandis que la mémoire maximale d’
Incremental Sortdans PG16 était de 26 Ko - Le temps d’exécution passe de 414,226 ms avec PG15 à 263,167 ms avec PG16
-
Optimisation des agrégations avec
ORDER BYouDISTINCT- Dans PostgreSQL 15 et les versions antérieures, les fonctions d’agrégation avec une clause
ORDER BYouDISTINCTeffectuaient toujours un tri à l’intérieur du nœudAggregate - Le planificateur de PostgreSQL 16 peut produire un plan d’exécution fournissant au nœud
Aggregatedes lignes dans le bon ordre, et l’exécuteur omet le tri interne si l’entrée est déjà triée - Dans l’exemple
COUNT(DISTINCT b), PG15 comme PG16 utilisentGroupAggregateetIndex Only Scan, mais la sortie de PG15 affichetemp read=4540 written=4560 - Ces E/S de fichiers temporaires résultent du tri implicite de PG15 qui a déversé sur disque
- La sortie de PG16 ne contient pas ces E/S temporaires, et le temps d’exécution passe de 302,693 ms avec PG15 à 115,534 ms avec PG16, soit plus de deux fois plus rapide
- Dans PostgreSQL 15 et les versions antérieures, les fonctions d’agrégation avec une clause
Améliorations des recherches répétées et des plans de jointure
-
Application de Memoize à l’intérieur de
UNION ALL- Le nœud de plan
Memoizea été introduit pour la première fois dans PostgreSQL 14 et agit comme une couche de cache entre uneNested Loopparamétrée et son entrée interne - Le planificateur de PostgreSQL 16 envisage aussi l’utilisation de
Memoizelorsqu’une requêteUNION ALLse trouve à l’intérieur d’uneNested Loopparamétrée - Dans l’exemple, PG15 exécutait
Appendun million de fois, tandis que PG16 placeMemoizeau-dessus d’Append - Le
Memoizede PG16 indiqueHits: 999990,Misses: 10,Memory Usage: 2kB - Le nombre d’exécutions d’
Appendpasse d’un million avec PG15 à 10 avec PG16 - Le temps d’exécution passe de 1926,151 ms avec PG15 à 282,120 ms avec PG16, soit environ 6 fois plus rapide
- Le nœud de plan
-
Prise en charge de Right Anti Join
- Dans un
Hash Joind’unINNER JOIN, il est généralement préférable de construire la table de hachage sur la plus petite table - Une petite table de hachage nécessite moins de travail de construction, est plus favorable au cache CPU et réduit aussi le risque de CPU stall en attendant des données depuis la mémoire principale
- Avant PostgreSQL 16, un
Anti Joinplaçait toujours la table mentionnée dansNOT EXISTSà l’intérieur de la jointure, ce qui pouvait imposer de construire la table de hachage sur la plus grande table - PostgreSQL 16 prend en charge Right Anti Join, ce qui permet de hacher le plus petit des deux côtés
- Dans l’exemple, PG15 hachait la table
larged’un million de lignes avec une utilisation mémoire de 6446 Ko, tandis que PG16 hache la tablesmallde 100 lignes et n’utilise que 12 Ko - Le temps d’exécution passe de 139,023 ms avec PG15 à 77,076 ms avec PG16, soit presque deux fois moins
- Dans un
-
Jointures par hachage parallèles pour les jointures FULL/RIGHT
- PostgreSQL 11 a introduit
Parallel Hash Join, où plusieurs workers parallèles participent à la création d’une seule table de hachage - Le
Parallel Hash Joinde PostgreSQL 16 prend en charge les types de jointureFULLetRIGHT - Les plans
FULL OUTER JOINetRight Joinpeuvent également s’exécuter en parallèle - Dans l’exemple de
FULL JOIN, PG15 utilisait un simpleHash Full Join, tandis que PG16 utiliseParallel Hash Full JoinetGather - La sortie de PG16 affiche
Workers Planned: 1,Workers Launched: 1 - Le temps d’exécution baisse nettement, de 220,677 ms avec PG15 à 129,769 ms avec PG16
- PostgreSQL 11 a introduit
Optimisation des fonctions de fenêtre
-
Omission du traitement RANGE inutile
- Pour les fonctions de fenêtre comme
row_number(),rank(),dense_rank(),percent_rank(),cume_dist()etntile(), PostgreSQL utilise par défaut l’optionRANGEsi la clause de fenêtre ne contient pas d’optionROWS - L’option
RANGEdoit examiner les lignes précédentes pour trouver les peer rows ayant la même valeur de tri, ce qui peut devenir coûteux s’il existe beaucoup de valeurs identiques selonORDER BY - Le comportement de ces fonctions ne change pas selon que
ROWSouRANGEest spécifié, mais avant PostgreSQL 16, l’exécuteur ne pouvait pas faire cette distinction et devait vérifier les peer rows dans tous les cas - Le planificateur de PostgreSQL 16 sait quelles fonctions de fenêtre sont affectées par les options
ROWS/RANGEet transmet à l’exécuteur les informations nécessaires pour ignorer les traitements inutiles - Dans l’exemple
row_number() <= 10, PG15 lisait 50 410 lignes depuis l’index avant de s’arrêter, tandis que PG16 n’en lit que 11 - PG16 exploite le fait qu’une fois
row_numberarrivé à 11, aucune autre ligne ne peut satisfaire la condition<= 10 - Le temps d’exécution passe de 29,775 ms avec PG15 à 0,058 ms avec PG16, soit plus de 500 fois plus rapide
- Pour les fonctions de fenêtre comme
-
Extension de l’arrêt anticipé pour les fonctions de fenêtre monotones croissantes
- PostgreSQL 15 a permis d’arrêter plus tôt l’exécution de
WindowAgglorsque, pour certaines fonctions de fenêtre, une condition de la clauseWHEREne peut plus redevenir vraie après être devenue fausse - PostgreSQL 16 étend cette optimisation à
ntile(),cume_dist()etpercent_rank() - Dans PostgreSQL 15, elle ne s’appliquait qu’à
row_number(),rank(),dense_rank(),count()etcount(*) - Dans l’exemple
percent_rank() <= 0.01, PG15 traitait la condition comme unFilterdans la sous-requête, etWindowAggtraitait les 50 000 lignes - PG16 utilise la même condition comme
Run Conditionet interrompt plus tôt l’exécution deWindowAgg - Le temps d’exécution passe de 84,358 ms avec PG15 à 19,454 ms avec PG16, soit plus de 4 fois plus rapide
- PostgreSQL 15 a permis d’arrêter plus tôt l’exécution de
Tables partitionnées et traitement des DISTINCT triviaux
-
Suppression des LEFT JOIN sur les tables partitionnées
- PostgreSQL peut depuis longtemps supprimer les
LEFT JOINqui ne sont pas nécessaires à une requête et ne peuvent pas créer de lignes en double - Avant PostgreSQL 16, la suppression de LEFT JOIN n’était pas prise en charge pour les tables partitionnées
- La preuve nécessaire pour déterminer si les lignes internes risquaient de dupliquer les lignes externes n’existait pas pour les tables partitionnées
- Le planificateur de PostgreSQL 16 applique aussi l’optimisation de suppression des
LEFT JOINaux tables partitionnées - Cette optimisation peut être particulièrement utile dans les vues
- Car même si une vue comporte beaucoup de colonnes, les requêtes réelles ne consultent pas toujours toutes les colonnes
- Dans l’exemple, le plan de PG15 inclut la jointure vers
part_tab, tandis que le plan de PG16 effectue seulement un scan séquentiel denormal_table
- PostgreSQL peut depuis longtemps supprimer les
-
Traitement d’un DISTINCT au résultat déterminé comme un Limit
- Le planificateur PostgreSQL peut omettre le nœud de plan de déduplication des résultats s’il peut détecter que toutes les lignes ont la même valeur
- PostgreSQL 16 exploite le fait que le résultat ne contient que les mêmes valeurs lorsque toutes les colonnes ciblées par
DISTINCTsont fixées par des conditions d’égalité dans la clauseWHERE, et le traite avecLIMIT 1 - Dans la requête d’exemple
SELECT DISTINCT a,b,c FROM abc WHERE a = 5 AND b = 5 AND c = 5, chaque colonneDISTINCTest limitée à la même valeur - PG15 lit tout le résultat et le réduit à une ligne avec l’opérateur
Unique - PG16 utilise
Limitet un scan séquentiel pour ne renvoyer qu’une seule ligne - Le temps d’exécution passe de 30,381 ms avec PG15 à 0,025 ms avec PG16, soit plus de 1200 fois plus rapide
Utilisation accrue d’Incremental Sort après Merge Join
- Avant PostgreSQL 16, lorsque le planificateur envisageait un
Merge Join, il n’utilisait l’ordre de tri de la jointure que si cet ordre correspondait exactement aux exigences de l’opération supérieureDISTINCT,GROUP BYouORDER BY - Cette règle ne reflétait pas suffisamment le fait qu’
Incremental Sortpeut exploiter une entrée partiellement triée dans l’opération supérieure - PostgreSQL 16 assouplit la règle de prise en compte de l’ordre du
Merge Join, qui passe de « correspondance exacte requise » à « au moins une colonne de tête doit être correctement triée » - Grâce à ce changement, le planificateur peut utiliser plus souvent Incremental Sort pour adapter le résultat d’un
Merge Joinà l’opération supérieure- Incremental Sort exploite les entrées partiellement triées pour trier par petits lots, réduisant ainsi l’utilisation mémoire et le nombre de comparaisons par rapport à un tri complet
- Dans l’exemple, PG15 utilisait un
Sortcomplet aprèsMerge Join, tandis que PG16 utiliseIncremental Sort- La mémoire maximale d’
Incremental Sortdans PG16 était de 26 Ko - Le temps d’exécution diminue légèrement, de 1010,738 ms avec PG15 à 915,589 ms avec PG16, et la mémoire utilisée pour le tri baisse fortement
- La mémoire maximale d’
Mode d’application et vérification en pratique
- Les 10 améliorations du planificateur dans PostgreSQL 16 sont toutes activées par défaut
- Chaque optimisation s’applique dans tous les cas possibles, ou de manière sélective lorsque le planificateur estime qu’elle est utile
- Si vous utilisez une version antérieure de PostgreSQL, vous pouvez exécuter votre workload réel sur PostgreSQL 16 pour identifier les requêtes qui deviennent plus rapides
- Les retours d’expérience en production peuvent être partagés sur la liste de diffusion pgsql-general@postgresql.org
1 commentaires
Avis Hacker News
Ce serait vraiment bien si le query planner de PostgreSQL pouvait replanifier une requête en cours d’exécution
Les requêtes pathologiquement lentes viennent souvent du fait que le planner ne connaît pas les informations nécessaires sur la distribution des données et estime mal les coûts, ce qui peut facilement produire un écart de 1000x, par exemple avec un temps d’exécution de 1 s au lieu de 1 ms
Comme les statistiques de table ne peuvent pas être exactes à 100 %, si, après le démarrage de la requête, l’avancement est plus lent que prévu, il serait bien de réinjecter dans le planner les informations de progression courantes, comme le nombre de pages parcourues et les tuples correspondants, afin de produire un nouveau plan
Mais PostgreSQL envoie les résultats en streaming au fur et à mesure au lieu d’attendre de tout produire avant l’envoi, donc changer de plan au milieu demanderait de suivre les résultats déjà envoyés au client, ce qui implique de gros changements d’infrastructure
En plus, le client peut aussi inverser le sens au milieu de la requête et redemander les résultats précédents dans l’ordre inverse, ce qui augmente encore la complexité
Il n’y a même pas de garantie qu’un nouveau plan renverra les mêmes tuples. Par exemple, avec
SELECT * FROM table LIMIT 10, en l’absence deORDER BY, les tuples retournés sont non déterministesIl serait peut-être plus simple d’accumuler X tuples dans une file, puis de ne commencer à les envoyer qu’une fois la file pleine. Une fois cette file remplie, on considérerait qu’il est trop tard pour replanifier et on figerait le plan actuel
L’utilisateur pourrait ajuster X afin d’avoir plus de temps pour changer de plan, en échange de plus de mémoire consommée et d’un délai accru avant le premier tuple
La nouvelle requête ne pourrait pas simplement sauter les N premiers résultats et devrait comparer chaque ligne déjà envoyée avec un dictionnaire
J’utilise cet outil pour visualiser les requêtes : https://explain.dalibo.com/
Il y a aussi https://www.pgexplain.dev/, dont la sortie était moins bonne auparavant, mais aujourd’hui les deux me semblent comparables
Je me demande s’il existe un outil d’assainissement de plans d’exécution qui aide dans ce type de situation
Les améliorations du query planner sont toujours les bienvenues, et c’est une partie extrêmement importante d’une base de données. Bien sûr, c’est surtout quand il ne fait pas ce que je veux que cela saute aux yeux
Personnellement, ce qui m’a souvent frustré récemment, c’est le JIT dans les versions récentes de PostgreSQL. Les heuristiques qui décident quand l’utiliser ne me semblent pas du tout robustes
Je l’ai vu sur des requêtes typiquement générées par des ORM : la requête elle-même est simple, mais elle embarque beaucoup de tables via des jointures. Sans JIT, cela se termine en quelques millisecondes, alors qu’avec JIT il ajoute 1 à 1,5 seconde, ce qui rend le tout extrêmement lent même sur de petits volumes de données
Maintenant, je sais qu’il suffit de désactiver JIT, mais pour un utilisateur qui n’a pas encore compris pourquoi c’est lent, cela peut fortement dégrader son impression de PostgreSQL. J’aime PostgreSQL, mais laisser JIT activé par défaut me semble bien trop risqué
Dans PG16, il ne regarde que le coût total estimé du plan, sans tenir compte du nombre d’expressions à compiler
Compiler quelques expressions est rapide, mais lorsqu’on interroge une table partitionnée avec des centaines de partitions et qu’elles figurent toutes dans le plan, le compilateur JIT a beaucoup de travail
Avec un collègue, nous avons du code pour améliorer cela, mais à ce stade il n’est pas certain qu’il entre dans PG17
Même en cherchant dans les discussions de la mailing list PostgreSQL à propos de JIT, je n’ai pas trouvé de raison convaincante
Sur des charges de travail OLTP, il vaut mieux désactiver JIT
Je n’utilise pas d’ORM, donc ce n’est pas seulement à cause de schémas de requêtes bizarres
En revanche, la parallélisation des requêtes peut réellement être utile et, surtout, elle ne nuit que rarement
Après avoir mis à jour quelques paquets via
apt, une grosse requête exécutée toutes les 5 minutes a soudainement commencé à échouer. Plus précisément, PostgreSQL coupait silencieusement la connexion au milieu de l’exécution, sans rien écrire dans les logsEn testant manuellement avec
EXPLAIN, j’ai constaté que seule la variante de la requête qui activait JIT cassait, tandis que celle qui ne l’utilisait pas fonctionnait bien. Une fois JIT désactivé, tout est revenu à la normaleJe me demande à quelle fréquence ces changements ont un effet dans des requêtes réelles. En particulier, le changement « utiliser
Limitau lieu deUniquepour implémenterDISTINCTquand c’est possible » donne l’impression de ne s’appliquer qu’à des requêtes vraiment absurdesJe me demande si les développeurs de PostgreSQL disposent d’informations pour en juger
Si les améliorations de DISTINCT rendent le système plus robuste face aux mauvaises requêtes, c’est très bénéfique. Cela ne corrigera pas tous les problèmes, mais toute amélioration est bonne à prendre
pgsql-hackersJe suis d’accord pour dire qu’il est peu probable que cela s’applique souvent, mais l’avantage est que détecter si cela s’applique revenait à quelque chose d’aussi simple que vérifier si un pointeur est
NULLLa détection est très simple et, la plupart du temps, cela ne s’appliquera pas, mais quand c’est applicable, cela peut apporter un gain de performance considérable
Ce n’est sans doute pas un problème extrêmement courant, mais je ne serais pas surpris qu’il apparaisse de temps en temps
select distinct email from users where email = ?Je ne pense pas qu’il y ait jamais eu plus de 100 lignes avec la même adresse e-mail. La plupart étaient des utilisateurs de test qu’on aurait pu supprimer, mais je m’égare
J’aimerais qu’il existe dans PostgreSQL un mode strict pour tester les applications. Un mode qui examine uniquement la requête elle-même et, indépendamment des statistiques, renvoie une erreur si un index absent ferait qu’une requête ne s’améliore asymptotiquement qu’en sa présence
J’aimerais aussi une commande
CREATE INDICES FORqui crée les index concernés pour les mises à niveau d’application, ainsi qu’un mode de création automatique d’index pour l’usage interactif et le développementPlus généralement, le système devrait être conçu de sorte qu’aucune exécution asymptotiquement sous-optimale ne puisse jamais se produire
Je ne comprends pas pourquoi ils n’implémentent pas les hints
pg_hint_plan. Le danger des hints, c’est que même s’ils sont corrects au moment où on les écrit, ils peuvent devenir contre-productifs si la taille des tables ou la distribution des données changeD’après ce dont je me souviens des anciennes discussions sur les hints, il n’y avait pas d’opposition générale tant que cela ne contraignait pas trop fortement le planner et que cela lui permettait de s’adapter aux changements des données sous-jacentes
Par exemple, au lieu d’indiquer qu’un prédicat donné correspond à 10 lignes, on lui signalerait qu’il existe une corrélation entre deux colonnes
https://news.ycombinator.com/item?id=2179433 (60 commentaires, 2011)
La position officielle sur le wiki PostgreSQL est ici : https://wiki.postgresql.org/wiki/OptimizerHintsDiscussion
En substance : « nous ne sommes pas intéressés par les hints exactement tels qu’ils sont couramment implémentés dans d’autres bases de données »
Parmi les problèmes des systèmes de hints existants : ils dégradent la maintenabilité du code applicatif, compliquent les montées de version, encouragent de mauvaises habitudes chez les DBA et ne s’adaptent pas bien aux changements d’échelle des données
Je ne veux pas vraiment leur reprocher cette position, mais c’est frustrant quand PostgreSQL choisit un plan stupide et qu’on ne peut pas le convaincre de faire un choix raisonnable alors qu’on sait qu’il se trompe
Un ami, DBA Microsoft pour des entreprises de taille intermédiaire, a dit qu’on ne pouvait pas faire de choses sérieuses avec PostgreSQL. Il aurait même été choqué d’apprendre que PostgreSQL n’avait pas de query planner
En laissant la moquerie de côté un instant, je me demande s’il y a du vrai dans l’affirmation plus large selon laquelle MSSQL peut gérer des charges à une échelle pour laquelle PostgreSQL ne conviendrait pas. Intuitivement, ça me semble absurde, mais je ne suis pas DBA du tout
Ils ont historiquement résolu les problèmes en y injectant de l’argent et du matériel — donc encore plus d’argent — jusqu’à ce qu’ils disparaissent. Il y a bien sûr aussi de la technique intelligente, mais au fond ils ont bénéficié de bien plus d’ingénierie pendant bien plus longtemps
Ils peuvent faire du scale-out horizontal à une échelle plus grande que ce que PostgreSQL peut raisonnablement faire
Cela dit, PostgreSQL rattrape son retard, et on pourrait aussi dire que MySQL/MariaDB a toujours eu une histoire plutôt correcte sur ce point. Les options de montée en charge horizontale s’améliorent en permanence
Aujourd’hui, il est devenu plus simple d’exploiter un cluster PostgreSQL de plusieurs téraoctets sur un petit nombre de machines avec un trafic important, tout en plaçant le « big data » dans des bases plus spécialisées. L’ancienne approche qui consistait à tout faire entrer dans MSSQL/Oracle est peut-être un peu datée
Ce à quoi ton ami faisait peut-être allusion, c’est au fait que PostgreSQL n’a pas de moyen de mettre en cache ou de figer un plan de requête. PostgreSQL replannifie chaque instruction à moins d’utiliser manuellement des instructions préparées, et encore, cela ne fonctionne qu’au niveau de la connexion
MSSQL met en cache et réutilise les plans depuis longtemps, ce qui permet au planner de consacrer plus de temps à l’élaboration du plan. Il y a aussi des hints, et on peut figer un plan
PostgreSQL aurait vraiment besoin de hints. Même si l’optimiseur est excellent, parfois je sais mieux que lui et j’aimerais pouvoir le forcer à m’écouter
PostgreSQL n’a pas non plus de véritable index clusterisé et toutes les tables sont des heaps. Dans MSSQL, on utilise cela très souvent, en général en définissant la clé primaire comme index clusterisé, si bien que la table elle-même devient l’index et qu’il n’y a pas d’indirection pour les recherches par clé
Fait intéressant, SQLite est l’inverse : une table y a toujours un index clusterisé, qu’on le crée ou non, tandis que MSSQL permet de choisir entre heap et table organisée par index
Il existe aussi des exemples de très grosses bases PostgreSQL qui fonctionnent bien, donc PostgreSQL peut clairement passer à l’échelle
Cela dit, SQL Server a des fonctionnalités que PostgreSQL n’a pas, et si elles comptent vraiment, il peut mieux convenir à certains cas d’usage. Au final, ce sont simplement deux bases de données différentes, avec des forces et des faiblesses différentes
Au départ, j’allais écrire que j’aurais recommandé à mon entreprise de migrer vers PostgreSQL si nous n’avions pas eu des applications fournies par des éditeurs qui exigeaient SQL Server
Puis j’ai réalisé tout ce qu’il faudrait remplacer de ce que Microsoft inclut : reporting services, integration services, jobs, intégration AD, service broker, etc.
notify/listenn’a pas de types de messagesNous n’utilisons plus analysis services, mais à l’époque où nous l’utilisions, cela aussi aurait été difficile à remplacer
C’est ce genre de choses qui retient les gens. Je n’ai aucune idée du temps qu’il faudrait pour remplacer tout cela, et passer un an à remplacer ce qu’on a déjà n’offre pas un bon retour sur investissement
Je me demande pourquoi cela a été publié chez citusdata plutôt que sur postgresql.org. Je ne sais pas si c’est réservé à des fonctionnalités payantes ou si c’est un ajout open source
À quand l’utilisation d’un index pour accélérer les requêtes
IS NOT DISTINCT FROM? ;)