[Looker]Refimentsを駆使して1ランク上のLookMLを書いていく

リファインしてこ
2020.08.14

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

大阪オフィス所属だが奈良県でリモートワークしている玉井です。

今回は、Lookerのバージョン7.6から出たRefinementsという機能をご紹介します。

公式情報

Refinementsとは

既存のViewやExploreをいじることなく、それらに追記・編集することができるものです。

…っていっても、おそらくピンとこないのではないでしょうか。というわけで、以降の「実際にやってみた」を見ていただくのが手っ取り早いと思います。

ちなみに、この時点で「Extendsと同じじゃないの?」と思った方、目の付け所がなかなかいいですね。Extendsとの違いは後で言及します。

とりあえずやってみた

超簡単な使い方をやってみる

下記のようなViewがあったとします。見ればわかりますが、dimentionが3つ定義されています。

view: test_refinements {
  sql_table_name: xxxxxxx ;;

  dimension: author_id {
    label: "著者ID"
    type: string
    sql: ${TABLE}.author_id ;;
  }

  dimension: facebook {
    label: "SNS(Facebook)"
    type: number
    sql: ${TABLE}.facebook ;;
  }

  dimension: hatena {
    label: "SNS(はてブ)"
    type: number
    sql: ${TABLE}.hatena ;;
  }
}

で、ここからもう実際にRefinementsを使うのですが、別途下記のViewを作成します。

include: "/test_refinements/test_A.view"

view: +test_refinements {

  dimension_group: post {
    type: time
    timeframes: [
      raw,
      time,
      time_of_day,
      hour,
      hour_of_day,
      date,
      week,
      day_of_week,
      month,
      month_num,
      month_name,
      day_of_month,
      quarter,
      year
    ]
    sql: ${TABLE}.post_date ;;
  }
}

詳細は後で説明しますが、この状態でtest_refinementsをExploreとして定義して、実際に画面を見ると、下記の通りになります。

4つのフィールドが表示されていますが、posttest_refinementsには記述していません。postがあるのは、後に作成した+test_refinementsです。この時点で大体わかったかと思いますが、+test_refinementsに定義しているものが、test_refinements側に追記されている形になっています。

注目するのは下記2点。

  • test_refinementsをインクルードしている
  • Viewの名前が+test_refinements

Refinementsとは、元々のViewやExploreの名前にプラス記号をつけて、別途記述することで、元々のファイルに追記・編集する機能となります。元のView等をいじることなく、そのViewを元に追記していくことができます。

ちなみに、Refinements側で、元ファイルと競合する記述をした場合(同じ名前のdimentionを書いた時など)は、Refinements側が優先されます(要するに上書き)。

複数記述してみる

Refinementsは複数書くことができます。先程のRefinementsに、さらに追記するとこんな感じになります。

include: "/test_refinements/test_A.view"

view: +test_refinements {

  dimension_group: post {
    type: time
    timeframes: [
      raw,
      year
    ]
    sql: ${TABLE}.post_date ;;
  }
}

view: +test_refinements {

  measure: count {
    label: "投稿本数"
    type: count
    drill_fields: []
  }

}

投稿本数というmeasureが追加されています。

Refinementsの適用順

適用(Refinementsが追記)される順番ですが、上から順番に適用されます。例えば、下記の場合、measureのcountに付与されるlabelは「投稿されたブログの本数だぜ」になります。

view: +test_refinements {
  measure: count {
    label: "投稿本数"
    type: count
  }
}

view: +test_refinements {
  measure: count {
    label: "投稿されたブログの本数だぜ"
    type: count
  }
}

また、同一オブジェクトに対するRefinementsを、複数ファイルにまたがって記述している場合、Modelファイルにそれらのファイルをインクルードしますが、それも「上からインクルードしている順」に適用されていきます。

下記の場合だと、test_A.view後に適用されます。上書がかれる記述があった場合は、test_A.viewのものが適用されます。

include: "/test_refinements/test_Z.view"
include: "/test_refinements/test_A.view"

ですので、Refinementsを使っているViewファイルをインクルードする場合、ワイルドカードでまとめて指定するのは避けるべきです(順番がわからなくなる)。ちょっと面倒ですが、ファイル毎に記述しましょう。

Exploreにも使える

Viewファイルの例ばかり書いてきましたが、ExploreもRefinementsの対象です。下記の場合、test_refinementsのlabelは「Refinements Test」となります。

explore: test_refinements {}

explore: +test_refinements {
  label: "Refinements Test"
}

ちょっと混乱しそうなのですが、上記の場合、実際に定義されているExploreは1つだけですtest_refinementsというExploreに追記しているだけ…というところに注意です(+test_refinementsという新しいExploreが誕生しているわけではない)。

Refinementsの疑問

Extendsとの違いは?

ここまでくると、ますます「Extendsと一緒やん」ってなる方も多いと思います。しかし、両者は似てるようで実は違う機能です。

大きな違いをざっくりいうと下記の通り。

  • Extendsは元ファイルを(内部的に)コピーして、Extends先とマージする(新しいLookMLオブジェクトを生成する)
  • Refinementsは元ファイルに追記するだけ(新しいLookMLオブジェクトは生成しない)

Refinementsの方が動作としてはシンプルですね。公式ドキュメントにも言及されていますが、「今までExtendsでやってきたけど、Refinementsで代用できるかも。」というケースもあるかもしれません(ただし、まるまるExtendsの上位互換というものではないところには注意しましょう)。

ちなみに、ExtendsとRefinementsは併用できます(具体例は別ブログで書きたい所存…)。

いつ使うの?(ユースケース)

例えば、下記のようなものが考えられます。

  • 流用したいLookMLがあるが、元ファイルに手を入れたくない場合
  • 他Projectからインポートしたファイルを流用したい場合
  • 既存のLooker Blockを追記していきたい場合
  • dimentionとかmeasureがめちゃくちゃ多くて超長いViewを分割して管理したい場合

他にも色々あると思いますが、使い方の例の一つして、次はRefinementsの応用編をご紹介します。

Refinements応用編

自分の環境でいい例が浮かばなかったので、公式ドキュメントのサンプルコードを例に、Refinementsを使いこなす例を解説します。

Projectを階層化(レイヤー化)する

例えば、「FAA」というProjectを作成するとします。最初のLookMLはDBのスキーマから自動生成するとします。テーブル毎にViewファイルが生成されて、それらにダダーっとカラムがdimentionとして自動定義されますよね。この自動生成されたままの状態のViewをfaa_raw.lkmlとして残しておきます。

そして、それはそのままにしておきつつ、faa_basic.lkmlというファイルを作成します。下記はその例です。

include: "faa_raw.lkml"

explore: +flights {
  join: carriers {
    sql_on: ${flights.carrier} = ${carriers.name} ;;
  }
}

view: +flights {
  measure: total_seats {
    type: sum
    sql: ${aircraft_models.seats} ;;
  }
}

上記はあくまで例なので、ExploreもViewも一緒くたに書いてますけど、実際は、命名規則を決めた上で、ModelファイルやViewファイルに分割することになると思います。

重要なのは、この時点で、FAAというProject下のLookMLファイルには「DBから自動生成しただけの層」と「そこに独自の記述をRefinementsしている層」の2つが誕生しているというところです。後者の層(LookML)は、あくまでRefinementsなので元ファイルはそのままです。こうやって、Projectにレイヤーをもたせることで、LookMLの管理をわかりやすくできます。

分析担当のレイヤーを用意する

自動生成のLookML→ベースとなるLookML、ときたら、次はもうちょい高度な分析をするレイヤーを用意します。ここでもRefinementsが活躍します。

※公式のサンプルの順番を説明しやすいように変えています。

include: "faa_basic.lkml"

view: +flights {
  measure: distance_avg {
    type: average
    sql: ${distance} ;;
  }
  measure: distance_stddev {
    type: number
    sql: STDDEV(${distance}) ;;
  }
  dimension: distance_tiered2 {
    type: tier
    sql: ${distance} ;;
    tiers: [500,1300]
  }
}

view: distance_stats {
  derived_table: {
    explore_source: flights {
      bind_all_filters: yes
      column: distance_avg {field:flights.distance_avg}
      column: distance_stddev {field:flights.distance_stddev}
    }
  }
  dimension: avg {
    type:number
    sql: CAST(${TABLE}.distance_avg as INT64) ;;
  }
  dimension: stddev {
    type:number
    sql: CAST(${TABLE}.distance_stddev as INT64) ;;
  }
  dimension: distance_auto_tier {
    sql:
      CASE
        WHEN ${flights.distance} < ${avg} + ${stddev} * -2
          THEN CONCAT('T00 (-inf,', CAST(${avg} + ${stddev} * -2 AS STRING),')')
        WHEN ${flights.distance} < ${avg} + ${stddev} * -1
          THEN CONCAT('T01 [', CAST(${avg} + ${stddev} * -2 AS STRING),',',
                CAST(${avg} + ${stddev} * -1 AS STRING),')')
        WHEN ${flights.distance} < ${avg} + ${stddev} * 0
          THEN CONCAT('T02 [', CAST(${avg} + ${stddev} * -1 AS STRING),',',
                CAST(${avg} + ${stddev} * 0 AS STRING),')')
        WHEN ${flights.distance} < ${avg} + ${stddev} * 1
          THEN CONCAT('T03 [', CAST(${avg} + ${stddev} * 0 AS STRING),',',
                CAST(${avg} + ${stddev} * 1 AS STRING),')')
        WHEN ${flights.distance} < ${avg} + ${stddev} * 2
          THEN CONCAT('T04 [', CAST(${avg} + ${stddev} * 1 AS STRING),',',
                CAST(${avg} + ${stddev} * 2 AS STRING),')')
        WHEN ${flights.distance} >= ${avg} + ${stddev} * 2
          THEN CONCAT('T05 [', CAST(${avg} + ${stddev} * 2 AS STRING),',',
                'inf',')')
        ELSE
          'yea'
      END
    ;;
  }
}

explore: +flights {
  join: distance_stats {
    relationship: one_to_one
    type: cross
  }
}

上から順に見ていきます。

view: +flights

ここでflightsというViewに、いくつか新しいdimentionを足しています。

view: distance_stats

ネイティブ派生テーブルを定義していますが、このNDTの元となっているExploreはflightsです。このExploreのベースのViewはflightsです。ここがミソなのですが、NDTで定義している新しいカラムは、上記の+flightsでRefinementsとして追加されたカラムが使われています。

explore: +flights

上記のNDTが定義されたdistance_statsがjoinされています。名前を見ればわかる通り、このExploreもRefinementsですので、(単独のJOINではなく)flightsというExploreに新しいNDTを追加でJOINしている形となります。

まとめると…

既存ViewにRefinementsで追記→追記されたdimentionを使ってNDTを定義→既存ExploreにそのNDTをRefinementsで追加JOIN。…という流れです。

おわりに

機能単体としては簡単ですが、使いこなすのはなかなかムズそうです。