web-dev-qa-db-fra.com

Jointure SQL contre des plages de dates?

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?

15
Herb Caudill

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.

20
Lucero

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;
3
Jonathan Leffler

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
1
Ray
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.

0
Paul Creasey

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.

0
van

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().

0
ErikE