← Cours BD02

⚡ Exercices BD02 — PostgreSQL Avancé

5 exercices · Solutions masquées

Exercice 1 — JSONB : Catalogue produits

Créez une table products avec un champ JSONB attributes. Insérez 3 produits avec des attributs différents (couleur, taille, poids, etc.). Puis : (a) filtrez par couleur rouge, (b) ajoutez un champ "discount" à tous les produits, (c) créez un index GIN et vérifiez son utilisation.

Voir la solution
CREATE TABLE products (
  id         SERIAL PRIMARY KEY,
  name       VARCHAR(100),
  price      NUMERIC(8,2),
  attributes JSONB DEFAULT '{}'
);

INSERT INTO products (name, price, attributes) VALUES
  ('T-shirt rouge', 19.99, '{"color":"red","size":"M","weight":200,"material":"coton"}'),
  ('Jean bleu', 49.99, '{"color":"blue","size":"L","weight":600,"material":"denim"}'),
  ('Polo rouge', 29.99, '{"color":"red","size":"S","weight":180,"material":"polo"}');

-- (a) Filtre par couleur
SELECT name, attributes->>'color' AS color
FROM products
WHERE attributes @> '{"color":"red"}';

-- (b) Ajouter champ discount
UPDATE products SET attributes = attributes || '{"discount":0}'::jsonb;

-- (c) Index GIN
CREATE INDEX idx_products_attrs ON products USING GIN (attributes);
EXPLAIN SELECT * FROM products WHERE attributes @> '{"color":"red"}';

Exercice 2 — Full-Text Search

Créez une table articles avec titre et contenu. Ajoutez une colonne search_vector tsvector, un trigger pour la maintenir automatiquement, et un index GIN. Effectuez une recherche full-text avec ranking.

Voir la solution
CREATE TABLE articles (
  id            SERIAL PRIMARY KEY,
  title         TEXT NOT NULL,
  body          TEXT,
  search_vector TSVECTOR
);

-- Trigger de mise à jour
CREATE OR REPLACE FUNCTION update_article_fts() RETURNS trigger AS $$
BEGIN
  NEW.search_vector := to_tsvector('french',
    coalesce(NEW.title, '') || ' ' || coalesce(NEW.body, ''));
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_articles_fts
BEFORE INSERT OR UPDATE ON articles
FOR EACH ROW EXECUTE FUNCTION update_article_fts();

CREATE INDEX idx_articles_fts ON articles USING GIN (search_vector);

INSERT INTO articles (title, body) VALUES
  ('Introduction à PostgreSQL', 'PostgreSQL est une base de données relationnelle open-source très performante.'),
  ('Les index en PostgreSQL', 'Les index B-Tree et GIN améliorent les performances des requêtes.'),
  ('Redis et le caching', 'Redis est une base de données clé-valeur en mémoire utilisée pour le cache.');

-- Recherche avec ranking
SELECT title, ts_rank(search_vector, query) AS rank
FROM articles, to_tsquery('french', 'postgresql & index') query
WHERE search_vector @@ query
ORDER BY rank DESC;

Exercice 3 — Window Functions

À partir d'une table sales(id, seller, region, amount, sale_date), calculez pour chaque vente : (a) le rang du vendeur dans sa région, (b) le total cumulé des ventes de sa région, (c) la moyenne mobile sur 3 mois, (d) la différence avec la vente précédente du même vendeur.

Voir la solution
CREATE TABLE sales (
  id        SERIAL PRIMARY KEY,
  seller    VARCHAR(50),
  region    VARCHAR(50),
  amount    NUMERIC(10,2),
  sale_date DATE
);

INSERT INTO sales (seller, region, amount, sale_date) VALUES
  ('Alice', 'Nord', 1200, '2024-01-15'), ('Bob', 'Nord', 900, '2024-01-20'),
  ('Alice', 'Nord', 1500, '2024-02-10'), ('Charlie', 'Sud', 800, '2024-01-05'),
  ('David', 'Sud', 1100, '2024-02-12'), ('Charlie', 'Sud', 950, '2024-03-01');

SELECT
  seller, region, amount, sale_date,
  RANK() OVER (PARTITION BY region ORDER BY amount DESC)     AS region_rank,
  SUM(amount) OVER (PARTITION BY region ORDER BY sale_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)        AS cumul,
  AVG(amount) OVER (PARTITION BY seller ORDER BY sale_date
    ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)                AS avg_3m,
  amount - LAG(amount) OVER (PARTITION BY seller ORDER BY sale_date) AS vs_prev
FROM sales
ORDER BY region, sale_date;

Exercice 4 — CTE récursive

Modélisez une hiérarchie d'employés (manager/subordonné). Créez la CTE récursive qui retourne l'arborescence complète depuis un manager racine, avec le niveau de profondeur et le chemin complet (breadcrumb).

Voir la solution
CREATE TABLE employees (
  id         SERIAL PRIMARY KEY,
  name       VARCHAR(100),
  manager_id INTEGER REFERENCES employees(id)
);

INSERT INTO employees (id, name, manager_id) VALUES
  (1, 'CEO', NULL), (2, 'CTO', 1), (3, 'CFO', 1),
  (4, 'Lead Dev', 2), (5, 'Dev 1', 4), (6, 'Dev 2', 4),
  (7, 'Comptable', 3);

WITH RECURSIVE org AS (
  -- Racine
  SELECT id, name, manager_id, 0 AS depth,
         ARRAY[name] AS path, CAST(name AS TEXT) AS breadcrumb
  FROM employees WHERE manager_id IS NULL

  UNION ALL

  -- Récursion
  SELECT e.id, e.name, e.manager_id, o.depth + 1,
         o.path || e.name,
         o.breadcrumb || ' > ' || e.name
  FROM employees e
  JOIN org o ON e.manager_id = o.id
)
SELECT id, REPEAT('  ', depth) || name AS tree_name,
       depth, breadcrumb
FROM org
ORDER BY path;

Exercice 5 — Vue matérialisée + trigger refresh

Créez une vue matérialisée mv_monthly_revenue qui calcule le CA mensuel par catégorie. Créez un index UNIQUE dessus. Écrivez une procédure de refresh et programmez-la (simulation via NOTIFY).

Voir la solution
-- Tables source
CREATE TABLE categories (id SERIAL PRIMARY KEY, name TEXT);
CREATE TABLE orders (
  id SERIAL PRIMARY KEY, category_id INT REFERENCES categories(id),
  amount NUMERIC(10,2), created_at TIMESTAMPTZ DEFAULT NOW()
);

INSERT INTO categories (name) VALUES ('Electronics'),('Clothing'),('Books');

-- Vue matérialisée
CREATE MATERIALIZED VIEW mv_monthly_revenue AS
SELECT
  DATE_TRUNC('month', o.created_at) AS month,
  c.name AS category,
  COUNT(o.id) AS nb_orders,
  SUM(o.amount) AS revenue
FROM orders o
JOIN categories c ON c.id = o.category_id
GROUP BY 1, 2
WITH DATA;

CREATE UNIQUE INDEX ON mv_monthly_revenue (month, category);

-- Refresh en arrière-plan (CONCURRENTLY grâce à l'index UNIQUE)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_monthly_revenue;

-- Trigger pour notifier les changements
CREATE OR REPLACE FUNCTION notify_mv_stale() RETURNS trigger AS $$
BEGIN
  PERFORM pg_notify('mv_refresh_needed', TG_TABLE_NAME);
  RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_orders_notify
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH STATEMENT EXECUTE FUNCTION notify_mv_stale();