web-dev-qa-db-ja.com

各グループの最初の行と最後の行のフィールド値の差を計算する方法

私はこのような構造のテーブルがあります:

+-------+------------------+
| Value |       Date       |
+-------+------------------+
|    10 | 10/10/2010 10:00 |
|    11 | 10/10/2010 10:15 |
|    15 | 10/10/2010 10:30 |
|    15 | 10/10/2010 10:45 |
|    17 | 10/10/2010 11:00 |
|    18 | 10/10/2010 11:15 |
|    22 | 10/10/2010 11:30 |
|    30 | 10/10/2010 11:45 |
+-------+------------------+

現在、私はgroup byを使用して、min、max、avgを取得し、次のような1時間ごとのレポートを取得しています。

+-----+-----+-------+------------------+
| min | max |  avg  |       Date       |
+-----+-----+-------+------------------+
|  10 |  15 | 12.75 | 10/10/2010 10:00 |
|  17 |  30 | 21.75 | 10/10/2010 11:00 |
+-----+-----+-------+------------------+

各グループの最後の行と最初の行の値の差を計算して、次のようなものを生成する方法:

+-----+-----+-------+------+------------------+
| min | max |  avg  | diff |       Date       |
+-----+-----+-------+------+------------------+
|  10 |  15 | 12.75 |    5 | 10/10/2010 10:00 |
|  17 |  30 | 21.75 |   13 | 10/10/2010 11:00 |
+-----+-----+-------+------+------------------+

ありがとう。

4
r.zarei

diffなしで結果を取得するために使用しているクエリを表示していません。私はそれがこのようなものであることを想定しています:

_SELECT
  min  = MIN(Value),
  max  = MAX(Value),
  avg  = AVG(Value),  -- or, if Value is an int, like this, perhaps:
                      -- AVG(CAST(Value AS decimal(10,2))
  Date = DATEADD(HOUR, DATEDIFF(HOUR, 0, Date), 0)
FROM atable
GROUP BY
  DATEADD(HOUR, DATEDIFF(HOUR, 0, Date), 0)
;
_

また、firstおよびlastの意味を説明していません。この回答では、firstグループの最初Date値による)を表し、同様にlastは- グループの最新

diffをスローする1つの方法は次のようになります。

最初に、2つの集計列、minDatemaxDateを元のクエリに追加します。

_SELECT
  min     = MIN(Value),
  max     = MAX(Value),
  avg     = AVG(Value),
  minDate = MIN(Date),
  maxDate = MAX(Date),
  Date    = DATEADD(HOUR, DATEDIFF(HOUR, 0, Date), 0)
FROM atable
GROUP BY
  DATEADD(HOUR, DATEDIFF(HOUR, 0, Date), 0)
;
_

次に、minDatemaxDateで(個別に)元のテーブルに集計結果セットを結合し、対応するValuesにアクセスします。

_SELECT
  g.min,
  g.max,
  g.avg,
  diff = last.Value - first.Value,
  g.Date
FROM (
  SELECT
    min     = MIN(Value),
    max     = MAX(Value),
    avg     = AVG(Value),
    minDate = MIN(Date),
    maxDate = MAX(Date),
    Date    = DATEADD(HOUR, DATEDIFF(HOUR, 0, Date), 0)
  FROM atable
  GROUP BY
    DATEADD(HOUR, DATEDIFF(HOUR, 0, Date), 0)
) g
INNER JOIN atable first ON first.Date = g.minDate
INNER JOIN atable last  ON last .Date = g.maxDate
;
_

上記では、Date値(少なくとも対応する時間の最初または最後の値)が一意であると想定しています。そうしないと、出力の一部の時間に複数の行が表示されます。

SQL Server 2005以降のバージョンを使用している場合は、ウィンドウ集計関数MIN() OVER (...)およびMAX() OVER (...)を使用して、ValueまたはminDateのいずれかに対応するmaxDatesを計算してから、あなたがおそらく今それをしている方法と同様の結果。具体的に私が話しているのは次のとおりです。

_WITH partitioned AS (
  SELECT
    Value,
    Date,
    GroupDate = DATEADD(HOUR, DATEDIFF(HOUR, 0, Date), 0)
  FROM atable
)
, firstlast AS (
  SELECT
    Value,
    Date,
    GroupDate,
    FirstValue = CASE Date WHEN MIN(Date) OVER (PARTITION BY GroupDate) THEN Value END,
    LastValue  = CASE Date WHEN MAX(Date) OVER (PARTITION BY GroupDate) THEN Value END
  FROM partitioned
)
SELECT
  min  = MIN(Value),
  max  = MAX(Value),
  avg  = AVG(Value),  -- or, again, if Value is an int, cast it as a decimal or float
  diff = MAX(LastValue) - MIN(FirstValue),
  Date = GroupDate
FROM firstlast
GROUP BY
  GroupDate
;
_

ご覧のとおり、最初の 共通テーブル式(CTE) は、すべての行を返し、計算列GroupDateを追加するだけです。この列は、後でグループ化/パーティション化に使用されます。したがって、基本的にはグループ化式に名前を割り当てるだけです。これは、列が後で複数回参照されるため、クエリ全体の読みやすさ/保守性を向上させるために行われます。これは、最初のCTEが生成するものです。

_+-------+------------------+------------------+
| Value |       Date       |    GroupDate     |
+-------+------------------+------------------+
|    10 | 10/10/2010 10:00 | 10/10/2010 10:00 |
|    11 | 10/10/2010 10:15 | 10/10/2010 10:00 |
|    15 | 10/10/2010 10:30 | 10/10/2010 10:00 |
|    15 | 10/10/2010 10:45 | 10/10/2010 10:00 |
|    17 | 10/10/2010 11:00 | 10/10/2010 11:00 |
|    18 | 10/10/2010 11:15 | 10/10/2010 11:00 |
|    22 | 10/10/2010 11:30 | 10/10/2010 11:00 |
|    30 | 10/10/2010 11:45 | 10/10/2010 11:00 |
+-------+------------------+------------------+
_

2番目のCTEは、上記の結果にさらに2つの列を追加します。 window 集計関数MIN() OVER ...MAX() OVER ...を使用してDateと照合し、一致した場合、対応するValueが別の列に返されます。 FirstValueまたはLastValue

_+-------+------------------+------------------+------------+-----------+
| Value |       Date       |    GroupDate     | FirstValue | LastValue |
+-------+------------------+------------------+------------+-----------+
|    10 | 10/10/2010 10:00 | 10/10/2010 10:00 |         10 |      NULL |
|    11 | 10/10/2010 10:15 | 10/10/2010 10:00 |       NULL |      NULL |
|    15 | 10/10/2010 10:30 | 10/10/2010 10:00 |       NULL |      NULL |
|    15 | 10/10/2010 10:45 | 10/10/2010 10:00 |       NULL |        15 |
|    17 | 10/10/2010 11:00 | 10/10/2010 11:00 |         17 |      NULL |
|    18 | 10/10/2010 11:15 | 10/10/2010 11:00 |       NULL |      NULL |
|    22 | 10/10/2010 11:30 | 10/10/2010 11:00 |       NULL |      NULL |
|    30 | 10/10/2010 11:45 | 10/10/2010 11:00 |       NULL |        30 |
+-------+------------------+------------------+------------+-----------+
_

この時点で、すべてが最終的な集計の準備ができています。 minmax、およびavg列は以前と同じように集計され、集計されたdiffから集計されたFirstValueを減算することで、LastValueを簡単に取得できるようになりました。上記の結果セットからわかるように、さまざまな関数を使用して、グループのFirstValueLastValueを取得できます。これは、MINMAXSUMAVGのようになります。これらすべてのグループで値が1つしかないためです。

ただし、メインのSELECTは、ご覧のとおり、LastValueではなくMAX()を、FirstValueではMIN()をそれぞれ適用します。それは意図的なものです。この2番目の提案では、最初の提案とは異なり、Dateが一意である必要は実際にはないbutminDateまたはmaxDateのいずれかに複数のValueが関連付けられている場合は、 FirstValueまたはLastValueには、グループごとに複数の値が含まれ、次のようになります。

_+-------+------------------+------------------+------------+-----------+
| Value |       Date       |    GroupDate     | FirstValue | LastValue |
+-------+------------------+------------------+------------+-----------+
|     9 | 10/10/2010 10:00 | 10/10/2010 10:00 |          9 |      NULL |
|    10 | 10/10/2010 10:00 | 10/10/2010 10:00 |         10 |      NULL |
|    11 | 10/10/2010 10:15 | 10/10/2010 10:00 |       NULL |      NULL |
|    15 | 10/10/2010 10:30 | 10/10/2010 10:00 |       NULL |      NULL |
|    15 | 10/10/2010 10:45 | 10/10/2010 10:00 |       NULL |        15 |
|    17 | 10/10/2010 11:00 | 10/10/2010 11:00 |         17 |      NULL |
|    18 | 10/10/2010 11:15 | 10/10/2010 11:00 |       NULL |      NULL |
|    22 | 10/10/2010 11:30 | 10/10/2010 11:00 |       NULL |      NULL |
|    30 | 10/10/2010 11:45 | 10/10/2010 11:00 |       NULL |        30 |
|    33 | 10/10/2010 11:45 | 10/10/2010 11:00 |       NULL |        33 |
+-------+------------------+------------------+------------+-----------+
_

この状況では、最大の最後の値と最小の最初の値の違いを取るほうが自然だと思いました。ただし、ここで適用するルールをよく理解している必要があるため、それに応じてクエリを変更するだけです。

SQL Fiddleで両方のソリューションをテストできます。


更新

SQL Server 2012以降、 FIRST_VALUE および LAST_VALUE 関数を使用して、上記の最後のクエリでfirstlast CTEのCASE式を次のように置き換えることもできます。

_FirstValue = FIRST_VALUE(Value) OVER (PARTITION BY GroupDate ORDER BY Date ASC
                                      ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING),
LastValue  = LAST_VALUE(Value)  OVER (PARTITION BY GroupDate ORDER BY Date ASC
                                      ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
_

この場合、(メインSELECTで)FirstValueおよびLastValueに対してMINまたはMAXを使用するかどうかは関係ありません。各__columnは、同じValueグループのすべての行でまったく同じ値(最初または最後のGroupDate)になります。したがって、MIN()MAX()は、どちらの場合も同じ結果を返します。

実際、difffirstlast CTEで直接取得し、メインクエリで、MIN/MAXを使用して集計するか、GROUP BYに追加して、次のように集計せずに参照できます。

_WITH partitioned AS (
  SELECT
    Value,
    Date,
    GroupDate = DATEADD(HOUR, DATEDIFF(HOUR, 0, Date), 0)
  FROM atable
)
, firstlast AS (
  SELECT
    Value,
    Date,
    GroupDate,
    diff = LAST_VALUE(Value)  OVER (PARTITION BY GroupDate ORDER BY Date ASC
                                    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
         - FIRST_VALUE(Value) OVER (PARTITION BY GroupDate ORDER BY Date ASC
                                    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
  FROM partitioned
)
SELECT
  min  = MIN(Value),
  max  = MAX(Value),
  avg  = AVG(Value),
  diff,
  Date = GroupDate
FROM firstlast
GROUP BY
  GroupDate,
  diff
;
_

さらに一歩進めると、メインのクエリの代わりに、対応するウィンドウ関数を使用して、minmaxavgfirstlastを取得することもできます。

_min  = MIN(Value) OVER (PARTITION BY GroupDate),
max  = MAX(Value) OVER (PARTITION BY GroupDate),
avg  = AVG(Value) OVER (PARTITION BY GroupDate),
_

これらの3つの追加の列と以前の変更により、firstlast CTEは、例のように次のような行セットを返します。

_+-------+------------------+------------------+-----+-----+-------+------+
| Value |       Date       |    GroupDate     | min | max |  avg  | diff |
+-------+------------------+------------------+-----+-----+-------+------+
|    10 | 10/10/2010 10:00 | 10/10/2010 10:00 |  10 |  15 | 12.75 |    5 |
|    11 | 10/10/2010 10:15 | 10/10/2010 10:00 |  10 |  15 | 12.75 |    5 |
|    15 | 10/10/2010 10:30 | 10/10/2010 10:00 |  10 |  15 | 12.75 |    5 |
|    15 | 10/10/2010 10:45 | 10/10/2010 10:00 |  10 |  15 | 12.75 |    5 |
|    17 | 10/10/2010 11:00 | 10/10/2010 11:00 |  17 |  30 | 21.75 |   13 |
|    18 | 10/10/2010 11:15 | 10/10/2010 11:00 |  17 |  30 | 21.75 |   13 |
|    22 | 10/10/2010 11:30 | 10/10/2010 11:00 |  17 |  30 | 21.75 |   13 |
|    30 | 10/10/2010 11:45 | 10/10/2010 11:00 |  17 |  30 | 21.75 |   13 |
+-------+------------------+------------------+-----+-----+-------+------+
_

GroupDateminmaxavg、およびdiff(最終セットに実際に必要な列)が、同じグループに属するすべての行にわたって単純に繰り返されていることに注意してください。つまり、ValueDateを削除し、GroupDateDateに名前変更して、列を少し並べ替え、結果のセットにDISTINCTを適用すると、最後のSELECTを削除できます。

_WITH partitioned AS (
  SELECT
    Value,
    Date,
    GroupDate = DATEADD(HOUR, DATEDIFF(HOUR, 0, Date), 0)
  FROM
    atable
)
SELECT DISTINCT
  min  = MIN(Value) OVER (PARTITION BY GroupDate),
  max  = MAX(Value) OVER (PARTITION BY GroupDate),
  avg  = AVG(Value) OVER (PARTITION BY GroupDate),
  diff = LAST_VALUE(Value)  OVER (PARTITION BY GroupDate ORDER BY Date ASC
                                  ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
       - FIRST_VALUE(Value) OVER (PARTITION BY GroupDate ORDER BY Date ASC
                                  ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING),
  Date = GroupDate
FROM
  partitioned
;
_

最後に、GroupDateの計算を、minmaxなどが計算されるのと同じスコープに移動することもできます。そのためにCROSS APPLYを使用して、クエリを完全にネストする必要をなくすことができます。つまり、このようにして、partitioned CTEを取り除くこともできます。 entireクエリは次のようになります。

_SELECT DISTINCT
  min  = MIN(t.Value) OVER (PARTITION BY x.GroupDate),
  max  = MAX(t.Value) OVER (PARTITION BY x.GroupDate),
  avg  = AVG(t.Value) OVER (PARTITION BY x.GroupDate),
  diff = LAST_VALUE(t.Value)  OVER (PARTITION BY x.GroupDate ORDER BY t.Date ASC
                                    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
       - FIRST_VALUE(t.Value) OVER (PARTITION BY x.GroupDate ORDER BY t.Date ASC
                                    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING),
  Date = x.GroupDate
FROM
  atable AS t
  CROSS APPLY (SELECT DATEADD(HOUR, DATEDIFF(HOUR, 0, Date), 0)) AS x (GroupDate)
;
_

同じ結果を返します。あなたはそれをテストすることができます SQL Fiddle も。

12
Andriy M

現在実行していることを行い、列を追加します

max(value)-min(value) as diff
0
podiluska