Les opérateurs ensemblistes SQL permettent de combiner ou comparer les résultats de plusieurs requêtes SELECT. UNION réunit des données, INTERSECT trouve les éléments communs, et EXCEPT identifie les différences. Ces outils sont essentiels pour l'analyse de données complexes et reviennent fréquemment en entretien technique.
Ces opérateurs suivent les règles mathématiques des ensembles. Ils nécessitent que les requêtes aient le même nombre de colonnes avec des types compatibles. Contrairement aux JOINs qui associent des tables horizontalement, les opérateurs ensemblistes travaillent verticalement sur les lignes de résultats.
📌 Ce qu'il faut retenir
- UNION combine les résultats de plusieurs requêtes en éliminant les doublons par défaut
- UNION ALL garde tous les résultats, y compris les doublons, et améliore les performances
- INTERSECT retourne uniquement les lignes présentes dans toutes les requêtes
- EXCEPT (ou MINUS sur Oracle) retourne les lignes de la première requête absentes de la seconde
- Toutes les requêtes doivent avoir le même nombre de colonnes avec des types compatibles
- L'ordre des colonnes et leur position déterminent la correspondance
UNION : combiner des ensembles de données
UNION rassemble les résultats de deux ou plusieurs requêtes SELECT. Par défaut, il élimine automatiquement les doublons, ce qui peut impacter les performances sur de gros volumes.
-- Combiner clients actifs et prospects
SELECT nom, email, 'Client' as type
FROM clients
WHERE statut = 'actif'
UNION
SELECT nom, email, 'Prospect' as type
FROM prospects
WHERE date_creation > '2026-01-01';
UNION ALL conserve tous les résultats, incluant les doublons. Utilisez-le quand vous êtes certains qu'il n'y a pas de doublons ou quand vous voulez les conserver.
```sql
-- Historique complet des ventes (plus rapide)
SELECT date_vente, montant, 'Magasin' as canal
FROM ventes_magasin
WHERE YEAR(date_vente) = 2026
UNION ALL
SELECT date_commande, total, 'Online' as canal
FROM commandes_web
WHERE YEAR(date_commande) = 2026;
### Cas d'usage avancés avec UNION
Marie, analyste BI chez une chaîne de distribution, utilise UNION pour créer un tableau de bord unifié des ventes. Elle combine les données de 15 magasins physiques avec celles de la boutique en ligne :
```sql
-- Consolidation multi-canaux pour reporting mensuel
SELECT
'Physique' as canal,
magasin_id as point_vente,
DATE_FORMAT(date_vente, '%Y-%m') as mois,
SUM(montant) as ca_mensuel,
COUNT(*) as nb_transactions
FROM ventes_magasin
WHERE date_vente >= '2026-01-01'
GROUP BY magasin_id, DATE_FORMAT(date_vente, '%Y-%m')
UNION ALL
SELECT
'Digital' as canal,
'WEB-001' as point_vente,
DATE_FORMAT(date_commande, '%Y-%m') as mois,
SUM(total) as ca_mensuel,
COUNT(*) as nb_transactions
FROM commandes_web
WHERE date_commande >= '2026-01-01'
GROUP BY DATE_FORMAT(date_commande, '%Y-%m');
💡 Bon à savoir
Pour optimiser UNION sur de gros volumes, créez des index sur les colonnes utilisées dans les clauses WHERE et ORDER BY. Préférez UNION ALL quand vous contrôlez l'unicité des données.
INTERSECT : identifier les éléments communs
INTERSECT retourne uniquement les lignes présentes dans toutes les requêtes. Cet opérateur est parfait pour analyser les recoupements entre différents segments.
-- Clients ayant acheté à la fois en magasin et en ligne
SELECT client_id, nom, email
FROM clients c
JOIN ventes_magasin vm ON c.id = vm.client_id
INTERSECT
SELECT client_id, nom, email
FROM clients c
JOIN commandes_web cw ON c.id = cw.client_id;
⚠️ Attention
INTERSECT n'est pas supporté par MySQL. Utilisez plutôt EXISTS ou des JOINs pour obtenir le même résultat.
-- Alternative pour MySQL
SELECT DISTINCT c.client_id, c.nom, c.email
FROM clients c
JOIN ventes_magasin vm ON c.id = vm.client_id
WHERE EXISTS (
SELECT 1 FROM commandes_web cw
WHERE cw.client_id = c.id
);
Analyse de segments cross-canal
Thomas, responsable CRM, identifie les clients multi-canaux pour personnaliser ses campagnes. Cette approche est particulièrement utile pour les analyses de funnel conversion :
-- Clients premium présents sur tous les canaux
SELECT client_id, segment_valeur
FROM analyse_comportement
WHERE canal = 'magasin' AND segment_valeur = 'premium'
INTERSECT
SELECT client_id, segment_valeur
FROM analyse_comportement
WHERE canal = 'web' AND segment_valeur = 'premium'
INTERSECT
SELECT client_id, segment_valeur
FROM analyse_comportement
WHERE canal = 'mobile' AND segment_valeur = 'premium';
Cette approche révèle le noyau dur des clients à forte valeur, présents sur tous les points de contact.
EXCEPT : analyser les différences
EXCEPT (MINUS sur Oracle) retourne les lignes de la première requête qui ne sont pas présentes dans la seconde. L'ordre des requêtes est crucial car EXCEPT n'est pas commutatif.
-- Clients inscrits mais n'ayant jamais commandé
SELECT client_id, nom, email
FROM clients
WHERE date_inscription < '2026-01-01'
EXCEPT
SELECT c.client_id, c.nom, c.email
FROM clients c
JOIN commandes cmd ON c.id = cmd.client_id;
⚠️ Attention
EXCEPT compare toutes les colonnes sélectionnées. Une différence sur n'importe quelle colonne inclura la ligne dans le résultat.
Détection d'anomalies et d'écarts
Julien, data analyst, utilise EXCEPT pour détecter les incohérences entre les systèmes :
-- Produits en stock selon l'ERP mais absents du catalogue web
SELECT sku, nom_produit, stock_physique
FROM inventaire_erp
WHERE stock_physique > 0 AND statut = 'actif'
EXCEPT
SELECT sku, nom_produit, stock_affiche
FROM catalogue_web
WHERE disponible = 1;
Cette requête révèle les opportunités de vente manquées dues à une synchronisation défaillante.
Spécificités par SGBD et alternatives
Compatibilité des opérateurs ensemblistes
| SGBD | UNION | INTERSECT | EXCEPT/MINUS | Particularités |
|---|---|---|---|---|
| MySQL | ✅ Complet | ❌ Non supporté | ❌ Non supporté | Utiliser EXISTS/JOINs |
| PostgreSQL | ✅ Complet | ✅ Complet | ✅ EXCEPT | Support complet SQL standard |
| SQL Server | ✅ Complet | ✅ Complet | ✅ EXCEPT | Performance optimisée |
| Oracle | ✅ Complet | ✅ Complet | ✅ MINUS | Syntaxe MINUS historique |
| SQLite | ✅ Complet | ✅ Complet | ✅ EXCEPT | Limité sur gros volumes |
Alternatives pour MySQL
Depuis la version 8.0, MySQL propose des solutions de contournement plus performantes :
-- INTERSECT avec Window Functions (MySQL 8.0+)
WITH combined AS (
SELECT client_id, nom, 'magasin' as source FROM clients_magasin
UNION ALL
SELECT client_id, nom, 'web' as source FROM clients_web
)
SELECT client_id, nom
FROM (
SELECT client_id, nom,
COUNT(DISTINCT source) as nb_sources
FROM combined
GROUP BY client_id, nom
HAVING nb_sources = 2
) intersect_result;
-- EXCEPT avec NOT EXISTS (plus performant)
SELECT client_id, nom, email
FROM clients c
WHERE NOT EXISTS (
SELECT 1 FROM commandes cmd
WHERE cmd.client_id = c.id
);
Optimisation et performances
Stratégies de performance
Les opérateurs ensemblistes peuvent être coûteux sur de gros volumes. Voici les meilleures pratiques :
- Filtrage précoce : Appliquez les WHERE avant les opérateurs
- Index appropriés : Créez des index sur les colonnes de jointure
- UNION ALL vs UNION : Préférez UNION ALL quand possible
- Limitation des colonnes : Sélectionnez uniquement les colonnes nécessaires
-- ❌ Inefficace : opération sur toutes les données
SELECT * FROM grosse_table_1
UNION
SELECT * FROM grosse_table_2;
-- ✅ Optimisé : filtrage précoce
SELECT id, nom, email FROM grosse_table_1
WHERE date_creation >= '2026-01-01'
UNION ALL
SELECT id, nom, email FROM grosse_table_2
WHERE date_creation >= '2026-01-01';
Métriques de performance observées
D'après les benchmarks sur une base de données e-commerce (10M de lignes) :
| Opération | Temps d'exécution | Utilisation mémoire | Optimisation clé |
|---|---|---|---|
| UNION ALL | 1.2s | 256 MB | Pas de tri nécessaire |
| UNION | 4.7s | 512 MB | Tri + déduplication |
| INTERSECT | 3.1s | 384 MB | Hash join optimal |
| EXCEPT | 2.8s | 320 MB | Index sur clés de comparaison |
Erreurs fréquentes à éviter
1. Incompatibilité des types de données
-- ❌ Erreur : types incompatibles
SELECT nom, age FROM employes -- age = INTEGER
UNION
SELECT nom, salaire FROM consultants; -- salaire = DECIMAL
-- ✅ Correct : conversion explicite
SELECT nom, CAST(age AS DECIMAL) FROM employes
UNION
SELECT nom, salaire FROM consultants;
2. Nombre de colonnes différent
⚠️ Attention
Chaque requête dans un opérateur ensembliste doit retourner exactement le même nombre de colonnes, dans le même ordre.
-- ❌ Erreur : nombre de colonnes différent
SELECT nom, prenom FROM clients
UNION
SELECT nom, prenom, email FROM prospects;
-- ✅ Correct : nombre égal de colonnes
SELECT nom, prenom, NULL as email FROM clients
UNION
SELECT nom, prenom, email FROM prospects;
3. Confusion entre UNION et JOIN
Sarah, développeuse junior, confondait souvent ces concepts. Pour mieux comprendre les différences, consultez notre guide sur les JOINs en SQL :
-- ❌ Tentative incorrecte de "jointure"
SELECT c.nom, c.email FROM clients c
UNION
SELECT a.numero, a.montant FROM achats a; -- Colonnes non liées
-- ✅ Solution correcte avec JOIN
SELECT c.nom, c.email, a.numero, a.montant
FROM clients c
LEFT JOIN achats a ON c.id = a.client_id;
Cas concrets sectoriels
E-commerce : analyse de panier
Laurent, data scientist chez une marketplace, analyse les comportements d'achat :
-- Produits achetés ensemble mais jamais séparément
SELECT produit_id FROM paniers
WHERE panier_id IN (
SELECT panier_id FROM paniers
GROUP BY panier_id
HAVING COUNT(*) > 1
)
EXCEPT
SELECT produit_id FROM paniers
WHERE panier_id IN (
SELECT panier_id FROM paniers
GROUP BY panier_id
HAVING COUNT(*) = 1
);
Finance : détection de fraude
Amélie, analyste risque, utilise INTERSECT pour identifier les transactions suspectes :
-- Comptes avec transactions à la fois très élevées ET fréquentes
SELECT compte_id, SUM(montant) as total
FROM transactions
WHERE montant > 10000
GROUP BY compte_id
INTERSECT
SELECT compte_id, COUNT(*) as frequence
FROM transactions
WHERE DATE(date_transaction) = CURRENT_DATE
GROUP BY compte_id
HAVING COUNT(*) > 20;
Ressources humaines : gestion des compétences
David, RH dans une ESN, cartographie les compétences :
-- Consultants maîtrisant à la fois Java ET Python ET SQL
SELECT consultant_id, nom FROM competences
WHERE technologie = 'Java' AND niveau >= 4
INTERSECT
SELECT consultant_id, nom FROM competences
WHERE technologie = 'Python' AND niveau >= 4
INTERSECT
SELECT consultant_id, nom FROM competences
WHERE technologie = 'SQL' AND niveau >= 4;
Validation et tests des requêtes
Méthodologie de vérification
Avant de déployer des requêtes ensemblistes en production, suivez cette checklist :
- Vérification des types : Utilisez DESCRIBE ou SHOW COLUMNS
- Test sur échantillon : Limitez avec LIMIT/TOP pour valider la logique
- Contrôle de cohérence : Vérifiez les totaux avec COUNT(*)
- Performance : Mesurez avec EXPLAIN PLAN
-- Test de cohérence pour UNION
SELECT 'Table1' as source, COUNT(*) as lignes FROM table1
WHERE conditions
UNION ALL
SELECT 'Table2' as source, COUNT(*) as lignes FROM table2
WHERE conditions
UNION ALL
SELECT 'UNION_ALL' as source, COUNT(*) as lignes FROM (
SELECT * FROM table1 WHERE conditions
UNION ALL
SELECT * FROM table2 WHERE conditions
) combined;
Debugging des résultats inattendus
Quand vos opérateurs ensemblistes ne retournent pas les résultats escomptés :
-- Analyser les différences de structure
SELECT
'Query1' as source,
COLUMN_NAME,
DATA_TYPE,
ORDINAL_POSITION
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'table1'
UNION ALL
SELECT
'Query2' as source,
COLUMN_NAME,
DATA_TYPE,
ORDINAL_POSITION
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'table2'
ORDER BY ORDINAL_POSITION, source;
Comparaison des opérateurs ensemblistes
| Opérateur | Fonction | Gestion doublons | Support MySQL | Performance |
|---|---|---|---|---|
| UNION | Combine en éliminant doublons | Supprime automatiquement | ✅ Oui | Moyen (tri requis) |
| UNION ALL | Combine en gardant doublons | Conserve tout | ✅ Oui | Rapide (pas de tri) |
| INTERSECT | Éléments communs uniquement | Supprime automatiquement | ❌ Non | Moyen |
| EXCEPT | Différence entre ensembles | Supprime automatiquement | ❌ Non | Moyen |
Règles et bonnes pratiques
Les opérateurs ensemblistes imposent des contraintes strictes sur la structure des requêtes. Le nombre de colonnes doit être identique, et les types de données compatibles à chaque position.
-- ✅ Correct : même structure
SELECT nom, age FROM employes
UNION
SELECT prenom, salaire FROM consultants; -- Types compatibles
-- ❌ Incorrect : nombre de colonnes différent
SELECT nom, age FROM employes
UNION
SELECT prenom, salaire, departement FROM consultants;
L'ordre des colonnes détermine la correspondance. Les noms de colonnes du résultat final proviennent de la première requête.
-- Les noms finaux seront 'nom' et 'age'
SELECT nom, age FROM employes
UNION
SELECT prenom, salaire FROM consultants;
Pour optimiser les performances, utilisez UNION ALL quand c'est possible et ajoutez des clauses WHERE pour réduire les datasets avant l'opération.
Gestion des valeurs NULL
Les opérateurs ensemblistes traitent NULL comme une valeur normale lors des comparaisons :
-- NULL = NULL pour les opérateurs ensemblistes
SELECT nom, manager_id FROM employes -- manager_id peut être NULL
UNION
SELECT nom, responsable_id FROM stagiaires; -- responsable_id peut être NULL
💡 Bon à savoir
Contrairement aux comparaisons classiques où NULL ≠ NULL, les opérateurs ensemblistes considèrent deux valeurs NULL comme identiques lors de la déduplication.
Cas concret : analyse de campagne marketing
Analysons l'efficacité de différents canaux marketing en utilisant les opérateurs ensemblistes.
-- 1. Tous les contacts touchés (email + SMS)
SELECT client_id, 'Contact' as statut
FROM campagne_email
WHERE date_envoi >= '2026-03-01'
UNION
SELECT client_id, 'Contact' as statut
FROM campagne_sms
WHERE date_envoi >= '2026-03-01';
-- 2. Clients touchés par les deux canaux
SELECT client_id
FROM campagne_email
WHERE date_envoi >= '2026-03-01'
INTERSECT
SELECT client_id
FROM campagne_sms
WHERE date_envoi >= '2026-03-01';
-- 3. Clients email seulement (pas SMS)
SELECT client_id
FROM campagne_email
WHERE date_envoi >= '2026-03-01'
EXCEPT
SELECT client_id
FROM campagne_sms
WHERE date_envoi >= '2026-03-01';
Cette analyse révèle la portée globale, les recoupements et les segments exclusifs à chaque canal.
Extension de l'analyse marketing
Céline, responsable marketing digital, pousse l'analyse plus loin en croisant 4 canaux :
-- Matrice de présence cross-canal
WITH canal_email AS (
SELECT DISTINCT client_id FROM campagne_email
WHERE date_envoi >= '2026-03-01'
),
canal_sms AS (
SELECT DISTINCT client_id FROM campagne_sms
WHERE date_envoi >= '2026-03-01'
),
canal_push AS (
SELECT DISTINCT client_id FROM notifications_push
WHERE date_envoi >= '2026-03-01'
),
canal_courrier AS (
SELECT DISTINCT client_id FROM campagne_courrier
WHERE date_envoi >= '2026-03-01'
)
SELECT
'Email + SMS + Push + Courrier' as segment,
COUNT(*) as nb_clients
FROM canal_email e
JOIN canal_sms s ON e.client_id = s.client_id
JOIN canal_push p ON e.client_id = p.client_id
JOIN canal_courrier c ON e.client_id = c.client_id
UNION ALL
SELECT
'Email uniquement' as segment,
COUNT(*) as nb_clients
FROM (
SELECT client_id FROM canal_email
EXCEPT
(SELECT client_id FROM canal_sms
UNION
SELECT client_id FROM canal_push
UNION
SELECT client_id FROM canal_courrier)
) email_only;
Techniques avancées et patterns
Pattern de réconciliation de données
Lors de migrations ou de synchronisations entre systèmes :
-- Audit de cohérence entre systèmes source et cible
SELECT 'Source seulement' as statut, source_id, nom
FROM systeme_source
EXCEPT
SELECT 'Source seulement', target_id, nom
FROM systeme_cible
UNION ALL
SELECT 'Cible seulement' as statut, target_id, nom
FROM systeme_cible
EXCEPT
SELECT 'Cible seulement', source_id, nom
FROM systeme_source
UNION ALL
SELECT 'Commun' as statut, s.source_id, s.nom
FROM systeme_source s
INTERSECT
SELECT 'Commun', c.target_id, c.nom
FROM systeme_cible c;
Pattern d'analyse temporelle
Comparer les mêmes métriques sur différentes périodes :
-- Clients actifs : évolution mensuelle
SELECT 'Mars 2026' as periode, client_id, nb_commandes
FROM (
SELECT client_id, COUNT(*) as nb_commandes
FROM commandes
WHERE DATE_FORMAT(date_commande, '%Y-%m') = '2026-03'
GROUP BY client_id
HAVING COUNT(*) >= 2
) clients_mars
EXCEPT
SELECT 'Mars 2026', client_id, nb_commandes
FROM (
SELECT client_id, COUNT(*) as nb_commandes
FROM commandes
WHERE DATE_FORMAT(date_commande, '%Y-%m') = '2026-02'
GROUP BY client_id
HAVING COUNT(*) >= 2
) clients_fevrier;
Cette requête identifie les nouveaux clients actifs apparus en mars. Pour approfondir votre maîtrise des requêtes complexes, consultez notre article sur les CTEs et Common Table Expressions.
Questions fréquentes
Quelle est la différence entre UNION et JOIN ?
UNION combine verticalement les résultats de plusieurs requêtes avec une structure similaire. JOIN associe horizontalement les données de tables différentes selon des critères de correspondance. UNION travaille sur des ensembles de résultats, JOIN sur des relations entre tables.
Pourquoi UNION est-il plus lent que UNION ALL ?
UNION élimine automatiquement les doublons, ce qui nécessite un tri et une comparaison de toutes les lignes. UNION ALL concatène simplement les résultats sans vérification, d'où sa rapidité supérieure. Utilisez UNION ALL quand vous contrôlez l'absence de doublons.
Comment remplacer INTERSECT sur MySQL ?
Utilisez EXISTS avec une sous-requête ou un INNER JOIN avec DISTINCT. Ces approches reproduisent le comportement d'INTERSECT en identifiant les correspondances entre les ensembles de données.
EXCEPT fonctionne-t-il dans les deux sens ?
Non, EXCEPT n'est pas commutatif. A EXCEPT B retourne les éléments de A absents de B, tandis que B EXCEPT A retourne les éléments de B absents de A. L'ordre des requêtes change complètement le résultat.
Les opérateurs ensemblistes respectent-ils l'ordre des lignes ?
L'ordre n'est pas garanti sauf si vous ajoutez une clause ORDER BY à la requête globale. Les opérateurs ensemblistes peuvent réorganiser les lignes lors du traitement, particulièrement UNION qui effectue un tri pour éliminer les doublons.
Peut-on imbriquer plusieurs opérateurs ensemblistes ?
Oui, mais respectez la priorité des opérations. INTERSECT et EXCEPT ont la même priorité et sont évalués de gauche à droite. UNION a une priorité inférieure.
-- Évaluation : (A INTERSECT B) UNION C
SELECT * FROM table_a
INTERSECT
SELECT * FROM table_b
UNION
SELECT * FROM table_c;
-- Pour forcer l'ordre : A INTERSECT (B UNION C)
SELECT * FROM table_a
INTERSECT
(SELECT * FROM table_b
UNION
SELECT * FROM table_c);
Comment gérer les performances avec de très gros volumes ?
- Partitionnez vos données par date ou critère métier
- Utilisez des index composites sur les colonnes de sélection
- Préférez les CTEs aux sous-requêtes pour la lisibilité
- Considérez la création de vues matérialisées pour les calculs récurrents
-- Optimisation avec partition temporelle
SELECT client_id, montant FROM ventes_2025
WHERE date_vente >= '2025-12-01'
UNION ALL
SELECT client_id, montant FROM ventes_2026
WHERE date_vente >= '2026-01-01';
Ressources complémentaires
Pour maîtriser complètement les opérateurs ensemblistes et progresser en SQL, consultez :
- Guide complet des JOINs en SQL
- CTEs et Common Table Expressions
- Exercice : tunnel de conversion e-commerce
- Réussir un entretien technique SQL
Les opérateurs ensemblistes restent des outils puissants pour manipuler et analyser les données. Pratiquez régulièrement pour maîtriser leurs subtilités et savoir quand privilégier UNION, INTERSECT ou EXCEPT selon votre contexte.
