Pour compléter cette introduction, consultez aussi le tutoriel PostgreSQL avancé et administration et le tutoriel PostgreSQL PL/pgSQL.
Cette page sert à deux choses : reprendre les bases utiles de PostgreSQL en SQL, puis donner à un profil expert Oracle les différences concrètes à connaître pour adapter rapidement ses requêtes sans perdre de temps sur les pièges les plus fréquents.
L'idée n'est pas de tout couvrir sur PostgreSQL, mais de fournir une base directement exploitable pour lire, écrire, migrer et corriger des requêtes du quotidien.
Conseil : si vous venez d'Oracle, commencez par lire les exemples PostgreSQL, puis la section Oracle vers PostgreSQL en fin de page.
-- Sélection simple
SELECT * FROM employees;
SELECT employee_id, first_name, last_name, salary
FROM employees;
-- DISTINCT
SELECT DISTINCT department_id
FROM employees;
-- Alias
SELECT
first_name AS prenom,
last_name AS nom,
salary * 12 AS salaire_annuel
FROM employees;
-- Concaténation
SELECT first_name || ' ' || last_name AS nom_complet
FROM employees;
-- Comparaison
SELECT * FROM employees WHERE salary > 5000;
SELECT * FROM employees WHERE hire_date >= DATE '2024-01-01';
-- BETWEEN / IN
SELECT * FROM employees
WHERE salary BETWEEN 5000 AND 10000
AND department_id IN (10, 20, 30);
-- LIKE / ILIKE
SELECT * FROM employees WHERE last_name LIKE 'S%';
SELECT * FROM employees WHERE email ILIKE '%@example.com';
-- IS NULL
SELECT * FROM employees WHERE commission_pct IS NULL;
-- Regex
SELECT * FROM employees
WHERE email ~* '^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$';
-- ORDER BY SELECT * FROM employees ORDER BY salary DESC; SELECT * FROM employees ORDER BY department_id ASC, salary DESC; -- NULLS FIRST / NULLS LAST SELECT * FROM employees ORDER BY commission_pct NULLS LAST; -- LIMIT SELECT * FROM employees ORDER BY salary DESC LIMIT 10; -- OFFSET SELECT * FROM employees ORDER BY employee_id OFFSET 20 LIMIT 10; -- SQL standard également supporté SELECT * FROM employees ORDER BY salary DESC FETCH FIRST 10 ROWS ONLY;
SELECT e.first_name, e.last_name, d.department_name FROM employees e JOIN departments d ON e.department_id = d.department_id; SELECT e.first_name, d.department_name FROM employees e LEFT JOIN departments d ON e.department_id = d.department_id; SELECT e.first_name AS employe, m.first_name AS manager FROM employees e LEFT JOIN employees m ON e.manager_id = m.employee_id;
-- Plus que la moyenne globale
SELECT *
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
-- EXISTS
SELECT *
FROM employees e
WHERE EXISTS (
SELECT 1
FROM departments d
WHERE d.department_id = e.department_id
AND d.location_id = 1700
);
-- Corrélation
SELECT e1.first_name, e1.salary, e1.department_id
FROM employees e1
WHERE e1.salary > (
SELECT AVG(e2.salary)
FROM employees e2
WHERE e2.department_id = e1.department_id
);
-- Agrégations
SELECT
department_id,
COUNT(*) AS nb_employés,
AVG(salary) AS salaire_moyen,
SUM(salary) AS masse_salariale
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 5000
ORDER BY department_id;
-- Window functions
SELECT
employee_id,
department_id,
salary,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rang,
AVG(salary) OVER (PARTITION BY department_id) AS moyenne_departement
FROM employees;
PostgreSQL est très fort sur les window functions. Si vous faites déjà du SQL analytique sous Oracle, vous retrouverez l'essentiel avec une syntaxe très proche.
-- Maintenant
SELECT CURRENT_DATE;
SELECT CURRENT_TIMESTAMP;
SELECT NOW();
-- Formatage
SELECT TO_CHAR(CURRENT_TIMESTAMP, 'YYYY-MM-DD HH24:MI:SS');
-- Conversion texte vers date
SELECT TO_DATE('2026-05-31', 'YYYY-MM-DD');
SELECT TO_TIMESTAMP('2026-05-31 14:30:00', 'YYYY-MM-DD HH24:MI:SS');
-- Null handling
SELECT COALESCE(commission_pct, 0) AS commission
FROM employees;
-- Chaînes
SELECT UPPER(last_name), LENGTH(last_name), SUBSTRING(last_name FROM 1 FOR 3)
FROM employees;
-- Date truncation
SELECT DATE_TRUNC('month', CURRENT_TIMESTAMP);
Attention : PostgreSQL distingue fortement les types, et la conversion implicite est souvent moins permissive que dans des usages Oracle historiques.
-- INSERT
INSERT INTO departments (department_id, department_name)
VALUES (70, 'Data');
-- INSERT avec RETURNING
INSERT INTO departments (department_name)
VALUES ('Platform')
RETURNING department_id;
-- UPDATE
UPDATE employees
SET salary = salary * 1.05
WHERE department_id = 10;
-- DELETE
DELETE FROM employees
WHERE employee_id = 999;
-- UPSERT PostgreSQL
INSERT INTO departments (department_id, department_name)
VALUES (10, 'Sales')
ON CONFLICT (department_id)
DO UPDATE SET department_name = EXCLUDED.department_name;
-- CTE simple
WITH salaires AS (
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
)
SELECT e.first_name, e.salary, s.avg_salary
FROM employees e
JOIN salaires s ON s.department_id = e.department_id;
-- CTE récursive (équivalent fréquent de CONNECT BY)
WITH RECURSIVE hierarchy AS (
SELECT employee_id, manager_id, first_name, 1 AS niveau
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.employee_id, e.manager_id, e.first_name, h.niveau + 1
FROM employees e
JOIN hierarchy h ON e.manager_id = h.employee_id
)
SELECT *
FROM hierarchy
ORDER BY niveau, employee_id;
Cette section cible directement le cas d'un utilisateur Oracle qui veut adapter vite ses requêtes. Le plus efficace est de mémoriser les changements de réflexe suivants.
Oracle PostgreSQL ------------------------------ ------------------------------------------ SYSDATE CURRENT_DATE ou CURRENT_TIMESTAMP ou NOW() SYSTIMESTAMP CURRENT_TIMESTAMP NVL(a, b) COALESCE(a, b) NVL2(a, b, c) CASE WHEN a IS NOT NULL THEN b ELSE c END DECODE(x, 1, 'A', 2, 'B', 'X') CASE x WHEN 1 THEN 'A' WHEN 2 THEN 'B' ELSE 'X' END LISTAGG(col, ',') STRING_AGG(col, ',') MINUS EXCEPT ROWNUM LIMIT ou ROW_NUMBER() OVER (...) CONNECT BY WITH RECURSIVE DUAL inutile dans la plupart des cas VARCHAR2 VARCHAR NUMBER NUMERIC ou INTEGER ou BIGINT selon le cas
-- Oracle SELECT SYSDATE FROM dual; -- PostgreSQL SELECT CURRENT_TIMESTAMP; -- Oracle SELECT NVL(commission_pct, 0) FROM employees; -- PostgreSQL SELECT COALESCE(commission_pct, 0) FROM employees; -- Oracle SELECT * FROM employees WHERE ROWNUM <= 10; -- PostgreSQL SELECT * FROM employees LIMIT 10; -- Oracle SELECT * FROM a MINUS SELECT * FROM b; -- PostgreSQL SELECT * FROM a EXCEPT SELECT * FROM b;
Voici une série de cas typiques que l'on rencontre très souvent quand on passe d'Oracle à PostgreSQL. Le but n'est pas seulement de mémoriser une équivalence, mais d'adopter le bon réflexe PostgreSQL.
-- Oracle SELECT SYSDATE, USER FROM dual; -- PostgreSQL SELECT CURRENT_TIMESTAMP, CURRENT_USER;
-- Oracle SELECT employee_id, NVL(commission_pct, 0) AS commission FROM employees; -- PostgreSQL SELECT employee_id, COALESCE(commission_pct, 0) AS commission FROM employees;
-- Oracle
SELECT employee_id,
NVL2(commission_pct, 'commissionnée', 'sans commission') AS statut
FROM employees;
-- PostgreSQL
SELECT employee_id,
CASE
WHEN commission_pct IS NOT NULL THEN 'commissionnée'
ELSE 'sans commission'
END AS statut
FROM employees;
-- Oracle
SELECT employee_id,
DECODE(status, 'A', 'Actif', 'I', 'Inactif', 'Inconnu') AS libelle
FROM users_app;
-- PostgreSQL
SELECT employee_id,
CASE status
WHEN 'A' THEN 'Actif'
WHEN 'I' THEN 'Inactif'
ELSE 'Inconnu'
END AS libelle
FROM users_app;
-- Oracle SELECT * FROM employees WHERE ROWNUM <= 5; -- PostgreSQL SELECT * FROM employees LIMIT 5;
-- Oracle 12c+ SELECT * FROM employees ORDER BY employee_id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY; -- PostgreSQL SELECT * FROM employees ORDER BY employee_id OFFSET 20 LIMIT 10;
-- Oracle
SELECT employee_id, manager_id, first_name, LEVEL
FROM employees
START WITH manager_id IS NULL
CONNECT BY PRIOR employee_id = manager_id;
-- PostgreSQL
WITH RECURSIVE hierarchy AS (
SELECT employee_id, manager_id, first_name, 1 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.employee_id, e.manager_id, e.first_name, h.level + 1
FROM employees e
JOIN hierarchy h ON h.employee_id = e.manager_id
)
SELECT employee_id, manager_id, first_name, level
FROM hierarchy;
-- Oracle SELECT employee_id FROM anciens_employes MINUS SELECT employee_id FROM employees; -- PostgreSQL SELECT employee_id FROM anciens_employes EXCEPT SELECT employee_id FROM employees;
-- Oracle
SELECT department_id,
LISTAGG(last_name, ', ') WITHIN GROUP (ORDER BY last_name) AS noms
FROM employees
GROUP BY department_id;
-- PostgreSQL
SELECT department_id,
STRING_AGG(last_name, ', ' ORDER BY last_name) AS noms
FROM employees
GROUP BY department_id;
-- Oracle
SELECT TRUNC(SYSDATE) FROM dual;
-- PostgreSQL
SELECT CURRENT_DATE;
-- Variante si on part d'un timestamp
SELECT DATE_TRUNC('day', CURRENT_TIMESTAMP);
-- Oracle
SELECT TRUNC(SYSDATE, 'MM') FROM dual;
-- PostgreSQL
SELECT DATE_TRUNC('month', CURRENT_TIMESTAMP);
-- Oracle SELECT ADD_MONTHS(SYSDATE, 3) FROM dual; -- PostgreSQL SELECT CURRENT_DATE + INTERVAL '3 months';
-- Oracle SELECT LENGTH(last_name), SUBSTR(last_name, 1, 3) FROM employees; -- PostgreSQL SELECT LENGTH(last_name), SUBSTRING(last_name FROM 1 FOR 3) FROM employees;
-- Oracle
SELECT *
FROM employees
WHERE UPPER(last_name) LIKE UPPER('sm%');
-- PostgreSQL
SELECT *
FROM employees
WHERE last_name ILIKE 'sm%';
-- Oracle (souvent MERGE)
MERGE INTO departments d
USING (SELECT 10 AS department_id, 'Sales' AS department_name FROM dual) s
ON (d.department_id = s.department_id)
WHEN MATCHED THEN
UPDATE SET d.department_name = s.department_name
WHEN NOT MATCHED THEN
INSERT (department_id, department_name)
VALUES (s.department_id, s.department_name);
-- PostgreSQL
INSERT INTO departments (department_id, department_name)
VALUES (10, 'Sales')
ON CONFLICT (department_id)
DO UPDATE SET department_name = EXCLUDED.department_name;
-- Oracle
INSERT INTO departments (department_id, department_name)
VALUES (seq_departments.NEXTVAL, 'Support')
RETURNING department_id INTO :v_department_id;
-- PostgreSQL
INSERT INTO departments (department_name)
VALUES ('Support')
RETURNING department_id;
-- Oracle ancien style SELECT e.first_name, d.department_name FROM employees e, departments d WHERE e.department_id = d.department_id(+); -- PostgreSQL SELECT e.first_name, d.department_name FROM employees e LEFT JOIN departments d ON e.department_id = d.department_id;
-- Oracle
SELECT seq_factures.NEXTVAL FROM dual;
-- PostgreSQL
SELECT nextval('seq_factures');
-- Oracle
SELECT *
FROM (
SELECT e.*,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn
FROM employees e
)
WHERE rn = 1;
-- PostgreSQL
SELECT *
FROM (
SELECT e.*,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn
FROM employees e
) sub
WHERE rn = 1;
-- Équivalent fréquent pour garder la première ligne par groupe
SELECT DISTINCT ON (department_id)
department_id, employee_id, first_name, salary
FROM employees
ORDER BY department_id, salary DESC;
-- Oracle
SELECT TO_NUMBER('123'), TO_CHAR(456)
FROM dual;
-- PostgreSQL
SELECT CAST('123' AS NUMERIC), CAST(456 AS TEXT);
-- Variante PostgreSQL courte
SELECT '123'::NUMERIC, 456::TEXT;
-- Oracle
SELECT *
FROM clients
WHERE REGEXP_LIKE(email, '^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$', 'i');
-- PostgreSQL
SELECT *
FROM clients
WHERE email ~* '^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$';
-- Oracle : '' est souvent traite comme NULL SELECT NVL(code_interne, 'N/A') FROM clients; -- PostgreSQL : '' reste une vraie chaîne vide SELECT COALESCE(NULLIF(code_interne, ''), 'N/A') FROM clients;
-- Oracle SELECT 10 AS id, 'test' AS libelle FROM dual; -- PostgreSQL SELECT 10 AS id, 'test' AS libelle; -- Ou quand on veut un petit jeu de données inline VALUES (10, 'test'), (20, 'demo');
SELECT NOW(); suffit.'' comme NULL, PostgreSQL non.LIMIT et OFFSET, même si FETCH FIRST existe.INSERT ... ON CONFLICT.TRUNC(date), en PostgreSQL on utilise souvent DATE_TRUNC().SELECT ... FROM dual par SELECT ....NVL par COALESCE et DECODE par CASE.CONNECT BY par WITH RECURSIVE.MINUS par EXCEPT.STRING_AGG à la place de LISTAGG.GENERATED ... AS IDENTITY ou les séquences avec nextval().Pour les rappels Oracle, consultez aussi le tutoriel Oracle SQL et le tutoriel Oracle PL/SQL. Pour prolonger PostgreSQL, consultez aussi PostgreSQL avancé et administration et PostgreSQL PL/pgSQL. Pour les usages SQL orientés données et IA, voir également la page IA Oracle / SQL / PL/SQL.