🐢

PostgreSQLでユーザー定義関数をつくってみよう

はじめに

フォルシアの高速検索を支える重要な技術のひとつである PostgreSQL には標準でも様々な関数が用意されていますが、独自にユーザ定義の関数をC言語で用意することも可能です。

C言語は処理が高速であることがメリットでありますが、一方で往々にしてコードが難解になりがちです。また、私も含め若いエンジニアはそもそもC言語にほとんど触ったことがないことも多いと思います。

そこで、今回は実際に手を動かしながら PostgreSQL で動作するC言語関数を実装するための課題を作成しました。これらの課題は先日社内の勉強会で実際に使用したものです。

事前準備として、PostgreSQL と gcc がお手元の環境で動くようにしてください。バージョンは問いません。

(インストールされた(されている) pgsql に include/server/postgres.h はありますか?もしなければ、開発用のモジュールを追加でインストールする必要があります。 ubuntu であれば sudo apt-get install postgresql-server-dev-all このコマンドで開発に必要なモジュールが /usr/include/postgresql/(version)/server/ 以下にインストールされるはずです)

課題1 echo関数をつくる

こちらはコピペでできるようにしました。
まずは全体の流れをおさえていただければと思います。

コード内にコメントもぎっしり書いておきました。
コードもコマンドも行数が少ないので、1行1行意味を確認しながら進めていただくとよいかと思います。

step1 任意のディレクトリに echo.c をつくる

echo.c
/*
ほとんどのPostgreSQLの内部型はpostgres.hに宣言されています。
一方、関数管理インタフェース(PG_FUNCTION_ARGSなど)はfmgr.hで宣言されています。
したがって、少なくともこの2つのファイルをincludeする必要があります。
https://www.postgresql.jp/document/14/html/xfunc-c.html#id-1.8.3.14.9

これらは /{path_to_pgsql}/pgsql/include/server 以下にあります。
(もしくは /usr/include/postgresql/(version)/server/ 以下にあります)
include時にはファイル名のみ書いておいて、
gccでコンパイル時に -I オプションで参照先ディレクトリを指定することができます。
*/
#include "postgres.h"
#include "fmgr.h"

/*
確実に、動的にロードされるモジュールが互換性がないサーバにロードされないように、
PostgreSQLは、そのファイルに適切な内容を持つ「マジックブロック」が含まれているかどうか検査します。
https://www.postgresql.jp/document/14/html/xfunc-c.html#id-1.8.3.14.6.11

2回以上記述するとエラーとなるので注意が必要です。複数ファイルを一つのsoファイルにまとめるときも全体で1回だけ記述します。
*/
PG_MODULE_MAGIC;

/*
現在、1つの呼び出し規約だけがC言語関数で使用されています(「version 1」)。
その呼び出し規約をサポートしていることは、以下に示すように、
その関数用に呼び出しマクロPG_FUNCTION_INFO_V1()を書くことで示されます。
https://www.postgresql.jp/document/14/html/xfunc-c.html#id-1.8.3.14.3
*/
PG_FUNCTION_INFO_V1(echo);

/*
Version-1呼び出し規約では、引数と結果の引き渡しの複雑さをなくすためにマクロを使用しています。
Version-1関数のC言語宣言は必ず下記のように行います。
Datum funcname(PG_FUNCTION_ARGS)
https://www.postgresql.jp/document/14/html/xfunc-c.html#id-1.8.3.14.8
*/
Datum echo(PG_FUNCTION_ARGS) {
	/*
	Version-1関数では、それぞれの実引数は、引数のデータ型に合ったPG_GETARG_xxx()マクロを使用して取り出されます。
	https://www.postgresql.jp/document/14/html/xfunc-c.html#id-1.8.3.14.8
    
	TEXT型はpostgresqlのカスタム型で pgsql/include/server/c.h で定義されています。制限なし可変長の文字列。
	*/
	text *input1 = PG_GETARG_TEXT_P(0);

	/*
	PG_RETURN_xxx()は、その引数として、実際に返す値を取ります。
	https://www.postgresql.jp/document/14/html/xfunc-c.html#id-1.8.3.14.8
	*/
	PG_RETURN_TEXT_P(input1);
}

コメントを消すとこれだけのコードです。

echo.c
#include "postgres.h"
#include "fmgr.h"

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(echo);

Datum echo(PG_FUNCTION_ARGS) {
	text *input1 = PG_GETARG_TEXT_P(0);
	PG_RETURN_TEXT_P(input1);
}

step2 コンパイルする

bash
gcc echo.c -shared -o echo.so -I /{path_to_pgsql}/pgsql/include/server

I オプションには postgres.h などの開発用モジュールが置いてあるディレクトリを指定します。
(もし postgresql-server-dev-all でインストールした場合は /usr/include/postgresql/(version)/server にあるはずです。)

step3 database を create する

postgres
CREATE DATABASE cfunction_study TEMPLATE template0;

step4 さきほどコンパイルしたファイルを読み込んで関数をつくる

postgres
CREATE OR REPLACE FUNCTION echo(text)
RETURNS text
AS '/{path_to_so_file}/echo.so'
LANGUAGE C
;

step5 関数を呼ぶ

postgres
SELECT echo('Yes We Can.');

どうでしょうか?思いのほか簡単にC言語関数が作れましたね!

課題2 ラーメンの税込み価格を教えてください

step1 ラーメンプライステーブルをつくる

postgres
DROP TABLE IF EXISTS ramen_price;
CREATE TABLE ramen_price (
	ramen_name text NOT NULL,
	tax_excluded_price integer NOT NULL,
	keigen_rate_flg character(1) NOT NULL
);
INSERT INTO ramen_price	VALUES
	(
		'CUP RAMEN'
		,168
		,'1'
	)
	,(
		'OMISE NO RAMEN'
		,836
		,'0'
	)
;

step2 税込み価格を教えてください

消費税は軽減税率フラグが1なら8%、0なら10%です。
端数は四捨五入してください。
以下のような結果が得たいです。

postgres
    ramen_name    | tax_included_price
------------------+--------------------
 CUP RAMEN        |                181
 OMISE NO RAMEN   |                920
(2 rows)
私の回答も載せておきます(もし行き詰ったら読んでみてください)

きれいではないかもしれませんが、一応正しく動作するものです。

回答1(フラグを数値にCAST)

add_tax.c
#include "postgres.h"
#include "fmgr.h"
#include "math.h" // 四捨五入でround関数を使いたい

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(add_tax); // PostgreSQLでは大文字小文字が区別されないので小文字で名前をつける

Datum add_tax(PG_FUNCTION_ARGS) {
	int32 tax_excluded_price = PG_GETARG_INT32(0);
	int32 keigen_rate_flg = PG_GETARG_INT32(1); // 文字列比較は大変なので数値で受け取る
	int32 ret;

	if (keigen_rate_flg == 1) {
		ret = round(tax_excluded_price * 1.08);
	} else {
		ret = round(tax_excluded_price * 1.10);
	}

	PG_RETURN_INT32(ret);
}
postgres
CREATE OR REPLACE FUNCTION add_tax(int4, int4)
RETURNS int4
AS '/{path_to_so_file}/add_tax.so'
LANGUAGE C
;

SELECT
	ramen_name
	,add_tax(
		tax_excluded_price
		,keigen_rate_flg::integer -- 文字列比較より数値比較のほうが簡単なのでCAST
	) AS tax_included_price
FROM ramen_price
;

回答2(フラグを文字列のまま処理)

add_tax2.c
#include "postgres.h"
#include "fmgr.h"
#include "math.h" // 四捨五入でround関数を使いたい
#include "string.h" // 文字列比較でstrcmp関数を使いたい
#include "utils/builtins.h" // textをC言語関数で扱える文字列に変換するためにtext_to_cstring関数を使いたい

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(add_tax2); // PostgreSQLでは大文字小文字が区別されないので小文字で名前をつける

Datum add_tax2(PG_FUNCTION_ARGS) {
	int32 tax_excluded_price = PG_GETARG_INT32(0);
	/*
	与えられたテキストオブジェクトをC文字列に変換
	https://www.postgresql.jp/document/15/html/spi-examples.html
	*/
	char *keigen_rate_flg = text_to_cstring(PG_GETARG_TEXT_PP(1));
	int32 ret;

	if (strcmp(keigen_rate_flg ,"1") == 0) { // 文字列比較(char)
		ret = round(tax_excluded_price * 1.08);
	} else {
		ret = round(tax_excluded_price * 1.10);
	}

	PG_RETURN_INT32(ret);
}
postgres
CREATE OR REPLACE FUNCTION add_tax2(int4, character)
RETURNS int4
AS '/{path_to_so_file}/add_tax2.so'
LANGUAGE C
;

SELECT
	ramen_name
	,add_tax2(
		tax_excluded_price
		,keigen_rate_flg -- 文字列のまま
	) AS tax_included_price
FROM ramen_price
;

課題3 Makefileをつくってコンパイルを簡単にする

さきほどまでの課題ではコンパイルを手動でやっていました。

bash
gcc echo.c -shared -o echo.so -I /{path_to_pgsql}/pgsql/include/server

こういうコマンドですね。
ただ、毎度このコマンドを打つのは面倒ですし、もしほかにオプションがたくさん必要なケースなどでは、「あれ?このファイルのコンパイルには何のオプションをつければいいのだっけ」となってしまいそうです。

PostgreSQL には pgxs という仕組みがあり、このあたりをいい感じにラップしてくれます。

さきほど add_tax.c(名前は任意ですが)を作ったディレクトリに Makefile という名前で以下のようなファイルを作成してください。

Makefile
MODULES = add_tax

PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

pg_config というのは通常 pgsql/bin 以下にありますが、そこにパスが通っている必要があります。

そして、さきほど手動でコンパイルして生成された add_tax.so は add_tax.so.original など適当な名前にリネームしておきましょう。

以下のコマンドを打ちます。

bash
make

これで add_tax.so が生成されたはずです。
そして、add_tax.so.original と中身も全く同じになっているはずです。

これは必要最小限の記述で、ほかにもいろいろオプションをつけることができます。詳しくは公式ドキュメントにて。
https://pgsql-jp.github.io/current/html/extend-pgxs.html

pgsql/lib 以下にインストール

ついでにもう一つやっておきましょう。
さきほど、add_tax.so は add_tax.c と同じディレクトリに生成されましたが、これを pgsql/lib 以下にインストール(コピーされるだけですが)しましょう。

bash
# postgresユーザーで実行(ほかのユーザーだと書き込み権限がないはず)
make install

つねにインストールするようにしておけば create function をするときに、pgsql/lib 以下を見に行けばよいので、「あのソースってどこに置いたっけ?」ということが起きなくなります。

課題4 extension化して誰でも簡単に使えるようにする

せっかく add_tax という素晴らしい関数をつくったので、これを社内のほかの人やあるいは世界中のひとに使ってもらいたい!と思ったとします。

いまのままでも一応は可能です。
add_tax.co と Makefile を送ってあげて、make と make install をしてもらったうえで、psql で以下を叩いてもらいます。

postgres
CREATE OR REPLACE FUNCTION add_tax(int4, int4)
RETURNS int4
AS '/{path_to_pgsql}/pgsql/lib/add_tax.so'
LANGUAGE C
;

でも、少し面倒ですね。これを叩いてね、ということをドキュメントなり readme なりで伝えてあげる必要があります。

と、いうことで extension の仕組みを使いましょう。

まず、設定ファイルを作ります。

add_tax.control
comment = 'add_tax is a function for adding consumption tax.'
default_version = '1.0'
module_pathname = '$libdir/add_tax'

何の extension かコメントをつけ、デフォルトのバージョンを指定し(extension 作成時にパラメータで任意のバージョンを指定可能)、モジュール(soファイル)の置き場を書いておきます。

次に create extention する部分を sql ファイルに書き出します。

add_tax--1.0.sql
CREATE OR REPLACE FUNCTION add_tax_extension(int4, int4)
RETURNS int4
AS 'MODULE_PATHNAME', 'add_tax'
LANGUAGE C
;

※ 関数名がさきほど作成したものと被るので _extension と付けました。同名の関数は2つ作れません。

AS 'MODULE_PATHNAME', 'add_tax' の MODULE_PATHNAME というのが、add_tax.control に記述した module_pathname に対応しています。そのパス以下の add_tax というファイルを読んでください、という意味になります。

そして最後に Makefile に以下の2行を追記します。

EXTENSION = add_tax
DATA = add_tax--1.0.sql

EXTENSION に control ファイル名を、DATA に sql ファイル名を指定します。

Makefile
MODULES = add_tax
EXTENSION = add_tax
DATA = add_tax--1.0.sql

PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

ここまでできたら postgres ユーザーになって make install しましょう。
pgsql/share/extension/ 以下に control と sql ファイルがインストールされましたね!

postgres を再起動して、create extension してみましょう。

postgres
cfunction_study=# create extension add_tax;
CREATE EXTENSION
cfunction_study=# SELECT
        ramen_name
        ,add_tax_extension(
                tax_excluded_price
                ,keigen_rate_flg::integer -- 文字列比較より数値比較のほうが簡単なのでCAST
        ) AS tax_included_price
FROM ramen_price
;
    ramen_name    | tax_included_price
------------------+--------------------
 CUP RAMEN        |                181
 OMISE NO RAMEN   |                920
(2 rows)

これで Makefile, add_tax--1.0.sql, add_tax.c, add_tax.control を tar なり zip なりで固めて人に渡して、あとは「make && make install」だけやってもらえば extension を使ってもらえる状態になりました。

ちなみに \dx コマンドで extension の一覧を確認することもできます。

postgres
cfunction_study=# \dx
                            List of installed extensions
  Name   | Version |   Schema   |                    Description
---------+---------+------------+---------------------------------------------------
 add_tax | 1.0     | public     | add_tax is a function for adding consumption tax.
 plpgsql | 1.0     | pg_catalog | PL/pgSQL procedural language
(2 rows)

さきほど control ファイルに書いたコメントもここで見られますので、ユーザーフレンドリーなコメントを書きましょう!

さらに \dx+ で、その extension で定義されている関数を見ることもできます。

postgres
cfunction_study=# \dx+ add_tax
       Objects in extension "add_tax"
             Object description
---------------------------------------------
 function add_tax_extension(integer,integer)
(1 row)

おわりに

いかがでしたでしょうか。これで PostgreSQL でC言語関数を書いて、それを広く使ってもらうための方法が一通りマスターできたと思います。あとは思い思いの関数を実装してみてください。

この記事を書いた人

長尾 和昌
元営業、元経理、元社長、今エンジニア。
珈琲と純文学が好き。

FORCIA Tech Blog

Discussion