подсчет значений, последовательных и не сгруппированных таблиц - PullRequest
4 голосов
/ 02 февраля 2011

У меня есть таблица SQL с именем samples, определенная так:

sampledate (datetime, 24 records per day per parameter)
parameterID (int)
value (decimal)
valid (bit, 1=valid data, 0=invalid data)

выборка пары и parameterid уникальны.

каждый отобранный образец имеет формат 02/02/2011 12:00, поэтому для каждого параметра ID в день или менее 24 строки (например, может произойти сбой или работа технического обслуживания, и он выдаст менее 24 выборок ).

Мне нужно рассчитать среднесуточные значения для параметра. Среднее значение действительно для данного дня, только если

  1. присутствует не менее 18 допустимых значений
  2. не более 5 недействительных последовательных значений присутствуют

Условие 1) довольно просто для заданного @parameter:

  SELECT CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, sampledate))) as avgdate,
         AVG(value) as avg, parameterID, 
         isValid = CASE  
           WHEN COUNT(value) > 17 THEN 1
           ELSE 0
         END 
    FROM samples
   WHERE parameterId=@parameter
GROUP BY parameterId, CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, sampledate))), valid
  HAVING valid = 1  
ORDER BY sampledate

Как я могу добавить условие 2, которое сводится к подсчету последовательных 0 в интервале 24 часа, возможно, с лучшими показателями?

у нас есть миллионы сэмплов, а курсоры медленные.

Ответы [ 6 ]

1 голос
/ 09 февраля 2011

А вот и мое рекурсивное решение CTE, и оно настраивается:

WITH
seq_samples AS (
  SELECT
    sampledate, parameterID, value, valid,
    avgdate = CAST(FLOOR(CAST(sampledate AS float)) AS datetime),
    rownum = ROW_NUMBER() OVER (
      PARTITION BY parameterID, CAST(FLOOR(CAST(sampledate AS float)) AS datetime)
      ORDER BY sampledate)
  FROM samples
),
rec_samples AS (
  SELECT
    sampledate, parameterID, value, valid, avgdate, rownum,
    inv_seq_num = 1 - valid
  FROM seq_samples
  WHERE rownum = 1
  UNION ALL
  SELECT
    ss.sampledate, ss.parameterID, ss.value, ss.valid, ss.avgdate, ss.rownum,
    inv_seq_num = CASE ss.valid WHEN 1 THEN 0 ELSE rs.inv_seq_num + 1 END
  FROM seq_samples ss
    INNER JOIN rec_samples rs ON ss.avgdate = rs.avgdate
      AND ss.parameterID = rs.parameterID AND ss.rownum = rs.rownum + 1
)
SELECT
  avgdate,
  parameterID,
  avgvalue = AVG(value)
FROM rec_samples
GROUP BY avgdate, parameterID
HAVING SUM(CAST(valid AS int)) >= @minimal_valid_count
   AND MAX(inv_seq_num)        <= @critical_invalid_count

Ваша идея в основном реализована здесь. Используется дополнительная нумерация, которая применяется только к недопустимым строкам и нарушается только при переходах по дате и появлении допустимых значений. В конце MAX применяется к столбцу нумерации, чтобы узнать, не превышено ли максимальное число @critical_invalid_count. А для другого параметра, очевидно, достаточно проверить сумму атрибутов valid.

Итак, вот и вы.


РЕДАКТИРОВАТЬ для seq_samples CTE (для применения к адаптированной версии исходного запроса).

seq_samples AS (
  SELECT
    *,
    rownum = ROW_NUMBER() OVER (
      PARTITION BY parameterID, avgdate
      ORDER BY sampledate)
  FROM (
    SELECT
      sampledate, parameterID, value, valid,
      avgdate = CAST(FLOOR(CAST(sampledate AS float)) AS datetime)
    FROM samples
  ) s
),

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

1 голос
/ 03 февраля 2011

Это решение, которое использует в основном тот же подход, что и Dems.Я думаю, что логика в моем решении немного отличается.(Или, может быть, он просто по-другому структурирован ...)

WITH sortedsamples AS (
  SELECT
    sampledate,
    parameterID,
    value,
    valid,
    avgdate = CAST(FLOOR(CAST(sampledate AS float)) AS datetime),
    rownum = ROW_NUMBER() OVER (
      PARTITION BY parameterID, CAST(FLOOR(CAST(sampledate AS float)) AS datetime)
      ORDER BY sampledate
    )
  FROM samples
)
SELECT
  ss1.parameterID,
  ss1.avgdate,
  avg = AVG(value),
  isValid = CAST(CASE
    WHEN SUM(CAST(ss1.valid AS int)) < 18 THEN 0
    ELSE MIN(CAST(ss1.valid | ISNULL(ss2.valid, 1) | ISNULL(ss3.valid, 1)
                            | ISNULL(ss4.valid, 1) | ISNULL(ss5.valid, 1) AS int))
  END AS bit)
FROM sortedsamples ss1
  LEFT JOIN sortedsamples ss2 ON ss1.avgdate = ss2.avgdate
    AND ss1.parameterID = ss2.parameterID AND ss1.rownum = ss2.rownum + 1
  LEFT JOIN sortedsamples ss3 ON ss1.avgdate = ss3.avgdate
    AND ss1.parameterID = ss3.parameterID AND ss1.rownum = ss3.rownum + 2
  LEFT JOIN sortedsamples ss4 ON ss1.avgdate = ss4.avgdate
    AND ss1.parameterID = ss4.parameterID AND ss1.rownum = ss4.rownum + 3
  LEFT JOIN sortedsamples ss5 ON ss1.avgdate = ss5.avgdate
    AND ss1.parameterID = ss5.parameterID AND ss1.rownum = ss5.rownum + 4
GROUP BY ss1.parameterID, ss1.avgdate
1 голос
/ 02 февраля 2011

Несколько мыслей и комментариев ...

Есть много способов сделать дату-время только значением даты.Я использую DATEADD (DAY, DATEDIFF (DAY, 0,), 0).Но для следующего кода я предлагаю сделать вид, что есть поле justDate, чтобы сделать вещи короче:)


Последовательность важна, а в таблице нет "идентификатор последовательности ".ROW_NUMBER () может дать вам это ...

ROW_NUMBER() OVER (PARTITION BY parameter_id, justDate ORDER BY sampledate) AS "sequence_id"


Кажется, есть несколько способов сделать это.

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

WITH
  sequenced_samples
AS
(
  SELECT
    parameterID AS "parameter_id",
    sampledate AS "sample_date_time",
    DATEADD(DAY, DATEDIFF(DAY, 0, sampledate), 0) AS "sample_date",
    ROW_NUMBER() OVER (PARTITION BY parameter_id, DATEADD(DAY, DATEDIFF(DAY, 0, sampledate), 0) ORDER BY sampledate) AS "sequence_id",
    CASE WHEN valid = 1 THEN value ELSE NULL END as "value",
    -(valid - 1) AS "invalid" -- turns 1 to 0, and 0 to 1
  FROM
    samples
)

SELECT
  "sample_1".parameter_id,
  "sample_1".sample_date,
  AVG(value) AS average_value
FROM
  samples                "sample_1"
LEFT JOIN
  samples                "sample_2"
    ON  "sample_2".parameter_id = "sample_1".parameter_id
    AND "sample_2".sample_date  = "sample_1".sample_date
    AND "sample_2".sequence_id  = "sample_1".sequence_id + 1
LEFT JOIN
  samples                "sample_3"
    ON  "sample_3".parameter_id = "sample_1".parameter_id
    AND "sample_3".sample_date  = "sample_1".sample_date
    AND "sample_3".sequence_id  = "sample_1".sequence_id + 2
LEFT JOIN
  samples                "sample_4"
    ON  "sample_4".parameter_id = "sample_1".parameter_id
    AND "sample_4".sample_date  = "sample_1".sample_date
    AND "sample_4".sequence_id  = "sample_1".sequence_id + 3
LEFT JOIN
  samples                "sample_5"
    ON  "sample_5".parameter_id = "sample_1".parameter_id
    AND "sample_5".sample_date  = "sample_1".sample_date
    AND "sample_5".sequence_id  = "sample_1".sequence_id + 4
GROUP BY
  "sample_1".parameter_id,
  "sample_1".sample_date
HAVING
  5 > MAX("sample_1".invalid + "sample_2".invalid + "sample_3".invalid + "sample_4".invalid + "sample_5".invalid)
  AND 17 < (COUNT(*) - SUM("sample_1".invalid))


Следующее немного более изящно (но немного), но я несидел где-нибудь с доступом к MS SQL Server, поэтому я не могу сказать, является ли он более производительным.

Вместо 4 объединений, присоединяйтесь только один раз, но сопоставляя 5 последовательных выборок.Выполнение двух уровней группировки.

WITH
  sequenced_samples
AS
(
  SELECT
    parameterID AS "parameter_id",
    sampledate AS "sample_date_time",
    DATEADD(DAY, DATEDIFF(DAY, 0, sampledate), 0) AS "sample_date",
    ROW_NUMBER() OVER (PARTITION BY parameter_id, DATEADD(DAY, DATEDIFF(DAY, 0, sampledate), 0) ORDER BY sampledate) AS "sequence_id",
    CASE WHEN valid = 1 THEN value ELSE NULL END AS "value",
    -(valid - 1) AS "invalid"  -- Turns 0 to 1, and 1 to 0
  FROM
    samples
)
,
  checked_samples
AS
(
SELECT
  "sample".parameter_id,
  "sample".sample_date,
  "sample".value,
  "sample".invalid,
  SUM("next_5_samples".invalid) AS "sequence_invalidity"
FROM
  samples                "sample"
INNER JOIN
  samples                "next_5_samples"
    ON  "next_5_samples".parameter_id  = "sample".parameter_id
    AND "next_5_samples".sample_date   = "sample".sample_date
    AND "next_5_samples".sequence_id  >= "sample".sequence_id + 1
    AND "next_5_samples".sequence_id  <= "sample".sequence_id + 4
GROUP BY
  "sample".parameter_id,
  "sample".sample_date,
  "sample".valid,
  "sample".value
)
SELECT
  parameter_id,
  sample_date,
  AVG(value)
FROM
  checked_samples
GROUP BY
  parameter_id,
  sample_date
HAVING
  5 > MAX(sequence_invalidity)
  AND 17 < (COUNT(*) - SUM(invalid))


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


РЕДАКТИРОВАТЬ: Следующий запрос имеллевое соединение в рекурсивном CTE, и теперь это не так.

WITH
  sequenced_samples
AS
(
  SELECT
    parameterID AS "parameter_id",
    sampledate AS "sample_date_time",
    DATEADD(DAY, DATEDIFF(DAY, 0, sampledate), 0) AS "sample_date",
    ROW_NUMBER() OVER (PARTITION BY parameter_id, DATEADD(DAY, DATEDIFF(DAY, 0, sampledate), 0) ORDER BY sampledate) AS "sequence_id",
    value,
    valid
  FROM
    samples
)
,
  recursed_totals
AS
(
SELECT
  parameter_id,
  sample_date,
  sequence_id - 1                           AS "next_sequence_id",
  CASE WHEN valid = 1 THEN value ELSE 0 END AS "cumulative_value",
  valid                                     AS "cumulative_count",
  CASE WHEN valid = 1 THEN 0     ELSE 1 END AS "cumulative_invalid",
  CASE WHEN valid = 1 THEN 0     ELSE 1 END AS "max_cumulative_invalid"
FROM
  sequenced_samples
WHERE
  sequence_id = (
                 SELECT
                   COUNT(*)
                 FROM
                   sequenced_samples "look_up"
                 WHERE
                       "look_up".parameter_id = sequenced_samples.parameter_id
                   AND "look_up".sample_date  = sequenced_samples.sample_date
                )

UNION ALL

SELECT
  "cumulative_samples".parameter_id,
  "cumulative_samples".sample_date,
  "next_sample".sequence_id - 1,
  "cumulative_samples".cumuatlive_value + CASE WHEN "next_sample".valid = 1 THEN "next_sample".value ELSE 0 END,
  "cumulative_samples".valid + ISNULL("next_sample".valid, 0),
  CASE
    WHEN "next_sample".valid = 0 THEN "cumulative_samples".cumulative_invalid + 1
    WHEN "cumulative_samples".cumulative_invalid = 5 THEN 5
    ELSE 0
  END,
  CASE
    WHEN "next_sample".valid = 1 THEN "cumulative_samples".max_cumulative_invalid
    WHEN "cumulative_samples".cumulative_invalid = "cumulative_samples".max_cumulative_invalid THEN "cumulative_samples".max_cumulative_invalid + 1
    ELSE "cumulative_samples".max_cumulative_invalid
  END
FROM
  recursed_totals   AS "cumulative_samples"
INNER JOIN
  sequenced_samples AS "next_sample"
    ON  "next_sample".parameter_id = "cumulative_samples".parameter_id
    AND "next_sample".sample_date  = "cumulative_samples".sample_date
    AND "next_sample".sequence_id  = "cumulative_samples".next_sequence_id
)
SELECT
  parameter_id,
  sample_date,
  CAST(cumulative_value AS FLOAT) / CAST(cumulative_count AS FLOAT) AS "average",
  cumulative_count AS "valid_samples",
  max_cumulative_invalid AS "max_consecutive_invalid_samples"
FROM
  recursed_totals
WHERE
  parameter_id = @parameter_id
0 голосов
/ 14 февраля 2011

Вот еще один ответ, использующий CROSS APPLY и избегающий использования ROW_NUMBER ().

Если, однако, в одно и то же время происходит более 5 выборок для одного и того же параметра параметра, это не будет работать правильно. Если это так, вам снова нужно ROW_NUMBER ().

SELECT
  parameterID                                              AS "parameter_id",
  DATEADD(DAY, DATEDIFF(DAY, 0, sampledate), 0)            AS "sample_date",
  SUM(value)                                               AS "total",
  SUM(CASE WHEN valid = 1 THEN value ELSE 0 END)           AS "total_valid",
  COUNT(*)                                                 AS "count",
  SUM(valid)                                               AS "count_valid",
  MAX(invalid)                                             AS "date_invalidated"
FROM
  samples
CROSS APPLY
(
  SELECT
    CASE WHEN SUM(valid) = 0 THEN 1 ELSE 0 END AS "invalid"
  FROM
  (
    SELECT TOP 5
      valid
    FROM
      samples AS "5_samples"
    WHERE
          "5_samples".parameterID  = "samples".parameterID
      AND "5_samples".sampledate  >= "samples".sampledate
      AND "5_samples".sampledate  <  DATEADD(DAY, DATEDIFF(DAY, 0, "samples".sampledate), 1)
    ORDER BY
      sampledate
  )
    AS "data"
)
  AS "check"
WHERE
  parameterID = @parameterID
GROUP BY
  parameter_id,
  DATEADD(DAY, DATEDIFF(DAY, 0, sampledate), 0)
0 голосов
/ 09 февраля 2011

ваши решения довольно интересны (и я многому у них научился), но мне интересно, можно ли их улучшить.

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

Мне было интересно, можно ли найти решение, исходя из решений rownum (): если бы я мог найти способ сброситьсчитать каждый разрыв в допустимом столбце, я мог бы просто сделать недействительным день, когда я нахожу строку, имеющую rownum> N и valid = 0, что было бы супер просто, быстро и универсально.

Я пытаюсь объяснить этоИдея получше:

скажем, я мог бы использовать rownum или аналогичную функцию для получения этого:

date                       par    value        valid     rownum

2010-01-26 00:00:00.000 25  14.0000000000       1      1
2010-01-26 01:00:00.000 25  15.3000001907       1      2
2010-01-26 02:00:00.000 25  16.8999996185       1      3
2010-01-26 03:00:00.000 25  13.6000003815       1      4
2010-01-26 04:00:00.000 25  16.2000007629       1      5
2010-01-26 05:00:00.000 25  12.1999998093      -1      1
2010-01-26 06:00:00.000 25  17.2000007629      -1      2
2010-01-26 07:00:00.000 25  16.2999992371       1      1
2010-01-26 08:00:00.000 25  18.2999992371       1      2
2010-01-26 09:00:00.000 25  15.0000000000       1      3
2010-01-26 10:00:00.000 25  17.7000007629       1      4
2010-01-26 11:00:00.000 25  16.5000000000       1      5
2010-01-26 12:00:00.000 25  17.3999996185       1      6
2010-01-26 13:00:00.000 25  17.7000007629       1      7
2010-01-26 14:00:00.000 25  18.2999992371       1      8
2010-01-26 15:00:00.000 25  15.1000003815      -1      1
2010-01-26 16:00:00.000 25  16.5000000000      -1      2
2010-01-26 17:00:00.000 25  10.3999996185      -1      3
2010-01-26 18:00:00.000 25  10.8999996185      -1      4
2010-01-26 19:00:00.000 25  10.1000003815      -1      5  <-----!!!!  
2010-01-26 20:00:00.000 25  13.6999998093       1      1
2010-01-26 21:00:00.000 25  12.6999998093       1      2
2010-01-26 22:00:00.000 25  15.3999996185      -1      1
2010-01-26 23:00:00.000 25  8.6000003815       -1      2

, если N = 5, существование строки

2010-01-26 19:00:00.000 25  10.1000003815      -1      5  

означало бы, что весь день недействителен (не говоря уже об общем количестве неверных данных)

Что вы думаете об этой идее?

(я не знаю, должно ли это иметьбыл правкой или внятным ответом)

0 голосов
/ 02 февраля 2011

Какой из ваших миллионов образцов имеет менее 5 неправильных значений в день?Если это достаточно высокий процент, вы в порядке, потому что вы можете легко исключить их из обработки курсора.

Если количество выборок с 5 или более недопустимыми значениями в день все еще исчисляется миллионами, выв течение длительного ожидания.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...