あきろぐ

いろいろめもするよ🐈🐈🐈

対象日から過去1週間のユニークユーザー数を出したい

何かしらのイベントのデータ分析をする際に、特定イベントのデイリーのユニークユーザー数を出すだけでなく、その日から過去1週間のユニークユーザー数を出したい場合があるかと思います。 例えば、2023-04-16のユニークユーザー数と2023-04-16から過去1週間のユニークユーザー数(2023-04-09~2023-04-16)を出したいという場合などです。

このようなケースで、どのようにクエリを書けば求めたい数値が出せるか悩んだので、備忘録的に残しておきます。

環境

  • ツール:Redash
  • データソース:Snowflake

データ構造

シンプルに以下のようなデータ構造であった場合を想定します。

イベント日 (event_date) ユーザーID (user_id) イベント名 (event_name)
yyyy-mm-dd xxxx-xxxxx-xxxx test_event1
yyyy-mm-dd yyyy-yyyyy-yyyy test_event1
yyyy-mm-dd zzzz-zzzzz-zzzz test_event2

解決策

対象日から過去1週間のデータを引っ張るために、SnowflakeDATEADD関数を使います。

docs.snowflake.com

-- with句を使ってサブクエリを書く

WITH target_events AS (
  SELECT
    user_id,
    event_date
  FROM
    events
  WHERE
    event_name = 'test_event1'
),
target_dates AS (
  SELECT
    event_date
  FROM
    events
  GROUP BY
    event_date
)

-- target_datesのevent_dateカラムの値を使ってUUを算出
SELECT
  event_date,
  (SELECT COUNT(DISTINCT user_id) FROM target_events WHERE event_date = t.event_date) AS daily_uu, -- デイリーのユニークユーザー数
  (SELECT COUNT(DISTINCT user_id) FROM target_events WHERE event_date between DATEADD(day, -(7), t.event_date) and t.event_date) AS weekly_uu -- ウィークリーのユニークユーザー数(過去1週間なので-7)
FROM
  target_dates t
WHERE
  t.event_date between '{{ start_date }}'
  and '{{ end_date }}' -- redashのパラメータを埋め込む

もっとシンプルなクエリが書けそうではありますが、これで取得したいデータを得ることができます。

イベント日 daily_uu weekly_uu
yyyy-mm-dd 100 300
yyyy-mm-dd 200 400
yyyy-mm-dd 300 500

FactoryBotでデータを大量に作成する2パターンの速度比較をしてみた

背景

Rspecでテストを行う際にテストデータが大量に必要だったので、create_listを使ってデータ作成していましたが、create_listを使うよりbuild_listしたものをbulk_importした方が高速化できると教えてもらいました。

bulk_importActiverecord-Importというライブラリを入れるとバルクインサートができるようになるものです。

github.com

そこで、実際にどの程度高速化するのか気になったので、create_listを使う場合とbuild_listbulk_importを使う場合で速度検証してみました。

検証内容

  • 作成するデータ量は、5、10、100、200、300の5パターンとする
  • 計測するのはデータを作成するのにかかった時間(Time.currentをデータ作成前後で取得し差分とる)
  • 各パターンで5回の速度を計測したものの平均値を算出し、比較する
# create_listを使う場合
create_list(:article, 5)

# build_listとbulk_importを使う場合
Article.bulk_import build_list(:article, 5)

検証結果

データ量が300になると、2秒程度の差になりました。想定よりそこまで差がつかない結果となりました。

回数 create_listの場合(s) build_list & bulk_importの場合(s)
5 0.3082704 0.2623282
10 0.7855298 0.6998084
50 3.892437 3.3376432
100 7.7685084 7.0694844
200 15.8506986 14.4952762
300 22.8225408 20.3966336

Rspecのプロファイリングを有効にする--profileオプションを使ってテスト実行にかかった時間でも比較してみました。

※ 300データのみで計測(5回平均)

# rspecのテストはAPIレスポンスが200を返すかどうかの確認のみ
# 細かい部分は省略
subject { get articles_path }

it 'return 200' do
  subject
  expect(response.status).to eq 200
end
回数 create_listの場合(s) build_list & bulk_importの場合(s)
300 24.358 19.116

テストの実行時間でみると約5秒の差がつきました。

どちらも大きな差がでるほどではありませんでしたが、これが複数のexampleだったり大量のテストデータを何回も作成する場合であれば、大きな差になりそうです。

まとめ

どちらを使っていても大量にデータを作成する場合は、数に比例して遅くなってしまうので、例えばデータの数を閾値としてつかっていて、定数化しているならstub_constを使って定数をスタブ化し、低い閾値にした方がテストが遅くならずに効率的に実施できるかと思います。

rubydoc.info

Railsで時間をJSON形式の文字列に変換するときに気をつけたいこと

前提

Railscreated_atを含む文字列をJSONに変換したものを受け取った後、そのJSON文字列からcreated_atを取り出し、DBにデータを取りに行くような実装がされていたとします。 その際、想定通りのデータがDBから取得できずに困ったので、その調査&原因をまとめます。

# articleテーブルから1レコードを取得する
article = Article.last
  Article Load (6.0ms)  SELECT "articles".* FROM "articles" ORDER BY "articles"."id" DESC LIMIT $1  [["LIMIT", 1]]
=>
#<Article:0x000000010c6eae08
...

# articleのレコードを使ってjsonデータを生成する
data = { id: article.id, created_at: article.created_at }
=> {:id=>1040597104, :created_at=>Sun, 05 Mar 2023 21:34:37.707825000 JST +09:00}
json = JSON.generate(data)
=> "{\"id\":1040597104,\"created_at\":\"2023-03-05 21:34:37 +0900\"}"

問題

上記のようにJSON化されたデータを受け取り、パースした上でDBにデータを取りに行くと、articlesに含まれていてほしいレコードが存在していませんでした。

data = JSON.parse(json)
=> {"id"=>1040597104, "created_at"=>"2023-03-05 21:34:37 +0900"}

articles = Article.where('id < ? and created_at < ?', data["id"], data["created_at"])
  Article Load (1.8ms)  SELECT "articles".* FROM "articles" WHERE (id < 1040597104 and created_at < '2023-03-05 21:34:37 +0900')
=>
[#<Article:0x000000010c1d16d8
...

原因

created_atを含む文字列をJSON.generateするとミリ秒が切り捨てられるため、ほぼ同時刻にDBに保存されたデータは対象外となり取得できていませんでした。

解決策

created_atiso8601形式のStringに変換した後、JSON化すればミリ秒を保持することができます。

data = { 'id': article.id, 'created_at': article.created_at.iso8601(6) }
=> {:id=>1040597104, :created_at=>"2023-03-05T21:34:37.707825+09:00"}

# ミリ秒まで保持されている
json = JSON.generate(data)
=> "{\"id\":1040597104,\"created_at\":\"2023-03-05T21:34:37.707825+09:00\"}"

パースする時は、in_time_zoneメソッドでActiveSupport::TimeWithZoneクラスのオブジェクトに変換します。

data = JSON.parse(json)
=> {"id"=>1040597104, "created_at"=>"2023-03-05T21:34:37.707825+09:00"}

data['created_at'].in_time_zone
=> Sun, 05 Mar 2023 21:34:37.707825000 JST +09:00

参考

api.rubyonrails.org

docs.ruby-lang.org

Elasticsearchの調査系コマンド&便利な使い方

基本的なElasticsearchのコマンドは、たくさんまとまった記事があると思うので特に困らないと思いますが、プラスαとしてElasticsearchクラスタを運用していく上で知っていると便利かもと感じたコマンドや使い方を備忘録としてまとめてみました。

定期的に追加していくかもしれないです。

表示するカラムを絞り込む

例えば、クラスタに所属するノードの情報を表示させたい場合、cat nodes APIを利用しますが、デフォルトだとレスポンスは以下のようになります。

$ curl -X GET "localhost:9200/_cat/nodes?v"

ip           heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name
xxx.xx.x.xxx           65          97  19    0.08    0.12     0.09 mdi       -      node-1
xxx.xx.x.xxx            72          97  17    0.24    0.19     0.16 mdi       *      node-2

レスポンスに含めるのは必要な情報だけで良い場合、以下のようにクエリパラメータにhを使うことでカラムを絞り込むことができます。

# ip, cpu, heapPercentだけ表示
$ curl "localhost:9200/_cat/nodes?h=ip,cpu,heapPercent&v=true"

ip           heap.percent cpu
xxx.xx.x.xxx          80   8
xxx.xx.x.xxx           85   4

また、デフォルトでは表示されないカラムを指定し、レスポンスに含めることも可能です。表示できるカラムは公式ドキュメントを確認してください。

# heap.max, pidも含めて表示(選出は適当)
$ curl -X GET "localhost:9200/_cat/nodes?h=ip,heap.percent,heap.max,cpu,pid&v=true"

ip           heap.percent heap.max cpu pid
xxx.xx.x.xxx            69  495.3mb   2 4257
xxx.xx.x.xxx            65  495.3mb  11 3686

www.elastic.co

特定カラムの昇順、降順でソートする

ノードのリソース使用状況を確認する上で、使用率が高いもの順に表示させたいというユースケースもあると思います。その場合、クエリパラメータsを使うことで降順または昇順にソートさせることができます。

# heapPercent降順で表示
$ curl -X GET "localhost:9200/_cat/nodes?h=ip,heap.percent,cpu&s=heap.percent:desc&v=true"
ip           heap.percent cpu
xxx.xx.x.xxx            84  10
xxx.xx.x.xxx            61   5

シャードがUNASSIGNEDになった原因を調べる

Elasticsearchクラスタを運用する上で、基本的に可用性担保のためアロケーションを有効化している場合が多いかと思います。 ノードに障害が発生しても自動的に正常なノードにシャードがアロケーションされるようになっているので、アロケーションによってシャードが配置されないことは稀にしか発生しないと思いますが、何かしらの作業によってシャードがUNASSIGNEDになってしまい困った・・・という場合に使えるコマンドが、cluster allocation explain APIです。 このコマンドは、UNASSIGNEDになってしまった原因について確認することができます。

UNASSIGNEDが発生する原因は、ノード数に対してインデックスのレプリカ数が多かったり、アロケーションが無効化されていることによってシャードが配置できない等いくつか挙げられます。

# レスポンスは公式ドキュメントの例
$ curl localhost:9200/_cluster/allocation/explain?pretty

{
  "index" : "my-index-000001",
  "shard" : 0,
  "primary" : true,
  "current_state" : "unassigned",                 
  "unassigned_info" : {
    "reason" : "INDEX_CREATED",                   
    "at" : "2017-01-04T18:08:16.600Z",
    "last_allocation_status" : "no"
  },
  "can_allocate" : "no",                          
  "allocate_explanation" : "cannot allocate because allocation is not permitted to any of the nodes",
  "node_allocation_decisions" : [
    {
      "node_id" : "xxxxxx",
      "node_name" : "node-0",
      "transport_address" : "127.0.0.1:9401",
      "node_attributes" : {},
      "node_decision" : "no",                     
      "weight_ranking" : 1,
      "deciders" : [
        {
          "decider" : "filter",                   
          "decision" : "NO",
          "explanation" : "node does not match index setting [index.routing.allocation.include] filters [_name:\"nonexistent_node\"]"  
        }
      ]
    }
  ]
}

www.elastic.co

アロケーションの有効化、無効化

例えば、ノードの入れ替え作業で一時的にアロケーションを無効化したい場合などに使えます。

現在のアロケーション設定の確認および変更は、cluster APIを使います。 cluster.routing.allocation.enableに設定できるのは、allprimariesnew_primariesnoneの4つです。allは全てのシャードがアロケーション対象になります。

www.elastic.co

# アロケーション確認(有効な場合、cluster.routing.allocation.enable = allなどnone以外)
$ curl localhost:9200/_cluster/settings?pretty

{
  "persistent" : {
    "cluster" : {
      "routing" : {
        "allocation" : {
          "enable" : "all"
        }
      }
    }
  },
  "transient" : {
    "cluster" : {
      "routing" : {
        "allocation" : {
          "exclude" : {
            "_name" : ""
          }
        }
      }
    }
  }
}
# アロケーション無効にする(cluster.routing.allocation.enable = node)
$ curl -XPUT localhost:9200/_cluster/settings -d '{
"persistent": {"cluster.routing.allocation.enable": "none"}
}'

# 確認
$ curl localhost:9200/_cluster/settings?pretty
{
  "persistent" : {
    "cluster" : {
      "routing" : {
        "allocation" : {
          "enable" : "none"
        }
      }
    }
  },
  "transient" : { }
}

特定ノードにアロケーションされないようにする

ノードの入れ替え作業で特定ノードにアロケーションされないようにするときに使うコマンドです。 cluster.routing.allocation.exclude._nameにdataノード名を指定することでそのノードにアロケーションされないようになります。

curl -XPUT localhost:9200/_cluster/settings -d '{
  "transient": {
    "cluster.routing.allocation.exclude._name": "dataノード名"
  }
}'

ノード名(_name)だけでなく_ip_hostなどで指定することも可能です。

www.elastic.co

参考

Elasticsearch関連の本はこちらがおすすめです。