概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Faster JSON generation using PostgreSQL JSON function | BigBinary Blog
- 原文公開日: 2018/05/29
- 著者: Chirag Shah
- サイト: BigBinary
参考: JSON関数と演算子 -- 本記事で扱われている関数が掲載されています
RailsのJSON生成をPostgreSQLのJSON関数で高速化(翻訳)
RailsでのJSON生成方法は多種多様です。#to_json
メソッドはRailsに組み込まれていますし、jbuildergemやactive_model_serializers gemも同じ目的に使えます。
データベースのレコード数が増加するに連れて、Railsのレスポンス生成時間が大きく増大します。このボトルネックを追うと一般にJSON生成部分にたどり着きます。
最近私たちのアプリで、ページ読み込みに異常に時間がかかる問題が発生しました。そのページはサイトで最も訪問数が多いので、この読み込み時間は重大です。そのページは、あるレースとその出場レーサーや詳細なラップタイムを読み込みます。
レーサーが10〜15人でラップが30〜50程度の短距離レースなら問題ありませんでしたが、レーサーが50〜80人でラップが700〜800の耐久レースともなると、読み込み時間のボトルネックが顕在化しました。
JSON生成のベンチマークを取ったところ、バックエンドが容疑者であることが判明しました。
この問題の修正方法を調べているうちに、PostgreSQLのJSON関数を見つけました。
JSON生成はPostgreSQL 9.2以降で組み込みとしてサポートされており、row_to_json
関数やarray_to_json
関数を用います。この2つをもう少し詳しく見てみましょう。
row_to_json
row_to_json
は、各行をJSONオブジェクトとして返します。
select row_to_json(laps) from laps;
{"id":1,
"number":1,
"position":4,
"time":"628.744",
"flag_type":"Green"
}
.
.
.
サブクエリを用いて、必要な属性やカラムだけをフェッチすることもできます。
select row_to_json(lap)
from (
select id, number, position, time, flag_type from laps
) lap;
{"id":1,"number":1,"position":4,"time":"628.744","flag_type":"Green"}
{"id":2,"number":2,"position":4,"time":"614.424","flag_type":"Green"}
.
.
.
array_to_json
array_to_json
を理解するには、最初にarray_agg
という集約関数を調べなければなりません。集約関数は、入力値のセットから単一の結果を算出するもので、sum
やmin
やmax
も集約関数です。array_agg
はすべての入力値を結合してPostgreSQLのarrayにまとめます。
select array_agg(lap)
from (
select id, number, position, time, flag_type from laps
) lap;
{"(1,1,4,\"628.744\",\"Green\")","(2,2,4,\"614.424\",\"Green\")", ... }
PostgreSQLのarrayをISONに変換するには、array_to_json
関数を次のように用います。
select array_to_json(array_agg(lap))
from (
select id, number, position, time, flag_type from laps
) lap;
[{"id":1,
"number":1,
"position":4,
"time":"628.744",
"flag_type":"Green"},
...]
より複雑な例
上の2つの関数を組み合わせてカスタムJSONレスポンスを生成できます。
select row_to_json(u)
from (
select first_name, last_name,
(
select array_to_json(array_agg(b))
from (
select number, position, time, flag_type
from laps
inner join racer_laps
on laps.id = racer_laps.lap_id
where racer_laps.racer_id = racers.id
) b
) as laps
from racers
where first_name = 'Jack'
) u;
{
"first_name": "Jack",
"last_name": "Altenwerth",
"laps": [
{
"number": 1,
"position": 4,
"time": "628.744",
"flag_type": "Green"
},
{
"number": 2,
"position": 4,
"time": "614.424",
"flag_type": "Green"
},
...
]
}
関数をRailsで使う
上述の関数をRailsで使うには以下のようにします。
query = <<~EOQ
select row_to_json(u)
from (
select first_name, last_name,
(
select array_to_json(array_agg(b))
from (
select number, position, time, flag_type
from laps
inner join racer_laps
on laps.id = racer_laps.lap_id
where racer_laps.racer_id = racers.id
) b
) as laps
from racers
where first_name = 'Jack'
) u;
EOQ
generated_json = ActiveRecord::Base.connection.execute(query).values;
puts generated_json
{
"first_name": "Jack",
"last_name": "Altenwerth",
"laps": [
{
"number": 1,
"position": 4,
"time": "628.744",
"flag_type": "Green"
},
{
"number": 2,
"position": 4,
"time": "614.424",
"flag_type": "Green"
},
...
]
}
上の方法でJSONを生成すると、Railsでの通常のJSON方法に比べて冗長で読みづらくなりますが、その代り高速です。
導入の結果
レーサーのページでPostgreSQL関数を用いてJSONを生成したところ、以下の改善が見られました。
短距離レース(レーサーが10〜15人でラップが30〜50程度)の場合、APIの平均レスポンスタイムは40ms
から15ms
に短縮されました。
耐久レース(レーサーが50〜80人でラップが700〜800)の場合、APIの平均レスポンスタイムは1200ms
から20ms
と大きく短縮されました。
まとめ
問題がなければ、Rails wayでJSONを生成しましょう。パフォーマンスが問題視されるようになったら、データベースで使えるこの機能をためらわずに使いましょう。この場合パフォーマンスと複雑さのトレードオフになりますが、時にはトレードオフの価値があるものです。