ぼくは明日、昨日のじぶんに頼りたい

明日のためのメモです。

PostgreSQLのパフォーマンス改善用のメモ

個人的に色々と参考させていただいているものをまとめました。

HugePages

オンプレのLinuxは有効にすることを検討する。

インデックス

カバリングインデックスの作成も検討する。
ただ、インデックスオンリースキャンを狙いにいっても可視性の情報はみないといけないので更新が多いテーブルは注意する。

FILLFACTOR

更新が多いテーブルはFILLFACTORを設定することを検討する。

ロック管理

場合によるが、deadlock_timeoutの変更を検討する。
デッドロック検出処理が同時セッション数の分行われサーバ負荷が高くなってどうにもならなくなる可能性を考慮する。

ユーザ定義関数が返す行数の推定値

デフォルト推定値(ROWS result_rows)は1000行になっているので変更することを検討する。

パーティショニング

結構データ量ないと意味ない感じと思われる(11系で約100万行だと変わらなかった)
意味ある状況に遭遇したい。

全体的に参考にしているもの

バージョンアップによる改善メモ

リリースノートを見て気になったものを抜粋。
あと、11からバージョンアップする場合はJITの挙動に注意する。

11 → 12

12 → 13

13 → 14

Chrome拡張機能をCreate React Appを使って作る

Chrome拡張機能を作ってみようと思いました。
とりあえずTypeScriptの方がいいかと思ったり、Reactでも使ってみるかと思いました。
検索するとスケルトン的なのを公開してくれている方もいたのですが、若干バージョン古くなってしました。
少し考えた結果、Create React App使えば良くないと思ったのでメモを残します。

Node.jsのインストール

いろいろ参考になる記事あると思うので、参考にしてインストールします。

$ node -v
v16.15.1

Create React App

表題の通りcreate-react-app使います。

$ npx create-react-app my-app --template typescript

create-react-app.dev

manifest.jsonを修正

Chrome拡張用に修正というか置き換えます。

developer.chrome.com

$ cat > public/manifest.json <<EOF
{
  "name": "__MSG_ext_name__",
  "description": "__MSG_ext_desc__",
  "version": "1.0",
  "manifest_version": 3,
  "default_locale": "ja",
  "icons": {
    "16": "logo192.png",
    "48": "logo192.png",
    "128": "logo192.png"
  },
  "action": {
    "default_popup": "index.html"
  }
}
EOF

細かい説明はドキュメントに任せますが、とりあえずindex.htmlをpopupとして使用する形にしています。popup.htmlの方がいいかもしれません。
権限とかは作成する拡張によるので、実装時に調整します。

言語化はしないと思いますが、たまたま目についてしまったので対応しときます。

developer.chrome.com

$ mkdir -p public/_locales/{en,ja}
$ cat > public/_locales/en/messages.json <<EOF
{
  "ext_name": {
    "message": "Extension Name"
  },
  "ext_desc": {
    "message": "Extension Name"
  }
}
EOF

$ cat > public/_locales/ja/messages.json <<EOF
{
  "ext_name": {
    "message": "拡張機能の名前"
  },
  "ext_desc": {
    "message": "拡張機能の名前"
  }
}
EOF

Chrome拡張向けに微調整

他にも微調整必要ではというツッコミはなしとして

Chrome拡張には不要なファイルを削除する

$ rm public/{favicon.ico,logo512.png,robots.txt}

@types/chromeをインストール

$ npm install @types/chrome --save-dev

ビルド

$ npm run build

Chrome拡張機能の「パッケージ化されていない拡張機能を読み込む」で、buildディレクトリを指定します。
これで動くっぽいことを確認できます。

https://developer.chrome.com/docs/extensions/mv3/getstarted/#unpacked

実装

ひとまずは、App.tsxを修正して拡張機能を作っていけば良いかなと思ったところで終わりました。

日付型が条件にあるSQLをライブラリを介して発行した際に困ったことになった

とある言語のライブラリをロクに検証せず使っていたら困ったことになって、そこ気にしないといけないのだっけと思った次第です。無知なだけです。
PostgreSQLを使っています。DATE 型のカラムに対してライブラリを介して日付型のパラメータを渡した際の挙動どうなるかを確認して行きたいと思います。

準備

postgres=# SELECT version();
-[ RECORD 1 ]------------------------------------------------------------------------------------------------------------------------
version | PostgreSQL 14.2 (Debian 14.2-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit

CREATE TABLE sample
(
  id serial,
  name text,
  start_date date NOT NULL,
  end_date date NOT NULL,
  start_datetime timestamp without time zone NOT NULL,
  end_datetime timestamp without time zone NOT NULL,
  PRIMARY KEY (id)
);

-- データ登録
INSERT INTO sample (name, start_date, end_date, start_datetime, end_datetime)
VALUES ('Alice', CURRENT_DATE, CURRENT_DATE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);

CURRENT_DATECURRENT_TIMESTAMPを利用してデータを登録していますが、途中外出して日付変わってしまったので入れなおしてます。

PostgreSQLで確認

いったん素のSQLで念のため確認します。

CURRENT_DATEで確認(DATE = DATE)

-- CURRENT_DATEとCURRENT_TIMESTAMPを型を変換するパターンで確認
postgres=# SELECT * FROM sample WHERE CURRENT_DATE BETWEEN start_date AND end_date;
 id | name  | start_date |  end_date  |       start_datetime       |        end_datetime        
----+-------+------------+------------+----------------------------+----------------------------
  1 | Alice | 2022-05-28 | 2022-05-28 | 2022-05-28 10:54:26.129743 | 2022-05-28 10:54:26.129743 
(1 row)

これはDATEとDATEで時分秒は関係ないので1件取得できました。期待通り。

CURRENT_DATE::TIMESTAMPで確認(TIMESTAMP = DATE)

postgres=# SELECT * FROM sample WHERE CURRENT_DATE::TIMESTAMP BETWEEN start_date AND end_date;
 id | name  | start_date |  end_date  |       start_datetime       |        end_datetime        
----+-------+------------+------------+----------------------------+---------------------------- 
  1 | Alice | 2022-05-28 | 2022-05-28 | 2022-05-28 10:54:26.129743 | 2022-05-28 10:54:26.129743  
(1 row)

これはTIMESTAMPとDATEですが、'2022-05-28 00:00:00'になると思っているので1件取得できました。期待通り。

postgres=# EXPLAIN SELECT * FROM sample WHERE CURRENT_DATE::TIMESTAMP BETWEEN start_date AND end_date;
-[ RECORD 1 ]-------------------------------------------------------------------------------------------------------------------------------------
QUERY PLAN | Seq Scan on sample  (cost=0.00..33.00 rows=102 width=60)
-[ RECORD 2 ]-------------------------------------------------------------------------------------------------------------------------------------
QUERY PLAN |   Filter: (((CURRENT_DATE)::timestamp without time 
zone >= start_date) AND ((CURRENT_DATE)::timestamp without time 
zone <= end_date))

postgres=# SELECT (CURRENT_DATE)::timestamp;
-[ RECORD 1 ]+--------------------
current_date | 2022-05-28 00:00:00

CURRENT_TIMESTAMPで確認(TIMESTAMP = DATE)

postgres=# SELECT * FROM sample WHERE CURRENT_TIMESTAMP BETWEEN start_date AND end_date;
 id | name | start_date | end_date | start_datetime | end_datetime
----+------+------------+----------+----------------+--------------
(0 rows)

これはTIMESTAMPとDATEで、'2022-05-28 10:59:26'などになるので取得できません。期待通り。

CURRENT_TIMESTAMP::DATEで確認(DATE = DATE)

postgres=# SELECT * FROM sample WHERE CURRENT_TIMESTAMP::DATE BETWEEN start_date AND end_date;
 id | name  | start_date |  end_date  |       start_datetime       |        end_datetime        
----+-------+------------+------------+----------------------------+---------------------------- 
  1 | Alice | 2022-05-28 | 2022-05-28 | 2022-05-28 10:54:26.129743 | 2022-05-28 10:54:26.129743  
(1 row)

これはDATEとDATEで、'2022-05-28 10:59:26'::DATEなどが'2022-05-28'::DATEで取得できました。期待通り。

postgres=# SELECT (CURRENT_TIMESTAMP)::date;
-[ RECORD 1 ]-----+-----------
current_timestamp | 2022-05-28

日時部分を文字列でDATEに型変換で確認(DATE = DATE)

postgres=# SELECT * FROM sample WHERE '2022-05-28 10:59:26'::DATE BETWEEN start_date AND end_date;
 id | name  | start_date |  end_date  |       start_datetime    
   |        end_datetime
----+-------+------------+------------+----------------------------+----------------------------
  1 | Alice | 2022-05-28 | 2022-05-28 | 2022-05-28 10:54:26.129743 | 2022-05-28 10:54:26.129743
(1 row)

これはDATEとDATEで時分秒は関係ないので1件取得できました。期待通り。

日時部分を文字列でTIMESTAMPに型変換で確認(TIMESTAMP = DATE)

postgres=# SELECT * FROM sample WHERE '2022-05-28 10:59:26'::TIMESTAMP BETWEEN start_date AND end_date;~
 id | name | start_date | end_date | start_datetime | end_datetime
----+------+------------+----------+----------------+--------------
(0 rows)

これはTIMESTAMPとDATEで、'2022-05-28 10:59:26'などになるので取得できません。期待通り。

日時部分を文字列で明示的な型変換なしで確認(? = DATE)

postgres=# SELECT * FROM sample WHERE '2022-05-28 10:59:26' BETWEEN start_date AND end_date;
 id | name  | start_date |  end_date  |       start_datetime    
   |        end_datetime
----+-------+------------+------------+----------------------------+----------------------------
  1 | Alice | 2022-05-28 | 2022-05-28 | 2022-05-28 10:54:26.129743 | 2022-05-28 10:54:26.129743
(1 row)

これは'2022-05-28 10:59:26'が暗黙の型変換でDATEになり取れているということですかね。

postgres=# EXPLAIN SELECT * FROM sample WHERE '2022-05-28 10:59:26' BETWEEN start_date AND end_date;
-[ RECORD 1 ]-----------------------------------------------------------------------------------
QUERY PLAN | Seq Scan on sample  (cost=0.00..23.80 rows=102 width=60)
-[ RECORD 2 ]-----------------------------------------------------------------------------------
QUERY PLAN |   Filter: (('2022-05-28'::date >= start_date) AND ('2022-05-28'::date <= end_date))

EXPLAINで見ると'2022-05-28'::dateとキャストされているようです。

ライブラリを経由してみる

C#のものは今回の話の流れとして必要でしたが、他は本当は色々試したかったけど、という感じです。

C#

EF Core(.NET6)

概要 - EF Core | Microsoft Docs

$ dotnet --version
6.0.300

# 新しいプロジェクトを作成する
$ dotnet new console -o EFGetStarted

リバース エンジニアリング - EF Core | Microsoft Docs

$ dotnet tool install --global dotnet-ef
$ dotnet add package Microsoft.EntityFrameworkCore.Design

# モデルを作成する
$ dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
$ dotnet ef dbcontext scaffold "Host=localhost;Port=15432;Database=postgres;Username=postgres;Password=password" Npgsql.EntityFrameworkCore.PostgreSQL
namespace EFGetStarted
{
    public partial class Sample
    {
        public int Id { get; set; }
        public string? Name { get; set; }
        public DateOnly StartDate { get; set; }
        public DateOnly EndDate { get; set; }
        public DateTime StartDatetime { get; set; }
        public DateTime EndDatetime { get; set; }
    }
}

ちなみにDateOnlyは.Net6からです。
※ usingは省略しています。

namespace EFGetStarted;

internal class Program
{
  private static void Main()
  {
    using (var db = new postgresContext())
    {
      var now = DateOnly.FromDateTime(DateTime.Now);
      Console.WriteLine(now);

      var sample = db.Samples
          .Where(s => s.StartDate >= now && s.EndDate <= now)
          .First();
      Console.WriteLine($"{sample.Id} {sample.Name} {sample.StartDate} {sample.EndDate} {sample.StartDatetime} {sample.EndDatetime}");
    }
  }
}

DateOnly.FromDateTime(DateTime.Now)を使って範囲検索してみます。

$ dotnet run
2022/05/28
1 Alice 2022/05/28 2022/05/28 2022/05/28 10:54:26 2022/05/28 10:54:26   

DateOnly型を明示的に使う側で渡しているのもあり、期待した形で取得できました。

EF Core(.NET Core 3.1)

使用する .NET のバージョンを選択する - .NET | Microsoft Docs

$ dotnet new globaljson --sdk-version 3.1.419

$ dotnet --version
3.1.419

# 新しいプロジェクトを作成する
$ dotnet new console -o ../EFGetStarted3.1
$ dotnet new tool-manifest
$ dotnet tool install dotnet-ef --version 3.1.25
$ dotnet add package Microsoft.EntityFrameworkCore.Design --version 3.1.25

# モデルを作成する
$ dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 3.1.18
$ dotnet ef dbcontext scaffold "Host=localhost;Port=15432;Database=postgres;Username=postgres;Password=password" Npgsql.EntityFrameworkCore.PostgreSQL
namespace EFGetStarted3._1
{
    public partial class Sample
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime StartDate { get; set; }
        public DateTime EndDate { get; set; }
        public DateTime StartDatetime { get; set; }
        public DateTime EndDatetime { get; set; }
    }
}

DateOnlyは.Net6からなのでDateTimeになりました。

namespace EFGetStarted3._1
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Now");
      var now = DateTime.Now;
      Test(now);

      Console.WriteLine("Today");
      var today = DateTime.Today;
      Test(today);
    }

    static void Test(DateTime date)
    {
      Console.WriteLine(date);

      using (var db = new postgresContext())
      {
        var sample = db.Sample
            .Where(s => s.StartDate >= date && s.EndDate <= date)
            .First();
        Console.WriteLine($"{sample.Id} {sample.Name} {sample.StartDate} {sample.EndDate} {sample.StartDatetime} {sample.EndDatetime}");
      }
    }
  }
}

DateTime.NowDateTime.Todayを使って範囲検索してみます。

$ dotnet run
Now
2022/05/28 17:05:40
1 Alice 2022/05/28 0:00:00 2022/05/28 0:00:00 2022/05/28 10:54:26 2022/05/28 10:54:26
Today
2022/05/28 0:00:00
1 Alice 2022/05/28 0:00:00 2022/05/28 0:00:00 2022/05/28 10:54:26 2022/05/28 10:54:26 

両方とも検索できました。

2022-05-29 15:06:47.599 JST [107] LOG:  execute <unnamed>: SELECT s.id, s.end_date, s.end_datetime, s.name, s.start_date, s.start_datetime
    FROM sample AS s
    WHERE (s.start_date >= $1) AND (s.end_date <= $1)
    LIMIT 1
2022-05-29 15:06:47.599 JST [107] DETAIL:  parameters: $1 = '2022-05-29'
2022-05-29 15:06:47.599 JST [107] LOG:  duration: 0.014 ms  plan:
    Query Text: SELECT s.id, s.end_date, s.end_datetime, s.name, s.start_date, s.start_datetime
    FROM sample AS s
    WHERE (s.start_date >= $1) AND (s.end_date <= $1)
    LIMIT 1
    Limit  (cost=0.00..0.23 rows=1 width=60)
      ->  Seq Scan on sample s  (cost=0.00..23.80 rows=102 width=60)
            Filter: ((start_date >= '2022-05-29'::date) AND (end_date <= '2022-05-29'::date))

PostgresのログをみるとパラメータがNowの時でも'2022-05-29'となってました。

Dapper
$ dotnet --version
6.0.300

# 新しいプロジェクトを作成する
$ dotnet new console -o ../DapperGetStarted
$ dotnet add package Dapper --version 2.0.123
$ dotnet add package Npgsql --version 6.0.4

エンティティはEF Coreと同じものを使用しています。

namespace DapperGetStarted
{
  class Program
  {
    static async Task Main(string[] args)
    {
      Console.WriteLine("Now");
      var now = DateTime.Now;
      await TestAsync(now);

      Console.WriteLine("Today");
      var today = DateTime.Today;
      await TestAsync(today);
    }

    static async Task TestAsync(DateTime date)
    {
      Console.WriteLine(date);

      using (var connection =
          new NpgsqlConnection("Host=localhost;Port=15432;Database=postgres;Username=postgres;Password=password"))
      {
        var sample = (await connection.QueryAsync<Sample>(
            "SELECT * FROM sample WHERE start_date >= @start_date AND end_date <= @end_date",
            new { start_date = date, end_date = date })).FirstOrDefault();
        if (sample != null)
        {
          Console.WriteLine($"{sample.Id} {sample.Name} {sample.StartDate} {sample.EndDate} {sample.StartDatetime} {sample.EndDatetime}");
        }
      }
    }
  }
}

同じくDateTime.NowDateTime.Todayを使って範囲検索してみます。

$ dotnet run
Now
2022/05/28 23:01:55
Today
2022/05/28 0:00:00
1 Alice 0001/01/01 0:00:00 0001/01/01 0:00:00 0001/01/01 0:00:00 0001/01/01 0:00:00

DateTime.Nowは取得できません。型情報はDateTime以外わからないので、こうことになってしまう感じですかね。

# DateTime.Now
2022-05-29 15:12:20.015 JST [114] LOG:  execute <unnamed>: SELECT * FROM sample WHERE start_date >= $1 AND end_date <= $2
2022-05-29 15:12:20.015 JST [114] DETAIL:  parameters: $1 = '2022-05-29 15:12:19.474152', $2 = '2022-05-29 15:12:19.474152'
2022-05-29 15:12:20.019 JST [114] LOG:  duration: 3.971 ms  plan:
    Query Text: SELECT * FROM sample WHERE start_date >= $1 AND end_date <= $2
    Seq Scan on sample  (cost=0.00..23.80 rows=102 width=60)
      Filter: ((start_date >= '2022-05-29 15:12:19.474152'::timestamp without time zone) AND (end_date <= '2022-05-29 15:12:19.474152'::timestamp without time zone))

# DateTime.Today
2022-05-29 16:30:03.040 JST [210] LOG:  execute <unnamed>: SELECT * FROM sample WHERE start_date >= $1 AND end_date <= $2
2022-05-29 16:30:03.040 JST [210] DETAIL:  parameters: $1 = '2022-05-29 00:00:00', $2 = '2022-05-29 00:00:00'
2022-05-29 16:30:03.040 JST [210] LOG:  duration: 0.009 ms  plan:
    Query Text: SELECT * FROM sample WHERE start_date >= $1 AND end_date <= $2
    Seq Scan on sample  (cost=0.00..23.80 rows=102 width=60)
      Filter: ((start_date >= '2022-05-29 00:00:00'::timestamp without time zone) AND (end_date <= '2022-05-29 00:00:00'::timestamp without time zone))

パラメータは「timestamp」でキャストされている。
また、DateOnlyは「Unhandled exception. System.NotSupportedException: The member start_date of type System.DateOnly cannot be used as a parameter value」でまだ使えないみたいです。

Rails(Ruby)

ActiveRecord
$ rails db:schema:dump
$ rails generate model Sample --skip-migration

テーブル名sampleにしてしまっていたって気づきました。

$ rails c
irb(main):001:0> now = Time.now
=> 2022-05-29 15:17:06.6508589 +0900
irb(main):002:0> Sample.where("start_date >= ?", now).where("end_date <= ?", now)   
  Sample Load (2.9ms)  SELECT "sample".* FROM "sample" WHERE (start_date >= '2022-05-29 06:17:06.650858') AND (end_date <= '2022-05-29 06:17:06.650858')
=> 
[#<Sample:0x00007fea7fcf6608
  id: 1,
  name: "Alice",
  start_date: Sun, 29 May 2022,
  end_date: Sun, 29 May 2022,
  start_datetime: Sun, 29 May 2022 14:04:26.409945000 UTC +00:00,
  end_datetime: Sun, 29 May 2022 14:04:26.409945000 UTC +00:00>] 

WHERE (start_date >= '2022-05-29 06:17:06.650858')ってなってるけど、取得できている。

2022-05-29 15:17:13.364 JST [120] LOG:  execute <unnamed>: SELECT "sample".* FROM "sample" WHERE (start_date >= '2022-05-29 06:17:06.650858') AND (end_date <= '2022-05-29 06:17:06.650858')
2022-05-29 15:17:13.364 JST [120] LOG:  duration: 0.010 ms  plan:
    Query Text: SELECT "sample".* FROM "sample" WHERE (start_date >= '2022-05-29 06:17:06.650858') AND (end_date <= '2022-05-29 06:17:06.650858')
    Seq Scan on sample  (cost=0.00..23.80 rows=102 width=60)
      Filter: ((start_date >= '2022-05-29'::date) AND (end_date <= '2022-05-29'::date))

Dapperとは異なりパラメータ直接埋まってます。

TypeScript

Prisma

Add Prisma to an existing project | Prisma Docs

# Set up Prisma
$ npm install prisma --save-dev
$ npx prisma init

# 事前に.envのDATABASE_URLを修正
# Introspect your database with Prisma
$ npx prisma db pull

# Install and generate Prisma Client
$ npm install @prisma/client
$ npx prisma generate

# Querying the database
$ npx ts-node index.ts
model sample {
  id             Int      @id @default(autoincrement())
  name           String?
  start_date     DateTime @db.Date
  end_date       DateTime @db.Date
  start_datetime DateTime @db.Timestamp(6)
  end_datetime   DateTime @db.Timestamp(6)
}

スキーマの型がDateTime @db.DateとDateTime @db.Timestamp(6)で認識されています。この時点で良さそうです。

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient({
  // https://www.prisma.io/docs/concepts/components/prisma-client/working-with-prismaclient/logging
  log: ['query', 'info', 'warn', 'error'],
});

async function test(date: Date): Promise<void> {
  console.log({ date })

  const samples = await prisma.sample.findMany({
    where: { AND: [{ start_date: { lte: date } }, { end_date: { gte: date } }] },
  })
  console.log({ samples })
}

async function main() {
  const now = new Date()
  await test(now)
}

main()
  .catch((e) => {
    throw e
  })
  .finally(async () => {
    await prisma.$disconnect()
  })

new Date()を使って範囲検索してみます。

$ npx ts-node index.ts
{ date: 2022-05-29T06:25:09.893Z }
{
  samples: [
    {
      id: 1,
      name: 'Alice',
      start_date: 2022-05-29T00:00:00.000Z,    
      end_date: 2022-05-29T00:00:00.000Z,      
      start_datetime: 2022-05-29T14:04:26.409Z,
      end_datetime: 2022-05-29T14:04:26.409Z   
    }
  ]
}

取得できました。

2022-05-29 15:25:10.321 JST [128] LOG:  execute s0: SELECT "public"."sample"."id", "public"."sample"."name", "public"."sample"."start_date", "public"."sample"."end_date", "public"."sample"."start_datetime", "public"."sample"."end_datetime" FROM "public"."sample" WHERE ("public"."sample"."start_date" <= $1 AND "public"."sample"."end_date" >= $2) OFFSET $3
2022-05-29 15:25:10.321 JST [128] DETAIL:  parameters: $1 = '2022-05-29', $2 = '2022-05-29', $3 = '0'
2022-05-29 15:25:10.321 JST [128] LOG:  duration: 0.010 ms  plan:
    Query Text: SELECT "public"."sample"."id", "public"."sample"."name", "public"."sample"."start_date", "public"."sample"."end_date", "public"."sample"."start_datetime", "public"."sample"."end_datetime" FROM "public"."sample" WHERE ("public"."sample"."start_date" <= $1 AND "public"."sample"."end_date" >= $2) OFFSET $3
    Seq Scan on sample  (cost=0.00..23.80 rows=102 width=60)
      Filter: ((start_date <= '2022-05-29'::date) AND (end_date >= '2022-05-29'::date))

渡っているパラメータが'2022-05-29'となってます。

まとめ

結果はDapperだけ気を付ける必要がありました。Dapperが悪いわけではありません。 何種類かのライブラリをチュートリアル的に触る感じになってしまいましたが、結論的にはちゃんと単体テストしようねということですね。

PostgreSQLのVACUUMを整理する

以下の動画をみてVACUUMのこと何も知らないなと思ったので個人的に整理しました。

www.youtube.com

www.techscore.com

上の記事を参考にハンズオン形式で整理したいと思います。

version

PostgreSQL 14.2 (Debian 14.2-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit

postgresql.conf

log_autovacuum_min_duration = 0
autovacuum_vacuum_scale_factor = 0.2

テストデータの準備

テスト用のテーブルを作成

CREATE TABLE sample
(
  id serial NOT NULL,
  name text,
  create_date timestamp without time zone NOT NULL
);

テーブルの実ファイルの確認

$ psql -U postgres -d sample
sample=# SELECT datid, datname FROM pg_stat_database WHERE datname = 'sample';
-[ RECORD 1 ]---
datid   | 16384
datname | sample
sample=# SELECT relfilenode,relname FROM pg_class WHERE relname = 'sample';
-[ RECORD 1 ]-------
relfilenode | 16386
relname     | sample
sample=# exit
$ ls -lh $PGDATA/base/16384/16386*
-rw------- 1 postgres postgres 0 May  1 12:55 /var/lib/postgresql/data/base/16384/16386

ファイルサイズは0です。

1行INSERT

INSERT INTO sample(name, create_date )
VALUES (
  'abc',
  -- 2022/4/1~2023/3/31でランダムを期待している
  ('2022-04-01 00:00:00'::timestamp + trunc(random() * 365) * '1 day'::interval + trunc(random() * 24) * '1 hour'::interval)
);

テーブルの実ファイルの確認

$ ls -lh $PGDATA/base/16384/16386*
-rw------- 1 postgres postgres 8.0K May  1 13:01 /var/lib/postgresql/data/base/16384/16386

ファイルサイズが8KBとなりました。SELECTしてみます。

SELECT

sample=# EXPLAIN ANALYZE SELECT * FROM sample WHERE id = 1;
                                            QUERY PLAN
--------------------------------------------------------------------------------------------------
 Seq Scan on sample  (cost=0.00..24.12 rows=6 width=44) (actual time=0.020..0.021 rows=1 loops=1) 
   Filter: (id = 1)
 Planning Time: 0.188 ms
 Execution Time: 0.042 ms
(4 rows)

1000万件INSERT

INSERT INTO sample(name, create_date )
SELECT
md5(clock_timestamp()::TEXT),
-- 2022/4/1~2023/3/31でランダムを期待している
('2022-04-01 00:00:00'::timestamp + trunc(random() * 365) * '1 day'::interval + trunc(random() * 24) * '1 hour'::interval)
FROM generate_series(1,10000000);

テーブルの実ファイルの確認

$ ls -lh $PGDATA/base/16384/16386*
-rw------- 1 postgres postgres 731M May  1 13:10 /var/lib/postgresql/data/base/16384/16386    
-rw------- 1 postgres postgres 200K May  1 13:08 /var/lib/postgresql/data/base/16384/16386_fsm
-rw------- 1 postgres postgres  24K May  1 13:10 /var/lib/postgresql/data/base/16384/16386_vm 

700MBを超えました。空き領域マップ(fsm)、可視性マップ(vm)のファイルも作成されました。

68.1. データベースファイルのレイアウト

SELECT

sample=# EXPLAIN ANALYZE SELECT * FROM sample WHERE id = 1;
                                                       QUERY PLAN

-------------------------------------------------------------------------------------------------------------------------
 Gather  (cost=1000.00..146541.38 rows=1 width=45) (actual time=3.516..296.051 rows=1 loops=1)
   Workers Planned: 2
   Workers Launched: 2
   ->  Parallel Seq Scan on sample  (cost=0.00..145541.28 rows=1 width=45) (actual time=167.849..260.686 rows=0 loops=3)
         Filter: (id = 1)
         Rows Removed by Filter: 3333333
 Planning Time: 0.366 ms
 JIT:
   Functions: 6
   Options: Inlining false, Optimization false, Expressions true, Deforming true
   Timing: Generation 1.252 ms, Inlining 0.000 ms, Optimization 0.758 ms, Emission 8.043 ms, Total 10.054 ms
 Execution Time: 296.749 ms
(12 rows)

1000万件DELETE

idが1のもの1件だけを残して削除します。

sample=# DELETE FROM sample WHERE id != 1;
DELETE 10000000
sample=# SELECT COUNT(*) FROM sample;
-[ RECORD 1 ]-
count | 1
$ ls -lh $PGDATA/base/16384/16386*
-rw------- 1 postgres postgres 8.0K May  1 13:56 /var/lib/postgresql/data/base/16384/16386    
-rw------- 1 postgres postgres  24K May  1 13:56 /var/lib/postgresql/data/base/16384/16386_fsm
-rw------- 1 postgres postgres 8.0K May  1 13:56 /var/lib/postgresql/data/base/16384/16386_vm 

データ削除で、ファイルサイズは変わらないはずでしたが変わりました。参考記事の下の方に書いてる「VACUUMの例外ケース」です。

2022-05-01 13:56:57.315 JST [221] LOG:  automatic vacuum of table "sample.public.sample": index scans: 
0
        pages: 93457 removed, 1 remain, 0 skipped due to pins, 0 skipped frozen
        tuples: 4774018 removed, 1 remain, 0 are dead but not yet removable, oldest xmin: 741
        index scan not needed: 93458 pages from table (100.00% of total) had 10000000 dead item identifiers removed
        avg read rate: 30.876 MB/s, avg write rate: 15.100 MB/s
        buffer usage: 135949 hits, 144514 misses, 70674 dirtied
        WAL usage: 231536 records, 44621 full page images, 52825589 bytes
        system usage: CPU: user: 1.53 s, system: 1.32 s, elapsed: 36.56 s

あらためて1000万件INSERT、1件残しのDELETE

sample=# INSERT INTO sample(name, create_date) SELECT md5(clock_timestamp()::TEXT), ('2022-04-01 00:00:00'::timestamp + trunc(random() * 365) * '1 day'::interval + trunc(random() * 24) * '1 hour'::interval) FROM generate_series(1,10000000);
INSERT 0 10000000
$ ls -lh $PGDATA/base/16384/16386*
-rw------- 1 postgres postgres 731M May  1 17:10 /var/lib/postgresql/data/base/16384/16386
-rw------- 1 postgres postgres 200K May  1 17:10 /var/lib/postgresql/data/base/16384/16386_fsm
-rw------- 1 postgres postgres  24K May  1 17:10 /var/lib/postgresql/data/base/16384/16386_vm
sample=# DELETE FROM sample WHERE id != 30000001;
DELETE 9999999
sample=# SELECT COUNT(*) FROM sample;
-[ RECORD 1 ]-
count | 1
$ ls -lh $PGDATA/base/16384/16386*
-rw------- 1 postgres postgres 731M May  1 17:33 /var/lib/postgresql/data/base/16384/16386
-rw------- 1 postgres postgres 200K May  1 17:32 /var/lib/postgresql/data/base/16384/16386_fsm
-rw------- 1 postgres postgres  24K May  1 17:33 /var/lib/postgresql/data/base/16384/16386_vm

データ削除しましたがファイルサイズは変わりませんでした。期待した状況です。

自動VACUUMの確認

YouTubeの話になりますが、約1000万件削除したので「autovacuum_vacuum_scale_factor = 0.2」等により閾値を超えて自動VACUUMが動作していました。

ログで気にする項目
  • index scans: 1以外はワークメモリ不足してるので確認する
    • このテーブルはインデックスがないので「index scans: 0」ということかと思います
  • tuples: 削除できない不要行増えてないか
    • tuples: 9999999 removed なので問題なし
2022-05-01 17:33:10.474 JST [334] LOG:  automatic vacuum of table "sample.public.sample": index scans: 0
        pages: 0 removed, 93458 remain, 1 skipped due to pins, 0 skipped frozen
        tuples: 2858184 removed, 108 remain, 0 are dead but not yet removable, oldest xmin: 750
        index scan not needed: 93457 pages from table (100.00% of total) had 9999892 dead item identifiers removed
        avg read rate: 15.174 MB/s, avg write rate: 16.528 MB/s
        buffer usage: 113394 hits, 73608 misses, 80177 dirtied
        WAL usage: 213627 records, 26716 full page images, 43675683 bytes
        system usage: CPU: user: 1.57 s, system: 1.00 s, elapsed: 37.89 s
2022-05-01 17:33:11.128 JST [334] LOG:  automatic analyze of table "sample.public.sample"
        avg read rate: 298.308 MB/s, avg write rate: 0.060 MB/s
        buffer usage: 5312 hits, 24972 misses, 5 dirtied
        system usage: CPU: user: 0.01 s, system: 0.05 s, elapsed: 0.65 s
2022-05-01 17:33:32.600 JST [340] LOG:  automatic vacuum of table "sample.public.sample": index scans: 0
        pages: 0 removed, 93458 remain, 0 skipped due to pins, 93456 skipped frozen
        tuples: 0 removed, 4 remain, 0 are dead but not yet removable, oldest xmin: 751
        index scan not needed: 1 pages from table (0.00% of total) had 107 dead item identifiers removed
        avg read rate: 2.230 MB/s, avg write rate: 2.230 MB/s
        buffer usage: 89 hits, 1 misses, 1 dirtied
        WAL usage: 3 records, 0 full page images, 509 bytes
        system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s

pg_stat{all,sys,user}tables 統計情報ビューを確認

28.2. 統計情報コレクタ

以下が気にするポイントだとYouTubeで話していました。

  • n_dead_tup(不要行数)が定常的に増えていないか
  • 定期的に自動VACUUMMは行われているか
sample=# SELECT relname, n_tup_ins, n_tup_del, n_live_tup, n_dead_tup, last_autovacuum, autovacuum_count FROM pg_catalog.pg_stat_user_tables WHERE relname = 'sample';
-[ RECORD 1 ]----+------------------------------
relname          | sample
n_tup_ins        | 30000001
n_tup_del        | 39235147
n_live_tup       | 4
n_dead_tup       | 0
last_autovacuum  | 2022-05-01 17:33:32.599949+09
autovacuum_count | 7

あと、pg_stat_progress_vacuum 統計情報ビューでVACUUMの処理フェーズを確認できるとのことでした。

28.4. 進捗状況のレポート

ヒープ=テーブルのデータ部分とのこと
vacuuming indexesでmaintenance_work_memが不足しているとバキューム実行ごとに複数回発生する可能性があるというのが「index scans: 1」を確認することにつながるみたいです。
YouTubeの話しでは、不要行を1行回収するのに必要なメモリは6バイト、デフォルトの64MBでも約1000万行回収できるとのことでした。

定期的に自動VACUUMMは行われているかに関しては、競合するロックが要求されると自動VACUUMは自動的にキャンセルされるのでログに同じテーブルのキャンセルが続いていないか確認するとYouTubeで話してました。

ERROR: canceling autovacuum task
CONTEXT: automatic vacuum of table "xxxxxxxx"

再度1000万件INSERT

参考記事とは異なり自動VACUUMで不要領域が回収されているので、ここではファイルサイズは変わらないはずです。

sample=# INSERT INTO sample(name, create_date) SELECT md5(clock_timestamp()::TEXT), ('2022-04-01 00:00:00'::timestamp + trunc(random() * 365) * '1 day'::interval + trunc(random() * 24) * '1 hour'::interval) FROM generate_series(1,10000000);
INSERT 0 10000000
sample=# SELECT COUNT(*) FROM sample;
  count   
----------
 10000001
(1 row)

$ ls -lh $PGDATA/base/16384/16386*
-rw------- 1 postgres postgres 731M May  1 17:54 /var/lib/postgresql/data/base/16384/16386    
-rw------- 1 postgres postgres 200K May  1 17:53 /var/lib/postgresql/data/base/16384/16386_fsm
-rw------- 1 postgres postgres  24K May  1 17:53 /var/lib/postgresql/data/base/16384/16386_vm 

sample=# SELECT relname, n_tup_ins, n_tup_del, n_live_tup, n_dead_tup, last_autovacuum, autovacuum_count FROM pg_catalog.pg_stat_user_tables WHERE relname = 'sample';
-[ RECORD 1 ]----+------------------------------
relname          | sample
n_tup_ins        | 40000001
n_tup_del        | 39235147
n_live_tup       | 10000006
n_dead_tup       | 0
last_autovacuum  | 2022-05-01 17:53:55.123102+09
autovacuum_count | 8

sample=# EXPLAIN ANALYZE SELECT * FROM sample WHERE id = 39234530;
                                                       QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------        
 Gather  (cost=1000.00..146541.46 rows=1 width=45) (actual time=7.130..882.356 rows=1 loops=1)
   Workers Planned: 2
   Workers Launched: 2
   ->  Parallel Seq Scan on sample  (cost=0.00..145541.36 rows=1 width=45) (actual time=494.259..783.931 rows=0 loops=3)
         Filter: (id = 39234530)
         Rows Removed by Filter: 3333333
 Planning Time: 0.195 ms
 JIT:
   Functions: 6
   Options: Inlining false, Optimization false, Expressions true, Deforming true
   Timing: Generation 2.756 ms, Inlining 0.000 ms, Optimization 1.743 ms, Emission 20.110 ms, Total 24.609 ms
 Execution Time: 883.301 ms
(12 rows)

ファイルサイズに変わりはありませんでした。pg_stat_user_tables の値は期待したものと微妙に一致はしないものなのか。(統計情報更新されてないからか)
「VACUUM FULL」をやる前にいくつか試します。

autovacuum_vacuum_scale_factorに関して

19.10. 自動Vacuum作業

現状はpostgresql.confで「autovacuum_vacuum_scale_factor = 0.2」と設定しています。

テーブルごとにautovacuum_vacuum_scale_factorを調整

24.1. 定常的なバキューム作業

「バキューム閾値 = バキューム基礎閾値 + バキューム規模係数 * タプル数」

  • バキューム基礎閾値(autovacuum_vacuum_threshold=50)
  • バキューム規模係数(autovacuum_vacuum_scale_factor)
  • タプル数(pg_class.reltuples)
sample=# SELECT reltuples FROM pg_catalog.pg_class WHERE relname = 'sample';
   reltuples   
---------------
 1.0000006e+07

デフォルトのバキューム規模係数は「autovacuum_vacuum_scale_factor = 0.2」なので、少なめに設定して動作を確認します。

ALTER TABLE sample
  SET (autovacuum_vacuum_scale_factor = 0.001, autovacuum_analyze_scale_factor = 0.001)

10,050(バキューム閾値) = 50(バキューム基礎閾値) + 0.001(バキューム規模係数) * 1000万(タプル数)

sample=# SELECT COUNT(*) FROM sample;
  count   
----------
 10000001
(1 row)

sample=# DELETE FROM sample WHERE id IN (SELECT id FROM sample LIMIT 10000);
DELETE 10000

1万削除で自動VACUUMが実行されました。
autovacuum_vacuum_scale_factor に関しては定期的にデータ量を確認してその時々の適切な値を指定するのが正しい 運用ということですね。

VACUMM FULL

VACUMM FULLを行って、ファイルサイズが小さくなることを確認します。
※ 手順を色々と失敗したので仕切り直しています。

sample=# SELECT COUNT(*) FROM sample;
-[ RECORD 1 ]-
count | 912000
sample=# SELECT relname, n_tup_ins, n_tup_del, n_live_tup, n_dead_tup, last_autovacuum, autovacuum_count FROM pg_catalog.pg_stat_user_tables WHERE relname = 'sample';
-[ RECORD 1 ]----+-----------------------------
relname          | sample
n_tup_ins        | 50000001
n_tup_del        | 66575055
n_live_tup       | 891153
n_dead_tup       | 0
last_autovacuum  | 2022-05-01 21:16:31.92196+09
autovacuum_count | 25

sample=# SELECT datid, datname FROM pg_stat_database WHERE datname = 'sample';
-[ RECORD 1 ]---
datid   | 16384
datname | sample
sample=# SELECT relfilenode,relname FROM pg_class WHERE relname = 'sample';
-[ RECORD 1 ]-------
relfilenode | 16416
relname     | sample

$ ls -lh $PGDATA/base/16384/16416*
-rw------- 1 postgres postgres 797M May  1 21:17 /var/lib/postgresql/data/base/16384/16416
-rw------- 1 postgres postgres 224K May  1 21:15 /var/lib/postgresql/data/base/16384/16416_fsm
-rw------- 1 postgres postgres  32K May  1 21:15 /var/lib/postgresql/data/base/16384/16416_vm

# ここでsampleのバックアップを取得

sample=# VACUUM FULL sample;
VACUUM
sample=# SELECT relfilenode,relname FROM pg_class WHERE relname = 'sample';
-[ RECORD 1 ]-------
relfilenode | 16436
relname     | sample

$ ls -lh $PGDATA/base/16384/16436*
-rw------- 1 postgres postgres 67M May  1 21:24 /var/lib/postgresql/data/base/16384/16436

sample=# SELECT relname, n_tup_ins, n_tup_del, n_live_tup, n_dead_tup, last_autovacuum, autovacuum_count FROM pg_catalog.pg_stat_user_tables WHERE relname = 'sample';
-[ RECORD 1 ]----+-----------------------------
relname          | sample
n_tup_ins        | 50000001
n_tup_del        | 66575055
n_live_tup       | 891153
n_dead_tup       | 0
last_autovacuum  | 2022-05-01 21:16:31.92196+09
autovacuum_count | 25

sample=# EXPLAIN ANALYZE SELECT * FROM sample WHERE id = 49088002;
                                                      QUERY PLAN
----------------------------------------------------------------------------------------------------------------------      
 Gather  (cost=1000.00..14274.10 rows=1 width=45) (actual time=0.185..46.649 rows=1 loops=1)
   Workers Planned: 2
   Workers Launched: 2
   ->  Parallel Seq Scan on sample  (cost=0.00..13274.00 rows=1 width=45) (actual time=11.008..25.856 rows=0 loops=3)       
         Filter: (id = 49088002)
         Rows Removed by Filter: 304000
 Planning Time: 0.044 ms
 Execution Time: 46.666 ms
(8 rows)

VACUUM FULLによりファイルサイズが小さくなることを確認できました。
実は気になっていたので、論理バックアップ・リストアした場合にリストア先の状況がどうなっているのかを念のため確認します。

sample2データベースを作成してリストアします。論理バックアップはVACUUM FULL前に取得していました。

$ psql -U postgres -d sample2
sample2=# SELECT datid, datname FROM pg_stat_database WHERE datname = 'sample2';
-[ RECORD 1 ]----
datid   | 16441
datname | sample2

sample2=# SELECT relfilenode,relname FROM pg_class WHERE relname = 'sample';
-[ RECORD 1 ]-------
relfilenode | 16442
relname     | sample

$ ls -lh $PGDATA/base/16441/16442*
-rw------- 1 postgres postgres  67M May  1 21:30 /var/lib/postgresql/data/base/16441/16442
-rw------- 1 postgres postgres  40K May  1 21:29 /var/lib/postgresql/data/base/16441/16442_fsm
-rw------- 1 postgres postgres 8.0K May  1 21:30 /var/lib/postgresql/data/base/16441/16442_vm

sample2=# SELECT relname, n_tup_ins, n_tup_del, n_live_tup, n_dead_tup, last_autovacuum, autovacuum_count FROM pg_catalog.pg_stat_user_tables WHERE relname = 'sample';
-[ RECORD 1 ]----+------------------------------
relname          | sample
n_tup_ins        | 912000
n_tup_del        | 0
n_live_tup       | 912000
n_dead_tup       | 0
last_autovacuum  | 2022-05-01 21:30:39.399737+09
autovacuum_count | 1

不要領域は含まれないファイルサイズでした。また、n_tup_insやlast_autovacuum、autovacuum_countの値も初期化されていました。

終わりに

長くなりましたが、ドキュメント読むよりは体感的に動作が理解できました。
VACUUMは不要領域を再利用可能な状態に、VACUUM FULLはファイルサイズも小さくなります。ただ気軽にVACUMM FULLはできないという話。

GitHub - reorg/pg_repack: Reorganize tables in PostgreSQL databases with minimal locks

AWSコンテナ設計・構築[本格]入門のハンズオンやり終えた

AWSコンテナ設計・構築[本格]入門をようやく一通り終わったのでメモを残します。

最近、技術書を電子書籍(Kindle)で購入することも多くなってきたんですが、固定レイアウトはなかなか難しいですね。
固定レイアウトなら書籍で購入したほうが良いのかなと思いました。電子書籍良いんですが。

やってみた感想

普段の業務ではAWSは使ってないのですが、「AWS 認定ソリューションアーキテクト – アソシエイト」は一応取得しています。AWSのハンズオンをいくつかやっていて、なんとなくはEC2使うなら一通り構築できると思うことにしている感じの人です。
最初に必要なAWSの知識の章があり、とても勉強になりました。その後にハンズオンの流れになるので何やってるかわからんとならなかったです。
(ページを行ったり来たりしたいときは、書籍がいいのかなって感じました)
運用設計に限らないですが、色々と選択肢を示してくれるのが勉強になります。
AWSが公開している各種ドキュメントなどが紹介されていたので、読んで理解したいと思います。

github.com

awsstash.com

awesome-aws-workshops.com

docs.aws.amazon.com

docs.aws.amazon.com

docs.aws.amazon.com

docs.aws.amazon.com

AWS セキュリティドキュメント

aws.amazon.com

www.slideshare.net

注意点というかあらためて感じられたこと

  • ハンズオンで作成しているECSのタスクやRDSは課金されることが明白なので、1日でやりきれない場合は停止する
    • 4章終わるのに力量によると思いますが3時間はかかったみたいです
  • ELBは1つだと無料枠内ですが、2つ以上ある場合は無料枠超えてしまうので放置しないほうが良い
    • もともと1つ放置ELBがあったので1000円くらいになって気づきました
  • VPCエンドポイントもあるだけで課金されるので注意が必要でした。時間課金されるのでタイミングによって削除したほうが良い
    • 200円くらいで気づきました
  • あるあるかもですがRDSを停止していて次の週末に続きできないことがあり、気づいたら動き出して課金されるやつを自分がやるとは思いませんでした。気を付ける
    • 500円くらい持ってかれました。

書籍の中でも、しっかり説明されています。要件によってはNATゲートウェイが良いかもまで説明されています。

まとめ

ECSに特化してまとまった内容を学べるのは非常に良いと思いました。AWS Well-Architected フレームワーク沿ったものかを自分の手を動かすことで、資格勉強でなんとなく知った内容を深堀できたりしたのかなと思いました。
書籍内のハンズオン実施有無で無となっていた項目を含めて、あらためて時間のあるときにやろうと思います。

OpenRestyを使わずにnginxでCache Key Preprocessingしたい

OpenRestyで簡単にできるCache Key Preprocessing を素のnginxで使いたいだけです。
OpenResty使えよって気持ちはわかるのですが、追加のモジュールを入れるのは気が重いので使わない方法を調べてみました。

まずはOpenRestyを試してみる

準備

公式のイメージを利用します。

github.com

初期のdefault.confはコンテナから取得しました。

$ docker cp <コンテナID>:/etc/nginx/conf.d/default.conf conf.d/default.conf

以下のdocker-composeファイルでconfをマウントします。

version: "3"
services:
  openresty:
    image: openresty/openresty:1.19.9.1-6-alpine
    container_name: openresty-test
    ports:
      - "8081:80"
    volumes:
      - type: bind
        source: "./conf.d"
        target: "/etc/nginx/conf.d"

confの修正

Cache Key Preprocessingに記載されている以下の部分をdefault.confに追記します。

 location = /t {
     rewrite_by_lua '
         local args = ngx.req.get_uri_args()
         args.SID = nil
         args.UID = nil
         ngx.req.set_uri_args(args)
     ';

     echo $args;
 }

試す

$ docker-compose up -d
$ curl 'localhost:8081/t?RT=62&SID=BC3781C3-2E02-4A11-89CF-34E5CFE8B0EF&UID=44332&L=EN&M=1&H=1&UNC=0&SRC=LK'    
UNC=0&H=1&SRC=LK&L=EN&M=1&RT=62

ドキュメントにも書いてあるように、順番変わりましたがSIDとUIDがなくなりました。
OpenRestyを使えてしまえば、すごく簡単です。すごいです。
echoもだいぶ強力です。

nginxでやりたい

準備

公式のイメージを利用します。

github.com

初期のdefault.confはコンテナから取得しました。
あらためてlua-nginx-moduleecho-nginx-moduleも使わない話でした。

以下のdocker-composeファイルでconfをマウントします。

version: "3"
services:
  nginx:
    image: nginx:1.20.2-alpine
    container_name: nginx-test
    ports:
      - "8080:80"
    volumes:
      - type: bind
        source: "./conf.d"
        target: "/etc/nginx/conf.d"

confの修正(#1)

とりあえずレスポンスを返すことで、echoで確認できてたことを確認できるようにします。
default.confに追記します。

    location /t {
        set $_args $args;

        default_type application/json;
        return 200 '{"args": "$args", "_args": "$_args"}';
    }

試す(#1)

$ docker-compose up -d
$ curl 'localhost:8080/t?RT=62&SID=BC3781C3-2E02-4A11-89CF-34E5CFE8B0EF&UID=44332&L=EN&M=1&H=1&UNC=0&SRC=LK'
{"args": "RT=62&SID=BC3781C3-2E02-4A11-89CF-34E5CFE8B0EF&UID=44332&L=EN&M=1&H=1&UNC=0&SRC=LK", "_args": "RT=62&SID=BC3781C3-2E02-4A11-89CF-34E5CFE8B0EF&UID=44332&L=EN&M=1&H=1&UNC=0&SRC=LK"}

confの修正(#2)

recruit.gmo.jp

正規表現を使ってrewriteできれば良さそうです。

    location /t {
        set $_args $args;
        # SIDを削除
        if ($_args ~ (.*)SID=[^&]*(.*)) {
            set $_args $1$2;
        }
        # UIDを削除
        if ($_args ~ (.*)UID=[^&]*(.*)) {
            set $_args $1$2;
        }

        default_type application/json;
        return 200 '{"args": "$args", "_args": "$_args"}';
    }

試す(#2)

$ docker-compose up -d
$ curl 'localhost:8080/t?RT=62&SID=BC3781C3-2E02-4A11-89CF-34E5CFE8B0EF&UID=44332&L=EN&M=1&H=1&UNC=0&SRC=LK'
{"args": "RT=62&SID=BC3781C3-2E02-4A11-89CF-34E5CFE8B0EF&UID=44332&L=EN&M=1&H=1&UNC=0&SRC=LK", "_args": "RT=62&&&L=EN&M=1&H=1&UNC=0&SRC=LK"}

アンパサンド、正規表現力が足りてないとかあると思いますができました。これで良いことにしたいと思います。

proxy_cacheで使ってみる

confの修正

以下を適当に追記しました。

proxy_cache_path /var/cache/nginx/test levels=1:2 keys_zone=my-key:16m max_size=100m inactive=1d;
...
proxy_cache my-key;
proxy_cache_valid 200 302 1d;
proxy_cache_key "$host$uri$is_args$_args";
add_header X-Nginx-Cache $upstream_cache_status;

試す

# RT=62 => X-Nginx-Cache: MISS
$ curl -I 'localhost:8080/tpcache?RT=62&SID=BC3781C3-2E02-4A11-89CF-34E5CFE8B0EF&UID=44332&L=EN&M=1&H=1&UNC=0&SRC=LK'     
HTTP/1.1 200 OK
Server: nginx
Date: Mon, 21 Mar 2022 04:25:37 GMT
Content-Type: text/html
Content-Length: 612
Connection: keep-alive
Last-Modified: Tue, 16 Nov 2021 15:04:23 GMT
ETag: "6193c877-264"
X-Nginx-Cache: MISS
Accept-Ranges: bytes

# RT=62(最初と同じもの)=> X-Nginx-Cache: HIT
$ curl -I 'localhost:8080/tpcache?RT=62&SID=BC3781C3-2E02-4A11-89CF-34E5CFE8B0EF&UID=44332&L=EN&M=1&H=1&UNC=0&SRC=LK'     
HTTP/1.1 200 OK
Server: nginx
Date: Mon, 21 Mar 2022 04:25:53 GMT
Content-Type: text/html
Content-Length: 612
Connection: keep-alive
Last-Modified: Tue, 16 Nov 2021 15:04:23 GMT
ETag: "6193c877-264"
X-Nginx-Cache: HIT
Accept-Ranges: bytes

# RT=63 => X-Nginx-Cache: MISS
$ curl -I 'localhost:8080/tpcache?RT=63&SID=BC3781C3-2E02-4A11-89CF-34E5CFE8B0EF&UID=44332&L=EN&M=1&H=1&UNC=0&SRC=LK'     
HTTP/1.1 200 OK
Server: nginx
Date: Mon, 21 Mar 2022 04:26:22 GMT
Content-Type: text/html
Content-Length: 612
Connection: keep-alive
Last-Modified: Tue, 16 Nov 2021 15:04:23 GMT
ETag: "6193c877-264"
X-Nginx-Cache: MISS
Accept-Ranges: bytes

# RT=62, SID変更(BC37..→AD37..) => X-Nginx-Cache: HIT
$ curl -I 'localhost:8080/tpcache?RT=62&SID=AD3781C3-2E02-4A11-89CF-34E5CFE8B0EF&UID=44332&L=EN&M=1&H=1&UNC=0&SRC=LK'     
HTTP/1.1 200 OK
Server: nginx
Date: Mon, 21 Mar 2022 04:26:50 GMT
Content-Type: text/html
Content-Length: 612
Connection: keep-alive
Last-Modified: Tue, 16 Nov 2021 15:04:23 GMT
ETag: "6193c877-264"
X-Nginx-Cache: HIT
Accept-Ranges: bytes

# RT=62, UID変更(44332→55443) => X-Nginx-Cache: HIT
$ curl -I 'localhost:8080/tpcache?RT=62&SID=BC3781C3-2E02-4A11-89CF-34E5CFE8B0EF&UID=55443&L=EN&M=1&H=1&UNC=0&SRC=LK'     
HTTP/1.1 200 OK
Server: nginx
Date: Mon, 21 Mar 2022 04:27:19 GMT
Content-Type: text/html
Content-Length: 612
Connection: keep-alive
Last-Modified: Tue, 16 Nov 2021 15:04:23 GMT
ETag: "6193c877-264"
X-Nginx-Cache: HIT
Accept-Ranges: bytes

期待された形でX-Nginx-Cacheが返ってきました。良さそうです。
念のためキャッシュされたファイルも確認します。

$ docker exec -it nginx-test sh
$ cd /var/cache/nginx/test
$ ls
5  8
$ cat 8/62/2764b90faf9a7484b00c51d615edb628
...
KEY: localhost/tpcache?RT=62&&&L=EN&M=1&H=1&UNC=0&SRC=LK     
HTTP/1.1 200 OK
Server: nginx
Date: Mon, 21 Mar 2022 04:25:37 GMT
...

アンパサンド、とりあえずOKということで終わります。

SQLをいい感じに補完してほしい

SQLを書くときにテーブル名やカラム名を全部入力したり、タイポが怖くてコピー&ペーストしたりと今更ですが効率悪いことしてるなと思ったのでツールを見直そうと思いました。
実際はそこまで酷くはないのですが。

やりたいこと

  • テーブル名やカラム名などの補完をしてほしい
    • SQLキーワードは大文字になってほしい
  • 定義情報を簡単にみたい
  • できれば複数のデータベース(PostgreSQL, MySQL)に対応したい

試したツール

Googleで「sql 補完」を検索して5ページぐらいをざっくり見ました。 検索結果を少し考慮しつつ私の気持ちで候補を決めます。

  • pgAdmin 4
    • 個人的にPostgreSQLは割と使うことがあるので専用ツールですが許します。
  • DBeaver
    • 検索結果にあり、少し使ったことがある状況。
  • A5:SQL MK-2
    • 検索結果にあり、Windows環境が主なので候補に入ります。
  • DataGrip
    • 検索結果にあり、有償ツールの凄さか気になるので試します。
  • sqls
    • 検索結果にあり、Go Conferenceで見て気になってました(だいぶ経ってるが)

gocon.jp

評価の方法は、以下で書いたSQLの書き心地で決めます。
基本的にSELECT系のみというか色々試してたら時間がないのでSELECT系のみです。

tmfmym.hatenablog.com

pgAdmin 4

www.pgadmin.org

試したバージョン:pgAdmin 4 v6.7

普段はPostgreSQLを使うことが多いので、GUIのクライアントにpgAdmin 4が入っていたりするので使ってます。
ただ、SQLを実行させるために使うだけでここでたくさん書くことはなかったです。
今回試したバージョンはデスクトップアプリになってました(言い方あってるのか)。ブラウザで起動するものを使っていたので少しびっくりしました。バージョンを定期的に上げよう。勝手に上げられないことも多いですが。

使ってみて

「Query Tool」を使います

  1. デフォルトで「Ctrl + Space」で補完が行われます
  2. selで「SELECT」に補完できます
  3. froで「FROM」に補完でき、FROMの後はテーブルやスキーマやVIEWが補完候補となります
  4. FROMでテーブルなどが決まればカラム名は補完できる
    • テーブル名.カラム名は想定されてないっぽく、テーブル名.カラム名したいときはテーブル名までは書かないといけない
  5. 補完されない
    • latで「LATERAL」は補完できない
    • ASで別名つけたもののカラム名は補完できない
    • おまけでUNIONの後は、selで「SELECT」は補完できない
  6. 定義情報は補完時やホバーなどでは見られない

デスクトップアプリになって補完されるまでが早くなった気がしました。
関係ないけど、ERD Toolが増えてDBを右クリックのGenerate ERDでER図がとりあえず作成された。

DBeaver

dbeaver.io

試したバージョン:Community Edition 22.0

以下の設定でSQLキーワードは大文字、テーブル名などは小文字で補完できる形になった。

  • Preferences -> Editors -> SQL Editor
    • Code Completion
      • Insert caseはDefaultのままにする
      • Insert space after table/column namesをオフに変更する
    • Code Editor
      • Auto formatのConvert keyword caseをオフにしないと小文字になる
    • Formatting
      • Keyword caseをUpperに変更する

フォーマットの調整に時間がかかってしまいました。

使ってみて(pgAdmin 4との差分)
  • FROMでテーブル名.カラム名も問題ない感じ
  • ASで別名つけたもののカラム名は補完できる(サブクエリはできないみたい)
  • UNIONの後のselで「SELECT」は補完できる
  • 定義情報はカラムだと型やNot Nullかなどは補完時に確認できる

感覚的にはpgAdminより良いと思われる。

A5:SQL MK-2

a5m2.mmatsubara.com

試したバージョン: 2.17.1

DBeaverと同様に設定の微調整が必要でした。

  • オプション -> SQL整形
    • キーワードの大文字/小文字変換を大文字に変更する
使ってみて(DBeaverとの差分)
  • 補完時に定義情報はカラムだと型はみることできましたが、Not Nullかやデフォルト値はみれなかった
    • 逆にコメントを見られる

DataGrip

www.jetbrains.com

コード補完-機能 | DataGrip

使ってみて

個人でしかもホビー利用だとお高いですよね。ということでフォーマット系はひとまず気にせず。

  • latで「LATERAL」は補完できる
  • 補完時に定義情報はカラムだと型はみることできましたが、Not Nullかやデフォルト値はみれなかった
    • 補完時ではないがカラム名をホバーすると定義情報(alterなんちゃらやコメント)を確認できる
  • ASで別名つけたもののカラム名は補完できる(サブクエリも)

他のツールで届かなかった痒いところに届きました。補完もIDE使ってる感覚です。

sqls

github.com

VSCodeで使います。

GitHub - lighttiger2505/vscode-sqls: This extension adds language support for the SQL to VS Code.

Install sqls

$ go install github.com/lighttiger2505/sqls@v0.2.20

gccが必要でした(私の環境はWindowsです)
Download | tdm-gcc

$ mkdir -p ~/.config/sqls/
$ cat <<EOF > ~/.config/sqls/config.yml
# Set to true to use lowercase keywords instead of uppercase.
lowercaseKeywords: false
connections:
  - alias: test
    driver: postgresql
    proto: tcp
    user: postgres
    passwd: password
    host: 127.0.0.1
    port: 15432
    dbName: postgres
EOF

とりあえず上の感じでconfig配置すれば動きました。
あと、説明に書いてるようにsqlの拡張子のファイルに書く感じです。
(Open any *.sql file in VS Code. The extension is now activated.)
lowercaseKeywordsでtrue/falseいじってみましたが、selectなどのSQLキーワードが大文字にならず。

使ってみて(pgAdmin 4、DBeaverとの差分)
  • DBeaverと同じくFROMでテーブル名.カラム名も問題ない感じ
  • DBeaverと同じくASで別名つけたもののカラム名は補完できる(サブクエリはできないみたい)
  • DBeaverと同じく定義情報はカラムだと型は補完時に確認できる
    • sqlsだとホバー時にも定義情報みれる
  • DBeaverと同じくUNIONの後のselで「SELECT」は補完できる

設定等を理解できていないのですが、以下を解決できればVSCodeで使えるので一番良いかなと思っています。

  • SQLキーワードを大文字にしたい
  • config.ymlの切り替え

これから使おうと思ったツール

適当な項目で適当に順位をつけてみました。
(スコアって書いてるのは順位を合計しただけなので、小さいほうが良い)

補完 定義情報確認 複数DB対応 コスト スコア
pgAdmin 4 5 5 5 1 16
Dbeaver 2 2 1 1 6
A5:SQL MK-2 2 4 1 1 8
DataGrip 1 1 1 5 8
sqls 2 2 1 1 6

個人的な感想としては、(DataGrip) > sqls > DBeaver > A5:SQL MK-2 > pgAdmin 4 といった感じです。
DataGripがさすがに有償ですし、雰囲気違う感がありました。でも、そんな君SQL書かないよねと。
普段割と使っているエディタ(VSCode)上で実行できるといいと思ったので、もう少しsqlsの設定等を調べつつ使ってみたいなと思いました。