💽

郵便番号APIのようなシンプルなREST APIをlambdaで作る時にマスターデータを何で持つべきか計測してみた

5 min read

こういうAPIを作らなければいけないときって割とよくあると思います(有料データベースと契約してて、csvでもらったデータをAPIで使えるようにするとか)

そんな時に一番シンプルな解決方法として思いつくのがaws lambda + API Gatewayになると思います

そんな時にふとした疑問が生まれました

元のcsvって何の形式で持つのが一番効率良いのだろう

こういうのって何となく適当に選んじゃうことが多いのですが実際に計測して選ぶことも大切なので色々計測してみました

別解

多分goあたりでプログラム本体に含めてコンパイルしちゃうのが早いと思いますが、今回はrubyでやりたかったのでそれ以外の解決策を考えてみます

準備

郵便局の出している住所 <-> 郵便番号データをサンプルとして使います
元データがcsvなのですがその時点で以下のようになっています

  • 行数:12万4500行
  • ファイルサイズ:10.6MB
  • 文字数:10,802,495文字(改行コード含む)

https://www.post.japanpost.jp/zipcode/download.html

1. csvでそのまま持つ

require 'csv'
Benchmark.bm 10 do |r|
  r.report 'CSV' do
    10.times do
      CSV.read('./sample.csv')
    end
  end
end

※ベンチマークの見方がわからない人向けに簡単に説明すると、右の「total 秒」かかっているという事がわかればOKです。高速なプログラムの場合1回では時間が短すぎてうまく計測が出来ないことがあったり、繰り返すことによって誤差をへらし平均を取る目的で10回とか100回とか同じプログラムを動かしてそれをx回で割って1回あたりの速さを求めることを良くやります。上の例であれば10.times……つまり10回繰り返しているので上の画像のtotalである6.6405を10で割って1回あたり0.66秒くらいかかってる事が分かります

もっと詳しく知りたい人はこの記事がおすすめです

https://qiita.com/scivola/items/c5b2aeaf7d67a9ef310a

csvで読み込むと→0.66秒

コメント:
rubyだからかな?と思ったのですがrubyのcsvパースは充分に高速だという記事がありました。つまりcsvを解釈する時点でこれくらいの遅さは避けられないのでしょう

APIを返すたびに最低でも660ms=地球5周分かかるというのはちょっとおそすぎるのでcsvをそのまま使うのはなしになりそうです

csvはrubyの標準ライブラリなのでgemをインストールしなくても使えると言うのはメリットなのですが逆に言うとそれしかメリットは無いので他の形式に変換したいですね

2. yamlで持つ

csv -> yaml

  • 行数:12万4500行 → 87万行
  • ファイルサイズ:10.6MB → 13.83MB
  • 文字数:10,802,495文字(改行コード含む) → 11,549,487文字(改行コード含む)
require 'yaml'
Benchmark.bm 10 do |r|
  r.report 'YAML' do
    10.times do
      YAML.load_file('./sample.yaml')
    end
  end
end

yamlで読み込むと→2.61秒

コメント:
yamlはファイルの持ち方的にどうしても肥大化します。ただ普段使っているyamlは読み込みが非常に高速なので個人的には予想外の結果でした。yamlもcsv同様にrubyの標準ライブラリに含まれるのでgemのインストールが必要ない=更新や追従しなくてもrubyだけアップデートすれば一生使える(一生とは言ってない)ので非常に楽ですが、この結果であれば素直にcsvのまま使うべきでしょう

3. jsonで持つ

csv -> json

  • 行数:12万4500行 → 1行(jsonはデータ形式的に1行で表現できる)
  • ファイルサイズ:10.6MB → 12.1MB
  • 文字数:10,802,495文字(改行コード含む) → 9,806,123文字
require 'json'
Benchmark.bm 10 do |r|
  r.report 'JSON' do
    10.times do
      File.open("./sample.json") do |f|
        JSON.parse(f.read)
      end
    end
  end
end

jsonで読み込むと→0.18秒

コメント:
jsonはパースが早いというイメージが有りましたが本当か???と思わず疑ってしまう速さ
上の書き方を何種類か変えてみましたが変わらず早いままでしたのでミスではなさそう
この速度で読み込めるならギリ使えそうですが、確実に180msかかると考えるとちょっと遅いような気がしてきますね

4. jsonで持つ oj.gem 編

csv -> json

  • 行数:12万4500行 → 1行(jsonはデータ形式的に1行で表現できる)
  • ファイルサイズ:10.6MB → 12.1MB
  • 文字数:10,802,495文字(改行コード含む) → 9,806,123文字
require 'oj'
Benchmark.bm 10 do |r|
  r.report 'JSON' do
    10.times do
      File.open("./sample.json") do |f|
        Oj.load(f.read)
      end
    end
  end
end

ojで読み込むと→0.13秒

コメント:
json.gemは遅いとよく言われるので逆に高速と言われるoj.gemでパースしてみる
早いけどそれでも130ms、ギリ使えそうな微妙なライン

5. marshalで持つ

csv -> marshal

  • 行数:12万4500行 → 58万行
  • ファイルサイズ:10.6MB → 15.49MB
  • 文字数:10,802,495文字(改行コード含む) → 13,351,185文字(改行コード含む)
Benchmark.bm 10 do |r|
  r.report 'Marshal' do
    10.times do
      File.open("./sample.marshal") do |f|
        Marshal.load(f.read)
      end
    end
  end
end

marshalで読み込むと→0.31秒

コメント:
rubyに組み込まれてるmarshalを使うと良いかも?とちょっと思ったので使ってみました
yamlよりもcsvよりも早いがjsonより遅いのでMarshal自身の知名度の低さからも「なんだこれ?」と思われる可能性もあるし採用できなそう

6.sqlite3

csv -> sqlite

  • 行数:12万4500行 → ***
  • ファイルサイズ:10.6MB → 11.27MB
  • 文字数:10,802,495文字(改行コード含む) → ***
require 'sqlite3'
Benchmark.bm 10 do |r|
  r.report 'SQLite3' do
    10.times do
      db = SQLite3::Database.new('./sample.db')
      db.execute('SELECT * from yubin')
    end
  end
end

sqlite3で読み込むと→0.32秒

コメント:
DBに一回保存してアップロードしてしまえば検索も出来るしSQLで良いから楽かなーと思い実験(流石にmysqlとかと接続するのはしんどいのでsqlite3)。300msは流石に遅すぎない?と思うかもしれませんが上のケースはあくまでも全件取得した際の例です。実際のケースとしては検索してx件を抜き出す~という使い方だと思いますので、次にそっちをやってみます

※ちなみに他のサンプルを「データにアクセス出来る状態にするためにメモリ上に乗せる」という条件だと捉えるならsqlite3では以下のように0.003秒ほどなので比べ物にならないくらい早いです

7.sqlite3で80件くらい抜く

csv -> sqlite

  • 行数:12万4500行 → ***
  • ファイルサイズ:10.6MB → 11.27MB
  • 文字数:10,802,495文字(改行コード含む) → ***
require 'sqlite3'
Benchmark.bm 10 do |r|
  r.report 'SQLite3' do
    10.times do
      db = SQLite3::Database.new('./sample.db')
      db.execute('SELECT * from yubin WHERE area LIKE "%豊島区%"')
    end
  end
end

sqlite3で絞り込みもやるなら→0.01秒

コメント:
10ms、僕らの知ってるいつもの速度って感じですね
そもそも元のcsvから簡単にsqlite3ファイルを生成出来るという点を考えるとこの方法が正解な気がします

参考

https://www.dbonline.jp/sqlite/sqlite_command/index7.html

全部

結論

当然ですがsqlite使っとくのが早かったです
gemをインストールできないとか、宗教上の理由でsql書けないとか、特殊な条件下であればmarshalが次点で早かったです

雑にcsvをそのまま使っちゃう事が多かった人はこれを気に再計測してみるのもよいかもしれません

Discussion

ログインするとコメントできます