各イベントの結果が成功または失敗であるイベントの時系列を前提として、エンティティと期間の列によるイベントの比率を、集計配列のセル値でピボットするにはどうすればよいですか?これはcrosstab
クエリとarray_agg
で実行できると思います。
稼働時間ステータスレポートと同様に、SQLで次のようなものを計算しようとしています。
データ量は十分に少ないので、汎用言語でクライアント側で削減を実行できますが、データ量が多い場合は、データベースでこれを効率的に実行すると便利です。
+-----------------------------------------+
| DateTime Entity Result |
+-----------------------------------------+
| 2016-01-01 11:00... :a :success |
| 2016-01-01 17:00... :a :success | -- two events for :a on same day
| 2016-01-01 11:01... :b :fail |
| 2016-01-01 11:03... :c :success |
| 2016-01-01 13:00... :d :success | -- only one event for :d
| 2016-01-02 11:00... :a :success |
| 2016-01-02 11:01... :b :fail |
| 2016-01-02 11:03... :c :success |
| ... |
+-----------------------------------------+
キー列の後の各値セルは、形状[cnt_total cnt_success cnt_fail]
の配列です。
+-----------------------------------------------+
| Entity 2016-01-01 2016-01-02 2016-01-xx |
+-----------------------------------------------+
| :a [2 2 0] [1 1 0] ... |
| :b [1 0 1] [1 0 1] ... |
| :c [1 1 0] [1 0 1] ... |
| :d [1 1 0] [0 0 0] ... |
+-----------------------------------------------+
簡単にするために、このレポートには10を超える日付ウィンドウ列は必要ありません。SQLピボット出力を動的にテンプレート化できます。
この変換を分解する必要がある場合:
[count_total count_success count_fail]
の配列などのデータ構造に蓄積します[entity period1 period2 ...]
として返し、クライアントに%を表示します。この質問は古いですが、まだ回答を受け入れていません。別の質問を追加します。
データの集計とピボットテーブルが必要です。前者を行う最もエレガントな方法はCTEを使用することであり、後者を行う最もエレガントな方法はCROSSTAB
を使用することです。ただし、Postgres 9.6以降では、他のDBMSとは異なり、CROSSTAB
からCTEを参照することはできません。 2つの可能な方法のそれぞれの例を示します。1)CTEを使用して、貧しい人のピボットを自分で再実装します。2)CTEの代わりに、すべてに対してビューを1回作成し、CROSSTAB
クエリでそれを参照します。どちらの場合も、レポートごとに1つのクエリを発行するだけでよく、一時テーブルを作成する必要はありません。
ピボットの一般的な問題は、純粋なSQLでは、結果の列数が可変であるクエリを定義できず、列見出しを動的に定義できないことです。それが必要な場合は、サーバー側(plpgsql
、Abelistoの回答のように)またはクライアント側(PHP
、Java
など)のいずれかの手続き型言語でクエリを作成する必要があります。以下の私の例は純粋なSQLであるので、固定された日数(例のデータのように3日)、固定の列見出し("day 1", "day 2", "day 3"
)がありますが、変更すると必要な編集を最小限に抑える方法で構築されます。
まず、初期データ。私はjoanoloを使用したものから始めましたが、SMALLINT
ではなくBOOLEAN
の代わりにresult
を使用しているため、私のアプローチは異なります。これを行う理由は以下で明らかになります。
CREATE TABLE time_series (
date_time TIMESTAMP NOT NULL,
entity TEXT NOT NULL,
result SMALLINT DEFAULT 0 -- 1 means success, 0 failure.
);
INSERT INTO time_series VALUES
('2016-01-01 11:00', 'a', 1),
('2016-01-01 17:00', 'a', 1),
('2016-01-01 11:01', 'b', 0),
('2016-01-01 11:03', 'c', 1),
('2016-01-01 13:00', 'd', 1),
('2016-01-02 11:00', 'a', 1),
('2016-01-02 11:01', 'b', 0),
('2016-01-03 11:03', 'e', 1),
('2016-01-03 11:04', 'e', 1),
('2016-01-03 11:05', 'e', 1),
('2016-01-03 11:06', 'e', 0);
本当に必要なのは、2つの整数(私の例ではa
)の配列、a[1]
(合計数)とa[2]
(成功数)だけです。失敗数は単にa[1] - a[2]
で、成功率は100*(a[2]::float)/a[1]
です。合計数はCOUNT(result)
で計算できます。 result SMALLINT
を定義する場合は、SUM(result)
を使用して成功数を追跡できます。 result
をBOOLEAN
として保存する場合は、SUM(CASE WHEN result THEN 1 ELSE 0 END)
を使用する必要があります。それらを文字列として保存する場合は、SUM(CASE WHEN result = 'success' THEN 1 ELSE 0 END)
。 time_series
テーブルを変更できない場合は、以下のコードを適切に編集してください。
ソリューション1
これは見苦しい広告ですが、CTEの使用方法とCROSSTAB
が登場する前に苦労しなければならなかった苦痛を示すために示す価値はあります。間隔を変更する場合は、初日、最終日、メイン選択リストの行、結合テーブルのリストの行の4か所で変更する必要があります。ただし、数値列rn
を使用すると、結合されたテーブルに明示的な日付を書き込まないため、タスクが簡略化されます。
WITH ct AS (
SELECT EXTRACT('days' FROM day - MIN(day) OVER()) + 1 AS rn, sub.*
FROM (
SELECT
entity,
DATE_TRUNC('day', date_time) AS day,
ARRAY[COUNT(result), SUM(result)] AS a
FROM time_series
WHERE date_time BETWEEN TIMESTAMP '2016-01-01' -- initial day
AND TIMESTAMP '2016-01-03 23:59:59' -- last second of final day
GROUP BY 1,2
) AS sub
)
SELECT e.entity
, d1.a AS "day 1" -- add as many as you need
, d2.a AS "day 2"
, d3.a AS "day 3"
FROM (SELECT DISTINCT entity FROM ct) e
LEFT JOIN (SELECT entity, a FROM ct WHERE rn = 1) d1 USING(entity) -- add as many as you need
LEFT JOIN (SELECT entity, a FROM ct WHERE rn = 2) d2 USING(entity)
LEFT JOIN (SELECT entity, a FROM ct WHERE rn = 3) d3 USING(entity)
ORDER BY e.entity;
entity | day 1 | day 2 | day 3
--------+-------+-------+-------
a | {2,2} | {1,1} |
b | {1,0} | {1,0} |
c | {1,1} | |
d | {1,1} | |
e | | | {4,3}
ソリューション2
ビューのコードは基本的に前の例のCTEコードですが、CROSSTAB
はタイムスタンプ値でGENERATE_SERIES
を使用できるため、データを分類するために数値のrn
列を必要としないため、より単純です。このビューは、作成後は変更する必要がないことに注意してください。
CREATE VIEW ts_view AS
SELECT
entity,
DATE_TRUNC('day', date_time) AS day,
ARRAY[COUNT(result), SUM(result)] AS a
FROM time_series
GROUP BY 1,2;
これがメインのクエリです。間隔を変更するときは、threeの場所で変更する必要があります:初日、最終日、出力列。フォーマットはクライアント側で行うのが最適ですが、この場合はサーバー側で行っています。これを変更する方法の説明はコメントにあります
SELECT * FROM CROSSTAB ($$
SELECT
entity,
day,
-- You have to repeat the result type of the following expression
-- as the type of the "day N" columns below.
-- e.g. a --> INTEGER[] , 100*(a[2]::FLOAT)/a[1] --> FLOAT , etc.
-- In TO_CHAR, D is changed to your locale's decimal point
TO_CHAR(100*(a[2]::float)/a[1], '990D99')||'%'
FROM ts_view ORDER BY 1
$$,$$
SELECT GENERATE_SERIES (
TIMESTAMP '2016-01-01', -- initial day
TIMESTAMP '2016-01-03', -- final day
'1 day'
)
$$
) AS (
entity TEXT
, "day 1" TEXT -- add as many as you need
, "day 2" TEXT
, "day 3" TEXT
);
entity | day 1 | day 2 | day 3
--------+----------+----------+----------
a | 100.00% | 100.00% |
b | 0.00% | 0.00% |
c | 100.00% | |
d | 100.00% | |
e | | | 75.00%
最後に、CROSSTAB
の2番目の引数は次のようになります。
$$
WITH n(ow) AS (VALUES(DATE_TRUNC('day', NOW())))
SELECT GENERATE_SERIES(n.ow + '-2 days', n.ow, '1 day') FROM n
$$
また、クエリは常に過去3日間(今日を含む)の動的レポートを返します。
リクエストのわずかなバリエーションで、同等のレスポンス(と思います).
まず、これが開始データであると仮定しましょう。
_CREATE TABLE t
(
date_time timestamp NOT NULL,
entity text NOT NULL,
result boolean NOT NULL, /* true means 'success', false 'fail' */
PRIMARY KEY(date_time, entity, result)
) ;
INSERT INTO
t
(date_time, entity, result)
VALUES
('2016-01-01 11:00', 'a', true),
('2016-01-01 17:00', 'a', true), -- two events for a on same day
('2016-01-01 11:01', 'b', false),
('2016-01-01 11:03', 'c', true),
('2016-01-01 13:00', 'd', true), -- only one event for d
('2016-01-02 11:00', 'a', true),
('2016-01-02 11:01', 'b', false),
('2016-01-03 11:03', 'e', true), -- 75% success 'e' on day 3
('2016-01-03 11:04', 'e', true),
('2016-01-03 11:05', 'e', true),
('2016-01-03 11:06', 'e', false) ;
_
(一時的な中間)テーブルを作成します。これを_"success_summary"
_と呼びます。これには、さまざまなエンティティと日(実際に発生する場所)ごとの成功率がすべて含まれています。
_CREATE TEMPORARY TABLE success_summary
AS
SELECT
date_period,
entity,
/* If needed, following line gives approx. your aggs. */
/* array[count_successes + count_failures, count_successes, count_failures] AS summary, */
/* Next computation renders the percentage of success as etxt */
to_char(
count_successes::double precision*100.0 / (count_successes + count_failures),
'990.00%') AS pct_text
FROM
(
SELECT
/* date_trunc('day', date_time) AS date_period */
date_time::date AS date_period,
entity,
/* We count successes and failures. We profit from the facxt that CASE always has an implicit ELSE NULL */
count(CASE WHEN result THEN 1 END) AS count_successes,
count(CASE WHEN not result THEN 1 END) AS count_failures
FROM
t
GROUP BY
date_period, entity
) AS q1 ;
_
この時点で、テーブルには次のものが含まれています。
_SELECT to_char(date_period, 'yyyy-mm-dd') AS date_period, entity, pct_text
FROM success_summary
ORDER BY entity, date_period;
| date_period | entity | pct_text |
|-------------|--------|----------|
| 2016-01-01 | a | 100.00% |
| 2016-01-02 | a | 100.00% |
| 2016-01-01 | b | 0.00% |
| 2016-01-02 | b | 0.00% |
| 2016-01-01 | c | 100.00% |
| 2016-01-01 | d | 100.00% |
| 2016-01-03 | e | 75.00% |
_
ここで、(簡単に)crosstab
を実行できるようにするために、すべての欠損値を「埋める」必要があります。これにより、すべてが矩形行列に入力されます。これは、たとえば、前の表には実際には存在しない_(2016-01-01, 'a', *something*)
_値を持つ行があることを意味します。 (注:何かとしてNULLを選択しましたが、代わりにcoalesce(pct_text, 'N/A')
を使用して「N/A」などのテキストを使用できます_pct_text
_)。
それには、さらに別の中間テーブルを使用して、_(date_periods) x (entities)
_のデカルト積を作成します。
_CREATE TEMPORARY TABLE all_success_summary AS
SELECT
date_period, entity, pct_text
FROM
(
-- Cross join to have all (date_period, entity) possible pairs
(SELECT DISTINCT date_period FROM success_summary) AS q00
CROSS JOIN
(SELECT DISTINCT entity FROM success_summary) AS q01
) AS q0
-- Left join with original data to retrieve actual pct_text
-- where it exists (it will be NULL, otherwise)
LEFT JOIN success_summary USING(date_period, entity) ;
_
この中間テーブルの内容は次のとおりです。
_| date_period | entity | pct_text |
|-------------|--------|----------|
| 2016-01-01 | a | 100.00% |
| 2016-01-02 | a | 100.00% |
| 2016-01-03 | a | (null) |
| 2016-01-01 | b | 0.00% |
| 2016-01-02 | b | 0.00% |
| 2016-01-03 | b | (null) |
| 2016-01-01 | c | 100.00% |
| 2016-01-02 | c | (null) |
| 2016-01-03 | c | (null) |
| 2016-01-01 | d | 100.00% |
| 2016-01-02 | d | (null) |
| 2016-01-03 | d | (null) |
| 2016-01-01 | e | (null) |
| 2016-01-02 | e | (null) |
| 2016-01-03 | e | 75.00% |
_
この時点で、最初のバージョンの crosstab
を使用してすべてのデータを取得できますPIVOTed
:
_SELECT
*
FROM
crosstab(
'SELECT entity, date_period, pct_text
FROM all_success_summary
ORDER BY entity, date_period')
AS ct (entity text, "2016-01-01" text, "2016-01-02" text, "2016-01-03" text) ;
_
結果のテーブルは次のとおりです。
_| entity | 2016-01-01 | 2016-01-02 | 2016-01-03 |
|--------|------------|------------|------------|
| a | 100.00% | 100.00% | |
| b | 0.00% | 0.00% | |
| c | 100.00% | | |
| d | 100.00% | | |
| e | | | 75.00% |
_
集計ベクトルやその他の表現は含まれていませんが、SpreadSheetに直接インポートできるフォーマットされた結果が含まれています。
1:CrossTab関数を使用するときに必要な適切な列定義を知るには、次のクエリを使用できます。
_SELECT
'(entity text, ' || string_agg(c, ', ') || ')' AS column_definition
FROM
(
SELECT DISTINCT
'"' || date_period || '" text' AS c
FROM
all_success_summary
ORDER BY
c
) AS q1 ;
_
2:_"date_period"
_を1日だけに設定しました(場所によっては、表示しやすいように結果をフォーマットしました)。 date_trunc('week', date_time) AS date_period
などを使用して、私が使用した定義の代わりに、日ではなく週で要約することで、すべて同じことが実現できます。これは、あらゆるタイプのグループに一般化できます。
3:配列が必要な場合は、success_summaryの定義に関するヒントをどこから取得するかについてのヒントがあります。
4:中間テーブルをすべてスキップできます(名前が表示される場所に定義を重複して配置することにより)。また、ユーザー定義関数内に非表示にして、使用後に削除することもできます。 crosstab
はWITH
ステートメントによって作成された仮想テーブルを「理解」しないため、CTEを使用してそれらを回避することはできません。とにかく、通常は_column_definition
_...も必要なので、一時テーブルが便利です。
5:crosstab
を使用する代わりに、JSON形式で_all_success_summary
_からすべての行を取得することもできます、この情報を後処理します。それはすべて、特定のユースケースに依存します(ただし、Excelのスクリーンショットを見ました...そして、最も近い方向に移動しました;-)。 Excelは、「success_summary」データ(およびおそらく元のデータ)からのすべてのデータをPIVOT自体にPIVOTできると言わなければなりません。
これのほとんど(クロス集計自体を除く)は SQLFiddle で確認できます。
-- Test data
drop table if exists t cascade;
create table t(datetime timestamptz, entity char(1), result bool);
insert into t values
('2016-01-01 11:00:01', 'a', true),
('2016-01-01 17:00:01', 'a', true), -- two events for :a on same day
('2016-01-01 11:01:01', 'b', false),
('2016-01-01 11:03:01', 'c', true),
('2016-01-01 13:00:01', 'd', true), -- only one event for :d
('2016-01-02 11:00:01', 'a', true),
('2016-01-02 11:01:01', 'b', false),
('2016-01-02 11:03:01', 'c', true);
do $$ -- Here we will create the view to select desired data
declare
select_clause text := 'entity';
dates date[];
d date;
date_filter text;
begin
-- Generate array with dates for our columns
select array_agg(dt) into dates
from generate_series((select min(datetime) from t)::date, (select max(datetime) from t)::date, '1 day') as dt;
raise info '%', dates;
-- Generate "select part"
foreach d in array dates loop
date_filter := format('datetime::date = %L', d);
raise notice '%', date_filter;
select_clause :=
select_clause ||
', array[count(*) filter(where ' || date_filter ||
'), count(*) filter(where ' || date_filter ||
' and result), count(*) filter(where ' || date_filter ||
' and not result)] as ' ||
quote_ident(d::text);
end loop;
raise info '%', select_clause;
-- Create temporary view using previously generated "select part"
-- "temp view" is session-wide
execute 'create or replace temp view v as select ' || select_clause || ' from t group by entity';
end $$;
select * from v order by entity;