Notes de cours sur PostgreSQL
Mis à jour le 18/03/2025

Installation

à faire avant de démarrer le PPT

  • Windows
    mettre les données sur D:\pg<version>/data (à créer)
  • Linux
  • psql vers chaque OS
    tout faire avec psql/linux après avoir vu les limites de psql/Windows
    faire un fichier pg-vars.bat pour psql/Windows
c:
cd C:\Program Files\PostgreSQL\15\bin

set PGDATA=D:\pg15\data
set PGPORT=5432
set PGUSER=postgres
set PGDATABASE=postgres

OUTILS

  • VSC
  • dbeaver

Gestion de la RAM

shared_buffers

mémoire dédiée au cache de données -> toute l’instance => toutes les Bases
défaut: 128MB
Conseil: entre 15 et 25% de la RAM
limité à 8GB car au delà, on empiète sur effective_cache_size et les checkpoint peuvent être lents

effective_cache_size

estimation du cache système effectif
utile pour le query planner
une valeur trop faible va privilégier les scans de table plutôt que les scans d’index
Conseil: 50% de la RAM
Mais on peut être plus agressif: 75% si:

  • serveur dédié
  • grosse base
  • requêtes qui retournent de gros volumes

wal_buffers

cache du LOG
défaut: -1 => 1/32 shared_buffers dans la limite de 16MB
Faire des tests pour voir si une valeur supérieure améliore les performances (notamment dans le cas de fortes concurrence car snapshots dans wal)

work_mem

utilisé par les tris, distinct, merge join, group by et les hash stockés en fichiers temporaires
a un impact sur l’usage des fichiers temporaires (donc I/O disques)
défaut: 4MB
La bonne valeur est fonction des requêtes
Conseil: Total RAM * 0.25 / max_connections
peut être assigné à 1 rôle:

alter user test set work_mem='4GB';

maintenance_work_mem

utilisé par: vacuum, create index, reindex, alter table add foreign key …
défaut: 64MB
Conseil: Total RAM * 0.05 et devrait être supérieur à work_mem

temp_buffers

cache des tables temporaires, des tris
défaut: 8MB dépend des besoins
Attention: ne peut affecté qu’UNE FOIS PAR SESSION avant l’usage de toute table temporaire

Autres paramètres

max_connections & max_worker_processes

Tablespaces

regroupement du stockage par analogie aux schemas

2 par défaut

  • pg_default: les données
  • pg_global: tables systèmes

ainsi, on peut séparer les données de table des indexs

difficile à déplacer -> https://it-inzhener.com/en/articles/detail/postgresql-moving-tablespaces

pg_basebackup est plus complexe à utiliser (–tablespace-mapping & tablespace_map file)

Remarque: Dans pg_class, si reltablespace vaut 0 alors cela signifie que l’objet est dans le tablespace par défaut de la base

-- après avoir créer le répertoire et mis les bons droits (sous linux, il suffit que l'utilisateur postgres crée le répertoire)
CREATE TABLESPACE tb_test1 OWNER postgres LOCATION 'D:/pg15/tablespaces';

create table in_tb (id int) tablespace tb_test1 ;

SELECT relname FROM pg_class
WHERE reltablespace = ( SELECT oid FROM pg_tablespace
                        WHERE spcname = 'tb_test1');

ALTER DATABASE nom_bdd SET TABLESPACE tb_test1 ;

COPY

-- csv -> table
copy tableName from 'path.csv' with (format 'csv',HEADER);
-- HEADER signifie que le csv contient un header identique à la structure de la table

-- table->csv
copy tableName to 'path.csv' with (format 'csv',HEADER);

-- mais aussi
copy (delete from bruno where id=256 returning *) to 'e:\delete_bruno.csv'  with (format 'csv',HEADER);
-- mais aussi: copy (select ....) 

CONNEXIONS

pg_hba.conf
liste de règles ordonnées

changer mdp:
alter user postgres with password 'a';

Connexion automatique avec fichier de mots de passe

dans %APPDATA%
créer rep postgresql
fichier: pgpass.conf
csv sans entête de 5 colonnes séparées par “:”
hostname:port:database:username:password
ex:
:5433::postgres:a

Gestion des rôles

que des rôles (concept de user+group)
1 rôle peut hériter d’un autre, c’est ce qui permet de mettre un rôle dans un rôle

exemple:

-- ici c'est contextuel à la bdd sauf create role (donc grant et revoke agissent sur l'instance, deny n'existe pas)
create role cours_postgres;
\dg+
grant select on t1,t2 to cours_postgres; 
create role bruno with login password 'a';
GRANT CONNECT ON DATABASE test TO bruno;
create role jpp with login password 'a' noinherit;
grant cours_postgres to bruno,jpp;
grant select,update on t1,t2 to jpp;

grant update on all tables in schema public to jpp;
select * from pg_roles where rolname='jpp';
select * from pg_user;
select grantee,table_name,privilege_type from information_schema.table_privileges  where grantee='jpp';

-- droits sur les schemas
grant usage on SCHEMA hr to bruno ;
grant all on all tables in schema hr to bruno ;
-- ceci est pour les tables existantes à l'instant t (pas les futures tables), donc pour du dynamisme, ajouter:
alter default privileges in schema hr grant all on tables to bruno ;

-- droit par colonne
grant select (val) on t1 to cours_postgres ; 

la suppression d’un rôle nécessite qu’il ne soit propriétaire d’aucun objet
OU

create role tmp
reassign owned by cours_postgres to tmp;

sauf depuis v15 (cf https://supabase.com/blog/new-in-postgres-15)
Tout le monde peut créer des tables dans le schéma public
cf: https://stackoverflow.com/questions/69273183/allow-create-table-for-some-users-not-all#:~:text=PostgreSQL%20doesn’t%20have%20a,other%20objects)%20in%20that%20schema
PostgreSQL doesn’t have a special CREATE TABLE privilege, everybody is always allowed to create tables. Creating tables is restricted by schema permissions: if you grant a role the CREATE privilege on a schema, that role can create tables (and other objects) in that schema. So grant CREATE on a schema to the one role and don’t grant CREATE on any schema to the other role.
OU
https://dba.stackexchange.com/questions/35316/why-is-a-new-user-allowed-to-create-a-table
qui explique qu’il y a un rôle caché nommé “public” dont tout le monde est membre, et conseille:
REVOKE CREATE ON SCHEMA public FROM public;

J’ai trouvé aussi:
REVOKE ALL ON SCHEMA public FROM PUBLIC;
du coup plus aucun droit sur le schéma public, donc il faut faire:
GRANT USAGE ON SCHEMA public TO user1;
Le droit USAGE est le droit d’utiliser les objets du schéma. Par analogie, c’est comme le droit x sur un répertoire Posix.
The rights tests are done in order:

Do you have `USAGE` on the schema? 
    No:  Reject access. 
    Yes: Do you also have the appropriate rights on the table? 
        No:  Reject access. 
        Yes: Check column privileges.

Rôle dans un rôle et Affichage

Jusqu’à la version 15, \du/\dg affichait une colonne Member of, elle a depuis disparue !

On la récupère avec : \drg

Exemple :

grant pg_read_all_data to bruno;
\drg
                  List of role grants
 Role name |    Member of     |   Options    | Grantor  
-----------+------------------+--------------+----------
 bruno     | cours_postgres   | INHERIT, SET | postgres
 bruno     | pg_read_all_data | INHERIT, SET | postgres
 jpp       | cours_postgres   | INHERIT, SET | postgres

Journalisation de l’activité / traces

-> fichiers de trace
peu gourmand en ressources

Configuration

alter system set logging_collector=on;

-- si absent, un rep est créé dans le rep de l'instance pour le stockage
alter system set log_directory='F:\psqlSaves-LOG\STAGE\log';

-- ce qu'on veut dans les log
-- toute instruction de durée >0 => tout
alter system set log_statement='all';
alter system set log_min_duration_statement=0;
-- login
alter system set log_connections=on;
alter system set log_disconnections=on;

alter system set log_checkpoints=on;
alter system set log_lock_waits=on;
-- si log_temp_files=2 on a l'evt à partir du 2/3ème fichier temp
alter system set log_temp_files=0;

Sauvegardes

  • physique = copie de fichiers
  • logique = export

DUMP/sauvegardes logiques

  • pg_dump -> pour une bdd vers un script SQL ou un fichier binaire
  • pg_dump_all -> pour toute l’instance sous forme d’un script SQL

restauration

psql …. script
pg_restore …. binaire

récap

Format Dump Restore
script pg_dump -Fp / pg_dumpall psql
tar pg_dump -Ft pg_restore
custom = compression pg_dump -Fc pg_restore
directory pg_dump -Fd pg_restore

Exemples

pg_dump -Fc  --file=F:\psqlSaves-LOG\STAGE\sauvegardes\exports\prod.dump prod  
# Suppression de la base  
pg_restore -C -d postgres F:\psqlSaves-LOG\STAGE\sauvegardes\exports\prod.dump
pg_dump -Fp  --file=F:\psqlSaves-LOG\STAGE\sauvegardes\exports\prod.sql prod  
# Suppression de la base  
createdb prod  
psql -d prod <F:\psqlSaves-LOG\STAGE\sauvegardes\exports\prod.sql
# avec les 2 sauvegardes précédentes, restauration d'une table
drop table bruno;

pg_restore -d prod -t bruno F:\psqlSaves-LOG\STAGE\sauvegardes\exports\prod.dump  

delete from bruno;
pg_restore -d prod -t bruno --data-only  F:\psqlSaves-LOG\STAGE\sauvegardes\exports\prod.dump  

# on peut aussi sauver une table:  
pg_dump -Fp --table bruno --file=F:\psqlSaves-LOG\STAGE\sauvegardes\exports\table_bruno.sql prod  
# dump_all
pg_dumpall > F:\psqlSaves-LOG\STAGE\sauvegardes\exports\all.sql
# => demande le mdp pour chaque base
# restauration sous un autre nom
createdb preprod
pg_restore -d preprod F:\psqlSaves-LOG\STAGE\sauvegardes\exports\prod.dump

Sauvegarde Physique

physique =copie

  • instance
  • archives
  • tablespaces
  • current wal

PHYSIQUE bdd ouverte

select pg_start_backup(concat('sauve_stage1 ',current_timestamp));
le nom du start va dans un txt nommé: backup_label.old dans le dossier de l’instance mais aussi dans le wal et/ou archive puis copier/coller le dossier de l’instance pour sauvegarder à la main select pg_stop_backup();

depuis v15: déprécié, renommé pg_backup_start()/pg_backup_stop() et le fichier backup_label.old n’existe plus (il est donc recommandé d’utiliser pg_basebackup)

crash disque => copier/coller à l’envers

tout en automatique

pg_basebackup --checkpoint=fast -D F:\psqlSaves-LOG\STAGE\sauvegardes\pg_basebackup
version >=10
fait start_backup, stop_backup et copie dans -D qui doit être vide et s’il n’existe pas, il le créé

PITR

depuis un serveur “neuf” (sans base) sous linux

doc: https://public.dalibo.com/exports/formation/manuels/modules/i2/i2.handout.html

Utile: export PGDATA=/var/lib/pgsql/data

Activer les archives

  • créer le répertoire backups/archives-log

Configuration

archive_mode='on'
archive_command='cp "%p" "/var/lib/pgsql/backups/archives-log/%f"'

version Windows

archive_command='copy "%p" "c:\\pgsql\\backups\\archives-log\\%f"' 

Bien mettre des doubles antislashs “\\” surtout sur la commande restore_command vue plus loin

Vérification

select pg_switch_wal();
-- provoque un archivage de select pg_walfile_name( pg_current_wal_lsn()); le "current wal" (voir les wal avant et après)
-- réfléchir aux paramètres wal_segment_size & archive_timeout ou utiliser pg_receivewal en mode synchrone
select * from pg_stat_archiver;

Scénario

-- créer une base puis s'y connecter
create database pitr;
\c pitr
-- créer une table
create table test (val varchar(100));
-- mettre des données dans la table -> ligne1
insert into test values ('début scénario');
table test ;
  • sauvegarder:
    pg_basebackup --checkpoint=fast -D /var/lib/pgsql/backups/bak1 -v --progress --wal-method=none # ce dernier paramètres signifie qu'on ne sauvegarde pas les wal (on peut changer d'avis) 
    

    il y a aussi les options -Ft, -z et -Z 9 pour tar,compression et niveau de compression.
    bak1 DOIT être vide (voir inexistant) -> ceci afin de créer le 1er point de cohérence de l’instance

    -- mettre des données dans la table -> ligne2
    insert into test values ('après bak1');
    table test ;
    
  • noter l’heure à la seconde près
    -- supprimer des données de la table
    delete from test ;
    table test ;
    -- archivage manuel pour simuler une "certaine activité" (voir les répertoires pg_wal et archives avant et après)
    select pg_switch_wal();
    
  • pg_waldump -p /var/lib/pgsql/backups/archives-log fichierWAL sur le dernier fichier archivé, repérer les horaires réels des 2 transactions
  • facultatif: noter l’heure à nouveau
  • facultatif: sauvegarder
    pg_basebackup --checkpoint=fast -D /var/lib/pgsql/backups/bak2
    
  • arrêter le service (comme s’il y avait eu un gros crash): pg_ctl stop -m immediate
  • copier bak1 sur data. Si possible, restaurer le dernier journal de transactions connu [Restore]
  • démarrer le service pour voir que ça ne fonctionne pas car pas de wal (mais il suffit de lire backup_label et les récupérer dans les archives)
  • revenir à [Restore]
  • touch /var/lib/pgsql/data/recovery.signal -> indique qu’on veut faire un PITR
  • dans postgresql.conf ou .auto: en utilisant l’heure notée plus haut
    restore_command='cp "/var/lib/pgsql/backups/archives-log/%f" "%p"'
    recovery_target_time='2023-02-17 16:08:10'
    
  • démarrer le service
  • vérifier les données de la table et que l’instance est en lecture seule (voir le fichier de log)
  • ici on peut avancer l’heure target_time et redémarrer le service. Si on commente/enlève le *recovery_target_** et redémarre, les archives s’appliquent jusqu’au bout.
  • Pour remettre l’instance en r/w: select pg_wal_replay_resume(); => recovery.signal a été supprimé

REMARQUE
Il existe recovery_target_name géré avec pg_create_restore_point(), cf: https://pgpedia.info/p/pg_create_restore_point.html

Rétention des archives

pg_archivecleanup --help
pg_archivecleanup -n backups/archives-log/ 000000010000000000000003.00000028.backup
pg_archivecleanup -d backups/archives-log/ 000000010000000000000003.00000028.backup
# help indique la commande à mettre dans le paramètre archive_cleanup_command='pg_archivecleanup /var/lib/pgsql/backups/archives-log/ %r' qui est appelé au restartpoint (donc si standby server)

CHECK

pg_controldata
# notament:
# Enregistrement de fin de sauvegarde requis :non

DEMOS

Initialisation

create database test with template=template1;
\c test
CREATE TABLE public.t1 (
	id int4 NOT NULL,
	val varchar(1000) NOT NULL,
	CONSTRAINT t1_pk PRIMARY KEY (id)
);

select oid,datname from pg_database;
select pg_relation_filepath('t1');
-- ou plus malléable
select oid,relname,relfilenode from pg_class where relname='t1';

-- 5M de lignes dans t1
with recursive cte as (
    select 1 as id,'val1' as val
	union all 
	select id+1,concat('val',(id+1))
	from cte
	where id<5000000
)
insert into t1
select * from cte;

-- copie de t1 sans index
create table t2 as (select * from t1);

shared_buffers

select * from t1 t 
order by id offset 3000000 rows fetch next 10 rows only 
; -- pour être sûr d'avoir des données dans shared_buffers

-- taux de remplissage du shared_buffers
create extension pg_buffercache;
select count(*)*100.0/(select count(*) from pg_buffercache) as "%use" 
from pg_buffercache 
where relfilenode is not null
;
-- hit ratio (cf https://dataegret.com/2017/03/deep-dive-into-postgres-stats-pg_stat_database/)
select round((blks_hit::float/(blks_read+blks_hit+1)*100)::numeric,2) as "hitsRatio"
from pg_catalog.pg_stat_database 
where datname ='test'
;
select sum(heap_blks_read) as heap_read,sum(heap_blks_hit) as heap_hit,  
  sum(heap_blks_hit)/(sum(heap_blks_hit)+sum(heap_blks_read)) as ratio
from pg_catalog.pg_statio_user_tables 
;

-- dirty pages dans shared_buffers
select count(*) from pg_buffercache where isdirty !='f';

-- hits
create extension pg_stat_statements; -- log des requêtes avec indications réelles des statistiques
alter system set shared_preload_libraries='pg_stat_statements';
select pg_reload_conf();
select * from pg_settings;
  -- PUIS RESTART PG
SELECT pg_stat_statements_reset();
select * from t2 where id=123456;
explain (analyse,buffers) select * from t2 where id=123456;
select query,shared_blks_hit,shared_blks_read,calls from pg_stat_statements where query like '%t2%';
-- shared_blks_hit & shared_blks_read correspondent à peu près à EXPLAIN, puis faire bouger calls
-- cf https://pganalyze.com/blog/5mins-postgres-explain-pg-stat-statements-plan-cache-mode-normalized-query pour un explain depuis pg_stat_statements

work_mem

show work_mem;
-- Réglages d'affichage de "log"
show log_temp_files;
set log_temp_files to 0;
show client_min_messages;
set client_min_messages to log;

-- \timing
-- timing n'est pas pertinent ici, mais il est bon de savoir à quoi cela sert

-- Sort Method: external merge
explain (analyse,buffers) select * from t2 order by id desc;
-- Sort Method: quicksort
set work_mem ='512MB';
explain (analyse,buffers) select * from t2 order by id desc;

temp_buffers

A regarder avec le moniteur de RAM en parallèle

-- grosse table --
-------------------------------
CREATE TABLE public.grosse (
	id int4 NOT NULL,
	val char(1000) NOT NULL,
	CONSTRAINT grosse_pk PRIMARY KEY (id)
);

insert into grosse  
select x,'toto'||x from generate_series(1,2000000) as x
;

show temp_buffers;
create temporary table tmp as (select * from grosse);
drop table tmp;
-- déconnexion (ou pas pour pour tester le message d'erreur)
set temp_buffers to '1GB';
create temporary table tmp as (select * from grosse);

Index

drop table frag;
create table frag (
num int
);
create index ix_frag_num on frag(num);
insert into frag 
select floor(random()*2000000000)::int from generate_series(1,1000000) as x
;
-- état des indexs
create extension pgstattuple ;

SELECT i.indexrelid::regclass,
       s.*
FROM pg_index AS i
   JOIN pg_class AS t ON i.indexrelid = t.oid
   JOIN pg_opclass AS opc ON i.indclass[0] = opc.oid
   JOIN pg_am ON opc.opcmethod = pg_am.oid
   CROSS JOIN LATERAL pgstatindex(i.indexrelid) AS s
WHERE t.relkind = 'i'
  and t.reltype=0 and i.indexrelid::regclass::varchar not like 'pg\_%';
  
-- faire des "trous"
delete from frag 
where num<(select avg(num) from frag);
-- refaire plusieurs fois l'Insert et le delete, attendre...

Divers

-- liste des connexions (select * bien aussi)
select usename,pid,application_name from  pg_stat_activity where datname='nom_bdd';
-- tuer des connexions
select pg_terminate_backend(pid) from pg_stat_activity where usename='user_name';

LOCKS

-- faire un lock
begin;
Lock table t1 IN Access exclusive mode;
rollback;
-- essayer l'accès sur une autre connexion
-- chercher les lock
  SELECT blocked_locks.pid     AS blocked_pid,
         blocked_activity.usename  AS blocked_user,
         blocking_locks.pid     AS blocking_pid,
         blocking_activity.usename AS blocking_user,
         blocked_activity.query    AS blocked_statement,
         blocking_activity.query   AS current_statement_in_blocking_process
   FROM  pg_catalog.pg_locks         blocked_locks
    JOIN pg_catalog.pg_stat_activity blocked_activity  ON blocked_activity.pid = blocked_locks.pid
    JOIN pg_catalog.pg_locks         blocking_locks 
        ON blocking_locks.locktype = blocked_locks.locktype
        AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
        AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
        AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
        AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
        AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
        AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
        AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
        AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
        AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
        AND blocking_locks.pid != blocked_locks.pid

    JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
   WHERE NOT blocked_locks.granted;

results matching ""

    No results matching ""