毛無しさん@キレートレモン

Hive クエリを最適化する

を参考に、HiveQL が どんな Map/Reduce タスクに展開されるのかを想像しつつ(ソースは読んでないのであくまで想像)、 効率の良い Hiveクエリの書き方を考えてみる。

まずは、普通のクエリ

SELECT * FROM movie

は、どんな Map/Reduce タスクに変換されるんでしょうか?

hive で

> EXPLAIN SELECT * FROM movie;

とやってみると、

ABSTRACT SYNTAX TREE:
  (TOK_QUERY (TOK_FROM (TOK_TABREF (TOK_TABNAME movie))) (TOK_INSERT (TOK_DESTINATION (TOK_DIR TOK_TMP_FILE)) (TOK_SELECT (TOK_SELEXPR TOK_ALLCOLREF))))

STAGE DEPENDENCIES:
  Stage-0 is a root stage

STAGE PLANS:
  Stage: Stage-0
    Fetch Operator
      limit: -1

と返っくる。 たぶん、Stage-0 は「ただ吐き出す」ていうタスクなんだろう。

SELECT * FROM movie LIMIT 10 と比較してみる:

> EXPLAIN SELECT * FROM movie LIMIT 10;

ABSTRACT SYNTAX TREE:
  (TOK_QUERY (TOK_FROM (TOK_TABREF (TOK_TABNAME all_member))) (TOK_INSERT (TOK_DESTINATION (TOK_DIR TOK_TMP_FILE)) (TOK_SELECT (TOK_SELEXPR TOK_ALLCOLREF)) (TOK_LIMIT 10)))

STAGE DEPENDENCIES:
  Stage-0 is a root stage

STAGE PLANS:
  Stage: Stage-0
    Fetch Operator
      limit: 10

Stage-0 は、LIMIT の面倒だけ見ている気がする。

SELECT WHERE してみる

> EXPLAIN SELECT id FROM movie WHERE year = 2000 LIMIT 10;

ABSTRACT SYNTAX TREE:
  (TOK_QUERY (TOK_FROM (TOK_TABREF (TOK_TABNAME movie))) (TOK_INSERT (TOK_DESTINATION (TOK_DIR TOK_TMP_FILE)) (TOK_SELECT (TOK_SELEXPR (TOK_TABLE_OR_COL id))) (TOK_WHERE (= (TOK_TABLE_OR_COL year) 2000))))

STAGE DEPENDENCIES:
  Stage-1 is a root stage
  Stage-0 is a root stage

STAGE PLANS:
  Stage: Stage-1
    Map Reduce
      Alias -> Map Operator Tree:
        movie 
          TableScan
            alias: movie
            Filter Operator
              predicate:
                  expr: (year = 2000)
                  type: boolean
              Select Operator
                expressions:
                      expr: id
                      type: int
                outputColumnNames: _col0
                File Output Operator
                  compressed: false
                  GlobalTableId: 0
                  table:
                      input format: org.apache.hadoop.mapred.TextInputFormat
                      output format: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat

  Stage: Stage-0
    Fetch Operator
      limit: -1

たぶん mapper はこんなイメージかな…?

public void map(Integer key, Text row, OutputCollector<Integer, Text> output, ...) {
    Integer id = row.getId();
    Integer year = row.getYear();

    if (year == 2000) {
        output.collect(key, new Writable(id));
    }
}

GROUP BY してみる

> EXPLAIN SELECT year, count(*) FROM movie GROUP BY year;

ABSTRACT SYNTAX TREE:
  (TOK_QUERY (TOK_FROM (TOK_TABREF (TOK_TABNAME movie))) (TOK_INSERT (TOK_DESTINATION (TOK_DIR TOK_TMP_FILE)) (TOK_SELECT (TOK_SELEXPR (TOK_TABLE_OR_COL year)) (TOK_SELEXPR (TOK_FUNCTIONSTAR count))) (TOK_GROUPBY (TOK_TABLE_OR_COL year))))

STAGE DEPENDENCIES:
  Stage-1 is a root stage
  Stage-0 is a root stage

STAGE PLANS:
  Stage: Stage-1
    Map Reduce
      Alias -> Map Operator Tree:
        movie 
          TableScan
            alias: movie
            Select Operator
              expressions:
                    expr: year
                    type: string
              outputColumnNames: year
              Group By Operator
                aggregations:
                      expr: count()
                bucketGroup: false
                keys:
                      expr: year
                      type: string
                mode: hash
                outputColumnNames: _col0, _col1
                Reduce Output Operator
                  key expressions:
                        expr: _col0
                        type: string
                  sort order: +
                  Map-reduce partition columns:
                        expr: _col0
                        type: string
                  tag: -1
                  value expressions:
                        expr: _col1
                        type: bigint
      Reduce Operator Tree:
        Group By Operator
          aggregations:
                expr: count(VALUE._col0)
          bucketGroup: false
          keys:
                expr: KEY._col0
                type: string
          mode: mergepartial
          outputColumnNames: _col0, _col1
          Select Operator
            expressions:
                  expr: _col0
                  type: string
                  expr: _col1
                  type: bigint
            outputColumnNames: _col0, _col1
            File Output Operator
              compressed: false
              GlobalTableId: 0
              table:
                  input format: org.apache.hadoop.mapred.TextInputFormat
                  output format: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat

  Stage: Stage-0
    Fetch Operator
      limit: -1

Map Operator Tree の中に Reduce Output Operator があるのは、map task と reduce task 間のデータ転送量を小さくするために map task 側で集約関数(combiner)を呼ぶためだと思われる。

たぶん mapper はこんなイメージ…

public void map(Integer key, Text row, OutputCollector<Integer, Text> output, ...) {
    Integer year = row.getYear();

    output.collect(year, new Writable(key));
}

reducer (と combiner) は多分こんな感じ…

publick void reduce(Text year, Iterator<Text> values, OutputCollector<Text,BigInt> output, ...) {
    BigInt count = values.count();

    output.collect(<何かしらのKEY>, new Writable(new Row(year, count)));
}

問題は JOIN

> EXPLAIN SELECT a.id, b.rating FROM movie a JOIN score b on a.id = b.movie_id

ABSTRACT SYNTAX TREE:
  (TOK_QUERY (TOK_FROM (TOK_JOIN (TOK_TABREF (TOK_TABNAME movie) a) (TOK_TABREF (TOK_TABNAME score) b) (= (. (TOK_TABLE_OR_COL a) id) (. (TOK_TABLE_OR_COL b) movie_id)))) (TOK_INSERT (TOK_DESTINATION (TOK_DIR TOK_TMP_FILE)) (TOK_SELECT (TOK_SELEXPR (. (TOK_TABLE_OR_COL a) id)) (TOK_SELEXPR (. (TOK_TABLE_OR_COL b) rating)))))

STAGE DEPENDENCIES:
  Stage-1 is a root stage
  Stage-0 is a root stage

STAGE PLANS:
  Stage: Stage-1
    Map Reduce
      Alias -> Map Operator Tree:
        a 
          TableScan
            alias: a
            Reduce Output Operator
              key expressions:
                    expr: id
                    type: int
              sort order: +
              Map-reduce partition columns:
                    expr: id
                    type: int
              tag: 0
              value expressions:
                    expr: id
                    type: int
        b 
          TableScan
            alias: b
            Reduce Output Operator
              key expressions:
                    expr: movie_id
                    type: int
              sort order: +
              Map-reduce partition columns:
                    expr: movie_id
                    type: int
              tag: 1
              value expressions:
                    expr: rating
                    type: int
      Reduce Operator Tree:
        Join Operator
          condition map:
               Inner Join 0 to 1
          condition expressions:
            0 {VALUE._col0}
            1 {VALUE._col1}
          handleSkewJoin: false
          outputColumnNames: _col0, _col3
          Select Operator
            expressions:
                  expr: _col0
                  type: int
                  expr: _col3
                  type: int
            outputColumnNames: _col0, _col1
            File Output Operator
              compressed: false
              GlobalTableId: 0
              table:
                  input format: org.apache.hadoop.mapred.TextInputFormat
                  output format: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat

  Stage: Stage-0
    Fetch Operator
      limit: -1

処理の流れはたぶんこんな感じ:

  1. MAPPER

    movie テーブルと score テーブルから必要なカラムを抜きだす。

  2. REDUCER

    mapper からデータをコピーする。このとき、同じ結合キー (movie.id と score.movie_id) を持つデータは 同じ reduce task にコピーされる。 で、結合して、指定されたカラムを返す。

MAPJOIN してみる

HADOOP HACKS とかに、マップサイドジョンが紹介されていたので、試してみる。

> EXPLAIN SELECT /*+ MAPJOIN(a) */ id, rating FROM movie a JOIN score b ON a.id = b.movie_id;

ABSTRACT SYNTAX TREE:
  (TOK_QUERY (TOK_FROM (TOK_JOIN (TOK_TABREF (TOK_TABNAME movie) a) (TOK_TABREF (TOK_TABNAME score) b) (= (. (TOK_TABLE_OR_CO
L a) id) (. (TOK_TABLE_OR_COL b) movie_id)))) (TOK_INSERT (TOK_DESTINATION (TOK_DIR TOK_TMP_FILE)) (TOK_SELECT (TOK_HINTLIST (TOK_HINT TO
K_MAPJOIN (TOK_HINTARGLIST a))) (TOK_SELEXPR (TOK_TABLE_OR_COL id)) (TOK_SELEXPR (TOK_TABLE_OR_COL rating)))))

STAGE DEPENDENCIES:
  Stage-3 is a root stage
  Stage-1 depends on stages: Stage-3
  Stage-0 is a root stage

STAGE PLANS:
  Stage: Stage-3
    Map Reduce Local Work
      Alias -> Map Local Tables:
        a 
          Fetch Operator
            limit: -1
      Alias -> Map Local Operator Tree:
        a 
          TableScan
            alias: a
            HashTable Sink Operator
              condition expressions:
                0 {id}
                1 {rating}
              handleSkewJoin: false
              keys:
                0 [Column[id]]
                1 [Column[movie_id]]
              Position of Big Table: 1

  Stage: Stage-1
    Map Reduce
      Alias -> Map Operator Tree:
        b 
          TableScan
            alias: b
            Map Join Operator
              condition map:
                   Inner Join 0 to 1
              condition expressions:
                0 {id}
                1 {rating}
              handleSkewJoin: false
              keys:
                0 [Column[id]]
                1 [Column[movie_id]]
              outputColumnNames: _col0, _col3
              Position of Big Table: 1
              Select Operator
                expressions:
                      expr: _col0
                      type: int
                      expr: _col3
                      type: int
                outputColumnNames: _col0, _col3
                Select Operator
                  expressions:
                        expr: _col0
                        type: int
                        expr: _col3
                        type: int
                  outputColumnNames: _col0, _col1
                  File Output Operator
                    compressed: false
                    GlobalTableId: 0
                    table:
                        input format: org.apache.hadoop.mapred.TextInputFormat
                        output format: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat
      Local Work:
        Map Reduce Local Work

  Stage: Stage-0
    Fetch Operator
      limit: -1

たしかに reduce task がなくなっている。

https://cwiki.apache.org/confluence/display/Hive/LanguageManual+JoinOptimization には、MAPJOIN の説明が以下のようにされている。

MAPJOINs are processed by loading the smaller table into an in-memory hash map and matching keys with the larger table as they are streamed through. The prior implementation has this division of labor:

  • Local work:
    • read records via standard table scan (including filters and projections) from source on local machine
    • build hashtable in memory
    • write hashtable to local disk
    • upload hashtable to dfs
    • add hashtable to distributed cache
  • Map task
    • read hashtable from local disk (distributed cache) into memory
    • match records’ keys against hashtable
    • combine matches and write to output
  • No reduce task

Stage-3 で、movie テーブルを読み込んで hashtable を構築しているようだ。 で、Stage-1 の map task で、この hashtable を使いながら socre テーブルを読み込んでいくのだろう。

Map Side Join についてもう少し考えてみる

Hadoop本の中で、「map側結合」が紹介されていた。(p.254 8.3.1 map側結合) ここで説明されている map 側結合と、Hive の MAPJOIN はどうも違うようだ。

Hadoop本の map 側結合が Hive クエリ実行時に行なわれることはないのだろうか? Join 対象になる2つのテーブル (もしくは、サブクエリの結果) のデータが、 結合キーごとに同じデータノードに入っている状況。

うん。。

思いつかない。

その他の JOIN の最適化方法

とかもあるみたい。

Bucket Map Join

Join 対象のテーブルが bucketize されている場合、構築された hashtable を bucket key に基づいて、必要な map task にだけ配布するっぽい。

Sort Merge Bucket Map Join

Join 対象のテーブルが bucketize されていて、しかも sort されている場合、もっと早くなるみたい。

でも、sort するのが大変。

試してみた。

> set hive.input.format=org.apache.hadoop.hive.ql.io.BucketizedHiveInputFormat;
> set hive.optimize.bucketmapjoin = true;
> set hive.optimize.bucketmapjoin.sortedmerge = true;
> explain select  /*+ mapjoin(b) */ id, rating from movie a join score b on a.id = b.movie_id;

ABSTRACT SYNTAX TREE:
  (TOK_QUERY (TOK_FROM (TOK_JOIN (TOK_TABREF (TOK_TABNAME movie) a) (TOK_TABREF (TOK_TABNAME score) b) (= (. (TOK_TABLE_OR_COL a) id) (. (TOK_TABLE_OR_COL b) movie_id)))) (TOK_INSERT (TOK_DESTINATION (TOK_DIR TOK_TMP_FILE)) (TOK_SELECT (TOK_HINTLIST (TOK_HINT TOK_MAPJOIN (TOK_HINTARGLIST b))) (TOK_SELEXPR (TOK_TABLE_OR_COL id)) (TOK_SELEXPR (TOK_TABLE_OR_COL rating)))))

STAGE DEPENDENCIES:
  Stage-1 is a root stage
  Stage-0 is a root stage

STAGE PLANS:
  Stage: Stage-1
    Map Reduce
      Alias -> Map Operator Tree:
        a 
          TableScan
            alias: a
            Sorted Merge Bucket Map Join Operator
              condition map:
                   Inner Join 0 to 1
              condition expressions:
                0 {id}
                1 {rating}
              handleSkewJoin: false
              keys:
                0 [Column[id]]
                1 [Column[movie_id]]
              outputColumnNames: _col0, _col7
              Position of Big Table: 0
              Select Operator
                expressions:
                      expr: _col0
                      type: int
                      expr: _col7
                      type: int
                outputColumnNames: _col0, _col7
                Select Operator
                  expressions:
                        expr: _col0
                        type: int
                        expr: _col7
                        type: int
                  outputColumnNames: _col0, _col1
                  File Output Operator
                    compressed: false
                    GlobalTableId: 0
                    table:
                        input format: org.apache.hadoop.mapred.TextInputFormat
                        output format: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat

  Stage: Stage-0
    Fetch Operator
      limit: -1

すごい! reduce task が無い上に、hashtable の構築すら消えた!!

これが Hadoop 本で言っていた map側結合なのかもしれない。

Skewed Join

よく分からない。

その他、思ったこと

  • MAPJOIN はテーブル1つをまるごとメモリに載せないといけないので、 せいぜい数十メガから数百メガのテーブルじゃないと辛そう。 サブクエリの中で十分絞り込んだ後 JOIN するのが実用的かも。

  • Bucket Map Join は、既存のHiveテーブルに対して、後から bucketize するのは大変そう。 集計用の中間テーブルを作ることがあったら、検討してみるといいかも。

  • Semi Join を使うのも有効らしい

  • HADOOP HACKS の中で、UDF (User Define Fucntion) や UDAF (User Define Aggregate Function) の作り方が載っていた。 この辺をもっとサクサク使えるようになるとはかどりそうな気がした。 むしろ、使わないともったいない。

Hadoop のチューニングに関してまとめ

1効率の良い HiveQL を書くときに気をつけるべきことをまとめてみる。

以下に書いたことは、ほとんど全部自分なりの解釈なので、間違いが多々あると思います

Hadoop一般のチューニングに関しては、ここを参考にした。

http://www.cloudera.co.jp/jpevents/cloudera-world-tokyo/pdf/A3_Hadoop%E3%81%AE%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E8%A8%AD%E8%A8%88_%E9%81%8B%E7%94%A8%E3%81%AE%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88.pdf

Hadoop本 を参考に、この資料の補足してみた↓

ラック内ネットワークとラック間ネットワークは何が違うのか?

(p.69 「ネットワークトポロジとHadoop」 / p.267 9.1.1 ネットワークトポロジ)

Hadoop は、データを読み書きする際にネットワークのトポロジやネットワーク構成を勘案しながら動作する。 読み込みを行う際には、クライアントになるべく 近い ノードから読み込むように調整するし、 書き込みを行う際には、データのレプリカが近い場所に集中してしまわないように調整する。

2つのノード間のデータ転送速度が早いほど、この2つのノードが 近い と見做される。 Hadoop は、ノード間の距離をネットワーク構成から算出している。 つまり、「同じラックにある2つのノードは互いに近い = 転送速度が早い」ものとして処理を進める。

そのため、同じラック内にあるのに実際には転送速度が遅い、ということになると、 Hadoop が効率良くデータを処理することができなくなってしまう。

スレーブノードでは、なぜ RAID0 を組んではダメなのか?

(p.266 「RAIDを使わないのはなぜ?」)

  • HDFSはノード間の複製によって冗長性を持つので、RAIDで冗長性を提供してもらう必要がない。(←弱い)
  • RAID0 でデータロストが発生した事例あり

マスターノードでRaidを組まないと何が起きるのか?

マスターノード (= ネームノード) は、クラスタ上のどのノードにどのファイルのデータが保存されているかを管理している。 マスターノードのデータが壊れると、HDFSからファイルデータを読み取ることができなくなってしまう。

そのため、マスターノードは信頼性を確保しておく必要がある。

マスターとスレーブで、求められるCPU性能が違うのはなぜ?

実際に計算するのはスレーブノード。 マスターノードは、クライアントの要求を受けて適切なスレーブノードに指令を出すのが仕事。 だから、マスターノードはたいしてCPU性能を必要としない。

ECCとはなんぞ?

Error Check and Correct memory

メモリに誤った値が記録されていることを検出し、正しい値に訂正することができるメモリモジュール。

なんで圧縮したら「速度」があがるの?

マップタスクからの出力は一時的にディスクに保存される。 レデュースタスクからの出力もディスクに保存される。

一般的には、Hadoopはかなり大きなデータを扱うので、出力結果を圧縮しないままディスクに書き込む時間よりも、 (圧縮する時間 + 圧縮データをディスクに書き込む時間) の方が短くなる。 読み込む時も同様に、非圧縮のデータを読み出す時間よりも、(圧縮されたデータを読み出す時間 + 解凍する時間) の方が短かくなる。

ブロック数(サイズ)とヒープサイズの関係は? (16枚目)

ブロック数が小さい = ブロックサイズが大きい = ディスクから1回に読み込むデータのサイズが大きい = ディスクへのアクセスが減る

ヒープサイズが大きい = 計算結果を沢山メモリに置いておける = 計算結果をディスクに書き出すことが減る = ディスクへのアクセスが減る

速い

フェデレーション?て何?

http://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/Federation.html

The prior HDFS architecture allows only a single namespace for the entire cluster. A single Namenode manages this namespace. HDFS Federation addresses limitation of the prior architecture by adding support multiple Namenodes/namespaces to HDFS file system.

In order to scale the name service horizontally, federation uses multiple independent Namenodes/namespaces. The Namenodes are federated, that is, the Namenodes are independent and don’t require coordination with each other. The datanodes are used as common storage for blocks by all the Namenodes. Each datanode registers with all the Namenodes in the cluster. Datanodes send periodic heartbeats and block reports and handles commands from the Namenodes.

ネームノードの性能をスケールさせるために、HDFSファイル空間を2つに分けてしまって2つのネームノードでそれぞれ担当しよう、という話のようだ。

ネームノードの冗長化とはまた別の話なのかな?

「ディスクのマウントポイントごとに別々のディレクトリをカンマ区切りで指定すること」てどういう意味?(21枚目)

1つのデータノードのストレージはいくつかのディスクで構成されている。 ディスクが複数あってそれぞれにデータが分散して保存されているから、 データノードは処理を並列して行うことができる。

で、データノードは dfs.datanode.data.dir に指定されたディレクトリに対してラウンドロビンで書き込んでいくから、 dfs.datanode.data.dir には、複数あるディスクのそれぞれのマウントポイントを指定しないとダメだよ、ということ。

ちなみに、JBOD っていうのは、http://ja.wikipedia.org/wiki/JBOD 参照。

「タクク数 > ディスク数」てどういう状態?(25枚目)

それぞれのタスクは、(普通は)ディスクからデータを読みとってから計算をはじめる。 「タクク数 > ディスク数」になってしまうと、並行してディスクアクセスを行なえるスレッド数(プロセス数?)の限界を越えて、 タスクノードがディスクアクセスを要求してくることになる。

で、ディスクアクセスが詰まってしまう。

ActiveRecord #1

前回 Arel が SQL を構築するところを読んだので、次は ActiveRecord がデータベースを操作するときに、どのタイミングで SQL を構築しているのかを見たいと思います。

このサンプルで、User.where(...)の時点でSQLが発行されてしまうと、その後に続く.limit(10)が反映されないはずじゃないか、というのか今回の疑問です。


まず、#whereの場所を探します。

ActiveRecordはざっくりこんなかんじになっています↓

Module#delegate

ref. http://api.rubyonrails.org/classes/Module.html#method-i-delegate

delegate は、active_supportの中でModule#delegateとして定義されています。

上の例では、Hoge#methodの呼び出すと、Hoge#targetが返すオブジェクトのmethodメソッドが呼ばれます。

Concern#included

includedメソッドも同じくactive_supportで定義されており、 そのモジュールがincludeされた時に、ブロックの中身が実行されます。


というわけで、Relation#whereが呼び出されることが分かります。

Relation#where

build_whereの中身を追っていくと、 PredicateBuilder#build_from_hash内の以下のコードが実行されることが分かります。

上のコード内のdefault_tableには Arel::Tableのインスタンスが入っています。

とういわけで、User.where(:name => "John")を実行したとき、 Relation#build_whereの結果は[table[:name].eq("John")]となります。 これがRelation#where_valuesに格納されます。

以上で、ActiveRecord::Base#whereの呼び出しは終わりです。

まとめると、ActiveRecord::Base#whereを呼び出すと、Relationクラスのインスタンスが返されます。 その中にはtable[column].eq(value)たちが入った配列が格納されています。

Relation#limit

Relation#limitも同じかんじです。

で、いつSQL?

さて、User.where(..).limit(10)の結果、Relationクラスのインスタンスが返ってくることは分かりましたが、 これがいつSQLに変換され、データベースに向けて投げられるんでしょうか?

whereの検索結果が必要になるのはどんな時でしょうか?

大抵の場合、検索結果から個々のUserクラスのインスタンスを取り出すときかと思います。 このとき、検索結果に対しては Array としてアクセスすると思います。

というわけで、Relation#to_aについて見てみました。

scopeとかeager_loadingとかよく分からないことは置いておいて、デバッガで追いかけると、 @klass.find_by_sql(arel, @bind_values)が実行されることが分かります。

ということは、arelの値が問題になりそうです。

こんなかんじになってました。 このbuild_arelで、whereとかlimitに渡された引数たちを、 まとめてArelのオブジェクトに変換していってます。

arel.xxxが連なってる感じ、壮観です。

find_by_sql

この時点ではarelはSQLになっていません。

なので、find_by_sqlの中を追っていくことにします。

追っていくと、 ActiveRecord::ConnectionAdapters::DatabaseStatements#select_all に行き着きます。

DatabaseManager#to_sqlの中の visitor.accept(arel.ast) do ... というのが、 Arel::TreeManger#to_sqlと同じことをやっているのが分かります。

というわけで、ここに来て、Arelのオブジェクトが無事SQLに変換されて、 DBMに向けて投げられたことが分かりました。

めでたし。

to_aは誰が呼ぶ?

to_aを呼び出せば、SQLが発行されることは分かりましたが、 誰がどのタイミングでto_aを呼ぶのでしょうか?

そういえば、whereの検索結果に対しては、eachメソッドを呼べます。

eachがどこで定義されているのか調べてみました。

なるほど。 eachとかmapなどが呼ばれたタイミングで、to_aにデリゲートされると。

Relation#inspect

では、たとえば、irbの中で、> p User.where(...)などとしたときはどうなんでしょうか?

このときもto_aが呼ばれるのでしょうか?

http://doc.ruby-lang.org/ja/1.9.2/class/Object.htmlによると↓とのこ とです。

inspect -> String

  オブジェクトを人間が読める形式に変換した文字列を返します。

  組み込み関数 Kernel.#p は、このメソッドの結果を使用して オブジェクトを表示します。

pinspectの結果を表示するようです。

Relation#insepct

となってます。

やっぱり、to_aが呼ばれるようです。

まとめ

Arelのときもそうでしたが、 whereなどのメソッドが呼ばれた時点では単に引数だけを保存しておいて、 必要になった時点でSQLを発行するということでした。

「必要になった時点」の判断の仕方が面白いと思いました。

必要になった時点でto_aを呼ぶわけですが、eachからto_aにdelegateされるとか、 inspectの中でto_aを呼ぶとか。

しかし、whereとかをわざわざActiveRecordでラップする必要があるんですかね… RelationのなかでArelのオブジェクトを保持しておいて、whereの処理はArelにまかせて、 Relationto_aとかのまわりだけ面倒みたらだめなのかな…?

Linux From Scratch 始めます

前々からやる、やる、と言っていた Linux from Scratch に手をつけようと思います。

Linux from scratch (LFS) ていうのは、

Linux From Scratch(リナックス フロム スクラッチ、LFS)とは、Linuxを1から構築しようという試みである。すなわち、カーネルなども含めてすべて手作業でソースコードからコンパイルして作り上げる。

具体的には、まず現在動作しているLinuxシステム上で、LFSをコンパイルするためのシステムを構築する。そのシステム上で、実際に稼働するLFSのシステムをコンパイルし、実際に起動することが出来る状態にまでもって行くという手順を踏む。

ref. http://ja.wikipedia.org/wiki/Linux_from_Scratch

ということです。でもって、

LFS teaches people how a Linux system works internally Building LFS teaches you about all that makes Linux tick, how things work together and depend on each other. And most importantly, how to customize it to your own tastes and needs.

ref. http://www.linuxfromscratch.org/lfs/

楽しく勉強できる、ということのようです。

本家 : http://www.linuxfromscratch.org

準備

http://www.linuxfromscratch.org/lfs/downloads/stable/ から、LFS-BOOK-7.2.pdf もしくは、LFS-BOOK-7.2.tar.bz (HTMLが入ってる)をダウンロードします。このBOOKに従って粛々とビルドしていきます。

次に、適当なマシンにLinuxをインストールします。LFSやるなら、32bitマシンが無難ぽいです。

というわけで、Core 2 Quad のマシンにGentooをインストールしました。安心の最小構成です。 手元のノートからSSHでアクセスすることにします。

Gentooがインストールされたシステムを「ホスト」と呼びます。 ホスト側で最小限のシステムをビルドした後、新しく構築した側のシステムに移りビスドを進めていくという 流れです。

パーティション作成

LFSをビルドするために、新しいのパーティションを作ります。今回は10GBとしました。 スワップ領域、ブートパーティションはホストと共有することにました。

新しくつくったパーティションを/mnt/lfs以下にマウントし、そこにソースをダウンロードしてきます。

ダウンロードするべきソースのリストは、http://www.linuxfromscratch.org/lfs/downloads/stable/LFS-BOOK-7.2.tar.bz2 に含まれる wget-list にあります。md5sum も含まれています。

あとはユーザを作って、環境変数を調整したり… 詳しくはLFS-BOOK参照。

これで、ソースが手に入ったので、あとはひたすらビルドしてくだけなんですかね…?そうだといいですね。

とりあえず、今日はここまで。

Arel #2

arel-3.0.2 での以下のコードの実行過程を追い掛けています。

Table#[]

次に、books[:title] の部分について見ます。

Table::[]は、Arel::Attribute.newを呼びだしています。

AttributeStruct について

Structとは、、、

構造体クラス。Struct.new はこのクラスのサブクラスを新たに生成します。 個々の構造体はサブクラスから Struct.new を使って生成します。

ref. http://doc.ruby-lang.org/ja/1.9.3/class/Struct.html

クラスを作るコンストラクタって何だよ…

arel/attributes/attribute.rb では以下のように使われています。

Arel::Predications では例えば次のようなメソッドを定義しています。

  • Arel::Predications#not_eq
  • Arel::Predications#not_eq_any
  • Arel::Predications#not_eq_all
  • Arel::Predications#eq
  • Arel::Predications#eq_any

つまり、Attribute という構造体(のサブクラス)に対して、比較や算術演算などができるようになっている、ということです。

Table#[] はこの拡張された構造体を返していることが分かります。

つまり、books[:title].relationにはselfが、books[:title].nameには:titleが格納されます。 さらに、books[:title]Predicationsモジュールから取り込まれたeqというメソッドをもっています。

ちなみに、::Arel::Attribute.newとしていますが、Arel::AttributeArelモジュールの定数で、 実体はArel::Attributes::Attributeクラスです。

Predications#eq

Predications#eqは↓

Nodes::Equalityクラスは、Binaryを継承しています。

Binaryのコンストラクタは、単に引数を記憶しておくだけです。

この時点では、具体的な処理はまだ何もしていません。

eqメソッドはArel::Nodes::Equalitiyクラスのインスタンスを返します。

Class.newconst_set

話は逸れますが、binary.rbに以下のような記述がありました。

Module#const_setは、モジュールに定数を定義するメソッドです。

Class.new(supercass = Object)は、superclassを親クラスとして匿名クラスをつくります。

Nodeモジュールに、Binaryクラスのサブクラスを値として、各種バイナリオペレータに対応する定数を定義しています。

ぱっとみエグいです。

Table#where

books.where を呼びだすと、新たに SelectManagerクラスのインスタンスが生成され、 そのインスタンスが保持するwheresというリストに、引数が追加されます。

Expressions#eqと同様、引数を記憶しておくだけです。

SelectManager#to_sql

さて、具体的にSQLに変換している部分を見ていくことにします。

Table#where を実行すると、SelectManagerクラスのインスタンスを返します。

SelectManager#to_sql の呼び出しは、継承元の TreeManagerクラスに渡ります。

engine.connection.visitor ですが、このengineは最初に

ででてきたengineです。

デバッガで、visitorが現れる場所を探していくと、

に行きつきます。ActiveRecord側で何が起きているかは深く考えないことにして、 とにかく、Arel::Visitors::SQLite を見ていくことにします。

結局、TreeManager#to_sql は、Arel::Visitors::ToSql#acceptに行き着くことになります。

ちなみに、accept の引数@astは、books.whereを呼びだした時に生成された SelectManagerクラスのインスタンスで、これがbooks.whereに渡された引数を保持しています。

Visitors::Visitor

Visitors::ToSqlVisitors::Visitor を継承しています。

Hash.new {|hash, key| ...} はハッシュのデフォルト値を定めます。
ref. http://www.ruby-lang.org/ja/old-man/html/Hash.html

上のコードで、acceptに渡される引数はArel::Nodes::SelectStatementクラスのインスタンスでした。 したがって、dispatch[object.class] が実行された場合、得られる値は"visit_Nodes_SelectStatement"となります。

visitでは、sendメソッドを呼んでいます。sendObjectクラスのメソッドで、 obj.send(name,args)は、objnameメソッドをargsを引数として呼びだします。

つまり、ToSqlクラスのvisit_Nodes_SelectStatementというメソッドが呼ばれることになります。

ちなみに、Visitorクラスのサブクラスには、

  • DepthFirst
  • Dot
  • Informix
  • OrderClauses

などがあります。これらのクラスのインスタンスのvisitメソッドを呼びだしたときも、同様の処理が行われることになります。

To_Sql#visit_Nodes_SelectStatement

o.coresの中に、books.whereに渡された引数が格納されています。

ここで察しがつくかと思いますが、ここからvisitを再帰的に呼び出していきます。

o.wheresbooks.where に渡された引数 books[:title].eq(...) が格納されています。 books[:title].eq(...)のクラスはArel::Nodes::Equalityなので、8行目でvisit_Arel_Nodes_Equalityが呼ばれます。

visit o.leftの部分で、books[:title]に対してvisitが呼ばれます。

最終的に、"SELECT FORM books WHERE books.title = 'Head First Rails'"という文字列が得られるはずです。

まとめ

Arelに特徴的なのは、#where#eq というメソッドが呼ばれたときに、 特に具体的な変換は行わず、引数やselfを保持するインスタンスを返すだけ、ということではないでしょうか。

いってみれば、#where#eqなどを使いながら「SQL構文木」を構築していくのだと思います。

#to_sqlが呼ばれたときに、この「構文木」のノードや葉を対応するSQLに変換していくわけです。

Arel #1

Arelとは、SQLクエリを構築するためのrubyのライブラリです。

ActiveRecord(モデル層)とデータベースの間に立ち、 ActiveRecordにおけるメソッド呼び出しをSQLクエリに変換してくれます。
ref. http://gihyo.jp/dev/serial/01/ruby/0043

arel-3.0.2、activerecord-3.2.8 を使ってます。

目標は、次のコードの実行過程を理解すること。

books = Arel::Table.new :books
books = books.where(books[:title].eq('Head First Rails'))
puts books.to_sql

このコードを実行すると、SELECT FROM books WHERE books.title = 'Head First Rails'という文字列が返ってきます。


Arel を使うためには、データベースとのコネクションを作る必要があるのですが、 その部分は ActiveRecord のおまかせします。

require 'arel'
require 'sqlite3'
require 'active_record'

ActiveRecord::Base.configurations = {'development' => {:adapter => 'sqlite3', :database => 'books.sqlite3'}}
ActiveRecord::Base.establish_connection('development')

Arel::Table.engine = ActiveRecord::Base

さて、つらつらと読んでいくことにします。

でも、眠いので今日はここまで。

Rails Sourcecode Reading #3 – 準備

まずは、ソースコードを用意しました。 bundle を利用することにしました。

$ mkdir rails_sourcecode_reading
$ cd rails_sourcecode_reading
$ mkdir src

$ vi Gemfile

-----------------------------
# Gemfile
source "http://rubygems.org"

gem "rails", "3.2.8"
-----------------------------

$ bundle install --path="./src"

これで、src/ruby/1.9.1/gems 以下に、railsに関連するgemたちが全部入ります。

次に、ソースコードを読む環境を整えました。 もちろん、emacsで読んでいきます。 以下は、利用した elisp とか gem とかです。

まずは、ActiveRecord と Arel あたりから読んでいこうと思います。

Rails Sourcecode Reading #2

ruby の勉強と Webアプリケーションフレームワークの勉強のために、 Rails のソースコードをじっくり読んでいこう、と思い立ちました。

そろそろまじめにやろうと思います。

勉強したことは、このページに書いていきます。

ruby のバージョンは 1.9.3p194、rails のバージョンは 3.2.8 です。

Dzenで日本語

dzen で日本語を表示するまでのまとめ。

portage で提供されている dzen をインストールしたところ、日本語が化けた。

どうやら、xft のサポートをつける必要があるみたい。

ところが、通常のレポジトリで提供されているソースは xft に対応していないようだ。

というわけで、https://github.com/robm/dzen から最新版をもってきて、普通に make してインストールしたら、表示できました。

ちなみに、フォントを指定してやらないと、かなり汚い。アンチエイリアスも設定できた。

dzen2 -fn 'xft\
          \:IPAGothic\
          \:pixelsize=13\
          \:weight=regular\
          \:width=semicondensed\
          \:dpi=96\
          \:hinting=true\
          \:hintstyle=hintslight\
          \:antialias=true\
          \:rgba=rgb\
          \:lcdfilter=lcdlight\
          \'

一応ebuildをのっけてみる。。。

https://github.com/ojima-h/my-overlays/blob/master/x11-misc/dzen/dzen-9999.ebuild