ちょっと話題の記事

『Amazon Redshift チュートリアル: テーブル設計チューニング』を実践してみた

2014.08.11

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Amazon Redshiftに於いて『パフォーマンスチューニング』は重要なトピックの1つです。Redshiftクラスタを立ち上げて、データを投入して、実際使ってみたものの思ったような速度・レスポンスが返って来ない...という状況も時折遭遇する事と思います。

AWS公式ドキュメント(英語版)を漁ってみると、まさにその『パフォーマンスチューニング』に焦点を当てたチュートリアルが公開されているではありませんか!当エントリではそのドキュメントを参考にひと通り実践してみた内容をまとめてみました。各種手順を1エントリに集約したので超長いエントリとなってしまいましたが、その辺りは目を瞑りつつ実践内容を順を追ってご覧頂ければと思います。

目次

1.テスト用データセットの作成

DWH(データウェアハウス)では一般的に『スタースキーマ』でテーブル設計を行うのが良いとされています。これは、中央に『ファクトテーブル』(分析の中心となるデータ)、ファクトテーブルを囲む形で『ディメンションテーブル』(ファクトテーブルの項目に対する、説明的な詳細属性情報を含むテーブル)で構成される形となります。ファクトテーブルはディメンションテーブルに対し、[ファクトテーブル.外部キー]=[ディメンションテーブル.主キー]の形で結合します。

このチュートリアルで用いるテーブル設計は以下のようなものとなります。

tutorial-optimize-tables-ssb-data-model

テスト用データセットの作成

では実際にデータが入る器、Amazon Redshiftクラスタを構築しましょう。所定のVPC、セキュリティグループ(ポート番号:5439を許可)、クラスタサブネットグループ、クラスタパラメータグループを作成した後、クラスタ本体を作成。チュートリアルではNode Type:dw2.large、ノード数:4で進める形で記載されていますので、ここでも同様の構成でクラスタを起動してみます。(※東京リージョン(ap-northeast-1)で起動してみる事にします)

クラスタが立ち上がりました。psqlコマンド等でデータベースにログインし、所定のテーブルを作成します。

CREATE TABLE part 
(
  p_partkey     INTEGER NOT NULL,
  p_name        VARCHAR(22) NOT NULL,
  p_mfgr        VARCHAR(6) NOT NULL,
  p_category    VARCHAR(7) NOT NULL,
  p_brand1      VARCHAR(9) NOT NULL,
  p_color       VARCHAR(11) NOT NULL,
  p_type        VARCHAR(25) NOT NULL,
  p_size        INTEGER NOT NULL,
  p_container   VARCHAR(10) NOT NULL
);

CREATE TABLE supplier 
(
  s_suppkey   INTEGER NOT NULL,
  s_name      VARCHAR(25) NOT NULL,
  s_address   VARCHAR(25) NOT NULL,
  s_city      VARCHAR(10) NOT NULL,
  s_nation    VARCHAR(15) NOT NULL,
  s_region    VARCHAR(12) NOT NULL,
  s_phone     VARCHAR(15) NOT NULL
);

CREATE TABLE customer 
(
  c_custkey      INTEGER NOT NULL,
  c_name         VARCHAR(25) NOT NULL,
  c_address      VARCHAR(25) NOT NULL,
  c_city         VARCHAR(10) NOT NULL,
  c_nation       VARCHAR(15) NOT NULL,
  c_region       VARCHAR(12) NOT NULL,
  c_phone        VARCHAR(15) NOT NULL,
  c_mktsegment   VARCHAR(10) NOT NULL
);

CREATE TABLE dwdate 
(
  d_datekey            INTEGER NOT NULL,
  d_date               VARCHAR(19) NOT NULL,
  d_dayofweek          VARCHAR(10) NOT NULL,
  d_month              VARCHAR(10) NOT NULL,
  d_year               INTEGER NOT NULL,
  d_yearmonthnum       INTEGER NOT NULL,
  d_yearmonth          VARCHAR(8) NOT NULL,
  d_daynuminweek       INTEGER NOT NULL,
  d_daynuminmonth      INTEGER NOT NULL,
  d_daynuminyear       INTEGER NOT NULL,
  d_monthnuminyear     INTEGER NOT NULL,
  d_weeknuminyear      INTEGER NOT NULL,
  d_sellingseason      VARCHAR(13) NOT NULL,
  d_lastdayinweekfl    VARCHAR(1) NOT NULL,
  d_lastdayinmonthfl   VARCHAR(1) NOT NULL,
  d_holidayfl          VARCHAR(1) NOT NULL,
  d_weekdayfl          VARCHAR(1) NOT NULL
);
CREATE TABLE lineorder 
(
  lo_orderkey          INTEGER NOT NULL,
  lo_linenumber        INTEGER NOT NULL,
  lo_custkey           INTEGER NOT NULL,
  lo_partkey           INTEGER NOT NULL,
  lo_suppkey           INTEGER NOT NULL,
  lo_orderdate         INTEGER NOT NULL,
  lo_orderpriority     VARCHAR(15) NOT NULL,
  lo_shippriority      VARCHAR(1) NOT NULL,
  lo_quantity          INTEGER NOT NULL,
  lo_extendedprice     INTEGER NOT NULL,
  lo_ordertotalprice   INTEGER NOT NULL,
  lo_discount          INTEGER NOT NULL,
  lo_revenue           INTEGER NOT NULL,
  lo_supplycost        INTEGER NOT NULL,
  lo_tax               INTEGER NOT NULL,
  lo_commitdate        INTEGER NOT NULL,
  lo_shipmode          VARCHAR(10) NOT NULL
);

次いで、S3の所定バケットからデータをCOPYします。計6つのリージョン向けにサンプルデータが既にアップロードされていますのでそのまま使いましょう。東京リージョンの場合はバケット名はawssampledbapnortheast1となります。シークレットキー及びアクセスキーも任意の情報を設定した上でCOPY処理を実行します。

copy customer from 's3://awssampledbapnortheast1/ssbgz/customer' 
credentials 'aws_access_key_id=<Your-Access-Key-ID>;aws_secret_access_key=<Your-Secret-Access-Key>'
gzip compupdate off;

copy dwdate from 's3://awssampledbapnortheast1/ssbgz/dwdate' 
credentials 'aws_access_key_id=<Your-Access-Key-ID>;aws_secret_access_key=<Your-Secret-Access-Key>'
gzip compupdate off;

copy lineorder from 's3://awssampledbapnortheast1/ssbgz/lineorder' 
credentials 'aws_access_key_id=<Your-Access-Key-ID>;aws_secret_access_key=<Your-Secret-Access-Key>'
gzip compupdate off;

copy part from 's3://awssampledbapnortheast1/ssbgz/part' 
credentials 'aws_access_key_id=<Your-Access-Key-ID>;aws_secret_access_key=<Your-Secret-Access-Key>'
gzip compupdate off;

copy supplier from 's3://awssampledbapnortheast1/ssbgz/supplier' 
credentials 'aws_access_key_id=<Your-Access-Key-ID>;aws_secret_access_key=<Your-Secret-Access-Key>'
gzip compupdate off;

それぞれのテーブルに関しては何度か投入を繰り返して時間を計測してみました。(※後程比較に使いますので、実行結果は控えておきましょう。)

# \timing
タイミングは on です。

<< CUSTOMER >>
# copy customer from 's3://awssampledbapnortheast1/ssbgz/customer'...
INFO:  Load into table 'customer' completed, 3000000 record(s) loaded successfully.
時間: 6869.708 ms
時間: 5761.899 ms
時間: 4655.569 ms
時間: 4262.411 ms
時間: 4547.109 ms
時間: 4945.159 ms

<< DWDATE >>
# copy dwdate from 's3://awssampledbapnortheast1/ssbgz/dwdate'...
INFO:  Load into table 'dwdate' completed, 2556 record(s) loaded successfully.
時間: 1183.848 ms
時間: 1990.743 ms
時間: 783.742 ms
時間: 1169.219 ms
時間: 1150.823 ms
時間: 852.049 ms

<< LINEORDER >>
# copy lineorder from 's3://awssampledbapnortheast1/ssbgz/lineorder'...
Load into table 'lineorder' completed, 600037902 record(s) loaded successfully.
時間: 820481.337 ms
時間: 788918.708 ms
時間: 945216.918 ms
時間: 947621.563 ms

<< PART >>
# copy part from 's3://awssampledbapnortheast1/ssbgz/part'...
INFO:  Load into table 'part' completed, 1400000 record(s) loaded successfully.
時間: 4447.001 ms
時間: 4643.314 ms
時間: 3918.008 ms
時間: 4530.556 ms
時間: 4461.159 ms

<< SUPPLIER >>
# copy supplier from 's3://awssampledbapnortheast1/ssbgz/supplier'...
INFO:  Load into table 'supplier' completed, 1000000 record(s) loaded successfully.
時間: 2760.094 ms
時間: 3844.060 ms
時間: 2937.730 ms
時間: 3022.458 ms
時間: 2810.537 ms
時間: 3405.167 ms

投入が完了したら、各テーブルの件数を確認してみます。結果は以下の様になりました。

# select count(*) from LINEORDER;
   count   
-----------
 600037902
(1 行)

時間: 894.963 ms
# SELECT COUNT(*) FROM lineorder;
   count   
-----------
 600037902
(1 行)

時間: 882.130 ms
# SELECT COUNT(*) FROM part;
  count  
---------
 1400000
(1 行)

時間: 134.306 ms
# SELECT COUNT(*) FROM customer;
  count  
---------
 3000000
(1 行)

時間: 157.337 ms
# SELECT COUNT(*) FROM supplier;
  count  
---------
 1000000
(1 行)

時間: 134.040 ms
# SELECT COUNT(*) FROM dwdate;
 count 
-------
  2556
(1 行)

時間: 155.553 ms
#

2.ベースラインを作るためのシステムパフォーマンステスト実践

前項でも述べましたが、テーブルのチューニング前後でのシステムパフォーマンスを比較する為に、詳細な記録を付けておく必要があります。ポイントは以下3点です。

  • ロードタイム(Load Time)
  • ストレージ使用量(Storage Use)
  • クエリパフォーマンス(Query Performance)

今回の例ではdw2.large 4ノードで構築した場合の結果に基づいています。この結果は同じクラスタ設定だったとしても(当ドキュメントの)結果と異なる場合もあるかも知れませんが、システムパフォーマンスは多くの要因に拠って影響を受けます。凡そ同じ様な結果になるのだな、程度の認識で進めて頂ければと思います。

ベースライン確立の為のシステムパフォーマンステストの実践

前項でのテーブルデータのロード時間を控えて置いたかと思いますので、次はストレージ利用量に関する情報を取得します。以下クエリはテーブル毎に1MBサイズのディスクスペースが使われているか、を取得するものです。STV_BLOCKLISTテーブルを参照する事で得られる情報です。

select
  stv_tbl_perm.name as table,
  count(*) as mb
from
  stv_blocklist,
  stv_tbl_perm
where
  stv_blocklist.tbl = stv_tbl_perm.id
  and stv_blocklist.slice = stv_tbl_perm.slice
  and stv_tbl_perm.name in ('lineorder','part','customer','dwdate','supplier')
group by
  stv_tbl_perm.name
order by
  1 asc;

結果は以下の様になりました。

    table     |  mb   
--------------+-------
 customer     |   384
 dwdate       |   160
 lineorder    | 51024
 part         |   200
 supplier     |   152
(5 行)

任意のクエリ実行時のパフォーマンスも計測しておきます。クエリの初回実行時、Amazon Redshiftはコードをコンパイルし、そのコンパイル済のコードを計算ノードにそれぞれ配布します。この事により初回実行は時間が掛かるので、クエリの実行時間を計測する場合は、初回実行時の記録は使うべきではありません。この辺りの詳細は以下参照。

-- Query 1
-- Restrictions on only one dimension. 
select sum(lo_extendedprice*lo_discount) as revenue
from lineorder, dwdate
where lo_orderdate = d_datekey
and d_year = 1997 
and lo_discount between 1 and 3 
and lo_quantity < 24;

-- Query 2
-- Restrictions on two dimensions 

select sum(lo_revenue), d_year, p_brand1
from lineorder, dwdate, part, supplier
where lo_orderdate = d_datekey
and lo_partkey = p_partkey
and lo_suppkey = s_suppkey
and p_category = 'MFGR#12'
and s_region = 'AMERICA'
group by d_year, p_brand1
order by d_year, p_brand1;

-- Query 3
-- Drill down in time to just one month 

select c_city, s_city, d_year, sum(lo_revenue) as revenue 
from customer, lineorder, supplier, dwdate
where lo_custkey = c_custkey
and lo_suppkey = s_suppkey
and lo_orderdate = d_datekey
and (c_city='UNITED KI1' or
c_city='UNITED KI5')
and (s_city='UNITED KI1' or
s_city='UNITED KI5')
and d_yearmonth = 'Dec1997'
group by c_city, s_city, d_year
order by d_year asc, revenue desc;

実行結果は以下の様になりました。

# \timing
タイミングは on です。

<< Query 1 >>
# select sum(lo_extendedprice*lo_discount) as revenue
# from lineorder, dwdate
# where lo_orderdate = d_datekey
# and d_year = 1997 
# and lo_discount between 1 and 3 
# and lo_quantity < 24;
    revenue     
----------------
 32879160652772
(1 行)

時間: 8516.798 ms
時間: 6194.914 ms
時間: 6174.647 ms
時間: 6198.564 ms
時間: 6120.653 ms
# 

<< Query 2 >>
# select sum(lo_revenue), d_year, p_brand1
# from lineorder, dwdate, part, supplier
# where lo_orderdate = d_datekey
# and lo_partkey = p_partkey
# and lo_suppkey = s_suppkey
# and p_category = 'MFGR#12'
# and s_region = 'AMERICA'
# group by d_year, p_brand1
# order by d_year, p_brand1;
     sum     | d_year | p_brand1  
-------------+--------+-----------
 50258511019 |   1992 | MFGR#121
 51638981598 |   1992 | MFGR#1210
 54435432115 |   1992 | MFGR#1211
 51810546242 |   1992 | MFGR#1212
 53250409709 |   1992 | MFGR#1213
 :
 :
 31460340551 |   1998 | MFGR#128
 28629104373 |   1998 | MFGR#129
(280 行)

時間: 16349.402 ms
時間: 10878.786 ms
時間: 10771.186 ms
時間: 10811.213 ms
時間: 10960.680 ms

<< Query 3 >>
# select c_city, s_city, d_year, sum(lo_revenue) as revenue 
# from customer, lineorder, supplier, dwdate
# where lo_custkey = c_custkey
# and lo_suppkey = s_suppkey
# and lo_orderdate = d_datekey
# and (c_city='UNITED KI1' or
# c_city='UNITED KI5')
# and (s_city='UNITED KI1' or
# s_city='UNITED KI5')
# and d_yearmonth = 'Dec1997'
# group by c_city, s_city, d_year
# order by d_year asc, revenue desc;
   c_city   |   s_city   | d_year |  revenue  
------------+------------+--------+-----------
 UNITED KI5 | UNITED KI5 |   1997 | 373974482
 UNITED KI1 | UNITED KI5 |   1997 | 365124652
 UNITED KI1 | UNITED KI1 |   1997 | 358824801
 UNITED KI5 | UNITED KI1 |   1997 | 355652352
(4 行)

時間: 16170.027 ms
時間: 12959.871 ms
時間: 12785.167 ms
時間: 13248.973 ms
時間: 13332.809 ms
#

『チューニング前』の結果はこの様な状況となりました。(※計測時間は直近4回の平均を採用)

benchmark-before

3.ソートキーの選定

テーブル作成時、1つ以上のソートキーをテーブルに対して設定する事が出来ます。Amazon Redshiftはソートキーの設定に基づいて、データをディスクに格納します。どのようにデータがソートされるのか、という点は、ディスクI/Oや列圧縮、クエリパフォーマンスに重要な影響を与えます。以下の条件に合う項目があれば、それらはソートキー指定対象の候補となります。

  1. 直近のデータが頻繁にアクセスされるようなケースであれば、TIMESTAMP列をソートキーの先頭に指定しましょう。
  2. ある列で範囲指定や等価判定を行う場合、その項目をソートキーに指定しましょう。
  3. (ディメンション)テーブルと頻繁に結合を行う場合、その結合列をソートキーに指定しましょう。

ソートキーの選択

結果をフィルタリングするTIMESTAMP列を見つける為に、クエリを評価してみます。例えば、LINEORDERテーブルではlo_orderdateを使った等値判定を良く行います。

where lo_orderdate = d_datekey and d_year = 1997

範囲指定と等価判定で使用されている列を探します。LINEORDERテーブルはlo_orderdateを範囲指定でも利用しています。

where lo_orderdate = d_datekey and d_year >= 1992 and d_year <= 1997

2つのベストプラクティスに基づき、lo_orderdateが最善のソートキーとなりそうです。チューニングテーブル内には、LINEORDERに対するソートキーとしてlo_orderkeyを指定します。

残りのテーブルも3つめのベストプラクティスに基づき、主キーをソートキーとして指定します。以下の様にソートキーが指定出来ました。次のステップでは分散スタイルを埋めて行きます。

テーブル名 ソートキー 分散スタイル
LINEORDER lo_orderdate
PART p_partkey
CUSTOMER c_custkey
SUPPLIER s_suppkey
DWDATE d_datekey

4.分散キーの選定

データをテーブルにロードする際、Amazon Redshiftはテーブルで指定されている分散スタイルに基づいて、テーブルの行を各ノードスライスに分散します。スライスの数は、ノード上のプロセッサコアの数と同じです。例えば、当チュートリアルで使用しているdw2.largeクラスタは4つのノード、8つのスライスを持っています。全てのノードがスライスに分散するデータを取り込み、並列クエリの実行に参加します。

結合と集計を実行する必要性に応じたクエリを実行すると、クエリオプティマイザは計算ノードに対して行の再分散を行います。この再分散は結合に対して特定の行をノードに送るかも知れないし、またテーブル全体を全てのノードに配信する事になるかも知れません。

ゴールを達成する為には、分散スタイルを割り当てる必要があります。

  • 結合テーブルから、行を併置(コロケーション)する:列に対する結合行が同じスライスに有る場合、クエリ実行時のデータ移動は少なくて済みます。
  • クラスタ内の均等に対し、均等にデータを配置する:もしデータが均等に分散されている場合、全てのスライスに均等にワークロードが割り当てられます。

これらのゴールは幾つかのケースで競合する可能性があり、システム全体にとってどの戦略がベストチョイスなのかを評価する必要性があります。例えば、EVEN分散はは同じスライスに全て一致した列の値を配置出来ます。この手順では、データ分散のゴールに関してSSBテーブルの分散を評価し、テーブルに最適な分散スタイルを選択します。

分散スタイル

分散スタイルは以下3つが指定可能です。

KEY分散(KEY Distribution)]
この設定を行うと、行はある列の値に従って分散されます。リーダーノードは同じノードスライスに一致する値を配置しようと試みます。もし2つのテーブルをキーで結合して分散させる場合、リーダーノードは結合した列の値に基づいて行をスライスに併置(コロケーション)します。
共通列の値が一致することで、物理的にデータが同じ場所に格納されます。
ALL分散(ALL Distribution)
テーブル全体のコピーが全てのノードに分散されます。EVEN分散またはKEY分散がノードに対しテーブルの特定行を配置するのに対し、ALL分散は全ての行が、テーブルが関わっている全ての結合に併置(コロケーション)される事を保証します。
EVEN分散(EVEN Distribution)
行は特定の列の値に関係なく、ラウンドロビン形式でスライスに分散されます。指定が無い場合(デフォルト指定)はこの形式が採用されます。

この辺の詳細については以下参照。

分散スタイルの選択

クエリ実行時、クエリオプティマイザは結合や集約の必要に応じて計算ノードに対し行の再分散を行います。クエリが実行される前に、あるべき場所にデータを配置しておく事で、再分散のインパクトを最小限に抑える事が出来ます。

最初のゴールはテーブル結合により一致した行データを併置(コロケーション)された状態でデータを分散させる事です。 この事は、テーブル結合で一致した行が同じノードのスライス上にあることを意味します。

1.

クエリプランの再分散ステップを見てみるには、以下のようにEXPLAINコマンドを実行します。この例では、3つあるうちのクエリから、2つめのものを実行しています。

explain
select sum(lo_revenue), d_year, p_brand1
from lineorder, dwdate, part, supplier
where lo_orderdate = d_datekey
and lo_partkey = p_partkey
and lo_suppkey = s_suppkey
and p_category = 'MFGR#12'
and s_region = 'AMERICA'
group by d_year, p_brand1
order by d_year, p_brand1;

実行例。ラベルがDS_BCASTで始まる形で結果が弾き出されました。

QUERY PLAN
XN Merge  (cost=1038007224737.84..1038007224738.54 rows=280 width=20)
  Merge Key: dwdate.d_year, part.p_brand1
  ->  XN Network  (cost=1038007224737.84..1038007224738.54 rows=280 width=20)
        Send to leader
        ->  XN Sort  (cost=1038007224737.84..1038007224738.54 rows=280 width=20)
              Sort Key: dwdate.d_year, part.p_brand1
              ->  XN HashAggregate  (cost=38007224725.76..38007224726.46 rows=280 
                    ->  XN Hash Join DS_BCAST_INNER  (cost=30674.95..38007188507.46 
                          Hash Cond: ("outer".lo_orderdate = "inner".d_datekey)
                          ->  XN Hash Join DS_BCAST_INNER  (cost=30643.00..37598119820.65 
                                Hash Cond: ("outer".lo_suppkey = "inner".s_suppkey)
                                ->  XN Hash Join DS_BCAST_INNER  
                                      Hash Cond: ("outer".lo_partkey = "inner".p_partkey)
                                      ->  XN Seq Scan on lineorder  
                                      ->  XN Hash  (cost=17500.00..17500.00 rows=56000 
                                            ->  XN Seq Scan on part  (cost=0.00..17500.00 
                                                  Filter: ((p_category)::text = 
                                ->  XN Hash  (cost=12500.00..12500.00 rows=201200
                                      ->  XN Seq Scan on supplier  (cost=0.00..12500.00 
                                            Filter: ((s_region)::text = 'AMERICA'::text)
                          ->  XN Hash  (cost=25.56..25.56 rows=2556 width=8)
                                ->  XN Seq Scan on dwdate  (cost=0.00..25.56 rows=2556
  • DS_BCAST_INNERは、内部結合テーブルが全てのスライスに展開された事を示しています。
  • DS_DIST_BOTHは、もしあった場合、外部結合テーブルと内部結合テーブルがスライス全体に再分散されている事を示します
  • 散布(broadcasting)や再分散(redistribution)はクエリパフォーマンスの面からすると、高コストとなります。

散布や再分散のステップをを減らしたり無くしたりするような戦略を選択したくなるでしょう。EXPLAINに関するより詳しい情報については、以下を御参照ください。

2.

ファクトテーブルとディメンションテーブルを共通の列で分散します。以下ダイアグラムはファクトテーブルのLINEORDERと、SSBスキーマのディメンションテーブルとの関連を示しています。

tutorial-optimize-tables-ssb-data-model-join-keys

それぞれのテーブルは1つだけ分散キーを持つ事が出来ます。この事は、共通の列でスキーマ内に併置(コロケーション)されうるテーブルのペアは1対だけである事を意味します。中央のファクトテーブルは明確な最初の選択となります。ペアに於ける2つ目のテーブルは、通常ファクトテーブルと結合する最も大きなディメンション(テーブル)を選択します。この設計では、LINEORDERがファクトテーブルとなり、PARTテーブルが最も大きなディメンション(テーブル)となります。PARTテーブルはLINEORDERテーブルと、主キーであるp_partkeyで結合します。

lo_partkeyをLINEORDERに対する分散キーとして指定し、p_partkeyをPARTの分散キーとして指定します。これでデータが読み込まれる時、結合キー項目で一致した値の行は同じスライスに併置(コロケーション)されるようになります。

3.

幾つかのディメンションテーブルはALL分散に切り替えます。

ディメンションテーブルがファクトテーブルやその他の重要な結合テーブルと併置(コロケーション)出来ない場合、対象テーブル全体を全てのノードに配布する事によってクエリパフォーマンスを改善する事が出来ます。

"ALL分散"は列の結合が全てのスライス上で行われ、併置(コロケーション)される事を保証します。ALL分散を使う場合事前にそのテーブルの大きさを確認しておきましょう。全てのノードスライスに併置(コロケーション)させるため、単純にストレージ容量はN倍されます。また、ロードタイムやシステムメンテナンスの時間についてもテーブルが増える分、増加します。

CUSTOMER, SUPPLIER, DWDATEの各テーブルはLINEORDERテーブルとプライマリーキーで結合しています。しかし、LINEORDERはPARTテーブルと併置(コロケーション)されているため、残りのテーブルについてはDISTSTYLE ALLを使う事になるでしょう。これは、テーブルのサイズが比較的小さく、また頻繁には更新されないため、ALL分散を使っても最小限のストレージ及びロード時間のインパクトで済むからです。

4.

その他テーブルはEVEN分散を用います。

全てのテーブルがDISTKEY指定、またはALL指定の分散スタイルで定義されました。ですので、EVENについては指定するものは今回はありません。パフォーマンス結果を評価してみて、幾つかのテーブルはALLからEVENに変更する事はあるかもしれません。

ここまでの経過で定まった分散スタイルの一覧が以下となります。

テーブル名 ソートキー 分散スタイル
LINEORDER lo_orderdate DISTKEY:lo_partkey
PART p_partkey DISTKEY:p_partkey
CUSTOMER c_custkey ALL
SUPPLIER s_suppkey ALL
DWDATE d_datekey ALL

5.圧縮エンコーディングのレビュー

圧縮(Compression)はデータ格納時にサイズを減らすために行われるカラムレベルの操作です。ストレージを節約し、読み込み時のサイズを削減します。ディスクI/Oのパフォーマンス改善に繋がります。

デフォルトではAmazon Redshiftはそのデータを非圧縮状態(raw)で格納します。Amazon Redshiftでテーブルを作成する際、カラム毎に列圧縮タイプを指定する事が可能です。この辺りの詳しい情報については以下を御参照ください。

テーブル作成時に列圧縮タイプを指定出来る他、COPYコマンドでデータ内容を分析し、自動的にオススメの列圧縮タイプを割り出す事も可能です。

圧縮エンコーディングをレビューする

1.テーブル列毎にどの程度スペースを使用しているかを調べる

システムビュー:STV_BLOCKLISTを調べる事で、列毎に1MBブロックをどの程度使用資しているかを確認する事が出来ます。MAX集計関数はカラム毎で一番多く使っている数値を返します。以下例ではシステム生成の列を除外する為にWHERE句にcol < 6を使っています。

コマンド実行例:

select col, max(blocknum)
from stv_blocklist b, stv_tbl_perm p
where (b.tbl=p.id) and name ='lineorder'
and col < 6
group by name, col
order by col;

結果はこの様に返ってきます:

col | max
-----+-----
 0   | 286
 1   | 286
 2   | 286
 3   | 286
 4   | 286
 5   | 286

2.異なるエンコーディングを試してみる

このステップでは『同一の列名』且つ『異なる圧縮エンコーディング』のテーブルを作成します。PARTテーブルのp_name列から同じデータを大量にINSERTします。 そして最後にテーブル間の結果を比較するという流れです。

a.比較用に、異なる圧縮エンコーディングを設定したテーブルを作成。

create table encodingshipmode (
moderaw varchar(22) encode raw,
modebytedict varchar(22) encode bytedict,
modelzo varchar(22) encode lzo,
moderunlength varchar(22) encode runlength,
modetext255 varchar(22) encode text255,
modetext32k varchar(22) encode text32k);

b.INSERT - SELECT文の実行。このSQLは若干時間が掛かります。(実際実行した際は1分30秒程掛かりました)

insert into encodingshipmode
select
  lo_shipmode as moderaw,
  lo_shipmode as modebytedict,
  lo_shipmode as modelzo,
  lo_shipmode as moderunlength,
  lo_shipmode as modetext255,
  lo_shipmode as modetext32k
from
  lineorder
where
  lo_orderkey < 200000000;

c.どの程度1MBブロックを使用しているかを確認するために以下クエリを実行。

select
  col,
  max(blocknum)
from
  stv_blocklist b, stv_tbl_perm p
where
  (b.tbl=p.id) and name = 'encodingshipmode'
  and col < 6
group by
  name, col
order by
  col;

結果は以下の様な形で返ってきます。クラスタ構成によって結果は異なってくるかも知れませんが、だいたいこんな感じで返ってくるかと。

 col | max
-----+-----
0    | 221
1    | 26
2    | 61
3    | 192
4    | 54
5    | 105
(6 rows)

順序はこうでした。

  • Raw
  • Bytedict
  • LZO
  • Runlength
  • Text255
  • Text32K

これらの結果から、Bytedictが一番良い結果を弾き出しました。raw(221)と比べるとおよそ8分1という形となります。勿論、項目によってこれらの算出結果は異なって来ます。

3.既存テーブルに対してオススメのエンコーディングを確認するためにANALYZE COMPRESSIONを使う

以下コマンドを実行してみます。

analyze compression lineorder;

結果はこんな感じで返ってくるはず。

 Table	   | Column	          |    Encoding
 ----------+------------------+-------------------
lineorder    lo_orderkey           delta
lineorder    lo_linenumber         delta
lineorder    lo_custkey            raw
lineorder    lo_partkey            raw
lineorder    lo_suppkey            raw
lineorder    lo_orderdate          delta32k
lineorder    lo_orderpriority      bytedict
lineorder    lo_shippriority       runlength
lineorder    lo_quantity           delta
lineorder    lo_extendedprice      lzo
lineorder    lo_ordertotalprice    lzo
lineorder    lo_discount           delta
lineorder    lo_revenue            lzo
lineorder    lo_supplycost         delta32k
lineorder    lo_tax                delta
lineorder    lo_commitdate         delta32k
lineorder    lo_shipmode           bytedict

ANALYZE COMPRESSIONlo_shipmode列に対してBYTEDICTエンコーディングを選出しました。

この辺りの手順、項目毎の圧縮エンコーディングを個別・手動で定めていく流れについては以下のページを参考に進めてみると良いでしょう。

4.自動圧縮をSSBテーブル群に適用させる

デフォルトでは、COPYコマンドは RAWエンコーディング(無圧縮)以外の圧縮エンコーディングでは無い空のテーブルにデータを読み込む際、自動的に圧縮エンコーディングを適用します。

6.テスト用データセットの再作成

ここまでの過程で、テーブル毎に分散キー、ソートキーを選択して来ました。テーブルの再作成を行い、データをリロードしてみます。COPYコマンド実行時に、ロードデータを分析し圧縮エンコーディングをデータに対して自動的に適用させる事が出来ます。

データセットの再生成

1.CREATE TABLE文実行の前に、これまで作成していたテーブルを一旦削除します。

drop table part cascade;
drop table supplier cascade;
drop table customer cascade;
drop table dwdate cascade;
drop table lineorder cascade;

2.ソートキー・分散キーを指定したCREATE TABLE文を実行します。

CREATE TABLE part (
  p_partkey     	integer     	not null	sortkey distkey,
  p_name        	varchar(22) 	not null,
  p_mfgr        	varchar(6)      not null,
  p_category    	varchar(7)      not null,
  p_brand1      	varchar(9)      not null,
  p_color       	varchar(11) 	not null,
  p_type        	varchar(25) 	not null,
  p_size        	integer     	not null,
  p_container   	varchar(10)     not null
);

CREATE TABLE supplier (
  s_suppkey     	integer        not null sortkey,
  s_name        	varchar(25)    not null,
  s_address     	varchar(25)    not null,
  s_city        	varchar(10)    not null,
  s_nation      	varchar(15)    not null,
  s_region      	varchar(12)    not null,
  s_phone       	varchar(15)    not null)
diststyle all;

CREATE TABLE customer (
  c_custkey     	integer        not null sortkey,
  c_name        	varchar(25)    not null,
  c_address     	varchar(25)    not null,
  c_city        	varchar(10)    not null,
  c_nation      	varchar(15)    not null,
  c_region      	varchar(12)    not null,
  c_phone       	varchar(15)    not null,
  c_mktsegment      varchar(10)    not null)
diststyle all;

CREATE TABLE dwdate (
  d_datekey            integer       not null sortkey,
  d_date               varchar(19)   not null,
  d_dayofweek	      varchar(10)   not null,
  d_month      	    varchar(10)   not null,
  d_year               integer       not null,
  d_yearmonthnum       integer  	 not null,
  d_yearmonth          varchar(8)	not null,
  d_daynuminweek       integer       not null,
  d_daynuminmonth      integer       not null,
  d_daynuminyear       integer       not null,
  d_monthnuminyear     integer       not null,
  d_weeknuminyear      integer       not null,
  d_sellingseason      varchar(13)    not null,
  d_lastdayinweekfl    varchar(1)    not null,
  d_lastdayinmonthfl   varchar(1)    not null,
  d_holidayfl          varchar(1)    not null,
  d_weekdayfl          varchar(1)    not null)
diststyle all;

CREATE TABLE lineorder (
  lo_orderkey      	    integer     	not null sortkey,
  lo_linenumber        	integer     	not null,
  lo_custkey           	integer     	not null,
  lo_partkey           	integer     	not null distkey,
  lo_suppkey           	integer     	not null,
  lo_orderdate         	integer     	not null,
  lo_orderpriority     	varchar(15)     not null,
  lo_shippriority      	varchar(1)      not null,
  lo_quantity          	integer     	not null,
  lo_extendedprice     	integer     	not null,
  lo_ordertotalprice   	integer     	not null,
  lo_discount          	integer     	not null,
  lo_revenue           	integer     	not null,
  lo_supplycost        	integer     	not null,
  lo_tax               	integer     	not null,
  lo_commitdate         integer         not null,
  lo_shipmode          	varchar(10)     not null
);

3.データをロードします。以前のSQLではcompupdate offの指定をしてましたが、今回はそれを外します。 これでCOPYコマンドが圧縮エンコーディングを自動で適用するようになります。

copy customer from 's3://<region-specific-bucket-name>/ssbgz/customer' 
credentials 'aws_access_key_id=<Your-Access-Key-ID>;aws_secret_access_key=<Your-Secret-Access-Key>' gzip;

copy dwdate from 's3://<region-specific-bucket-name>/ssbgz/dwdate' 
credentials 'aws_access_key_id=<Your-Access-Key-ID>;aws_secret_access_key=<Your-Secret-Access-Key>' gzip;

copy lineorder from 's3://<region-specific-bucket-name>/ssbgz/lineorder' 
credentials 'aws_access_key_id=<Your-Access-Key-ID>;aws_secret_access_key=<Your-Secret-Access-Key>' gzip;

copy part from 's3://<region-specific-bucket-name>/ssbgz/part' 
credentials 'aws_access_key_id=<Your-Access-Key-ID>;aws_secret_access_key=<Your-Secret-Access-Key>' gzip;

copy supplier from 's3://<region-specific-bucket-name>/ssbgz/supplier' 
credentials 'aws_access_key_id=<Your-Access-Key-ID>;aws_secret_access_key=<Your-Secret-Access-Key>' gzip;

ロード時間を計測してみましょう。各テーブルの計測時間は以下のようになりました。初回実行はチューニング後よりも時間が掛かっている様ですが、2回目以降は改善されていますね。(とは言え、実行時間が長くなったテーブルもあるようです。)

<< CUSTOMER >>
時間: 53674.706 ms
時間: 13065.793 ms
時間: 12435.864 ms
時間: 12090.514 ms
時間: 12249.941 ms

<< DWDATE >>
時間: 28246.456 ms
時間: 2727.605 ms
時間: 966.135 ms
時間: 948.880 ms
時間: 953.681 ms
時間: 949.184 ms

<< LINEORDER >>
時間: 940926.593 ms
時間: 940926.131 ms
時間: 909363.441 ms
時間: 1064005.981 ms
時間: 1062814.205 ms

<< PART >>
時間: 26005.555 ms
時間: 4638.184 ms
時間: 3834.709 ms
時間: 3894.809 ms
時間: 3377.251 ms

<< SUPPLIER >>
時間: 26566.266 ms
時間: 5358.111 ms
時間: 4186.689 ms
時間: 5238.045 ms
時間: 4467.133 ms

上記結果を反映した表は以下の様になりました。(※計測時間は直近4回の平均を採用)

benchmark-after1

7.システムパフォーマンステスト実践(再)

ソートキー・分散キーや圧縮エンコーディングの設定を付与してデータセットを再作成したら、再テストを実施します。

チューニング後のパフォーマンステストを再実施

1.

ストレージ使用量を計測します。

select
  stv_tbl_perm.name as "table",
  count(*) as "blocks (mb)"
from
  stv_blocklist,
  stv_tbl_perm
where
  stv_blocklist.tbl = stv_tbl_perm.id
  and stv_blocklist.slice = stv_tbl_perm.slice
  and stv_tbl_perm.name in ('customer', 'part', 'supplier', 'dwdate', 'lineorder')
group by
  stv_tbl_perm.name
order by
  1 asc;

結果はこうなりました。

    table    | blocks (mb) 
-------------+-------------
 customer    |         604
 dwdate      |         160
 lineorder   |       27152
 part        |         200
 supplier    |         236
(5 行)

2.

『傾斜分布(distribution skew)』の確認を行います。

不均一な分散またはデータ分散スキューは、クエリパフォーマンスを制限する点に加えてより多くの作業を幾つかのノードに強いる事となります。

分散スキューを確認するには、システムビューのSVV_DISKUSAGEを参照します。このテーブルの各行は1つのディスクブロックの統計情報を記録します。num_values列はディスクブロック内の行数を返し、sum(num_values)はスライス毎の行数を返します。

SSBデータベース内のテーブルの全ての分散状況を参照するためのクエリを実行します。

select
  trim(name) as table,
  slice,
  sum(num_values) as rows,
  min(minvalue),
  max(maxvalue)
from
  svv_diskusage
where
  name in ('customer', 'part', 'supplier', 'dwdate', 'lineorder') 
  and col =0
group by
  name, slice
order by
  name, slice;

実行結果は以下のような形で返ってきます。

   table   | slice |   rows   |   min    |    max
-----------+-------+----------+----------+-----------
 customer  |     0 |  3000000 |        1 |   3000000
 customer  |     2 |  3000000 |        1 |   3000000
 customer  |     4 |  3000000 |        1 |   3000000
 customer  |     6 |  3000000 |        1 |   3000000
 dwdate    |     0 |     2556 | 19920101 |  19981230
 dwdate    |     2 |     2556 | 19920101 |  19981230
 dwdate    |     4 |     2556 | 19920101 |  19981230
 dwdate    |     6 |     2556 | 19920101 |  19981230
 lineorder |     0 | 75029991 |        3 | 599999975
 lineorder |     1 | 75059242 |        7 | 600000000
 lineorder |     2 | 75238172 |        1 | 599999975
 lineorder |     3 | 75065416 |        1 | 599999973
 lineorder |     4 | 74801845 |        3 | 599999975
 lineorder |     5 | 75177053 |        1 | 599999975
 lineorder |     6 | 74631775 |        1 | 600000000
 lineorder |     7 | 75034408 |        1 | 599999974
 part      |     0 |   175006 |       15 |   1399997
 part      |     1 |   175199 |        1 |   1399999
 part      |     2 |   175441 |        4 |   1399989
 part      |     3 |   175000 |        3 |   1399995
 part      |     4 |   175018 |        5 |   1399979
 part      |     5 |   175091 |       11 |   1400000
 part      |     6 |   174253 |        2 |   1399969
 part      |     7 |   174992 |       13 |   1399996
 supplier  |     0 |  1000000 |        1 |   1000000
 supplier  |     2 |  1000000 |        1 |   1000000
 supplier  |     4 |  1000000 |        1 |   1000000
 supplier  |     6 |  1000000 |        1 |   1000000
(28 rows)

以下グラフは大きなテーブル上位3つの分散状況を図示したものです。CUSTOMERテーブルはALL分散を使用しているので、1ノードに対して1つだけに分散されています。この分散状況は比較的均一と言えます。ですので分布のスキューを調整する必要はありません。

3.

クエリに対するクエリプランを確認するためにEXPLAINコマンドを実行します。以下例はQuery2に対するEXPLAINコマンド実行です。

explain
select
  sum(lo_revenue),
  d_year,
  p_brand1
from
  lineorder,
  dwdate,
  part,
  supplier
where
  lo_orderdate = d_datekey
  and lo_partkey = p_partkey
  and lo_suppkey = s_suppkey
  and p_category = 'MFGR#12'
  and s_region = 'AMERICA'
group by
  d_year, p_brand1
order by
  d_year, p_brand1;

以下が実行結果となります。DS_BCAST_INNERラベルがDS_DIST_ALL_NONEDS_DIST_NONEに置き換わっています。これは再分散の必要が無くなり、クエリはより高速化されるだろう、という分析結果を意味しています。

tutorial-optimize-tables-compression-chart

QUERY PLAN                                                      
 XN Merge  (cost=1000014243538.45..1000014243539.15 rows=280 width=20)
   Merge Key: dwdate.d_year, part.p_brand1
   ->  XN Network  (cost=1000014243538.45..1000014243539.15 rows=280 width=20)
         Send to leader
         ->  XN Sort  (cost=1000014243538.45..1000014243539.15 rows=280 width=20)
               Sort Key: dwdate.d_year, part.p_brand1
               ->  XN HashAggregate  (cost=14243526.37..14243527.07 rows=280 width=20)
                     ->  XN Hash Join DS_DIST_ALL_NONE  (cost=30643.30..14211277.03 rows=4299912 width=20)
                           Hash Cond: ("outer".lo_orderdate = "inner".d_datekey)
                           ->  XN Hash Join DS_DIST_ALL_NONE  (cost=30611.35..14114497.06 rows=4299912 width=20)
                                 Hash Cond: ("outer".lo_suppkey = "inner".s_suppkey)
                                 ->  XN Hash Join DS_DIST_NONE  (cost=17640.00..13758507.64 rows=24001516 width=24)
                                       Hash Cond: ("outer".lo_partkey = "inner".p_partkey)
                                       ->  XN Seq Scan on lineorder  (cost=0.00..6000378.88 rows=600037888 width=16)
                                       ->  XN Hash  (cost=17500.00..17500.00 rows=56000 width=16)
                                             ->  XN Seq Scan on part  (cost=0.00..17500.00 rows=56000 width=16)
                                                   Filter: ((p_category)::text = 'MFGR#12'::text)
                                 ->  XN Hash  (cost=12500.00..12500.00 rows=188541 width=4)
                                       ->  XN Seq Scan on supplier  (cost=0.00..12500.00 rows=188541 width=4)
                                             Filter: ((s_region)::text = 'AMERICA'::text)
                           ->  XN Hash  (cost=25.56..25.56 rows=2556 width=8)
                                 ->  XN Seq Scan on dwdate  (cost=0.00..25.56 rows=2556 width=8)

4.

時間計測用にクエリを再度実行。比較用に前以て実行していたクエリ群を再度実行します。

-- Query 1
-- Restrictions on only one dimension. 
select sum(lo_extendedprice*lo_discount) as revenue
from lineorder, dwdate
where lo_orderdate = d_datekey
and d_year = 1997 
and lo_discount between 1 and 3 
and lo_quantity < 24;

-- Query 2
-- Restrictions on two dimensions 

select sum(lo_revenue), d_year, p_brand1
from lineorder, dwdate, part, supplier
where lo_orderdate = d_datekey
and lo_partkey = p_partkey
and lo_suppkey = s_suppkey
and p_category = 'MFGR#12'
and s_region = 'AMERICA'
group by d_year, p_brand1
order by d_year, p_brand1;

-- Query 3
-- Drill down in time to just one month 

select c_city, s_city, d_year, sum(lo_revenue) as revenue 
from customer, lineorder, supplier, dwdate
where lo_custkey = c_custkey
and lo_suppkey = s_suppkey
and lo_orderdate = d_datekey
and (c_city='UNITED KI1' or
c_city='UNITED KI5')
and (s_city='UNITED KI1' or
s_city='UNITED KI5')
and d_yearmonth = 'Dec1997'
group by c_city, s_city, d_year
order by d_year asc, revenue desc;

結果を確認します。幾つかの要因を変更した事でパフォーマンスが改善されている事が確認出来ました。

<< Query 1 >>
時間: 5627.639 ms
時間: 2730.031 ms
時間: 2747.846 ms
時間: 2629.781 ms
時間: 2709.245 ms

<< Query 2 >>
時間: 11928.977 ms
時間: 8798.777 ms
時間: 8996.882 ms
時間: 9189.499 ms
時間: 8914.112 ms

<< Query 3 >>
時間: 15024.798 ms
時間: 11051.512 ms
時間: 11790.093 ms
時間: 11555.179 ms
時間: 11352.935 ms

検証結果を集計した比較内容は以下の通りとなりました。

benchmark-after2

8.結果検証

ここまでロード時間、ストレージ容量、クエリ実行について、チューニング前後の記録を比較する事で検証を重ねてきました。

今回このドキュメントを参考に実践してみた結果は以下のようになります。

benchmark-after3

データのロード時間

ソートキー、分散キー、列圧縮タイプを設定した事により、若干時間が増えました。特にこの場合、圧縮エンコード設定がされていないテーブル毎に対してロード時間が掛かる事になる自動圧縮を用いていたのもありました。今後、このテーブルに対する読み込みは高速化されていくでしょう。分散スタイル:ALLを指定した事も時間が増えた要因の1つとなります。

幾つかのテーブルについては分散スタイル:EVENまたはDISTKEYを使う事でロード時間を減らす事も出来ました。しかしこの決定はクエリパフォーマンスが遅くなる可能性もあるので状況に応じて云々。

ストレージ使用量

カラム圧縮の利用によるストレージ部分の改善部分の幾らかは分散スタイル:ALLの使用によって総裁されました。

繰り返しますが、分散スタイル:EVENもしくはDISTKEYの指定を使う事によって幾つかのテーブルについては改善を行う事が出来ますが、その替わりにパフォーマンスが犠牲になるでしょう。

分散

分散方法を選択・指定した結果、傾斜分布が無い事は確認出来ました。EXPLAINでのチェックによって、テストクエリに対する幾つかのデータ再分散が除去する事が出来ました。

クエリ実行時間

クエリパフォーマンスの改善は ソートキー、分散スタイル、圧縮エンコーディングの組み合わせによるものでした。

多くの場合、クエリパフォーマンスはクエリの書き換えやWLM(Workload Management)の設定によって改善する事が出来ます。より詳細な情報については、以下ドキュメントを御参照ください。

9.リソースのお掃除

クラスタは起動し続けている限り、料金が発生し続けます。今回検証用に構築したクラスタ環境であれば、忘れずにクラスタを削除しておきましょう。

クラスタは残すけれども今回使ったデータは要らないという場合は以下DROP TABLE文を実行し、テーブルを削除してください。

drop table part cascade;
drop table supplier cascade;
drop table customer cascade;
drop table dwdate cascade;
drop table lineorder cascade;

まとめ

以上、公式ドキュメント記載の実行記録でした。実際にサンプルデータを用いて問題点の特定・改善作業・検証を行う事で、パフォーマンスチューニングの勘所も掴み易くなる事が実感出来ると思います。

こちらからは以上です。