スキャンしたドキュメントがたくさんあるPostgreSQLデータベースがdocument bytea
テーブルscans
の列。数十万のドキュメントがあり、大きくてバックアップに不便でした。また、重複率は高く、メジャーではなく5〜10%でした。
これらのドキュメントをデータベースの外部に保存して、インクリメンタルtarを介してバックアップできるようにし、pg_dumpデータベースバックアップのサイズを縮小したいと思いました。
Plperluを使用して、以下で共有したいソリューションを思いつきました。コメントやさらなる最適化のアイデアは大歓迎です!
PostgreSQLには、データベースの外部にBLOBを格納する方法があります。彼らは Large Objects と呼びました。あなたはそれらがあなたのニーズに合うと思うかもしれません。内部的には、彼らはこれを非常に行っています。 OIDを使用する場合を除きます。 md5sumなどをデータベースの行に保存できます。
次のソリューションでは、blobを外部に保存/取得し、重複を回避して、blob自体のmd5ハッシュにちなんで名付けられたフォルダーとファイルのツリーに保存できます。 。すべてのファイルはフォルダ$PGDATA/extstore
に保存されます。
ドキュメントは$PGDATA/extstore/db/zzz/yy/xxxxxx
に保存されます。ここで、db
は現在のデータベース名、zzz
は特定の個別のフォルダ(オプション)、yy
はの最初のX桁ですブロブのmd5ハッシュであり、xxxxxx
はブロブのmd5ハッシュです。 Xの値が高いほど、フォルダーあたりのファイル数は少なくなります。つまり、X = 1の場合、すべてのファイルは16個のフォルダー(0
からf
という名前)に分散されます。すべてのファイルは256個のフォルダー(00
からff
という名前)に分散されます。
2つの異なるファイルが同じMD5ハッシュを生成する可能性があります(したがって、一方が他方を上書きする可能性があります)が、その可能性は確かに非常に小さく、2 ^ 128に1つです。不可能ではありませんが、信じられないほどありそうにありません。
次のすべての関数を、extstore
という別のスキーマで作成しました。
CREATE SCHEMA IF NOT EXISTS extstore;
次の関数は、「extstore」リポジトリ内にファイルを保存します。
SET ROLE postgres;
CREATE OR REPLACE FUNCTION extstore.f_save_file_pl(text, int, text, bytea) RETURNS void AS $$
use strict;
my($folder, $level, $md5, $contents) = @_;
my $rv = spi_exec_query('SELECT current_catalog AS db');
my $db = $rv->{rows}[0]->{'db'};
$md5 = lc $md5;
$md5 =~ '^[0-9a-f]{32}$' or die 'Malformed MD5';
$level >= 0 && $level < 4 or die 'Invalid level';
my $err = 'Cannot create folder ';
my $dir = 'extstore';
if (! -d $dir) { mkdir $dir or die "$err$dir"; }
$dir .= "/$db";
if (! -d $dir) { mkdir $dir or die "$err$dir"; }
if ($folder) {
$folder =~ s/\//-/g;
$dir .= "/$folder";
if (! -d $dir) { mkdir $dir or die "$err$dir"; }
}
if ($level) {
my $subf = substr $md5, 0, $level;
$dir .= "/$subf";
if (! -d $dir) { mkdir $dir or die "$err$dir"; }
}
my $file = "$dir/$md5";
if (! -f $file) {
open(my $out, '>:raw', $file) or die "Unable to create file $file: $!";
print $out decode_bytea($contents);
close($out);
}
return;
$$ LANGUAGE plperlu SECURITY DEFINER;
CREATE OR REPLACE FUNCTION extstore.f_save_file(p_folder text, p_level int, p_data bytea) RETURNS text AS $$
SELECT md5
FROM (
SELECT md5, extstore.f_save_file_pl($1, $2, md5, $3) AS none
FROM (
SELECT md5($3) AS md5
) a
) a;
$$ LANGUAGE SQL SECURITY DEFINER;
次の関数は、「extstore」リポジトリからファイルを読み取ります。
SET ROLE postgres;
CREATE OR REPLACE FUNCTION extstore.f_read_file_pl(text, int, text) RETURNS bytea AS $$
use strict;
my($folder, $level, $md5) = @_;
my $rv = spi_exec_query('SELECT current_catalog AS db');
my $db = $rv->{rows}[0]->{'db'};
$md5 = lc $md5;
$md5 =~ '^[0-9a-f]{32}$' or die 'Malformed MD5';
$level >= 0 && $level < 4 or die 'Invalid level';
my $dir = 'extstore';
if (! -d $dir) { return undef; }
$dir .= "/$db";
-d $dir or return $dir;
if ($folder) {
$folder =~ s/\//-/g;
$dir .= "/$folder";
-d $dir or return undef;
}
if ($level) {
my $subf = substr $md5, 0, $level;
$dir .= "/$subf";
-d $dir or return undef;
}
my $file = "$dir/$md5";
-f $file or return undef;
open(my $out, '<:raw', $file) or die "Unable to open file $file: $!";
local $/ = undef;
my $contents = encode_bytea(<$out>);
close($out);
return $contents;
$$ LANGUAGE plperlu SECURITY DEFINER;
CREATE OR REPLACE FUNCTION extstore.f_read_file(p_folder text, p_level int, p_md5 text) RETURNS bytea AS $$
SELECT extstore.f_read_file_pl($1, $2, $3);
$$ LANGUAGE SQL SECURITY DEFINER;
次の関数は、extstore
に格納されているファイルの情報を取得します。
CREATE OR REPLACE FUNCTION extstore.f_get_file_size(text, int, text) RETURNS int AS $$
use strict;
my($folder, $level, $md5) = @_;
my $rv = spi_exec_query('SELECT current_catalog AS db');
my $db = $rv->{rows}[0]->{'db'};
$md5 = lc $md5;
$md5 =~ '^[0-9a-f]{32}$' or die 'Malformed MD5';
$level >= 0 && $level < 4 or die 'Invalid level';
my $dir = 'extstore';
if (! -d $dir) { return undef; }
$dir .= "/$db";
-d $dir or return $dir;
if ($folder) {
$folder =~ s/\//-/g;
$dir .= "/$folder";
-d $dir or return undef;
}
if ($level) {
my $subf = substr $md5, 0, $level;
$dir .= "/$subf";
-d $dir or return undef;
}
my $file = "$dir/$md5";
-f $file or return undef;
return -s $file;
$$ LANGUAGE plperlu SECURITY DEFINER;
CREATE OR REPLACE FUNCTION extstore.f_get_files(text) RETURNS SETOF text AS $$
use strict;
my($folder) = @_;
my $rv = spi_exec_query('SELECT current_catalog AS db');
my $db = $rv->{rows}[0]->{'db'};
my $dir = 'extstore';
if (! -d $dir) { return undef; }
$dir .= "/$db";
-d $dir or return $dir;
if ($folder) {
$folder =~ s/\//-/g;
$dir .= "/$folder";
-d $dir or return undef;
}
open(my $out, '-|', "/bin/find '$dir' -type f -printf '%f\\n'") or die "Unable to list folder $dir: $!";
while(<$out>) {
chomp;
return_next($_);
}
close($out);
return undef;
$$ LANGUAGE plperlu SECURITY DEFINER;
たとえば、次の小さなテキストドキュメント
abcd1234
ASDF
'8cfc4f81e28149a4d4c79dae83d7ceb3'
のmd5ハッシュと、データベースtemplate1
からの次の関数呼び出しがあります。
SELECT extstore.f_save_file('scans', 2, 'abcd1234\015\012ASDF'::bytea);
$PGDATA/template1/scans/8c/8cfc4f81e28149a4d4c79dae83d7ceb3
に保存します。
その後、ファイルは次の方法で読み取ることができます。
SELECT extstore.f_read_file('scans', 2, '8cfc4f81e28149a4d4c79dae83d7ceb3');
ファイルシステムからファイルサイズを取得するだけで、ファイルをメモリに読み込んでblobのサイズを計算しなくても、blobのサイズを照会できます。
SELECT extstore.f_get_file_size('scans', 2, '8cfc4f81e28149a4d4c79dae83d7ceb3');
最後に、次の関数は、フォルダー(この場合は$PGDATA/extstore/template1/scans
)内のすべてのファイル名(つまり、blobのMD5ハッシュ)のリストを生成します。
SELECT * FROM extstore.f_get_files('scans');