web-dev-qa-db-ja.com

PostgreSQL UDF(ユーザー定義関数)オーバーヘッド

免責事項

タスクは難解に思えるかもしれませんが、それでも、ある種のPOCを作成したいと思います。

目標

私の目標は、PostgreSQLデータベース(バージョン10)を使用するアプリケーションにAPIを公開することです。

APIは、UDFのセットの形式である必要があります。すべての関数は、アプリケーションにアクセスできる唯一のパブリックスキームに属しています。テーブルやその他のものはプライベートスキームに隠されています。 オブジェクト指向データベースとほとんど同じです。
これが私がそれを機能させようとしている理由です:

  • アプリケーションからデータベースを切り離すため、前者を再構築/最適化/非正規化して後者を破壊するリスクを軽減できます。メンテナンスを別のチームまたは部門に委任することもできます(oh my)
  • APIはサービスの要件を形式化します。データベースは確かにサービスですが、migrationsと呼ばれる従来のメカニズムは、そこで行われていることを理解するのに役立ちません。長年にわたって収集された何百または何千ものマイグレーションを考えてください。それらのいくつかは壊れており、再び機能することはありません。

まあ、気にしないでください。

問題

したがって、いくつかの非常に単純な関数(テーブルからすべてのレコードを取得するなど)を作成しようとしたとき、それらは常に、ラップするクエリよりも遅いと述べました。これは完全に受け入れ可能であり、それ自体で理解可能ですが、タイミングの違いは非常に大きくなる可能性があります。したがって、受け入れられません。

このようなテーブルがあります。

CREATE TABLE notifications (
    id SERIAL PRIMARY KEY,
    source_type INTEGER NOT NULL,
    content JSONB,
    created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
)

そして、120,000以上のレコード。
それらをすべて取得したいとします。
ここでは、単純なクエリを使用して実行します。インデックスはありません。JSONBデータはすべてのレコードでほぼ1kbです。

EXPLAIN (ANALYZE,VERBOSE,BUFFERS) SELECT * FROM private.notifications;
                                                         QUERY PLAN                                                              
-------------------------------------------------------------------------------------------------------------------------------------
Seq Scan on private.notifications  (cost=0.00..16216.13 rows=120113 width=877) (actual time=0.015..496.473 rows=120113 loops=1)
  Output: id, source_type, content, created
  Buffers: shared hit=15015
Planning time: 0.063 ms
Execution time: 973.935 ms

496ms。
では、次のようなpl/pgsql関数を利用してみましょう。

CREATE OR REPLACE FUNCTION notifications_get()
RETURNS SETOF private.notifications AS
$$
BEGIN
    RETURN QUERY SELECT * from private.notifications;
END
$$
LANGUAGE 'plpgsql' 
SECURITY DEFINER;

EXPLAIN (ANALYZE,VERBOSE,BUFFERS) SELECT * FROM notifications_get();

                                                            QUERY PLAN                                                             
-----------------------------------------------------------------------------------------------------------------------------------
Function Scan on notifications_get  (cost=0.25..10.25 rows=1000 width=48) (actual time=99.561..589.129 rows=120113 loops=1)
  Output: id, source_type, content, created
  Function Call: notifications_get()
  Buffers: shared hit=15015
Planning time: 0.045 ms
Execution time: 1091.698 ms

589ms。
明らかに、関数とクエリの違いは、最初のレコードの取得に費やされたこれらの99.5msです。
さらに最適化を試みました(たぶん素朴かもしれません):

  1. 行を調整して、クエリの計画をより現実的にします。 120kとしましょう。同じ結果が得られます(102.373..593.628)
  2. SQL言語を使用します(十分に公正で、クエリは単純です)。驚いたことに、同じ結果(95.760..595.746)
  3. 関数を安定させます。今すぐ良くなるはずですよね?いいえ。同じ結果(93.132..594.331)

ご質問

  1. (単純なクエリと比較して)関数のパフォーマンスを向上させるために他にできることはありますか?
  2. これらのトリックがどれも違いを生まなかったのはなぜですか?
  3. これらの最初の100msは正確には何ですか?これらは一定ではありません。テーブルに20k行ある場合、関数は最初にsomethingを実行しようとすると、不可解な18-20msを費やします。したがって、明らかにそれをすべての単一行テーブルで実行しようとします。この無駄を減らす、またはまったく取り除く方法は?これは可能ですか?

PS

私が直面したもう1つの問題は、IDでレコードを取得する関数にありました。 0.25ms vs 0.025ms。違いは10倍ですが、多かれ少なかれ、それはどこから来たのかを理解しています。繰り返しますが、上記の最適化のトリックで違いが生じたわけではありません(そうすべきではないようです)。

5
ash

これは問題の関数と(ほぼ)同等ですが、単純なSELECTのように機能します。

CREATE OR REPLACE FUNCTION notifications_get_faster()
  RETURNS SETOF private.notifications AS
$func$
SELECT * FROM private.notifications
$func$  LANGUAGE sql STABLE;

ほぼ、これはSECURITY DEFINERではないため、望ましい効果が得られません。

特に、クエリプランにはSeq ScanではなくFunction Scanが表示されます。それが違いのほとんどを作るものです。

どうして?

テーブル関数のインライン化all条件を満たしていません。 。この機能はありません。特に:

  • 関数はLANGUAGE SQLです

  • 関数はSECURITY DEFINERではありません

  • 関数がSTABLEまたはIMMUTABLEとして宣言されている

そのため、Postgresは関数本体を受け取り、関数のオーバーヘッドなしで実行できます(「関数のインライン化」)。プレーンなSELECTと比較して、ごくわずかな追加の計画コストが追加されるだけです。

余談ですが、言語名を引用しないでください。識別子です。

4