TECH PLAY

エス・エム・エス

エス・エム・エス の技術ブログ

264

こんにちは、介護事業者向け経営支援サービス「カイポケ」のフロントエンドエンジニアの setoh です。2022年12月から株式会社エス・エム・エスで働いています。 私は大学時代にAndroidアプリ開発を始め、就職後は一貫してAndroidアプリ開発を軸に10年以上のキャリアを積んできました。しかし心機一転、エス・エム・エスに入社後はこれまで全くの未経験であったフロントエンドエンジニアとして業務を行っています。 今回の記事ではAndroidアプリエンジニアがフロントエンドエンジニアとして業務をこなせるようになるまでに効果的だった学習内容について書きます。 学習する観点 以前に同じフロントエンドエンジニアの城内が記事にしていますが、「カイポケ」のフルリニューアルプロジェクトではTypeScriptとReactを採用しています。まずは業務レベルでコードを書けるようになるために、こちらの内容を中心に学習することにしました。 tech.bm-sms.co.jp 今後フロントエンドエンジニアとして働いていくためには幅広い分野の知識をしっかりと身につけることが必要だと思っており、長期的にそこを目指していくために言語とUIフレームワークから基礎固めをするという狙いもありました。 実際、入社3ヶ月以内には任されたタスクを設計に沿って1人で実装できるレベルにはなっていたため、ある程度効果的であったと感じています。 React 以前から「Reactは難しい」「React何もわからない」という話をよく聞いていたためかなり警戒しており、まず最初に学習することにしました。 業務でReactを使う複数の友人から『りあクト!』シリーズ *1 を読む事を薦められたため、転職前のタイミングで読みました。 前職のAndroidアプリでは宣言的UIフレームワークであるJetpack Composeを採用していました。そこで、同じく宣言的UIフレームワークに分類されるReactはJetpack Composeとどのような点が同一でどのような点が異なるのかという観点を意識して比較することにしました。結果として、自分にとってはJetpack Composeと細かな違いはあれど大まかな書き方では同一に感じ、あまり苦労せず基礎を理解できました。 また『りあクト!』では、JavaScriptの成り立ちから解説されていたため、自分のようにフロントエンド経験がない人には基礎知識のキャッチアップとして役立ちました。 入社後1~2ヶ月くらいまでは『りあクト!』を片手にコードを書くことも多くありましたが、最近は内容が一通り身についたため、Reactの公式のリファレンスから情報を得るようにしています。 TypeScript TypeScriptとKotlinは構文が似ているという認識だったため、最初は書き方がわからない部分について逆引き的に調べていました。しかしコードを書くことが増えるにつれ両言語には思想のベースが異なる部分が多々あり、TypeScriptの基礎から学ばなければTypeScriptとしての良い設計ができないと気付きました。 良い評判を聞いていたので 『プロを目指す人のためのTypeScript入門』 を読んで学習することにしました。TypeScriptの基本的な文法から細かな仕様までが網羅されており、業務で頻繁に使う仕様を把握するには効率的な本でした。 この本を読んでからは以前より高い解像度でコードを読み書きできるようになったと感じたため、もっと早く読むべきだったという後悔があります。 学習したことを身につける 自分は座学や読書の学習が好きであまり実践をしないという悪癖があるのですが、実践せずとも学習しただけで初心者でもすらすらとコードが書ける...というような甘いことはありませんでした。 フロントエンドチームではチームビルディングの一環としてモブプロをしており、自分も入社3日目から参加しました。本を読んで完全に理解した気になっていたのですが、いざモブプロになると全くコードを書く手を動かせずに固まってしまい、学習したことがコーディングに生かせるほど身についていないと認識できました。 自分はモブプロに慣れていたので「何をしていいか皆目見当がついていない」「○○は理解できているしそれを使いたいが、そこまで持っていく実装がわからない」など思っていることを率直に口にし、同僚からヒントをもらいつつなんとか実装をすることができました。 当たり前ですが、モブプロという場で半強制的に手を動かしてコードを書いたことは学習したことを身につけるのにとても効果的だったと感じています。また同僚のサポートがありつつも、自分が書いたコードがプロダクトに反映されることは大きな自信に繋がる良い体験でした。 しかしながらチームに入ったばかりで自分のようにストレートな発言ができる人ばかりではないと思っており、初心者でもよりスムーズに知識を身につけていける方法を模索中です。 知識を深めて広げる ReactとTypeScriptについて学んだりペアプロ・モブプロによって最低限コードを書けるようになったため、次のステップとして特定の分野の知識を深めていくことでレベルアップをしようと考えました。いわゆるT字型と言われるような知識獲得を目指し、深く掘りつつ広げていく狙いです。 テストから知識を深める Androidアプリ開発ではテストまわりの実装を整えることが多かったため、フロントエンド開発でもテストを起点にして知識を深めていくことにしました。 まずはテストのフレームワークの使い方を覚えるために、公式のドキュメントを積極的に読みました。その中でも使えると思った知識は積極的にチームのコーディングガイドラインに反映してコードを修正しています。 「テストについてわからないことがあったらsetohに聞けば大丈夫だな」とチームメンバーに思ってもらえることを目標に、今後も知識を深めていきたいと思います。 テストから知識を広げる テストフレームワークの動作や仕組みが気になった部分については内部実装を読むようにしています。フレームワークのAPIを深掘りしていくと背景には多くのAPIが使われており、それぞれについて調べるだけでもかなりの学習になっています。実践的なTypeScriptらしい書き方なども感じることができました。 テストについて学習する際に、自分が学習する必要のある分野を把握することも心がけています。 例えば、React Testing Libraryの QueryのPriorityについてのドキュメント を読んだことにより、アクセシビリティを意識したコンポーネントの構造を考えるようになりました。AndroidではMaterial Designに沿っていればある程度のアクセシビリティが確保できていたため意識が薄くなりがちでしたが、WebアプリケーションではDOMの構造などの観点も考える必要があり、この点についても学習の必要があると認識できました。 最後に 業務内容を変える際には学ぶことが非常に多く、この記事ではほんの一部しか書くことができていません。他に観点はあると思いますし、もっと効果的な学習方法もあると思います。 この記事が起点になり、フロントエンドエンジニアの学習に効果的な情報が多く発信されると嬉しく思います。 *1 : 『りあクト! TypeScriptで始めるつらくないReact開発 第4版【① 言語・環境編】』 ほか。
2023年1月にエス・エム・エスに入社した荒巻です。現在、介護事業者向け経営支援サービス「カイポケ」のフロントエンドのエンジニアリングマネージャーを担っています。これまでは組み込み開発やモバイルアプリなど、興味のままに関わってきました。自分のキャリア観は5年ごとくらいで徐々に変化しているように感じており、転職にあたり、その変遷と、なぜエス・エム・エスなのか、ということについて書いてみたいと思います。 サービス志向に至るまで キャリアの初期には、とにかく技術的なことに興味がありました。学生時代は日本の製造業の絶頂期で、NHKスペシャルの「電子立国 日本の自叙伝」に感化され、「世界中で使われるデバイスを作ることこそが世の中の役に立つことである」という認識を持っていました。とはいえハードウェアは専攻していなかったため、組み込み業界でファームウェアを書くところからキャリアをスタートしました。製造業ではハードウェアがソフトウェア開発を主導しており、モノを作っている感覚がありました。無線のプロトコルやCPUの開発などに携わることができ、技術的な好奇心はある程度満たされました。 そうこうしているうちに、世の中では「Software is eating the world」に象徴されるように、ソフトウェアやサービスからハードウェアが生み出される流れになってきていました。そこで、自分の目標を「自分でも使いたくなるようなサービス開発に携わりたい」にアップデートしました。使いたい技術を使うよりも顧客やユーザーの問題を解決するほうが重要であるという意識が強まってきたこともあり、webやスマートフォンアプリの開発にスイッチしました。 グローバル企業で感じた壁 いくつかのアプリ開発を経験後、スマートニュース株式会社に入社しました。いくつかの施策が成功して会社が急成長し、グローバル企業となりました。優秀な英語話者が大量に入社してきて、いわゆるベイエリアのスタイルが導入され、王道のプロダクト開発を行えている実感がありました。エンジニアリングマネージャーも経験することができ、多くのことを学ぶことができたと同時に、いくつかの壁を感じることにもなりました。 一番大きなギャップは英語力です。それなりに英語力がついたものの、日本語力とは大きな隔たりがありました。自分はボトムアップで考えがちですが、英語だと語彙力が足りないので、まず日本語であれこれ考えてから抽象化し、英語に変換してからGrammarlyで修正するような毎日を送っていました。会社にさらなる貢献をしたいとは思っていたものの、チーム内の責務からはみだしたことに挑戦するには英語力が不足していました。 また、それまで大きな組織でのプロダクト開発の経験がなかったこともあり、プロダクト開発のベストプラクティスや組織づくりに関する知見がまだまだ足りない状態でした。そのため、英語とプロダクト開発の二重のギャップがあるように感じてきていました。 スマホアプリからSaaSへ グローバルエンジニアになりきれてはいないものの、グローバルエンジニアになることが目的化しつつあるように感じていました。それよりも足場を固め、プロダクト開発に貢献することを考え始めました。 社内では数多くのSaaSを利用しており、BtoCのスマホアプリから見て、SaaSのビジネスモデルは魅力的に映りました。BtoCのサービスはユーザーベースは大きいものの、「ユーザーはユーザーのことを知らない」と言われたりします。せっかく開発した新しい機能も、A/Bテストを実施した結果、ユーザーの興味を惹くことができなければ正式リリースしない、というようなこともよくあります。一方BtoBのSaaSでは、日々のビジネスオペレーションの一翼を担っており、ユーザーが切実に必要とする機能を開発していけるのではという期待を持ちました。そのようなタイミングでエス・エム・エスから声をかけてもらい、転職を決めました。 エス・エム・エスに入社してみて 現在、介護事業者向け経営支援サービス「カイポケ」のリニューアルプロジェクトに従事しています。(詳しくは、 カイポケリニューアル のタグのついた記事をぜひ読んでいただければと思います) 3ヶ月やってみての感想ですが、よいプロダクトを届けるため、必要なことをチーム全体で実直に取り組んでいると感じています。一つ一つは地道な作業で、プロダクトオーナーとドメインエキスパートがユーザーからヒアリングしてストーリーマップを作り、デザイナーとソフトウェアエンジニアが議論して機能やUIに落とし込んでいきます。開発プロセスとしてはスクラム開発で不確実性を徐々に減らしながら、着実に成果を積み重ねていっています。これだけだと簡単そうに聞こえますが、品質を犠牲にせずに、必要な機能を洗練されたUIで、ある程度のスピード感を持って開発を進めています。もちろん方向転換や手戻りなども多少ありますが、そこを含めて総合的には絶妙のバランス感を持って前進できているかなと思います。 特徴的なこととしては、積極的にユーザーとコミュニケーションを取っており、介護事業所に訪問してヒアリングやインタビューできる機会が定期的に用意されています。私も参加してみたところ、ちょうど開発している機能に相当する業務を現行バージョンのカイポケで行っていました。実際の操作を見せていただき、使いづらい点をその場で質問できたことで、ユーザーの課題感を肌で感じることができ、対象業務の理解も深まりました。正式リリースはもう少し先ですが、良い体験がユーザーに届けられそうな予感がしています。 技術スタックとしても、backendでSpring for GraphQL、frontendはReact + Storybookといった、新しめの要素技術を利用して、長期的に高い生産性を保てるようにチャレンジしているところです。このように充実した環境ですが、日々、開発対象業務が拡大していっており、まだまだ新しい仲間を必要としております。ご興味があればぜひ、下のリンクからカジュアル面談を検討していただければ幸いです。
エス・エム・エスでEM兼採用担当をしている emfurupon777 です。 2022年1月に入社して1年が経過し、採用を軸にして職務にあたる時間もかなり多くなっていますが、私よりエス・エム・エス歴の長い仲間たちに加え、多くの新しい仲間のJOINにより、楽しみな2年目を過ごしています。 今回は、一人のEMとして、”リーダーシップ”についての見解を書いてみたいと思います。 会社のテックブログの場を借りていますが、私個人の主観も大きく影響しており、必ずしもエス・エム・エスで語られることと表現や詳細が一致しているとは限らないのでご容赦ください。 我々が相対しているもの VUCA(Volatility(変動性)・Uncertainty(不確実性)・Complexity(複雑性)・Ambiguity(曖昧性))なんて言葉を使うような世の中ですが、我々が日々挑んでいるプロダクト開発は、そもそも不確実性が高いことを前提にしておく必要があります。相対する課題も、技術的課題・適応課題どちらも山積ですよ・・・というのがほとんどの会社で感じられているのが正直なところではないでしょうか。 そんな中、さまざまなバックグラウンドを持った仲間と仕事を進めるにあたって、EMとしてはよく意識して行動しているものの、意外とリーダーシップについて話すことってないなー、と感じている今日この頃で、今回筆を取りました。 リーダーシップとはなんなのか? 私は、リーダーシップを WHY: 設定された成果のために WHEN: 決定に十分な情報が完全には揃っていないとき HOW: 高い視座・複眼で考え、できる前提の短いポジティブな表現を使って(Can do attitude) WHERE: 周囲を巻き込んだ場所で WHAT: 設定した良いイシューを WHO: 解決しようという意志を持っている人が 行動すること、と定義しています。 これだけでは伝わらない面が大きいかと思いますので、それぞれについてもう少し言及してみようと思います。 WHY: なぜリーダーシップを発揮する必要があるのか 組織が存在するからには何かしらの成果期待があり、経営者・従業員である以上、我々は所属している組織の成果を最大化する責務があるからです。 より高い成果を目的とするからこそリーダーシップは必要となってくるものと考えます。 逆に言えば、成果に繋げようという行動でなければ単なる干渉・おせっかいになっている可能性が高く、実際後で振り返ると自分の理解が足りていなかった、感情的な対立があった、などの背景があり、その回避行動のあらわれだった・・というケースがままあります。 WHEN: いつリーダーシップを発揮するのか 不確実性が高い時、まだ重要な「根拠となるファクトが揃っていない時」にこそリーダーシップは必要です。条件が出揃っていて、みんなが同じ結論を導けるならばそれは単なる”判断”で、意思決定ではありません。 不確実性が高い中で行うからこそ価値があるのが意思決定で、その意思決定に達するために必要なのがリーダーシップです。 多くのケースで意思決定の後に新たなファクトが出てきているため、後になって振り返りをしてみれば、「より良い決定ができたのではないか・・・?」と言う思いが湧いてくるのは多くの方が経験したことがあるのではないでしょうか。この思いを抱けるのであればなんらかの意思決定をしていたと言えると思います。 HOW: どのように発揮するのか リーダシップの発揮時には自分が普段見ているところよりも、1段・2段高い視座に立ち、取りうる手段を考えることが重要です。 例えば、部・課・チームのような組織階層があるとき、チームの課題を解決するためには課長・部長と目線を上げていくことによって、ヒト/モノ/カネなど扱えるリソースが大きくなることによって強制的に思考のリフレーミングを起こせるなどの効果があります。 (これは一例なので、必ずしも組織構造ではなく、ロールなどで考えてももちろん良いと思います) また、視座を高くするだけでなく複眼(複数の視点)で観察することも重要です。1視点では対象を立体で把握することができません。 複数の視点から観察することで対象を立体的にとらえることができ、より的確にファクトを捉えることができるはずです。 そして、最後の「Can do attitude」は私が以前一緒に仕事をしていた尊敬するCTOに重ねて伝えられた言葉で、これはリーダーシップを発揮するときには非常に重要だと思っています。どんなに難しいイシューでもできる(Can do)ことを前提に議論をする、それでこそ実現にあたって超えるべきギャップを測ることができます。 (私は当時この振る舞いができておらず、「先にできない理由を並べてチャレンジしない状況では、成果はついてこないし、そもそも誰も相談に来なくなるでしょう?」と言われ、「確かに…」としか言えませんでした。。) WHERE: リーダーシップを発揮すべきなのはどこで? リーダーシップは同じ成果を出すために同じ方向を向いて欲しい人たちがいる場で発揮すべきです。一人でやってみてうまくいってから皆に共有する・・・のは、やらないよりははるかに良いものの、もう一歩だと考えています。 リーダーシップは一人が発揮しているだけでは成果が大きくならず、継続性も生まれにくいと考えます。その影響する範囲は大小さまざまかもしれませんが、関係している人たちがそれぞれリーダシップを発揮した総量や、その重なりによってこそ期待通り、あるいは期待を超える成果に繋がっていくものだと思います。 WHAT: 何に対してリーダーシップを発揮すべきなのか 一言で言ってしまえば、「良いイシュー」で、詳細には参考図書にあげている『イシューから始めよ』に書いてある3点になります。 本質的な選択肢である 深い仮説がある 答えを出せる とは言え、いきなりこれを追い求めるのはハードルが高いですし、リーダーシップの発揮は習慣化していく必要がある性質のものでもあると思っています。そのため、ちょっとしたことで・・・例えば、やろうと思えば誰でも対応できるかもしれない、ちょっとした社内の困りごとについて自らリーダーシップを発揮して対応してみるのがおすすめです。 これは、優れたリーダーシップを発揮されている方のお話を伺っていると、自分に求められている役割がどのようなときでも、可能な範囲(もしくはそれをちょっと超えた範囲)でリーダーシップを発揮するという経験をごく小さなものから繰り返し積み上げていくことで、より広い範囲でリーダーシップを発揮できるようになっていることを観測しているためです。 WHO: リーダーシップを発揮すべきなのは誰なのか リーダーシップを発揮すべきなのはマネージャーですか?リーダーですか? いえ、リーダーシップには権威・権限は不要で、誰でも発揮することができるはず・・・立場に関わらず意志を持って臨める人こそリーダーシップを発揮すべきです。 あなたの所属会社では、マネージャーとリーダーをポジション・ロールとして明確に定義しているでしょうか?両者を区別して扱っているでしょうか? プロダクト開発にあたって、明確なポジション・ロールとして定義した場合は、プレイヤーとして組織の平均以上のスキル・能力も求められることが多いのではないでしょうか。そのため、組織の価値観に沿った形で、かなり広範囲にリーダーシップを発揮することが求められることになり、結果その任にあたれる人は絞られてくるかと思います。 また、暗黙のうちに広範囲で継続的なリーダーシップを求められることが多いと思います。 前述の定義によれば、「根拠となるファクトが揃っていない時」に求められるのですから心労も絶えません、、 ・・・大変ですね。 ですが、マネージャーやリーダーになったからリーダーシップを発揮しよう!と考えている方はほとんどいないと思います。むしろその逆で、なんらかのリーダーシップを発揮してきた結果、マネージャーやリーダーを担うようになったはずです。 会社組織として予算管理、労務管理、コンプライアンス遵守などの任を遂行せねばならず、組織として一定の規模が出てくるとマネージャーは必要です。 一方で、組織の規模にかからわず必要なのがリーダーシップで、これは組織がある以上目標としている成果があるはずで、その達成には不可欠だからです。 この不可欠なリーダーシップを発揮している人にリーダーというポジション・ロールをつけることがある、というのが私の認識です。 せっかく組織だって活動をしているのだから、明示的なポジション・ロールに興味があるかないかは別にして、成果を残していくためにリーダーシップを発揮するという意識を持った方達と働けたら面白いなと思います。 エス・エム・エスで求められるもの エス・エム・エスのプロダクト開発組織では「自治と信頼」を重要視しており、弊社内ドキュメントでは下記のように説明されています。 自治と信頼 ユーザー接点のチームが自立的に思考し意思決定していくために、上意下達で思考停止する組織でなく、自治と信頼をベースとした組織を目指していく。 あなたがコミュニティ 組織というコミュニティがなにかをしてくれるのではない。自治組織ではあなたがコミュニティ。あなたがコミュニティを代表する一人として考え行動することで自治組織になる。あなたが他人事だと思った瞬間から自治は崩れていく。機能性を担保するために役割 (ロール) を設定しプロトコルを決めることはあるかもしれない。でも、それがあなた自身のコミュニティへの責任を肩代わりするわけではない。役割によらずあなたも等しく責任を担っている。序列を強化することは権威勾配の高い関係性をつくり、コミュニティのパフォーマンスを悪化させる。序列はいらない。マーケットだけが王様だ。 参考 「コミュニティ」とは誰か 心理的安全性ガイドライン(あるいは権威勾配に関する一考察) 自治と信頼を継続していくためには、リーダーシップを発揮し続けられる仲間が欠かせません。 どういう状態を目指すのか・・と考えると、参考書籍にあげた「採用基準」という書籍のなかに出てくる「リーダーシップの総量」という表現が色々なことを内包していて非常に興味深いです。EMとして、ぜひ仲間を集め、リーダーシップの総量を上げて素晴らしい開発組織に成長させていきたいです。 最後に 取り止めのない話になってしまっている気もしますが、エス・エム・エスの「リーダーシップの総量」を上げて社会課題に挑んでみるのも面白いかも?と少しでも興味をもっていただける方はカジュアルなところからおはなしさせてください!We are Hiring! 参考書籍 伊賀 泰代『採用基準』 安宅和人『イシューからはじめよ――知的生産の「シンプルな本質」』 ロナルド・A・ハイフェッツ, マーティ・リンスキー, アレクサンダー・グラショウ『最難関のリーダーシップ』
介護事業者向け経営支援サービス「カイポケ」のエンジニアリングマネージャー、酒井( @_atsushisakai )です。 前回は、 カイポケのフルリニューアルプロジェクト の紹介をさせてもらいましたが 、今回はローンチから17年も経過したカイポケのような大規模 SaaS プロダクトをフルリニューアルする、という大きなゴールを目指すプロジェクトに立ち向かうために、どのようなプロセスとマインドで開発を進めているのかをご紹介させていただきます。 フルリニューアルプロジェクトの特徴 まず前提として、このフルリニューアルプロジェクトは、いくつかの観点で、プロジェクトの進行難易度を高めるような特徴がいくつかあります。 作るべき「価値」が膨大に存在することが最初からわかっている このプロジェクトでは、すでに現在動いているカイポケの重要なお客様にとっての「価値」を大きく損なわないように開発を進めていかねばなりません。これは、機能的な互換性を保つ、ということではなく、あくまで「価値」にフォーカスした話です。そして、その価値はローンチから17年間も積み重ねてきた結果、非常に膨大な分量になっています。 ただし、すべてのお客様にご満足いただけるような機能一式をいきなりすべて作り上げてビッグバンリリースするわけにもいきません。フィードバックを得ながらも戦略的に小さく価値を積み上げ、結果的に現在動いているカイポケが提供している価値以上のものへと、プロダクトを超高速に育てていくことがどうすれば可能なのか、ということを常に模索していく必要があります。 関わるメンバーやチーム数が最初から多い 上記の通り、作るべき価値が膨大であることは最初からわかっているため、関わるメンバーもそれなりに大人数となっています。少数精鋭で密なコミュニケーションを取りながら、小さく小さく…、という作り方ではなく、ドメイン分析を行い、最初から複数のチームで責務分割しながらたくさんの価値を並行で生み出していけるような開発プロセスが必要です。 チームの構成や規模感はこちらでご紹介しています 。 介護保険制度や介護事業所の業務への深い理解が必要不可欠 例えば、 先日の宮坂さんの記事 で書かれているように複雑な介護保険制度への理解が必要不可欠であったり、法改正についての情報のキャッチアップも必要です。また、介護事業所における細かな業務を再度理解し直すことも重視しています。これらの複雑度の高い情報を扱いながら、抽象度高く戦略的・長期的にプロダクトがユーザーの課題を解決し続けていくことを狙えるプロセスを見つけていくことは非常に難易度が高いものです。 プロダクトマネジメント体制 さて、上記でご紹介したようにこのフルリニューアルプロジェクトは、意識せずにやっているとコミュニケーションコストが高く、解くべき課題が膨大かつ超高難度、という非常に難しい特徴があります。このような状況に対応するために、常に開発組織全体がさまざまな観点の解像度を高く維持することができるよう、プロダクトマネジメントに関わる各種ロールが設計されていますのでご紹介します。 プロダクトマネージャー プロダクトマネージャーは、新しいカイポケにおける複数のアプリケーションを束ねた SaaS 全体の視点から顧客価値をスケールさせ、事業価値へつなげていくためのビジョンや戦略の立案を行います。ユーザーストーリーマッピングを元に、プロダクト全体の長期ロードマップとスコープ管理を横断的に行っていきます。 プロダクトオーナー プロダクトオーナーは、カイポケ内にドメインごとに設計された複数のフロントエンドアプリケーションやバックエンドサービスのそれぞれについて、プロダクトビジョンを作り、プロダクトバックログアイテムの優先順位を管理していくことに責任を持ちます。プロダクトオーナーに紐づく形で開発者やQA、デザイナーなどのクロスファンクショナルなスクラムチームが組成されています。 ドメインエキスパート ドメインエキスパートは、プロダクト開発組織に対して、複雑な介護保険制度や介護事業所内の業務プロセスの専門知識を提供し、プロダクトの成長に貢献します。過去の経歴において、実際に介護事業所での勤務を経験しているため、開発する対象への深い理解と意味づけをしてくれる重要な存在です。 このような体制を敷くことで、プロジェクトの全体像を組織全体で捉えながら、スクラムチームで足元の仮説検証を繰り返し、ユーザーや業務・制度の理解を深めていくことで提供価値に向き合えるように体制設計されています。 プロダクトバックログアイテムができるまで それでは次に、各スクラムチームがプロダクトバックログアイテムの優先順位を確定し、チームのスプリントで着手可能なタスクにどのように分解されていくのかを解説します。 まずは、プロダクトマネージャーとプロダクトオーナーが協力して将来2〜3ヶ月分で創出したい価値を「エピック」と、それに紐づく「プロダクトバックログアイテム」という粒度でざっくりとリストアップします。エピックは、プロジェクト開始時に作成された巨大なユーザーストーリーマッピングが元になっています。 その後、ここで作成されたプロダクトバックログアイテムを各チームのエンジニアがレビューし、背景や要件を理解しながら、さらに細かなプロダクトバックログアイテムの分割を行っていきます。ある程度プロダクトバックログアイテムの粒度を整理し、チーム間の依存関係なども理解しあった段階で、開発の規模感を見積もるためにストーリーポイントを用いた「ざっくり見積もり」(組織内では本当にこう呼ばれています)を行います。その後、見積もりを材料に、スコープと優先順位を再度プロダクトマネージャー/プロダクトオーナー/エンジニアで話し合いながら、向こう2〜3ヶ月で取り組む開発内容を確定させていきます。 既にドキュメントやUIのドラフトとなる資料が Figma などにある場合は、その上にでコメントし合ったり、プロダクトオーナーとエンジニアやデザイナーが直接ディスカッションしたり、ドメインエキスパートにヒアリングして介護事業所で行われている業務を把握したりしながら、開発対象の理解を深めていきます。正直、果てしなく地道な作業でとても大変なのですが、ここでの対話はその後の開発対象への理解を深めるための非常に重要なプロセスとなっています。 フロントエンドチームでのスクラム さて、ここからは私が所属しているフロントエンドチームのスクラム開発の様子を解説します。まず、すでに作成されたプロダクトバックログアイテムに情報を加えながら、 チームで管理している JIRA プロジェクトに全てのプロダクトバックログアイテムを改めて登録していきます。ある程度事前に見積もりもされているので、それをそのままストーリーポイントとして入力していきます。 プロダクトバックログアイテムは、最終的に以下のようなフォーマットに整理して登録を行っていきます。 このフォーマットは、非常にシンプルに記載できますし、主にユーザーに提供する価値にフォーカスしているため、個人的に非常に気に入っています。例えば、How をプロダクトバックログアイテムに記載しすぎると、見積もりの正確性を求めすぎてしまったり、相対的に Why への意識が目減りしてしまったりするので、このぐらいの内容でざっくり書いておくと開発チームが自律的に思考できるようになる、という良い影響を感じた経験があります。 バックログが完成すると、バックログリファインメントで着手していくための戦略をリードエンジニアが中心となって立てていきます。リードエンジニアは事前に関連するバックエンドエンジニアとのコミュニケーションを取っており、その情報と合わせて最短経路で上位のプロダクトバックログアイテムやエピックを実現するための手順を考えながら、スプリントにおける優先順位の調整や必要であればさらにプロダクトバックログアイテムの分割、再見積もりなどをリードしていきます。そして、毎週の「バックログリファインメント」の時間では、精度が上がり、開発対象への理解が深まったプロダクトバックログアイテムがスクラムチーム内で再度共有されることになります。スプリントが終了する頃には、次のスプリントで着手するスプリントバックログが完成されており、スプリントプランニングでは、計測されたベロシティをもとに、見積もり済みのプロダクトバックログアイテムを使ってコミットラインを決めていきます。 最終的に、このスプリントで達成したいとチームで決めたことを1文で表したスプリントゴールを決めることにしています。スプリントゴールを決める目的は、チーム内で以下のように定義しています。 チームとして期間中に達成したいことを一言で表現することで認識を揃える このゴールを達成するために障害となることを積極的に取り除いていくように意識を働かせる スプリントの途中でも、時々ゴールを見直して、本当にゴールに向かえているかをデイリースタンドアップミーティングの中で確認しあって、集中できているかを確認したりします。 そのほか、スプリントレビューやレトロスペクティブ、スプリントプランニングなど、各種イベントをスクラムガイドの通りにしっかりと行っています。経験を大事にして、チームが成長する過程で出てきた、チームに最適化されたローカルルールもいくつかで始めており、イテレーションの回数が増えて行くほど効率化したり、チャレンジしたりできているのも実感しています。 まとめ ここまで述べてきたように、私たちは、カイポケという非常に大きな SaaS プロダクトをフルリニューアルするという非常にチャレンジングな開発に取り組んでいます。この記事では、難易度の高いプロダクトをどのようにマネジメントし、日々の仕事に落とし込んでいるかというプロセスを紹介してきました。この記事を書いてみて感じたのですが、我々が今取り組んでいることは、複雑さや難易度の高さを正面から受けとめ、現実的な落とし所を見つけていく作業の繰り返しというのが正直なところです。驚くような効率的なプロセスではないですが、地道に向き合いながら、スクラムのプラクティスを取り入れて実直に経験を積み重ねるように開発を進めています。フルリニューアルプロジェクトは、まだまだ不確実性が高い状態なので安定した開発プロセス、というのとはほど遠いものですがチャレンジングであるがゆえの面白さも日々感じています。 経験主義で技術もチームワークも学習することを大事にしながらプロダクトと共にチーム全体が成長している実感を味わえる非常にスリリングでわくわくするプロジェクトだと思います。興味のある人はぜひより詳しい話を聴きにきませんか?
こんにちは、介護事業者向け経営支援サービス「カイポケ」のエンジニアの加藤です。 カイポケが提供するサービスの一つ、障害者支援を行う事業所向けサービスの開発をしています。 現在、私たちのチームは一部機能のリプレースを行っています。本稿ではリプレースに至った経緯についてお話ししたいと思います。 ※「しょうがい」には「障害」「障がい」「障碍」といった複数の表記があります。それぞれに意味があり、何が適切かは様々な見解がありますが、本記事では 法令表記 もある「障害」で統一しています。 ※ 本稿では、伝わり易くすることを優先した表現を使っています。本稿の表現に正確性が欠けるものがある点を、あらかじめご了承ください。 我々のミッション 私たちチームのミッションは一言で表すと、 ユーザーの業務効率化と経営状態健全化に貢献すること です(カイポケのミッションの詳細は こちら をご参照ください)。このミッションの実現のために、私たちが考えていること・行っていることをお話しします。 私たちチームの担当プロダクトのユーザーは、障害者支援(障害児含む)を行っている障害者支援事業所の経営者・職員の方々です。 ユーザーは日々、障害を持つ方々の介護・自立支援を主体業務としつつも、事業所経営に必要な多くの付帯業務をこなしています。そこでカイポケを利用することで、付帯業務の効率を上げて、主体業務に集中しやすい状況を作っていただきたいと考えています。 複雑なドメインを持つユーザーの付帯業務 付帯業務と一言で表してはいても実際は複数の業務があります。その中でも、効率アップを実現したい業務の一つに請求業務が挙げられます。なぜなら障害者支援事業者が行う請求は、一般的なサービスの請求より複雑なためです。 一般的なサービスでは「サービス提供者が、サービス内容と料金を考え、サービス利用者にサービスを提供して、料金をサービス利用者へ請求する」という流れが多いと思います。 これが障害者支援では、「国や自治体がサービス内容と料金を法令に基づいて制度化する。その制度に沿ってサービス提供者(事業者)がサービス利用者(障害者)にサービスを提供して、料金の一部をサービス利用者に請求し、残りは自治体に請求する」といった流れです。(この流れは医療の保険制度と似ています。) 一見すると、本来サービス提供者が考えるべきサービス内容と料金が制度化されているなら、その分楽に見えるかもしれません。しかし、もちろんそう簡単な話ではありません。 国や自治体が制度化した内容は、理解しやすい内容ではなく、とても複雑です。複雑である理由はいくつかありますが、ここでは3つに絞って説明します。 1つ目は、サービス内容のパターンが多く料金設定も細かいこと。 2つ目は、請求業務に登場するアクターが多いこと。 3つ目は、制度自体が改定され続けていること。 まず1つ目、サービス内容のパターンが多く料金設定も細かいこと。 一言で障害者といっても、どこに障害をお持ちなのか、どの程度のものなのかが人によって異なります。そのため、障害者に必要なサポートの形も様々で、それに即した多様なサービス内容が必要です。 多様なサービスがあるのだから当然料金設定も細かくなりますし、同じサービス内容でも時間単位の段階性料金が設定されていたりと、とても細かい料金パターンとなっています。 次に2つ目、請求業務に登場するアクターが多いこと。 一般的なサービスの請求業務のアクターは、サービス提供者とサービス利用者の2アクターだと思います。一方、障害者支援はここに国・自治体というアクターが追加されます。ここでは詳しく話しませんが、さらに他事業所というアクターが追加される場合もあります。 業務におけるアクターが増えれば増えるほどフローも複雑になるので、それを制度化したものも伴って複雑化します。 最後に3つ目、制度自体が改定され続けていること。 この制度は不変ではなく改定され続けています。大小様々な改定が数ヶ月から数年毎に入ります。人々の生活は社会情勢や技術の進歩によって日々変化しています。その変化に対応するため、障害者支援の制度も変えていく必要があるということです。 制度に沿って請求する以上、改定される制度の情報をキャッチアップし続ける必要があります。 ドメインの複雑度をカイポケが引き受けたい これらの理由で複雑化した制度を理解した上で請求をすることは大変そう、ということはご想像いただけるかと思います。障害者支援を行う方々は、複雑な制度の上に成り立つ請求業務を介護・自立支援といった主体業務と並行して行う必要があるのです。 そこで私たちは、複雑な制度を読み解き、制度の変更に追随しながら、使いやすいプロダクト(カイポケ)にしてユーザーへ提供する。つまり付帯業務の持つ複雑度をカイポケが引き受けて吸収する。そうそうすることで障害者支援事業所の業務効率アップに貢献できると考えています。 ここまでミッションと実現したいことについてお話ししてきました。これらのために日々頑張ってはいるのですが、もちろん全てがうまくいっているわけではありません。課題も多くあります。 複雑な制度 × 期限必達 = 複雑化した仕様と肥大化したコードベース カイポケの障害者支援領域は複数プロダクトがありますが、最も長いプロダクトはサービスインから8年経過しています。その間、サービスを運用していく中で、大きな制度改定への対応も数回乗り越えてきました。 改定後の新しい制度に関する情報は厚生労働省から公開されるものをソースとしています。新しい制度の情報は少しずつドラフトの状態で公開されるのですが、確定情報が出てから制度施行日まで余裕がないこともあります。過去に私が経験した制度改定への対応では、制度施行日の数日前まで確定情報が公開されないということもありました。 そういった背景もあり、ときには開発に十分な時間が確保できず、急いでリリースまで行う必要があることがあります。「新しい制度への対応版のリリースが遅れる=ユーザーの業務が止まってしまう」ということを意味するからです。 理想を言えば、「適切な時間を使って、要件を検討し、既存の要件と新しい要件を最適化しながら仕様に落とし込み、その仕様に沿って実装し、リファクタリングを行い、必要なテストをクリアしてから、リリースする」というプロセスを踏みたいところです。しかし、デットラインまであと数日という状況ではそうも言ってられません。 ではどこを削るか?ですが、過去の私たちは、”既存の要件と新しい要件を最適化しながら仕様に落とし込む” と ”リファクタリングを行う” に対し、”適切な時間を掛けること” を削るという選択をしました。全ての変更において一貫してこの選択をしたわけではないですが、いくつかの変更において選択してきたことは事実です。 新しい要件は、既存の仕様に簡単に上乗せできるものばかりではありません。ときには既存の仕様を見直した上で新しい仕様を取り込むべきものもあります。 しかしながら、新しい制度の施行日という期限がある以上、変更箇所を少なくするために既存に手を入れずに新しい要件を加えるということもありました。そういった対応は少なからず歪さや冗長な面を残すことになります。 このような歪さや冗長な面は、その瞬間においては許容できるレベルであったとしても、年月を経るごとに当時の記憶も薄まり、忘れたころに複雑で理解し難い仕様として開発者を苦しめることになります。 ミッションの説明でお伝えしたように、障害者支援における請求のドメインはとても複雑です。ドメインが複雑な上に輪を掛けて仕様も複雑になってしまっています。つまり結果だけ見れば、過去の私たちの選択は「仕様の複雑度 > ドメインの複雑度」という状態を招いてしまいました。 また、仕様の複雑度が高いということは、コードベースの複雑度も増します。複雑度が増せば自然とコード量も増え肥大化していきます。複雑度が高く肥大化したコードベースのメンテナンス性は言わずもがな悪いものです。 これまでも私たちは、コードベースに一定の秩序を取り戻そうと、制度改定の合間などの時間的制約が少ないときにリファクタリングを行ってきました。ただリファクタリングも肥大化したコードベース相手では膨大な時間を要するため、まだまだ道半ばです。 仮にこの先、多くのリソースを投入してリファクタリングを完遂したとしても、複雑な仕様は残ります。それでは一時的な効果はありますが、根本的な解決とはなりません。 時を経るごとに複雑になっていく仕様、複雑度を増す仕様に引きずられて肥大化していくコードベース、そんな状況を改善しようと行うリファクタリング。自分たちで穴を掘って、後で穴を埋める、そしてまた穴を掘る、という消耗戦を続けてきました。 この消耗戦を終わらせなければ、いつか私たちの手に負えない状況になってしまうでしょう。そうなる前に「複雑化した仕様」と「肥大化したコードベース」をなんとかしなければなりません。 ユーザーへの価値提供を増やしたい 話は変わりますが、数年前からカイポケは内製化を進めてきました。(内製化の話は「 【前編】開発内製化の5年の軌跡。「消耗戦の悪魔のループ」をどう乗り越えたのか 」をご参照ください) 私を含めカイポケのエンジニアの多くは、内製化へ舵切りしてからJOINしています。それから、制度改定や機能追加、リファクタリング、不具合への対応などを通して、ドメインとプロダクトに関する知識を蓄積してきました。 そうなると見えてくるものがあります。「ココがこうだったら便利そう」とか「ユーザー業務を考えると今と違うアプローチの方が使い易くなるだろう」といった、ユーザーに今より多くの価値を提供できそう!というアイデアです。 ではそのアイデアを実装してみよう!となるのですが、アイデアはあくまで仮説です。事前にいくら検証したところで効果が100%保証されるものではありません。そのため、仮説→検証→実装→リリース→効果測定→仮説→(以下略)というループを高速に回していきたいのですが、そこで壁となるのが複雑化した仕様と肥大化したコードベースです。 複雑化した仕様と肥大化したコードベースに変更を入れることは容易ではなく、設計・実装・テストに多くの時間を要します。アイデアを実現しようとするたびに、たくさんの時間が掛かってしまうのであれば、実現するアイデアはより効果が期待できるものに限定する必要があります。ユーザーに提供できる価値のありそうなアイデアはたくさんあるのに、それらを実現するための時間が足りなくて、仮説のまま終わってしまうアイデアが多い。これはとても勿体ない話です。 リプレースへ ここまで私たちが抱える課題についてお話ししてきましたが、ここからは課題解決に向けて私たちが決断したリプレースについてお話しします。 上述の課題は以前より認識はしていました。ただ、制度改定であったり、リソースが足りなかったりと、なかなか根本的な解決に踏み切ることができない状況でした。そんな状況がしばらく続きましたが、ついに根本的な対応ができそうなタイミングが巡ってきました。それが今です。 次回の大きな制度改定まで期間があり(次回予定は2024年4月)、内製化以降エンジニアも増えてドメインやプロダクトに関する知識も蓄積できてきた、という2つの要因が揃った今だと判断して、根本的な解決としてリプレースを行うことにしました。 まず、リプレースのスコープは、広げ過ぎると時間も掛かりリスクも大きいため、現実的かつ効果的なスコープとして、一つの領域における請求機能(※)に絞りました。 ※私たちチームが担当している領域は障害者と障害児の2種類あり、今回は障害児の請求機能がスコープ。 次に、どのように進めるかですが、リプレースで行うことは大きく3つです。 既存の仕様を参考にしつつも、複雑さを排除した新たなドメインモデルを設計・構築する。 言語の表現力も借りて、基本に沿って秩序あるコードベースを再構築する。 ドメインモデルとコードベースに対し、変更容易性を維持する仕組み作りや取り組みをする。 1つ目は複雑化した仕様に対する対処で「仕様の複雑度 ≦ ドメインの複雑度」を実現すべく新しいドメインモデルを再構築します。 2つ目は肥大化したコードベースへの対処で、モダンな言語仕様の力も借りて、適切なコード量でメンテナブルなコードベースを手に入れるために、基本を守って実装します。 1つ目と2つ目を達成できれば、開発スピードが上がることが期待できますが、ここで終わってしまっては一時凌ぎにしかなりません。そこで大事なのが3つ目です。 3つ目は、今後も続く制度改定や新しい価値を提供するための機能追加を行うときに、過去の私たちと同じ選択(既存変更を抑えて単に上乗せする選択)をしなくて済むように短時間で開発できる状態を維持するための仕組み作りや取り組みです。 しかしながら画期的なソリューションを発見したわけではありません。変更容易性を維持するために、やるべきことをしっかりやる、というだけです。 具体例を挙げると、 ドメインモデルは、制度改定や機能追加によって変更される可能性を考慮し、それぞれの境界と接続ポイントを可視化(ココを変えるとコッチに影響するよね、というのがわかり易くなっている状態)しておくこと。そして仕様を変更する際は必ずドメインモデルの最適化を行うこと。 SOLID原則などの原則論に沿って実装すること。 テストコードをちゃんと書くこと。 PRレビューによって複数人の目を通すこと。 などです。(当たり前のことではありますが大事なので例に挙げました) これらは行動規範や規約として設けることは大事ですが、それだけでは人に依存してしまいます。新たなメンバーを迎え入れたときにスムーズにキャッチアップできるよう、可能なものはCIや静的解析などの機械的な仕組みで担保するようにしています。 2023年4月現在、請求機能リプレースはまだ完了しておらず鋭意開発中です。そのため今回のリプレースの決断と取り組み内容の良し悪しは、まだ評価できません。もちろん現時点では良い決断・良い取り組みだと信じているわけですが、最終的な評価はまだ先になります。 リプレース後に、ユーザーから使いやすくなった等の嬉しい声をいただく、カイポケの利用者が増加するといった目に見える成果がでる、または数年後の私たちが「リプレースしたお陰で開発しやすい」と感じることができたら良い決断だったと言えるでしょう。 最後に 今回お話しした請求機能のリプレースの成功は、私たちのゴールではなく一つの中継地点でしかありません。 解決したい課題や実現したいアイデアはまだまだあります。今後も新しいアイデアは出てくると思います。その時に一緒に考えて課題を解決するための仲間はまだまだ足りていないです。 今回はあまり技術的な内容に触れませんでしたが、今回のリプレースは以下の技術要素で開発中です。 Kotlin x Spring Boot TypeScript x Vue3 AWS Fargate Aurora Serverless v2(PostgreSQL) これらの要素にご興味があり、「ユーザーに多くの価値を提供したい!」と考える人がいらっしゃいましたら、ぜひ一緒に働きたいと思っております。 We are hiring! Join our team!!
はじめに 医療・介護・ヘルスケア・シニアライフの4つの事業領域で高齢社会の情報インフラを構築している、株式会社エス・エム・エスのAnalytics&Innovation推進部(以下、A&I推進部)の新卒3年目の小貝です。現在は主に介護職向け求人情報サービスである「カイゴジョブ」のデータ分析・アルゴリズム開発を担当しています。 A&I推進部はエス・エム・エス社内のデータを横断的に収集し、データの分析や加工から、データに基づく施策展開までを行う部門です。部門の役割としては以下の3つが挙げられます。 事業課題解決:事業会社のデータ活用組織として、PL貢献をする 技術開発:データ活用組織としての専門性をもつ 事業開発:専門性をお金に換える そのなかで最も優先度が高いのは1つ目の事業課題解決なのですが、組織としての専門性を維持するために技術開発や事業開発にも取り組んでいます。 今回は私が技術開発の一環で行っていた、介護事業者向け経営支援サービスである「カイポケ」のデータを使った「数理最適化による訪問介護のシフトスケジューリングモデル開発」について紹介します。 数理最適化とは 数理最適化とは、「ある条件を満たしつつ、利益やコストなどの関数が最大or最小になる変数の値を求める手法」です。扱える問題の例として、 300円以内で最も効用が高いおやつの組み合わせを求める(ナップザック問題) 商品の配送先がいくつかあったとき、移動距離が最も短くなるような巡回の仕方を求める(巡回セールスマン問題) ある駅からある駅への最安経路を求める(最短路問題) などが挙げられます。身近なところだと、乗り換えや経路検索のアプリは内部で数理最適化アルゴリズムが動いています。 最近では、「機械学習によって予測した応募率をもとに、数理最適化によって期待応募数が最大になるように求人をレコメンドする」「機械学習によって商品の需要を予測し、数理最適化によって最も経済的な在庫数を決める」といったように、機械学習との相性が良いこともあり、企業での活用が進みつつある技術です。 医療・介護業界における「勤務シフト作成」の難しさ 数理最適化という分野の中で有名な問題の1つに「ナース・スケジューリング問題」があります。名前の通り、病院などで働く看護師の勤務シフトを作成する問題です。「そんなの簡単じゃないの?」と思う人もいるかもしれませんが、看護師のシフトには以下のように考慮しなければならないことがたくさんあります。 各時間帯に◯人以上配備する必要があり、そのうち△人はベテラン看護師でなければならない AさんはBさんのメンターなので同じ時間帯に入れる必要がある 1人の看護師が2連続で夜勤をするのはNG AさんとCさんは不仲なので同じ時間帯に入れてはいけない すべての看護師さんで希望休の反映度合いを同じくらいにする必要がある(公平性) … これらすべての条件を満たしたシフトを、人間が作るのはかなり大変だと思われます。 実際、この問題の研究者が病院に行ったアンケートでは「勤務シフト作成にかかる時間は最大で30時間」「多くの人は勤務時間内ではなくプライベートの時間で作成している」「やりたくないと答えた人が90%」といった結果も出ています。 https://orsj.org/wp-content/or-archives50/pdf/bul/Vol.41_08_436.pdf ここまで複雑なのは看護師特有かもしれませんが、介護業界における介護ヘルパーでも同様に、勤務シフト作成の業務は発生しています。特に、ヘルパーが利用者の自宅を訪問する訪問介護では、介護士の勤務可能日時だけでなく、地理的な要素も考慮する必要がある(あまりにも遠くて非効率的な移動はさせたくない、など)ため、人間が手でシフトを作るのはなかなか大変だと思われます。 そこで今回は、数理最適化によって「訪問介護のシフトスケジューリング問題」を解いてみました(A&I推進部内に訪問介護のカイポケデータにとても詳しい人がいた、ということも訪問介護を扱うことにした大きな理由です)。 問題の整理・定式化 問題を解くために、「どういうデータが使えるのか」「どういう問題を解きたいのか」の整理をします。 カイポケには介護事業者の経営を支援する機能がいくつもありますが、その中に 利用者のサービス提供予定を登録する ヘルパーの勤務可能日時を登録する という機能があります。今回はその2つを使って、利用者のサービス提供予定が与えられたとき、ヘルパーの勤務可能日時を守りながら自動で各サービスにヘルパーを割り当てる数理最適化モデルを作ってみました。また、カイポケ内には「実際にどのような割当がされていたか」というデータもあるので、モデルによる出力と比較もしてみます。 データとモデルによる出力の比較 先ほども書いたように、数理最適化は「ある条件を満たしつつ、利益やコストなどの関数が最大or最小になる変数の値を求める手法」なので、 どういう条件で(=制約条件) 何を最大化 or 最小化するか(=目的関数) の2つを決める必要があります。思考の過程は省略しますが、今回は以下のような問題を解くことにします。 制約条件 可能な限りすべてのサービスにヘルパーを割り当てる 各ヘルパーの月間勤務時間の合計を一定の範囲内で収める(働かせすぎず、休ませすぎない) これに関するデータはないので「各ヘルパーは1日8時間まで勤務できる」としました 1人のヘルパーが同時にできないサービスには同時に割り当てない ヘルパーを分身・瞬間移動させない 遠すぎる移動もさせたくないので、移動距離が一定以上になるサービス(利用者宅)のペアも同時割当NGにしました 目的関数 未割当のサービスができたり、ヘルパーの勤務時間の合計が過不足した場合にペナルティを与え、それを最小化する 問題を数式に落とし込むことを分野特有(?)の言葉で「定式化」というのですが、今回の問題を定式化すると以下のようになります。なお、定式化にはこちらの書籍を参考 *1 にしました。 https://www.kindaikagaku.co.jp/book_list/detail/9784764905580/ www.kindaikagaku.co.jp データ 決定変数 minimize, subject to ※補足 データ:問題を解く前にわかっている入力データ 決定変数:問題を解かないとわからない(求めたい)変数 「minimize ~~~」:~~~(目的関数)を最小化する 「subject to ~~~」:~~~を制約条件とする Pythonによる数値実験 さて、問題の定式化ができたので、実際のカイポケデータを使って解いてみます。 数理最適化問題の解き方には大きく分けて「解を求めるアルゴリズムを自分で書く」「数理最適化ソルバーで解く」という2種類があります。とてもざっくり言うと、前者は解の探索手法や効率的な計算方法など競技プログラミング的なスキルが強く求められ、後者はソルバーで解ける形に数式を落とし込むという学術的な知識やスキルが求められるといった違いがあります。今回は個人的になじみのある、後者のソルバーを使ったやり方で解いてみます。 数理最適化ソルバーには有償・無償のものを含めいくつか種類があり、当然有償のほうが高性能ですが、今回はそこまで問題の規模が大きくないので、無償かつ商用フリーのCBCというソルバーを使います。また、ソルバーに問題を入力するラッパー(モデリング言語)としてPython-MIPを使います。先ほど定式化したものをPythonで実装すると以下のようになります。 https://github.com/coin-or/Cbc https://www.python-mip.com def model (): ''' ヘルパーごとの勤務時間量の上下限をなるべく守って各サービスに担当可能なヘルパーを割り当てる ヘルパーごとの移動範囲をコンパクトにするために、移動距離が大きい利用者のペアは1人のヘルパーに同時に割り当てない ''' model = Model( "model" ) model.emphasis = 1 # 最適性よりも実行可能であることを優先する model.verbose = 1 # ログを表示する model.preprocess = 1 # 定式化を解きやすくする # 変数の定義 x, alpha, beta_m, beta_p = {}, {}, {}, {} for h in target_helper_id_list: # ヘルパーごとの勤務時間量の上下限を下回る/上回る量 beta_m[h] = model.add_var(lb= 0 , name=f "beta_m_{h}" ) beta_p[h] = model.add_var(lb= 0 , name=f "beta_p_{h}" ) for s in target_shift_id_list: # ヘルパーhにシフトsを割り当てるとき1、それ以外は0になる変数 x[h, s] = model.add_var(var_type=BINARY, name=f "x_{h}_{s}" ) for s in target_shift_id_list: # シフトsがどのヘルパーにも割り当てられなかったとき1、それ以外は0になる変数 alpha[s] = model.add_var(var_type=BINARY, name=f "alpha_{h}_{s}" ) # 目的関数 ## シフトにヘルパーが割り当てられなかったり、ヘルパーの勤務時間の過不足に関するペナルティを最小化する p1 = 10000 # シフトにヘルパーが割り当てられなかったときのペナルティ p2 = 10 # ヘルパーの勤務時間の過不足のペナルティ(1時間ごと) model.objective = minimize(xsum(p1 * alpha[s] for s in target_shift_id_list) + xsum(p2 * (beta_m[h] + beta_p[h]) for h in target_helper_ids)) # 制約式 for s in target_shift_id_list: ## すべてのシフトにヘルパーを割り当てる(アルファで緩和) model += (xsum(x[h, s] for h in target_helper_id_list) + alpha[s] == 1 ) for h in target_helper_id_list: ## ヘルパーごとに勤務時間量の上下限を守る model += (xsum(shifts_time_amounts[s] * x[h, s] for s in target_shift_id_list) >= low_time_amounts[h] - beta_m[h]) model += (xsum(shifts_time_amounts[s] * x[h, s] for s in target_shift_id_list) <= upp_time_amounts[h] + beta_p[h]) ## 勤務時間量の緩和制約 model += (beta_m[h] <= low_time_amounts_ease[h] - low_time_amounts[h]) model += (beta_p[h] <= upp_time_amounts[h] - upp_time_amounts_ease[h]) # 同時に担当不可能なサービスには割り当てない for s1, s2 in overlapped_shifts: for h in target_helper_id_list: model += (x[h, s1] + x[h, s2] <= 1 ) model.__data = x, alpha, beta_m, beta_p return model 得られた結果と考察、課題 カイポケ内のデータにある10事業所に対して、今回の問題を解いてみたところ、以下のような結果が得られました。 得られた結果 「ヘルパー数」「利用者数」「サービス数」が入力データに関する情報、「実行時間」がソルバーによる計算にかかった時間を表しています。「ヘルパー1人あたりの移動時間」は、カイポケ内に記録されている実際のシフトでの移動時間と、今回作ったモデルの出力での移動時間、その差分を表しています。差分がマイナスなほど、モデルによって移動時間が大きく削減されていることを意味します。なお、地図上の直線距離から移動時間を計算していて、移動速度は簡単のため東京都では自転車移動を仮定して分速250m、東京都以外では車移動を仮定して分速500m(時速30km)としています。 これを見ると、まずおおむね実際よりも移動時間が短くなるシフトが得られていることがわかります。最大だとヘルパー1人あたり月間279分(=4.6時間)もの移動時間の削減ができています。一方で、差分がプラスで削減されなかったケースもありますが、そこそこ効率的なシフトが「人の手を借りず自動で」得られたことにも価値はあると考えています。 また実行時間をみると、月間のサービス数が多いと実行時間が長くなることがわかります。特に、サービス数が1000を超えたあたりから急激に実行時間が伸びていそうです。今回はあくまでも技術開発として進めましたが、もしこのモデルによる「シフト自動作成機能」を実際にカイポケに組み込むとすると、月間サービス数が1000を超える大きい事業所には何らかの工夫をする必要がありそうです。 モデルの挙動をもう少し細かく見てみます。こちらの図は、とある事業所の一日のシフトを地図上に可視化したものです。 シフトの可視化A 色はヘルパーに対応し、①などの数字はヘルパーごとの訪問順を表します。左がカイポケ内に登録されている実際のシフト、右が今回のモデルによって得られたシフトです。 これらを比較すると、実際よりもモデルによるシフトのほうが遠距離の移動が減り、全体的にコンパクトに利用者宅を巡回していることがわかります。合計の移動時間も25分→15分に削減されており、「遠すぎる移動はさせない」という制約条件をモデルに組み込んだ効果が得られています。 一方で、望ましい結果が得られていないケースもあります。こちらの図は、先ほどと同じ事業所の別日のシフトを可視化したものです。 シフトの可視化B これを見ると、たしかにモデルによるシフトのほうが移動時間は大きく削減されているものの、1シフトのためだけに出勤するヘルパーが増えてしまっていることがわかります。「ヘルパーを増やしたら移動時間も短くなる」というある意味当然の結果になってしまっており、これを改善するためには移動時間以外の効率性も考慮する必要がありそうです。 さいごに 今回はカイポケ内のデータと数理最適化の技術を使って、訪問介護のシフトスケジューリング問題を解いてみました。 簡単なモデルを作成して結果を見てみると、おおむね実際のシフトよりも効率的に移動するシフトが得られました。一方で、今回のモデルだと月間のサービス数が一定を超えると計算時間が急増したり、「移動時間が減るかわりに稼働するヘルパーが増える」といった微妙な結果になることもありましたが、改善のための示唆が得られたという点ではプラスだと思います。 そして何よりも、社内データを使って数理最適化の技術検証ができたというのは、A&I推進部にとって大きな意義があると考えています。数理最適化にかぎらず、今後もさまざまな技術を検証して、ゆくゆくは事業課題解決に役立てることができたら嬉しいです。 エス・エム・エスには数多くのサービスがあり、介護、医療に特化したデータを数多く保有しています。データサイエンスを活用したサービス向上ニーズも高く、今回のような技術開発にも力を入れています。今回の記事で少しでも興味を持って頂ける方がいらっしゃいましたら、ぜひお話を聞きに来ていただけたらと思います。 *1 : シリーズ:最適化モデリング 第3巻 ナース・スケジューリング 問題把握とモデリング p.157
介護事業者向け経営支援サービス「カイポケ」のソフトウェア開発者の空中清高( @soranakk )です。 2021年12月に入社し、介護事業者向け経営支援サービス「カイポケ」のフルリニューアルプロジェクトに携わっています。 今回はフルリニューアルプロジェクトのアーキテクチャ選定や背景についてご紹介したいと思います。 フルリニューアルプロジェクトのアーキテクチャ フルリニューアルプロジェクトのシステムはざっくりと解説すると、バックエンドにいくつかのサービスが並んでいて、フロントエンドとバックエンドの間にゲートウェイが立っているサービスベースアーキテクチャになっています。 それぞれのサービスはAWSに配置するようになっていて、この図のような構成になっています。 またフロントエンドとバックエンドはGraphQLを利用して連携している形になっていて、さらにバックエンド間通信においてもGraphQLを利用しています。 このようなアーキテクチャを採用したのは介護業界で適切なソフトウェアを開発するためです。 そこで、介護業界とはどんなものなのかを説明して、なぜこのようなアーキテクチャを採用したのかを深ぼっていきたいと思います。 複雑で変化の激しい介護業界 まず一口に介護業界と言っても、そこには多種な介護サービスがあり、また介護サービスには登場人物がたくさんいます。 介護サービスは通常のサービスと異なり、介護を受ける利用者が介護サービスを提供している事業者から介護サービスを受ける、というだけではありません。 その人にどんな介護サービスが必要で、どこの事業所ならその介護サービスが提供できて、一月分の予定(ケアプランと言います)はどうしたらいいか、ということを考えて利用者にケアプランを提供するケアマネジャーという方がいます。 介護が必要になった人はまず、このケアマネジャーからケアプランを受け取って、それを元に介護サービスを提供している事業所で介護サービスを受ける、という形になっています。 また、この時のお金の流れも普通の買い物などと異なり、介護保険というものが大きく関わってきます。 利用者は介護サービスを受けるときに全額負担ではなく、介護保険によって1~3割の負担を事業所に支払います。 介護サービスを提供した事業者は利用者から受け取らなかった保険分は1ヶ月分をまとめて次の月に国民健康保険団体連合会(通称:国保連)に請求して残りの金額を受け取る(介護保険請求)、という流れになっています。 また、そもそも利用者から直接報酬を受け取っていないケアマネジャーは次の月にまとめて国保連に請求することで報酬を受け取るようになっています。 このときケアマネジャーの提出するケアプランの内容と事業所の提出するサービスの提供実績に齟齬があると介護給付費が支払われないため、単に国保連に提出するだけではない複雑さがあります。 介護保険請求の流れ さてここまでで、介護サービスを受ける人、ケアプランを決定するケアマネジャー、介護サービスを提供する事業所、介護給付費を支払う国保連、という登場人物が出てきました。 さらに介護サービスと言っても、そのサービスの種類はたくさんあります。 介護施設で受けるサービスの他に、例えば在宅で受けるサービスは内容によって数種類ありますし、他にも福祉用具の貸し出しサービスなど、本当にたくさんのサービス種類があります。 さらに数年毎に法改正が行われるため、たびたびルールに変更が加わったりサービス種類が増えたりと変化がとても激しいです。 サービスベースアーキテクチャ カイポケのフルリニューアルプロジェクトでは、このように複雑で変化の激しい介護事業の環境に耐えながら継続して開発し続けられるシステムの形をドメイン駆動設計を通じて考えてきました。 その結果、「カイポケ」はいくつかの領域に分割できるだろうということがわかってきました。 大まかに言うと、ある介護サービス種類に特化した領域、介護保険との関連が強い介護報酬に関係する領域、カイポケをSaaSとして提供するための領域、などです。 そこでカイポケのフルリニューアルプロジェクトでは、確実に分解できそうな粒度の粗い領域毎のマイクロサービスにシステムを分割するサービスベースアーキテクチャを採用することにしました。 これまでモノリシックだった「カイポケ」をいくつかのサービスに分割して並行開発体制を取ることで、介護事業の複雑さや法改正による変化に対応しようとしています。 サービス分割を行なったばかりの頃は、まだフロントエンドとバックエンドには分かれておらず、それぞれのサービス毎にカイポケを開発していく想定でした。 いわゆるマイクロフロントエンド構成です。 次はどのようにしてマイクロフロントエンド構成からフロントエンドとバックエンドが分かれたのかについて、ご紹介したいと思います。 フロントエンドとバックエンドの分割 初期のアーキテクチャでは分割したサービス毎にフロントエンドを開発するマイクロフロントエンドの構成を考えていました。 しかし更なるドメイン駆動設計やユーザーテストの結果からバックエンドの分け方とフロントエンドの分け方は異なることがわかってきました。 バックエンドは介護業界のサービス種類や介護保険などの法律上の制約で分けることができたのですが、フロントエンドも同じような分け方をすると使いづらいソフトウェアになってしまうことがわかりました。 また、フロントエンドはカイポケを利用する人、つまり介護業界に登場する人物毎に分けた方がUXがよさそうということがわかりました。 そのためフロントエンドはバックエンドのサービスとは分かれ、マイクロフロントエンドの構成をやめることになりました。 その経緯について、こちらの記事でも詳細に書かれていますので、ご紹介します。 tech.bm-sms.co.jp ただし上の記事からのアップデートとして、一部の利用者に依らない共通のフロントエンドについてはバックエンドのサービスと合わせてマイクロフロントエンド構成で開発を進めているサービスもあります。 フロントエンド/バックエンド連携 フロントエンドとバックエンドは分かれましたが、フロントエンドからいくつもあるバックエンドを選んで通信を行うのは辛いだろうということからフロントエンドとバックエンドの間にAPIゲートウェイを置いています。 当初から認証の観点でゲートウェイを置くことになっていましたが、さらにAPIを束ねる機能も担うことになりました。 また、フロントエンド/バックエンド間通信のプロトコルはクライアント側から柔軟にデータの取得方法を選択できるGraphQLを採用しています。 このAPIゲートウェイの置き方を少し工夫しているのでご紹介します。 APIゲートウェイでバックエンドのGraphQLを束ねる フロントエンドからはゲートウェイだけが見える状態になっていて、ゲートウェイへGraphQLリクエストがやってきます。 その後のゲートウェイから適切なバックエンドへ流す処理を実現するため Apollo Federation を採用しています。 Apollo Federation は複数あるGraphQLサーバーを一つにまとめることができる技術です。 Apollo Federationを用いることでゲートウェイにやってきたGraphQLリクエストを適切なバックエンドのGraphQLサーバーに流すことができ、適切なレスポンスをフロントエンドに返すことが可能になります。 Apollo Federation を使わないで複数のGraphQLを束ねようとすると、ゲートウェイにやってきたGraphQLリクエストから適切なサーバーを選んでルーティングする処理を実装しなければならず、実装コストの増加や管理が煩雑になる懸念があります。 ただし、Apollo Federation を使う場合にも注意しなければならないことがあります。 バックエンドのGraphQLサーバーはそれぞれ独立しているのですが、最終的にゲートウェイで一つに束ねられることになります。 そのためバックエンドのGraphQLのスキーマ同士で、それぞれ名前等が衝突しないように作っておく必要があります。 また命名規則も統一しておかないと、あるバックエンドのGraphQLスキーマでは複数形は s を付けるようになっているけど、別のバックエンドでは List と付けるようになっていて〜、といった混乱が起こってしまいます。 なのでスキーマの命名規則などはバックエンドのチーム間で話し合って統一するようにしています。 また名前の衝突など、バックエンドのGraphQLスキーマを束ねた結果に不整合が発生していないことをチェックするため、GraphQLのスキーマ管理サーバーとしてApollo Studioを利用しています。 それぞれのバックエンドサーバーはApollo StudioにGraphQLスキーマを登録する形になっていて、ゲートウェイからはApollo Studioを参照して合成GraphQLスキーマを取得するようにしています。 またバックエンドのチームはCIで合成したGraphQLスキーマが壊れてしまわないこともチェックするようにして、不意に壊れてしまってフロントエンドからアクセスできなくなることを防いでいます。 フロントエンド・バックエンド間の通信だけでなく、サービス間連携においてもGraphQLを採用しています。次に、そのことについてご紹介します。 サービス間連携におけるGraphQLの採用 リニューアルプロジェクトではバックエンド間のサービス間通信プロトコルでもGraphQLを採用しています。 バックエンド間通信だけを考えるとGraphQLは適切なのだろうか?といった疑問もありました。 しかしそれぞれのバックエンドはフロントエンド用にGraphQLプロトコルを提供することが決定していたため、サービス間通信用に新しいプロトコルに対応すると、フロントエンド用とサービス間通信用で二つのプロトコルを管理することになります。 また二種類のスキーマを管理するのは煩雑になるだろうというもありました。 そこで、スキーマ管理とプロトコルスタックを少なくしてシンプルにしたいという理由からバックエンド間通信でもGraphQLを利用することに決定しました。 フロントエンド用とバックエンドのサービス間通信用のGraphQLスキーマを統一的に管理するために少し工夫しています。 完全に二つのスキーマファイルを分けて管理する方法も検討したのですが、queryやmutationは違うだろうけど、type等は同じものを使いたい場面がありそうで、その場合にそれらを共有する方法をどうしたらいいのかが課題になりました。 そこでGraphQLのDirectiveとApollo Studioの機能を使って、一つのスキーマファイルで両方を管理できるようにしています。 Directiveには可視性を制御できるものがあり、バックエンドのサービス間通信用のスキーマは不可視の状態でApollo Studioに登録することで、ゲートウェイからApollo Studioを参照したときは存在しないように見せることができます。 一方、サービス間通信の場合のスキーマはバックエンドの統一スキーマではなく、通信したいバックエンド単体のスキーマで良いので、直接そのスキーマを見るようにして参照します。 最適なシステムの形を目指して 今回紹介した構成で実際に並行開発体制を維持しながら法改正に対応できるかどうかは、これから確認していくことになります。 その結果からまたドメイン駆動設計を行うというサイクルを通じて、私たちはより良い「カイポケ」を作り上げていこうとしています。 昨今のソフトウェアは開発してリリースしたら終わりという訳ではなく、継続して改善し続けていくことが基本となっています。 私たち「カイポケ」のフルリニューアルプロジェクトチームでも次の法改正を視野に入れながら、変化していく介護業界で継続して改善し続けられるシステムアーキテクチャを目指して開発を続けています。 実際、今回紹介したアーキテクチャではフロントエンドとバックエンドは完全に分かれているのですが、さらなる検討の結果から、一部の利用者に依らない共通のフロントエンドについてはマイクロフロントエンドのようにバックエンドもフロントエンドも開発している状況だったりします。 こういったエス・エム・エスでのアーキテクトの仕事の仕方についてはこちらの記事で詳細に書かれていますのでご紹介します。 tech.bm-sms.co.jp 最後になりますが、フルリニューアルプロジェクトではエンジニアだけでなくプロダクトマネージャー・UI/UXデザイナーなど、複雑で変化の激しい介護業界での最適なシステムの形を目指して一緒に開発をしてくれるメンバーを募集中です! プロジェクト専用サイト 内に募集中のポジションを記載していますので、興味のある方はぜひご覧ください! またカジュアル面談もやっておりますので「選考に進む気持ちはまだないけどもっと突っ込んだ話を聞きたい」とか「ここはどうなってるの?」みたいな感じで軽く話してみるだけでも結構ですので、興味があれば是非お気軽にお声がけください!
はじめまして。 @kimukei です。 2022年9月1日からソフトウェアエンジニアとして株式会社エス・エム・エスで働いています。 この記事では、「カイポケリニューアルプロジェクト」(以下「リニューアル」)とこの会社について、最近ジョインした私の目線から実際どんな感じなのかを掘り下げていきます。興味を持っていただけている方にチームの雰囲気や文化などが伝われば幸いです。 リニューアルについては以下のサイトもぜひご覧ください。 careers.bm-sms.co.jp これを書いている人 2018年に新卒で株式会社LIFULLに入社してからずっとWebの領域でフルスタックに色々エンジニアリングしてきました。正社員エンジニアとして働きながら副業エンジニアとしても複数の会社でプロダクトを手がけていたりと精力的に活動してきました。前職のBASE株式会社では主に顧客管理システムのローンチや新機能の開発のリードをしてたりしていました。 詳しくはここにまとめていますので興味があれば覗いてみてください。 https://kimuchanman.github.io/self-introduction/ では早速本題に移ります。 そもそもなんでエス・エム・エスに入社したの? まず、エス・エム・エスをどうやって知ったかですが、一番最初は転職ドラフトの指名です。私の場合、指名いただいた企業様の指名理由やテックブログなどに目を通してからお返事していました。 エス・エム・エスはテックブログやドキュメントから、アーキテクトを定義しアーキテクトへのキャリアパス *1 *2 があることが伺えて興味を持ちました。組織によってアーキテクトの役割が異なりフワッとしがち、なんならアーキテクトという役割は設けていなかったりする中、自組織におけるアーキテクトの役割や考え方を明確に語っている組織は多くないと思います。 選考の前に、カジュアル面談を通して介護領域の複雑さと、2025年問題や2040年問題などの近い将来に迫った問題について説明していただきました。 日本が高齢化先進国になるということは、それを支えるシステムは高齢化社会の世界的なモデルとして模倣されるようになるはずです。そのため介護業界は近い将来、日本が世界をリードする領域になり得ることに関心を持ちました。 エス・エム・エスとはカジュアル面談を3回行なったのですが、リニューアルの指揮を執っている三浦さん *3 とのカジュアル面談では、プロジェクト概要や開発プロセスを説明してもらいました。 開発プロセスについては Domain-Driven Design Starter Modelling Process に沿った本格的な DDD のプロセスを導入しており、実際に介護現場での経験が長い方がドメインエキスパートとしてプロジェクトにジョインしているということを知りました。ここまでしっかりした DDD のプロセスを組織全体で実行するのは相当な体力がないとできないことだと思い興味が深まりました。 また、介護ドメインは複雑ながらも、入社前から介護制度に明るいエンジニアはごく少数であることやみんな入社してから学べていること *4 から、未経験の介護ドメインに飛び込む勇気が湧きました。 そういった経緯で、このプロジェクトは学びが多そうで面白そうだと思い入社に至りました。 実際に入ってみて 概ね入社前に聞いていた情報と大きな乖離はありませんでした。 リニューアルでは、多くのシニアメンバーがプレイヤーとして実際に手を動かしてサービス開発をしています。みんな経験豊富で、日々多様な視点や解決法が見られプレイヤーとしてはなんとも代え難い福利厚生を享受していると感じます。 また、実際に学んでみると介護制度の複雑さがわかってきました。というのも、そもそも介護まわりの法制度が既存のものに継ぎ足しで今があるような構造なのです。ソフトウェアエンジニアの方には、機能の追加を重ねていく中で複雑度が上がってきてしまったソフトウェアをイメージしてもらうと伝わりやすいかと思います。 更に、エス・エム・エス社内の中で紹介されている推薦図書に 『最新図解 スッキリわかる! 介護保険 第2版 基本としくみ、制度の今とこれから』 があり、これを読むとなおさら介護制度の複雑さが理解できました。 たとえば、単位数の計算がサービス種類や加算の種類の多さによって複雑になっていることや、サービス種類によって関わる人の役割や職種が変わってくることが挙げられます。そして、法改正の際にはこれらに変更が生じることもあります。リニューアルプロジェクトにおいても、このような複雑さをどのように取り扱うかは大きな焦点の一つとなっています。 その過程でドメインエキスパートと一緒にドメインモデリングができたり、ドメインエキスパートに気軽に相談できたり、定期的に施行される法改正についてドメインエキスパートによる説明会が行われることは非常に安心感があります。 しかし、入社前の想像と実際に入ってみてのギャップを感じる点もありました。入社時点(2022年9月)に感じたギャップとしては、ADR(Architecture Decision Records)などのドキュメント類が完全に整備されている訳ではなかったことが挙げられます。詳しくは後述します。 ドキュメンテーションについて 入社時点では、ドキュメントはそこまで整備されている方ではなかったです。 これは、当時はプロジェクトメンバーが少数で MTG やモブプロなど同期的な場で解決されることが多かったことが理由としてはあると思います。 しかし、プロジェクトメンバーの規模が拡大していく中でこのやり方に課題を感じ、組織としても、 GitLab Handbook を参考にしたコミュニケーションガイドラインを策定し、なるべく非同期で仕事を進められるようにドキュメンテーションをより意識的に取り組んでいます。また、フロー情報とストック情報を意識し、決定事項をストック情報としてロックして共有する方法を取るようになりました。 プルリクエストについても、GitHubのテンプレートの機能も活用して、変更の背景や設計方針や今回の変更のスコープ外の点などを明記するようにしており、プロジェクトとしても少しずつそういった「なぜ」や「どうやって」を明文化する意識が根付いてきたように感じます。 どういった人におすすめできるか エス・エム・エスはプロダクトごとに組織の色は違っていて、私がジョインしたリニューアルを手がける組織では、大きく二つの特徴があると思います。 一つ目は、個人に与えられる裁量が大きいということです。組織構造として縦のレイヤーが少なく、かなりフラットな体制を取っています。そのためきっちり分割されたタスクという単位では仕事が降ってくることは稀です。自発的にプロジェクト全体を成功に導くために必要なことは領域を問わず発揮することを期待されます。 自発的にタスクを定義し調和を取りつつ進めるような動きを取るため、ルーズボールが発生しやすい仕組みになっているとも言えます。そのため、ルーズボールを積極的に拾って行けたり、そもそもそういった問題を未然に防げたり、幅広く動ける人が現況だとより活躍しやすそうです。ファシリテーション力や巻き込み力も重要です。 よく「鳥の目、虫の目、魚の目」と称されるような、視点の使い分けをしながらのエンジニアリングスキルがあると非常に活躍できると思います。 とはいえ、この組織体制のままではなかなかスケールしません。現状についてはプロダクトがローンチ前であることに起因していそうで、開発初期はファジーで動的な要素が多いためであり、この構造はプロダクトの輪郭が出来上がるにつれて解決されていきそうです。 二つ目は、更なる成長を目指している大規模で複雑なシステムの構築に初期段階から携われる点にあります。 勝負する介護業界は前述の通り複雑性が高く、構築するシステムも簡単なものではありません。つくるものも多岐に渡りますが、その分得られる知識や体験は大きいです。 会社としてもスキルの習得には最大限補助をしてくれます *5 し、学ぼう、成長しようと思えばどんどん成長していける環境にあると思います。 また、まわりのメンバーのアウトプットから刺激をもらうことも多いと思います。 これらの点に魅力を感じてくださる方には是非ともジョインしていただきたい、成長に飢えていたり刺激が欲しい人にはもってこいの環境です! おわりに リニューアルの全容や詳細は、カジュアル面談や会食の場の方がもっと話せてよりニュアンスなどが伝わりやすいと思います。 高齢化社会を支えるシステムの需要は明確にあります。 また、今後の日本の高齢化現象を考えると介護はかなりの成長市場で、どんどん面白くなってくると思います。社会的なインパクトや意義も大きい分野です。 そんな市場で一緒にエンジニアリングしていくメンバーを随時募集中です。 以下のページから連絡いただくか、直接 私 に連絡してくれればお話します! やっていきましょう! *1 : https://tech.bm-sms.co.jp/entry/2021/01/05/142920 *2 : https://tech.bm-sms.co.jp/entry/2021/03/09/090000 *3 : 三浦さんの入社エントリー: https://tech.bm-sms.co.jp/entry/2021/05/18/120000 *4 : Domain-Driven Design Starter Modelling Processで学んでいった過程や成果物は Miro に残しているという話も伺っていたため、入社後にそれまでのドメインモデリングのキャッチアップは可能だろうとも考えていました。 *5 : 特徴的な制度・環境: https://careers.bm-sms.co.jp/engineer/kaipoke-renewal#block-ed672e58230841bd92a655e618bf7983
エス・エム・エスのエンジニアの宮坂です。2022年1月に入社し、以来、介護事業者向け経営支援サービス「カイポケ」の リニューアル開発 に携わっています。 今回は私が開発を担当している「介護報酬の算定ロジック開発」についてお話したいと思います。 介護報酬とは何か そもそも介護報酬とは何でしょう? 高齢になったり病気になったりして、周りのサポートがないと生活が難しい状態になってしまった場合、介護サービスを受けることになります。介護サービスは、提供したサービス内容や介護の必要度合いなどに応じて金額が定められていて、これを介護保険(一部、利用者負担)から介護サービスを提供した事業者に支払われます。これが介護報酬と呼ばれるものです。 介護保険は、法律によって40歳以上になると誰もが加入することになり、その保険料は毎月の給与などから引かれているので、介護を受けていない人も含めて介護サービスを支えていることになります。 難解な介護報酬の算定 この介護報酬、ちゃんと計算して請求しないと介護サービスを提供している事業者は収入を得られないので事業を続けられず困ってしまうのですが、この算定がすごく難しいのです。 提供される介護サービスにはそれぞれ「単位数」というものが定義されていて、提供した条件によってはさらに単位数が加算されます。その計算のルールが算定構造として国から提供されており、それをもとにロジックを組み立てていきます。こうして、算定した単位数に、地域ごとに定められた「地域単価」を掛けることで、介護報酬が決まります。(介護報酬 = 単位数 × 地域単価) 例えば、ある介護サービスの利用者に 「身体介護」という訪問介護のサービスを20分提供した 訪問は2人で行った その提供時間は「早朝」だった とします。このときの地域単価が10円の場合の介護報酬は、 身体介護 20分以上30分未満 = 250 単位 … ① 2人の訪問介護員等による場合 = ① × (200/100) = 500 単位 … ② 夜間もしくは早朝の場合 = ② + ② × (25/100) = 625 単位 … ③ 介護報酬 = ③ × 10 = 6,250 円 となります。このうちの利用者の負担額が1割とすると、 介護保険への請求額 = 6,250 × (1 - 0.1) = 5,625 円 利用者への請求額 = 6,250 - 5,625 = 625 円 となり、介護サービス事業者は、介護保険から 5,626 円、介護サービスの利用者から 625 円を受け取る計算になります。 (介護保険の自己負担は所得に応じて1割から3割までのいずれかとなります) 図1:訪問介護費の算定構造の一部 https://www.wam.go.jp/gyoseiShiryou-files/documents/2022/0322211552821/202203a.pdf (赤囲み線は引用者) (2023年3月3日 閲覧) こういった単純なパターンであればそこまで難しくありませんが、介護報酬を算定する際には様々な条件を考慮しなくてはなりません。 単位数が加算される条件の考慮 図1 だと「緊急時訪問介護加算」は「身体介護」提供時にしか加算されない 利用者の介護度(どの程度の介護が必要なのかのレベル) 介護度に応じた介護保険から支払うことができる金額の限度額の考慮 生活保護などによる公費負担 利用者が引っ越しをした、介護度が変わったなど介護サービス提供中に発生した変更の考慮 などなど、挙げ出したらキリがないですが、介護報酬をキチンと計算するには、すべてのパターンを考慮した算定ロジックを実装する必要があります。このパターンとこのパターンを組み合わせた場合はどう算定したら良いのだろう…というような算定構造だけではわからない不明瞭なパターンも多くあり、様々な資料を読み漁りつつ、正確な算定を実装していく必要があり、ここが介護報酬の算定を難解にしている理由の一つでもあります。 3年に一度の介護制度の改正 もう一つ、介護報酬の算定にとって高いハードルがあります。それは3年に一度行われる報酬改定です。刻々と変わる社会の状況に対応するため、定期的に介護保険制度の見直しが行われています。これによって、今までの制度が更新されることもあれば、新しい制度が追加されることもあるので、これに対応して行かなければいけません。 報酬改定については以前にも記事にしているので、興味がある方はそちらもご覧ください。 tech.bm-sms.co.jp この改正された制度は大半が4月から(一部は10月から)適用されるのですが、適用開始のギリギリまで正式決定されません。例えば令和3年の報酬改定では、令和3年3月31日に確定された旨が通知されました。お金に関わる計算なので、正確な実装を求められるのですが、制度の内容が不明瞭な中での短期間での開発が必要になり、開発の難易度が高くなっています。 もちろん、過去分の請求をしていない人たちも存在するため、制度改正前の介護報酬の算定もある程度の期間は維持しておかないといけません。これもロジックの実装の難易度を上げる要因となっています。 難解な算定ロジックと闘うために だからといって、この部分の実装を諦めるわけにはいきません。介護報酬の算定とその請求処理はカイポケのコア機能です。これが正しく実装されなければ、プロダクトが成り立たないのです。 そこで、難易度の高い計算をより容易により正確に実装するため、様々なトライをしています。 新たな介護報酬算定ロジックの開発 度重なる報酬改定の短期間での開発で、継ぎ足し継ぎ足しの実装をしていった結果、既存の介護報酬の算定ロジックは非常に複雑度の高いものになっています。そこで、介護報酬の算定順序や算定に影響のある条件を整理し直して、介護報酬算定ロジックの開発を一から行っています。 先程ご紹介した算定構造はある程度のパターンを持った構造で表現できるので、これを元に算定ロジックの実装を進めています。一度算定構造の理解が誤っていて、実装を一からやり直しをすることがありましたが、理解度が上がった段階で整理をすることができたので、シンプルな設計に再構成することができました。 まだまだ完成には程遠いですが、短期間での報酬改定に対応できる算定ロジックを目指して開発を進めて行きたいと思っています。 専門家によるテストケースの作成 紹介したとおり、介護報酬の算定には、様々なパターンが考えられ、すべてのパターンで正確に算定できる必要があります。算定ロジックの実装と並行して、ドメインエキスパートやQA、プロダクトオーナーなどエンジニア以外のロールの方々の知識を集結して、考えうるパターンを網羅したテストケースを作成してテスト実行を自動化しました。 これによって、実装の変更による不具合をすぐに検出し、品質の担保をしていきたいと思っています。 まとめ 介護報酬の算定の難解さと、その難解な算定ロジックとどのように闘っているかを紹介しました。正直、私も入社して1年ほどなので、まだまだわからないことだらけです…。 ですが、いろいろな知識を持ったチームメンバーと、難解なロジックを一つのアプリケーションに落としていく過程にやりがいも面白さを感じています。チームの雰囲気も良く、協力的なので、様々なトライをしながらの開発ができ、そういった環境もロジック開発の面白さを感じさせてくれる部分なのかなと思ったりしています。 まだまだ開発の半ばなので、一緒にチャレンジしてくれる仲間が必要です!少しでも興味を持ってもらえたら幸いです!
こんにちは。2022年10月にエス・エム・エスに入社した塩井です。 現在はプロダクト開発部介護キャリア開発グループにて、介護職向け求人情報サイト「カイゴジョブ」の開発を行なっています。 この記事では、私が前職からの転職先にエス・エム・エスを選んだ理由、そして実際にエス・エム・エスで働いてみて感じていることなどをご紹介したいと思います。 誰? 改めましてしおいと申します。Twitterでは @coe401_ 、GitHubでは @shioimm と名乗っています。 前職では東京と福岡に教室を展開する小中高校生向け英語塾の社内開発チームで学習管理システムの開発を行なっていました。 プログラミング言語Rubyとそのコミュニティが大好きで、好きが高じた結果RubyKaigi 2021とRubyKaigi 2022にて登壇の機会をいただきました。よく地域Rubyコミュニティのミートアップに出入りしています。 前職までのあらすじ 前職が英語塾の会社であったことを前述しましたが、更に遡るとそれ以前はプログラマではなく、コールセンター業務や事務職で数社を転々としていました。そうするうち、「せっかく働くのなら自分が働いた分、少しでも世の中が良くなるような仕事をしたい」と考えるようになりました。 世の中には、解決すればもっと世の中が良くなるような課題がたくさん存在しており、それらの課題に取り組んでいる企業・団体・人々もまたたくさん存在しています。その中でも特に、テクノロジーの力で社会課題の解決を目指すようなプロダクトを作る仕事をしたい、と考えてプログラマになることを志し、自分の中での大きな関心ごとであった教育事業に関わるため前職で働き始めました。 とはいえ特にコンピュータサイエンスについての素養はなく、付け焼き刃で少々Rubyをかじった程度の状態で職に就いたため、最初は右も左もわからず…。社内では周りの上司や先輩方にお世話になり、社外ではRubyコミュニティの人々と交流しながら七転八倒しているうちに何とか業務でも趣味でもたのしくプログラミングができるようになっていきました。 前職は小さなチームで小さなプロダクトをじっくり開発するという特徴があったため、自分たちで考えて実現できることも多く、次第に開発をスムーズに進めるための自分たちらしいやり方も整っていきました。 2021年から2022年にかけては事業部を含めたチームメンバーたちとのn人n+1脚で、将来のサービス開発に向けた土壌を整えるための一連の大きな改修を行い、それらがひと段落つきそうな気配が見えた時、このサービスはこれから先も続いていくけれど、自分の仕事はここまでだな、という感覚が自分の中に芽生えました。プログラマとして初めての転職のタイミングがやってきたのです。 3つの転職条件とエス・エム・エス さて転職を考えるにあたり、こんな会社で働きたい、というふんわりとした条件を3つ考えました。それは、「やっぱり世の中が良くなるような仕事がしたい」「今よりももっと技術力を高めたい」「Rubyが好きな人々と一緒に働きたい」というものです。 そして、いろいろな会社の方からお話を伺った中で、これらの条件を全て満たしていたのがエス・エム・エスでした。 ①「世の中が良くなるような仕事がしたい」 転職先が自分自身の問題として力を尽くしたい社会課題に取り組んでいる会社であってほしい、というのは自分がプログラマになることを決めた動機づけであり、どうしても外すことのできない条件でした。 これに対し、エス・エム・エスは高齢社会が直面する社会課題の解決を目指す会社です。 ご存じのとおり少子高齢化・人口減少は、この社会における喫緊の課題であり、これから時間の経過と共に人口動態がどのように変化していくか、それによって世の中がどのように変化していくか、はある程度予測可能である一方、それをどのようにすれば解決できるのかについては今なお手探り状態です。エス・エム・エスでは40以上ものサービスを展開しており、介護・医療・ヘルスケア・シニアライフという4つの領域でそれぞれに情報インフラを構築することでこれらの課題に向き合おうとしています。 また医療・介護は社会にとっての大事な関心ごとであると同時に、自分の家族や自分自身の人生に直接関わる大事な関心ごとでもあります。老いと無縁な人はいません。 カジュアル面談の際に「介護にまつわるサービスは介護を受けるその人のためのサービスでもあるけれど、その家族のためのサービスでもある」と技術責任者である田辺さんが仰っていたことが印象に残っています。 ②「今よりももっと技術力を高めたい」 前職ではプロダクト開発が売上に直結するビジネスモデルではなかったため、腰を据えて機能を考える、開発に取り組むという経験を積むことができました。 一方で自分自身の経験不足による引き出しの少なさを痛感しており、転職先では前職とはまた異なる開発経験を積むためにより規模が大きく・成長がはやく・そして長く使われるプロダクトの開発に携わりたいと思っていました。 これに対し、前述の通りエス・エム・エスは4つの事業領域で40以上ものサービスを提供しており、それらの中には歴史があり規模の大きいものから比較的新しいものまで様々なものが含まれています。それらの共通点は、いずれも10年20年と成長し続けるサービスを目指して提供されているという点です(高齢社会の課題に向き合うということはそれほどに長い時間を見据えてのサービス提供が必要であるためです)。 こうした特徴を踏まえて、エス・エム・エスで開発者として働くということは、長い時間をかけて成長し続けるサービスに関わることができるチャンスであると感じました。 同時に、キャリアを積んでいく中で場合によっては特徴の異なる複数のサービスに関わるなど選択肢の幅が広がる可能性もあるのではないかと考えました。 ③「Rubyが好きな人々と一緒に働きたい」 冒頭でも自己紹介しましたが、私は職業プログラマであると同時にRubyistでもあります。そのため、同じRubyが好きな人々が働いているような、そして業務外での自分のRubyistとしての個人的な活動を見守ってもらえるような環境で働きたいと思いました。 一方、エス・エム・エスでは多くのサービスを多くのチームで開発しており、その全てのプロダクトがRubyで開発されているわけではありません。 しかし技術責任者の田辺さんはもちろん、入社後私の所属先となった介護キャリア開発グループにもRubyコミュニティでおなじみの人々が所属しており、そして会社としてはRubyKaigiやKaigi on Railsなどのカンファレンスにもスポンサーとして協賛しており、Rubyコミュニティと地続きの環境であることに間違いありません。 (余談: 入社直前に参加したRubyKaigi 2022では現地会場のホワイエにて、入社後の私の上長である諸橋さん、私、そして私にとってのRubyコミュニティの憧れの人々という面々でお喋りするというとてもRubyKaigiっぽい一幕があり、地続き感溢れていました) こうしたことを踏まえて、改めて「こんな会社で働きたい」を考えてみた時、自分にとってエス・エム・エス以上の選択肢はない、と判断しました。そして実際の転職活動ではエス・エム・エス一社のみに選考を申し込み、ありがたいことに入社が決まったという次第です。 エス・エム・エス入社後の日々 ということで入社して数ヶ月が経ちました。 予想はしていたものの、前職とは会社の規模・事業ドメイン・技術スタック(一部)・開発の進め方…と何もかもが違う環境に身を置いているためまだまだ学ぶことの多い目の回るような毎日を過ごしています。 と同時に、ここまでの数ヶ月は自分にとってエス・エム・エスは最良の選択だった、という点について確信を持つことのできた数ヶ月でもありました。 私の所属先である介護キャリア開発グループで開発している「カイゴジョブ」は、介護従事者の方々が新しいキャリアを歩むお手伝いをするための求人広告サービスです。 エス・エム・エス社内にはカイゴジョブに隣接する他のサービスもあるのですが、私の入社当時はそれらのサービス間を横断するような形でより手厚くユーザーの方の支援ができるようにするための機能開発の真っ最中でした。どんな機能を開発すると、それがどんな形で介護従事者の方々のもとに届き、それによってどんな風に世の中が少し良くなるか、を入社直後から早速体感することになりました。 技術面では予想の通り、前職との主に事業ドメインの特性や事業規模やインフラ構成の違いなどから自分にとって初めて出会うような課題が数多くあり、その分学びの機会に恵まれています。 それに加えて頼りになるチームメンバーの先輩たちに囲まれながら、困り事を気軽に相談できるSlackチャンネルや過去の開発の知見が詰まった膨大なドキュメントを活用しつつ日々の開発を行なっています。 さらには開発メンバーの間でごく自然にペアプロ・モブプロが行う文化があり、私の好きな『達人プログラマー』の一節にある「一人ぼっちでコーディングに取り組んではいけない」というTipsが体現されている環境で働いています。 (最近は開発タスクを管理しているTrelloにてペアプロ相手を自動アサインする「モブりたい」ラベルが追加されるなどどんどん運用が進化しています) そして何より、開発に責任感を持って真摯に向き合い、ごく自然にお互いに助け合うチームメンバーの姿勢からはいつも刺激をもらっています。 チームメンバーである児玉さんによる先日の記事『アジャイルでビッグバンリリースの不確実性を低減する試み』の結びに 今回、私達は開発とテストの工程を並行するプロセスを採用し、デザインリニューアルプロジェクトを進めてきましたが、はじめからこの開発手法を確立できていたわけではありません。日々のプロセスの中で上手くいったことや課題に感じたことをチームでふりかえりながらプロセスを最適化してきました。 とあることからも、より良いやり方を皆で模索し続けるチームの真摯さが伺えます。 カジュアル面談の際に田辺さんが「エス・エム・エスのメンバーには誠実な人が多い」と仰っていたのを日々実感しており、自分もその一員として事業、プロダクト、チームに対して誠実なメンバーでありたいと思っています。 tech.bm-sms.co.jp おわりに 以前、チームメンバーである真下さんの記事『エンジニアはプロダクトの事業領域に関心を持ってなければいけないのか?』が投稿されました。 tech.bm-sms.co.jp エス・エム・エスで開発に携わる各部署メンバーへ入社時点でエス・エム・エスの事業領域(医療・介護・シニアライフ・ヘルスケア)についてどの程度興味を持っていたかについて調査した結果についてまとめたもので、なかなか興味深い結果となっています。 私自身も医療・介護・シニアライフ・ヘルスケアという事業領域に明確な興味を持っていたわけではありませんが、ふんわりとした「世の中が良くなる仕事がしたい」「もっと技術力を高めたい」「Rubyが好きな人々と一緒に働きたい」という気持ちで入社したエス・エム・エスで、現在は事業への手応えを感じつつ、技術的なコンフォートゾーンから出つつ、信頼できる人々と一緒に働くことができています。 この記事では執筆者である私にとってのエス・エム・エスが転職先としてどんな会社だったのか、について書き連ねてきました。この記事でエス・エム・エスに少しでも興味を持ってくださる方がいれば幸いです。
はじめまして、エス・エム・エスでSREとして介護事業者向け経営支援サービス「カイポケ」の開発・運用に携わっている小笠原です。 2022年10月に入社してしばらく働くなかで組織やプロダクト開発の状況がある程度見えてきたので、入社エントリを兼ねて自身の所属するチームや仕事について紹介します。 内容は現在のスナップショットにはなりますが、中で働いている人や組織の雰囲気、向き合っている課題について今後入社を検討される方の参考になると嬉しいです。 入社までにやってきたこと 先に自身の経歴について紹介しますと、情報系の大学院を出て2017年からIT業界で働き始めて今年で6年目です。ポジション的にはいわゆる中堅どころのエンジニアです。新卒でSIerに入社してSEとして顧客の比較的規模の大きなWeb基盤の開発・運用を1年ほど行い、そこからWebベンチャーに転職して4年半ほど中規模なWebシステムの開発・運用に幅広く携わりました。 前職ではReact/Railsを用いたアプリの機能開発を経験し、その後は基盤開発に軸足を置いて基盤の構築・運用やアプリと基盤の間に落ちるような諸々の課題解決に取り組んでいました。 個人的に興味のあった課題(フロントエンド/バックエンドの機能開発、アプリのパフォーマンス改善、基盤の刷新、CI/CD構築、セキュリティ強化、モニタリングの導入・運用、コンテナオーケストレーションツールを用いたアプリ開発など)を一通り経験でき満足したことと、コンフォートゾーンに入っていたこともあり一度環境を変えようと考えたのが転職のきっかけです。 Web開発に関して一通りの経験は積めたものの技術的にはまだまだ未熟と感じていたため、これまでの経験を活かしつつさらに経験を深められる会社を探していました。 エス・エム・エスを知ったきっかけ そんな中、LinkedInでEMの古萱からメッセージをもらったのがエス・エム・エスを知るきっかけでした。それまではエス・エム・エスについては会社の名前も知らなかったのですが、メッセージに添えられていた 技術責任者田辺のブログ を読んで興味を持ちました。この記事では「継続性アーキテクト」という概念で技術を深める方向のキャリアを説明しており、技術志向のエンジニアに寄り添った考え方に感銘を受けました。そのような考え方を持った人が運営する組織であるなら一度話を聞いてみたいと考えました。 そして、実際に転職活動を行う中で採用要件や向き合っている課題について話を聞く中で私がこれまで行ってきた経験が多く活かせそうだと気づき応募するに至りました。 入社の決め手 複数社を受けて総合的に判断した結果入社を決めたので「これで決めた!」というものはないのですが、エス・エム・エスが他と比べて良いと思ったのは以下の点です。 ビジネスモデルが強い 医療介護業界は今後も需要が伸びるためそこをターゲットとしているエス・エム・エスは継続的に事業が発展する可能性が高く、エンジニアとして長期でコミットできそうと考えました。 事業に興味を持てた 私は自分が携わったシステムやサービスが最終的にどんな価値を社会に提供できているかが仕事のモチベーションになっています。エス・エム・エスが取り組んでいる医療・介護領域の事業はその点提供価値がわかりやすいため取り組みがいがあると考えました。 課題が多くて挑戦の余地がある サービスおよび機能の数が多く歴史もあるため、課題が多く取り組む仕事の範囲も広くて飽きないと考えました。よい設計で整理が進んだプロダクトの場合、かえって面白い仕事がない場合もあるので課題が多いことは自分は肯定的に受け取りました。 個人の裁量や動きやすさがありそうに見えた 大企業ではあるものの開発組織は人数的にも体制的にもまだまだ発展段階にあり、開発者個人の裁量が大きく動きやすさがあると考えました。また、組織の技術責任者やリードする立場の方がいわゆるWeb系の企業出身者が多く、エンジニア個人の動きやすさに配慮している印象を受けていました。 実際に入社してからは「カイポケ」というプロダクト(後述)およびそのリニューアルプロジェクトにSREメンバーとして関わって働いています。2つのチームで色はそれぞれ異なるのですが、自分が入社前に抱いていた組織やチームへの印象は入社後も大きく変わることはなかったように感じています。 関わっているサービス「カイポケ」について 「カイポケ」は介護事業者向けのSaaSです。 介護事業者が国の保険制度を利用する際の請求業務を行ったり、事業所職員がタブレットから記録業務を行ったり、それ以外にも介護事業に関わる様々な業務をこのサービス上で行うことができます。 カイポケの現行システムは開発開始から10年以上が経過しており、当初は主に外部のリソースを利用して開発していたところ、その後 内製化に舵を切って取り組んできた歴史があります 。現在4万を超える事業所で導入されており、40個以上の機能を備えています。 機能・ドメインごとにサーバが分割されているためサーバ台数は多いものの、主となるシステムの基盤構成はシンプルでALB、EC2、RDSをベースとした一般的なWebアプリを想像してもらえると良いかと思います。アプリケーションサーバのレベルでは分割されているものの、DBなどのリソースのアプリケーション間で共有されているため、システム全体としてはモノリシックな側面も残っています。 ところで、先日以下の記事でもご紹介したように、現在、この現行のシステムをリニューアルするプロジェクトが進んでいます。 tech.bm-sms.co.jp リニューアルをすることにした理由はいくつかあるのですが、先述のようにモノリシックな部分があるがゆえに変更の影響範囲が広くなりやすいこと、また、共有されているリソースの存在が組織全体の開発スピード向上のボトルネックになりうること、などがSREチームに関わる部分として挙げられます。 そのような背景のなかで私は現行「カイポケ」(以下、現カイポケ)のSREとリニューアルプロジェクトのSRE、2つのチームにメンバーとして関わっています。それぞれ向き合っている課題と求められる仕事に違いがあるため、雰囲気がわかるよう簡単に紹介していければと思います。 現カイポケのSREについて まず現カイポケのSREの責務ですが、開発・運用含め基盤関連の仕事全般と言うような形でふわっと与えられています。開発チームからの依頼対応や各種基盤リソースの障害対応、各種コンポーネントのバージョン更新、脆弱性対応などインフラエンジニアという括りで担当が分かれるような運用寄りの仕事もあれば、リリース改善やトイルの削減、プラットフォームのモダン化と言った一般的にSREが担当するような仕事まで様々です。 その中で何を優先してどこまで手を出すかはチームの体制・余力に応じてチーム内で調整するようになっており、チームの裁量は大きいです。チームが少数体制だったときには現状維持を主な目的とした「守り」の施策に集中していたときもありましたが、現在では人員が増えたこともありチームの業務範囲を拡大して現状改善を主な目的とした「攻め」の施策の割合を増やしているところです。 例えば直近で進んでいる施策には「バッチ実行基盤のマネージドサービスへの移行」、「デプロイ自動化」、「オブザーバビリティ改善」などがあり運用負荷削減や開発効率化、システム安定性向上といった現状からの改善を図っています。 現カイポケはシステムが少し古い構成で動いていることもあり、仕事の傾向としては現代的な構成や運用に持っていくことで価値が出ることが多いです。アプリは介護ドメインの複雑さを反映してビジネスロジックは複雑である反面、非機能要件はシビアではないため基盤側は世の中的によく使われているWebアプリケーション運用のベスト・プラクティスを正しく導入できれば着実に改善を進めることができるように見えています。 ただ、プロダクトの主たる部分がモノリシックな作りということもあり変更の影響範囲が広くなる傾向があります。新しい技術スタックの導入を試みると、検証中に思わぬ箇所で導入を妨げるような課題にぶつかります。課題にはいろいろな種類があるのですが、経緯を紐解いて地道に解決の道筋を作る必要が出ることが多く、歴史の長いプロダクト特有の難しさを感じます。 個人的にはそのような課題がある中でうまく差し込めるような解決策を考えるのがパズルゲームのようで面白いと考えています。技術責任者やEM、アーキテクトの方々も「過去のしがらみは気にせずどんどん改善を進めてくれたら良いよ」と肩を押してくれるので、今後もSRE主導での改善活動に積極的に取り組んでいきたいと考えています。 リニューアルプロジェクトのSREについて 一方でリニューアルプロジェクトのSREの責務はプロダクトが初期構築のフェーズということで柔軟性を持って動くことを期待されており、明確な定義はありません。ただしチームの大きな目標としては「システムが安定して動いている状態」と「システムを早く世の中にリリースできる状態」を維持することを目指して動いています。 SREの立ち位置がわかりやすくなるので先に開発体制について説明しておくと、リニューアルプロジェクトではアジリティの高い開発体制を目指しており、これを実現するため開発チームには基盤リソースのアクセス権限が与えられています。開発チームがオーナーシップを持つサブシステムごとの基盤リソースについては開発チームが管理するため、SREチームはシステムの全体設計や共通コンポーネントの管理を始めとするシステム横断の技術課題の解決に集中できるようになっています。 例えば直近の仕事で言うと以下の記事で紹介しているように「OpenTelemetryの検証」や「本運用を見据えた監視体制の整備」などに取り組んでいます。 tech.bm-sms.co.jp リニューアルプロジェクトではシステムを0ベースに近い形で設計しているため、制約が少ない中で技術選定や開発が行える環境です。もちろん技術の導入には妥当な選定理由は必要ですが、検証で該当技術の成熟度を測って導入判断を行ったり新しいバージョンのライブラリの使い勝手を試したりを気軽にできるのは新規構築ならではの楽しい部分だと考えています。 現在リニューアルプロジェクトでは初期バージョンのリリースに向けて動いており、運用基盤もそれに合わせて整備を進めていく段階です。SREチームでも開発状況に合わせて様々な施策を担っていくことになるので、自分でもどんどん手を動かしてプロジェクトの成功に貢献していきたいと考えています。 まとめ 個人的な転職の経緯から、現在関わっているチームや仕事について簡単に紹介させていただきました。これを読んで「カイポケ」のSREチームの雰囲気や普段どのような課題と向き合って仕事をしているかについて理解が深まると嬉しいです。 どちらのSREチームも開発状況が日々少しずつ更新されているため、本文では敢えて具体的に利用している技術の話やシステム構成の話、組織内の詳細な体制の話などは省略しました。より具体的な話や今後の見通しなどが気になった方はぜひ一度カジュアル面談にいらしてください。
2022年10月、介護事業者向け経営支援サービス「カイポケ」のエンジニアリングマネージャー (以下、EM)としてエス・エム・エスに入社した酒井( @_atsushisakai )です。先日、転職活動については 個人のブログ の方に書いてみましたが、入社エントリとしてエス・エム・エスの入社までの経緯と、EMとして実際にどのような仕事をしているかも簡単にご紹介します。これから「EMとして」転職を控えている方で、不安を持たれている方にとっての指針になったりすると嬉しいです。 私のバックグラウンド 前職では、株式会社 MIXI で「家族アルバム みてね」というプロダクトを、創業メンバーとして立ち上げからグロースまで8年以上に渡り開発を続けていました。ソフトウェアエンジニアとしては、iOS/Android のネイティブアプリ開発、Ruby on Rails でのバックエンド開発や AWS でインフラを構築・運用するなど、少人数チームならではの領域を越えまくるスリリングな開発を行なっていました。 また、後半数年間は EM として、ピープルマネジメントとプロダクト開発チームのスケールのための組織作り・プロセス作り・採用活動などに取り組んでいました。 エス・エム・エスとの出会い そんな中、エス・エム・エスに出会ったのは転職のおよそ1年前、YOUTRUST でのスカウトメッセージでした。メッセージを受け取った後、コーポレートサイトを見てミッションや事業内容が非常に特徴的に感じ、その時は転職を全く考えていなかったのですが「面白そうだし一回話を聞かせてもらおう。自分の組織のために学びも得たいし!」くらいの軽い気持ちでカジュアル面談を申し込み、技術責任者の @sunaot と話をしました。 カジュアル面談では、エス・エム・エスの ミッション を中心に、展開しているサービスの課題、開発組織の課題などを広く聞かせてもらいつつ、自身の組織マネジメントの悩みなどを話して共感してもらったりなど、とても楽しく会話したのを思い出します。 なぜエス・エム・エスに入社を決めたのか その後、何度か @sunaot と話をさせてもらう中で私は転職する決意をし、最終的にエス・エム・エスを選びました。転職先としてエス・エム・エスを選んだ理由はいくつかあります。 エンジニアリング組織をより大きくスケールさせていくフェーズであること 私はこれまで、比較的小〜中規模 (50名くらい) の規模でプロダクト開発を中心とする組織のEMとして活動していました。自身の中には、EMとしてより成長していくためには、さらに大きな規模の組織で発生する課題に向き合い、そこから得られる経験が必要であると感じており、その規模感とエス・エム・エスのプロダクト開発組織がこれからスケールしていくであろう規模感のイメージが合致していたことが、この会社を選んだ一つの理由です。 戦略を特に重要視していること カジュアル面談で何度も話を聞いていく中で感じたのは、エス・エム・エスが一貫して「戦略」をとても重要視していることでした。社会課題に対してどのような視点からペインを解決しなければいけないかという Why の部分と、それをいつまでに解決すべきかという When の部分が徹底的に言語化されており、それに沿った形で非常に多くの事業やプロダクトに紐づいています。自分自身、事業における戦略が明確かつ相応な分量で言語化されていることは、迷った時に立ち返ることができる安心材料になるのでマネジメントの観点からも非常に業務が進めやすそうな魅力的な会社だと感じました。 子ども世代への影響度が大きいこと 最後は当然、解決しようとしている課題とそのための事業についてです。エス・エム・エスが展開している非常に多くの事業は、2040年問題と呼ばれる高齢者人口がピークに達する時代を見据えた課題解決の方向性を示そうとしています。2040年といえば、現在3歳の自分の子どもがちょうど社会に出る時代です。それを考えた時に、ひとつのプロダクトをじっくり作っていくことも重要ですが、多くの観点から社会の根本的な課題を解決するために自分の能力を使い、少しでも生きやすい社会に変化させていくことが、今の自分にとっては納得度も高く、何より我が子の未来のためになるのではないかと考え、最終的にこの会社に入社することを決めました。 入社してどうだったか? 入社後、最初の一週間は非常に戸惑いました。それは、「EMとして会社にジョインする」ということが初めてだったことが一番大きな理由だと今振り返って思います。膨大な情報量と日々目まぐるしく動くプロジェクトの状況など、プロダクトの立ち上げからほとんど全てを把握していた前職の環境からは一変し、自分自身がどの情報を選択し、深くインプットした上でチームやメンバーに影響を与えていくことができるかを判断するのがとても苦労しました(これは正直、今でも苦労しています)。 ただ、チームメンバーは非常にオープンマインドの方が多いので、積極的に1on1を申し込んだりなどを重ねて、コミュニケーションを取る機会を増やしていったことで思ったよりも早く溶け込むことができました。そうすることで、EMとしてコミットできる小さな課題を徐々に見つけることができるようになっていったのを思い出します。エス・エム・エスの開発カルチャーとして、モブプログラミングや意識的なオンライン雑談の時間などが充実しており、新しい人を受け入れやすいマインドが備わっているのも影響が大きいと感じています。 入社後にやった仕事 現在は介護事業者向け経営支援サービス「カイポケ」の開発プロジェクトにおいて、フロントエンドチームの EM としてアサインしてもらっています。また、エンジニアやデザイナーのプロダクト開発に必要な人材の採用活動も幅広く扱わせてもらったり、プロジェクト全体のエンジニアリング部門におけるマネージャーとして、課題の集約や情報共有の結節点となるなど、現場に近いところでプロジェクト推進のためにできることを徐々に増やしていっています。 この中でもいくつか、入社後にフロントエンドチームの EM として取り組んだ仕事をご紹介します。私が EM を務めるフロントエンドチームは、私が入社する1ヶ月前に新たなチームとして発足しており、様々な整備やチームビルディングを行っていかなければいけない状況でした。以下は、そういう状況で私が EM として取り組んだ課題解決のいくつかの事例となります。 チームの価値観や暗黙知を言語化する 私がジョインしたタイミングでは、徐々に技術選定は進んでおり、チームがコードを書き始められるような土台は出来上がってきていました。ただ、立ち上がったばかりのチームなので、複数人でコードを書いていく上でそれぞれのメンバーが持つ開発の価値観を揃えたり、暗黙知となっている事柄を言語化することなどがまだそれほど出来ていない状態であったことに気づきました。これらを言語化することが長期的なパフォーマンス向上を実現する土台となるという経験はこれまでもあったので、まずはここに取り組ませてもらいました。そのひとつの成果として「コードレビューガイドライン」を GitHub リポジトリ上に立ち上げたり、同様に「コーディングガイドライン」も叩きとして作り、今後コードを書いていく中で何か取り決めがあった方が良いだろうというものをガイドラインとして言語化し、明確に共有していく流れを作りました。 コードレビューガイドライン スクラムの導入 コードベースが徐々に整理されていくのと同時に、ユーザー価値の実現プロセスとしては、スクラムを取り入れていくことが初期に決まっていました。スクラムのフレームワークを使い、どのようにプロダクトバックログアイテムを管理・見積もりを行うか、チーム内でスクラム関連のイベントをどのタイミングで、どのぐらいのタイムスケールで実施するかなどを PdM とじっくり話しながらプロセスを構築していきました。プロセス全体においては自身がスクラムマスターとして、チームがスクラムにおけるメリットをしっかりと享受できるよう、まずは教科書的に必要な要素をしっかり実践していくことを徹底しています。このあたりは、前職で長年スクラムを実践してきた経験がとても活かされていると感じています。 フロントエンドチームのMission/Vison策定 開発が安定的に進み始め、新たにメンバーがジョインした頃に、より細かくお互いの価値観を揃え、チームとして目指したい姿を共有するために、今後数年を見据えた Mission/Vision をメンバーみんなで作ることにしました。プロジェクト全体のミッションを達成するためにチームとしてどのような貢献をするべきか (Mission)、また「組織・技術・採用・プロダクト」の様々な観点で、チームがどのような状態になっていたいか (Vision) を複数回ディスカッションを重ね、言語化し、将来像をメンバー間で共有しました。 フロントエンドチームのMission/Vision フロントエンドチームの分割 チームが立ち上がり数ヶ月経つと、様々な方面からチームにメンバーがアサインされるようになり、人員のボリューム感も出てきました。今後開発対象となるアプリケーションも複数あるため、チームを分割していく流れを作っていくことが自然な状況になってきました。チームをどういう単位で分割するか、また分割していく上で事前に考える必要がある様々な事柄を整理し、ガイドライン化しながら、チーム内外で認識合わせをしたりなどをしてきました。主に実施したことは以下のようなものです。 誰をどのチームにアサインするかの決定 メンバー間での役割分担 (マネジメント、プロジェクトリードなど) 複数チームの情報流通のためのドキュメントや会議体の整備 複数チームで行うコードレビューのためのガイドラインのアップデート 事前のコードベースのリファクタリング 共通コンポーネントの整理 現在は、ふたつのフロントエンドチームがお互いに連携しながら、アプリケーションを開発しています。 おわりに 以上が、入社の経緯や入社して以降の実際の仕事の内容でした。まだ入社して数ヶ月ではありますが、EMとして大きな裁量を持ち、自分自身が一番レバレッジを効かせられる場所を見つけ、自由に動いて良いということを技術責任者の @sunaot から言ってもらえています。そういう中で、徐々に転職した理由であるEMとしての「マネジメントの規模感」を大きくしていけるような視点も増えてきており、これから EM としてエス・エム・エスでプロダクト開発と事業に全力投球できることを非常にワクワクしながら過ごしています。 EMとしてこれから転職を考えている方は「どのように既存の開発組織に入っていくのが良いだろうか?」ということを悩まれると思いますが、今回の私のポストの内容が少しでも役に立つと嬉しいです。 最後になりますが、エス・エム・エスでは社会の課題解決を一緒にやっていただける様々なプロダクト開発の人材を募集しておりますので、気になった方は、ぜひ一緒によりプロダクト開発に取り組んでいきましょう!
こんにちは、SREをやっている @okazu_dm です。 経歴としては、サーバサイドエンジニアからセキュリティエンジニアを経て、エス・エム・エスではサービス横断で技術的な課題を解決しています。 基本的には組織に必要なことと自分ができることや、やりたいことが交わるポイントで仕事をしており、現在はSREとして働いています。 今回は、 過去の記事 とは違い、既存SREチームとは別に、介護事業者向け経営支援サービス「カイポケ」のリニューアルプロジェクトにスコープを絞って新しく立ち上げたSREチームとしての活動を紹介します。 リリース前のプロダクトのSREなので、一般的なSREの定義とは乖離する内容もある点ご留意ください。 リニューアルプロジェクトの概要 昨年末に出したフロントエンドの技術選定に関する記事 での説明と内容的な差分はありませんが、SREの業務の説明の前にこちらでも改めて触れておきます。 インターネットサービスとしては比較的長く稼働しているカイポケですが、ソフトウェアとして見ると、10年以上の時間の中で発生した様々な需要に応じる形で機能を継ぎ足されてきた大きなモノリシックアプリケーションです。 現在進行しているリニューアルプロジェクトは、カイポケのサービスの安定性やプロダクトの優位性を高めるべく、アーキテクチャレベルで見直しをかけ、部分ごとに移行する形でリリースしていく開発プロジェクトです。詳しくは以下のサイトをご覧ください。 careers.bm-sms.co.jp リニューアルプロジェクトのSREチームの担当範囲 開発途中という性質上、プロダクト全体の仕様やシステム構造のボラティリティも高く、SREの役割もプロダクトの状況に応じて変化しています。 その上でチームの大きな目標として「システムが安定して動いている状態」と「システムを早く世の中にリリースできる状態」を維持することを目指しています。 実務的には現在は以下のような役割を担っています。 インフラの設計&構築 内部的にサービスを分割する設計となるため、インフラについても各開発チームと分割統治する形をとっています。SREチームは全てのサービスが乗る基盤となるインフラを担当しています。 開発プロセスの大まかな整備 同じAWSアカウント上で複数チームが同時並列で開発を進めるために、アカウント全体の利用ルールやコスト管理などはSREチームの担当としています。 モニタリング基盤の整備 モニタリングに利用する外部SaaSの選定やシステム全体のアーキテクチャにどのようにモニタリングを組み込むかについてはSREチーム主導で検討し、構築を進めています 今後の開発の中でプロダクトの全体像が明確になっていくにつれて役割が増えることが予想されますが、現状は以上が主要な業務です。 直近の技術的トピック SREチームでは前述のように役割が流動的なため、日々の業務に伴い広くツールや外部サービスの技術検証や導入を行っているのですが、その中でもこの場に書きやすいものをピックアップしてご紹介します。 認証基盤の選定 リニューアルするに伴い認証部分についても検討が必要になりました。これについてはSREチームを含む一部のメンバーで最初にいくつかの候補を出し、最終的にAuth0を採用することに決めました。 大まかな方針として、「ユーザの認証情報を社内に持たない」点と「各開発チームが簡単に扱える(独自仕様が少なく直感的である)」点を重視して選定しました。 候補としたものと、調査した結果の短い所感をそれぞれ以下に書いていきます。 Google Identity Platform: 特に不足はなさそうだが、Auth0のほうが便利そうだった。 Amazon Cognito: 他のソリューションと比べて安上がりだが、仕様がやや独特で開発者全員がスムーズに理解できるかは不安があった。 Azure AD B2C: 安くて安定していそうだが、Azure自体を触ったことがなく使いたい機能が限定的であるのに対して未知の部分が多いため今回は見送り。 汎用的なモニタリングのパイプラインの実現 こちらでは、具体的にはOpenTelemetry関連ツール導入の実現可能性について検証し、実際に導入して分散システムのモニタリングの構築を進めている現況の紹介をします。 OpenTelemetry まず、OpenTelemetry(以下で一部otelと略していることもあります)について簡単に紹介します。 プロジェクトの公式サイト( https://opentelemetry.io/ )から引用したものを日本語訳して貼りますが、以下のように表現されています。 OpenTelemetryは、ツール、API、および SDK の集合です。テレメトリデータ(メトリクス、ログ、およびトレース) を計測、生成、収集、およびエクスポートし、ソフトウェアのパフォーマンスと動作の分析に役立てます。 これだけだと理解が難しいかと思うので、公式の図を踏まえつつもう少し具体的な紹介をします。 図は https://opentelemetry.io/docs/ より引用 図の中心にあるOTel Collector(以下単にCollectorと称する)が重要な役割を果たすツールです。これは、アプリケーションやインフラ(図の左側のコンポーネント群)から能動的または受動的にテレメトリデータを吸い上げ、Collectorがサポートするいくつかの形でエクスポートします。エクスポート先の例としては以下のようなものが存在します。( https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter より) Jaeger Zipkin Prometheus Datadog AWS X-Ray また、アプリケーション側が自動でリクエストや関数呼び出しなどの単位(上記の公式の文中にあるテレメトリデータの中で言うところのトレース)で処理の実行記録を残すために、アプリケーションに組み込む計測用ライブラリも開発されています。 これは使い方の一例ですが、例えばDatadogの従来の導入方法は、公式のライブラリをアプリケーションに組み込み、Datadog Agentを立ててDatadog Agent経由でDatadogにログやトレースを送る、という形でした。これのもう一つの選択肢として、ライブラリをDatadogのものからOpenTelemetry用のものに変更し、Dataodg Agentの代わりにCollectorを導入することで同様のことが実現できるようになりました。 このように、OpenTelemetryは従来のベンダが個々に作っていたモニタリングのパイプラインの一部を統一的な形で実現しています。 導入の背景 OpenTelemetryを導入すると何ができるのか、という点については前述したように、少なくとも現状の我々の利用範囲だと何か新しいことができるようなものではありません。 その上でOpenTelemetry導入に至った経緯について説明します。 まず、今回のプロダクトでは初期のリリースの規模などから見てもモニタリング基盤は外部のソリューションに任せた方が良いだろうという方針でDatadogを採用することにしました。 そして既に社内でOpenTelemetryを採用しているチームがあった点と、プロダクトが拡大した場合はDatadog以外のソリューションを検討することもあるだろうという将来的な可能性を見据えて、サービス間の乗り換えが比較的容易そうなOpenTelemetryを採用しました。 Datadog Agent vs OTel Collector Datadog AgentはOTLP(OpenTelemetryProtocol)のメッセージを受けることが可能なので、OpenTelemetryを採用する場合でも以下の図のようにCollectorを使うか、Datadog Agentを使うかの選択肢が発生します。 図は https://docs.datadoghq.com/opentelemetry/ より引用 それぞれ単純なアプリケーション1つの環境でsidecarとして動作させて、デプロイ時の落とし穴がないか、という点やコンピューティングリソースの消費量などをざっくり確認してみました。検証の結果、どちらも単体で使う分には大きな差はないように見受けられたため、中長期的にCollectorの柔軟性が活きることを期待して、Collectorを標準のツールとして採用しました。 また、計測用のライブラリについては未検証ですが、OpenTelemetryのものを使ったコンポーネントとDatadog製のものを使ったコンポーネントを同時に使った場合、分散トレーシングに問題が出るのではないかと考えています。 具体的には以下のように、DatadogとOpenTelemetryの間でトレースのIDのデータフォーマットが違う点が問題となることが予想されます。 https://docs.datadoghq.com/ja/tracing/other_telemetry/connect_logs_and_traces/opentelemetry/ なお、仕組みの話を補足するとトレースのID自体は計測用ライブラリ側で生成しており、OpenTelemetryのライブラリを利用した場合Datadog AgentやCollectorのDatadog ExporterがDatadogに送る前にフォーマットを変換しているようです。 例: Datadog Exporterでフォーマット変換していると思われる箇所 github.com 現状の懸念 ここまで、利便性や今後の可能性について様々な点から紹介してきましたが、一方で現状で本番投入するにあたっては以下のような懸念もあります。 Stableでない部分も多く、各種データの吸い上げからエクスポートの部分までをすべて公式が実装していた状態よりは今後の不安がある(どちらか片方の破壊的変更が今後起こった場合など) 現在、sidecarとしてAWS FireLens(fluentbit) *1 とCollectorを立てているが、関係する要素が増えると事故が起こる可能性もそれだけ増えるため可能であればもっと単純な形に置き換えたい(単純計算だとシステム内部に直列で繋がる要素が増えると事故率は上がる) まとめ この記事では、現在進行中のカイポケリニューアルプロジェクトのSREチームが担っている役割についての紹介をしました。 また、SREチーム内の直近の業務紹介の中で、OpenTelemetryの紹介とDatadog連携についての調査内容の一部分を要約して説明しました。 興味を持っていただけた方は、直近での転職意思の有無にかかわらず、ぜひカジュアル面談にお越しいただければと思います。 *1 : ECSタスクのログをS3に転送するために使用しています https://aws.amazon.com/jp/blogs/news/under-the-hood-firelens-for-amazon-ecs-tasks/
介護事業者向け経営支援サービス「カイポケ」のエンジニアリングマネージャー、酒井( @_atsushisakai )です。現在、私たちは「カイポケ」のフルリニューアルプロジェクトに着手しています。今日は、このプロジェクトをご紹介します。また、プロジェクトで扱っている課題や私たちが日々取り組んでいる技術的なチャレンジについて、これからさまざまな観点の記事を deep dive してお届けしていきます。そのあたりについても簡単にご紹介します。 「カイポケ」フルリニューアルプロジェクトとは? 私たちは、介護事業所の運営に不可欠な「保険請求」の機能をはじめ、事業所経営を総合的に支援するための多くの機能を提供する業界特化型 SaaS である「カイポケ」を開発・運用しています。 「カイポケ」はサービスローンチから17年が経過し、システムの安定性・開発効率の観点で多くの課題を抱えているのが現状です。同時に、高齢化社会が進む背景もあり介護業界からのニーズは年々高まっています。そこで、今後の更なる継続的な事業成長を見越して、アーキテクチャから全てを見直すような開発プロジェクトを2021年から始動してきました。 今回、このプロジェクトについて、扱っている技術的な課題や将来の展望など、詳しい取り組み内容についてまとめたサイトを新しく立ち上げました。情報量はとても多いのですが、非常にチャレンジングで難易度が高いが故のプロジェクトの面白さが伝わる内容になっていると思います。是非一度ご覧になっていただき、興味を持っていただけると嬉しいです。 careers.bm-sms.co.jp プロジェクトの裏側を順次公開中 今後、多くの難易度の高い技術的な課題をどう解決していこうとしているかであったり、多くの人が関わる複雑で規模の大きなプロジェクトをどのように進めているかというプロセスの工夫など、さまざまな観点でフルリニューアルプロジェクトの裏側をご紹介していく予定です。 既にフロントエンドの技術選定については以下の記事でご紹介しています。 tech.bm-sms.co.jp 来週には @okazu_dm さんによる SREチームの取り組みについての記事が公開されます。さらに、今後、以下のようなテーマでコンテンツを順次公開していく予定です。(内容は変更の可能性もあるのでご了承ください。) 極めて複雑な介護費用請求金額の計算について 大規模なプロジェクトを運営するための開発プロセスについて カイポケの新しいアーキテクチャについて プロジェクトを支えるメンバーを積極採用中 最後になりますが、このフルリニューアルプロジェクトでは、まだまだ多くのエンジニアの方の協力を必要としています。先ほどご紹介したプロジェクト専用サイト内に 募集中のポジション を記載していますので、興味のある方はぜひご覧ください!また、エンジニアだけでなく、 プロダクトマネージャー ・ UI/UXデザイナー などプロジェクトに携わるポジションを全方位的に募集しています。 カジュアル面談もやっておりますので「選考に進む気持ちはまだないけどプロジェクトについては面白そうなので聞いてみたい」という方でも構いません。ご興味があれば是非お気軽にお声がけください!
はじめに エス・エム・エス BPR推進部カスタマーデータGrで、「 ナース人材バンク 」等のキャリア事業を中心に、社内のデータ活用の推進、データ基盤の開発を担当しています、橘と申します。 私達カスタマーデータGrでは、Google CloudのBigQueryを中心としたデータ基盤を構築しており、社員がデータを利用して意思決定をする業務をサポートする役割を担っています。本記事ではそのデータ基盤についてご紹介したいと思います。 カスタマーデータGrのデータ基盤について 私達が運用するデータ基盤のデータ連携アーキテクチャについて、簡単にまとめると以下の図のようになります。 エス・エム・エスのキャリア事業で活用したいデータは、AWS上に構築されたシステム、Salesforce、GoogleドライブやGoogleスプレッドシート、その他SaaS等、様々な場所に散らばっております。これらをGoogle Cloud上のストレージに収集し、利用目的に応じたデータストアへ連携し、業務への活用をする、といった構成となっています。 データ基盤のワークフローエンジン これらのデータを連携する基盤として、GCP上でApache Airflowをマネージドで提供するサービス、Cloud Composerを利用しています。Airflowでは次の図のようなワークフローをPythonで定義して運用することができます。各処理を動かすタスクを整理し、それを有向非巡回グラフで依存関係を定義し、前のデータのロードが終わったら、そのデータを加工するタスクを動かす、といったことが実現できます。 Airflowには、BigQueryやCloud Storage等のGoogle Cloud上の各サービスへ接続する機能が元々あります。その他の外部サービスへ接続する共通処理であったり、プログラムでビジネスロジックを記述する必要がある処理に関しては、Pythonを書いてこのAirflow上で動かすことができます。 3年ほど前にこのAirflowを本番環境で導入し、今では100件以上のワークフローを動かす基盤として運用しています。 BigQueryを中心としたETL基盤 BigQueryは、とにかく大量のデータを蓄積することが可能なので、Google Cloudに連携したデータのほとんどをここにロードしています。BigQueryに蓄積したデータを加工して、集計・分析用のデータマートを作成する、ELT(Extract, Load, Transform)の構成を取っています。データの加工には、SQLやJavaScript(BigQuery SQLのUDFとして利用)を主な手段として採用しています。BigQuery上のSQLであれば当然リソースはBigQueryのリソースを利用できるので、大量データを効率良く処理ができます。導入前はオンプレミス環境に構築したETLツールを用いて、サーバのリソースを割いてデータ加工の処理をしていましたが、BigQueryの導入によって、数億件のデータでも安定して高速で処理ができるようになりました。 BigQuery上のデータは、マーケターやエンジニア等のデータ利用者がSQLを実行して参照したり、Looker Studio、Googleスプレッドシート等を用いたレポートを作成して閲覧できるようにしています。 また、BigQueryのデータは集計データを蓄積するのみにとどまりません。後述するGoogle Cloud上に構築した業務Webアプリケーションが利用するCloud SQLやFirestoreに連携したり、BigQueryのリソースを用いて大量データを集計した結果を、AWSやSalesforce等の社内の別システムへの連携をしたりと、大量データを処理する業務システムとしての役割も担っています。 Looker StudioやGoogleスプレッドシートを用いたデータの活用 弊社ではエンジニア組織のみならず、全社的にグループウェアとしてGoogle Workspaceを利用しています。そのため、GoogleスプレッドシートやLooker StudioといったGoogleのサービスを用いることで、Googleアカウントとデータの閲覧権限があれば、URL一つで誰もが必要なデータにアクセスできる環境を目指しています。 Looker Studioでは、次の図のような図表やフィルタを簡単に作成して共有ができます。日々の業務で観測したいデータをまとめたダッシュボードを作成して、事業の意思決定に役立てています。参照するデータは前述のBigQuery上のデータを用いています。BigQueryやSQLの知識がなくても、GUI上で見たい指標を選んでグラフを作成したり、簡単な数式も組んだりすることができるので、データエンジニアに依存することなく、データの利用者がダッシュボードを編集できるようになりました。 Googleスプレッドシートにもまた、BigQueryに接続できるコネクタがあります。Looker Studioのようなダッシュボードではなく、慣れ親しんだ表計算を用いて集計業務をしたいケースに応えるために利用しています。従来はExcel上でマクロを組んで、複数のデータソースをまとめて処理していた業務を、BigQueryとスプレッドシートに置き換えることで、ある程度の業務の自動化が可能となりました。 また、集計や分析に必要なデータは、データ基盤に蓄積したデータのみではありません。集計したい指標の軸となる独自のマスタデータを管理して、それをBigQueryに連携するインターフェースとしてもGoogleスプレッドシートを利用しています。Googleスプレッドシートはデータ利用の入出力のインターフェースとして欠かせないものとなっています。 Cloud SQLやFirestoreを用いた業務アプリケーション BigQueryは大量データを処理したり、集計したデータを一括で閲覧したりするのに向いている一方で、RDBMSのように業務アプリケーションで用いるデータベースとしては、パフォーマンスやコストの観点で非常に不向きです。Google Cloud上にためたデータをWebアプリケーション上で活用したいといったケースにも対応する場合は、Cloud SQLやFirestoreといった別のデータストアサービスも利用しています。 Cloud SQLは、RDBMSを利用できるマネージドサービスで、蓄積したデータをWebアプリケーション上で閲覧したり編集したりするために用いています。Webアプリケーションは同じくGoogle Cloud上のApp Engineで構築しています。BigQueryは、Cloud SQLへの接続設定をすることで、BigQuery上のデータとCloud SQL上のデータをSQLで結合して利用することもできるので非常に使い勝手が良いです。 Firestoreは、構成が複雑な構造化データをシステム化するために用いたNoSQLのデータストアです。RDBMSで表現がしにくい構造のデータ、項目数が多かったりばらつきがあったりして定義が難しいデータ等を用いたアプリケーション用に利用しています。 今後の課題 さて、今回ご紹介したデータ基盤ですが、今はデータの活用を推進していくフェーズとなっております。ビジネス上の課題を解決するためにデータ基盤を利用したり、○○のデータを集計したいといった話は多数いただきます。一方でデータの利用用途が部署ごとにバラバラで、結局は集計したいデータを一元で管理できていなかったり、あるいは既存のETLツールで組んだデータ連携処理+Excel上での集計処理から抜け出すことができなかったりと課題は多数あります。 データを活用する基盤がある今、更なる活用に向けて、私達データエンジニアも事業の理解と、データ活用の推進により力を入れていく必要があると感じています。 おわりに 本記事ではエス・エム・エスのキャリア事業におけるデータ基盤についての事例をご紹介しました。 私達のデータ基盤は、ご紹介した通りGoogle Cloudの技術をフルに活用しています。パブリッククラウドの知識やデータサイエンスの知識も勿論求められますが、ある程度の基盤が開発され、実際のデータの活用を推進していくフェーズの今、データを利用する人たちの課題を理解する力、そしてどういったデータを用いれば課題解決に繋がるのかを提案できる力も必要となっています。 エス・エム・エスは新しいメンバーを募集しています。 私達のチームは、筆者のようなデータエンジニアから、元は基幹アプリケーションの開発に携わっていたエンジニア、データやドメイン知識が豊富なプロダクトオーナーなど、様々なスキルセットを持ったメンバーで構成されています。データ活用を推進するBPR推進部として一丸となって日々の業務の課題の理解と解決に取り組んでいます。 弊社の事業に携わってみたい方、興味のある方は、ぜひこちらのページものぞいてみてください。 エス・エム・エス - エンジニア採用情報
「エス・エム・エス社員に訊いてみた」第三弾として、介護事業者向け経営支援サービス「カイポケ」の事業責任者である園田さんへのインタビュー記事をお届けします。 事業責任者の役割 本日はよろしくお願いします。はじめに、園田さんの担っている「事業責任者」という役割がどんな役割なのかを教えてください。 カイポケに関わる組織のうち、プロダクトマネジメントとエンジニアリングの部署は田辺さん( @sunaot )が担当で、それ以外のセールス、マーケティング、カスタマーサクセス、事業開発といった部署を僕が担当しています。事業責任者が担っているのは、 「顧客に提供する価値を最大化するために必要なことすべて」 です。事業の方針を立てて、必要になるリソースを調達して、プロダクトを市場に投下・フィットさせていくというのが一つの流れです。代表的には、調査をして事業計画を立てたり、人を採用したり、予算を取ってきたり、ステークホルダーと対話したりといった事柄となります。 取り組む事柄には、連続的なものと非連続的なものがあります。事業上のボトルネックを特定して解消し、また次にボトルネックになったところを特定して解消し……というのを繰り返すのは連続的な部分です。また、個々の担当者がそれぞれの担当分野を見ているのに対して、横断的に見ることのできる自分が事業全体を統合するというのも役割になります。一方で、非連続なことを考える場面というのもあります。ミッションやビジョンの定義といったタイムラインの長いものを考えたり、事業を再定義するといったことも、僕が担うことの多い仕事です。 カイポケはSaaSのソフトウェアだけでなく、金融事業やICT機器のレンタル事業、M&A仲介事業といった、ソフトウェアと非ソフトウェアの事業を統合的に提供しているので、サービス全体のポートフォリオマネジメントも重要です。新規のサービスを作ることよりも、「何かの機能を無くす、事業を撤退する」という意思決定をする方が、担当者としては難しい場合がありますが、プロダクト全体の観点をもって、一定のユーザーがいる中で「削ぎ落す」という意思決定をする事は自身にしかできないので、とても大事だと思っています。 これまでのキャリア ありがとうございます。事業責任者になるまではどのようにキャリアを歩んできたのですか? 一貫して、「必要なことは全部やる」という仕事の仕方をしています。新卒で小さなベンチャー企業に就職して、2年目くらいには一つの事業の責任者という立場になったので、色々やりました。電子書籍サービスの会社だったので、配信サイトのUI/UXの改善をやったり、コンテンツを増やすために版権を取りにいく動きをしたり、市場を広げるために営業に行ったりと、その時々にボトルネックになっている部分を解消しにいく動きをしていました。その後の転職先でも同じような動きをしていましたし、エス・エム・エスのグループ会社であるエムスリーキャリアやMIMSに在籍していたときもそうです。その後いちど起業をしていた時期もありました。 カイポケに関わるようになってから なるほど、エス・エム・エスには起業を経験されたあとで戻ってきたのですよね。戻ってきてからはどのようなことをして今に至っているのですか? カイポケには10程度のプロダクトラインが有るのですが、それぞれのLTV(ライフタイムバリュー)を把握している人はいなくて、リソース投下の優先順位も曖昧な状態でした。そこで、まず各部署からデータを取得して、各プロダクトのLTVを分析しました。 各部署の長にヒアリングをしていく中で、サイロ化していて、事業全体を統合して把握している人が存在しないことを課題に感じました。そのせいなのか、データが各グループ内に閉じていてデータの形態もそれぞれ独自(Excelがあったり、CSVファイルがあったり、あるいは時系列データで持っているところがある一方でスナップショット的なデータしかないところもあったり)で、横串で分析をする事が極めて困難になっていたのです。 こういった部分の改善と並行して、ボトルネックを探索的にみていって特定するという活動をしました。ちなみに、最近ではデータの民主化がだいぶ進んできています。データの民主化については以下の記事も見てみてください。 tech.bm-sms.co.jp その後、分析対象を広げていって、事業をこうしたら良いのでは?という提案を行い、最終的には、事業の責任者になりました。 PythonやSQLを使ったデータ分析の話 データの話が出ましたが、園田さんはPythonやSQLを使ってデータ分析などをバリバリできる人だと聞きました。どういうきっかけで身に付けたんですか? 僕は、別にデータ分析を専門としているわけではないのですが、事業のために必要なことをやるというスタンスで仕事をしていく中で、やはりデータ分析という所作は必要で、以前からよく行っていました。昔はExcelとVBAでやっていたのですが、大量データを扱うことが多かったので、SQLで分析するようになりました。Pythonで前処理を行うようになったのも似たような理由です。 カイポケの事業運営の特徴 カイポケの事業運営についてお話を伺います。データの話も出ていましたが、カイポケの事業運営の特徴を教えてください。 可能な限り「勘」で動くことを避けて、調査/分析→仮説構築→仮説検証→施策実行という流れにそって運営しています。そして、分析のアプローチの一つとして、現場に身を投じて感覚的に理解することと、データ分析の2つを重視しています。 前提として、カイポケが対象とする医療介護業界は、エンタメやBtoCのようなトレンドに左右される事業という性質は薄く、一定程度はStable(安定的)なマーケットです。プロダクトを作る側が何か新機軸を打ち出すということよりも、顧客のニーズを捉えてそこにソリューションで応えていくことの方が重要だと考えています。さらに、カイポケは既に多くの顧客に利用していただいていて、データも社内に相当程度蓄積されています。そのため、意思決定をするときにデータを活用するということがフィットしていると思います。また、マーケットの性格上、中小の法人の顧客が大多数を占めていて、特定の顧客に売上の多くを依存しているわけではないということも、データ分析の重みが増す要因です。 なるほど、プロダクト開発においてデータ分析を用いるというのは近年では当たり前になってきている印象もありますが、カイポケは特にそれがフィットしているという側面があるのですね。ここまでの話では机上の分析を重んじている印象がありますが、一方で事業所訪問などもよくしていますよね。 はい、プロダクトの使われる現場に行くことから得られる肌感覚も大切にしています。介護事業所などを訪問することはやはり大事で、僕自身も率先して行きますし、メンバーにも推奨しています。カイポケに関わるメンバーは若い人が多いので、介護事業所に縁のある人が少なく、実際にプロダクトが使われる場面のイメージを持ちづらいんです。いくら分析をしても、イメージが湧かないところに対して考えることというのは的を射ていないものになりがちです。たとえば、障害児通所支援事業所を訪れてみると、カイポケを使ってもらう端末としてタブレットはあまりフィットしないということがわかります。介護事業所と違ってそこにいるのは子どもなので、職員のタブレットで遊びたがってしまうんですよね。こういったことは現場に行くとわかるので、現場に行くことは問題解決のためには非常に有益なんです。また、現場に行くとやっぱりがんばろうという気持ちにもなれて、仕事にも熱がこもってきます。営業の方であれば当然現場には行くんですが、プロダクトマネージャーやエンジニアの方というのは普段の仕事の中では現場に行く機会は意図して行わないと少ないので、例えば「事業所に訪問したい!」とチャットで投げれば営業側ですぐに設定するようにするなど、各人が現場を訪問するハードルを極力下げるようにして、どんどんメンバーには訪問してもらえるようにしています。 カイポケのプロダクト開発の特徴 プロダクト開発という観点ではカイポケにはどんな特徴があるでしょうか? 先ほど言ったことと関連するのですが、SaaSらしいプロダクト開発をしているということは言えます。「SaaSらしい」というのは、Slerのように、特定の顧客のためにカスタマイズを頑張るのではなく、顧客全体にとってのValueを考え、最大公約数的にプロダクトを作っていくということです。そのようにして作ったプロダクトの仕様から外れる部分については、顧客の業務を可能な限りシステムに寄せてもらうという思想ですね。これが有効なのは、カイポケが対象としているマーケットが、中小の顧客が圧倒的多数を占めるマーケットだからです。もちろん顧客の声というのは重要なインプットではあるのですが、営業が大口の顧客と約束してきてしまったから特殊な機能を作らないといけない、みたいなことは避けています。特定の顧客のための例外処理的なものを実装するとシステムとしても複雑になってしまいますし、多数の顧客にとって使わない機能が存在していると、プロダクトの完成度はむしろ落ちると思っています。 また、カイポケで一番大事にしているのは顧客への提供価値を最大化することで、システム開発ですべて解決しようとはしていません。顧客の課題を解決するのにシステムでの対応が一番的確な場合はもちろんシステム開発をしますが、たとえばサポートでの支援で解決するというのも選択肢のひとつとして扱っています。システムという狭義のプロダクトに限らず、セールスやカスタマーサクセス、サポートや外部の協力会社といったエコシステム全体を「プロダクト」として捉えています。これはテクノロジーで全てをスマートに解決してやろうという志向の人からすると「え〜」と思うような側面かもしれませんが、介護や医療の現場のリアルで複雑な課題に対して、「現実解で解く」という面白さもあります。システム開発ではなく問題の解決が私たちの商売というわけです。 プロダクト開発部門との関係 エンジニアとしても、言われたものをただ作るというのは避けたいし、作るなら本当に顧客にとって必要なものを作りたいので、事業全体としてそういう方針だとやりやすいですね。園田さんと、プロダクトマネージャーやエンジニアの部門との関係はどういうふうになっているのですか? プロダクトマネージャーはプロダクトに責任を負います。何かを決めるという時に、エンジニアリングリーダーシップ、プロダクトリーダーシップ、ビジネスリーダーシップという3つに分けて考えるというのを田辺さんが社内で提唱しているのですが、僕はこのうちのビジネスリーダーシップのみを権限として持つようにしています。ですから、最終的にプロダクトのHOWの部分(どうするのか、どう作るのか)については、事業責任者である僕にも「こうしろ」という権限はありません。ビジネスの観点から、こういう背景があるとか、こういう未来予測になるとか、収益上こういう形にしたいとか、「こうするのがいいと思う」という意見はもちろん言います。しかし、最終的にそれをプロダクトとしてどのように表現するかは、プロダクトリーダーシップの担い手であるプロダクトマネージャーが責任を負っていますし、技術的にどのように実現するかはエンジニアリングリーダーシップを担うエンジニアが責任を負います。ここは完全に権限が委譲されている状態です。たとえば、プロダクトマネージャーの決めたロードマップに対して僕が何かを差し込むというのはできないようになっています。 なぜそうしているかというと、何かを最終的に決める人というのを1人に決めておくことによって色々なことがスムーズにいくと考えているからです。これはプロダクト開発部門に対してだけでなく、セールスやマーケティングといった事業部門のメンバーに対してもそうで、僕は「ここはあなたに100%委譲したので僕は決められません、あなたが決めてください」とよく言っています。これは元々の僕のマネジメントスタイルで、そこと先ほどの田辺さんの言っている3つのリーダーシップの話はマッチしていたので今の組織運営はそういう形になっています。 エンジニアへのメッセージ 最後に、エンジニアに対して伝えたいことはありますか? 理想としては、エンジニアの方にも、顧客に対して価値をどう提供するかについてよりよい方法を提案してもらえると嬉しいです。もちろん最終的には実装する、ものづくりをするというのがエンジニアの職責だとは思うのですが、プロダクトマネージャーがプロダクトマネジメントトライアングルの全てを担うことはかなり難しいので、「いやそこはこう作った方が拡張性があるよ」や「そこは実装しなくていいんじゃないか」といった議論をエンジニアからもしてもらえるといいなと思います。そのためにはやはり、できれば顧客を理解してほしいなと思っています。 もちろん、エンジニアとしては、一番興味がある、やりがいを感じるのはそういう部分じゃなくて技術の部分だ、という人もいると思うので、みんながみんなそうなるとは思っていないのですが、もし顧客を理解してものづくりをするという部分にも興味を持っているエンジニアがいれば、とてもハッピーですし、そういう人にとってやりやすい環境を作る努力は惜しみません。社内でのコミュニケーションの場も用意しますし、顧客と話をする機会もアレンジします。事業所訪問のハードルを下げるというのもこういう意図でやっていることです。ですから、こういう動き方に興味がある人がいたら一緒に働けると嬉しいです。 ありがとうございました! (終)
介護事業者向け経営支援サービス「カイポケ」の開発をしている伊藤です。2019年4月に入社し、一貫してプロダクト開発部の介護レセチームでエンジニアとして活動しています。 本稿では私の所属する介護レセチームで実施している、システムの改善活動について気をつけていることや気がついたことをまとめます。 介護レセチームとは 介護レセチームは、介護事業者向け経営支援サービス「カイポケ」の介護領域の開発を担当するチームです。「カイポケを使った介護業務をノンストレスで行えるようにすることで利用者に向きあうゆとりを増やすこと」をチームのミッションに掲げ、日々カイポケの機能開発などを実施しています。 カイポケでは介護事業所の経営を支援するための様々な機能を提供していますが、コア機能の1つとして、介護事業所が収入を得るために必要となる請求業務の支援機能があります (請求業務はレセプト業務とも呼ばれ、介護レセチームの名前の由来にもなっています)。この請求業務の支援機能は、2000年から施行されている介護保険制度のルールに依存する機能です。この介護保険制度は数年ごとに「介護報酬改定」と呼ばれる見直しが実施され、様々な改正が行われます。介護報酬改定についての詳細はこちらの過去のブログをご参照ください。 tech.bm-sms.co.jp 介護報酬改定の改定内容をカイポケへ素早く反映し、ユーザへ価値を届ける必要があるため、介護レセチームでは普段からリファクタリングなどのシステム改善活動に取り組んでいます。 介護レセチームでのシステムの改善活動について システムの改善を行うにあたって、まずは一般的によいとされている知識をインプットすることから始めるのがよいと考えています。システム改善の理想的なゴールを思い描きゴールに向かって効率的に進むために、まずは知識や方法論の理解が必要で、これは大前提になると思います。余談ですが、介護レセチームは技術知識のインプットに意欲的なメンバーが多く、毎週実施しているふりかえりの中で、だいたい誰かしらが読み終えた技術書の紹介をしてくれます。 介護レセチームでは、新しい機能開発を実施する場合でもすぐに改修を実装するのではなく、リファクタリングから始めます。修正対象のモジュールにテストがない場合はまずテストコードを書き、リファクタリング対象の振る舞いを壊さないよう保護したうえでリファクタリングを実施しています。 リファクタリングを実施する上での方法論の詳細については、本稿での説明は控えますが以下の書籍が参考になると思います。 www.seshop.com このように特別なことをしているわけではないのですが、介護レセチームはこれらの改善活動を継続的に実施しています。 システムの継続的な改善活動 システムの改善は長丁場の作業になりがちです。秘孔のような改善ポイントを見つけて、そこを修正するだけで一発で理想に到達できれば理想的なのですが、なかなかそうはいきません。特にカイポケでは、前述のとおり介護報酬改定に起因する開発を実施することがあり、スケジュールの締め切りが事実上決まっていることが度々あるので、毎回改善活動を理想的なゴールまで持っていけるわけではありません。 しかし、一発で理想にたどり着けないからといって何も改善しなければ、文字通り何も改善することはできません。一発で理想にたどり着けないのならば、今回の対応で最低限どこまで改善するのかチームで認識を合わせ、少しずつ理想に近づける作業を継続的に続ける必要があります。 繰り返しになりますが、システムの改善を実施する上では効率的に改善を実施するための知識や方法論を知っておくことは大前提です (もしそれらが足りていないようなら、知識や方法論をインプットすることから始めるのがよいと思います)。しかし、継続的に改善のサイクルを回していくためには知識や方法論を知っているだけでは不十分です。システムの改善活動は、ユーザからは目に見える改善につながらない場合も多くあります。そのため、継続的に取り組むためには改善の必要性について関係者(開発チームのメンバーやプロダクトマネージャー、QAチームのメンバーなど)の間で共通認識を作り、皆の目線をあわせた上で、粘り強く継続的に取り組むことが大切です。 無力感に気づき、無力感と向きあう ある程度の規模のサービスに関わっていると、大量の課題に優先順位をつけて順番に対応していくことが多いと思いますが、介護レセチームでも各課題を評価した上で優先度順に対応を行っています。 しかし、ある時一歩下がって課題全体の消化状況を見てみると、消化できているのは緊急性が高い課題ばかりになっていることに気がつきました。反面、やればすぐに対応できる課題や重要だが緊急性の高くない課題が積み上がっている状態になっていました。 振り返ってみると、前述の通りカイポケではスケジュールの締め切りが事実上決まっていることが度々あるため、緊急性の高い課題を締め切りに間に合わせることにフォーカスすることが多く、やればすぐに対応できる課題や重要だが緊急性の高くない課題に手をつけにくい雰囲気になってしまっていたように思います。 結果として、「自分たちはこんな簡単なことにも着手できないのか……」、「サービスの将来にとって重要な課題に着手できない……」といった、もやもやとした無力感がチームに溜まった状態になっていました。 介護レセチームでは、開発メンバーとプロダクトマネージャーで課題の扱い方について対話を重ねる中で、自分たちが無力感を感じていることに気づきました。無力感に気づいてから、開発メンバーとプロダクトマネージャーの間で、「無力感を放っておくとメンバーの日々の活動に対するモチベーション維持やキャリア形成(技術/業務スキルの取得や、技術/業務スキルを活かした業務経験を得ることなど)にネガティブな影響があり、各メンバーに介護レセチームで長く生き生きと活躍してもらうことが難しくなるのでは?」という懸念について認識をすり合わせることができました。 現在では以下のように課題を扱っています。 課題の優先度を決める際には以下のように評価を行う 緊急性の高い課題の緊急度合いにも濃淡があるので、対応しなかった場合、サービスにどの程度影響があるのか個別に評価する 課題の緊急性だけでなくサービスにとっての将来的な重要性や、対応した際にチームが得られる学びや経験の多さなども評価する やればすぐに対応できる課題については、新しいメンバーが加入した時の最初のタスクにしたり、手が空いたときに着手する課題としてチームで認識を共有しておくことで消化しやすくする 無力感を認識することで緊急性以外の観点から課題を扱えるようになり、少しずつこれまで積極的に実施できていなかったサービスのデリバリー方法の改善などの改善施策にも取り組めるようになってきています。 活動をチームで粘り強く継続的に続けるには、チームメンバーが消耗せず、生き生きと活躍できる状態に近づけることも必要だと思います。そのために、無力感のような自分たちの負の感情と向きあい、どのようにそれを扱っていくのか明確にすることも重要だと感じています。 小さい歩みを続けるために ここまでシステムの改善活動は継続性が大切だと繰り返してきましたが、現実問題として、スケジュールの期限がある中である課題の対応中に思わぬ問題が見つかったり、他の緊急性の高い対応依頼が飛び込んできたりすることもあるわけで、地道な活動を日々継続的に続けるのが苦しいときもあります。 私は苦しい状況になればなるほど、関係者の間で活動の必要性についての共通認識ができていて目線が揃っているかが試されると考えています。目線の揃ったチームは苦しい状況でも皆で励ましあいながら前に進むことができ、改善がうまくいったときには皆で互いに感謝しあい、喜びあうことができます。そしてまた次の改善活動に取り組むことができるのです。 介護レセチームの改善活動はまだまだ道半ばです。私達は一緒に喜びを分かちあい、励ましあいながら改善を進めてくれる仲間を探しています。関心を持ってくださった方は、ぜひ末尾のカジュアル面談のリンクから話を聞きにきてください。 最後に、私が尊敬する元プロ野球選手のイチローさんの言葉で本稿を終わろうと思います。 小さなことでも満足感、満足することっていうのはすごく大事なことだと思うんですよね。だから、僕は今日のこの瞬間とても満足ですし、それは味わうとまた次へのやる気、モチベーションが生まれてくると僕はこれまでの経験上信じているので。これからもそうでありたいと思っています。 (出典: 国際情勢研究会『イチロー 会見全文』 /「 第1章 メジャー通算3000本安打達成会見(全文)」p.19)
こんにちは、エス・エム・エスのフロントエンドエンジニアの城内です。 前職では、ヘルスケア系のスタートアップでソフトウェアエンジニアをしていましたが、2022年8月にエス・エム・エスへ入社し、介護事業者向けの経営支援サービス「カイポケ」の改善をするチームに所属しています。 カイポケの改善を進める開発チームでは、この度フロントエンド専任チームを立ち上げました。この記事ではフロントエンドチーム立ち上げの背景や、チームの立ち上げから進めてきた技術選定について書きたいと思います。 カイポケ改善プロジェクト 介護事業者向けの経営支援サービス「カイポケ」は、4万を超える事業所で導入されている SaaS 型のサービスです。介護事業には様々なサービスの種類(ex. 居宅介護支援、通所介護支援、訪問介護支援、etc…)があり、カイポケはそれぞれのサービス種類に対応した約40のサービス・機能を提供しています。カイポケは15年以上の歴史の中で、各サービス種類への対応や法改正に合わせてシステムを拡張してきました。カイポケはモノリシックなアプリケーションとして構築されてきたので、拡張に次ぐ拡張で様々な機能が複雑に絡み合う大きなプロダクトになっています。 高齢化が進む日本社会において、介護領域では制度改正や改正に伴う現場対応など、目まぐるしい変化が起こり続けています。システムとしても、こういった介護業界の変化や数年に一度行われる法改正へ対応していくことが求められます。今のカイポケは現在の介護事業を支えるプロダクトになっていますが、長期的なタイムスパンでこれらの法改正などの要望に応えていくことを考えた場合に、より良いシステムのアーキテクチャにできないかと改善プロジェクトのチームで検討を重ねてきました。そういった背景から、システムを改修する際の修正範囲の局所化と新しいサービス種類への対応などといった拡張性の担保、生産性向上のための並列性の確保を目的にマイクロサービスアーキテクチャへ移行を進める改善プロジェクトが始まりました。 カイポケの規模では一度に全てのサービスをマイクロサービスに移行するのは現実的でないため、今回の改善プロジェクトでは特定のサービス種類に対象を絞って少しずつリリースをして、小さく早くユーザーからのフィードバックを回して確実に価値を積み上げていく方針を取ることにしました。 フロントエンドチームの発足 当初想定していた改善プロジェクトの開発チームの体制は、ドメインごとに適切な単位でマイクロサービスに切り出し、各サービスごとに同じエンジニアがバックエンド・フロントエンドを実装する形でした。しかし、カイポケのサイト全体の情報設計を再検討しユーザビリティテストを行った結果、カイポケはドメイン単位ではなく様々なユースケース単位(ex. 経営者、ケアマネージャ、サービス事業者、etc…)で利用されていることが見えてきました。さらに、ユースケース起点でシステムを分割することで情報設計の複雑さが改善されナビゲーションがシンプルになるということがわかり、ユーザーインターフェイスとなるフロントエンドについてはユースケース単位で分割する方針になりました。 この変更によってドメイン単位で分割したバックエンドとユースケース単位で分割したフロントエンドでシステムの境界が異なることになり、従来通り一つのチームでバックエンドとフロントエンドを実装するチーム構成が合わなくなってきたため、バックエンドとフロントエンドのチームを分割し、フロントエンド専任のチームが発足することになりました。 全体的なシステムの構成としては、バックエンドとフロントエンドの間には (GraphQL)を配置して疎結合にし、フロントエンドからは Gateway を通して横断的にバックエンドの API を呼び出せる構成を取っています。 フロントエンドチームはまだ発足して数ヶ月で、今はチームビルディングやフロントエンドで採用する技術選定を進めているフェーズになります。 ここからはカイポケの改善活動でフロントエンドに求められる開発の要件と技術選定について紹介します。 改善プロジェクトのフロントエンドにおける要件 現在のカイポケの UI は1000ページ以上ある大規模なプロダクトです。 昨今のフロントエンド開発では、コンポーネントを組み合わせて画面を構築する手法が一般化しましたが、カイポケの改善プロジェクトも例外ではありません。複数の画面で利用するコンポーネントは適切な単位で共通化することはもちろんですが、コンポーネント数が増えても破綻しないような一貫性と拡張性を持った設計が求められます。 また、現在のカイポケは長年の機能追加によって改修の影響範囲が見えにくくなり部分的な改修が続いた結果、全体的に統一感のない UI や少しずつ機能の違う同じようなページが増えていきました。今回の改善プロジェクトでは、ユースケースを元にフロントエンドを分解し、それぞれのユースケースに適切な UX を目指すとともに、カイポケとして一貫したユーザー体験を提供するために統一感のある UI の開発も求められています。 技術選定 言語 技術選定する上で最初に考えたのは、型を中心に据えることです。 カイポケは巨大なプロダクトであり、改善プロジェクトも長期にわたることが想定されています。小さく早くユーザーからのフィードバックを回して確実に価値を積み上げていく方針のため、継続的な改善やリファクタリング等の活動も必要になります。こういったプロジェクトでは、静的型チェックによる整合性の検査、補完やリファクタリングを中心としたエディタの支援など、TypeScript を導入することによる生産性の向上は非常に大きいものがあります。 また、バックエンドとの通信には GraphQL を採用しました。 後述する GraphQL Code Generator を利用することで API のレスポンスに型がつくのはもちろん、フロントエンドとバックエンドでスキーマ定義の合意を取ることで、フロントエンドとしてはモックサーバーを利用して開発をし、バックエンドはスキーマを返すロジックを実装するという、スキーマ駆動で独立性高く並行して開発を進められるのも今回のプロジェクトのチーム体制にマッチしていました。 UI ライブラリ・フレームワーク UI ライブラリには、コンポーネント指向で画面を構築できること、TypeScript との親和性の高さやシェアの高さから React を採用しました。 React と合わせて、規約を持ち込むことによる生産性やパフォーマンスの向上を目的にフレームワークに Next.js を採用しました。pages 以下にファイルを配置すればルーティングされる仕組みが欲しかったのと、webpack の設定を隠蔽してくれたり、zero-configuration で TypeScript に対応できたり、v11.0.0 からは ESLint の設定がデフォルトで用意されたりと、プロジェクトの足回りを整えてくれる機能が備わっているので、ユーザーへの価値提供に繋がるアプリケーションの開発に集中することができています。 また、レンダリング方法として SSR / CSR / SSG を選べるのも Next.js の大きな特徴です。今回の改善プロジェクトでは CSR 中心で実装を進めていますが、将来的に SSR に対応したいページが出てきた場合にも対応できるように技術的な選択肢を残しておきたかったのも理由の1つです。 GraphQL Gateway との通信で利用する GraphQL クライアントは、 Apollo を採用しました。 バックエンドの各マイクロサービスの GraphQL スキーマをまとめるために Apollo Federation を採用しているのでそちらとの相性もありますが、GraphQL Code Generator で Apollo の Hooks を生成できる点も大きいです。フロントエンドは .graphql を書き、GraphQL Code Generator で各マイクロサービスの GraphQL スキーマから Hooks を含んだ生成ファイルを React Component が利用する形かつ型が一貫した状態で生成する仕組みを構築することができます。 UIフレームワーク 一般的な WEB アプリケーションで共通して使用する UI パーツについては車輪の再発明を避けたかったので、UI フレームワークを導入しました。MUI や Tailwind CSS などと比較した結果、以下の理由で Chakra UI を採用しました。 デザイナーが作成していたデザインとテイストが近い 標準でモーダルやフォーム、タブなどのコンポーネントが揃っている Color や Typography などのデザイントークンのルールが整備されている アクセシビリティが考慮されている Figma が提供されている Chakra UI は Color や Typography、Spacing などデザイントークンが JavaScript のオブジェクト形式で定義されており、これらを上書きすることでアプリケーション独自のスタイルを定義できる Theme という機構を持っています。今回のプロジェクトでは、統一感のある UI を作っていくためにデザイナーと連携してデザインシステムの構築を進めていて、デザイントークンの定義にこの機構を利用しています。 Chakra UI の Theme の機構とカイポケで作りたいデザインシステムのルールがマッチしないのではないかという懸念もありましたが、プリミティブトークンとセマンティックトークンを分けて定義できたり、プリセットで定義している Color や Typography、Spacing などのバリエーションが今回のプロジェクトのデザインシステムにおいては必要十分と判断して採用を決めました。 Figma も提供されているので、Chakra UI のコンポーネントをデザイナーが作成した Figma に取り込んでもらい、Chakra UI の Props と Figma の Component Properties を一致させた形でデザインシステムを整えていただいています。こういった形でデザイナーとエンジニアでデザイントークンやコンポーネントの粒度についての認識を揃えながら、デザインシステムを育てていっています。 その他の主要ライブラリ state management: Recoil form: React Hook Form schema validator: Zod component catalog: Storybook test: Jest test library: Testing Library 今後はE2E テストや Figma で更新されたデザイントークンをフロントエンド側に自動更新する仕組みなども整備していこうと考えています。 技術顧問 ここまで書いてきた技術選定は、私を含めたフロントエンドチームのメンバーと相談して決めていますが、チームメンバーがナレッジを持っていない領域のこともあります。そういった際に相談できる場として、Japan Node.js Association 代表理事の古川陽介さん( @yosuke_furukawa )に技術顧問として参画いただいています。中長期的な視野に沿った技術選定や、プロジェクトの進め方に関するアドバイスなどをいただけるので、チームやプロジェクトにあったちょうど良い技術選定ができていると感じています。こういった技術的なサポートがある環境なので、チームメンバーも疑問点を解消しながら安心感を持ってプロジェクトを進められています。 おわりに この記事では、介護事業者向け経営支援サービス「カイポケ」を改善するフロントエンドチームの発足の背景と技術選定の一部を紹介しました。 エス・エム・エスでは開発メンバーを募集しています。カイポケの開発に興味を持ったり、チャレンジしてみたいという方がいれば、ぜひこちらも覗いてみてください。またカジュアルに話だけ聞いてみたい、といった方も大歓迎です。こちらのページよりお気軽にご連絡ください!
この記事は、「MySQL Advent Calendar 2022」の13日目の記事です。 qiita.com 株式会社エス・エム・エスでエンジニアをしている @koma_koma_d です。今回はMySQLにおけるセミジョイン最適化について調べた内容を書きます。 ※記載内容に誤りなどがある場合は筆者のTwitter宛に連絡をいただけると幸いです。 前置き この記事で書くこと この記事では、MySQLにおけるセミジョイン最適化について、サンプルテーブルを用いた実行例を示しながら、 セミジョイン最適化とは何か セミジョイン最適化はなぜ有用か セミジョイン最適化にはどのような種類があるのか どのようにクエリやテーブル定義を変えると戦略が変化するか どのような実行計画になるか どのようなオプティマイザトレースになるか などを紹介します。 執筆にあたって、Web上のリソースなどをある程度調べましたが、自分が気になった上記のような内容を網羅しているものが見当たらなかったので、誰かの役に立つかもと思って書いています。 この記事で書かないこと MySQLの公式ドキュメントを読むだけでわかること 公式ドキュメントを読むだけでわかることについては、この記事では書きません(必要に応じて公式ドキュメントから引用をする場合はあります)。 MySQL :: MySQL 8.0 リファレンスマニュアル :: 8.2.2 サブクエリー、導出テーブル、ビュー参照および共通テーブル式の最適化 MySQL :: MySQL 8.0 Reference Manual :: 8.2.2.1 Optimizing IN and EXISTS Subquery Predicates with Semijoin Transformations コードリーディングに基づいた知見 MySQLのコードはGitHubで公開されているので、コードを読むことで動作を読み解くことも可能ではありますが、筆者にはその技量まではないので今回は対象外です。 GitHub - mysql/mysql-server: MySQL Server, the world's most popular open source database, and MySQL Cluster, a real-time, open source transactional database. MySQLのバージョン間の比較 MySQLはセミジョイン最適化が導入された後も進歩を続けており、その過程でセミジョイン最適化に関連するバージョンアップも行われているようですが、それらについて細かく言及することは今回の記事ではしません。 他のRDBMSとの比較 Oracleなどの他のRDBMSにも類似した最適化があるようですが、それらとの比較は今回は対象外とします。 セミジョイン最適化概説 前置きが長くなりましたが、本題に入っていきます。今回のテーマであるセミジョイン最適化は、MySQL 5.6 からサブクエリの実行に関する最適化として導入されたものです。まず、なぜセミジョイン最適化が「最適化」たりうるのか、パフォーマンス的に嬉しいのかを説明します。 なぜセミジョイン最適化がパフォーマンス的に嬉しいのか? 本来、サブクエリはメインクエリに従属しており、サブクエリ側からはメインクエリ側のカラムを参照できます(相関サブクエリはこれを利用したもの)。サブクエリ側からメインクエリ側のカラムを参照できるということは、メインクエリ側のテーブルが先にアクセスされるということです。 しかし、セミジョイン最適化が行われると、結合(JOIN)として処理することになるので、①先にサブクエリ側のテーブルにアクセスすることが可能になります。このため、サブクエリ側のテーブルに先にアクセスした方が効率が良い場合には、パフォーマンス上のメリットを享受できる可能性があります。 『詳解MySQL 5.7』 では以下のように記載されています。 MySQL 5.6 では、 IN サブクエリを SEMIJOIN という特殊な JOIN へと変換することで、より効率的な実行計画が選択されるようになった。SEMIJOINとは、駆動表の1行に対して内部表からマッチする行が1行だけになるという特殊な結果を産むJOINである。 SEMIJOINがなぜunique_subqueryやindex_subqueryより優れているかというと、テーブルをJOINする順序を入れ替えられるからである。サブクエリ内でアクセスされるテーブルを先にアクセスするような実行計画のほうが効率的なものになるケースは少なくない。 (『詳解MySQL 5.7』p.110) www.shoeisha.co.jp 更に、セミジョイン最適化を適用したクエリは、通常の結合では必ずしも満たされていない条件である、 最終的な結果にサブクエリ側のカラムが含まれない メインクエリ側の1行に対してサブクエリ側の複数行をマッチさせない(マッチすることを考慮しなくてよい) という条件を満たすため、②通常の結合では取ることのできない処理の効率化を行うことができます。(MySQLに焦点を当てた記述ではありませんが) 『SQL実践入門』 では以下のように記載されています。 「Semi-Join」は日本語では「準結合」または「半結合」と呼ばれています。これは通常の結合の際には現れない、EXISTS述語(とIN述語)を使ったときに特有のアルゴリズムです。 このアルゴリズムの特徴は次の2つです。 機能的には、結果には駆動表となるテーブルのデータしか含まれず、しかも1行につき必ず1行しか結果が生成されない(通常の結合の場合、1対Nの結合の場合は行数が増えることがある) 内部表にマッチする行を1行でも発見した時点で残りの行の検索を打ち切れるため、通常の結合よりもパフォーマンスが良い (『SQL実践入門』p.341) gihyo.jp 以上で記載した、 ①先にサブクエリ側のテーブルにアクセスすることが可能 ②通常の結合では取ることのできない処理の効率化を行うことができる という2つの特性をどのように活かすかが、次に紹介するそれぞれのセミジョインの戦略で違ってきます。 セミジョインにはどのような「戦略」があるのか? セミジョインには、いくつかの種類があります。MySQLではそれらを「戦略(Strategy)」と表現しています。各戦略の特徴は、以下のように整理できます。 なお、表中の「重複の除去」は、先述の「メインクエリ側の1行に対してサブクエリ側の複数行をマッチさせない(マッチすることを考慮しなくてよい)」という側面に関するもので、メインクエリ側の1行がサブクエリ結果との結合によって最終的な結果の中で重複しない理由、重複させない方法を記載しています。 戦略の名称 サブクエリ側の結合キー列のINDEXの必要性 駆動表と内部表 重複の除去 Table Pullout UNIQUE制約必要 可変 UNIQUE制約により保証 LooseScan 必要 サブクエリ側が駆動表 インデックスを活用しながら結合時に実施 Materialization 不要(一時表で自動作成) 可変 サブクエリ実体化時に除去 Duplicate Weedout 不要 可変 結果を返す前に除去 FirstMatch 不要 メインクエリ側が駆動表 結合時に除去 以下、個別に補足説明を加えます。記載している内容は参考資料に依拠しているほか、サンプルテーブルを使った実行例から分かる内容を記載しています。 主な参考資料は以下の2つです。 MySQL道普請便り 第43回 MySQLの準結合(セミジョイン)について セミジョインについての親切な紹介。駆動表と内部表がどうなるかはこちらに主に依拠した。 MariaDB 10.6 [日本語] 最適化とチューニング セミジョイン副問い合わせの最適化 (MySQLではなくMariaDBですが)それぞれの戦略が図付きで解説されていてわかりやすい。 各戦略の特徴 ここからは、上で記載した表の内容を戦略ごとに補足していきます。各戦略について細かく論じていくにあたって、サンプルテーブルを用いて実際に実行計画やオプティマイザトレースを取得した結果を随時示します。実行計画やオプティマイザトレースについては、実物を示すのが最も参考になると思いましたので、厚め(実行計画は全部、オプティマイザトレースは一部抜粋)に載せましたが、記事が長くなってしまうので折りたたんでいます。展開したい場合は「▶︎詳細」となっているところをクリックしてください。 サンプルテーブルの前提 サンプルテーブルの前提条件を記載しておきます。 使用したMySQLのバージョン MySQL 8.0.21 ※現時点での最新は 8.0.31 です。たまたま調べようと思ったタイミングでローカルに入っていたのがこのバージョン( 以前個人ブログの方に書いた記事 の検証で使ったバージョンだった)だったために過ぎません。 サンプルテーブル CREATE TABLE `employee` ( `emp_id` int unsigned NOT NULL AUTO_INCREMENT, `main_floor` int unsigned DEFAULT NULL , `gender` int unsigned DEFAULT NULL , PRIMARY KEY (`emp_id`), ) ENGINE=InnoDB; CREATE TABLE `department` ( `dept_id` int unsigned NOT NULL AUTO_INCREMENT, `main_floor` int unsigned DEFAULT NULL , `dept_name` varchar ( 100 ) DEFAULT NULL , PRIMARY KEY (`dept_id`) ) ENGINE=InnoDB; データ内容(クリックで展開) employee テーブル emp_id main_floor gender 1 1 1 2 2 2 3 3 3 4 4 1 5 5 2 6 6 3 7 7 1 8 8 2 9 9 3 10 10 1 11 11 2 12 12 3 13 13 1 14 14 2 15 15 3 16 16 1 17 17 2 18 18 3 19 1 1 20 2 2 21 3 3 22 4 1 23 5 2 24 6 3 25 7 1 26 8 2 27 9 3 28 10 1 29 11 2 30 12 3 31 13 1 32 14 2 33 15 3 34 16 1 35 17 2 36 18 3 department テーブル dept_id main_floor dept_name 1 1 Finance 2 2 Legal 3 3 Human Resorces 4 4 Corporate Planning 5 5 Sales 1 6 6 Sales 2 7 7 Accounting 8 8 Development 1 9 9 Development 2 Table Pullout Table Pullout 戦略は、以下のような特徴を持ちます。 通常のJOINとして処理する 結合キーにUNIQUE制約(PRIMARY KEY含む)がある場合に利用可能 メインクエリの結果1行に対してサブクエリから0or1行しか返らないことが保証されているので、通常のJOINにして結合順序を入れ替えてもメインクエリ側の行が最終結果の中で重複することがない サンプルテーブルを用いた実行例から分かることは以下の通りです。 department 表の main_floor 列にUNIQUE制約を付与したところ選択された オプティマイザトレースの pulled_out_semijoin_tables という項目に department 表が表れている EXPLAIN ANALYZE をみると、 Remove duplicates from ... という重複除去を表す情報がない ここが後述のLooseScanとの違い SELECT * FROM employee e WHERE e.main_floor IN ( SELECT main_floor FROM department d ) 実行計画 mysql> explain select * from employee e where e.main_floor in ( select main_floor from department d ); +----+-------------+-------+------------+-------+---------------------------+---------------------------+---------+----------------------+------+----------+--------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------------------+---------------------------+---------+----------------------+------+----------+--------------------------+ | 1 | SIMPLE | d | NULL | index | department_main_floor_IDX | department_main_floor_IDX | 5 | NULL | 9 | 100.00 | Using where ; Using index | | 1 | SIMPLE | e | NULL | ref | employee_main_floor_IDX | employee_main_floor_IDX | 5 | sandbox.d.main_floor | 2 | 100.00 | NULL | +----+-------------+-------+------------+-------+---------------------------+---------------------------+---------+----------------------+------+----------+--------------------------+ 2 rows in set, 1 warning ( 0.00 sec) Note (Code 1003 ): /* select#1 */ select `sandbox`.`e`.`emp_id` AS `emp_id`,`sandbox`.`e`.`main_floor` AS `main_floor`,`sandbox`.`e`.`gender` AS `gender` from `sandbox`.`department` `d` join `sandbox`.`employee` `e` where (`sandbox`.`e`.`main_floor` = `sandbox`.`d`.`main_floor`) mysql> explain analyze select * from employee e where e.main_floor in ( select main_floor from department d )\G *************************** 1 . row *************************** EXPLAIN : -> Nested loop inner join (cost= 7.45 rows = 18 ) (actual time = 0.040 .. 0.078 rows = 18 loops= 1 ) -> Filter: (d.main_floor is not null ) (cost= 1.15 rows = 9 ) (actual time = 0.023 .. 0.026 rows = 9 loops= 1 ) -> Index scan on d using department_main_floor_IDX (cost= 1.15 rows = 9 ) (actual time = 0.022 .. 0.024 rows = 9 loops= 1 ) -> Index lookup on e using employee_main_floor_IDX (main_floor=d.main_floor) (cost= 0.52 rows = 2 ) (actual time = 0.005 .. 0.005 rows = 2 loops= 9 ) オプティマイザトレース(一部抜粋) { "steps" : [ { "join_preparation" : { "select#" : 1, "steps" : [ // 中略 { "transformations_to_nested_joins" : { "transformations" : [ "semijoin" ] , "expanded_query" : "/* select#1 */ select `e`.`emp_id` AS `emp_id`,`e`.`main_floor` AS `main_floor`,`e`.`gender` AS `gender` from `employee` `e` semi join (`department` `d`) where ((`e`.`main_floor` = `d`.`main_floor`))" } } ] } } , { "join_optimization" : { "select#" : 1, "steps" : [ // 中略 { "pulled_out_semijoin_tables" : [ { "table" : "`department` `d`" , "functionally_dependent" : true } ] } , // 中略 ] } } , { "join_explain" : { "select#" : 1, "steps" : [ ] } } ] } LooseScan LooseScan 戦略は以下のような特徴を持ちます。 サブクエリ側のテーブルの結合キーのカラムにインデックスがある場合に利用可能 ※UNIQUE制約がついていれば Table Pullout も使えることになる サブクエリ側のインデックスを重複を避けながらスキャンしていく サブクエリ側が必ず駆動表になる 重複の除去を、サブクエリのインデックスを使って実現するため サンプルテーブルを用いた実行例から分かることは以下の通りです。 department 表の main_floor 列にインデックスを追加したところ、LooseScan が候補に上がるようになった UNIQUE制約をつけると Table Pullout も利用可能になる オプティマイザトレースの final_semijoin_strategy が LooseScan 実行計画の Extra 列に LooseScan と表示 EXPLAIN ANALYZE をみると、 Remove duplicates from ... という重複除去を表す情報がある ここがTable Pullout との違いになる SELECT * FROM employee e WHERE e.main_floor IN ( SELECT main_floor FROM department d ) ※ Table Pullout と同じクエリ 実行計画 mysql> explain select * from employee e where e.main_floor in ( select main_floor from department d ); +----+-------------+-------+------------+-------+---------------------------+---------------------------+---------+----------------------+------+----------+-------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------------------+---------------------------+---------+----------------------+------+----------+-------------------------------------+ | 1 | SIMPLE | d | NULL | index | department_main_floor_IDX | department_main_floor_IDX | 5 | NULL | 9 | 100.00 | Using where ; Using index ; LooseScan | | 1 | SIMPLE | e | NULL | ref | employee_main_floor_IDX | employee_main_floor_IDX | 5 | sandbox.d.main_floor | 2 | 100.00 | NULL | +----+-------------+-------+------------+-------+---------------------------+---------------------------+---------+----------------------+------+----------+-------------------------------------+ 2 rows in set, 1 warning ( 0.00 sec) Note (Code 1003 ): /* select#1 */ select `sandbox`.`e`.`emp_id` AS `emp_id`,`sandbox`.`e`.`main_floor` AS `main_floor`,`sandbox`.`e`.`gender` AS `gender` from `sandbox`.`employee` `e` semi join (`sandbox`.`department` `d`) where (`sandbox`.`e`.`main_floor` = `sandbox`.`d`.`main_floor`) mysql> explain analyze select * from employee e where e.main_floor in ( select main_floor from department d )\G *************************** 1 . row *************************** EXPLAIN : -> Nested loop inner join (actual time = 0.109 .. 0.141 rows = 18 loops= 1 ) -> Remove duplicates from input sorted on department_main_floor_IDX (actual time = 0.091 .. 0.095 rows = 9 loops= 1 ) -> Filter: (d.main_floor is not null ) (cost= 1.15 rows = 9 ) (actual time = 0.090 .. 0.093 rows = 9 loops= 1 ) -> Index scan on d using department_main_floor_IDX (cost= 1.15 rows = 9 ) (actual time = 0.089 .. 0.091 rows = 9 loops= 1 ) -> Index lookup on e using employee_main_floor_IDX (main_floor=d.main_floor) (cost= 4.70 rows = 2 ) (actual time = 0.004 .. 0.005 rows = 2 loops= 9 ) オプティマイザトレース(一部抜粋) { "steps" : [ { "join_preparation" : { "select#" : 1, "steps" : [ // 中略 { "transformations_to_nested_joins" : { "transformations" : [ "semijoin" ] , "expanded_query" : "/* select#1 */ select `e`.`emp_id` AS `emp_id`,`e`.`main_floor` AS `main_floor`,`e`.`gender` AS `gender` from `employee` `e` semi join (`department` `d`) where ((`e`.`main_floor` = `d`.`main_floor`))" } } ] } } , { "join_optimization" : { "select#" : 1, "steps" : [ // 中略 { "considered_execution_plans" : [ { "plan_prefix" : [ ] , "table" : "`department` `d`" , "best_access_path" : { "considered_access_paths" : [ { "access_type" : "ref" , "index" : "department_main_floor_IDX" , "usable" : false , "chosen" : false } , { "rows_to_scan" : 9, "filtering_effect" : [ ] , "final_filtering_effect" : 1, "access_type" : "scan" , "resulting_rows" : 9, "cost" : 1.15, "chosen" : true } ] } , "condition_filtering_pct" : 100, "rows_for_plan" : 9, "cost_for_plan" : 1.15, "semijoin_strategy_choice" : [ { "strategy" : "MaterializeScan" , "choice" : "deferred" } ] , "rest_of_plan" : [ { "plan_prefix" : [ "`department` `d`" ] , "table" : "`employee` `e`" , "best_access_path" : { "considered_access_paths" : [ { "access_type" : "ref" , "index" : "employee_main_floor_IDX" , "rows" : 2, "cost" : 6.3, "chosen" : true } , { "rows_to_scan" : 36, "filtering_effect" : [ ] , "final_filtering_effect" : 1, "access_type" : "scan" , "using_join_cache" : true , "buffers_needed" : 1, "resulting_rows" : 36, "cost" : 32.65, "chosen" : false } ] } , "condition_filtering_pct" : 100, "rows_for_plan" : 18, "cost_for_plan" : 7.45, "semijoin_strategy_choice" : [ { "strategy" : "LooseScan" , "recalculate_access_paths_and_cost" : { "tables" : [ { "table" : "`department` `d`" , "best_access_path" : { "considered_access_paths" : [ { "access_type" : "ref" , "index" : "department_main_floor_IDX" , "usable" : false , "chosen" : false } , { "rows_to_scan" : 9, "filtering_effect" : [ ] , "final_filtering_effect" : 1, "access_type" : "scan" , "resulting_rows" : 9, "cost" : 1.15, "chosen" : true } ] } , "unknown_key_1" : { "searching_loose_scan_index" : { "indexes" : [ { "index" : "department_main_floor_IDX" , "covering_scan" : { "cost" : 0.2522, "chosen" : true } } ] } } } ] } , "cost" : 7.4522, "rows" : 2, "chosen" : true } , { "strategy" : "MaterializeScan" , "recalculate_access_paths_and_cost" : { "tables" : [ { "table" : "`employee` `e`" , "best_access_path" : { "considered_access_paths" : [ { "access_type" : "ref" , "index" : "employee_main_floor_IDX" , "rows" : 2, "cost" : 6.3, "chosen" : true } , { "rows_to_scan" : 36, "filtering_effect" : [ ] , "final_filtering_effect" : 1, "access_type" : "scan" , "using_join_cache" : true , "buffers_needed" : 1, "resulting_rows" : 36, "cost" : 32.65, "chosen" : false } ] } } ] } , "cost" : 10.25, "rows" : 2, "duplicate_tables_left" : false , "chosen" : false } , { "strategy" : "DuplicatesWeedout" , "cost" : 12.05, "rows" : 18, "duplicate_tables_left" : false , "chosen" : false } ] , "chosen" : true } ] } , { "plan_prefix" : [ ] , "table" : "`employee` `e`" , "best_access_path" : { "considered_access_paths" : [ { "access_type" : "ref" , "index" : "employee_main_floor_IDX" , "usable" : false , "chosen" : false } , { "rows_to_scan" : 36, "filtering_effect" : [ ] , "final_filtering_effect" : 1, "access_type" : "scan" , "resulting_rows" : 36, "cost" : 3.85, "chosen" : true } ] } , "condition_filtering_pct" : 100, "rows_for_plan" : 36, "cost_for_plan" : 3.85, "semijoin_strategy_choice" : [ ] , "rest_of_plan" : [ { "plan_prefix" : [ "`employee` `e`" ] , "table" : "`department` `d`" , "best_access_path" : { "considered_access_paths" : [ { "access_type" : "ref" , "index" : "department_main_floor_IDX" , "rows" : 1, "cost" : 12.6, "chosen" : true } , { "access_type" : "scan" , "chosen" : false , "cause" : "covering_index_better_than_full_scan" } ] } , "condition_filtering_pct" : 100, "rows_for_plan" : 36, "cost_for_plan" : 16.45, "semijoin_strategy_choice" : [ { "strategy" : "FirstMatch" , "recalculate_access_paths_and_cost" : { "tables" : [ ] } , "cost" : 16.45, "rows" : 36, "chosen" : true } , { "strategy" : "MaterializeLookup" , "cost" : 10.5, "rows" : 36, "duplicate_tables_left" : false , "chosen" : true } , { "strategy" : "DuplicatesWeedout" , "cost" : 24.65, "rows" : 36, "duplicate_tables_left" : false , "chosen" : false } ] , "pruned_by_cost" : true } ] } , { "final_semijoin_strategy" : "LooseScan" , // 中略 } } ] } , //中略 ] } } , { "join_explain" : { "select#" : 1, "steps" : [ ] } } ] } Materialization Materialization 戦略は以下の特徴を持ちます。 サブクエリの結果を実体化して、結合キーにインデックスを作成して重複を取り除いてからメインクエリとJOINする ※結合キーのカラムにインデックスがあれば LooseScan が使えるし、UNIQUE制約がついていれば Table Pullout が使える ただし、コスト次第で他の戦略ではなくこちらが選択されることはある たとえば、サブクエリでWhere句での絞り込みをしていて、絞り込み後に実体化する Materialization の方が、LooseScan(JOIN時に絞り込みを行う)よりもコストが低くなるケースなど 作成されたインデックスのキーが <auto_key> という形で実行計画に現れることがある メインクエリ側とサブクエリ側のどちらが駆動表、内部表となるかはコストで決まる 実体化されたサブクエリ側が内部表になる場合が MaterializeLookup 実体化されたサブクエリ側が駆動表になる場合が MaterializeScan 参考: MySQL: Query Optimizer 4.MaterializeLookup (Materialize inner tables, then setup a scan over outer correlated tables, lookup in materialized table) 5.MaterializeScan (Materialize inner tables, then setup a scan over materialized tables, perform lookup in outer tables) サンプルテーブルを用いた実行例から分かることは以下の通りです。 これまでのクエリのサブクエリにWHERE句を追加したところ選択された オプティマイザトレースの final_semijoin_strategy が MaterializeScan 今回はサブクエリ側の方が駆動表になったパターン 実行計画の select_type に MATERIALIZED と表示 SELECT * FROM employee e WHERE e.main_floor IN ( SELECT main_floor FROM department d WHERE d.dept_name LIKE ' Sales% ' ) 実行計画 mysql> Explain select * from employee e where e.main_floor in ( select main_floor from department d where d.dept_name like 'Sales%' ); +----+--------------+-------------+------------+------+-------------------------+-------------------------+---------+------------------------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+--------------+-------------+------------+------+-------------------------+-------------------------+---------+------------------------+------+----------+-------------+ | 1 | SIMPLE | <subquery2> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | 100.00 | Using where | | 1 | SIMPLE | e | NULL | ref | employee_main_floor_IDX | employee_main_floor_IDX | 5 | <subquery2>.main_floor | 2 | 100.00 | NULL | | 2 | MATERIALIZED | d | NULL | ALL | NULL | NULL | NULL | NULL | 9 | 11.11 | Using where | +----+--------------+-------------+------------+------+-------------------------+-------------------------+---------+------------------------+------+----------+-------------+ 3 rows in set, 1 warning ( 0.00 sec) Note (Code 1003 ): /* select#1 */ select `sandbox`.`e`.`emp_id` AS `emp_id`,`sandbox`.`e`.`main_floor` AS `main_floor`,`sandbox`.`e`.`gender` AS `gender` from `sandbox`.`employee` `e` semi join (`sandbox`.`department` `d`) where ((`sandbox`.`e`.`main_floor` = `<subquery2>`.`main_floor`) and (`sandbox`.`d`.`dept_name` like 'Sales%' )) オプティマイザトレース(一部抜粋) { "steps" : [ { "join_preparation" : { "select#" : 1, "steps" : [ // 中略 { "transformations_to_nested_joins" : { "transformations" : [ "semijoin" ] , "expanded_query" : "/* select#1 */ select `e`.`emp_id` AS `emp_id`,`e`.`main_floor` AS `main_floor`,`e`.`gender` AS `gender` from `employee` `e` semi join (`department` `d`) where ((`d`.`dept_name` like 'Sales%') and (`e`.`main_floor` = `d`.`main_floor`))" } } ] } } , { "join_optimization" : { "select#" : 1, "steps" : [ // 中略 { "considered_execution_plans" : [ { "plan_prefix" : [ ] , "table" : "`department` `d`" , "best_access_path" : { "considered_access_paths" : [ { "rows_to_scan" : 9, "filtering_effect" : [ ] , "final_filtering_effect" : 0.1111, "access_type" : "scan" , "resulting_rows" : 1, "cost" : 1.15, "chosen" : true } ] } , "condition_filtering_pct" : 100, "rows_for_plan" : 1, "cost_for_plan" : 1.15, "semijoin_strategy_choice" : [ { "strategy" : "MaterializeScan" , "choice" : "deferred" } ] , "rest_of_plan" : [ { "plan_prefix" : [ "`department` `d`" ] , "table" : "`employee` `e`" , "best_access_path" : { "considered_access_paths" : [ { "access_type" : "ref" , "index" : "employee_main_floor_IDX" , "rows" : 2, "cost" : 0.7, "chosen" : true } , { "rows_to_scan" : 36, "filtering_effect" : [ ] , "final_filtering_effect" : 1, "access_type" : "scan" , "using_join_cache" : true , "buffers_needed" : 1, "resulting_rows" : 36, "cost" : 3.8503, "chosen" : false } ] } , "condition_filtering_pct" : 100, "rows_for_plan" : 2, "cost_for_plan" : 1.85, "semijoin_strategy_choice" : [ { "strategy" : "MaterializeScan" , "recalculate_access_paths_and_cost" : { "tables" : [ { "table" : "`employee` `e`" , "best_access_path" : { "considered_access_paths" : [ { "access_type" : "ref" , "index" : "employee_main_floor_IDX" , "rows" : 2, "cost" : 0.7, "chosen" : true } , { "rows_to_scan" : 36, "filtering_effect" : [ ] , "final_filtering_effect" : 1, "access_type" : "scan" , "using_join_cache" : true , "buffers_needed" : 1, "resulting_rows" : 36, "cost" : 3.8503, "chosen" : false } ] } } ] } , "cost" : 3.05, "rows" : 2, "duplicate_tables_left" : true , "chosen" : true } , { "strategy" : "DuplicatesWeedout" , "cost" : 3.25, "rows" : 2, "duplicate_tables_left" : false , "chosen" : false } ] , "chosen" : true } ] } , { "plan_prefix" : [ ] , "table" : "`employee` `e`" , "best_access_path" : { "considered_access_paths" : [ { "access_type" : "ref" , "index" : "employee_main_floor_IDX" , "usable" : false , "chosen" : false } , { "rows_to_scan" : 36, "filtering_effect" : [ ] , "final_filtering_effect" : 1, "access_type" : "scan" , "resulting_rows" : 36, "cost" : 3.85, "chosen" : true } ] } , "condition_filtering_pct" : 100, "rows_for_plan" : 36, "cost_for_plan" : 3.85, "semijoin_strategy_choice" : [ ] , "pruned_by_cost" : true } , { "final_semijoin_strategy" : "MaterializeScan" , // 中略 } ] } , // 中略 ] } } , { "join_explain" : { "select#" : 1, "steps" : [ ] } } ] } Duplicate Weedout Duplicate Weedout には以下の特徴があります。 JOINしてから、一時テーブルを作成して重複を取り除く メインクエリの1行に対してサブクエリの結果が複数行マッチする場合には、JOINで処理することによって重複が発生するので、それを取り除くという戦略 JOIN時にメインクエリとサブクエリのどちらが駆動表・内部表となるかはコストによって決まる(どちらともありうる) 重複の除去を最後にするので、JOINはどちらを駆動表として行ってもよいため サンプルテーブルを用いた実行例から分かることは以下の通りです。 Materialization 戦略が選択されたときのクエリをベースとして、メインクエリ側に AND で条件を足している final_semijoin_strategy が DuplicateWeedout 実行計画の Extra 列に Start temporary と End temporary と表示 EXPLAIN ANALYZE では一番上に Remove duplicate e rows using temporary table (weedout) と表示 重複の除去を一時テーブルを用いて実施している SELECT * FROM employee e WHERE e.main_floor IN ( SELECT main_floor FROM department d WHERE d.dept_name LIKE ' Sales% ' ) AND gender = 2 ; 実行計画 mysql> explain select * from employee e where e.main_floor in ( select main_floor from department d where d.dept_name like 'Sales%' ) and gender = 2 ; +----+-------------+-------+------------+------+-------------------------+-------------------------+---------+----------------------+------+----------+------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+-------------------------+-------------------------+---------+----------------------+------+----------+------------------------------+ | 1 | SIMPLE | d | NULL | ALL | NULL | NULL | NULL | NULL | 9 | 11.11 | Using where ; Start temporary | | 1 | SIMPLE | e | NULL | ref | employee_main_floor_IDX | employee_main_floor_IDX | 5 | sandbox.d.main_floor | 2 | 10.00 | Using where ; End temporary | +----+-------------+-------+------------+------+-------------------------+-------------------------+---------+----------------------+------+----------+------------------------------+ 2 rows in set, 1 warning ( 0.00 sec) Note (Code 1003 ): /* select#1 */ select `sandbox`.`e`.`emp_id` AS `emp_id`,`sandbox`.`e`.`main_floor` AS `main_floor`,`sandbox`.`e`.`gender` AS `gender` from `sandbox`.`employee` `e` semi join (`sandbox`.`department` `d`) where ((`sandbox`.`e`.`main_floor` = `sandbox`.`d`.`main_floor`) and (`sandbox`.`e`.`gender` = 2 ) and (`sandbox`.`d`.`dept_name` like 'Sales%' )) mysql> explain analyze select * from employee e where e.main_floor in ( select main_floor from department d where d.dept_name like 'Sales%' ) and gender = 2 \G *************************** 1 . row *************************** EXPLAIN : -> Remove duplicate e rows using temporary table (weedout) (cost= 1.85 rows = 0 ) (actual time = 0.051 .. 0.062 rows = 2 loops= 1 ) -> Nested loop inner join (cost= 1.85 rows = 0 ) (actual time = 0.047 .. 0.057 rows = 2 loops= 1 ) -> Filter: ((d.dept_name like 'Sales%' ) and (d.main_floor is not null )) (cost= 1.15 rows = 1 ) (actual time = 0.027 .. 0.030 rows = 2 loops= 1 ) -> Table scan on d (cost= 1.15 rows = 9 ) (actual time = 0.022 .. 0.025 rows = 9 loops= 1 ) -> Filter: (e.gender = 2 ) (cost= 0.52 rows = 0 ) (actual time = 0.011 .. 0.012 rows = 1 loops= 2 ) -> Index lookup on e using employee_main_floor_IDX (main_floor=d.main_floor) (cost= 0.52 rows = 2 ) (actual time = 0.011 .. 0.012 rows = 2 loops= 2 ) オプティマイザトレース(一部抜粋) { "steps" : [ { "join_preparation" : { "select#" : 1, "steps" : [ // 中略 { "transformations_to_nested_joins" : { "transformations" : [ "semijoin" ] , "expanded_query" : "/* select#1 */ select `e`.`emp_id` AS `emp_id`,`e`.`main_floor` AS `main_floor`,`e`.`gender` AS `gender` from `employee` `e` semi join (`department` `d`) where ((`e`.`gender` = 2) and (`d`.`dept_name` like 'Sales%') and (`e`.`main_floor` = `d`.`main_floor`))" } } ] } } , { "join_optimization" : { "select#" : 1, "steps" : [ // 中略 { "considered_execution_plans" : [ { "plan_prefix" : [ ] , "table" : "`department` `d`" , "best_access_path" : { "considered_access_paths" : [ { "rows_to_scan" : 9, "filtering_effect" : [ ] , "final_filtering_effect" : 0.1111, "access_type" : "scan" , "resulting_rows" : 1, "cost" : 1.15, "chosen" : true } ] } , "condition_filtering_pct" : 100, "rows_for_plan" : 1, "cost_for_plan" : 1.15, "semijoin_strategy_choice" : [ { "strategy" : "MaterializeScan" , "choice" : "deferred" } ] , "rest_of_plan" : [ { "plan_prefix" : [ "`department` `d`" ] , "table" : "`employee` `e`" , "best_access_path" : { "considered_access_paths" : [ { "access_type" : "ref" , "index" : "employee_main_floor_IDX" , "rows" : 2, "cost" : 0.7, "chosen" : true } , { "rows_to_scan" : 36, "filtering_effect" : [ ] , "final_filtering_effect" : 0.1, "access_type" : "scan" , "using_join_cache" : true , "buffers_needed" : 1, "resulting_rows" : 3.6, "cost" : 3.8541, "chosen" : false } ] } , "condition_filtering_pct" : 10, "rows_for_plan" : 0.2, "cost_for_plan" : 1.85, "semijoin_strategy_choice" : [ { "strategy" : "MaterializeScan" , "recalculate_access_paths_and_cost" : { "tables" : [ { "table" : "`employee` `e`" , "best_access_path" : { "considered_access_paths" : [ { "access_type" : "ref" , "index" : "employee_main_floor_IDX" , "rows" : 2, "cost" : 0.7, "chosen" : true } , { "rows_to_scan" : 36, "filtering_effect" : [ ] , "final_filtering_effect" : 0.1, "access_type" : "scan" , "using_join_cache" : true , "buffers_needed" : 1, "resulting_rows" : 3.6, "cost" : 3.8541, "chosen" : false } ] } } ] } , "cost" : 3.05, "rows" : 0.2, "duplicate_tables_left" : true , "chosen" : true } , { "strategy" : "DuplicatesWeedout" , "cost" : 2.89, "rows" : 0.2, "duplicate_tables_left" : false , "chosen" : true } ] , "chosen" : true } ] } , { "plan_prefix" : [ ] , "table" : "`employee` `e`" , "best_access_path" : { "considered_access_paths" : [ { "access_type" : "ref" , "index" : "employee_main_floor_IDX" , "usable" : false , "chosen" : false } , { "rows_to_scan" : 36, "filtering_effect" : [ ] , "final_filtering_effect" : 0.1, "access_type" : "scan" , "resulting_rows" : 3.6, "cost" : 3.85, "chosen" : true } ] } , "condition_filtering_pct" : 100, "rows_for_plan" : 3.6, "cost_for_plan" : 3.85, "semijoin_strategy_choice" : [ ] , "pruned_by_cost" : true } , { "final_semijoin_strategy" : "DuplicateWeedout" } ] } , // 中略 ] } } , { "join_execution" : { "select#" : 1, "steps" : [ ] } } ] } FirstMatch FirstMatch 戦略には以下の特徴があります。 通常のNLJ(Nested Loop Join)に似ているが、内側の(サブクエリ側の)ループを回しているときに1行でも見つかったら即座に内側のループを打ち切って外側のループの次の周回に進むことができるため効率が良い メインクエリ側が必ず駆動表になる 重複の除去を、内側のループを途中で打ち切ることによって実現しているため サンプルテーブルを用いた実行例から分かることは以下の通りです。 これまでのクエリとは違い、department表へのクエリをメインクエリとしている これまでは、サブクエリのテーブルの方が小さかったが、今回のクエリはメインクエリのテーブルの方が小さい オプティマイザトレースの final_semijoin_strategy が FirstMatch 実行計画の Extra 列に FirstMatch(department) の表示がある SELECT * FROM department WHERE department.main_floor IN ( SELECT main_floor FROM employee ); 実行計画 mysql> explain select * from department where department.main_floor in ( select main_floor from employee ); +----+-------------+------------+------------+------+-------------------------+-------------------------+---------+-------------------------------+------+----------+-------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+------------+------------+------+-------------------------+-------------------------+---------+-------------------------------+------+----------+-------------------------------------+ | 1 | SIMPLE | department | NULL | ALL | NULL | NULL | NULL | NULL | 9 | 100.00 | Using where | | 1 | SIMPLE | employee | NULL | ref | employee_main_floor_IDX | employee_main_floor_IDX | 5 | sandbox.department.main_floor | 2 | 100.00 | Using index ; FirstMatch(department) | +----+-------------+------------+------------+------+-------------------------+-------------------------+---------+-------------------------------+------+----------+-------------------------------------+ 2 rows in set, 1 warning ( 0.00 sec) Note (Code 1003 ): /* select#1 */ select `sandbox`.`department`.`dept_id` AS `dept_id`,`sandbox`.`department`.`main_floor` AS `main_floor`,`sandbox`.`department`.`dept_name` AS `dept_name` from `sandbox`.`department` semi join (`sandbox`.`employee`) where (`sandbox`.`employee`.`main_floor` = `sandbox`.`department`.`main_floor`) mysql> explain analyze select * from department where department.main_floor in ( select main_floor from employee )\G *************************** 1 . row *************************** EXPLAIN : -> Nested loop semijoin (cost= 5.20 rows = 18 ) (actual time = 0.034 .. 0.050 rows = 9 loops= 1 ) -> Filter: (department.main_floor is not null ) (cost= 1.15 rows = 9 ) (actual time = 0.022 .. 0.026 rows = 9 loops= 1 ) -> Table scan on department (cost= 1.15 rows = 9 ) (actual time = 0.022 .. 0.025 rows = 9 loops= 1 ) -> Index lookup on employee using employee_main_floor_IDX (main_floor=department.main_floor) (cost= 0.54 rows = 2 ) (actual time = 0.002 .. 0.002 rows = 1 loops= 9 ) オプティマイザトレース(一部抜粋) { "steps" : [ { "join_preparation" : { "select#" : 1, "steps" : [ // 中略 { "transformations_to_nested_joins" : { "transformations" : [ "semijoin" ] , "expanded_query" : "/* select#1 */ select `department`.`dept_id` AS `dept_id`,`department`.`main_floor` AS `main_floor`,`department`.`dept_name` AS `dept_name` from `department` semi join (`employee`) where ((`department`.`main_floor` = `employee`.`main_floor`))" } } ] } } , { "join_optimization" : { "select#" : 1, "steps" : [ // 中略 { "considered_execution_plans" : [ { "plan_prefix" : [ ] , "table" : "`department`" , "best_access_path" : { "considered_access_paths" : [ { "rows_to_scan" : 9, "filtering_effect" : [ ] , "final_filtering_effect" : 1, "access_type" : "scan" , "resulting_rows" : 9, "cost" : 1.15, "chosen" : true } ] } , "condition_filtering_pct" : 100, "rows_for_plan" : 9, "cost_for_plan" : 1.15, "semijoin_strategy_choice" : [ ] , "rest_of_plan" : [ { "plan_prefix" : [ "`department`" ] , "table" : "`employee`" , "best_access_path" : { "considered_access_paths" : [ { "access_type" : "ref" , "index" : "employee_main_floor_IDX" , "rows" : 2, "cost" : 4.0525, "chosen" : true } , { "access_type" : "scan" , "chosen" : false , "cause" : "covering_index_better_than_full_scan" } ] } , "condition_filtering_pct" : 100, "rows_for_plan" : 18, "cost_for_plan" : 5.2025, "semijoin_strategy_choice" : [ { "strategy" : "FirstMatch" , "recalculate_access_paths_and_cost" : { "tables" : [ ] } , "cost" : 5.2025, "rows" : 9, "chosen" : true } , { "strategy" : "MaterializeLookup" , "cost" : 10.5, "rows" : 9, "duplicate_tables_left" : false , "chosen" : false } , { "strategy" : "DuplicatesWeedout" , "cost" : 8.9025, "rows" : 9, "duplicate_tables_left" : false , "chosen" : false } ] , "chosen" : true } ] } , { "plan_prefix" : [ ] , "table" : "`employee`" , "best_access_path" : { "considered_access_paths" : [ { "access_type" : "ref" , "index" : "employee_main_floor_IDX" , "usable" : false , "chosen" : false } , { "rows_to_scan" : 36, "filtering_effect" : [ ] , "final_filtering_effect" : 1, "access_type" : "scan" , "resulting_rows" : 36, "cost" : 3.85, "chosen" : true } ] } , "condition_filtering_pct" : 100, "rows_for_plan" : 36, "cost_for_plan" : 3.85, "semijoin_strategy_choice" : [ { "strategy" : "MaterializeScan" , "choice" : "deferred" } ] , "pruned_by_heuristic" : true } , { "final_semijoin_strategy" : "FirstMatch" , "recalculate_access_paths_and_cost" : { "tables" : [ ] } } ] } , // 中略 ] } } , { "join_execution" : { "select#" : 1, "steps" : [ ] } } ] } 終わりに 以上、セミジョイン最適化について、個々の戦略の内容を含めて書いてきました。サンプルテーブルを用いて取得した実行計画やオプティマイザトレースはあくまで一例に過ぎず、少し条件を変えるとまた違った結果が得られるかもしれません。冒頭にも書いたように、記載している内容に誤りなどを見つけた場合は 筆者のTwitter までご一報いただけると幸いです。 参考資料 公式 5.6 8.2.1.18 サブクエリーの最適化 8.2.2.1 Optimizing Subqueries with Semijoin Transformations 8.2.2.2 Optimizing Subqueries with Materialization 8.8.5.2 切り替え可能な最適化の制御 8.0 8.2.2.1 Optimizing IN and EXISTS Subquery Predicates with Semijoin Transformations 8.9.3 オプティマイザヒント MySQL 8.0.30 Source Code Documentation その他 MySQL道普請便り 第43回 MySQLの準結合(セミジョイン)について MariaDB 10.6 [日本語] 最適化とチューニング セミジョイン副問い合わせの最適化 INとEXISTSはどちらが速いのか? MySQL のサブクエリって、ほんとに遅いの? MySQL 8.0.21 では Multi-Table Trick が必要なくなったらしい MySQL道普請便り第103回MySQL 8.0のセミジョインの変更点
介護職向け求人情報サイト「カイゴジョブ *1 」の開発をしている児玉卓也です。 弊社の プレスリリース でお知らせしているように、カイゴジョブは2022年5月にデザインリニューアルを行いました。 デザインリニューアルにはサイトのコンセプトなどはそのままで、UIなどの見た目を部分だけを変更するものから、コンセプトやユーザーシナリオというサービスの基盤となる部分から再設計し直すことがあるかと思いますが、カイゴジョブが行ったデザインリニューアルは後者の方で、カイゴジョブにおける求職者の体験をより良くするためにコンセプト・ユーザーシナリオをアップデートした上でデザインを全面的に変更しました。 全面リニューアルとなるとその影響範囲は大きく、特にプロダクトの品質を保ちつつ大規模リリースをすることは、経験された方にはわかると思いますが大変な作業となります。今回のデザインリニューアルでは40,000行、900を超えるファイル数の変更となり、我々が日々行っているリリースの数百倍くらい大きなリリースとなりました。 デザインリニューアル時のPRの変更行数 本記事では私達が大規模リリースをしなければならなかった理由を説明した後に、どのような開発手法を用いて大規模なリリースを安全に行えたかについて説明したいと思います。 デザインリニューアルの背景 環境の変化が激しく不確実性が高まる時代ではありますが、介護業界も例に漏れず変化が激しい業界です。カイゴジョブも市場の変化やユーザーニーズの変化に合わせて新機能を追加するなど求職者への価値提供を追求してきました。そのような中でビジネスモデルの変革を進めていくこととなりました。 ビジネスモデルが変わったことで求職者に期待したい行動やサイト上でのゴールも変わりました。しかし、サイト全体の体験は2019年に設計したものであったため、機能の開発をする際に既存の導線との接続であったり、体験の整合性をサイト全体と合わせるのが困難と感じることが増えてきました。 その問題を解決するためにデザインリニューアルを行い新しいビジネスモデルにそった体験を提供することとなりました。 アジャイルなビックバンリリース 前述したように今回のデザインリニューアルではサービスの基盤となるコンセプトから作り直していることもあって、画面単位、コンポーネント単位に分けてリリースをしてしまうと全体の動線の中で体験の整合性が合わなくなってしまうので、全画面の開発完了後に一斉にリリースする必要がありました。 このような大規模なリリースをしようとする際に、開発工程が完全に完了してからQAテストを行うとなると、バグが見つかったときの手戻りのコストが大きく、また、バグの度合いによってはリリース時期の延期をせざるを得ない状況となってしまいます。そのような事態になることは避けたかったので、開発工程と並行してテストを行えるようなプロセスで進めていきました。 このプロセスで開発をすすめる上で、タスクの作成方法と優先順位付けがポイントとなります。 テスト可能な単位でタスクを作成 まず、タスクの作成方法ですが、QAエンジニアによるテストの実行が可能となる単位でタスクを作成していきます。 私達の開発チームはエンジニア、デザイナー、QAエンジニアが1つのチームとなって活動しており、日々の活動の共有に加えて、チームの活動のふりかえり、さらに実装前のフェーズである要件定義なども一緒に行い、密にコミュニケーションをとるチームであることが特徴です。 デザインリニューアルのプロジェクトにおいても、普段の開発と同様に要件定義やユーザーストーリー作成のフェーズからQAエンジニアに参加してもらい、何を作らなければならないかを対話しながら具体化し、共通理解を深めながらタスクを作成し管理します。QAエンジニアは受け入れ条件を開発前から把握できるため、開発と並行してテストシナリオの作成などテスト実行に向けた準備が可能となります。 また、QAエンジニアが要件定義の場にいることで、仕様に抜け漏れがないか、不具合が入り込む余地がないかなど、QAエンジニアの観点で提案をもらうことができ、開発着手する前に不具合に気づくこともできました。 テストの優先度の高いものは早めに着手する デザインリニューアルのプロジェクトでは事業の影響度、開発コストなどの基準に加え、テスト優先度を含めて優先順位を決定し、開発・テストの取捨選択をしていきました。 基本的に事業への影響度が高いものはテストの優先度も高くなりますが、テストを効率よく進めるために以下の観点も重要視しています。 テストボリュームが多くなりそうな改修であるか 仕様が複雑そうな改修であるか テストの優先度が高いものは不確実性が高く、早期に解消しないとリリースブロックとなる可能性もあるので、優先順位付けをする中でも重要な基準となります。 副次的効果 開発・テストのサイクルを繰り返していくプロセスの副次的効果として、バグの修正が容易であることが挙げられます。というのも、開発完了からテストのフィードバックまでのリードタイムが短いため、バグ修正の際もコンテキストや実装内容の詳細の記憶を思い出す労力を多くかけずに対応できるからです。 おわりに 今回、私達は開発とテストの工程を並行するプロセスを採用し、デザインリニューアルプロジェクトを進めてきましたが、はじめからこの開発手法を確立できていたわけではありません。日々のプロセスの中で上手くいったことや課題に感じたことをチームでふりかえりながらプロセスを最適化してきました。 デザインリニューアルを終えた後もプロジェクトのふりかえりを行い、QAエンジニアが実施したシナリオテストを自動テストに落とし込んでいきたいなど課題が見つかり、今後も継続的に改善していきたいと思っています。 最後になりますが、デザインリニューアルによってカイゴジョブは求職者にとってより使いやすいサービスとなりました。しかし、求職者に提供できる価値はまだまだたくさんあると感じています。今後も求職者の圧倒的な不足という社会的な課題を解決するために、プロダクトの価値を高めていきたいと思っていますので、興味のある方はぜひお話を聞きに来てください! *1 : カイゴジョブは介護業界に特化した求人情報サイトで、2004年より展開しているサービスです。利用者はカイゴジョブのサイト上で求職者の希望に近い求人を検索し応募できることに加え、電話での求人検索や、応募手続きができます。 介護特有のこだわり条件で求人を探せることなど、介護に特化した求人情報サイトであることが特徴の1つでもあります。