đŠ PL/SQL â Guide complet (Oracle)
Procédures, fonctions, packages, triggers, exceptions, SQL dynamique, perf & sécurité. Idéal pour DBA / dev back-end / data apps Oracle.
BEGIN...END exĂ©cutĂ©s cĂŽtĂ© base (logique, rĂšgles mĂ©tier, batch, APIs). Ses forces : performances (proximitĂ© donnĂ©es), transactions, robustesse (exceptions), packages (encapsulation). Attention : couplage, SQL dynamique mal gĂ©rĂ©, et performances si code row-by-row.Vue dâensemble
Pourquoi PL/SQL, oĂč lâutiliser, architecture, modĂšles dâexĂ©cution.
OracleTransactionsAPI DBBlocs & structure
DECLARE/BEGIN/EXCEPTION/END, variables, types, scope, NULL.
BlockTypesExceptionsProcédures & fonctions
IN/OUT/IN OUT, RETURN, DETERMINISTIC, pipelined (aperçu).
ProcedureFunctionParamsCursors & boucles
FOR loops, bulk collect, forall, éviter le row-by-row.
CursorBulkFORALLPackages (API)
Spec vs Body, encapsulation, variables de package, init section.
PackageSpec/BodyEncapsulationTriggers
BEFORE/AFTER, row vs statement, audit, piĂšges & anti-patterns.
TriggerAuditRisquesSQL dynamique
EXECUTE IMMEDIATE, bind variables, DBMS_SQL, injection.
Dynamic SQLInjectionBindExceptions & erreurs
RAISE, RAISE_APPLICATION_ERROR, log, stratĂ©gie dâerreurs.
ExceptionLogErrorsPerformance
Bulk, context switches, instrumentation, plan SQL, tuning.
PerfExplainStatsSécurité & droits
Definer vs invoker, grants, roles, DBMS_ASSERT, least privilege.
SecurityAUTHIDGrantsTests & qualité
utPLSQL, jeux de données, transactions, mocks, CI (pistes).
utPLSQLCIQAOutils & workflow
SQL Developer, SQLcl, Liquibase/Flyway, code style, Git.
SQLclMigrationsGitPourquoi PL/SQL existe
Oracle a ajouté PL/SQL pour exprimer des rÚgles métier et des traitements au plus prÚs des données : validations, batch, orchestration, APIs internes, manipulations de gros volumes, et gestion transactionnelle robuste.
| Forces | Impact | Exemples |
|---|---|---|
| ProximitĂ© donnĂ©es | moins dâallers-retours app â DB | batch nightly, calculs, consolidation |
| Transactions | COMMIT/ROLLBACK maßtrisés | process multi-tables cohérents |
| Encapsulation | API DB (packages) stable | pkg_customer.create_customer() |
| Exceptions | erreurs gérées + logging | RAISE_APPLICATION_ERROR, journaux |
OĂč lâutiliser (cas âbonsâ)
- APIs DB : packages exposant des procédures/fonctions stables (contrat).
- Batch / ETL : traitement volumétrique, transformations, agrégations.
- Garanties métier : validations impossibles cÎté app seule (cohérence multi-tables).
- Audit : journalisation centralisée (attention aux triggers lourds).
- Ops DBA : scripts maintenance, purge, reindex, job scheduler.
OĂč Ă©viter (souvent)
- Logique UI/HTTP/JSON trop âapplicationâ (mieux cĂŽtĂ© service back).
- Triggers complexes en cascade (difficile à déboguer & perf).
- SQL dynamique sans bind variables (risque injection + perf).
ModĂšle dâexĂ©cution (simple)
Anti-patterns fréquents
| Anti-pattern | Pourquoi câest mauvais | Alternative |
|---|---|---|
| Row-by-row (cursor loop sur millions) | lent, context switches | SQL set-based + BULK COLLECT / FORALL |
| Triggers qui font âtoutâ | debug difficile, perf alĂ©atoire | packages + contraintes + audit ciblĂ© |
| EXECUTE IMMEDIATE concat | injection + hard parse | bind variables + DBMS_ASSERT si nécessaire |
Structure canonique
DECLARE
-- Déclarations (variables, constantes, types)
v_customer_id NUMBER;
v_name VARCHAR2(200);
BEGIN
-- Logique
SELECT customer_id, name
INTO v_customer_id, v_name
FROM customers
WHERE email = 'a@b.com';
DBMS_OUTPUT.PUT_LINE('ID='||v_customer_id||' name='||v_name);
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('No customer');
WHEN TOO_MANY_ROWS THEN
DBMS_OUTPUT.PUT_LINE('Email not unique!');
WHEN OTHERS THEN
-- à traiter proprement (log + re-raise si nécessaire)
DBMS_OUTPUT.PUT_LINE('Unexpected: '||SQLERRM);
END;
/Types & variables (rappel)
| Type | Usage | Tips |
|---|---|---|
| NUMBER | numériques | précision/scale si besoin |
| VARCHAR2 | texte | taille suffisante, éviter CHAR sauf cas |
| DATE / TIMESTAMP | temps | préférer TIMESTAMP si précision |
| %TYPE / %ROWTYPE | typages âdynamiquesâ | rĂ©duit mismatch avec schĂ©ma |
Scope & bonnes pratiques
- %TYPE pour coller au schĂ©ma (moins dâerreurs lors des Ă©volutions).
- NULL = valeur âinconnueâ : attention aux comparaisons (utiliser
IS NULL). - Préférer constants et noms explicites (préfixes v_/p_/g_).
- Ăviter DBMS_OUTPUT en prod â instrumentation propre (table log / APEX_DEBUG / etc.).
Truc pro : variables ancrées
v_salary employees.salary%TYPE; v_emp employees%ROWTYPE;
Procédure
Une procĂ©dure exĂ©cute une action (insert/update/process) ; elle ne âretourneâ pas directement une valeur comme une fonction, mais peut utiliser OUT/IN OUT.
CREATE OR REPLACE PROCEDURE p_create_customer( p_email IN VARCHAR2, p_name IN VARCHAR2, p_id OUT NUMBER ) AS BEGIN INSERT INTO customers(email, name) VALUES(p_email, p_name) RETURNING customer_id INTO p_id; END; /
Fonction
Une fonction retourne une valeur. Elle est souvent utilisée dans SQL (avec prudence) ou dans du PL/SQL.
CREATE OR REPLACE FUNCTION f_customer_status(p_id IN NUMBER)
RETURN VARCHAR2
AS
v_status VARCHAR2(20);
BEGIN
SELECT status INTO v_status FROM customers WHERE customer_id = p_id;
RETURN v_status;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN 'UNKNOWN';
END;
/IN / OUT / IN OUT
| Mode | RĂŽle | Ă utiliser quand |
|---|---|---|
| IN | entrĂ©e (read-only) | paramĂštre ânormalâ |
| OUT | sortie | renvoyer un id / statut / résultats |
| IN OUT | entrĂ©e + sortie | rare ; Ă©viter si API peut ĂȘtre plus claire |
-- Nommer clairement: p_customer_id IN NUMBER o_status OUT VARCHAR2
Patterns dâAPI PL/SQL (pro)
| Pattern | Idée | Exemple |
|---|---|---|
| CRUD API package | exposer des opérations stables | pkg_customer.create/update/delete |
| Validation centralisée | une seule source de vérité | pkg_rules.validate_order() |
| Batch orchestrator | coordonne étapes + commits | pkg_batch.run(p_date) |
Cursor FOR loop (simple)
FOR r IN (SELECT customer_id FROM customers WHERE status='ACTIVE') LOOP -- traitement NULL; END LOOP;
Bulk Collect + FORALL (pro perf)
DECLARE
TYPE t_ids IS TABLE OF customers.customer_id%TYPE;
v_ids t_ids;
BEGIN
SELECT customer_id BULK COLLECT INTO v_ids
FROM customers
WHERE status='ACTIVE';
FORALL i IN 1..v_ids.COUNT
UPDATE customers SET last_seen = SYSTIMESTAMP
WHERE customer_id = v_ids(i);
END;
/RĂšgles de perf
- Préférer SQL set-based (UPDATE/INSERT MERGE) plutÎt que boucles.
- Si boucle obligatoire : BULK COLLECT (par batch) + FORALL.
- Utiliser un LIMIT sur bulk collect en trĂšs gros volumes.
- Mesurer : instrumentation + stats + plans SQL (sinon âoptimisationsâ inutiles).
Bulk par batches
LOOP
FETCH c BULK COLLECT INTO v_rows LIMIT 1000;
EXIT WHEN v_rows.COUNT = 0;
FORALL i IN 1..v_rows.COUNT
-- DML
END LOOP;Package Spec (contrat public)
CREATE OR REPLACE PACKAGE pkg_customer AS PROCEDURE create_customer(p_email IN VARCHAR2, p_name IN VARCHAR2, o_id OUT NUMBER); FUNCTION get_status(p_id IN NUMBER) RETURN VARCHAR2; -- Constantes publiques (si utile) c_status_active CONSTANT VARCHAR2(10) := 'ACTIVE'; END pkg_customer; /
Package Body (implémentation)
CREATE OR REPLACE PACKAGE BODY pkg_customer AS
-- Private helper (non exposé)
FUNCTION normalize_email(p_email IN VARCHAR2) RETURN VARCHAR2 IS
BEGIN
RETURN LOWER(TRIM(p_email));
END;
PROCEDURE create_customer(p_email IN VARCHAR2, p_name IN VARCHAR2, o_id OUT NUMBER) AS
BEGIN
INSERT INTO customers(email, name)
VALUES(normalize_email(p_email), p_name)
RETURNING customer_id INTO o_id;
END;
FUNCTION get_status(p_id IN NUMBER) RETURN VARCHAR2 AS
v_status customers.status%TYPE;
BEGIN
SELECT status INTO v_status FROM customers WHERE customer_id = p_id;
RETURN v_status;
EXCEPTION
WHEN NO_DATA_FOUND THEN RETURN 'UNKNOWN';
END;
BEGIN
-- Section d'initialisation du package (optionnelle)
NULL;
END pkg_customer;
/Pourquoi les packages sont âla brique proâ
- Encapsulation : tu exposes une API stable, tu caches les détails.
- Contrat : la spec sert de documentation vivante.
- Organisation : regrouper par domaine (customer, billing, batch, security).
- Versioning : limiter les breaking changes pour les apps clientes.
Conventions (recommandées)
| ĂlĂ©ment | Convention | Exemple |
|---|---|---|
| Noms | pkg_ + domaine | pkg_order |
| Procédures | verbe + objet | create_order |
| ParamĂštres | p_ / o_ | p_id / o_status |
| Privés | helpers dans body | validate_x() |
Trigger : exemple simple
CREATE OR REPLACE TRIGGER trg_customers_ts
BEFORE INSERT OR UPDATE ON customers
FOR EACH ROW
BEGIN
:NEW.updated_at := SYSTIMESTAMP;
IF INSERTING THEN
:NEW.created_at := SYSTIMESTAMP;
END IF;
END;
/Audit (pattern classique)
CREATE OR REPLACE TRIGGER trg_customers_audit
AFTER INSERT OR UPDATE OR DELETE ON customers
FOR EACH ROW
BEGIN
INSERT INTO audit_log(table_name, action, pk, at_ts)
VALUES('CUSTOMERS',
CASE WHEN INSERTING THEN 'I' WHEN UPDATING THEN 'U' ELSE 'D' END,
NVL(:NEW.customer_id, :OLD.customer_id),
SYSTIMESTAMP);
END;
/PiÚges (trÚs fréquents)
| PiÚge | Conséquence | Signal |
|---|---|---|
| Trigger qui appelle dâautres tables / commits | perf + blocages + complexitĂ© | latence imprĂ©visible |
| Cascades | effets de bord invisibles | bug âfantĂŽmeâ |
| Mutating table | erreurs runtime | ORA-04091 |
| Logique métier lourde | hard to test, hard to reason | changement coûteux |
Alternatives recommandées
- Contraintes (NOT NULL, CHECK, FK, UNIQUE) : simples, rapides, explicites.
- Packages API : le mĂ©tier est dans lâAPI, pas dans des triggers cachĂ©s.
- Jobs (DB scheduler) : traitements asynchrones (purges, recalculs).
- Audit applicatif : pour besoins riches (qui/quand/quoi + contexte).
EXECUTE IMMEDIATE (avec bind variables)
DECLARE
v_sql VARCHAR2(4000);
v_cnt NUMBER;
p_status VARCHAR2(20) := 'ACTIVE';
BEGIN
v_sql := 'SELECT COUNT(*) FROM customers WHERE status = :1';
EXECUTE IMMEDIATE v_sql INTO v_cnt USING p_status;
DBMS_OUTPUT.PUT_LINE('cnt='||v_cnt);
END;
/DML dynamique (bind)
v_sql := 'UPDATE customers SET status = :1 WHERE customer_id = :2'; EXECUTE IMMEDIATE v_sql USING 'INACTIVE', 1234;
Ce quâil ne faut PAS faire
-- â Injection + hard parse : v_sql := 'SELECT * FROM customers WHERE email = ''' || p_email || '''';
RÚgles de sécurité
- Toujours utiliser des bind variables pour les valeurs.
- Si besoin dâobjets dynamiques (table/col) : whitelists + DBMS_ASSERT.
- Limiter les privilĂšges (least privilege).
Quand DBMS_SQL ?
DBMS_SQL est utile quand tu as des requĂȘtes dynamiques trĂšs gĂ©nĂ©riques (colonnes inconnues Ă compile-time, metadata, parsing avancĂ©). Sinon, EXECUTE IMMEDIATE suffit dans la majoritĂ© des cas.
Exceptions Oracle courantes
| Exception | Quand | Traitement |
|---|---|---|
| NO_DATA_FOUND | SELECT INTO 0 ligne | retour défaut / gestion métier |
| TOO_MANY_ROWS | SELECT INTO >1 | corriger filtre / unicité |
| ZERO_DIVIDE | division par 0 | guard clause |
| DUP_VAL_ON_INDEX | violation unique | upsert/merge, message clair |
| OTHERS | catch-all | log + re-raise (souvent) |
RAISE_APPLICATION_ERROR (métier)
IF p_amount <= 0 THEN RAISE_APPLICATION_ERROR(-20001, 'Amount must be > 0'); END IF;
Stratégie pro (recommandée)
Logging minimal (pattern)
BEGIN
-- ...
EXCEPTION
WHEN OTHERS THEN
INSERT INTO app_error_log(at_ts, code, msg, backtrace)
VALUES(SYSTIMESTAMP, SQLCODE, SQLERRM, DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
RAISE;
END;WHEN OTHERS THEN NULL; (ça masque les pannes).Checklist perf (ultra utile)
| Point | Pourquoi | Action |
|---|---|---|
| Row-by-row ? | lent sur gros volumes | réécrire en set-based / BULK / FORALL |
| Context switches | coĂ»t PL/SQLâSQL | regrouper DML / bulk |
| Indexes / stats | plan stable | analyser stats, revoir indexes |
| Hard parse | CPU | bind vars, éviter concat SQL |
| Instrumentation | sans métriques = au hasard | timers + logs + views perf |
Bulk processing : la technique qui change tout
-- Pattern batch: FETCH ... BULK COLLECT ... LIMIT N FORALL ... DML COMMIT (si batchs indépendants) / ou commit global selon cohérence
Instrumentation (minimum viable)
DECLARE
t0 NUMBER := DBMS_UTILITY.GET_TIME; -- centiĂšmes
BEGIN
-- traitement
DBMS_OUTPUT.PUT_LINE('elapsed cs='||(DBMS_UTILITY.GET_TIME - t0));
END;
/En prod, préfÚre un logger (table + contexte) plutÎt que DBMS_OUTPUT. Ajoute : nom job, paramÚtres, counts, durée, erreurs, backtrace.
Le plan SQL (toujours regarder)
Definer rights vs Invoker rights
| Mode | Clé | Effet | Quand |
|---|---|---|---|
| Definer | par défaut | exécute avec droits du propriétaire | API DB classique |
| Invoker | AUTHID CURRENT_USER | droits de lâappelant | outils gĂ©nĂ©riques, multi-schemas |
CREATE OR REPLACE PACKAGE pkg_admin AUTHID CURRENT_USER AS PROCEDURE do_admin_task; END; /
RĂšgles âleast privilegeâ
- Donner le minimum de GRANT nécessaire (SELECT/EXECUTE ciblés).
- Ăviter de sur-privilĂ©gier un schĂ©ma âappâ (limiter CREATE ANY / etc.).
- Auditer les EXECUTE sur packages sensibles.
- Dynamic SQL : bind + whitelist + DBMS_ASSERT.
Approche testable (réaliste)
- Ăcrire des packages âpur mĂ©tierâ (fonctions) testables sans dĂ©pendances externes.
- Isoler la logique DML dans des procédures, tester avec datasets contrÎlés.
- Utiliser des transactions / savepoints pour rollback entre tests.
- Centraliser des utilitaires de test (seed data, cleanup).
utPLSQL (idée)
-- Ex pseudo (concept)
-- describe('pkg_customer')
-- it('should create customer')
-- expect(select count(*) ...).to_equal(1)CI / pipeline (piste)
| Outil | RĂŽle | Pourquoi utile | Tips |
|---|---|---|---|
| SQL Developer | IDE Oracle | debug PL/SQL, objets DB | profilers, explain plan |
| SQLcl | CLI Oracle | scripts, automation, CI | idéal pipeline |
| Liquibase / Flyway | migrations | versioning schéma | séparer DDL vs code |
| Git | versioning | review, history, tags release | 1 fichier = 1 objet |
| Formatter/Style | lisibilité | réduire friction équipe | conventions + lint |
Convention â1 objet = 1 fichierâ (recommandĂ©e)
db/
packages/
pkg_customer.pks
pkg_customer.pkb
procedures/
p_rebuild_indexes.sql
triggers/
trg_customers_audit.sqlSyntaxe
-- bloc DECLARE ... BEGIN ... EXCEPTION ... END; / -- select into SELECT col INTO v FROM t WHERE ...; -- insert returning INSERT ... RETURNING id INTO v_id; -- exceptions WHEN NO_DATA_FOUND THEN ... WHEN OTHERS THEN ... SQLERRM ... -- procedure/function PROCEDURE p(x IN NUMBER, o OUT NUMBER); FUNCTION f(x IN NUMBER) RETURN VARCHAR2; -- package PACKAGE spec / PACKAGE BODY
Perf & sécurité
-- bulk (volumes) BULK COLLECT ... LIMIT FORALL ... DML -- dynamic SQL safe EXECUTE IMMEDIATE '... :1 ...' USING v; -- droits AUTHID CURRENT_USER (invoker) sinon definer rights -- log erreurs SQLCODE / SQLERRM / FORMAT_ERROR_BACKTRACE
| Niveau | Question | Ce quâon attend |
|---|---|---|
| Junior | Différence procédure vs fonction ? | RETURN, usages, params OUT |
| Junior | NO_DATA_FOUND / TOO_MANY_ROWS ? | SELECT INTO et gestion |
| Confirmé | Pourquoi FORALL est plus rapide ? | context switches + bulk DML |
| ConfirmĂ© | Spec vs Body dâun package ? | contrat public vs impl |
| Senior | AUTHID CURRENT_USER vs definer ? | droits dâexĂ©cution + sĂ©curitĂ© |
| Senior | Dynamic SQL : comment éviter injection ? | bind vars + whitelist + DBMS_ASSERT |
| Senior | Triggers : quand éviter ? | complexité/perf + alternatives |
| Senior | Comment diagnostiquer une lenteur ? | plan SQL + stats + instrumentation + rewrite |
