2013年9月19日木曜日

PostgreSQL の TOAST の仕組みと効果

こんにちは。渡辺です。

今回は、PostgreSQL の TOAST の仕組みについて紹介します。

PostgreSQL7.1 までは、タプル(フィールド)の大きさがブロックサイズ(デフォルト 8KB、最大 64KB。コンパイル時に指定)に制限されていました。

PostgreSQL7.2 以降は TOAST(The Oversized-Attribute Storage Technique)と呼ばれる仕組みが導入され、最大 1GB のタプルが利用できるようになりました。今回は TOAST の仕組みと効果について検証してみます。

■TOAST とは

TOAST とは、大きなタプルを複数に分割し、複数のブロックにデータを保存できるようにする仕組みです。データを分割すると同時に、圧縮も行います。
分割されたタプルはリンクドリスト型式で保存されており、TOAST が利用できるフィールド(テキスト型、Bytea 型、JSON 型など)を利用すると、自動的に PostgreSQL の内部に管理用のテーブルが作成されます。

# select relname from pg_class c where c.relname like '%pg_toast_%' order by relname;
        relname         
------------------------
 pg_toast_12450
 pg_toast_12450_index
(中略)
 pg_toast_3596
 pg_toast_3596_index
(36 行)

TOAST は、LZ 系の圧縮アルゴリズムを使いデータを圧縮しています。
小さなデータを圧縮してもあまり効果は得られないので、ブロックサイズの四分の一(通常は 2KB)を超えるデータの場合に圧縮が行われます。今回は、TOAST を利用した圧縮効果がどの程度なのかも検証してみます。

【検証に利用した環境】

  • CPU: Core2Quad Q9400
  • MEM: 8GB
  • HDD: SATA
  • OS: Linux 2.6 64bit
  • glibc: 2.12.90
  • PostgreSQL: 9.2.2
  • PHP: 5.3

PostgreSQL9.2 のデフォルト設定だとベンチマーク用スクリプトの実行が非常に遅いので、以下の設定を行いました。

【postgresql.conf の設定】

shared_buffers = 1024MB   
fsync = off

今回は圧縮効果を検証するベンチマークなので、fsync=off とする方が違いが分り易くなります。そのため、fsync の設定を off にしています。

■TOAST の戦略

TOAST は 4 つの保存戦略を持っています。保存戦略といっても難しいものではありません。どのように保存するかポリシーが決められているだけです。

  • PLAIN
  • 圧縮や行外の格納を防止。 TOAST 化不可能なデータ型の列に対してのみ作用するポリシーです。

  • EXTENDED
  • 圧縮と行外の格納を許可。 ほとんどの TOAST 可能のデータ型のデフォルト。 圧縮がまず行われ、それでも行が大きすぎる場合は行外に格納。

  • EXTERNAL
  • 非圧縮の行外格納を許可。 EXTERNAL を使用すると、text と bytea 列全体に対する部分の文字列操作が高速化されますが、格納領域が増加するという欠点があります。

  • MAIN
  • 圧縮を許可し、行外の格納は可能な限り行わない。

TOAST 可能なデータ型はそれぞれデフォルトの戦略があります。これらの戦略は ALTER TABLE SET STORAGE を利用して変更できます。通常 TEXT や BYTEA 型は TOAST を利用しますが、利用しないようにすることもできます。

■ベンチマーク用スクリプト

今回も PHP で簡単なベンチマーク用のスクリプトを作成しました。ざっと読んで頂ければ何を行っているのか分かると思います。

■bench-toast.php

<?php
// 接続するデータベース
$db = pg_connect('host=localhost user=user1') or die('Cannot connect to db');
// PostgreSQLのデータの場所
$pgdata_dir = '~/pgdata-9.2';
// データにランダム文字列を使う
$use_random_string = true;


// TOASTのデータ保存戦略
$strategies = array('PLAIN','EXTENDED','EXTERNAL','MAIN');
// フィールドサイズ
$field_sizes = array(1000, 3000, 7000, 15000, 31000);
// 行数
$num_rows = 50000;

// ベンチマーク
foreach($strategies as $st) {
 foreach($field_sizes as $sz) {
  // テーブル定義
  pg_query($db, "DROP TABLE test;");
  pg_query($db, "CREATE TABLE test (id serial, t text);") or die('Cannot create table');
  pg_query($db, "ALTER TABLE test ALTER t SET STORAGE ". $st .";");
  pg_query($db, "VACUUM FULL;");
  $tmp = $sz;
  if ($use_random_string) {
   // サイズ $sz のランダム文字列を作る
   $data = ''; while($tmp--) $data .= chr(mt_rand(32,126));
  } else {
   // Aの繰り返し
   $data = str_repeat('A', $sz);
  }
  $sql = "INSERT INTO test (t) VALUES ('".pg_escape_string($db, $data)."');";
  $start = microtime(true);
  for ($i = 0; $i < $num_rows; $i++) {
   if (!pg_query($db, $sql)) {
    break;
   }
  }
  $end = microtime(true);

  // database size
  $dbsz = exec('du '.$pgdata_dir.' | tail -n 1 | cut -f1');

  printf("Strategy: %-10s  Field Size: %7s   Time: % 7.3f   DB Size: % 4dMB".PHP_EOL,
      $st, $sz , $end - $start, (int)$dbsz/1024);
 }
 }

ベンチマークを実行する場合、ソースコードの最初に予め設定を行う必要がある箇所が 2 つあります。環境に合わせて変更してください。

// 接続するデータベース
$db = pg_connect('host=localhost user=user1') or die('Cannot connect to db');
// PostgreSQLのデータの場所
$pgdata_dir = '~/pgdata-9.2';

ベンチマークは、ランダム文字列と同じ文字の繰り返しの 2 パターンで行います。次の行の true を false に書き換えるとランダム文字列ではなく、圧縮しやすい A の繰り返しの文字列になるように記載しています。

// データにランダム文字列を使う
$use_random_string = true;

ベンチマークスクリプトは、データベースディレクトリのサイズも取得するようになっています。
テストのたびにテーブルをドロップし、VACUUM FULL を実行して掃除をしていますが、完全に領域を開放できないのでテストのたびに若干サイズが大きくなります。

今回は圧縮効果を見るだけなので、多少の違いは問題ありません。

■ベンチマーク結果

「ランダム文字列の場合」

$ php bench-toast.php
Strategy: PLAIN       Field Size:    1000   Time:   7.106   DB Size:  225MB
Strategy: PLAIN       Field Size:    3000   Time:   9.418   DB Size:  365MB
Strategy: PLAIN       Field Size:    7000   Time:  13.293   DB Size:  560MB

Warning: pg_query(): Query failed: ERROR:  row is too big: size 15032, maximum size 8160 in /home/yohgaki/ext/git/yohgaki/sios/007/bench-toast.php on line 34
Strategy: PLAIN       Field Size:   15000   Time:   0.013   DB Size:  170MB

Warning: pg_query(): Query failed: ERROR:  row is too big: size 31032, maximum size 8160 in /home/yohgaki/ext/git/yohgaki/sios/007/bench-toast.php on line 34
Strategy: PLAIN       Field Size:   31000   Time:   0.001   DB Size:  170MB
Strategy: EXTENDED    Field Size:    1000   Time:   7.048   DB Size:  225MB
Strategy: EXTENDED    Field Size:    3000   Time:  11.895   DB Size:  370MB
Strategy: EXTENDED    Field Size:    7000   Time:  16.867   DB Size:  567MB
Strategy: EXTENDED    Field Size:   15000   Time:  26.961   DB Size:  962MB
Strategy: EXTENDED    Field Size:   31000   Time:  58.290   DB Size: 1768MB
Strategy: EXTERNAL    Field Size:    1000   Time:   7.009   DB Size:  242MB
Strategy: EXTERNAL    Field Size:    3000   Time:  10.504   DB Size:  386MB
Strategy: EXTERNAL    Field Size:    7000   Time:  15.326   DB Size:  583MB
Strategy: EXTERNAL    Field Size:   15000   Time:  25.288   DB Size:  978MB
Strategy: EXTERNAL    Field Size:   31000   Time:  53.025   DB Size: 1800MB
Strategy: MAIN        Field Size:    1000   Time:   6.969   DB Size:  273MB
Strategy: MAIN        Field Size:    3000   Time:  10.388   DB Size:  413MB
Strategy: MAIN        Field Size:    7000   Time:  14.524   DB Size:  609MB
Strategy: MAIN        Field Size:   15000   Time:  28.403   DB Size: 1010MB
Strategy: MAIN        Field Size:   31000   Time:  59.255   DB Size: 1784MB

2 つエラーが発生していますが、これは PLAIN 戦略ではブロックサイズを超えるタプルを保存できない為に発生しているエラーです。

戦略ごとにデータベースの大きさが多少は異なっています。しかし、圧縮が有効であるはずの EXTENDED、MAIN の場合の方がデータ量が大きくなっている事が分かると思います。

残念ながらランダムな文字列の場合、圧縮が有効に機能せず、TOAST に必要な管理用テーブルのオーバーヘッドの方が大きくなってしまっている事が分かります。

特にフィールドサイズ(データサイズ)が大きな 31000 バイトの圧縮が無効な EXTERNAL と圧縮が有効な EXTENDED/MAIN と比べると、データサイズはさほど変わらない上、実行時間が大きくなっています。

Strategy: EXTENDED    Field Size:   31000   Time:  58.290   DB Size: 1768MB
Strategy: EXTERNAL    Field Size:   31000   Time:  53.025   DB Size: 1800MB
Strategy: MAIN        Field Size:   31000   Time:  59.255   DB Size: 1784MB

これではTOASTのを使う意味が無いように見えますが、次のベンチマーク結果を見てみましょう。

「A の繰り返しの場合」

$ php bench-toast.php
Strategy: PLAIN       Field Size:    1000   Time:   7.360   DB Size:  257MB
Strategy: PLAIN       Field Size:    3000   Time:   9.552   DB Size:  397MB
Strategy: PLAIN       Field Size:    7000   Time:  13.428   DB Size:  592MB

Warning: pg_query(): Query failed: ERROR:  row is too big: size 15032, maximum size 8160 in /home/yohgaki/ext/git/yohgaki/sios/007/bench-toast.php on line 36
Strategy: PLAIN       Field Size:   15000   Time:   0.062   DB Size:  202MB

Warning: pg_query(): Query failed: ERROR:  row is too big: size 31032, maximum size 8160 in /home/yohgaki/ext/git/yohgaki/sios/007/bench-toast.php on line 36
Strategy: PLAIN       Field Size:   31000   Time:   0.001   DB Size:  202MB
Strategy: EXTENDED    Field Size:    1000   Time:   7.069   DB Size:  258MB
Strategy: EXTENDED    Field Size:    3000   Time:   9.111   DB Size:  205MB
Strategy: EXTENDED    Field Size:    7000   Time:  12.852   DB Size:  208MB
Strategy: EXTENDED    Field Size:   15000   Time:  21.667   DB Size:  213MB
Strategy: EXTENDED    Field Size:   31000   Time:  37.873   DB Size:  222MB
Strategy: EXTERNAL    Field Size:    1000   Time:   7.061   DB Size:  258MB
Strategy: EXTERNAL    Field Size:    3000   Time:  10.642   DB Size:  402MB
Strategy: EXTERNAL    Field Size:    7000   Time:  15.203   DB Size:  599MB
Strategy: EXTERNAL    Field Size:   15000   Time:  26.530   DB Size:  994MB
Strategy: EXTERNAL    Field Size:   31000   Time:  47.505   DB Size: 1784MB
Strategy: MAIN        Field Size:    1000   Time:   7.134   DB Size:  258MB
Strategy: MAIN        Field Size:    3000   Time:   9.366   DB Size:  205MB
Strategy: MAIN        Field Size:    7000   Time:  13.345   DB Size:  208MB
Strategy: MAIN        Field Size:   15000   Time:  21.578   DB Size:  212MB
Strategy: MAIN        Field Size:   31000   Time:  38.764   DB Size:  221MB

圧縮の効果が最も発揮しやすいデータの場合、TOAST が有効に機能している事がはっきり分かります。

Strategy: EXTENDED    Field Size:   31000   Time:  37.873   DB Size:  222MB
Strategy: EXTERNAL    Field Size:   31000   Time:  47.505   DB Size: 1784MB
Strategy: MAIN        Field Size:   31000   Time:  38.764   DB Size:  221MB

データサイズは大幅に小さくなり、実行時間も短くなっています。実行時間は I/O が減った為に短くなったと考えられます。SSD を利用するともう少し違いが少なくなるかも知れません。

■ベンチマーク結果の考察

今回は、圧縮が効きづらいランダム文字列と、圧縮が最も効きやすい同じ文字の繰り返しの極端な例を比べてみました。そして、今回のベンチマーク結果から次のような事が分かりました。

  • TOAST の圧縮アルゴリズムは圧縮重視ではなく、性能(処理速度)重視である
  • 圧縮が効かないデータである程度の大きさがある場合、圧縮しない方が若干速く、データ量も増えない
  • 通常の場合、TOAST の利用(分割と圧縮)によるオーバーヘッドは無視して構わない

zip や gzip、bzip はテキストを効率よく圧縮し、1/5、1/10 になることもめずらしくありません。

しかし、TOAST は圧縮すると同時に管理用テーブルが必要になるため、圧縮が効きづらいランダムデータの場合には圧縮効果は期待できません。例としては、BYTEA 型で予め圧縮されているデータを保存する場合、TOAST の圧縮は意味がありません。

■まとめ

ベンチマークではマイナス面を強調したような結果になっていますが、実用的には TOAST がある方が、実行時間的にも、データベース容量的にも、劇的な効果はなくともより良い結果になるケースがほとんどです。

気にしていない間に、PostgreSQL がデータを勝手に分割・圧縮し、且つクエリ実行時間も短くなる TOAST は、マニュアルに記載されている通り「パンをスライスして以来の最高なもの(素晴らしいもの)」(TOAST)だと言えるでしょう。

MongoDB も、PostgreSQL 同様にデータを圧縮する機能を持っています。
PostgreSQL の場合、LZ 系の圧縮アルゴリズムを利用していますが、MongoDB の場合、snappy と呼ばれる圧縮ライブラリを利用しています。zlib の最も速い圧縮モードより速いそうですが、そのかわり 20% から 100% データサイズが大きくなるとしています。

PostgreSQL の圧縮は高速ですが、ASCII 文字だけのランダム文字列でも、管理領域を含めるとオーバーヘッドの方が大きくなっています。もしかすると snappy を利用した方が全体的により良い結果になるかも知れません。

圧縮アルゴリズムの入れ替えは比較的簡単です。Snappy は C++ のライブラリであるため C で作られた PostgreSQL で使用するには多少手間が必要ですが、PostgreSQL の内部に興味がある方向けの初めてのプロジェクトとしては、良い題材だと思います。興味がある方は是非チャレンジしてみてください。

■参考 URL

http://www.postgresql.jp/document/9.2/html/storage-toast.html
http://www.postgresql.org/docs/9.2/static/sql-altertable.html
http://www.mongodb.org/
http://code.google.com/p/snappy/

0 件のコメント:

コメントを投稿