4
votes

Moyen efficace de remplir les valeurs nulles dans les données de séries chronologiques à l'aide de T-SQL

J'ai une table avec des données de séries chronologiques qui sont principalement des valeurs nulles, et je veux remplir toutes les valeurs nulles avec la dernière valeur connue.

J'ai quelques solutions, mais elles sont beaucoup plus lentes que de faire le opération équivalente DataFrame.fillna (method = 'ffill') dans Pandas.

Une version simplifiée du code / des données que j'utilise:

date       price ff_price
---------- ----- --------
2016-07-11 0.79  0.79
2016-07-12 NULL  0.79
2016-07-13 NULL  0.79
2016-07-14 0.69  0.69
2016-07-15 NULL  0.69
...
2016-09-21 0.88  0.88
...

Pour produire le tableau

select d.[date], d.[price],
       (select top 1 p.price from price_table p
        where p.price is not null and p.[date] <= p.[date]
        order by p.[date] desc) as ff_price
from price_table d

J'ai plus de 100 millions de lignes, donc cela prend un certain temps.


0 commentaires

3 Réponses :


3
votes

Cela ressemble à une question «classique» sur les lacunes et les îles. En supposant que vous n'utilisez pas 2008 ou une version antérieure (qui ne sont (presque) pas entièrement compatibles), cela devrait obtenir le résultat que vous recherchez:

WITH CTE AS(
    SELECT [date],
           price,
           COUNT(CASE WHEN price IS NOT NULL THEN 1 END) OVER (ORDER BY [date]
                                                               ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS Grp
    FROM price_table p)
SELECT [date],
       price,
       MIN(price) OVER (PARTITION BY grp) AS ff_price
FROM CTE;

db violon


5 commentaires

C'était de loin plus rapide que les autres solutions: avec 135 millions de lignes, il s'est terminé en <21 secondes, contre> 330 pour toutes les autres réponses. C'était aussi le plus difficile à comprendre, mais maintenant que j'ai compris pourquoi cela fonctionne, cela semble vraiment intelligent.


Heureux que vous l'ayez compris notre @ user448830. la clause ROWS BETWEEN est un excellent ajout aux fonctions de fenêtre qui ont été ajoutées dans SQL Server 2012. Si vous comprenez la syntaxe, j'espère que vous pourrez la mettre à profit à l'avenir. :)


à quoi servent les lignes entre la partie précédente ... non bornée? J'essayais de faire quelque chose de similaire et cela fonctionne pour moi sans cette partie aussi. Une simple commande par date me suffit.


ROWS ENTRE UNBOUNDED PRECEDING ET ACURRENT ROW est la fenêtre par défaut @gouravkr, je préfère juste la définir; juste au cas où cela changerait.


J'avais une solution similaire où je définirais la variable de regroupement dans une sous-requête, puis en prendreais une somme cumulée à l'extérieur. Cette solution a réalisé la même chose avec un niveau de sous-requête en moins. Il est intéressant de voir combien de façons il existe pour résoudre le même problème.



1
votes

Vous pouvez également utiliser APPLY :

SELECT t.*, t1.price AS ff_price
FROM price_table t OUTER APPLY
     (SELECT TOP (1) t1.*
      FROM price_table t1
      WHERE t1.[date] <= t.[date] AND t1.price IS NOT NULL
      ORDER BY t1.[date] DESC
     ) t1;


0 commentaires

1
votes

En supposant que votre colonne est DATE et que le prix est DECIMAL (5,2) , veuillez tester cette approche:

date        price   ff_price    WindowedMax
2016-07-10  NULL    NULL        NULL
2016-07-11  0.79    0.79        0x9B3B0B050200014F
2016-07-12  NULL    0.79        0x9B3B0B050200014F
2016-07-13  NULL    0.79        0x9B3B0B050200014F
2016-07-14  0.69    0.69        0x9E3B0B0502000145
2016-07-15  NULL    0.69        0x9E3B0B0502000145
2016-07-21  0.88    0.88        0xA53B0B0502000158
2016-07-22  NULL    0.88        0xA53B0B0502000158

Ceci est une solution que j'ai implémentée avec un problème similaire et vous pouvez trouver l'explication détaillée ici . La raison pour laquelle cette approche est bonne est que elle ne nécessite pas de tri explicite , tant que vous avez un index par date .

Ce qu'il fait est essentiellement d'utiliser un MAX fenêtré avec la concaténation des 3 octets qui composent votre colonne de date (c'est pourquoi j'ai mentionné que votre colonne doit être DATE , sinon DATETIME aura besoin de 8 octets, vous pouvez éditer la requête pour travailler avec ça) avec les octets qui composent votre colonne de prix (qui font 5 octets, également supposés). Il s'agit de la partie CAST (P. [date] AS BINARY (3)) + CAST (P. [price] AS BINARY (5)) .

Lorsque vous calculez ceci et ORDER BY P. [date] ROWS UNBOUNDED PRECEDING , le moteur effectue essentiellement un roulement maximum avec des valeurs dont les octets les plus significatifs sont vos dates. Le résultat maximum sera toujours mis à jour lorsque la date change, mais considérant que la concaténation d'une valeur avec NULL comme prix donnera également NULL (en tant que binaire), puis le MAX ignorera toujours cette valeur et conservera le MAX non nul précédent (par P. [date] ROWS UNBOUNDED PRECEDING ).

Ceci est le résultat binaire du MAX fenêtré (j'ai ajouté un enregistrement précédent avec NULL donc vous voyez que le résultat est NULL pour les valeurs de prix nulles) :

SELECT
    P.[date],
    P.[price],
    ff_price = CONVERT(
        DECIMAL(5,2),       -- Original price datatype
        SUBSTRING(
            MAX(
                CAST(P.[date] AS BINARY(3)) +   -- 3: datalength of P.[date] column
                CAST(P.[price] AS BINARY(5))    -- 5: datalength of P.[price] column
            ) OVER (ORDER BY P.[date] ROWS UNBOUNDED PRECEDING),

            4,  -- Position to start that's not the binary part of the date

            5))-- Characters that compose the binary of the original price datatype
FROM
    price_table  AS P


2 commentaires

Cette solution a nécessité beaucoup de bidouillage pour fonctionner, mais la mécanique a beaucoup de sens pour moi. Il fonctionne en fait de manière comparable à la solution de @Larnu (23 secondes), bien mieux que ma solution originale ou l'approche APPLY.


@ user448830 oui, c'est un peu plus une solution "sous le capot", mais quand il s'agit de gros jeux de données ou de performances, apprendre ces astuces est vraiment utile. Heureux de vous aider :)