Optimisation SQL

Module J3

Dalibo SCOP

24.09

29 août 2024

Sur ce document

Formation Module J3
Titre Optimisation SQL
Révision 24.09
PDF https://dali.bo/j3_pdf
EPUB https://dali.bo/j3_epub
HTML https://dali.bo/j3_html
Slides https://dali.bo/j3_slides

Licence Creative Commons CC-BY-NC-SA

Cette formation est sous licence CC-BY-NC-SA. Vous êtes libre de la redistribuer et/ou modifier aux conditions suivantes :

  • Paternité
  • Pas d’utilisation commerciale
  • Partage des conditions initiales à l’identique

Marques déposées

PostgreSQL® Postgres® et le logo Slonik sont des marques déposées par PostgreSQL Community Association of Canada.

Versions de PostgreSQL couvertes

Ce document ne couvre que les versions supportées de PostgreSQL au moment de sa rédaction, soit les versions 12 à 16.

PostgreSQL : Optimisations SQL

PostgreSQL

Introduction

L’optimisation doit porter sur :

  • Le matériel
    • serveur, distribution, kernel, stockage, réseau…
  • Le moteur de la base :
    • postgresql.conf & co
    • l’organisation des fichiers de PostgreSQL
  • L’application
    • schéma, requêtes, vues…

Axes d’optimisation

« 80% des effets sont produits par 20% des causes. » (Principe de Pareto)

  • Il est illusoire d’optimiser une application sans connaître les sources de ralentissement
  • Cibler l’optimisation :
    • trouver ces « 20% »
    • ne pas micro-optimiser ce qui n’a pas d’influence

Quelles requêtes optimiser ?

Seul un certain nombre de requêtes sont critiques

  • Identification (outil de profiling)
    • à optimiser prioritairement
  • Différencier
    • longues en temps cumulé = coûteuses en ressources serveur
    • longues et interactives = mauvais ressenti des utilisateurs

Recherche des axes d’optimisation

Quelques profilers :

SQL et requêtes

Le SQL :

  • est un langage déclaratif :
    • on décrit le résultat, pas la façon de l’obtenir
    • c’est le travail de la base de déterminer le traitement à effectuer
  • décrit un traitement ensembliste :
    • ≠ traitement procédural
    • « on effectue des opérations sur des relations pour obtenir des relations »
  • est normalisé

Opérateurs relationnels

Les opérateurs purement relationnels :

  • Projection = SELECT
    • choix des colonnes
  • Sélection = WHERE
    • choix des enregistrements
  • Jointure = FROM/JOIN
    • choix des tables
  • Bref : tout ce qui détermine sur quelles données on travaille

Opérateurs non-relationnels

Les autres opérateurs sont non-relationnels :

  • ORDER BY
  • GROUP BY/DISTINCT
  • HAVING
  • sous-requête, vue
  • fonction (classique, d’agrégat, analytique)
  • jointure externe

Données utiles

Le volume de données récupéré a un impact sur les performances.

  • N’accéder qu’aux tables nécessaires
  • N’accéder qu’aux colonnes nécessaires
    • viser Index Only Scan
    • se méfier : stockage TOAST
  • Plus le volume de données à traiter est élevé, plus les opérations seront lentes :
    • tris et Jointures
    • éventuellement stockage temporaire sur disque pour certains algorithmes

Limiter le nombre de requêtes

SQL : langage ensembliste

  • Ne pas faire de traitement unitaire par enregistrement
  • Utiliser les jointures, ne pas accéder à chaque table une par une
  • Une seule requête, parcours de curseur
  • Fréquent avec les ORM

Sous-requêtes dans un IN

Un Semi Join peut être très efficace (il ne lit pas tout)

SELECT * FROM t1
  WHERE val1  IN (  SELECT val2 …  )
  • Sinon attention s’il y a beaucoup de valeurs dans la sous-requête !
    • dédoublonner :
    SELECT * FROM t1
      WHERE val1 IN (  SELECT  DISTINCT  val2 …  )
    • surtout : réécriture avec EXISTS (si index disponible)

Sous-requêtes liées

À éviter :

SELECT a,b
  FROM t1
  WHERE val IN (  SELECT f(b)  )
  • un appel de fonction ou sous-requête par ligne !
  • est-ce voulu ?
  • transformer en clause WHERE
  • penser à la clause LATERAL

Sous-requêtes : équivalences IN/EXISTS/LEFT JOIN

Ces sous-requêtes sont strictement équivalentes (Semi-join) :

SELECT * FROM t1
WHERE fk IN ( SELECT pk FROM t2 WHERE … )

SELECT * FROM t1
WHERE EXISTS  ( SELECT 1 FROM t2 WHERE t2.pk = t1.fk AND … )

SELECT t1.*
FROM   t1   LEFT JOIN t2   ON (t1.fk=t2.pk)
WHERE
     t2.id IS NULL

(Et Anti-join pour les variantes avec NOT)

  • Attention à NOT IN : préférer NOT EXISTS

Les vues

Une vue est une requête pré-déclarée en base.

  • Équivalent relationnel d’une fonction
  • Si utilisée dans une autre requête, elle est traitée comme une sous-requête
  • et inlinée
  • Pas de problème si elle est relationnelle…

Éviter les vues non-relationnelles

  • Attention aux vues avec DISTINCT, GROUP BY etc.
    • tous les problèmes des sous-requêtes déjà vus
    • impossible de l’inliner
    • barrière d’optimisation
    • …et mauvaises performances
  • Les vues sont dangereuses en termes de performance
    • masquent la complexité
  • Penser aux vues matérialisées si la requête est lourde

Accès aux données

L’accès aux données est coûteux.

  • Quelle que soit la base
  • Dialogue entre client et serveur
    • plusieurs aller/retours potentiellement
  • Analyse d’un langage complexe
    • SQL PostgreSQL : gram.y de 19000 lignes
  • Calcul de plan :
    • langage déclaratif => converti en impératif à chaque exécution

Coût des connexions

Se connecter coûte cher :

  • Authentification, permissions
  • Latence réseau
  • Négociation SSL
  • Création de processus & contexte d’exécution
  • Acquisition de verrous

→ Maintenir les connexions côté applicatif, ou utiliser un pooler.

Penser relationnel

  • Les spécifications sont souvent procédurales, voire objet !
  • Prendre du recul
  • Réfléchir de façon ensembliste
    • on travaille sur des ensembles de données
    • penser aux CTE (WITH)

Pas de DDL applicatif

Le schéma est la modélisation des données

  • Une application n’a pas à y toucher lors de son fonctionnement normal + exception : tables temporaires
  • SQL manipule les données en flux continu :
    • chaque étape d’un plan d’exécution n’attend pas la fin de la précédente
    • donc : une table temporaire est souvent une perte de temps

Optimiser chaque accès

Les moteurs SQL sont très efficaces, et évoluent en permanence

  • Ils ont de nombreuses méthodes de tri, de jointure, choisies en fonction du contexte
  • En SQL :
    • optimisation selon volume & configuration
    • évolution avec le moteur
  • Dans l’application cliente : vous devrez le maintenir et l’améliorer
  • Faites le maximum côté SQL :
    • agrégats, fonctions analytiques, tris, numérotations, CASE, etc.
    • Commentez avec -- et /* */

Ne faire que le nécessaire

Prendre de la distance vis-à-vis des spécifications fonctionnelles (bis) :

  • Ex : mise à jour ou insertion ?
    • tenter la mise à jour, et regarder combien d’enregistrements ont été mis à jour
    • surtout pas de COUNT(*)
    • éventuellement un test de l’existence d’un seul enregistrement
    • gérer les exceptions plutôt que de vérifier préalablement que les conditions sont remplies (si l’exception est rare)
    • et se renseigner sur la syntaxe

Index

  • Objets destinés à l’optimisation des accès
  • À poser par les développeurs :
CREATE INDEX ON ma_table (nom colonne) ;

Impact des transactions

  • Verrous : relâchés à la fin de la transaction
    • COMMIT
    • ROLLBACK
  • Validation des données sur le disque au COMMIT
    • écriture synchrone : coûteux
  • Contournements :
    • tables temporaires/unlogged ?
    • parfois : synchronous_commit = off (…si perte possible)
  • → Faire des transactions qui correspondent au fonctionnel
    • pas trop nombreuses
    • courtes, pas de travail inutile une fois des verrous posés

Verrouillage et contention

  • Chaque transaction prend des verrous :
    • sur les objets (tables, index, etc.) pour empêcher au moins leur suppression ou modification de structure pendant leur travail
    • sur les enregistrements
    • libérés à la fin de la transaction : les transactions très longues peuvent donc être problématiques
  • Sous PostgreSQL, on peut quand même lire un enregistrement en cours de modification : on voit l’ancienne version (MVCC)

Deadlocks

« Verrous mortels » : comment les éviter ?

  • Théorie : prendre toujours les verrous dans le même ordre
  • Pratique, ça n’est pas toujours possible ou commode
  • Conséquence : une des transactions est tuée
    • erreurs, ralentissements

Base distribuée

Écrire sur plusieurs nœuds ?

  • Complexité (applicatif/exploitation)
    • → risque d’erreur (programmation, fausse manipulation)
    • reprise d’incident complexe
  • Essayez avec un seul serveur plus gros
    • après avoir optimisé bien sûr
    • PostgreSQL peut vous étonner

Bibliographie

  • Quelques références :
    • The Art of SQL, Stéphane Faroult
    • Refactoring SQL Applications, Stéphane Faroult
    • SQL Performance Explained, Markus Winand
    • Introduction aux bases de données, Chris Date
    • The Art of PostgreSQL, Dimitri Fontaine
    • Vidéos de Stéphane Faroult sous Youtube

Quiz