プログラマ38の日記

主にプログラムメモです。

DWH: データモデリング (1.ディメンションとファクトは、物理的に1:Nで結合する。)

この記事は、前回書いたデータモデリングの9つのことの1つ目です。

 

crmprogrammer38.hatenablog.com

 

まず、物理的に1:Nで結合するという点について以下を示しています。

・ディメンションとファクトで結合する際に、ディメンション側の結合キーはユニーク制約があること

 

ポイントはユニーク制約なので、いわゆるPK(ユニーク+not null)である必要はありません。データベース側が結合条件の項目がユニークであると認識できることが必要です。(要するに、PKか、Unique Indexのどちらかが必要。TERADATAであればUPIも使えます)

 

当たり前のように感じますが、データが複雑になればなるほど見落としがちになります。

 

例えば、次のような店舗毎に営業日が異なるカレンダを考えます。 

(年月日と店舗でユニークです)

f:id:crmprogrammer38:20170222111655p:plain

 

結合するファクトに、年月日と店舗の要素があれば、上記のカレンダとファクトは、1:Nで結合できています。

f:id:crmprogrammer38:20170222111706p:plain

[結合条件] これは1:Nでの結合条件

       店舗別カレンダ.年月日 = ファクト.年月日

and 店舗別カレンダ.店舗    = ファクト.店舗

 

ですが、ファクトに店舗の要素がなく、店舗別カレンダの代表店舗を指定する場合の結合は、データとして考えれば1:Nですが、結合条件としては物理的に1:Nでありません。

f:id:crmprogrammer38:20170222111725p:plain

[結合条件] データとしては1:Nだが、結合条件としては1:Nではない。

       店舗別カレンダ.年月日 = ファクト.年月日

and 店舗別カレンダ.店舗    = '固定値の条件'

 

データベースのオプティマイザ次第ですが、結合が1:Nであることを明確にしたほうが性能も安定します。

 そのため結合条件に代表店舗を指定して結合するのではなく、代表店舗のカレンダレコードで新規にカレンダを物理的に作成して、そちらを利用します。

f:id:crmprogrammer38:20170222111743p:plain

テーブルが増えていくことだけが難点ですが、これで性能が安定するなら大したことではないです。

 

DWH: スタースキーマをベースにあらためて考えてみたデータモデリングの9つのこと

DWHのデータモデルでとても有名な"スタースキーマ"。

でも、アプライアンスDBを導入したんだからきちんと正規化した方が後々使いやすいといった意見もあったりします。

 

私は、スタースキーマはとてもシンプルでわかりやすく、そして結合が少なくて使いやすい、何より性能が安定するモデルと考えています。

そこで、自分の思いを踏まえてスタースキーマをベースにしたデータモデリングについて書いていこうと思います。

 

[いわゆるスタースキーマの絵] ※以前の記事にも同じものを貼っています

真ん中の売上データのテーブルを一般的にファクトテーブル(以下、ファクト)そして、周りのカレンダ、商品、店舗、顧客のテーブルをディメンションテーブル(以下、ディメンション)という名前で呼びます。

f:id:crmprogrammer38:20170208123750p:plain

 

 

1.ディメンションとファクトは、物理的に1:Nで結合する。

2.ディメンションとファクトは、内部結合にする。

3.ヘッダと明細の構成のファクトは1つのファクトに結合する。

4.結合するキーは極力サイズを小さくする。

5.ディメンションを大きくし過ぎない。

6.ファクトは極限まで小さくする。

7.先週との比較値、先月との比較値、前年との比較値などは、BIツールの機能を利用する。(機能がないならデータマートを作ろう)

8.ナチュラルキーを基本とし、サロゲートキーはピンポイントで使う。

9.1から8にとらわれすぎない。

 

次からの記事で1つ1つを細かく記載します。

DWH: BI/DWHのプロジェクトの作業で思うこと

私の経験として、BI/DWHのプロジェクトは他の業務パッケージのプロジェクトに比べて難しくて失敗しやすいなーと考えています。

 

明確に、なぜ失敗しやすいのかといった分析結果があるのかもしれませんが、個人的に思うことをつらつらと書いていこうと思います。

 

1.データの種類と、データ量の多さ、そして複雑さ

まずこれですよね、何するにしてもデータ量が多くて、軽く環境つくって試してみることができない。

あと、同じようなマスタが乱立していたり、同じ項目名なのに意味が違ったり、本当に混乱してきます。

 

2.BIツールの良さに反する定型帳票

BIツールは、使う人によって見る切り口が違ったり、その時々で見たい粒度が違ったりする場合に、さくっと自分のみたい項目を選んでデータを見る機能があります。

仮説の裏づけをしてみてもいいし、数値だけから客観的に物事をとらえてもいい、そんな便利なツールだと思っています。(使う人の操作は項目選んで表示するだけですが、データを見ることはとても重要です)

 

でも、既存にある複雑な定型帳票を作るには向いていなかったりします。今ある定型帳票を廃止するのはとても難しく、無理やりBIツールで定型を出力できるように、専用のデータマートを用意する必要がでてきて、スケジュールは逼迫、設計書も陳腐化していきます。

 

3.作り終わった後の性能問題

開発フェーズが終わり、本番相当のデータで動かすと発覚する性能の問題。

数万件のデータがの単体テスト環境では問題なく動作してるけど、数億件のデータが入った環境では動かなくてプロジェクトは混乱を極めます。

 

[所感]

みんな一生懸命作業してるのに、やっぱり大変な状況になるんですよねー。

ほんとつらい、とりあえず統計情報をとったり、SQLの見直しをしたりなど対応してみますが、先の見えない作業はいやなものです。

油断していると、日次のバッチ処理時間が24時間を越え始めたり

 

うまく行ったときは大変なだけにとても充実感があります。それだけが支えです。。

 

雑記: DWHのアプライアンスDBについて思うこと

TERADATAについての記事を書いてきましたが、他のDWHのアプライアンスDBはどうなんだろうなと思いました。

 

ただ、調べると、Oracle exadataとTERADATA以外は情報が少ないので、この2つの争いになってるように思います。

 

TERADATAからOracle exadataに移行したり、その逆もあったりしてますが、Oracleは情報が多いのと、技術者が多いのがいいなと思います。もちろんexadataを扱う上での作法はあると思いますが、使い馴染んだOracleSQL、使いやすい関数がそのまま使えるのは魅力的です。

 

TERADATAは数々の実績に裏打ちされた実力があって、多くの企業に採用されています。ただ、情報が少ないのと、技術者がOracleより少ない。この部分のハードルが高いと感じています。

 

どちらを選んでもとてもいいDWHが作れると思いますが、最近の時代の流れは情報が沢山あるOracle exadataの方が有利なんだろうなーとか意味も無く考えています。

TERADATA: TERADATAにデータを入れるツールを作りました(ACCESSのデータをTERADATAにロードできます)

開発中に手持ちのExcelデータをTERADATAに入れたいことは多々あります。

ちょっとした件数(10万件程度)を入れるのに便利なツールの紹介です。

 

通常TERADATAに入れるやり方は、

1.CSVファイルにして、FASTLOADや、MULTILOADのスクリプトを書いてデータを投入する。

2.ACCESSに、TERADATAのODBCリンクテーブルを作成し、Excelからデータを貼る。

3.作成したいデータのinsert文を作成し、TERADATAへSQLを実行する。

 

上記のようなものだと思いますが、私は以下のような感想を持っています。

 

1の方法は、FASTLOADとMULTILOADのスクリプトの作成に手間がかかるのと、ExcelデータをCSVにする作業がある。後、そもそもFASTLOADはサーバにしかインストールしてないなんてこともあります。

 

2の方法は、MS製品なのでExcelのデータはACCESSに貼り付けできますが、リンクテーブル経由でデータを投入するのにすごい時間がかかります。

 

3の方法ではinsert文の作成をしないといけなくて、日付型のフォーマットも意識しないといけなくなります。

 

2の方法が一番便利だけど、数万件登録しようとするといつ終わるかわからないといった状況は避けたい。

そんな時に使えるツールを作りました。自分がプログラマ3年目あたりで作成したものですが、きちんとテストしています。

 

 

    

ダウンロードはこちらから 50MB近くあります。

[ツール詳細]

Windows用です。

・zipを解凍してから利用します。

java+jdbcで動きます。javaは7、jdbcはTERADATA13用があらかじめ入っています。

・フォルダ内のdataload.batに設定ファイル(xml)をドラッグして実行します。

f:id:crmprogrammer38:20170221125450p:plain

<設定ファイルの書き方>

 設定ファイルに以下を定義します。

接続元の情報

接続元からデータを取得するためのSQL(select)

接続先の情報

接続先にデータをロードするためのSQL(insert)

※ただし、設定ファイルをxmlにしたので、SQL(select)など条件に ">" や "<" を利用する場合はxmlエスケープ が必要です。

サンプル

 <?xml version="1.0" encoding="Shift_JIS" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>Sample設定ファイル</comment>

<!-- ソース情報 -->
<entry key="SELECT_SQL">
Select
textfield
,datefield
,timestampfield
,decimalfield
From
table1
</entry>
<entry key="SELECT_USR"></entry>
<entry key="SELECT_PWD"></entry>
<entry key="SELECT_URL">jdbc:odbc:Driver=Microsoft Access Driver (*.mdb);DBQ=D:\sampledb\access\sample.mdb</entry>
<entry key="SELECT_JDBCDRIVER">sun.jdbc.odbc.JdbcOdbcDriver</entry>
<entry key="SELECT_FETCHSIZE">1000</entry>

<!-- ターゲット情報 -->
<entry key="INSERT_SQL">
INSERT INTO table1 (
textfield
,datefield
,timestampfield
,decimalfield
) values (
?,?,?,?
)
</entry>
<entry key="INSERT_USR">SAMPLEUSR</entry>
<entry key="INSERT_PWD">SAMPLEPWD</entry>
<entry key="INSERT_URL">jdbc:teradata://samplehost/CHARSET=KANJISJIS_0S,DATABASE=Sample,TMODE=ANSI</entry>
<entry key="INSERT_JDBCDRIVER">com.teradata.jdbc.TeraDriver</entry>
<entry key="INSERT_COMMITSIZE">10000</entry>
</properties>

 ほぼ見たままの設定です。

もちろん、以前の記事で書いた通りinsertはexecuteBatchとしています。

 

crmprogrammer38.hatenablog.com

 

TERADATAお使いの方は、お試しください!

 

 

TERADATA: Aggregate Join Indexでさらに検索を早くする

Join Indexはクエリーリライト用にあらかじめ結合、集計しておく仕組み

TERADTAに、Join Indexという機能があります。

これはOracleのMaterialized Viewをクエリーリライトに特化したような機能で、あらかじめ結合や集計をした結果を保持しておく仕組みです。

もちろん、元のテーブルが更新されれば即座にJoin Indexも更新されます。(V4ぐらいでは、元のテーブルが更新されてもJoin Indexは更新されなかったような記憶がありますが、最近のバージョンでは問題なく更新されます)

 

BI/DWHでは、BIツールとTERADATAでシステムを構築した場合、なるべく加工(集計)をしないでデータを持っているほうが、利用者は使いやすいシステムとなります。

が、加工(集計)しないと性能に問題が生じる場合が多く、その時にJoin Indexを利用すると性能が改善できます。

 

例えば、次のようなデータモデルで、

f:id:crmprogrammer38:20170208123750p:plain

売上データは集計していなくて件数が2億件あるとした場合、顧客別の分析をしない次のような人には、売上データに顧客コードを除いて集計したJoin Indexが有効です。

・店舗別で過去数年に渡ってデータを見たい

・商品別で過去数年に渡ったデータを見たい

・店舗/商品別で当年のデータを見たい

 

Join Indexのサンプル
--1.売上テーブルの定義

CREATE SET TABLE Sample1
(
     Datestring     CHAR(8)     CHARACTER SET LATIN CASESPECIFIC
    ,ProductCode    CHAR(10)    CHARACTER SET LATIN CASESPECIFIC
    ,StoreCode      CHAR(10)    CHARACTER SET LATIN CASESPECIFIC
    ,AccoundCode    CHAR(10)    CHARACTER SET LATIN CASESPECIFIC
    ,Amount         DECIMAL(6,0) COMPRESS (0)
)
PRIMARY INDEX
(
     Datestring
    ,ProductCode
    ,StoreCode
)
PARTITION BY (ZEROIFNULL(( CAST((Datestring) AS INTEGER))) MOD 65535 ) + 1
;

--2.顧客コードを除いたJoin Indexを指定する。

CREATE JOIN INDEX JX_Sample1
AS
SELECT
     Datestring
    ,ProductCode
    ,StoreCode
    ,SUM(Amount) AS Amount
FROM
    Sample1
GROUP BY
     Datestring
    ,ProductCode
    ,StoreCode
PRIMARY INDEX (
     Datestring
    ,ProductCode
    ,StoreCode
)
PARTITION BY
(CAST(Datestring AS INTEGER) MOD 65535) + 1
;

Join Indexが有効となるのは、そのJoin Indexを作成した場合、どのくらいデータサイズが小さくなるかがポイントとなります。

Join Indexを作成した結果、さほどデータサイズが小さくならないならそのJoin Indexはあまり効果がありません。

 

個人的な考え

Join Indexは"あらかじめ結合する"、"あらかじめ集計する"、"あらかじめ結合して集計する"を行うことができます。BIツールを使う場合、常にJoin Indexで指定した結合キーをつかって結合するとは限らないので、"あらかじめ集計する"のが一番使いやすいと思っています。

 

また、下記のように年月日が決まると確実に年月が定まるような項目はファクトに持たせておき、年月に対応するマスタを用意しておくと、年月別で集計できてJoin Indexのサイズをさらに小さくできます。

f:id:crmprogrammer38:20170210131922p:plain

 

Join Indexのサンプル
--1.売上テーブルの定義 CREATE SET TABLE Sample2 ( Datestring CHAR(8) CHARACTER SET LATIN CASESPECIFIC ,YearMonth CHAR(6) CHARACTER SET LATIN CASESPECIFIC ,ProductCode CHAR(10) CHARACTER SET LATIN CASESPECIFIC ,StoreCode CHAR(10) CHARACTER SET LATIN CASESPECIFIC ,AccoundCode CHAR(10) CHARACTER SET LATIN CASESPECIFIC ,Amount DECIMAL(6,0) COMPRESS (0) ) PRIMARY INDEX ( Datestring ,ProductCode ,StoreCode ) PARTITION BY (ZEROIFNULL(( CAST((Datestring) AS INTEGER))) MOD 65535 ) + 1 ; --2.顧客コードを除き、さらに年月別別でJoin Indexを指定する。 CREATE JOIN INDEX JX_Sample2 AS SELECT YearMonth ,ProductCode ,StoreCode ,SUM(Amount) AS Amount FROM Sample2 GROUP BY YearMonth ,ProductCode ,StoreCode PRIMARY INDEX ( YearMonth ,ProductCode ,StoreCode ) PARTITION BY (CAST(YearMonth AS INTEGER) MOD 65535) + 1 ;

 テーブル定義では、年月日にパーティション指定されているけど、Join Indexでは、年月にパーティション指定ができるのでパーティションも使えます。

ただ、普通に集計軸の考えだと、年月日のカレンダの上位に年月のカレンダ(年月)をつなぐのが普通なので、年月のカレンダ(年月)を単独で結合するケースはあまりないと思います。

が、BIツールでショートカット結合が用意されていれば、Join Indexをうまく利用できると思います。(Business Objectsではショートカット結合があります)

 

Business Objectsユニバースでのショートカット結合

ショートカット結合で、カレンダ(年月)の年月と売上データの計上年月を結合する。

f:id:crmprogrammer38:20170210160826p:plain

ユニバースでショートカット結合を指定すると、例えば、カレンダ(年月)の年月項目と売上データの売上金額を選択すると、発行されるSQLはテーブルは、カレンダ(年月)と売上データとなる。

そして、結合条件は「カレンダ(年月).年月=売上データ.計上年月」となる。

そのため売上データに対して、計上年月で集計したJoin Indexを利用可能となる。

他のBIツールにもショートカット結合と同様の機能があるかはわかりませんが、Business Objectsなら使えるので、お試しください!

TERADATA: 他のテーブルの値で更新するUPDATE文

UPDATEの文法って、DB毎に癖があって覚えられないのでメモ。

 

UPDATE [更新するテーブル]
FROM [更新元テーブル]
SET [更新するカラム] = [更新元テーブル].[更新元カラム]
WHERE [更新するテーブル].[結合カラム] = [更新元テーブル].[結合カラム];

 

TERADATAでいうと、INSERTが圧倒的に早いのと、大量データのテーブルのPrimary IndexをUpdateしたりすると、とてつもなく時間がかかったりするので、Updateはあまり使う機会がない気がします。

 

でもdeleteもしくはdropしてから入れ直しだと、ワークテーブルの数が増えてしまうのがデメリットかもしれません。