web-dev-qa-db-ja.com

データベースの外部にblobを格納する

スキャンしたドキュメントがたくさんあるPostgreSQLデータベースがdocument byteaテーブルscansの列。数十万のドキュメントがあり、大きくてバックアップに不便でした。また、重複率は高く、メジャーではなく5〜10%でした。

これらのドキュメントをデータベースの外部に保存して、インクリメンタルtarを介してバックアップできるようにし、pg_dumpデータベースバックアップのサイズを縮小したいと思いました。

Plperluを使用して、以下で共有したいソリューションを思いつきました。コメントやさらなる最適化のアイデアは大歓迎です!

PostgreSQLには、データベースの外部にBLOBを格納する方法があります。彼らは Large Objects と呼びました。あなたはそれらがあなたのニーズに合うと思うかもしれません。内部的には、彼らはこれを非常に行っています。 OIDを使用する場合を除きます。 md5sumなどをデータベースの行に保存できます。

1
Evan Carroll

次のソリューションでは、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');