PostgreSQLテーブルの作成日時と更新日時の取得について

# はじめに
サーバーリプレース作業にて、Windows Server 2008R2 + Oracle 11g から Windows Server 2016 + PostgreSQL 9.6 に移行作業をしています。自社アプリケーションの中にSQLServer側にレプリケーションするためにトリガーを作成するツールがありまして、テーブル情報として作成日時と更新日時が表示されていました。

Oracleではテーブル情報(USER_OBJECTS)を利用して、作成日時(created)と更新日時(last_ddl_time)が取得できます。

PostageSQLではテーブル情報(pg_stat)を参照しても、作成日時や更新日時に該当する項目はないようです。調べる限りでは、PostageSQLではそのような情報は保持する仕組みにはなっていないとのこと。

アプリケーション上は作成日時と更新日時は参考情報程度のものなので無くても問題にはならないが、参考情報程度であれば多少違っていても悪くない。

【2019/08/03追加】
スキーマ違いで同じテーブル名があると「ERROR:  more than one row returned by a subquery used as an expression」になりました。サブクエリのリターンが複数となっているのがエラー原因です。ということで一意になるようにプログラムを修正しました。

【2022/11/23追加】
フォルダやファイルが存在しなかった場合に例外エラー(File Not Found)が出ていたが、pg_stat_fileの第二引数にmissing_okがあることを知りました。
missing_okパラメータは、ファイルまたはディレクトリが存在しない場合の動作を指定できます。 trueの場合、関数はNULLを返します。 falseの場合はエラーが発生します。 デフォルトはfalseです。

「pg_stat_file」のみで検索した時にトップに出てくるのが、[PosgtgreSQL 8.1のドキュメント](https://www.postgresql.jp/document/9.6/html/functions-admin.html)なんですよね。このバージョンだと、まだmissing_okパラメータがないんですよね。URLの8.1のところを9.6に変更したドキュメントには、missing_okパラメータの説明が記載されています。なので、気が付くのが遅れた。
https://www.postgresql.jp/document/9.6/html/functions-admin.html

# ファイル情報から取得
調べてみると、`$PGDATA/base/{database id}/{table filenode id}` で取得できそうなことが分かった。

* [PostgreSQL：テーブル作成時間](https://code.i-harness.com/ja/q/275310)
* [PostgreSQL: Table creation time](https://stackoverflow.com/questions/2577168/postgresql-table-creation-time)
* [Automatically Drop Tables and Indexes Older than 90 Days [duplicate]](https://stackoverflow.com/questions/18849756/automatically-drop-tables-and-indexes-older-than-90-days/18852752#18852752])

pg_stat_file関数ではファイル作成時刻(creation:Windowsのみ)、最終更新時刻(modification)が取得できるので、これを利用する。

ちなみに、GreenplumというPostgreSQLを元に開発された並列処理可能なRDBには、pg_stat_last_operationテーブルがあり、CREATEした日時が取得できるようです。
[超高速のPostgreSQLとしてGreenplumを気軽に使う](https://www.scutum.jp/information/waf_tech_blog/2016/02/waf-blog-045.html)

# ソースコード
ファイル作成時刻と最終更新時刻の2つをサブクエリで取得するとファイルを2回アクセスするので、一度に済ませる方法を探したら、配列コンストラクタという方法を見つけた。
[サブクエリで複数カラムを返す](http://yanor.net/wiki/?PostgreSQL%2FSQL%2F%E3%82%B5%E3%83%96%E3%82%AF%E3%82%A8%E3%83%AA%E3%81%A7%E8%A4%87%E6%95%B0%E3%82%AB%E3%83%A9%E3%83%A0%E3%82%92%E8%BF%94%E3%81%99)

※table_schema = 'public' は任意のスキーマに変更してください。

```sql
SELECT
    table_name
   ,arr[1] AS creation
   ,arr[2] AS modification
FROM
(
    SELECT
         table_name
        ,(SELECT ARRAY[creation,modification] 
          FROM pg_stat_file('./base/' || folder || '/' || filenode, true)) AS arr
    FROM (
        SELECT
            table_name
           ,(SELECT MAX(pg_ls_dir::int)::text
             FROM pg_ls_dir('./base')
             WHERE pg_ls_dir <> 'pgsql_tmp'
               AND pg_ls_dir::int  <= 
            (SELECT relfilenode FROM pg_class cls
             INNER JOIN pg_namespace nsp ON cls.relnamespace = nsp.oid
             WHERE nsp.nspname = table_schema AND relname ILIKE table_name)) AS folder 
           ,(SELECT relfilenode FROM pg_class cls
             INNER JOIN pg_namespace nsp ON cls.relnamespace = nsp.oid
             WHERE nsp.nspname = table_schema AND relname ILIKE table_name) AS filenode
        FROM information_schema.tables
    WHERE table_type = 'BASE TABLE'
      AND table_schema = 'public'
   ) CTE
) X
```

## 結果
|table_name|creation|modification|
|:--|:--|:--|
|Fuga|2018-09-15 14:01:50+09|2018-09-15 14:01:50+09|
|Hoge|2018-09-15 14:03:01+09|2018-09-15 14:03:01+09|

Hoge テーブルにカラムを追加してみた。

|table_name|creation|modification|
|:--|:--|:--|
|Fuga|2018-09-15 14:01:50+09|2018-09-15 14:01:50+09|
|Hoge|2018-09-15 15:49:01+09|2018-09-15 15:49:01+09|

ファイル作成時刻と最終更新時刻が同じになってしまいました。確かにBaseフォルダにある該当ファイルのプロパティを見るとそうなっています。
![postgre16394.png](https://qiita-image-store.s3.amazonaws.com/0/14053/631f8c1d-698e-78a7-8a71-8aa70fded7dc.png)

# 改良版
>この回避策は、 PG_VERSIONが作成後に変更される可能性が最も低いと想定しています。
>[PostgreSQL：テーブル作成時間](https://code.i-harness.com/ja/q/275310)

ファイル作成時刻(creation)はWindows版のみのようですし、最終更新時刻(modification)のみを対象にする。ファイル作成時刻は変更される可能性が最も低いPG_VERSIONの最終更新時刻を使う。

※table_schema = 'public' は任意のスキーマに変更してください。

```sql
SELECT
    table_name
   ,(SELECT modification FROM pg_stat_file('./base/' || folder || '/PG_VERSION', true)) AS creation
   ,(SELECT modification FROM pg_stat_file('./base/' || folder || '/' || filenode, true)) AS modification
FROM (
    SELECT
        table_name
       ,(SELECT MAX(pg_ls_dir::int)::text
         FROM pg_ls_dir('./base')
         WHERE pg_ls_dir <> 'pgsql_tmp'
           AND pg_ls_dir::int  <= 
        (SELECT relfilenode FROM pg_class cls
         INNER JOIN pg_namespace nsp ON cls.relnamespace = nsp.oid
         WHERE nsp.nspname = table_schema AND relname ILIKE table_name)) AS folder 
       ,(SELECT relfilenode FROM pg_class cls
         INNER JOIN pg_namespace nsp ON cls.relnamespace = nsp.oid
         WHERE nsp.nspname = table_schema AND relname ILIKE table_name) AS filenode
    FROM information_schema.tables
    WHERE table_type = 'BASE TABLE'
      AND table_schema = 'public'
) CTE
```

|table_name|creation|modification|
|:--|:--|:--|
|Fuga|2018-09-15 13:56:29+09|2018-09-15 14:01:50+09|
|Hoge|2018-09-15 13:56:29+09|2018-09-15 15:49:01+09|

※ファイル作成時刻(creation)は変更される可能性が最も低いPG_VERSIONの最終更新時刻です。

## VIEW作成
こういうのは、ビューを作成するといいでしょう。

※table_schema = 'public' は任意のスキーマに変更してください。

```sql
CREATE OR REPLACE VIEW view_table_stat AS
SELECT
    table_name::text AS tablename
   ,(SELECT modification FROM pg_stat_file('./base/' || folder || '/PG_VERSION', true)) AS createdate
   ,(SELECT modification FROM pg_stat_file('./base/' || folder || '/' || filenode, true)) AS updatedate
FROM (
   SELECT
       table_name
      ,(SELECT MAX(pg_ls_dir::int)::text
        FROM pg_ls_dir('./base')
        WHERE pg_ls_dir <> 'pgsql_tmp'
          AND pg_ls_dir::int  <= 
       (SELECT relfilenode FROM pg_class cls
        INNER JOIN pg_namespace nsp ON cls.relnamespace = nsp.oid
        WHERE nsp.nspname = table_schema AND relname ILIKE table_name)) AS folder 
      ,(SELECT relfilenode FROM pg_class cls
        INNER JOIN pg_namespace nsp ON cls.relnamespace = nsp.oid
        WHERE nsp.nspname = table_schema AND relname ILIKE table_name) AS filenode
   FROM information_schema.tables
   WHERE table_type = 'BASE TABLE'
     AND table_schema = 'public'
) CTE;
```

## 関数作成
ついでに関数を生成してみました。
レコードを返す関数を作成するのに思ってたより情報がなくて苦労しました。
ビューで済むので関数作る意味はないんだけど、何かの参考にはなるでしょう。

※table_schema = 'public' は任意のスキーマに変更してください。

```sql
CREATE OR REPLACE FUNCTION get_table_stat() 
RETURNS TABLE (
    tablename text
   ,createdate timestamp with time zone
   ,updatedate timestamp with time zone) AS $$
BEGIN
    RETURN QUERY
    SELECT
         table_name::text AS tablename
        ,(SELECT modification FROM pg_stat_file('./base/' || folder || '/PG_VERSION', true)) AS createdate
        ,(SELECT modification FROM pg_stat_file('./base/' || folder || '/' || filenode, true)) AS updatedate
    FROM (
        SELECT
           table_name
          ,(SELECT MAX(pg_ls_dir::int)::text
            FROM pg_ls_dir('./base')
            WHERE pg_ls_dir <> 'pgsql_tmp'
              AND pg_ls_dir::int  <= 
           (SELECT relfilenode FROM pg_class cls
            INNER JOIN pg_namespace nsp ON cls.relnamespace = nsp.oid
            WHERE nsp.nspname = table_schema AND relname ILIKE table_name)) AS folder 
          ,(SELECT relfilenode FROM pg_class cls
            INNER JOIN pg_namespace nsp ON cls.relnamespace = nsp.oid
            WHERE nsp.nspname = table_schema AND relname ILIKE table_name) AS filenode
        FROM information_schema.tables
        WHERE table_type = 'BASE TABLE'
          AND table_schema = 'public'
    ) CTE;
END
$$ LANGUAGE plpgsql;

-- 使い方
SELECT * FROM get_table_stat()
```

# 最後に
CLUSTER コマンドや VACUUM FULL コマンドを使ってテーブルの再編成されてしまうと最終更新時刻も変更されてしまうので、あくまで参考情報です。
正確なファイル作成時刻と最終更新時刻を求めるなら専用テーブルと仕組みを作成するしかないですね。