かっこうのブログ

何かしら飲んでるエンジニア

「ライト、ついてますか―問題発見の人間学」理想と現実と問題解決の副作用

adventar.org

一人アドベントカレンダー2日目です

問題とは何かから、前提のリフレーミング、別の視点で見方まで色々記されているこの本だが、一番面白いのはやはり語り口調だろう。他の書籍だと難しく書かれているリフレーミングや視点の変え方の方法を、面白みのある具体例で表現している。あまりに面白み(現実味のなさ)が強いため、読み終わった後に自分の身に起きたこと・起きていることで考えてみるとしっくりくる作りになっている。

個人的に好きだなーと感じたところをつまんで書いていきます。

問題とはなにか

本書では問題を以下のように定義している。

問題とは、望まれた事柄と認識された事柄の間の相違である ~ 3章より

これの良いところは、問題を「望まれた事柄」と「認識された事柄」に分解できることにある。「望まれた事柄」とは何なのか、本当に望んでいるのは何なのかと、分解されていることで事柄を多角的にみることがしやすくなる。

また、「認識された事柄」も分解されることで、それがどのように問題なのか・そもそも問題なのかということを考えることができるようになる。

問題について考える

私はそれを本当に解きたいか? ~ 20章より

問題について考える時、最初に考えるべき問いが上記だ。

あなたが本当にそれを問題に思えるか・腹おちして考えることができるかだ。例えば発生頻度が1年に1回の問題を解きたいと思うだろうか、その時にとても面倒臭い対応を迫られるならば解きたいと思うだろう。

また、その問題が顧客に対して利益が生じるものであるならば、自分が顧客になった時その問題を解決されたいかを考えるのも重要になる。様々な視点から「その問題に解く価値があるか」を考えてから問題に向き合わなければ不毛な解答を作るだけになってしまう。

問題は誰の問題か・またはどこからきたのか ~ 4&5部

問題が発生する時、様々な人がそこにはいる。問題を生んでいる人・問題の被害を受けている人・問題を見逃している人・問題に気づいていない人などだ。

問題を解こうとするときは、それらの人々に対して「私たちの問題だ」と実際に問題を味わい・皆で問題を解くといい。

誰かの問題だと棚上げすれば不満が出るのは当然だ、それを私たちの問題として全員で解決していく。この辺りは現代の開発の「個人ではなく、システムやフローの問題にする」という全体最適に通じるところだろう。

解決が問題を生む

すべての解答は次の問題の出所 ~ 7章より

エンジニアならばとても実感することだろう。

もし「これから1ヶ月一切のバグを出さないでくれ」と言われたらどうするだろう。「でしたら新機能のリリースを1ヶ月やめます」というだろう。私もそうする。

問題を解決するとき、その副作用がどのように生じるかを認識することは非常に重要である。

もし新しい機能を追加したとき、あなたはどのような副作用が思いつくだろう?

  • 新しい機能がバグを内包しているかもしれない
  • 新しい機能によって、画面の動線がわかりづらくなるかもしれない
  • 機能が増え、新しいユーザーにとっては情報がわかりづらいかもしれない
  • 機能が増えたことで、コードの依存関係が強くなっているかもしれない

上げ出せばもっと出てくる。この時、重要になるのは「解決策と付き合わなければならない人間に合う解決策」にすることだ。どんな解決策であれ、割を食うのは、設計者より利用者なのだ。

問題解決の沼にどっぷり浸かった人同士で話し合うより、問題を知らない人・実際に問題を体験している人に聞く方が良い解決策に巡り合えるだろう。

「SOFT SKILLS 」私がブログを始めたきっかけ

adventar.org

一人アドベントカレンダー1日目🎉🎉🎉

ということで、ブログを始めるきっかけとなった書籍「SOFT SKILLS」について書いていきます。

サブタイトルの通り、この書籍はエンジニアの人生設計においてキャリアの高め方や働き方・生産性といったことに留まらずお金についてのことや投資・リタイア後の計画、体の大切さが記されています。

投資や老後の計画・体の大切さについては別の書籍に譲るとして、今回は本書で述べられているブログの利点と実際にブログを書いていてよかったことを紹介していきます。

自分をマーケティングする

この書籍で特に面白いと感じたのが、自分自身を1つの事業として捉えるという考え方。すなわち、自分で自分自身のブランドを定義し、キャッチフレーズやエレベーターピッチを作成し、周囲に価値を提供・アピールしていくことが重要とする考え方だ。

また、ブログを書く前に、自分の持っているスキルや興味などを棚卸し、どんな価値をブログを通して提供できるかを考え、実行計画も考え執筆を続けていくのが良いとされている。(私は勢いで始めたので散発的に書いている)

学ぶために教える

「学習を定着させるには、教えることが重要」というのは昨今ラーニングピラミッドとともに広まってきている。この書籍でもそのことからブログが進められている。

弟子を取るというのもあるが、まずはブログから始めるのがプレッシャーも少なく・簡単に人に教えることができ良いとされる。

ブログを始めてどうだったか

というわけで「自分のマーケティング「学習の促進」のためにこのブログを始めた。

クエリの実行計画などのニッチな話や、読んでいる書籍など個人の趣向が出るものを書くようにしている。それ以外の簡単な記事や作ってみたような記事はQiitaの方に譲っている。

「自分のマーケティングの効果だが、まだ1年ほどでポスト数も少ないものの、転職時の面接で「ブログを拝見して...」とお声かけいただくこともたまにあり意外とその効果の高さを感じている。

また、Qiitaと異なりHatena Blogはアクセス履歴でどのような経路でどの記事にアクセスしたかが見れるため、どのようなテーマが需要があるのかも確認しやすいのも利点だ。

「学習の促進」だが、こちらの方が圧倒的に利益を感じる。実際に記事にする際に、体感ではなくドキュメントの確認と実際に動作確認をすると新しい発見や「この場合どうなるんだ?」という新しい学習が得られる。

何より一度そこまで深掘りしたことは忘れない。そして深掘りしている分、その分野については議論もできるし、新しい知識の獲得もしやすい。

総括

ブログはいいぞ。 本当はブログを書くにあたってもニッチを攻めるなど、この書籍はさまざまなマーケティング方法が示されています。ただ、これらを決めてから始めるよりも「とりあえずやってみる」でブログを始めて走りながら方向を決めることをお勧めします。

また、この書籍自体はブログについてはごく一部で、人生についての諸事について語られています。是非、読む本がなくなってきたな・キャリアについて考えたい人は読んでみると学びになると思います。

「the deadline」現代でも通じるプロジェクト管理の物語


この本を読み終えて、いや読み進めていく中で「この本が1999年に出ていたのか…」と圧倒された。そして、1999年の良書に書かれている内容で知らないことが多くあったことに悔しさ、というか「もっと先に知りたかった」という気持ちが常にあった

この話は組織の心理的安全性に始まり、採用・登用するときのプラクティス・プロジェクトで何を管理するかの提言・人月の神話が起こる理由の解明・残業やプレッシャーの無意味さ・プランニングポーカーのようなタスクのポイント化・設計の重要性・MTGの無くし方、最後に「プロジェクトにおいての目標と予想」が語られて終わる。

スクラムなどで用いられるタスクのポイント化、GoやTypescriptで取り入れられている型、最近ではMTGの減らし方についてShopify CEOが語っていたことも形は違うが、この書籍で語られている。

この本には101もの項目が書かれている。個人的に好きな話・気になった項目をいくつか書いていく。

プロジェクトの成功の評価は、プロジェクトの出来だけではない

この本で最も好きな学びが「プロジェクトが成功することの価値は、良いチームができることである」ということだ。

もちろん、良いチームにならずプロジェクトが成功することもある。そのため「開発したソフトの出来だけで評価されるのではなく、次のプロジェクトもやりたいという結束した良いチームが少なくとも1つできたかどうかも評価されるべきだ」と語られている。

実際に同じメンバーで複数のプロジェクトを合計2年したことがあるが、1つ目のプロジェクトが成功した時点で「この人はこういうことを考えているだろう」「この人にはこういうことを言う方がやる気が出るだろう」「ここはこの人に任せて大丈夫」など信頼し、2つ目の大きなプロジェクトでは1つ面のプロジェクトより更に真っ向から話し合いプロジェクトを成功させることができた。

ただ、当時の私はそれを「団結力があがった。理解できるようになってきたな」くらいで捉え、そこまで大きな価値があるものだと思っていなかった。

普段ふりかえりを実施したりチーム作りをすることが多いが「プロジェクトでは進捗だけでなく、チームを作るためにも行動すべきだ」と改めて理解することができたのは大きな収穫だった。

怒りとは不安の表れである

不安な時、人はそのストレスを解放しようとする。その解放で、最も現れやすいのが「怒り」と言われている。だからこそ、怒っている人の不安を解消するのは大きな価値があるというのだ。

確かに怒られる時と言うのは大抵「炎上プロジェクト」と呼ばれるような無理難題が降ってきたときだったなと今になって思う。

個人的にはエンジニアは感情を殺すのがうまいため「怒り」と言う形でさえ出てくることが少ない気がしている。そのため、怒っていれば本当にサポートが必要な場面なのだろう。

優れたプロジェクトは、設計に費やす時間の割合がはるかに高い

これは現代のアジャイル的なモノづくりとは異なるところ。

コーディングをできる限り遅らせ、プロジェクトの中間の40%かそれ以上をかけ完璧にコードと対応できるような低レベルの詳細設計まで入念に行うと言うのだ。

また、テストが行われれば、ほとんどのテストがパスされる。そのレベルの設計をするというのだ。これは「包括的なドキュメントよりも動くソフトウェアを」の理念に反するところではある。

ただ、何らかの方法でこの考えを現代のスクラムにも持ち込めないかと考えている。

正直技術力が高くないと思っているので、納得のいく答えはまだ出ていない。

「バグというのはコードの中心ではなく、末端にある。すなわちインターフェースの境目にあることが多い」という台詞は現代にも通じるところがある。DIを用いることでインターフェースという境界を明らかにしつつ、バグが少ないとされるコア部分はある程度柔軟な設計をおこなうことができるだろう。

また、「コーディングしながら設計をしてはいけない」というセリフもモブやペアプロで最初に話し合い設計を行うなどで対応できるだろう。

ただ、もっと良い方法があると思っているので今後考えて行きたいと思う。

総括

発売は1990年と古いが、現代にも続くさまざまなプロジェクト管理の問題を紐解いている。

TDDやアジャイルなど開発サイクルとは一致しないところはあるものの、全体感を見ていく上では非常に有意義な本だと感じた。実際、「人を増やして開発速度を増やせ」と上から言われている人に「この本の10章を読んで伝えたら?」非常に感謝された。それほどまでに、この本は現代でも通じることがわかりやすく記載されている。

かなり書いたはずなのに、まだ90以上の項目がこの本に残っているというのが恐ろしい…

本のストーリーも「ほぼ嵌められて大企業のプロジェクト管理をさせられる様になった人間が頑張る話」と少し異世界転生のような雰囲気を醸し出しており、気軽に読めるのも魅力なところだ。

「速読思考」インプットとアウトプットを早くする方法論

最近積読がめちゃくちゃ増えており速読を身につけようと思い幾つもの速読本からなんとなくで手に取ったのがこの本でした。

ただ、この本は「速読」だけでなく、なぜ速読をするのかどのように速読を活かすのかが語られており、非常にハッとさせられることが多かったので記事にしたくなった次第です。

そんな「なぜ速読をするのか」からインプットからアウトプットを早くする方法論が書かれているのがこの速読思考です。

なぜ速読をするのか

この本で最初に語られる項目が「なぜ速読をするのか」なのがとても良いなと感じる。

そもそも速読をすることの目的は、「本を早く読むこと」ではなく「本で得たノウハウを早く活かすこと」にある。

私も例に漏れず目的と手段が逆転した状態でこの本を読み始めたので、改めてこういう指摘をしてくれる本に出会えて幸運だったと感じます。

そう。速読とは、「本で得たノウハウを活かす」の「本で得た」の部分のリードタイムを短くするためにあるのだ。

速読で身につく力

速読で身につくのは、単純に本を読むのが早くなるだけではないらしい。

思考力や判断力の向上にもつながるという。 速読では1ページで多くのページをみるため、そのページから重要な情報を取得する・頻出単語を拾っていくなど様々なことを高速で行う必要がある。 これが思考力や判断力の速度向上につながっていくというのだ。

実際、本書に書いてある速読方法を使って本を見たところ非常に疲れる。目ももちろん疲れるが、脳の疲れを非常に感じました。

速読とは

ここまで速読の方法について触れてこなかったので、軽く触れておきます。

1行ずつ「読む」のではなく1ページや1ブロックとして「見る」こと。 最初は頭に全く入ってこないがそのくらいの速度で読むのが良いらしい。 高速道路と同じで、脳は速度に対応していく「早く見る」を繰り返すことで着実に頭に入っていくように脳が学習していくとのこと。

本書には、この方法論についてどのような段階を踏むのか、速読の練習のためのページなどもあるがそこは速読がしたい人は買ってみてください。

速読とFB

本書籍では速読をする代わりに1冊を複数回読むことを推奨しています。 その理由はいくつか述べられていましたが、個人的に特に良さそうに感じたのは以下でした。

本に書いてあることをとりあえずやってみる。

そして試した後に本を読んでいく。そうすることで、感度が高くなり、最初の1回や2回では気づけなかった文章が見えるようになっている。

この現象は「本棚で興味のある分野の本だけ視界に入る」という現象に近い。実際はカクテルパーティ効果というらしい。

また、「とりあえずやってみる」という行動を読書とセットにすることでアウトプットを強制しやすくなる。また、試してみて「違った」というFBも早期に得ることができる。 また、実際に試すことでカクテルパーティ効果で視界に入ってきやすい情報が増え効率的に本が読めるようになっていくのだろう。

組織論系の本などは読んで、実際に組織に適用させようと考えた時に「この本の内容は今はまだ早いな」ということが多いので、早い段階でFBを得て「この本はまだ早い」と見切りをつけることができたり、ステップアップの方法を探しながら読み直せるのはとても効率が良さそうに感じる。


速読思考とは「PDCAを爆速で回す方法」に近い気がしている。本のインプットからアウトプットを最短にし、アウトプットからまたFBを得て再び本から学び直す。 自分の必要情報を必要な時に素早く学習できる。それが速読なんだろうと感じた一冊でした。

頭の中でMySQLのTreeを描けるようになろう

動機

業務でIndexがわからないと辛いということが多々あったので一旦BTreeを描くことができるようになろうとなりました。

結論から言うと、以下の動画がとても参考になりました。この研修を公開してくださったサイボウズさんありがとうございます。

https://www.youtube.com/watch?v=4Zj7Qgvt7RE

文字の方が得意という方は、こちらの記事も良いと思います。

SQLインデックスのリーフノード

SQLデータベースにおけるバランス検索木(Bツリー)

要点まとめ

以下は自分の認識を深めるために、MySQLのサンプルテーブルであるWorldを使って色々遊んだ備忘録になります。

MySQL :: Other MySQL Documentation

ブランチノードとリーフノードがある

country(以下のようなデータ)で、主キーがCode、NameにIndexが貼られている場合Indexのツリーを描いていきます。

まず、ルートノードがあります。

次にNameのブランチノードができます。ブランチノードとは、Indexの値を範囲で取ったノードです。例えばWhere Name = Japanで検索する場合、Japan : Kenyaのブランチノードの中身のみを見れば良いのでとても効率が良くなります。

Japan:Kenyaのブランチノードに紐づくリーフノードを描くとこのようになります。

IndexのリーフノードはPrimaryKeyの値を持つためこのような形になります。

そのため、以下のようにNameが特定の値と一致する全ての情報が欲しい場合は一旦Indexに対してクエリを発行し、PrimaryKeyを取得して再度全情報を取得するクエリを発行することになります。

SELECT * FROM `country` WHERE `Name` = 'Japan'; 
//*なので、IndexのTreeだけでは欲しい情報が得られない

1. SELECT * FROM `country_index_name` WHERE `Name` = 'Japan'; 
-> JPN
2. SELECT * FROM `country` WHERE `Code` = "JPN";

一方で、全ての情報ではなく、Codeが欲しいだけの場合Indexに対するクエリだけで要望を満たすことができます。Indexのみで情報の取得ができることをカバリングインデックスと呼ばれます。

複合インデックスがある場合、順序が重要になる

では、複合インデックスの場合どのようなTreeになるかを考えていきましょう。

cityテーブルに、都市の地域と都市名で複合Indexを貼ります(日本だけで言えば、市名の重複を避けるようになっています)。

日本列島「地名」をゆく!:ジャパンナレッジ 第61回 同一市名あれこれ(1)

alter table `city` add index `multiple_idx` (`Name` , `District`);

この場合、Indexのツリーは以下のようになります。複合キーがある分ブランチノードが増えています。

ここで、以下のクエリを実行するとこの順序で読み取りが行われます。

SELECT * FROM city 
WHERE Name = "Cartagena"
AND District = "Bolivar"

少し横に逸れてカーディナリティの差によるデータの読み込み量についてみてみましょう。

今回の例はカーディナリティにあまり差はありませんが、複合キーの順序を逆にしてみます。

alter table `city` add index `multiple_idx` (`District` , `Name`);

リーフノードが1つ増えています。

もっとカーディナリティに差があれば、カーディナリティが低いものを前に持ってくるとIndexを使っても参照範囲があまり減らないことが分かりやすいでしょう。

  1. 複合プライマリキーの場合、どのようなB木になるのだろう

最小、最大に対応するリーフノードがある

ブランチノードとリーフノードのところでは描きませんでしたが、概念的にはリーフノードには最小と最大があり、リーフノード間にはGAPというものがあります。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 14.2.6 InnoDB のレコード、ギャップ、およびネクストキーロック

UPDATEやDELETEクエリ・FOR UPDATEを単なるユニークなレコードへ使用した場合問題にはなりませんが、非ユニークな要素に対してクエリを実行すると前後のGAPもロックが施されます。

実際に試してみましょう。

countryのNameにINDEXを貼り、以下のクエリを実行します。この時点では、NameにUNIQUE制約がないため前後のGAPもロックされます。

begin;

SELECT *FROM `country`
WHERE `Name` = "Japan"
FOR UPDATE;
-- 当然ながらLock wait timeout exceeded; try restarting transaction
UPDATE country SET IndepYear = "-600" WHERE Name = "Japan";

-- japanの次のGAPに入れようとするためLock wait timeout
INSERT INTO `country` (`Code`, `Name`, `Continent`, `Region`, `SurfaceArea`, `IndepYear`, `Population`, `LifeExpectancy`, `GNP`, `GNPOld`, `LocalName`, `GovernmentForm`, `HeadOfState`, `Capital`, `Code2`)
VALUES ('HGE', 'japan1', 'Asia', '', '0.00', NULL, '0', NULL, NULL, NULL, '', '', NULL, NULL, '');

-- japanと関係のないところに入るため成功
INSERT INTO `country` (`Code`, `Name`, `Continent`, `Region`, `SurfaceArea`, `IndepYear`, `Population`, `LifeExpectancy`, `GNP`, `GNPOld`, `LocalName`, `GovernmentForm`, `HeadOfState`, `Capital`, `Code2`)
VALUES ('HGE', 'hoge', 'Asia', '', '0.00', NULL, '0', NULL, NULL, NULL, '', '', NULL, NULL, '');

-- これもGAPには影響がないため問題ない
UPDATE country SET Name = "hoge" WHERE Code = "KZN"

続いて、UNIQUE制約をつけた上で実行してみます。

UNIQUE制約がついたことでGAPがロックされないため、INSERTすることができるようになります。

MySQLの準結合戦略とサブクエリに対するロックの挙動

前置き

MySQL5.6から搭載された準結合戦略とサブクエリのロックの関係性ってどうなってるの?と気になったので 実際にクエリを動かしながら確認した記事になります。

実験環境

  • mysql8.0
  • Sequel Pro
  • MySQLのサンプルデータ(world) 導入方法はこちらを参照)

MySQL :: Other MySQL Documentation

要約

サブクエリをロックすると、サブクエリの対象のインデックスしかロックされないという挙動が実現できる。

準結合戦略でJOINとして評価されても、サブクエリと同様にサブクエリのインデックスしかロックされない。

1. サブクエリのロックについて

サブクエリでロックをした時の挙動についてご存知でしょうか?

サブクエリでロックをした際、ロックの対象をサブクエリの対象となったインデックスのみに限定することができます。

例えば、言語がJapaneseである国情報が欲しい場合を考えてみます。

公式のものをそのまま使っているので、countrylanguage.Languagecountry.Codeは共にindexが効いています。

start transaction;
SELECT *
FROM `country` 
WHERE `Code` IN (
    SELECT `CountryCode`
    FROM `countrylanguage`
    WHERE `Language` = 'Japanese'
    FOR UPDATE
);

この時、別のタブを開いてレコードをいじってみるとcountrylanguageテーブルのLanguageがJapaneseのレコードのみがロックされており、countryのレコードは一切ロックされていません。

ちなみに、上記のクエリをJOINで実現させた場合、countryの一部のレコードもロックされてしまいます。

start transaction;
SELECT `country`.`*` 
FROM `country` 
INNER JOIN `countrylanguage` ON `country`.`Code` = `countrylanguage`.`CountryCode`
WHERE `countrylanguage`.`Language` = 'Japanese'
FOR UPDATE;

このように、サブクエリを使うことで親のテーブルはロックしたくないけど子のテーブルのみロックしたいといった場面を解決することができます。

2. セミジョインについて

ここで疑問が1つ。

セミジョインの時、このサブクエリへのロックはどのように扱われるのか

セミジョインとは、MySQL5.6から搭載された機能で、以下の条件を満たす時にサブクエリをJOINとして評価して実行するというものです(参照)。

  1. INまたは=ANYを使用したサブクエリであること
  2. 単一のSELECT文でUNIONを使用していないこと
  3. Group byなどの集約関数を含めないこと
  4. LIMITを使用したORDER BYがないこと
  5. 外部テーブルとおよび内部テーブルの合計数が結合で許可されている最大テーブル数より少ないこと

第43回 MySQLの準結合(セミジョイン)について

評価自体はサブクエリをJOIN句に変換するように見えているので、これはどう動くのでしょうか?実際にサブクエリの動作を見ながら見ていきます。

3. セミジョインの動作を確認する

まずは、optimizer_switchから確認していきます。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 8.8.5.2 切り替え可能な最適化の制御

1.の時は、semijoinをoffにして実行していました。

>SET optimizer_switch='semijoin=off';
>SELECT @@optimizer_switch;
index_merge=on
index_merge_union=on
index_merge_sort_union=on
index_merge_intersection=on
engine_condition_pushdown=on
index_condition_pushdown=on
mrr=on
mrr_cost_based=on
block_nested_loop=on
batched_key_access=off
materialization=on
semijoin=off
loosescan=on
firstmatch=on
duplicateweedout=on
subquery_materialization_cost_based=on
use_index_extensions=on
condition_fanout_filter=on
derived_merge=on

続いて、サブクエリをつかったクエリを実行してみます。

explain
    -> SELECT *
    -> FROM `country` 
    -> WHERE `Code` IN (
    -> SELECT `CountryCode`
    -> FROM `countrylanguage`
    -> WHERE `Language` = 'Japanese'
    -> );
+----+-------------+-----------------+------------+-------+---------------------+-------------+---------+------+------+----------+--------------------------+
| id | select_type | table           | partitions | type  | possible_keys       | key         | key_len | ref  | rows | filtered | Extra                    |
+----+-------------+-----------------+------------+-------+---------------------+-------------+---------+------+------+----------+--------------------------+
|  1 | PRIMARY     | country         | NULL       | ALL   | NULL                | NULL        | NULL    | NULL |  239 |   100.00 | Using where              |
|  2 | SUBQUERY    | countrylanguage | NULL       | index | PRIMARY,CountryCode | CountryCode | 12      | NULL |  984 |    10.00 | Using where; Using index |
+----+-------------+-----------------+------------+-------+---------------------+-------------+---------+------+------+----------+--------------------------+

> show warnings;
| Note  | 1003 | /* select#1 */ select `world`.`country`.`Code` AS `Code`,`world`.`country`.`Name` AS `Name`,`world`.`country`.`Continent` AS `Continent`,`world`.`country`.`Region` AS `Region`,`world`.`country`.`SurfaceArea` AS `SurfaceArea`,`world`.`country`.`IndepYear` AS `IndepYear`,`world`.`country`.`Population` AS `Population`,`world`.`country`.`LifeExpectancy` AS `LifeExpectancy`,`world`.`country`.`GNP` AS `GNP`,`world`.`country`.`GNPOld` AS `GNPOld`,`world`.`country`.`LocalName` AS `LocalName`,`world`.`country`.`GovernmentForm` AS `GovernmentForm`,`world`.`country`.`HeadOfState` AS `HeadOfState`,`world`.`country`.`Capital` AS `Capital`,`world`.`country`.`Code2` AS `Code2` from `world`.`country` where <in_optimizer>(`world`.`country`.`Code`,`world`.`country`.`Code` in ( <materialize> (/* select#2 */ select `world`.`countrylanguage`.`CountryCode` from `world`.`countrylanguage` where (`world`.`countrylanguage`.`Language` = 'Japanese') ), <primary_index_lookup>(`world`.`country`.`Code` in <temporary table> on <auto_key> where ((`world`.`country`.`Code` = `materialized-subquery`.`CountryCode`))))) |

explainを見てもsubqueryになっています。また、show warningsをみてもmaterializeが作られておりサブクエリとして動作していることがわかります。

続いて、セミジョインを有効にしてクエリを実行して確認してみます。

>SET optimizer_switch='semijoin=on';
>SELECT @@optimizer_switch;
index_merge=on
index_merge_union=onindex_merge_sort_union=on
index_merge_intersection=on
engine_condition_pushdown=on
index_condition_pushdown=on
mrr=on
mrr_cost_based=on
block_nested_loop=on
batched_key_access=off
materialization=on
**semijoin=on**
loosescan=on
firstmatch=on
duplicateweedout=on
subquery_materialization_cost_based=on
use_index_extensions=on
condition_fanout_filter=on
derived_merge=on

まずは、サブクエリから実行してみます。

explain含め色々変わっています。show warningsを使うとJOIN句が使われていることもわかります。これがセミジョインの効果です。次に本当にJOIN句として評価されているかをみるためにJOIN句を使ったクエリを見てみます。

explain
    -> SELECT *
    -> FROM `country` 
    -> WHERE `Code` IN (
    -> SELECT `CountryCode`
    -> FROM `countrylanguage`
    -> WHERE `Language` = 'Japanese'
    -> );
+----+-------------+-----------------+------------+--------+---------------------+-------------+---------+-----------------------------------+------+----------+--------------------------+
| id | select_type | table           | partitions | type   | possible_keys       | key         | key_len | ref                               | rows | filtered | Extra                    |
+----+-------------+-----------------+------------+--------+---------------------+-------------+---------+-----------------------------------+------+----------+--------------------------+
|  1 | SIMPLE      | countrylanguage | NULL       | index  | PRIMARY,CountryCode | CountryCode | 12      | NULL                              |  984 |    10.00 | Using where; Using index |
|  1 | SIMPLE      | country         | NULL       | eq_ref | PRIMARY             | PRIMARY     | 12      | world.countrylanguage.CountryCode |    1 |   100.00 | NULL                     |
+----+-------------+-----------------+------------+--------+---------------------+-------------+---------+-----------------------------------+------+----------+--------------------------+
> show warnings;
| Note  | 1003 | /* select#1 */ select `world`.`country`.`Code` AS `Code`,`world`.`country`.`Name` AS `Name`,`world`.`country`.`Continent` AS `Continent`,`world`.`country`.`Region` AS `Region`,`world`.`country`.`SurfaceArea` AS `SurfaceArea`,`world`.`country`.`IndepYear` AS `IndepYear`,`world`.`country`.`Population` AS `Population`,`world`.`country`.`LifeExpectancy` AS `LifeExpectancy`,`world`.`country`.`GNP` AS `GNP`,`world`.`country`.`GNPOld` AS `GNPOld`,`world`.`country`.`LocalName` AS `LocalName`,`world`.`country`.`GovernmentForm` AS `GovernmentForm`,`world`.`country`.`HeadOfState` AS `HeadOfState`,`world`.`country`.`Capital` AS `Capital`,`world`.`country`.`Code2` AS `Code2` from `world`.`countrylanguage` join `world`.`country` where ((`world`.`country`.`Code` = `world`.`countrylanguage`.`CountryCode`) and (`world`.`countrylanguage`.`Language` = 'Japanese')) |

上記のサブクエリをINNER JOINに書き換えたものです。全く同じクエリになっています。

explain
SELECT `country`.`*` 
FROM `country` 
INNER JOIN `countrylanguage` ON `country`.`Code` = `countrylanguage`.`CountryCode`
WHERE `countrylanguage`.`Language` = 'Japanese';
+----+-------------+-----------------+------------+--------+---------------------+-------------+---------+-----------------------------------+------+----------+--------------------------+
| id | select_type | table           | partitions | type   | possible_keys       | key         | key_len | ref                               | rows | filtered | Extra                    |
+----+-------------+-----------------+------------+--------+---------------------+-------------+---------+-----------------------------------+------+----------+--------------------------+
|  1 | SIMPLE      | countrylanguage | NULL       | index  | PRIMARY,CountryCode | CountryCode | 12      | NULL                              |  984 |    10.00 | Using where; Using index |
|  1 | SIMPLE      | country         | NULL       | eq_ref | PRIMARY             | PRIMARY     | 12      | world.countrylanguage.CountryCode |    1 |   100.00 | NULL                     |
+----+-------------+-----------------+------------+--------+---------------------+-------------+---------+-----------------------------------+------+----------+--------------------------+

> show warnings;
| Note  | 1003 | /* select#1 */ select `world`.`country`.`Code` AS `Code`,`world`.`country`.`Name` AS `Name`,`world`.`country`.`Continent` AS `Continent`,`world`.`country`.`Region` AS `Region`,`world`.`country`.`SurfaceArea` AS `SurfaceArea`,`world`.`country`.`IndepYear` AS `IndepYear`,`world`.`country`.`Population` AS `Population`,`world`.`country`.`LifeExpectancy` AS `LifeExpectancy`,`world`.`country`.`GNP` AS `GNP`,`world`.`country`.`GNPOld` AS `GNPOld`,`world`.`country`.`LocalName` AS `LocalName`,`world`.`country`.`GovernmentForm` AS `GovernmentForm`,`world`.`country`.`HeadOfState` AS `HeadOfState`,`world`.`country`.`Capital` AS `Capital`,`world`.`country`.`Code2` AS `Code2` from `world`.`country` join `world`.`countrylanguage` where ((`world`.`country`.`Code` = `world`.`countrylanguage`.`CountryCode`) and (`world`.`countrylanguage`.`Language` = 'Japanese')) |

では、最後にセミジョインが有効の状態でサブクエリにFOR UPDATEをつけて実行してみます。countrylanguageテーブルは綺麗にロックされていますね。 では、counrtyテーブルはというとこちらは通常のJOIN句と違いロックされていません。

すなわち、セミジョイン戦略でJOIN句として評価されていてもFOR UPDATEのロック自体は変わることがありません。

まとめ

準結合戦略でJOINとして評価されても、サブクエリと同様にサブクエリのインデックスしかロックされません。

裏でどのような処理がされているのかが気になりますが、うまく後方互換性が担保されているようです。これにより、サブクエリを使ったクエリが非常に改善されるのでMySQL5.6未満を使っている方は、バージョンアップの恩恵が多そうですね。

ちなみにセミジョイン戦略はデフォルトでONになっているので、サブクエリを使ったクエリのEXPLAINが予想と違うとなった方はぜひご確認ください。

MySQLでユーザーの一覧を確認する

意味なし芳一

前置き

この話は同僚との雑談の中で生まれたパワーワードを文章に起こしたものであり、現実の組織•団体とは一切関係がありません。もちろん、まさかりなどでもありません。

本題

あるところに芳一という人がおりました。芳一は仕事熱心で、ファシリテートをする際も非常に丁寧な資料を作りMTGを行なっていました。

ある時、芳一は現場のチームごとに抱える課題が様々であり、お互いのチームが相談し合うことで課題が解決できると考えました。そこで芳一は、定期的に各チームが自分たちのチームの進捗や課題を共有し合い、話し合いや改善が行える場を用意しました。

しかし芳一はアジェンダに、MTGの意味を書かなかったのです。

最初は皆がMTGの意味を理解していたため良いMTGでした。しかし、次第に形骸化し、進捗や課題を共有するだけのMTGになっていきました。そうして、改善という意味を見失った、共有だけが行われる形式的なMTGが繰り返される様になって行きました。

このことから芳一は「意味なし芳一」と呼ばれるようになったのです。

解説

定期的に行うものほど形骸化しやすいため、本質的な意味を意識しなければいけない。

これには、MTGの度に冒頭にMTGの意味を話し意識し続ける。

MTG自体を常に改善していくといった方法が有効である。