PostgreSQL 10の使って嬉しい5つの機能(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

画像はすべて元記事からの引用です。

PostgreSQL 10の使って嬉しい5つの機能(翻訳)

ここ数年、RedisやMongoDBやCassandraやMemcachedやDynamoDBといったNoSQLデータベースを非常によく見かけました。これらは過分な評価を得たにもかかわらず、古きよきリレーショナルデータベースを置き換えるまでに至ることはほとんどありませんでした。オブジェクト-リレーショナル(これについてはまた別の機会にします)データベース管理システムであるPostgreSQLは、今も新たなファンを獲得し続けています。

本記事では、10月5日にリリースされたPostgreSQLの最新バージョンで導入された機能の例をいくつかご紹介します。記事で使われているクエリのほとんどは、PostgreSQLをUbuntu 16の上で動かしたクエリです。冒頭のIDカラムについての部分では、「かつて」PostgreSQL 9.6で行っていた場合との対比も示します。

環境設定

設定手順は比較的簡単です。最初に、ご利用のシステム設定に応じたDocker CEのインストールが必要です。

# PostgreSQL 9.6
$ docker pull postgres:9.6
$ docker run --name old-postgres -d postgres:9.6
$ docker run -it --rm --link old-postgres:postgres postgres:9.6 psql -h postgres -U postgres
# PostgreSQL 10
$ docker pull postgres:10
$ docker run --name new-postgres -d postgres:10
$ docker run -it --rm --link new-postgres:postgres postgres:10 psql -h postgres -U postgres

docker pullしてイメージを起動すると、利用中のPostgreSQLのバージョンが表示されます。

# PostgreSQL 9.6
$ docker pull postgres:9.6
$ docker run --name old-postgres -d postgres:9.6
$ docker run -it --rm --link old-postgres:postgres postgres:9.6 psql -h postgres -U postgres
# PostgreSQL 10
$ docker pull postgres:10
$ docker run --name new-postgres -d postgres:10
$ docker run -it --rm --link new-postgres:postgres postgres:10 psql -h postgres -U postgres

これで準備完了です。

1. IDカラム

MS SQL Serverから移ったときに今ひとつわからなかったのがこのIDカラムです。要するに、行のIDを保存するカラムであり、一意かつ自動でカウントアップされます。

-- PostgreSQL 9.6
CREATE TABLE foo (id SERIAL PRIMARY KEY, val1 INTEGER);
CREATE TABLE
\d
             List of relations
 Schema |    Name    |   Type   |  Owner   
--------+------------+----------+----------
 public | foo        | table    | postgres
 public | foo_id_seq | sequence | postgres
-- PostgreSQL 10
CREATE TABLE foo (id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, val1 INTEGER);
CREATE TABLE
\d
             List of relations
 Schema |    Name    |   Type   |  Owner   
--------+------------+----------+----------
 public | foo        | table    | postgres
 public | foo_id_seq | sequence | postgres

どちらのバージョンもよく似ており、sequenceも設定されています。そんなに大きな違いがあるのでしょうか?まず、新しい文法はSQLに準拠しているので、他のデータベースでも実行しやすいコードになります。さらに、INSERT時の挙動としてALWAYSDEFAULTのいずれかを明示的に指定できるようになりました。

もうひとつ便利なのは、sequenceの次の値の変更です。私もデータベース移行ではこの作業を嫌というほどやったものです。新しいバージョンでは、PostgreSQLのマジックのお陰で、テーブルで使われているsequenceについて特別な配慮が不要になりました。

-- PostgreSQL 9.6
ALTER SEQUENCE foo_id_seq RESTART WITH 1000;
ALTER SEQUENCE
-- PostgreSQL 10
ALTER TABLE foo ALTER COLUMN id RESTART WITH 1000;
ALTER TABLE

これでも腑に落ちないのであれば、私もこれまで知らなかった素晴らしい機能をもうひとつご紹介します。テーブルfooのコピーをひとつ作成したいとします。

-- PostgreSQL 9.6
CREATE TABLE bar (LIKE foo INCLUDING ALL);
CREATE TABLE
\d
             List of relations
 Schema |    Name    |   Type   |  Owner   
--------+------------+----------+----------
 public | bar        | table    | postgres
 public | foo        | table    | postgres
 public | foo_id_seq | sequence | postgres
-- PostgreSQL 10
CREATE TABLE foo (id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, val1 INTEGER);
CREATE TABLE
\d
             List of relations
 Schema |    Name    |   Type   |  Owner   
--------+------------+----------+----------
 public | foo        | table    | postgres
 public | foo_id_seq | sequence | postgres

この操作を行うとどちらのバージョンでも新しいテーブルが1つ作成されますが、PostgreSQL 10の場合だけ、sequenceも新たに作成されています。つまり、テーブルbarに新しい行をいくつかINSERTしたい場合、バージョン9.6ではidが1000(foo_id_seqを使った場合)になりますが、バージョン10ではidが1から開始されます。これは私にとっては望ましい結果です。さて、fooを削除しようとすると少々問題が起きることがあります。

DROP TABLE foo;
ERROR:  cannot drop table foo because other objects depend on it
DETAIL:  default for table bar column id depends on sequence foo_id_seq
HINT:  Use DROP ... CASCADE to drop the dependent objects too.
DROP TABLE foo;
DROP TABLE

sequenceが他で使われているため、fooは削除できません。かといって表示されるヒントに従おうとすると、barに値を挿入するのに手間がかかります。もし既存のテーブルを大量にコピーして新しいテーブルを作成していたとなれば大変な苦労が待ち構えています。

旧来の手法に潜むその他の怪物たちについて知りたい方は、ぜひこちらの記事をご覧ください。

2. ネイティブのパーティショニング機能

私はこの機能の強力さと応用範囲について非常に誇らしく感じています。この機能はまだ始まって間もないものですが、PostgreSQLにとっては使いやすさとパフォーマンスの面で9.6から大きく飛躍しました。

ところでパーティショニングとは何でしょう。いくつか例を挙げて説明します。

少し前、10Cloudsの同僚が機械学習によるスモッグレベルを予測する記事を書きました。この測定値をすべて保存するテーブルが1つあるとしましょう。最小限のモデルテーブルには、サンプル採取時のタイムスタンプ、サイトID、汚染の種類(CO2、PM10、PM2.5など)が含まれます。しばらくすると、テーブルのデータ総量は著しく増大し、パフォーマンスに影響が生じるでしょう。

もっとも頻繁に行われる計算の種類に応じて、データを分割してみます。月ごとの測定値のような別テーブルが欲しくなったら、パーティショニングの出番です。アプリからデータベースへのクエリが直近の2週間のレコードに限られるのであれば、全レコードをスキャンする代わりに、最大でも2つのパーティションをスキャンする必要があります。もちろん、スキャンするテーブルをクエリで指定することもできますが、正直申し上げるとこれは問題が生じやすいうえに美しくありません。

ここで達成したいのは次の2つです。

  • レベルの異なる抽象化の作成: クエリの対象は1つのテーブル(マスター)だけにしたい
  • サンプルのタイムスタンプに応じて、データを異なる子テーブルに分配したい

PostgreSQLでこれを行う手順は次のとおりです。

  1. マスターテーブルを作成する
  2. datetime制約を持つ子テーブルを必要な個数作成する
  3. 子テーブルにインデックス、キー、その他の制約を作成する
  4. マスターテーブルにトリガを設定し、適切な子テーブルに振り分けてからINSERTする

PostgreSQL 10からは第4項目が不要になりました。ここはDBMSがよしなにやってくれるので、文法が非常にシンプルになりました。サンプルの実装は次のような感じになります。

-- 1. マスターテーブルを作成し、パーティショニングルールを指定
CREATE TABLE measurement(
id INTEGER GENERATED ALWAYS AS IDENTITY,
datetime TIMESTAMPTZ,
site_id INTEGER,
pollutant_id INTEGER,
value FLOAT)
PARTITION BY RANGE (datetime);
-- 2. 子テーブルを複数作成し、保存するデータを制限するデータ範囲を定義
CREATE TABLE measurement_201708
PARTITION OF measurement(datetime)
FOR VALUES FROM ('2017-08-01') TO ('2017-09-01');
CREATE TABLE measurement_201709
PARTITION OF measurement(datetime)
FOR VALUES FROM ('2017-09-01') TO ('2017-10-01');
CREATE TABLE measurement_201710
PARTITION OF measurement(datetime)
FOR VALUES FROM ('2017-10-01') TO ('2017-11-01');
-- 3. 小テーブルごとにキーやインデックスを必要分追加
ALTER TABLE measurement_201708 ADD PRIMARY KEY (id);
ALTER TABLE measurement_201708 ADD CONSTRAINT fk_measurement_201708_site FOREIGN KEY (site_id) REFERENCES site(id);
CREATE INDEX idx_measurement_201708_datetime ON measurement_201708(datetime);

発生場所(sites)と汚染物質(pollutants)のテーブルは既に作成済みとします。以下は、測定値(measurement)テーブルにデータを追加してクエリをかけています。

-- INSERTは通常のテーブルと同じようにできる
INSERT INTO measurement(datetime, site_id, pollutant_id, value)
SELECT '2017-08-01'::TIMESTAMPTZ + ((random()*90)::int) * INTERVAL '1 day',
(1 + random()*(SELECT max(id)-1 FROM site))::int,
(1 + random()*(SELECT max(id)-1 FROM pollutant))::int,
random()
FROM generate_series(1,1000000);
-- データのSELECTも通常と同じようにできる
SELECT * FROM measurement WHERE datetime BETWEEN '2017-09-20' AND '2017-09-27';

特定範囲のデータや特定カテゴリに属するデータを簡単にパーティショニングできる構文が新しく提供されました。つまり、別のアプローチとしてどんな地域の測定値についても個別のテーブルを作成できるということです。パーティションの1つが用済みになったら、マスターテーブルからパーティションを簡単に切り離してアーカイブできます。ここでおそらくもっとも重要な点は、ネイティブパーティショニングのパフォーマンスが大きく向上したことです。これについてはdepeszが詳しく調べたブログ記事がありますので、一読をおすすめします。

一方、パーティショニングにはまだ問題がいろいろ残っています。キーやインデックスを子テーブルごとに設定しなければならないとか、異なる子テーブル全体で一意のキーを設定できないなどです。しかしパーティショニングについては懸命な作業が行われているので、今後のリリースに期待したいと思います。

3. マルチカラム統計

この機能は、過小評価された実行プランがたくさんあるデータを扱う多くの方にとって救いとなるかもしれません。架空の例として、1000人の子どもに数え歌を歌わせる(count rhymes)学級を持つビジネスを考えてみましょう。子どもたちは(強制ではなく)皆好きでやっているとします。そして何かトチ狂った理由に基いて、どの子どもがどの単語を口にしたかという情報を保存したいとします。あなたは自分が何を作っているかを知っていて、理由は聞かずにひたすらコードを書くものとします。データの保存と取り出しは次のような感じになるでしょう。

訳注: 数え歌は、日本のわらべ唄の「どちらにしようかな」に相当します。

CREATE TABLE counting_log (id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, datetime TIMESTAMP WITH TIME ZONE, child_id INTEGER, word TEXT);
CREATE TABLE
INSERT INTO counting_log(datetime, child_id, word) 
SELECT current_timestamp, i%1000, 
CASE WHEN i%4=1 THEN 'eeny' 
WHEN i%4=2 THEN 'meeny' 
WHEN i%4=3 THEN 'miny' 
WHEN i%4=0 THEN 'moe' 
ELSE 'nope' END 
FROM generate_series(1, 1000000) i;
INSERT 0 1000000
CREATE INDEX idx_counting_log_child_id on counting_log(child_id);
CREATE INDEX
CREATE INDEX idx_counting_log_datetime on counting_log(datetime);
CREATE INDEX

準備が整い、「idが123より小さい子どもが「miny」と歌ったら取り出すこと」と業務命令が下ります。PostgreSQLでは普通次のようにします。

  1. child_id=123という述語で総行数(p1)のうちどのぐらいの割合で返されるかを見積もる
  2. word = 'miny'という述語で総行数(p2)のうちどのぐらいの割合で返されるかを見積もる
  3. 両方の述語を使う行の総行数がtotal_rows*p1*p2に等しいと仮定する

この計算方法で正確な行数が返されるのは、おそらくこれらの述語のカラムが相関していない場合に限られます。ここでは相関がないことについてはおおよそ自明なので、見積もりは正確な結果を下回るでしょう。

しかし相関統計を導入すればこの点を改善できることがあります。使い方を見てみましょう。

CREATE STATISTICS st_counting_log_child_id_word ON child_id, word FROM counting_log;
CREATE STATISTICS
ANALYZE counting_log;
ANALYZE

例の表示は逆順になっています。まずは、新しく作った統計の見積もりを見てみましょう。

EXPLAIN SELECT datetime FROM counting_log WHERE child_id=123 AND word='miny';
                                        QUERY PLAN                                         
-------------------------------------------------------------------------------------------
 Bitmap Heap Scan on counting_log  (cost=19.92..2696.34 rows=967 width=8)
   Recheck Cond: (child_id = 123)
   Filter: (word = 'miny'::text)
   ->  Bitmap Index Scan on idx_counting_log_child_id  (cost=0.00..19.68 rows=967 width=0)
         Index Cond: (child_id = 123)

返される行数は967行であると予測されました。古い方法だとどう変わるでしょうか。

DROP STATISTICS st_counting_log_child_id_word;
DROP STATISTICS
EXPLAIN SELECT datetime FROM counting_log WHERE child_id=123;
                                        QUERY PLAN                                         
-------------------------------------------------------------------------------------------
 Bitmap Heap Scan on counting_log  (cost=19.92..2693.92 rows=967 width=8)
   Recheck Cond: (child_id = 123)
   ->  Bitmap Index Scan on idx_counting_log_child_id  (cost=0.00..19.68 rows=967 width=0)
         Index Cond: (child_id = 123)
EXPLAIN SELECT datetime FROM counting_log WHERE word='miny';
                             QUERY PLAN                              
---------------------------------------------------------------------
 Seq Scan on counting_log  (cost=0.00..19695.00 rows=202133 width=8)
   Filter: (word = 'miny'::text)

ここで少々算数のお時間です。

返される行数 = 総行数 * p1 * p2 = 1000000 * (967/1000000) * (202133/1000000) = 195.46

予測値が出ました。さて、PostgreSQLの回答ではいくつになるでしょうか。

EXPLAIN SELECT datetime FROM counting_log WHERE child_id=123 AND word='miny';
                                        QUERY PLAN                                         
-------------------------------------------------------------------------------------------
 Bitmap Heap Scan on counting_log  (cost=19.73..2696.14 rows=195 width=8)
   Recheck Cond: (child_id = 123)
   Filter: (word = 'miny'::text)
   ->  Bitmap Index Scan on idx_counting_log_child_id  (cost=0.00..19.68 rows=967 width=0)
         Index Cond: (child_id = 123)

ドンピシャリでした。

もうひとつチェックしてみましょう。ここまでは予測値だけを扱っていましたが、実際に返される行数はどうでしょう。

SELECT count(datetime) FROM counting_log WHERE child_id=123 AND word='miny';
 count 
-------
  1000

本項のまとめ: この例から、データを熟知していればそこから多くの成果を得られることがわかります。相関を無視してしまうと、予測を大きく下回ってしまいます(およそ1桁違い)。ここではそれほど深刻には見えないかもしれませんが、子どもたちのデータを保存する別のテーブルを結合(JOIN)する場合を考えていましょう。そのようなプランでは、ネステッドループ結合の場合(実際にはハッシュ結合すべき場合)が最もコストが小さいと考えられます。相関するマルチカラム統計に保存されるデータについてもっと知りたい方は、次を入力してみてください。

SELECT * FROM pg_statistic_ext WHERE stxname = 'st_counting_log_child_id_word' \gx
-[ RECORD 1 ]---+------------------------------
stxrelid        | 16555
stxname         | st_counting_log_child_id_word
stxnamespace    | 2200
stxowner        | 16385
stxkeys         | 3 4
stxkind         | {d,f}
stxndistinct    | {"3, 4": 1000}
stxdependencies | {"3 => 4": 1.000000}

本記事ではこれ以上立ち入りませんので、stxkeysがカラム数に対応する(2つより多く指定可能)ことと、stxdistinctstxdependenciesという2種類の統計が行われることだけ押さえておけば十分です。

ところで、今入力したクエリの末尾がセミコロンではなく\gxになっていたことにお気づきでしょうか。これも新機能の1つです😉

4. 並列性の強化

並列クエリの導入が始まったのはPostgreSQL 9.6からです。以来、シーケンシャルなスキャン、ハッシュ、ネステッドループJOIN戦略、集計を並列実行できるようになっていました。今回からは、マージ結合やビットマップヒープスキャン、そしておそらくもっとも重要なインデックススキャンとインデックスのみのスキャンも並列実行できるようになりました。

Postgresql.confファイルに並列クエリを用いるための新しい設定があるのはこのためです。

  • min_parallel_table_scan_size: 並列実行をトリガするテーブルの最小サイズ(デフォルトは8 MB)
  • min_parallel_index_scan_size: 上の対象がインデックスである以外は同様(デフォルトは512 kB)
  • max_parallel_workers: 使用する並列ワーカーの最大数(デフォルトは8)。PostgreSQLの以前のバージョンで導入されていたのはmax_parallel_workers_per_gatherなのでご注意ください。

これらの設定は何に使うのでしょうか。すべてはコスト次第です。並列ワーカーを設定しても、並列ワーカーなしでクエリを実行できるリソースを上回っていればまったく効果がないことがあります。繰り返しになりますが、予測されるコストが1000を上回る場合(これはparallel_setup_cost設定に対応)、デフォルト設定のクエリが複数ワーカーで実行されるよう(クエリ)プランナーで配慮されます。

私がPostgreSQL 10で多少経験した範囲では、新しい並列ワーカーは少々恥ずかしがり屋な生き物であると感じました。実際の動作を見てみましょう。さほど面白い例ではありませんが、次のように三角関数の値を保存してみます。

CREATE TABLE trigonometry AS SELECT i AS arg, sin(i) AS sine, cos(i) AS cosine, tan(i) AS tangent FROM generate_series(0, 100000, 0.01) i;
SELECT 10000001
CREATE INDEX idx_trigonometry_arg on trigonometry(arg);
CREATE INDEX
create index idx_trigonometry_sine on trigonometry(sine);
CREATE INDEX
create index idx_trigonometry_cosine on trigonometry(cosine);
CREATE INDEX

まず、9.6で導入されたいくつかの集計関数を使ってみましょう。

EXPLAIN SELECT count(arg) FROM trigonometry WHERE arg > 50000;
                                            QUERY PLAN                                            
--------------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=140598.88..140598.89 rows=1 width=8)
   ->  Gather  (cost=140598.67..140598.88 rows=2 width=8)
         Workers Planned: 2
         ->  Partial Aggregate  (cost=139598.67..139598.68 rows=1 width=8)
               ->  Parallel Seq Scan on trigonometry  (cost=0.00..134437.55 rows=2064449 width=8)
                     Filter: (arg > '50000'::numeric)

簡単ですね。予測コストがかなり大きくなったので、プランナーはワーカーを2つ追加することにしました。

次は並列インデックススキャンを試すことにしましょう。

EXPLAIN SELECT * FROM trigonometry WHERE arg > 50000;
                                             QUERY PLAN                                              
-----------------------------------------------------------------------------------------------------
 Index Scan using idx_trigonometry_arg on trigonometry  (cost=0.43..201369.28 rows=4954677 width=32)
   Index Cond: (arg > '50000'::numeric)

だめですね。別のワーカーを追加するコストを少し下げてあげるとどうなるでしょうか。

SET parallel_setup_cost=100;
SET
EXPLAIN SELECT * FROM trigonometry WHERE arg > 50000;
                                             QUERY PLAN                                              
-----------------------------------------------------------------------------------------------------
 Index Scan using idx_trigonometry_arg on trigonometry  (cost=0.43..201367.27 rows=4954562 width=32)
   Index Cond: (arg > '50000'::numeric)

まだ変わりません。これは私にとって少々驚きでした。予測コストは高く、テーブルやインデックスのサイズは閾値を十分上回っているのに…今度はクエリを変えて試してみましょう。

SET parallel_setup_cost=1000;
SET
EXPLAIN SELECT arg FROM trigonometry WHERE sine > 0.999 AND arg >100 AND arg < 10000;
                                                  QUERY PLAN                                                   
---------------------------------------------------------------------------------------------------------------
 Gather  (cost=1000.43..40801.85 rows=13403 width=8)
   Workers Planned: 2
   ->  Parallel Index Scan using idx_trigonometry_arg on trigonometry  (cost=0.43..38461.55 rows=5585 width=8)
         Index Cond: ((arg > '100'::numeric) AND (arg < '10000'::numeric))
         Filter: (sine > '0.999'::double precision)

ついにやりました。Parallel Index Scanが輝かしい姿を現したのです。私がこのクエリを数回実行してみたところ、平均で162msでした。もう少し遊んでみましょう。並列実行を強制し、かつ追加ワーカーを0にしてみました。

SET max_parallel_workers =0;
SET
SET force_parallel_mode=on;
SET
EXPLAIN ANALYZE SELECT arg FROM trigonometry WHERE sine > 0.999 AND arg >100 AND arg < 10000;
                                                                          QUERY PLAN                                                                           
---------------------------------------------------------------------------------------------------------------------------------------------------------------
 Gather  (cost=1000.43..40801.85 rows=13403 width=8) (actual time=0.265..263.796 rows=14097 loops=1)
   Workers Planned: 2
   Workers Launched: 0
   ->  Parallel Index Scan using idx_trigonometry_arg on trigonometry  (cost=0.43..38461.55 rows=5585 width=8) (actual time=0.072..262.133 rows=14097 loops=1)
         Index Cond: ((arg > '100'::numeric) AND (arg < '10000'::numeric))
         Filter: (sine > '0.999'::double precision)
         Rows Removed by Filter: 975902
 Planning time: 0.164 ms
 Execution time: 264.473 ms

まず、このクエリがちゃんと実行されていることがわかります。起動されたワーカー数が0に等しいようなので、追加のワーカーは存在しないということになりますが、その点は私にははっきりとはわからなかったことをお断りしておきます。

次に、利用可能なプロセス数を制限した場合、プランナーは実際に利用できる数より多くのプロセス数を求めるかもしれません。最後に、上のクエリの結果は平均して260ms後に返されたので、並列インデックススキャンを使った場合の実行結果は速くなっています。総合すると、これは素晴らしい結果です。多くの人はこの機能の存在に気づくことはないかもしれませんが。

5. JSONとJSONBの全文検索

いよいよ最後ですが、これも重要です。JSON型やJSONB型のカラムで全文検索がサポートされるようになりました。textカラムで全文検索が使える仕組みに親しんでいる方には大したことないように思えるかもしれませんが、PostgreSQL 10ですぐ使える素敵な機能であることに違いはありません。

支払い管理に外部のシステムを使っているとしましょう。ほとんどのデータは通常のカラムに保存しますが、安全のためにシステムからのレスポンスをJSONカラムにも保存しています。これは次のような感じになります。

CREATE TABLE transaction(id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, transaction_id VARCHAR(10), user_id INTEGER, created_datetime TIMESTAMP WITH TIME ZONE, result BOOL, amount INT ,response_data JSON);
CREATE
INSERT INTO transaction(transaction_id, user_id, created_datetime, result, amount, response_data)
SELECT tran.id, ceil(random()*100), tran.datetime, tran.result, tran.amount, 
('{"transaction": {"id": "'|| tran.id ||'", "transaction_datetime": "'|| tran.datetime || '","amount": '|| tran.amount::text || ',"is_success": "'|| tran.result || '", "message": "'|| tran.msg || '"}}')::json
FROM (
    SELECT substring(md5(random()::text), 1, 10) as id, 
        current_timestamp + (ceil(random()*1000)-500) * INTERVAL '1 minute' as datetime,
        ceil(random()*1000) as amount,
        NOT (i%3=1) as result,
        CASE WHEN i%9=1 THEN 'insufficient funds' WHEN i%9=4 THEN 'blocked account' WHEN i%9=7 THEN 'fraud detected' ELSE 'accepted' END as msg
    FROM generate_series(1,1000) i) tran;
INSERT 0 1000

これは簡単な例ですが、テーブルが著しく増大する場合は、次のようにJSONカラムにインデックスを作るとよいかもしれません。

CREATE INDEX idx_transaction_response_data ON transaction USING GIN (to_tsvector('english', response_data));
CREATE INDEX

テーブルにデータが入りましたので、たとえばレスポンスメッセージが「insufficient funds」の場合のクエリ方法を見てみましょう。新しいインデックスは効くでしょうか。

SELECT transaction_id FROM transaction WHERE to_tsvector('english', response_data) @@ to_tsquery('english', 'insufficient') LIMIT 5;
 transaction_id 
----------------
 b114b0927f
 28613b40c6
 649b9d3285
 ce9c16ec6f
 24f69de12e
EXPLAIN SELECT transaction_id FROM transaction WHERE to_tsvector('english', response_data) @@ to_tsquery('english', 'insufficient') LIMIT 5;
                                                QUERY PLAN                                                
----------------------------------------------------------------------------------------------------------
 Limit  (cost=8.04..23.12 rows=5 width=11)
   ->  Bitmap Heap Scan on transaction  (cost=8.04..23.12 rows=5 width=11)
         Recheck Cond: (to_tsvector('english'::regconfig, response_data) @@ '''insuffici'''::tsquery)
         ->  Bitmap Index Scan on idx_transaction_response_data  (cost=0.00..8.04 rows=5 width=0)
               Index Cond: (to_tsvector('english'::regconfig, response_data) @@ '''insuffici'''::tsquery)

素晴らしい、実にクールです。なお、効率のよいクエリにするためには「insufficient」の語幹だけを取り出した「insuffici」(語彙素: lexeme)に変換する必要があります。

これで全部?

いいえ、全貌に迫ったとは言えません。冒頭でお断りしたとおり、PostgreSQL 10では膨大な新機能が導入されたため、宣伝が大きく先走っています。ご紹介した5つの嬉しい機能はほんの一部に過ぎませんが、あえてこれらを選んだのは、(意識するかどうかにかかわらず)最も頻繁に使われると信じているからです。

紹介しなかった機能には次のものがあります。

  • 論理レプリケーション
  • 同期レプリケーション用のクォーラム(quorum)コミット
  • XMLテーブル
  • SCRAM認証
  • 一部の関数名の変更
  • psqlで利用できる\if\elif\else

他にもまだまだあります。

すべての新機能についてはPostgreSQL Wikiをご覧ください。PostgreSQL 10の次のバージョンが待ち遠しい方は、depeszのブログをご覧ください。こちらでは早くも次期PostgreSQL 11の機能の紹介が始まっています。

関連記事(PostgreSQL)

PostgreSQLの機能と便利技トップ10(2016年版)(翻訳)

Rails開発者のためのPostgreSQLの便利技(翻訳)

[Rails] RubyistのためのPostgreSQL EXPLAINガイド(翻訳)

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833

コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。
これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。
かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。
実は最近Go言語が好き。
仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

BPSアドベントカレンダー

週刊Railsウォッチ

インフラ

BigBinary記事より

ActiveSupport探訪シリーズ