Migration : réécrire ses jointures ANSI 89

Dans le cadre de la migration vers SQL Server, un problème de compatibilité souvent relevé avec l’assistant de migration est la présence de jointures externes (LEFT OUTER JOIN / RIGHT OUTER JOIN) écrites avec la syntaxe ANSI 89 (dans la clause WHERE avec *= ou =*).
Cette syntaxe est encore supportée en SQL Server 2005, 2008 ou 2008 R2, à condition de configurer la base de données avec le mode de compatibilité 80, mais cela reste un mode temporaire jusqu’à la correction complète des requêtes incriminées.
Concernant Denali (prochaine version de SQL Server), il est fort probable que ces syntaxes ne seront plus du tout supportées : la migration vers cette version nécessitera donc impérativement une réécriture préalable de ces requêtes.
Quelle méthode pour limiter le risque d’erreurs lors de la réécriture ?

Le problème :

Constituons d'abord un jeu d’exemples (en SQL 2008 R2 et mode de compatibilité 80) :

CREATE DATABASE ANSI_JOINS
GO
USE ANSI_JOINS
GO
EXEC sp_dbcmptlevel 'ANSI_JOINS', 80
GO
CREATE TABLE [dbo].[ENTITE] (
[id_entite]                [int] IDENTITY(1, 1) NOT NULL,
[code]                     [int] NOT NULL,
[indicateur_perimetre]     [int] NOT NULL,
[devise]                   [int] NOT NULL,
[zone_franc]               [int] NULL
)
ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[ENTITE] ON
INSERT [dbo].[ENTITE] ([id_entite], [code], [indicateur_perimetre], [devise], [zone_franc]) VALUES (1, 123, 123, 123, 123)
INSERT [dbo].[ENTITE] ([id_entite], [code], [indicateur_perimetre], [devise], [zone_franc]) VALUES (2, 123, 123, 123, 123)
SET IDENTITY_INSERT [dbo].[ENTITE] OFF
GO
CREATE TABLE [dbo].[LIB_ENTITE] (
[entite]            [int] NOT NULL,
[langue]            [int] NOT NULL,
[libelle_court]     char(30) NOT NULL,
[libelle_long]      char(60) NOT NULL
)
ON [PRIMARY]
INSERT [dbo].[LIB_ENTITE] ([entite], [langue], [libelle_court], [libelle_long]) 
VALUES (1, 1033, N'anglais                       ', N'anglais                                                     ')
INSERT [dbo].[LIB_ENTITE] ([entite], [langue], [libelle_court], [libelle_long]) 
VALUES (1, 1036, N'francais                      ', N'francais                                                    ')
INSERT [dbo].[LIB_ENTITE] ([entite], [langue], [libelle_court], [libelle_long]) 
VALUES (2, 1033, N'anglais                       ', N'anglais                                                     ')
GO

Considérons la requête suivante :

SELECT S.id_entite, S.code, LS.libelle_court 
FROM ENTITE S, LIB_ENTITE LS
WHERE LS.entite =* S.id_entite 

Jusque là, aucun problème de traduction :

SELECT S.id_entite, S.code, LS.libelle_court 
FROM LIB_ENTITE LS RIGHT JOIN
ENTITE S ON LS.entite = S.id_entite

ou

SELECT S.id_entite, S.code, LS.libelle_court 
FROM ENTITE S LEFT JOIN LIB_ENTITE LS 
ON LS.entite = S.id_entite

Même l’interface graphique s’y retrouve :

Le problème va se poser lorsque j’ai également des conditions en plus de mes clauses de jointures :

Cas 1 :

SELECT S.id_entite, S.code, LS.libelle_court 
FROM ENTITE S, LIB_ENTITE LS 
WHERE LS.entite =* S.id_entite AND LS.langue = 1036

Dans ce cas, l’interface graphique ne sait pas nous lire cette requête correctement, et nous risquerions de faire la même erreur...

Pour obtenir les bons résultats, la condition LS.langue = 1036 doit être dans la clause de jointure et non dans la clause WHERE !

SELECT S.id_entite, S.code, LS.libelle_court 
FROM ENTITE S LEFT JOIN LIB_ENTITE LS 
ON LS.entite = S.id_entite 
AND LS.langue = 1036

Cas 2 :

SELECT S.id_entite, S.code, LS.libelle_court 
FROM ENTITE S, LIB_ENTITE LS
WHERE LS.entite =* S.id_entite 
AND S.id_entite=1

L’interface graphique a raison dans ce cas et la condition est bien un filtre de la clause WHERE

SELECT S.id_entite, S.code, LS.libelle_court 
FROM ENTITE S LEFT JOIN LIB_ENTITE LS  
ON LS.entite = S.id_entite
WHERE S.id_entite = 1

Cas 3 (fusion du cas 1 + cas 2) :

SELECT S.id_entite, S.code, LS.libelle_court 
FROM ENTITE S, LIB_ENTITE LS 
WHERE LS.entite =* S.id_entite 
AND S.id_entite=1 AND LS.langue = 1036

Ici, une condition va dans la jointure, l’autre dans le filtre... ! Il est clair que l'interface graphiques'y laissera prendre...

SELECT S.id_entite, S.code, LS.libelle_court 
FROM ENTITE S LEFT JOIN LIB_ENTITE LS 
ON LS.entite = S.id_entite AND LS.langue = 1036
WHERE S.id_entite = 1

La méthode

J’ai eu à décrire et expliquer la méthode à un développeur (voyons le cas 3) :

1. Identifier les tables et le prédicat de jointure

SELECT S.id_entite, S.code, LS.libelle_court 
FROM LIB_ENTITE LS RIGHT JOIN ENTITE S 
ON LS.entite = S.id_entite

2. La tourner en LEFT JOIN rend les choses plus facile à lire (on peut même se servir de l’interface graphique de SSMS pour réécrire cette partie)

SELECT S.id_entite, S.code, LS.libelle_court 
FROM ENTITE S LEFT JOIN LIB_ENTITE LS 
ON LS.entite = S.id_entite

3. Ajouter les conditions qui portent sur la table interne dans la clause WHERE

SELECT S.id_entite, S.code, LS.libelle_court 
FROM ENTITE S LEFT JOIN LIB_ENTITE LS 
ON LS.entite = S.id_entite
WHERE S.id_entite = 1

4. Ajouter les conditions qui portent sur la table externe dans la clause JOIN

SELECT S.id_entite, S.code, LS.libelle_court 
FROM ENTITE S LEFT JOIN LIB_ENTITE LS 
ON LS.entite = S.id_entite AND LS.langue = 103
WHERE S.id_entite = 1

Là où ça devient amusant, c’est quand il y a de nombreuses tables et conditions... Pour être sûr de ne pas laisser d'erreurs, testez bien les jeux de résultat !