ポモドーロ・テクニックを2か月間やってみての感想

2か月ほどポモドーロ・テクニックを使って仕事をしてみたので感想を書きます。

前提

仕事ではWebアプリケーション*1*2の開発が主な業務。最近、会社の勤務体制が原則として在宅勤務に移行した*3

なぜ始めたか

在宅勤務はとても便利だが、家の中でずっと同じ景色で仕事しているので、なんとなく気分として新鮮味がなくなってきていた。

また、これは在宅勤務の前からだが、仕事が進んでいない感覚に陥ることがたまにあった。実際進んでいないわけでもないのだが、これについては、いろいろな仕事をちょっとずつやっている状態だとこういう感覚になるのではという仮説があった。

上記のような経験があったので改善したいと思い、よく見るポモドーロ・テクニックに手を出してみることにした。higeponさんの記事にも影響を受けている。

第4回 プロダクティビティの鬼:継続は力なり―大器晩成エンジニアを目指して|gihyo.jp … 技術評論社

取り組みかた

原典を読む

ポモドーロ・テクニックといえば25分ずつ作業するぐらいのイメージだったので、原典を読んだ。原典はFancesco Cirilloの"The Pomodoro Technique"で、今回は邦訳を読んだ。

ポモドーロ・テクニック入門 | CCCメディアハウス - 書籍

主に「個人としての目標達成」の章を読んだ。とりあえず一人で使う予定だったので「チームとしての目標達成」は流し読みしかしていない。

具体的な方法論に加えて、テクニックについての考案者の思想がいろいろと書いてある。どんどん生まれては過ぎ去っていく時間を「ポモドーロ」という単位で抽象化して取り出すことで、時間が過ぎ去ることに対する不安感を軽減し、一定のリズムに基づいて作業を進められようになり*4、測定やふりかえりもやりやすくなるというのが本質らしい。

ここから後ろの内容はこの本に書いてある用語や方法に基づきます。

原理主義的にやる

だまされたと思って、ひとまず原理主義的にやる。「使う道具をシンプルに保つこと」が強調されていたので、あえて高度なツールなどは使っていない。原理主義的にやるときには次のものを用意する:

  • iOSビルトインの時計アプリのタイマー
  • 「今日やること」(To Do Today)シート
  • 「仕事の在庫」(Activity Inventory)シート
  • 「記録」(Record)シート

Cirillo氏はキッチンタイマーがいいと主張しているが、持っていないのでiOSのものを使う。

「今日やること」シートは大学ノートの1ページを1日分として管理している。ここに優先順位の昇順に並べたタスクの一覧、ポモドーロ完了/中断、突発的なタスクの一覧などを書いていく。ポモドーロを始めてしばらくしてペースがつかめてきたら1日の最初にタスクをポモドーロ数で見積もるようにしたほうが、ポモドーロの目的からして好ましいのだが、最初の数週間はとりあえず記録だけしていた。

今日やることシートの例
f:id:kymmt90:20200729003457p:plain:w300

「仕事の在庫」と「記録」はとりあえずNotionで管理している。両方Notionのdatabaseを使っているが、「仕事の在庫」のほうは単純なタスクリストになっていてNotionである必要もそれほどない。ただ、たびたび追加/削除するので紙ではなくソフトウェア的に管理している。「記録」は毎日の終わりにその日のタスク、見積もったポモドーロ数、実際のポモドーロ数を記録するのだが、これが原理主義的にやるとき一番面倒な作業のように思える。ただ、これがあることで、あるタスクが見積もりとどれぐらいずれているかを見て認識を補正し、やたらとポモドーロ数を消費するタスクを効率的にやる方法を考えるきっかけになる。

感想

中断や休憩を意識するようになる

原理主義的にやる場合、とにかく今やっていること以外のことを考えたり突発的なタスクが舞い込むと中断の印を「今日やること」に記録する必要があり、そのまま集中力を失うとポモドーロ中断なので、嫌でも中断という事柄について意識するようになる。具体的にはSlackが強敵で、作業中はSlackのウインドウを閉じるようにしているが、無意識にウインドウを開こうとするときがたびたびある。このことに気づけたこと自体がよかったとも言える。

また、原理主義だと休憩を25分ごとに3分から5分取ることになる。このときは逆に仕事のことを考えてはいけないので、できるだけ机から離れて家の中を歩いたり水を飲んで休むようにしている。これは結構よくて、毎回のポモドーロで仕事の新鮮味を取り戻すのに役立っている。

4セットという原則を守るのが難しい

ポモドーロは25分作業+5分程度休憩を4セット実行するのが原則*5だが、これを守るのはけっこう難しい。まず、1日のスケジュールを見て、できるだけ4セット連続で実行できるように作業を配置しないといけない*6。また、あるタスクが大きすぎるときは1日で完了できる粒度に分け、反対に小さすぎるときは他のタスクと組み合わせる必要があるので、仕事を始める前に、タスクの分割と優先順位をあれこれ考えるようになる。その代わり、計画を立てて「今日やること」シートに記録することで、確実に仕事が進んでいることが可視化されるので、仕事が進んでいない感覚の軽減には役立っている。また、4セットごとに長めの休憩や軽い散歩をしている。

一方で、事前に計画しても、突発タスクでポモドーロを無視して作業してしまい、リズムが乱れてしまうことがある。このあたりは中断要因を自分で制御できるようになることもテクニックの狙いの一つなので、うまくやっていく必要がある。

その他、先日までいわゆる機能追加のようなプロジェクトに入っていた。このようなケースでは事前にプロジェクトとしてのバックログを作っているので、その中で着手しているタスク、次に着手するタスク、コードレビューをうまく配置すれば、あとは原則にしたがい集中してポモドーロをこなしていけばいいので、かなり捗る感覚があった。

時間と戦ってはいけない

これは原典に書いてあった文句の受け売りだが、時間と戦う、すなわちポモドーロ数(=作業時間)を稼ごうとした時点で負けらしい。要は、雑なポモドーロを続けるよりは、一つ一つのポモドーロの質を高めるほうが好ましいということ。

実際、仕事が完了したかどうかと、完了したポモドーロの数は関係がない。たとえば、ポモドーロの終盤での中断が繰り返されると、仕事はそれなりに進むが、その仕事の完了ポモドーロ数は0のままということになる。そういう状態になっているときは、自分自身の状態や作業の進めかたを気にかけたほうがいいということだろうと思った。また、ポモドーロ・テクニック自体、作業時間を区切って集中するための方法としての側面が強調されがちで、実際能率を上げる効果があるが、作業の進めかたを考えたり、ふりかえったりするための手段という点もかなり考慮されているということに、実際やってみて気がついた。

おわりに

まだ続いているので、飽きるまでは続けます。

*1:https://shop-pro.jp

*2:https://colorme-repeat.jp

*3:https://pepabo.com/news/press/202006011200

*4:本の中では「時間依存の反転」と呼んでいる

*5:残存体力によっては3セットでもよい

*6:難しいときは1ポモドーロだけ分割してしまうことがある。ミーティングなどもポモドーロに算入したほうがいいのかもしれない

意図せず関連先のカラムでwhereしつつeager loadしたらクエリのパフォーマンスが極端に悪化した事例

問題

15個ぐらいのさまざまなクエリパラメータを検索条件として受け付けることができる一覧取得API(「Item取得API」とする)があった。

そのAPIで取得するItemは複数の関連を持っていた。また、関連先の取得時にN+1問題の対策が不十分だったので、取得するデータで必要な関連先を漏れなくincludesでeager loadした結果、ほとんどのケースでパフォーマンスが改善していた。

しかし、あるクエリパラメータ(qとする)を使うときだけ極端にAPIのパフォーマンスが悪化するという現象が見られた。

先に結論

よく言われていることですが、関連先の取得方法が複雑になりそうなら、preload/eager_loadで読み込み方法を明示的に指定して、意図したクエリを作るようにしましょう。

原因

元々のコードは次のようなイメージ*1:

# ItemsController
def index
  relation = current_user.items

  # クエリパラメータに基づくたくさんのwhere ...
  if params[:q]
    relation = relation.where(genre: { q: params[:q] })
  end
  # さらに続く...

  # 関連先を漏れなくincludes
  relation = relation.includes(
    :genre,
    :sub_items,
    user: :store,
    category: {
      user: :store,
      :sub_items
    }
  )

  # JSONにしてrenderして終わり
end

qは関連先テーブルのカラムをWHEREの条件として用いるときのパラメータ。そのパラメータをAPIに渡してwhereで絞り込みをかけるときだけ、関連先テーブルのカラムでWHEREすることになる。

このとき、Item取得と同時に関連先のテーブルの情報で絞り込みをかける必要があるので、includesはLEFT JOINを発行するeager_loadの挙動となる。すると、includesは指定したすべての関連先を(たとえ他の関連先テーブルのカラムでWHEREするわけではなくても)LEFT JOINで読み込むことになる*2*3

関連先のテーブルすべてをLEFT JOINすると、テーブルの構造が原因で本来取りたいデータセットの行数の3乗程度の数のデータを取得しようとして、DB側でメモリを使い尽くして*4エラーになっていた。

もう少し詳しい経緯

同じ関連先を複数回includesの引数としていた

モデルItemの関連先どうしも関連を持っていた。たとえば、Itemが持つsub_itemscategoryについて、それらどうしやuserへ関連を持つイメージ。これにより、includesの引数に同じ関連先が複数回出てくることがあった:

relation.includes(
  :genre,
  :sub_items, # 1回目
  user: :store, # 1回目
  category: {
    :sub_items # 2回目
    user: :store, # 2回目
  }
)

例えばuserstoreは関連元Itemのメソッドでuser.store.foo?のようにアクセスされるのに加えて、Category#barでもuser.store.bar?のようにアクセスされていた。このとき、たとえばcategoryのネストした関連先指定を省くと、Category#barの呼び出しが複数回あるときに都度クエリが発行されてしまうので、上のように書かざるをえない*5

同じテーブルが複数回LEFT JOINされていた

上記のように関連先を指定していてeager_loadを使うとき、includesへの指定の仕方が2通りあることから、sub_itemsは2通りの結合条件でLEFT JOINされる。細かい点は説明しないが、テーブルの構成的に、このように2回LEFT JOINすると本来取りたい行数の2乗の数データが取得されてしまっていた*6。実際の事例ではもう少し込み入った感じになっていて、実際に取りたい行数の3乗程度の行数を取得しようとしていた。

解決法

解決法は簡単で、問題となったクエリパラメータを使うテーブル以外の関連先テーブルはつねにpreloadを明示的に指定するだけ。qでの絞り込み用のカラムを持つ関連先テーブルはincludesのままにする。

def index
  relation = current_user.items

  # ...
  if params['q']
    relation = relation.where(genre: { q: params['q'] })
  end
  # ...

  # whereで使わない関連先を漏れなくpreload
  relation = relation.preload(
    :sub_items,
    user: :store,
    category: {
      user: :store,
      :sub_items
    }
  )

  # whereで使われるかもしれないのでincludes
  relation = relation.includes(:genre)

  # JSONにしてrenderして終わり
end

問題のパラメータを指定しなければpreloadで読み込んでくれるし、必要なときはeager_loadになる。

今回は、テストの規模ではそこまで遅くなることに気付けなかったのと、問題のパラメータ指定時のクエリを見ていなかったのが敗因。単純なケースならincludesのほうが考えることが少なくて便利だが、関連先の取得方法が複雑になってきたら横着せずにpreloadや(今回は使わなかったが)eager_loadで関連先の読み込み方法を明示的に指定したほうがよい。

*1:出てくるモデルや関連はすべて仮のもの

*2:https://github.com/rails/rails/blob/7b5cc5a5dfcf38522be0a4b5daa97c5b2ba26c20/activerecord/lib/active_record/relation/finder_methods.rb#L379

*3:ちなみに他のクエリパラメータだけ使うときは、関連元テーブルitemsのカラムでWHEREしており、関連先テーブルは複数のクエリで取得するpreloadの挙動になっていた

*4:数十万〜の行数を一発で取ろうとしていた

*5:必ずItemの関連から引くようにするなどより大きい範囲でコードを改善をするのがベターだが、いまはそれができないという仮定

*6:いわゆるproductとproduct variantとoptionのような構成でproductとproduct variantそれぞれにoptionをLEFT JOINしたのに近い cf. https://shopify.dev/docs/admin-api/rest/reference/products/product-variant

『SQLパフォーマンス詳解』を読んだ

RDBのインデックス作成の戦略や実行計画の読みかたにもう少し詳しくなりたいと思っていたので、この手の話題が体系的にまとめられている『SQLパフォーマンス詳解』を読んだ。今回は和訳版を公式サイトで購入したが、SQLのインデックスとそのチューニングについてのオンラインブックでも同じ内容が読める。

内容

200ページ程度と短くまとまった本であることからわかるように、RDBのインデックスとその効果的な使いかただけに焦点を当てて解説している。書籍で示されている目次とは異なるが、次のような内容にまとめられると思う:

  • インデックスを実現するBツリーについて
  • インデックスの種類(複合インデックス、式インデックス、部分インデックスなど)について
  • (本文では主にOracleだが)実行計画の読み解きかたについて
  • 実行計画をもとにインデックスとクエリをチューニングする方法について

第2章の「where句」の分量が割合として多く、そこでwhereを使うときのチューニング方法や注意点が説明されている。joinやソート(order by)は別の章でそれぞれ説明がある。また、RDBの主要な各プロダクトについて実行計画の読みかたが付録でついていて、その点も便利。

実験用の環境

動かしながらのほうが本の内容を理解しやすいので実験用の環境を作った。

https://github.com/kymmt90/sql-performance-explained

ふだんPostgresを使うことが多いので、Postgresで実験できるようにした。書籍中で出てくるスキーマのテーブルに合わせて、ランダムな値を持つ大量のデータをRubyスクリプトでCSVとして作成し、psqlからcopyコマンドで一括投入することで、データ量の多い環境を再現した。本を読みながら、この環境でインデックスやクエリを変更して実行計画が変わることを確認できたので、理解の促進に役立った。

所感

アプリケーションの開発者がクエリがどのように使われるかを把握しているので、インデックスを効果的に張ることは開発者の仕事である、ということが書いてあったのがよかった。また、読みながらちょっと脇道にそれて、Postgresでlikeでインデックスを使うときは演算子クラスを付与する必要があることや、Postgresの実行計画で出てくるBitmap Heap ScanのRecheckについて次の記事で知ることができたのもよかった。

taityo-diary.hatenablog.jp

RDBの本にしては短くまとまっていて(個人の感想です)、すぐに読めるので、RDBを使うアプリケーションを書く人はみんな読めばいいのではと思いました。

macOSのDay Oneにプレーンテキストをインポートする

  • macOS Catalina 10.15.4
  • Day One 4.11.1

とします。

Day Oneのインポート機能

Day Oneはさまざまな形式のデータをエントリとしてインポートできる。

Importing data to Day One | Day One Help

日付形式の問題

上述したサポートページには、プレーンテキストをインポートするときは次のような形式にすると書いてある。

    Date:    June 24, 2016 at 10:59:06 AM MDT

test


    Date:    June 25, 2016 at 10:59:06 AM MDT

testtest
...

これはDay Oneの言語設定が英語のときは正しいのだが、それ以外の場合はこの形式のDateの値だとインポートでエラーになる。例えば、macOSのシステム言語設定が日本語だと、デフォルトでアプリの言語設定も日本語になるので、エラーが発生してしまう。

解決方法

二つある。

一つ目は、言語設定に応じたDateの形式にする方法。言語設定が日本語のときは、Dateの値を日本語表記にすることでインポートできる。

    Date:    2016年6月25日 01:59:06 AM JST

test


    Date:    2016年6月26日 01:59:06 AM JST

testtest
...

二つ目は、システム設定からアプリの言語設定を変える方法。「システム環境設定」→「言語と地域」→「アプリケーション」からアプリごとの言語設定を追加できる。ここでDay Oneの言語設定を英語にすれば、サポートページと同じ日時形式でプレーンテキストをインポートできる。

f:id:kymmt90:20200421221330p:plain

GraphQL開発のベストプラクティスをまとめた"Principled GraphQL"を読んだ

"Principled GraphQL"はApollo社が公開しているGraphQL開発のベストプラクティス集です。

principledgraphql.com

背景として、近年のアプリケーション開発では「データグラフ」が重要になってきているとしています。GraphQLを通じて、ある企業のすべてのアプリのデータとサービスとを統合してデータグラフとして提供することで、クライアントとしてのアプリケーションから効率よく簡単にサービスを使うことができるようになります。もはやGraphQLはクエリ言語というより、クライアントとサービス間の接続を担う包括的なソリューションなので、いろいろと考えるべきことも多く、GraphQL開発の統合環境を提供していて経験豊富なApolloがベストプラクティスをまとめた、というところのようです。

実際のところ最近GraphQLを触っていないのですが、ざっくり読んでみました。


構成

ドキュメントのフォーマットは"the Twelve-Factor App"に影響を受けていて、各項目では守るべき原則を表す簡潔なタイトルとリード文に続いて、具体的な説明が載る構成。ただし"Principled GraphQL"は10箇条であり、次の3件にカテゴリ分けされている。

  • Integrity Principles
  • Agility Principles
  • Operations Principles

Integrity Principles

One Graph

  • 各チームでバラバラにグラフを作るのではなく、会社で一つの統合されたグラフを作る
  • 利用者にとっては無駄や重複のないグラフとなって使い勝手が向上し、開発者にとってはスキルの共有や開発コストの削減になる

例えば、ある会社がジャンルの違う複数サービスを持っている場合は、それらの既存のREST APIなどをApolloプロダクトでデータグラフとして統合すべき、という意図と読める。

Federated Implementation

  • グラフの各部分の実装は複数チームでやる
  • チームごとに各々のリリースサイクルで開発できる

Apollo Federationでバックエンドの違いは吸収できる。"One Graph"と表裏一体の関係にあるような原則。

Track the Schema in a Registry

  • schema registryで変更を追跡する
    • schema registryはソースコードにおけるバージョン管理システムにたとえられている
  • VS Codeでの補完やスキーマバリデーションに利用できる
  • single source of truthとして扱う

schema registryはApolloの場合The schema registryのこと。知らなかったが便利そう。

Agility Principles

Abstract, Demand-Oriented Schema

  • スキーマとサービス/クライアントを疎結合にして、実装を隠して柔軟に利用できる抽象層として扱う
  • demand-orientedで開発する
    • 既存のグラフで機能開発しているアプリ開発者の開発者体験(DX)を高められるようなスキーマとする

インタフェースを設けることでバックエンドのアーキテクチャ変更も理論的には可能になるというのはソフトウェアエンジニアリングで頻出なので、もはや一般的な原則という感じもする。

Use an Agile Approach to Schema Development

  • 実際の要求に基づいてインクリメンタルにスキーマは開発していく
  • 定期的なバージョニングより必要なときにリリースする
    • スキーマにバージョンをつけて半年ごとに公開するよりは、必要なときに頻繁にリリース
    • いつでも新規フィールドを追加する。フィールドの削除時は非推奨化→削除の順で進める
  • schema registryを使って変更の影響をつねに確認できるようにしておくのも効果的

Apolloだと@deprecatedディレクティブがある。

Iteratively Improve Performance

  • グラフ利用者がどのように使おうとしているかをAPI開発者は知るべき
  • 実際に送られる形のクエリのパフォーマンス改善をすべき
  • 本番ではパフォーマンスを継続的に監視する

GraphQLはエンドポイントが一つで従来のAPMが使いづらいので、パフォーマンス監視のために後述するtracerがあったほうがよさそう。

Use Graph Metadata to Empower Developers

  • 開発のプロセス全体を通してグラフを使うことを意識する
  • ドキュメンテーション、補完、クエリのコスト計算、CIで変更の影響確認…
  • アプリで使うクエリを運用チームにも共有して、性能的に問題ないか見てもらう
  • 静的型付言語で型チェックに利用

型のついたスキーマを使うと補完が捗りそうというのは容易に想像がつく。GraphQLはクエリのコストを気にする必要があるので、そこへの対策もここで入ってくる。

Operations Principles

Access and Demand Control

  • クライアントごとにグラフへのアクセス権限を管理する
    • access: どのオブジェクトやフィールドにアクセスできるか
    • demand: どのようにどの程度リソースにアクセスできるか
      • クエリのコストを測定して制御する必要がある
  • demand controlのベストプラクティス
    • 認証済みの開発者が事前に登録したクエリだけを実行できるようにする
      • 内部アプリは緩和もOK
    • クエリのレビュー/承認フローを開発サイクルに組み込む
    • クエリコストの試算とユーザーごとアプリごとの利用可能コスト上限を決めておくことで、クエリの事前登録ができないときに対応する
    • もしものときのためにアプリ開発者は特定クエリの送信をとめられるようにしておく

GraphQL APIを公開するときはもちろん、内部的なAPIとして使うときも意図せずクエリのコストが高くなることはあるだろうと考えられるので、とにかく実行されるクエリの管理やコストの事前計測が大事そう。

Structured Logging

  • グラフに対する操作の構造化ログを取得し、グラフ利用状況を理解するためのツールとして扱う
    • 活用しやすいようにmachine readableな形式で取得する
  • グラフ操作の記録をトレースという
    • Analyzing performance - Graph Manager - Apollo GraphQL Docsで紹介されているようなアナリシスの機能
    • ビジネス情報(データがどのように使われているか)と技術的情報(どのバックエンドを使ったか、レイテンシ、キャッシュ利用の状況など)
  • フィールド利用状況の調査、監視、監査データ収集、BI、API利用料計算などに使える
  • traceは一か所に集めて、加工して監視システムに流すか、データウェアハウスにためておくとよい

Separate the GraphQL Layer from the Service Layer

  • GraphQLの層はサービスの層と分けるべき
  • 各サービスにはデータグラフをシステムとして提供するためのロードバランスやAPIキー管理の機能を入れず、クライアントのリクエストに対応することに専念させる
  • クエリの一部はエッジのキャッシュで返し、別の部分は他のサービスに問い合わせつつ、すべての操作をtraceで記録するということが可能になる
    • バックエンドのサービスはREST, gRPCなどさまざまな方式で存在しうる

REST APIにおけるAPI Gatewayのような役割。Apollo FederationにおけるGatewayがここで述べられている層の役割の一部を担っている。


感想

スキーマの活用が便利というのはもちろんのこと、クエリのコストについてさまざまな方法で十分な注意を払うべきという主張がなされているのは興味深かったです。原則を示しつつ、Apolloがそれに対応するツールを用意しているところが強いなという印象でした。

興味があるかたは読んでみてください。