3級ファイナンシャル・プランニング技能検定(FP3級)受験記

2021年9月実施の3級ファイナンシャル・プランニング技能検定に合格し、三級ファイナンシャル・プランニング技能士を名乗れることになりました。

どういう検定なのか

日本FP協会と金財が実施している検定で、社会保険・年金、生命保険、資産運用、税制、不動産、相続などについて扱います。

合格することで名乗れる「ファイナンシャル・プランニング技能士」は名称独占資格であり資格を持っていない人が名乗ることができないものです。

受験のモチベーション

そろそろ30代が近づいてきたので、自分の収入だとどれくらいの世帯でどれくらいの生活レベルを目指すことできるのか知りたくなってきました。マネーフォワードを使っていると右のほうに もっとおトクなお金のコラム としてオウンドメディアの記事が紹介されているかと思いますが、FPによる家計診断の中には「うらやましいなー」と思うほどの収入を得ていても「その生活を続けているようだと老後の生活が成り立ちません」という残酷な評価が下されているものもあります。こういう記事ばかり読んでいると必要以上に将来への漠然とした不安が募るばかりなので自分の経済状況を正しく認識するための知識を付けたかったというのが受験の理由です。

また副業の収入や寄付によって確定申告を毎年行っていたことや、投資信託の購入によって受験範囲の三分の一程度は概ね知っている内容になっていたため、これまで断片的に身に着けた知識を体系的に復習する機会としても使えそうだということも考えていました。

勉強方法

Amazon で一番売れているらしい以下の本を買って勉強しました。

ただし、この本をほかの人にもお勧めするかどうかは微妙なところもあります。この本は検定に合格することに特化したシケプリのようなものであり、系統立てて学ぶことを目的とした教科書とは少し趣が違うように感じました。特に税制の章では説明が不十分だと感じる箇所がかなりあり、不明点が見つかるごとに freee のヘルプサイトを見るなどして調べていました。

www.freee.co.jp

問題集も書店でたくさん売っていますが、3級では買わなくても十分対応できると思います。私は前日に FP3級ドットコム をポチポチやっていました。

受験におけるエピソード

試験では電卓の利用が許可されているのですが レギュレーションが決まっており、いわゆる普通の電卓しか持ち込むことができません。私は工学部出身なので関数電卓を今でも持っているのですがレギュレーションに違反していたため、わざわざ機能的に劣る電卓を買いなおしました。実際のところ、少なくとも3級では計算が必要な問題の数は少なく、計算の内容もほとんどは暗算で対応できる程度のものであるため、十分に勉強していれば電卓が無くても合格できると思います。

東京都においては受験者がかなり多いらしく、受験当日の朝の地下鉄は隣に座っている人は iPad でFP3級ドットコムで過去問を解いているし、向かいに座っている人は表紙に大きく「FP」と書かれた本を開いているしで、全人類がFPを受けているような錯覚に陥りました。

感想

今のところ本業とは全く関連のない資格のため、仕事で活かせることは残念ながら当面はなさそうです。仕事に活かすつもりでファイナンスの勉強をするなら簿記を受けたほうが役に立つ可能性が高いのではないでしょうか。私もこのままだと電卓を買ったお金が無駄になりそうなので勉強をしようと思っています。

雑談でお金の話になったときに話のネタに使いやすいという意味では日常生活で役に立つとはいえるかもしれません。たとえば「国税庁のホームページ を見てたら年収が700万円になるあたりで税率が20%から23%に上がるって書いてあったんだけど本当なの?」といった心配をしている人を見かけても「とりあえず落ち着いて社会保険料控除と給与所得控除と基礎控除を除こうか」という説明はすぐにできるようになるでしょう。

自分の経済状況を正しく認識するのに必要な知識があることを客観的に確認したいという意味ではよい勉強になったといえるでしょう。正しく経済状況を認識した結果として将来への不安が募ることのほうが多いかもしれませんが…

mysqlserverteam.com と mysqlhighavailability.com の跡地からアーカイブに遷移するブックマークレット

MySQL の深い話題に関する情報が得られる情報源として mysqlserverteam.com と mysqlhighavailability.com というサイトがありました。これらのドメインではそれぞれ MySQL Server Team と MySQL High Availability Team が情報発信を行っており、InnoDB のロックの仕組みを詳細に解説するシリーズが投稿されたり MySQLレプリケーションの進化について紹介する記事が投稿されたりと非常に有益なものでした。

web.archive.org

web.archive.org

ところが10月初めごろにこれらのドメインでの情報の公開は終了され、新しい記事は https://blogs.oracle.com/mysql/ で公開、過去の記事は https://dev.mysql.com/blog-archive/ で公開されることになりました。

新しい記事とアーカイブにはどちらも dev.mysql.com から遷移できるようです。

f:id:kujira16:20211107171234p:plain
新しいブログへの行き方

f:id:kujira16:20211107171148p:plain
アーカイブへの行き方

これは私の感想ですが、これまで使っていたドメインは「知る人ぞ知る」というような状況で、MySQL の深い話題を欲している層以外には存在が知られていなかったように思います。とても有益な情報が公開されていることもあるので移転を機に参照される頻度が増えるといいなと思います。

ただ、残念なことに古い URL にアクセスしてもアーカイブに自動的にリダイレクトしてくれるように設定されてはいないようです。たとえば https://mysqlhighavailability.com/replicate-from-gtid-disabled-source-to-gtid-enabled-replica-directly/ にアクセスすると blogs.oracle.com にリダイレクトされますが、アーカイブの記事が表示されるわけではありません。

いずれはアーカイブの記事にリダイレクトされるようになると信じていますが、不便極まりないので私は暫定的にブックマークレットを用意してアーカイブの URL に飛べるようにしています。

やり方は以下のコードをコピーして、

javascript:window.location.href='https://dev.mysql.com/blog-archive/'+window.location.pathname.split('/')[3]+'/'

適当な場所(ブックマークバーの中に入れておくと押しやすい)にブックマークとして保存するだけです。

f:id:kujira16:20211107173830p:plain

これで mysqlserverteam.com or mysqlhighavailability.com にアクセスして https://blogs.oracle.com/mysql/post/replicate-from-gtid-disabled-source-to-gtid-enabled-replica-directly のような URL に飛ばされて何も表示されなかったとしても、このブックマークをクリックすればアーカイブにジャンプすることができます。

本当はブラウザの拡張機能として実装して、blogs.oracle.com にアクセスして特定の条件を満たしていたら自動的にアーカイブに飛ぶようにしたいところです。いや… 理想なのは閲覧者側でこんなことをしなくても自動でアーカイブにリダイレクトしてくれることですね…

ISUCON11 予選参加記(最終スコア 117585)

チーム「四年ぶり二度目」で @brook_bach と @mayoko_ で参加しました。4年前に ISUCON7 に参加したときと同じチームです。

kujira16.hateblo.jp

「興味ある言語でやってみたいよね~」ということで Rust で参加しました。私は本を積んでいる状態だったので、2週間くらいで急いで「実践 Rust プログラミング入門」の5章までを読んで間に合わせました。

事前準備は過去の ISUCON で使った便利スクリプトをかき集めてきたことと、MySQLAWS 向けにチューニングしたことくらいです。

以下はやったことの概要です。典型的なものしか直せていないので、作問者の意図した「この課題特有のおもしろいところ」まではたどり着けていない感があります。

MariaDB から MySQL に差し替えた(担当 @shora_kujira16)

最近の MariaDB の進化に付いていけていないので MySQL 8.0.26 に差し替えて、事前に用意しておいた設定を適用しました。

これだけだと failed to connect db: Tls(InvalidDNSNameError) というエラーが出て MySQL に接続できない状態になってしまったので少し焦りました。以下の記事を参考にして Rust から MySQL に接続するときの TLS の実装を runtime-actix-rustls から runtime-actix-native-tls に変更しました。

blog.foresta.me

あとから気づいたのですが、TLS の CPU 負荷も少し気になるので、MySQL への接続に TLS を利用しないことを検討してもよかったのかもしれません。

あとは GROUP BY による暗黙のソートの仕様が MySQL 8.0 で変更されたことも今思えば少し危なかったかもしれません。SELECT `character` FROM `isu` GROUP BY `character` というステートメントが使われているのですが、MySQL 5.7 以前では GROUP BY `character` ORDER BY `character` と書かれたのと同じになる仕様でした。MySQL 8.0 ではその仕様がなくなったため、同じ挙動にするためには ORDER BY `character` を付けてやる必要があります。

yoku0825.blogspot.com

Ruby に関するファイルを削除

rust と rubyプレフィックスが ru まで一緒のため、ディレクトリの移動や isucondition.rust.service の再起動などあらゆるところで補完が止まります。コマンド入力の生産性の低下につながってくるので消しました。

ログイン状態の確認を簡略化(担当 @brook_bach)

アプリケーションのあらゆる個所で require_signed_in によって user テーブルにセッションの値を問い合わせて正式なユーザーであることの確認をしていますが、ユーザーを削除するような処理は存在しないのでセッションの値を信用するということで user テーブルへの問い合わせをやめました。

isucon11-qualify/main.rs at a443b242596003515c3037540dd3e48abfa87382 · isucon/isucon11-qualify · GitHub

GET /api/isu の N+1 を少し改善(担当 @shora_kujira16)

isu テーブルのレコードそれぞれについて isu_condition の最新の値をとってくるというクエリがあったので、この部分の改善を試みました。こういうパターンでは MySQL 8.0 で使えるようになったウィンドウ関数が使えたはずだという記憶があったので使い方をググったのですが、微妙にシンタックスエラーになるため以下のようなサブクエリで妥協しました。

SELECT
    isu.id AS id,
    isu.character AS `character`,
    isu.jia_isu_uuid AS jia_isu_uuid,
    isu.name AS name,
    (SELECT isu_condition.timestamp FROM isu_condition WHERE isu_condition.jia_isu_uuid = isu.jia_isu_uuid ORDER BY isu_condition.timestamp DESC LIMIT 1) AS timestamp,
    (SELECT isu_condition.is_sitting FROM isu_condition WHERE isu_condition.jia_isu_uuid = isu.jia_isu_uuid ORDER BY isu_condition.timestamp DESC LIMIT 1) AS is_sitting,
    (SELECT isu_condition.condition FROM isu_condition WHERE isu_condition.jia_isu_uuid = isu.jia_isu_uuid ORDER BY isu_condition.timestamp DESC LIMIT 1) AS `condition`,
    (SELECT isu_condition.message FROM isu_condition WHERE isu_condition.jia_isu_uuid = isu.jia_isu_uuid ORDER BY isu_condition.timestamp DESC LIMIT 1) AS message
FROM isu
WHERE isu.jia_user_id = ? ORDER BY isu.id DESC

他の人の参加記を読んで分かったのですが、こういった処理では MySQL 8.0.14 で追加された LATERAL 句も便利なようです。LATERAL 句では LIMIT 1 に限らず LIMIT N が必要になる場合でも対応できるようです。

インデックスを付ける(担当 @shora_kujira16)

N+1 の改善で全く速くならなかったのでなんだこれと思ってよく見直すとインデックスがまともについていなかったので付けました。MySQL 8.0 で使えるようになった降順インデックスがどれくらい寄与しているのかは分からないです。

-- WHERE isu.jia_user_id = ? ORDER BY isu.id DESC の高速化狙い
ALTER TABLE isu ADD INDEX ia_user_id_and_id (jia_user_id, id DESC);

-- WHERE isu_condition.jia_isu_uuid = ? ORDER BY isu_condition.timestamp DESC の高速化狙い
ALTER TABLE isu_condition ADD INDEX jia_isu_uuid_and_timestamp (jia_isu_uuid, timestamp DESC);

この改善でスコアが 13000 程度になりました。

画像を nginx から配信する(担当 @brook_bach, @mayoko_)

/api/isu/*/icon に10秒くらいかかっているものがあったので nginx から配信することにしました。今よく考えると、もしかすると Conditional GET のためのヘッダを適切に付与していれば省略できたのかもしれません。

画像ファイルが MySQL に保存されているのを静的ファイルとして保存するように変更するという課題は私たちのチームが前回参加した ISUCON 7 でまさに出題されたものです。今回の課題で違ったのは認証が含まれているということです。アプリケーション側でなにか処理をした結果に応じて静的ファイルを返すかどうか決めるというのはよく必要になる処理であり、多くの Web サーバーがそれに適した機能を提供しています。nginx ではアプリケーションから返すレスポンスヘッダに X-Accel-Redirect を付けると指定個所にリクエストを rewrite してくれるため、これを利用しました。

最新の isu_condition を取ってくる処理の高速化のため、最新の isu_condition を別テーブルに保存する(担当 @shora_kujira16)

GET /api/isu がまだ遅かった(1秒以上かかっていた)ため、N+1 を解消するためにサブクエリではなく JOIN が使えるように ORDER BY isu_condition.timestamp DESC LIMIT 1 の部分を別のテーブルに保存することにしました。

以下のようなテーブルを作り、

CREATE TABLE `last_condition` (
  `jia_isu_uuid` char(36) NOT NULL,
  `timestamp` datetime NOT NULL,
  `is_sitting` tinyint(1) NOT NULL,
  `condition` varchar(255) NOT NULL,
  `message` varchar(255) NOT NULL,
  PRIMARY KEY (`jia_isu_uuid`)
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4;

/initialize では以下のようなクエリで初期化し、

INSERT INTO last_condition SELECT
    a.jia_isu_uuid AS jia_isu_uuid,
    (SELECT isu_condition.timestamp FROM isu_condition WHERE isu_condition.jia_isu_uuid = a.jia_isu_uuid ORDER BY isu_condition.timestamp DESC LIMIT 1) AS timestamp,
    (SELECT isu_condition.is_sitting FROM isu_condition WHERE isu_condition.jia_isu_uuid = a.jia_isu_uuid ORDER BY isu_condition.timestamp DESC LIMIT 1) AS is_sitting,
    (SELECT isu_condition.condition FROM isu_condition WHERE isu_condition.jia_isu_uuid = a.jia_isu_uuid ORDER BY isu_condition.timestamp DESC LIMIT 1) AS `condition`,
    (SELECT isu_condition.message FROM isu_condition WHERE isu_condition.jia_isu_uuid = a.jia_isu_uuid ORDER BY isu_condition.timestamp DESC LIMIT 1) AS message
FROM isu_condition a GROUP BY a.jia_isu_uuid;

POST /api/condition/* では以下のようなクエリで最新の isu_condition を保存することで

INSERT INTO last_condition (jia_isu_uuid, timestamp, is_sitting, `condition`, message) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE
    is_sitting=IF(timestamp<VALUES(timestamp), VALUES(is_sitting), is_sitting),
    `condition`=IF(timestamp<VALUES(timestamp), VALUES(`condition`), `condition`),
    message=IF(timestamp<VALUES(timestamp), VALUES(message), message),
    timestamp=IF(timestamp<VALUES(timestamp), VALUES(timestamp), timestamp)

GET /api/isu では以下のようなクエリを使えるようになりました。

SELECT
    isu.id AS id,
    isu.character AS `character`,
    isu.jia_isu_uuid AS jia_isu_uuid,
    isu.name AS name,
    last_condition.timestamp AS timestamp,
    last_condition.is_sitting  AS is_sitting,
    last_condition.condition AS `condition`,
    last_condition.message AS message
FROM isu LEFT OUTER JOIN last_condition ON isu.jia_isu_uuid = last_condition.jia_isu_uuid
WHERE isu.jia_user_id = ? ORDER BY isu.id DESC;

この改善によりスコアが 18000 程度になりました。

私が知らなかった SQL の仕様として、ON DUPLICATE KEY UPDATE に書いた代入リストは前から順に適用されるということがあります。以下のような順番で書くと3行目の is_sitting=... 以降を実行する際にはすでに timestamp=... による代入が行われているため、意図しない結果となります。

ON DUPLICATE KEY UPDATE
    timestamp=IF(timestamp<VALUES(timestamp), VALUES(timestamp), timestamp),
    is_sitting=IF(timestamp<VALUES(timestamp), VALUES(is_sitting), is_sitting),
    `condition`=IF(timestamp<VALUES(timestamp), VALUES(`condition`), `condition`),
    message=IF(timestamp<VALUES(timestamp), VALUES(message), message)

GET /api/trend の N+1 を改善(担当 mayoko_)

GET /api/isu の改善と並行して /api/trend の改善も進めました。last_condition が用意されていれば以下のようなクエリで N+1 を解消することができ、レスポンスの組み立てをアプリケーション側で処理することができます。

SELECT
    isu.id AS id,
    isu.character AS `character`,
    last_condition.condition AS `condition`,
    last_condition.timestamp AS timestamp
FROM isu
INNER JOIN last_condition
ON isu.jia_isu_uuid = last_condition.jia_isu_uuid
WHERE isu.character IS NOT NULL

この改善によりスコアが 35000 程度になりました。

この改善ではベンチマーカーの採点基準に起因すると思われる不思議な現象に悩まされました。当初の実装ではレスポンスに含まれるデータの順序を考慮しておらず減点が発生していました。順序を考慮するような実装に修正して再度負荷走行を行ったところ「サービスの評判が上がりユーザーが増加しました」という旨のメッセージともに負荷レベルが上昇しました。これによりこれまで問題となっていなかったエンドポイントで1秒以上の時間がかかるようになってしまいタイムアウトが頻発し、ほとんどスコアが出なくなってしまいました。おそらく正攻法ではないのではないかと考えています。

分散構成に移行(担当 @brook_bach)

当初は以下のような構成を想定していました。

  • 192.168.0.11 は nginx, MySQL の処理と画像ファイルに関連するエンドポイントを処理
  • 192.168.0.12, 192.168.0.13 はその他のエンドポイントを処理

しかし謎のエラーが出てしまい、時間も迫っていたので以下のような構成で妥協しました。

  • 192.168.0.11 は MySQL
  • 192.168.0.12 は target_base_url の処理
  • 192.168.0.13 でその他すべてのリクエストを受け付け

結果発表後に調査したところ、エラーの原因は非常に単純で location /api/isu { } としなければならないところを location /api/isu/ { } と余計なスラッシュを付けてしまっていたことでした。

負荷走行中に htop を眺めていると 192.168.0.13 の CPU 使用率が100%だったためこの間違いがなければスコアの改善が見込めたと考えられますが、192.168.0.11 と 192.168.0.12 の CPU 使用率もそれなりに高かったため、たかだか 1.x 倍程度の改善だったと考えられます。

そのほかカーネルのチューニングやログの無効化などにより最終スコアが得られました。

感想

  • 毎度のことですが、作問者の意図した「この課題特有のおもしろいところ」まではたどり着けていないのは悔しいです。一方、以前と比べて理詰めで改善が行えている点は進歩と言えそうです
  • Rust を使ったことに関しては今回の課題はアプリケーション側の負荷が高めだったため、速度面での恩恵が受けられたと言えそうです。私にとってはあまりスムーズに開発できたとはいえない状況でしたが、それは本を買って積んでいた私がわるいのです

運営さんありがとう

今回の運営はこれまでと比べて格段にルールやレギュレーションの示し方が明確になっており、clar の受付や選手向けページの障害発生時の対応などについても違和感を感じることがなく、高いレベルで運営されていると感じました。とても有意義な時間になりました。ありがとうございました。