sql >> Database >  >> RDS >> PostgreSQL

Tabel met gemiddelde voorraadgeschiedenis

De speciale moeilijkheid van deze taak:u kunt niet zomaar datapunten binnen uw tijdbereik kiezen, maar moet rekening houden met de nieuwste datapunt voor het tijdbereik en de vroegste datapunt na bovendien het tijdsbestek. Dit varieert voor elke rij en elk gegevenspunt kan al dan niet bestaan. Vereist een geavanceerde zoekopdracht en maakt het moeilijk om indexen te gebruiken.

U kunt bereiktypen gebruiken en operators (Postgres 9.2+ ) om berekeningen te vereenvoudigen:

WITH input(a,b) AS (SELECT '2013-01-01'::date  -- your time frame here
                         , '2013-01-15'::date) -- inclusive borders
SELECT store_id, product_id
     , sum(upper(days) - lower(days))                    AS days_in_range
     , round(sum(value * (upper(days) - lower(days)))::numeric
                    / (SELECT b-a+1 FROM input), 2)      AS your_result
     , round(sum(value * (upper(days) - lower(days)))::numeric
                    / sum(upper(days) - lower(days)), 2) AS my_result
FROM (
   SELECT store_id, product_id, value, s.day_range * x.day_range AS days
   FROM  (
      SELECT store_id, product_id, value
           , daterange (day, lead(day, 1, now()::date)
             OVER (PARTITION BY store_id, product_id ORDER BY day)) AS day_range 
      FROM   stock
      ) s
   JOIN  (
      SELECT daterange(a, b+1) AS day_range
      FROM   input
      ) x ON s.day_range && x.day_range
   ) sub
GROUP  BY 1,2
ORDER  BY 1,2;

Let op, ik gebruik de kolomnaam day in plaats van date . Ik gebruik nooit basistypenamen als kolomnamen.

In de subquery sub Ik haal de dag uit de volgende rij voor elk item met de vensterfunctie lead() , waarbij de ingebouwde optie wordt gebruikt om "vandaag" als standaard op te geven als er geen volgende rij is.
Hiermee vorm ik een daterange en vergelijk het met de invoer met de overlap-operator && , berekent het resulterende datumbereik met de intersectie-operator * .

Alle reeksen hier zijn met exclusief bovenrand. Daarom voeg ik een dag toe aan het invoerbereik. Op deze manier kunnen we eenvoudig lower(range) . aftrekken van upper(range) om het aantal dagen te krijgen.

Ik neem aan dat "gisteren" de laatste dag is met betrouwbare gegevens. "Vandaag" kan nog veranderen in een real-life applicatie. Daarom gebruik ik "vandaag" (now()::date ) als exclusieve bovenrand voor open bereiken.

Ik geef twee resultaten:

  • your_result gaat akkoord met uw weergegeven resultaten.
    U deelt onvoorwaardelijk door het aantal dagen in uw datumbereik. Als een item bijvoorbeeld alleen voor de laatste dag wordt vermeld, krijgt u een zeer laag (misleidend!) "gemiddelde".

  • my_result berekent dezelfde of hogere getallen.
    Ik deel door de werkelijke aantal dagen dat een item wordt vermeld. Als een item bijvoorbeeld alleen voor de laatste dag wordt vermeld, retourneer ik de vermelde waarde als gemiddelde.

Om het verschil te begrijpen, heb ik het aantal dagen toegevoegd dat het item werd vermeld:days_in_range

SQL Fiddle .

Index en prestaties

Voor dit soort gegevens veranderen oude rijen meestal niet. Dit zou een uitstekende zaak zijn voor een gematerialiseerde weergave :

CREATE MATERIALIZED VIEW mv_stock AS
SELECT store_id, product_id, value
     , daterange (day, lead(day, 1, now()::date) OVER (PARTITION BY store_id, product_id
                                                       ORDER BY day)) AS day_range
FROM   stock;

Vervolgens kunt u een GiST-index toevoegen die de relevante operator && :

CREATE INDEX mv_stock_range_idx ON mv_stock USING gist (day_range);

Grote testcase

Ik heb een meer realistische test uitgevoerd met 200k rijen. De query met behulp van de MV was ongeveer 6 keer zo snel, wat op zijn beurt ~ 10x zo snel was als de query van @Joop. Prestaties zijn sterk afhankelijk van gegevensdistributie. Een MV helpt het meest bij grote tafels en een hoge deelnamefrequentie. Als de tabel kolommen heeft die niet relevant zijn voor deze zoekopdracht, kan een MV ook kleiner zijn. Een kwestie van kosten versus winst.

Ik heb alle oplossingen die tot nu toe zijn gepost (en aangepast) in een grote viool gestopt om mee te spelen:

SQL Fiddle met grote testcase.
SQL Fiddle met slechts 40k rijen - om time-out op sqlfiddle.com te voorkomen



  1. Verwijzen naar een associatieve array-waarde van PDO::FETCH_ASSOC

  2. Filter MYSQL-query met formulieropties

  3. Oracle invoegen als rij niet bestaat

  4. Ongeldig parameternummer:parameter is niet gedefinieerd Gegevens invoegen