Notes de cours sur PostgreSQL
Mis à jour le 18/03/2025
Installation
à faire avant de démarrer le PPT
- Windows
mettre les données surD:\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 fichierpg-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;