Considérez deux tables:
Transactions , avec montants en monnaie étrangère:
Date Amount
========= =======
1/2/2009 1500
2/4/2009 2300
3/15/2009 300
4/17/2009 2200
etc.
ExchangeRates , avec la valeur de la devise principale (disons dollars) dans la devise étrangère:
Date Rate
========= =======
2/1/2009 40.1
3/1/2009 41.0
4/1/2009 38.5
5/1/2009 42.7
etc.
Les taux de change peuvent être entrés pour des dates arbitraires - l'utilisateur peut les entrer quotidiennement, hebdomadairement, mensuellement ou à des intervalles irréguliers.
Pour convertir les montants en devises en dollars, je dois respecter ces règles:
A. Si possible, utilisez le dernier taux précédent. la transaction du 2/4/2009 utilise donc le taux du 2/1/2009 et la transaction du 15/03/2009 utilise le taux du 3/1/2009.
B. S'il n'y a pas de tarif défini pour une date antérieure, utilisez le tarif le plus ancien disponible. La transaction du 1/2/2009 utilise donc le taux du 2/1/2009, car aucun taux antérieur n'a été défini.
Cela marche...
Select
t.Date,
t.Amount,
ConvertedAmount=(
Select Top 1
t.Amount/ex.Rate
From ExchangeRates ex
Where t.Date > ex.Date
Order by ex.Date desc
)
From Transactions t
... mais (1) il semble qu'une jointure serait plus efficace et élégante, et (2) il ne traite pas de la règle B ci-dessus.
Existe-t-il une alternative à l'utilisation de la sous-requête pour trouver le taux approprié? Et y a-t-il une manière élégante de manipuler la règle B, sans me nouer?
Vous pouvez d’abord faire une auto-association sur les taux de change, classés par date, de manière à avoir la date de début et de fin de chaque taux de change, sans chevauchement ni écart entre les dates (peut-être ajoutez-le à votre base de données - dans mon cas, j'utilise simplement une expression de table commune).
Joindre ces taux "établis" aux transactions est simple et efficace.
Quelque chose comme:
WITH IndexedExchangeRates AS (
SELECT Row_Number() OVER (ORDER BY Date) ix,
Date,
Rate
FROM ExchangeRates
),
RangedExchangeRates AS (
SELECT CASE WHEN IER.ix=1 THEN CAST('1753-01-01' AS datetime)
ELSE IER.Date
END DateFrom,
COALESCE(IER2.Date, GETDATE()) DateTo,
IER.Rate
FROM IndexedExchangeRates IER
LEFT JOIN IndexedExchangeRates IER2
ON IER.ix = IER2.ix-1
)
SELECT T.Date,
T.Amount,
RER.Rate,
T.Amount/RER.Rate ConvertedAmount
FROM Transactions T
LEFT JOIN RangedExchangeRates RER
ON (T.Date > RER.DateFrom) AND (T.Date <= RER.DateTo)
Remarques:
Vous pouvez remplacer GETDATE()
par une date dans un avenir lointain. Je suppose ici qu'aucun taux pour l'avenir n'est connu.
La règle (B) est implémentée en définissant la date du premier taux de change connu sur la date minimale prise en charge par SQL Server datetime
, qui devrait (par définition si c'est le type que vous utilisez pour la colonne Date
) être la plus petite valeur possible.
Supposons que vous disposiez d’une table de taux de change étendue contenant:
Start Date End Date Rate
========== ========== =======
0001-01-01 2009-01-31 40.1
2009-02-01 2009-02-28 40.1
2009-03-01 2009-03-31 41.0
2009-04-01 2009-04-30 38.5
2009-05-01 9999-12-31 42.7
Nous pouvons discuter des détails pour savoir si les deux premières lignes doivent être combinées, mais l'idée générale est qu'il est trivial de trouver le taux de change pour une date donnée. Cette structure fonctionne avec l'opérateur SQL 'BETWEEN' qui inclut les extrémités des plages. Souvent, un meilleur format pour les gammes est «ouvert-fermé»; la première date indiquée est incluse et la seconde est exclue. Notez qu'il existe une contrainte sur les lignes de données: il n'y a (a) pas de lacunes dans la couverture de la plage de dates et (b) pas de chevauchements dans la couverture. L'application de ces contraintes n'est pas complètement triviale (euphémisme poli - méiose).
Maintenant, la requête de base est triviale et le cas B n'est plus un cas particulier:
SELECT T.Date, T.Amount, X.Rate
FROM Transactions AS T JOIN ExtendedExchangeRates AS X
ON T.Date BETWEEN X.StartDate AND X.EndDate;
La partie délicate consiste à créer la table ExtendedExchangeRate à partir de la table ExchangeRate donnée à la volée. S'il s'agit d'une option, il serait judicieux de modifier la structure de la table ExchangeRate de base afin qu'elle corresponde à la table ExtendedExchangeRate. vous résolvez le problème lorsque les données sont saisies (une fois par mois) au lieu de chaque fois qu'un taux de change doit être déterminé (plusieurs fois par jour).
Comment créer la table de taux de change étendue? Si votre système prend en charge l'ajout ou la soustraction de 1 à une valeur de date pour obtenir le jour suivant ou précédent (et possède un tableau à une seule ligne appelé "Double"), une variante Fonctionnera (sans utiliser aucun OLAP fonctions):
CREATE TABLE ExchangeRate
(
Date DATE NOT NULL,
Rate DECIMAL(10,5) NOT NULL
);
INSERT INTO ExchangeRate VALUES('2009-02-01', 40.1);
INSERT INTO ExchangeRate VALUES('2009-03-01', 41.0);
INSERT INTO ExchangeRate VALUES('2009-04-01', 38.5);
INSERT INTO ExchangeRate VALUES('2009-05-01', 42.7);
Première rangée:
SELECT '0001-01-01' AS StartDate,
(SELECT MIN(Date) - 1 FROM ExchangeRate) AS EndDate,
(SELECT Rate FROM ExchangeRate
WHERE Date = (SELECT MIN(Date) FROM ExchangeRate)) AS Rate
FROM Dual;
Résultat:
0001-01-01 2009-01-31 40.10000
Dernière rangée:
SELECT (SELECT MAX(Date) FROM ExchangeRate) AS StartDate,
'9999-12-31' AS EndDate,
(SELECT Rate FROM ExchangeRate
WHERE Date = (SELECT MAX(Date) FROM ExchangeRate)) AS Rate
FROM Dual;
Résultat:
2009-05-01 9999-12-31 42.70000
Rangées du milieu:
SELECT X1.Date AS StartDate,
X2.Date - 1 AS EndDate,
X1.Rate AS Rate
FROM ExchangeRate AS X1 JOIN ExchangeRate AS X2
ON X1.Date < X2.Date
WHERE NOT EXISTS
(SELECT *
FROM ExchangeRate AS X3
WHERE X3.Date > X1.Date AND X3.Date < X2.Date
);
Résultat:
2009-02-01 2009-02-28 40.10000
2009-03-01 2009-03-31 41.00000
2009-04-01 2009-04-30 38.50000
Notez que la sous-requête NOT EXISTS est plutôt cruciale. Sans cela, le résultat des "lignes du milieu" est:
2009-02-01 2009-02-28 40.10000
2009-02-01 2009-03-31 40.10000 # Unwanted
2009-02-01 2009-04-30 40.10000 # Unwanted
2009-03-01 2009-03-31 41.00000
2009-03-01 2009-04-30 41.00000 # Unwanted
2009-04-01 2009-04-30 38.50000
Le nombre de lignes non désirées augmente considérablement à mesure que la taille de la table augmente (pour N> 2 lignes, il y a (N-2) * (N - 3)/2 lignes non désirées, je crois).
Le résultat de ExtendedExchangeRate est l'union (disjointe) des trois requêtes:
SELECT DATE '0001-01-01' AS StartDate,
(SELECT MIN(Date) - 1 FROM ExchangeRate) AS EndDate,
(SELECT Rate FROM ExchangeRate
WHERE Date = (SELECT MIN(Date) FROM ExchangeRate)) AS Rate
FROM Dual
UNION
SELECT X1.Date AS StartDate,
X2.Date - 1 AS EndDate,
X1.Rate AS Rate
FROM ExchangeRate AS X1 JOIN ExchangeRate AS X2
ON X1.Date < X2.Date
WHERE NOT EXISTS
(SELECT *
FROM ExchangeRate AS X3
WHERE X3.Date > X1.Date AND X3.Date < X2.Date
)
UNION
SELECT (SELECT MAX(Date) FROM ExchangeRate) AS StartDate,
DATE '9999-12-31' AS EndDate,
(SELECT Rate FROM ExchangeRate
WHERE Date = (SELECT MAX(Date) FROM ExchangeRate)) AS Rate
FROM Dual;
Sur le SGBD de test (IBM Informix Dynamic Server 11.50.FC6 sur MacOS X 10.6.2), j'ai pu convertir la requête en vue, mais je devais arrêter de tricher avec les types de données - en forçant les chaînes dans des dates:
CREATE VIEW ExtendedExchangeRate(StartDate, EndDate, Rate) AS
SELECT DATE('0001-01-01') AS StartDate,
(SELECT MIN(Date) - 1 FROM ExchangeRate) AS EndDate,
(SELECT Rate FROM ExchangeRate WHERE Date = (SELECT MIN(Date) FROM ExchangeRate)) AS Rate
FROM Dual
UNION
SELECT X1.Date AS StartDate,
X2.Date - 1 AS EndDate,
X1.Rate AS Rate
FROM ExchangeRate AS X1 JOIN ExchangeRate AS X2
ON X1.Date < X2.Date
WHERE NOT EXISTS
(SELECT *
FROM ExchangeRate AS X3
WHERE X3.Date > X1.Date AND X3.Date < X2.Date
)
UNION
SELECT (SELECT MAX(Date) FROM ExchangeRate) AS StartDate,
DATE('9999-12-31') AS EndDate,
(SELECT Rate FROM ExchangeRate WHERE Date = (SELECT MAX(Date) FROM ExchangeRate)) AS Rate
FROM Dual;
Je ne peux pas tester cela, mais je pense que cela fonctionnerait. Il utilise coalesce avec deux sous-requêtes pour choisir le taux selon la règle A ou la règle B.
Select t.Date, t.Amount,
ConvertedAmount = t.Amount/coalesce(
(Select Top 1 ex.Rate
From ExchangeRates ex
Where t.Date > ex.Date
Order by ex.Date desc )
,
(select top 1 ex.Rate
From ExchangeRates
Order by ex.Date asc)
)
From Transactions t
SELECT
a.tranDate,
a.Amount,
a.Amount/a.Rate as convertedRate
FROM
(
SELECT
t.date tranDate,
e.date as rateDate,
t.Amount,
e.rate,
RANK() OVER (Partition BY t.date ORDER BY
CASE WHEN DATEDIFF(day,e.date,t.date) < 0 THEN
DATEDIFF(day,e.date,t.date) * -100000
ELSE DATEDIFF(day,e.date,t.date)
END ) AS diff
FROM
ExchangeRates e
CROSS JOIN
Transactions t
) a
WHERE a.diff = 1
La différence entre le transfert et la date du taux est calculée, puis les valeurs négatives (condition b) sont multipliées par -10000 afin qu’elles puissent toujours être classées mais des valeurs positives (condition a toujours priorité). Nous sélectionnons ensuite la différence de date minimale pour chaque transfert. en utilisant la clause de rang supérieur.
De nombreuses solutions fonctionneront. Vous devriez vraiment trouver celle qui fonctionne le mieux (le plus rapidement) pour votre charge de travail: cherchez-vous généralement une transaction, une liste, toutes?
La solution de départage en fonction de votre schéma est la suivante:
SELECT t.Date,
t.Amount,
r.Rate
--//add your multiplication/division here
FROM "Transactions" t
INNER JOIN "ExchangeRates" r
ON r."ExchangeRateID" = (
SELECT TOP 1 x."ExchangeRateID"
FROM "ExchangeRates" x
WHERE x."SourceCurrencyISO" = t."SourceCurrencyISO" --//these are currency-related filters for your tables
AND x."TargetCurrencyISO" = t."TargetCurrencyISO" --//,which you should also JOIN on
AND x."Date" <= t."Date"
ORDER BY x."Date" DESC)
Vous devez avoir les bons index pour que cette requête soit rapide. Aussi, idéalement, vous ne devriez pas avoir un JOIN
sur "Date"
, mais un champ semblable à "ID"
- (INTEGER
). Donnez-moi plus d'informations sur le schéma, je vais créer un exemple pour vous.
Aucune jointure qui soit plus élégante que la sous-requête corrélée TOP 1
dans votre message d'origine. Cependant, comme vous le dites, cela ne satisfait pas à l'exigence B.
Ces requêtes fonctionnent (SQL Server 2005 ou version ultérieure requise). Voir le SqlFiddle pour ces .
SELECT
T.*,
ExchangeRate = E.Rate
FROM
dbo.Transactions T
CROSS APPLY (
SELECT TOP 1 Rate
FROM dbo.ExchangeRate E
WHERE E.RateDate <= T.TranDate
ORDER BY
CASE WHEN E.RateDate <= T.TranDate THEN 0 ELSE 1 END,
E.RateDate DESC
) E;
Notez que CROSS APPLY avec une valeur de colonne unique est fonctionnellement équivalent à la sous-requête corrélée dans la clause SELECT
comme vous l'avez montré. Je préfère simplement CROSS APPLY dès maintenant, car il est beaucoup plus flexible et vous permet de réutiliser la valeur dans plusieurs emplacements, d’avoir plusieurs lignes (pour les non pivotés personnalisés) et d’avoir plusieurs colonnes.
SELECT
T.*,
ExchangeRate = Coalesce(E.Rate, E2.Rate)
FROM
dbo.Transactions T
OUTER APPLY (
SELECT TOP 1 Rate
FROM dbo.ExchangeRate E
WHERE E.RateDate <= T.TranDate
ORDER BY E.RateDate DESC
) E
OUTER APPLY (
SELECT TOP 1 Rate
FROM dbo.ExchangeRate E2
WHERE E.Rate IS NULL
ORDER BY E2.RateDate
) E2;
Je ne sais pas laquelle pourrait donner de meilleurs résultats, ou si l'une ou l'autre obtiendra de meilleurs résultats que les autres réponses de la page. Avec un index approprié sur les colonnes Date, ils devraient être assez performants - certainement mieux que toute solution Row_Number()
.