Экстраполировать ежедневные исторические значения из таблицы, которая записывает только при изменении значения (Postgresql 9.3) - PullRequest
10 голосов
/ 03 июля 2019

У меня есть таблица, в которой записывается строка для каждого изменения оценки местоположения.

счет_истории:

  • id int PK (uuid с автоматическим приращением int)
  • произошло_временная отметка (когда счет изменился)
  • location_id int FK (местоположение, для которого значение)
  • счет с плавающей точкой (новый счет)

Это было сделано с учетом эффективности и возможности просто получить список изменений для заданного местоположения и прекрасно подходит для этой цели.

Я пытаюсь вывести данные в очень избыточном формате дляпомочь загрузить его в жесткую внешнюю систему.Внешняя система ожидает строку для каждого местоположения * каждую дату.Цель состоит в том, чтобы представить последнее значение оценки для каждого местоположения для каждой даты.Таким образом, если оценка изменялась 3 раза за определенную дату, только оценка, ближайшая к полуночи, будет считаться закрывающей счетом за день.Я полагаю, что это похоже на задачу создания таблицы фактов уровня инвентаризации бизнеса.

У меня есть удобная таблица измерений даты в виде звездообразной схемы, в которой есть строка для каждой даты, полностью охватывающая этот примерный период, ибудущее.

Эта таблица выглядит как

dw_dim_date:

  • дата дата PK
  • куча других столбцов, таких как номер недели, is_us_holiday и т. д..

Итак, если бы у меня было только 3 записи в таблице Score_history ...

1, 2019-01-01:10:13:01, 100, 5.0
2, 2019-01-05:20:00:01, 100, 5.8
3, 2019-01-05:23:01:22, 100, 6.2

Желаемый результат был бы:

2019-01-01, 100, 5.0 
2019-01-02, 100, 5.0 
2019-01-03, 100, 5.0
2019-01-04, 100, 5.0 
2019-01-05, 100, 6.2

3Требования:

  1. Одна строка в день для каждого местоположения, даже если для этого дня нет записей о результатах.
  2. Если есть записи о результатах за этот день, последняя до полуночи должна бытьоценка значения для строки.В случае ничьей старший из них должен «выиграть».
  3. Если в этот день есть записи с нулевым счетом, счет должен быть самым последним предыдущим счетом.

Я гонялся за своим хвостом через подзапросы и оконные функции.

Поскольку я не решаюсь публиковать что-то без чего-либо, я попытался поделиться этим trainwreck, который производит вывод, но не имеет смысла ...

SELECT dw_dim_date.date,
       (SELECT score 
        FROM score_history 
        WHERE score_history.happened_at::DATE < dw_dim_date.date 
           OR score_history.happened_at::DATE = dw_dim_date.date 
        ORDER BY score_history.id desc limit 1) as last_score
FROM dw_dim_date
WHERE dw_dim_date.date > '2019-06-01'

Благодарен за руководство или ссылки на другие вопросы для чтения.

Ответы [ 4 ]

5 голосов
/ 06 июля 2019

Этого можно добиться с помощью коррелированных подзапросов и LATERAL:

SELECT sub.date, sub.location_id, score
FROM (SELECT * FROM dw_dim_date
      CROSS JOIN (SELECT DISTINCT location_id FROM score_history) s
      WHERE date >= '2019-01-01'::date) sub
,LATERAL(SELECT score FROM score_history sc 
         WHERE sc.happened_at::date <= sub.date
           AND sc.location_id = sub.location_id
         ORDER BY happened_at DESC LIMIT 1) l
,LATERAL(SELECT MIN(happened_at::date) m1, MAX(happened_at::date) m2 
         FROM score_history sc
         WHERE sc.location_id = sub.location_id) lm
WHERE sub.date BETWEEN lm.m1 AND lm.m2
ORDER BY location_id, date;

db <> fiddle demo

Howэто работает:

1) s (это перекрестное объединение всех дат для location_id)

2) l (выбор оценки для местоположения)

3)lm (выбор минимальной / максимальной даты для каждого местоположения для фильтрации)

4) WHERE фильтрация дат по доступному диапазону, при необходимости ее можно ослабить

2 голосов
/ 06 июля 2019

Я думаю, вы можете попробовать что-то вроде этого.Основными вещами, которые я изменил, является упаковка в DATE () и использование другого SO-ответа для поиска даты:

SELECT
  dw_dim_date.date,
  (
    SELECT
      score
    FROM
      score_history
    WHERE
      DATE(score_history.happened_at) <= dw_dim_date.date
    ORDER BY
      score_history.happened_at DESC
    LIMIT
      1
  ) as last_score
FROM
  dw_dim_date
WHERE
  dw_dim_date.date >= DATE('2019-01-01')

Здесь используется метод SQL, чтобы найти ближайшие данные за запрошенным: PostgreSQL возвращает точную или ближайшую дату к запрашиваемой дате

0 голосов
/ 08 июля 2019

Самое простое решение, вероятно, будет:

    select dw_dim_date.date, location_id, score
    from dw_dim_date, score_history S1
    where happened_at::date  <= dw_dim_date.date and 
          not exists (select * 
                      from score_history S2 
                      where S2.happened_at::date  <= dw_dim_date.date and 
                            S1.happened_at< S2.happened_at and
                            S1.location_id = S2.location_id)

При этом вычисляется декартово произведение между датами и историями оценок, а затем для каждой даты и местоположения берется оценка, для которой не существует более поздней оценки (в пределах периода дат). Я бы предложил начать с этого, поскольку, вероятно, его будет проще всего обслуживать, и использовать более сложное решение, только если оно недостаточно эффективно (с соответствующими индексами).

Поскольку SQL Fiddle на это в https://dbfiddle.uk/?rdbms=postgres_9.4&fiddle=3c2e4ae49cbc43f7840b942d223be119

0 голосов
/ 06 июля 2019
WITH
max_per_day_location AS (
SELECT
    SH.happened_at::DATE as day,
    SH.location_id,
    max(SH.happened_at) as happened_at
FROM
    score_history SH
GROUP BY
    SH.happened_at::DATE,
    SH.location_id
),
date_location AS (
SELECT DISTINCT
    DD."date",
    SH.location_id
FROM
    dw_dim_date DD,
    max_per_day_location SH
),
value_partition AS (
SELECT
    DD."date",
    DD.location_id,
    SH.score,
    SH.happened_at,
    MPD.happened_at as hap2,
    sum(case when score is null then 0 else 1 end) OVER
    (PARTITION BY DD.location_id ORDER BY "date", SH.happened_at desc) AS value_partition
FROM
    date_location DD
    LEFT JOIN score_history SH
    ON DD."date" = SH.happened_at::DATE
    AND DD.location_id = SH.location_id
    LEFT join max_per_day_location MPD
    ON SH.happened_at = MPD.happened_at
WHERE NOT (MPD.happened_at IS NULL
           AND
           SH.happened_at IS NOT NULL)
ORDER BY
    DD."date"
),
final AS (
SELECT
    "date",
    location_id,
    first_value(score) over w
FROM
    value_partition
WINDOW w AS (PARTITION BY location_id, value_partition
             ORDER BY happened_at rows between unbounded preceding and unbounded following)
order by "date"
)
SELECT DISTINCT * FROM final ORDER BY location_id, date
;

Я уверен, что есть менее подробные способы сделать это.

У меня есть SQLFiddle с некоторыми тестовыми данными здесь: http://sqlfiddle.com/#!17/9d122/1

Главное, чтоделает эту работу делает «раздел значения» для доступа к предыдущему ненулевому значению.Подробнее здесь:

Подзапрос date_location просто создает по одной строке на location_id в день, так как это базовый "уровень строки", требуемый в выходных данных.

Подзапрос max_per_day_location используется для фильтрацииболее ранние записи для комбинаций местоположения / день, которые имеют несколько баллов и сохраняют только последнюю для этого дня.

...