RDBのパフォーマンスを改善する手段として、「テーブル分割」という方法がある。更に、テーブル分割に代替する手段である「集約」についても本記事で取り上げる。
テーブル分割は行うべきではない
テーブル分割は原則として行うべきではない。理由は、テーブル分割はそれが行われる意味的な理由を持たず、テーブルを分割する理由が正規化の理論からは出てこないからだ。テーブル分割はパフォーマンス要求のみによって行われ、テーブルに意味的な破壊をもたらすことに注意しなければならない。そして、テーブル分割には代替手段が存在する。テーブル分割を行わずとも「パーティション」や「集約」といった方法で、テーブル分割に相当する効果が得られるので、そちらを利用するべきである。
テーブル分割の種類
テーブル分割の方法は、大きく以下の2つに分かれる。
- 水平分割
- 垂直分割
また、厳密には分割ではないが「集約」という、テーブル分割の代替手段的な方法も存在するので、これもあわせて記事中で説明する。
「水平分割」は、レコード単位にテーブルを分割することである。テーブルを水平にカットするのでこう呼ばれる。「垂直分割」は、その反対で、列単位にテーブルを分割する。
水平分割のイメージ
垂直分割のイメージ
以下より「水平分割」「垂直分割」について、それぞれ説明する。
水平分割
水平分割とは、レコード単位でテーブルを分割する手段である。
例として、会社の1年ごとの売上げを保持する以下のようなテーブルを考えてみる。
年度 | 会社コード | 売上(億円) |
---|---|---|
2001 | C0001 | 50 |
2001 | C0002 | 52 |
2001 | C0003 | 55 |
2001 | C0004 | 46 |
2002 | C0001 | 52 |
2002 | C0002 | 55 |
2002 | C0003 | 60 |
2002 | C0004 | 47 |
2003 | C0001 | 46 |
2003 | C0002 | 52 |
2003 | C0003 | 44 |
2003 | C0004 | 60 |
このテーブルは、サンプルなので12行しかレコードを含んでいないが、実際の業務システムではテーブルに何百万~何十億という数のレコードが含まれる。その結果、テーブルにアクセスするSQLのパフォーマンスが悪化する場合がある。
そこで、パフォーマンス改善について考えられる手段の一つが、SQL文がアクセスするテーブルのサイズを極力小さくしようとすることである。例えば、SQLが常に1年ごとにしか「売上げ」テーブルにアクセスしないのであれば、次のように年度ごとにテーブルを分割することでパフォーマンスを改善することができる。
年度 | 会社コード | 売上(億円) |
---|---|---|
2001 | C0001 | 50 |
2001 | C0002 | 52 |
2001 | C0003 | 55 |
2001 | C0004 | 46 |
年度 | 会社コード | 売上(億円) |
---|---|---|
2002 | C0001 | 52 |
2002 | C0002 | 55 |
2002 | C0003 | 60 |
2002 | C0004 | 47 |
年度 | 会社コード | 売上(億円) |
---|---|---|
2003 | C0001 | 46 |
2003 | C0002 | 52 |
2003 | C0003 | 44 |
2003 | C0004 | 60 |
これは分かりやすいと言えば分かりやすい解決策だが、記事の冒頭で説明した通り、RDBでは原則禁止とされている。この方法に最も近い代替策としてパーティションの利用が挙げられる。このパーティション機能は、DBMSによっては持っていなかったり、有料オプションだったりするが利用可能な環境においては、水平分割を回避しつつパフォーマンス改善が可能になる為、積極的に利用するべきである。
垂直分割
水平分割がレコードを軸にした分割だったのに対して、垂直分割は列を軸に分割する。例として、以下のような第3正規化された状態の「社員」テーブルを考えてみる。
会社コード | 社員ID | 社員名 | 年齢 | 部署コード |
---|---|---|---|---|
C0001 | 000A | 加藤 | 40 | D01 |
C0001 | 000B | 藤本 | 32 | D02 |
C0001 | 001F | 三島 | 50 | D03 |
C0002 | 000A | 斉藤 | 47 | D03 |
C0002 | 009F | 田島 | 25 | D01 |
C0002 | 010A | 渋谷 | 33 | D04 |
今、このテーブルに対する検索のSQL文に遅延が発生していて、改善の必要があるとする。かつ、その検索で利用する列は常に「会社コード」「社員ID」および「年齢」だけであるとする。このとき、次のようにテーブルを分割することでSQL文がアクセスするデータ量を減らすことができる。
会社コード | 社員ID | 年齢 |
---|---|---|
C0001 | 000A | 40 |
C0001 | 000B | 32 |
C0001 | 001F | 50 |
C0002 | 000A | 47 |
C0002 | 009F | 25 |
C0002 | 010A | 33 |
会社コード | 社員ID | 社員名 | 部署コード |
---|---|---|---|
C0001 | 000A | 加藤 | D01 |
C0001 | 000B | 藤本 | D02 |
C0001 | 001F | 三島 | D03 |
C0002 | 000A | 斉藤 | D03 |
C0002 | 009F | 田島 | D01 |
C0002 | 010A | 渋谷 | D04 |
ボトルネックがストレージのI/Oコストだった場合に限るが、このように必要な列だけに絞ってデータを保持した「社員1」テーブルを検索対象とすることで、SQL文のパフォーマンス改善が可能になる。
しかし、この垂直分割にも、分割することが論理的な意味を持たないという水平分割と同様の欠点があるため、原則利用するべきではない。特に垂直分割の場合は次に紹介する「集約」で代替可能である。
集約
集約は、テーブル分割ではなく、むしろテーブル分割の代替案に位置付けらえる方法である。その種類は、さらに細かく以下の2種類に分けられる。
- 列の絞り込み
- サマリテーブル
列の絞り込み
まず1つ目が、単純に保持するテーブルを作成するタイプである。これは、先ほどの垂直分割に対する代替案に相当する。「社員」テーブルのうち「会社コード」「社員ID」および「年齢」が頻繁に参照される列であるならば、これらの列だけを持った新しいテーブルを追加作成するわけである。オリジナルの「社員」テーブルは残す為、分割ではない。
例として、以下の「社員」テーブルに対して列の絞り込みを行う場合を考えてみる。
会社コード | 社員ID | 社員名 | 年齢 | 部署コード |
---|---|---|---|---|
C0001 | 000A | 加藤 | 40 | D01 |
C0001 | 000B | 藤本 | 32 | D02 |
C0001 | 001F | 三島 | 50 | D03 |
C0002 | 000A | 斉藤 | 47 | D03 |
C0002 | 009F | 田島 | 25 | D01 |
C0002 | 010A | 渋谷 | 33 | D04 |
列の絞り込みを行って作成したテーブルは以下である。この2つのテーブルは定期的にデータの同期が必要となる。
会社コード | 社員ID | 年齢 |
---|---|---|
C0001 | 000A | 40 |
C0001 | 000B | 32 |
C0001 | 001F | 50 |
C0002 | 000A | 47 |
C0002 | 009F | 25 |
C0002 | 010A | 33 |
このようにして作られる、オリジナルと比較して小規模なテーブルのことを、データマート(Data Mart)、あるいは省略して単にマートと呼ぶ。マートは非常に便利で、オリジナルのテーブルを意味的に破壊することなくパフォーマンスを向上させることができる。また、マートを利用する際に注意すべき問題があり、それがデータ同期の問題である。今、オリジナルのテーブルとマートは、部分的に同じ列を共有している。このとき、たとえば「年齢」列は社員が年齢を重ねる度に値を更新していく必要がある。そのため、オリジナルの「社員」テーブルの「年齢」列が更新されたら、マートの「年齢」列も更新しなければならない。問題となるのは、マートの更新タイミングである。このタイミングが短いほど、オリジナルのテーブルと齟齬がある期間も短くなるので、データ精度が高く、機能的に好ましいが、更新タイミングが短いほど更新処理の負荷が上がり、もともと解決するはずだった性能問題をかえって悪化させてしまう危険もある。したがって、多くの場合、マートの更新は1日1回~数回程度の頻度で一括更新(バッチ更新)されている。
サマリテーブル
サマリテーブルも集約の一手段である。列の絞り込みと違うのは、サマリテーブルは集約関数(SUM,AVGなど)によってレコードを集約した状態で保持することである。たとえば、会社別に社員の平均年齢が必要な業務があったとする。このとき、もちろん毎回「社員」テーブルに対してSELECT文でアクセスして集約関数(AVG)を実行しても良いが、テーブルの規模が大きくなると、その集約関数による処理のコストが大きくなり、実行時間が長くなる。
そこで、事前に集約を行ったテーブルを作っておくことで、社員の会社別平均年齢を求めたければ、そのテーブルに対する単純なSELECT文で求めることができる。
例として、以下の「社員」テーブルに対してサマリテーブルの作成を行う場合を考えてみる。
会社コード | 社員ID | 社員名 | 年齢 | 部署コード |
---|---|---|---|---|
C0001 | 000A | 加藤 | 40 | D01 |
C0001 | 000B | 藤本 | 32 | D02 |
C0001 | 001F | 三島 | 50 | D03 |
C0002 | 000A | 斉藤 | 47 | D03 |
C0002 | 009F | 田島 | 25 | D01 |
C0002 | 010A | 渋谷 | 33 | D04 |
「社員」テーブルを対象に、会社別に社員の平均年齢を求める為にサマリテーブルを作成する。
以下がサマリテーブルである。
会社コード | 平均年齢 |
---|---|
C0001 | 41 |
C0002 | 35 |
この「社員平均年齢」テーブルのサイズは、行列ともに元の「社員」テーブルより小さくなり、アクセスするときのI/Oコストを大きく削減する効果がある。ただし、この方法も先ほどの列の絞り込みと同じデメリットを共有している。つまり、更新のタイムラグによってデータの整合性がとれない時間帯が存在するということである。
まとめ
- テーブル分割は行うべきではない
- テーブルの水平分割は、パーティションで代替することができる
- テーブルの垂直分割は、集約で代替することができる