web-dev-qa-db-ja.com

履歴レコードを保存し、特定の期間からデータを抽出する

これはデータベースモデリングとPostgreSQLに関する質問です。これは post が質問に部分的に回答していますが、より技術的なもの、つまりデータを抽出するための列とクエリについてのアドバイスが必要です。

過去数年間の境界の動きを追跡できるようにするために、特定の領域内に管理境界の履歴を保存する必要があります。

境界がどのように移動するかの例を次に示します。

  • Martin Landエリアは常に存在しています
  • Bulwers Landエリアも常に存在しています
  • これらの領域は2015年に統合されました:
    • 新しいエリアの名前はMartin-Bulwersに変更されました
    • 保持された新しいエリアBulwers Landコード(Martin Landcode is deprecated)

これが私がデータを保存する方法です:

_gid  | code   | name            | change_date
-----+--------+-----------------+-------------
1    | 86001  | Martin Land     | 2000-01-01
2    | 86002  | Bulwers Land    | 2000-01-01
3    | 86002  | Martin-Bulwers  | 2015-01-01
_

エリアの履歴状況、つまり最初の変更が発生する前のデフォルトの_change_date_を_2000-01-01_に設定しました。

それから、私は多くの異なるケースがあります:マージされていない領域、異なる日付で他のマージされた領域。これにより、次の例が生成されます。

_CREATE TEMPORARY TABLE foo AS
SELECT gid,code,area,change_date::date FROM ( VALUES
  ( 1,86001,'Martin Land'   ,'2000-01-01' ),
  ( 2,86002,'Bulwers Land'  ,'2000-01-01' ),
  ( 3,86002,'Martin-Bulwers','2015-01-01' ),
  ( 4,86003,'Coveral Land'  ,'2000-01-01' ),
  ( 5,86004,'Big Tom Area'  ,'2000-01-01' ),
  ( 6,86005,'Small Tom Area','2000-01-01' ),
  ( 7,86004,'Tom Land'      ,'2016-01-01' )
) AS t(gid,code,area,change_date);
_

次に、指定された年の地域のリストを返すクエリを作成することが困難です。私はDISTINCT ON()句に依存しようとしましたが、これは私が必要とする仕事ではありません...

たとえば、次のクエリは次のテーブルを返します。

_SELECT DISTINCT ON (code) code, area, change_date

  FROM myTable WHERE change_date < '2016-01-01'

  ORDER BY code, change_date DESC ;

-- Result:

code   | area            | change_date
-------+-----------------+-------------
86001  | Martin Land     | 2000-01-01
86002  | Martin-Bulwers  | 2015-01-01
86003  | Coveral Land    | 2000-01-01
86004  | Big Tom Area    | 2000-01-01
86005  | Small Tom Area  | 2000-01-01
_

これは、以前のように部分的に正しいです_2016-01-01_、ビッグトムエリアおよびスモールトムエリアは一緒にマージされませんでしたがMartin LandBulwers Landが持っていました! 2014年には5つの分野があり、2015年には4つの分野、2016年には3つの分野がありました。

実際、次の結果が必要です。

_code   | area            | change_date
-------+-----------------+-------------
86002  | Martin-Bulwers  | 2015-01-01
86003  | Coveral Land    | 2000-01-01
86004  | Big Tom Area    | 2000-01-01
86005  | Small Tom Area  | 2000-01-01
_

これらの情報を保存したり、クエリを書き込んだりする別の方法はありますか?

3
wiltomap

問題は、領域がマージされてそのコードが再利用されるとその情報が得られますが、領域がマージされてそのコードが非推奨になるとそれがなくなるということです。つまり、あるエリアが廃止されたという情報がありません。

今、私は2つの解決策を見ます。まず、スキーマをそのままにして、エリアが廃止されたときはいつでも、新しいエリアであるとして新しい行を追加し、それが廃止されていることだけを示します。

INSERT INTO myTable(code, area, change_date)
VALUES(86001, 'deprecated', '2015-01-01')

したがって、クエリでは'deprecated'と表示されるか、フィルターで除外します。

2番目の、可能な最良のオプションは、エリアが非推奨になった時期(およびその場合)を示す新しい列を用意することです。

ALTER TABLE myTable ADD deprecated DATE;
UPDATE myTable SET deprecated = '2015-01-01' WHERE gid = 1;

したがって、クエリでフィルターを追加するだけです。

(deprecated IS NULL OR deprecated >= '2016-01-01')

完全なコード:

SELECT DISTINCT ON (code) code, area, change_date
FROM myTable
WHERE change_date < '2016-01-01'
    AND (deprecated IS NULL OR deprecated >= '2016-01-01')
ORDER BY code, change_date DESC;

非推奨ではない領域の別のオプションとして、NULLの代わりに'infinity'として設定できます。これにより、deprecated IS NULL条件は必要ありません。'infinity'は常に他のnull以外の値以上である。

2
MatheusOl

スキーマをクリーンアップすることをお勧めします。あなたが欲しいのはこのようなものです。

WITH RECURSIVE t(gid, code, area, change_date, parent, depth) AS (                                                                    SELECT gid, code, area, change_date, null::int, 0                                                                                   FROM foo                                                                                                                            WHERE change_date = '2000-01-01'::date                                                                                                                                                                                                                                  UNION ALL                                                                                                                         

  SELECT null, null, foo.area, foo.change_date, t.code, depth+1                                                                     
  FROM t                                                                                                                            
  JOIN foo ON (                                                                                                                     
    (                                                                                                                               
      ( foo.code BETWEEN t.code AND t.code+1 )                                                                                      
      AND foo.gid != t.gid                                                                                                          
    )                                                                                                                               
    AND foo.change_date >= t.change_date                                                                                            
    AND foo.change_date != '2000-01-01'                                                                                             
  )                                                                                                                                 
)                                                                                                                                   
SELECT * FROM t AS t1;  

そのクエリを実行すると、次のような結果セットが表示されます...

 gid | code  |      area      | change_date | parent | depth 
-----+-------+----------------+-------------+--------+-------
   1 | 86001 | Martin Land    | 2000-01-01  |        |     0
   2 | 86002 | Bulwers Land   | 2000-01-01  |        |     0
   4 | 86003 | Coveral Land   | 2000-01-01  |        |     0
   5 | 86004 | Big Tom Area   | 2000-01-01  |        |     0
   6 | 86005 | Small Tom Area | 2000-01-01  |        |     0
     |       | Martin-Bulwers | 2015-01-01  |  86001 |     1
     |       | Martin-Bulwers | 2015-01-01  |  86002 |     1
     |       | Tom Land       | 2016-01-01  |  86003 |     1
     |       | Tom Land       | 2016-01-01  |  86004 |     1

WHERE句を追加するだけで、必要なもの(またはより優れたバージョン)を取得できます。

この方法にはいくつかの問題はありませんが、それらはあなたのデータに固有のものであると思います。 Tom LandのコードはSmall Tom Areaよりも小さく、Coveral Landよりも新しいgidの日付が新しいです。

ここでは、あなたがchange_date/2000-01-01であると想定していますが、nullを受け入れることができない日付列がある可能性があります。テーブルを更新して

  1. 親IDを保存します。
  2. これを実行

    UPDATE foo
    SET date = NULL
    WHERE dates = 2000-01-01
    

私はこれをするでしょう

  1. infinityは悪い考えです:
  2. あなたのスキーマはあまり役に立たない
    • クエリを簡単にするために、下限と上限を無意味に記述しています。
    • これは本質的に論理的な問題ですが、親を保存していません。

必要に応じて、再帰CTEをさらに優れたビューに変えることができます。

とにかく、より良いスキーマを作成する必要があると思いますが、infinityはここでの答えではありません。 hierarchyが必要です。

0
Evan Carroll