やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

そもそも疑問に思うことがないという事実

心理的安全性の重要性について知る

日々の開発の中で、お互いに新機能に対して意見を言い合うことなどがあるかと思いますが、自分が所属するチームではなかなか、意見が出ずに意見交流が発生しないことが多々ありました。

これに対して自分の答えの1つとして、心理的安全性が確保されていないのではないかと思い「恐れのない組織――「心理的安全性」が学習・イノベーション・成長をもたらす」や「新 コーチングが人を活かす」という本を読みました。

www.okb-shelf.work

読書を通して、確かに心理的安全性は重要で確保されていないといけないと感じ、所属チームで雑談を積極的に取り入れたり、自分が答えを出さず質問・意見を求める、無知であるという取り組みを約1ヶ月ほど続けてきました。

1ヶ月前と比べて、他メンバーへの相槌や「なるほど」といった反応が生まれるようになりました。これに関しては「よしよし!」と思ったのですが、自分が求めているような意見の交流にまでは至りませんでした。

なぜ意見交流が起きないのか

MTG後にメンバーに対してやんわりと「...話にくい?」と聞いて回って話を聞いてみました。すると、ある答えが共通して出てきました。

話にくいとは感じてないですが、何を話したらいいの分からないです

...!

なるほど...そういうことかと雷に打たれたような衝撃を受けてこの記事を執筆しています。
確かに、特に思ったことがない場合、意見を求められていてもすにでベストアンサーが出ていては話すことは何もないという状況は十分に考えられます。

とはいえ、それでいいのか

自分が大学生の時に所属していた研究室では「何でも良いから質問をする」という姿勢を求められました。質問のクオリティを高めるためには、繰り返し質問を行い、普段からあらゆることに疑問に持つことが重要だということを先生から教わりました。

当時の質問の例をいくつか挙げてみました。

  • そもそも、〇〇って何でしたっけ?
  • ここの説明をもう1回聞きたいです
  • 〇〇ってこういうものだと理解しましたが合ってますか?

本当に意識さえあれば、誰でも出来る質問でした。そう考えると話すことが何もないという状況は姿勢に問題があるのではないかと思ってしまいます。

どうやってチームに習慣を作ればいいのか

現時点では、大学生の時と同様に「何でも良いから質問する」ということを意識づけるしかないと考えています。「新 コーチングが人を活かす」にも記載があったようにメンバーに「なぜ質問をしてほしいのか」「何を目標にしているのか」を明確に伝えていこうと思います。

今なら無料でkindle版が読めます。

経過はまた後日、ブログに記事を書きたいと思います。

M1mac+homebrewでpostgresql.confがある場所

探してみるも見つからず

SQLの統計情報の記録の設定しようと思い、以下を参考にpostgresql.confを探してみるも見つからず...

stackoverflow.com

homebrewを使っているところは同じようだけど、どうやらOSが違う、いやM1macなのが原因かと思い調べているとやはりそうだった。

M1macでの格納場所

/opt/homebrew/var/postgresでした。

$ ls /opt/homebrew/var/postgres
PG_VERSION              pg_dynshmem             pg_multixact            pg_snapshots            pg_tblspc               postgresql.auto.conf
base                    pg_hba.conf             pg_notify               pg_stat                 pg_twophase             postgresql.conf
:

stackoverflow.com

無事に変更することが出来ました。

shared_preload_libraries = 'pg_stat_statements'   # (change requires restart)
pg_stat_statements.max = 300
pg_stat_statements.track = 'top'
pg_stat_statements.track_utility = 'off'
pg_stat_statements.save = 'off'

postgresqlにおけるテーブルスキャンの実行例

テーブルスキャン

postgresqlでは、テーブルスキャンに該当する演算子が5つあります。

  • Seq scan: インデックスを使用せず、全件を検索
  • Index scan: インデックスを使用してスキャン
  • Bitmap scan: ビットマップを使用してスキャン
  • Index only scan: 問い合わせがインデックスに含まれるカラムのみで完結する場合のスキャン
  • Tid scan: 検索条件がタプルID(ctid)のスキャン

以下より引用

www.slideshare.net

Seq scanというのはいわゆる、フルスキャンでテーブルの中身を全て確認する必要があるため、一般的には動作が遅くなります。その一方でインデックスを適切に設置することで、Index scanとなる場合やIndex only scan、ビットマップインデックスを設定していればBitmap scanが使用されます。

Tid scanはレコードが物理的に格納されている位置を識別するためのctidを使用した検索時に使用されるもので、使用頻度はかなり低いでしょう。またctidはデータの更新、削除によって変わるため、使用には慎重になる必要があります。

補足: ビットマップインデックスについて

インデックスには、広くB-treeインデックスというツリー型のインデックスが使用されています。理由としては全てのケースで安定した性能が出るためです。しかしながら、B-treeインデックスはOR検索では使用することが出来ない、カラムのカーディナリティー(値の種類)が低い場合に、検索効率が悪いというデメリットがあります。

その点、ビットマップインデックスはカラムのカーディナリティー数に対応してビットマップを作成するため、カーディナリティーが低い場合に検索効率が良く、ツリーを辿る必要がなくビットマップを複合することで、OR検索時にもインデックスを使用することが出来ます。

例: 注文テーブルのsizeにビットマップインデックスを作成する

order_id kind size
1 food M
2 drink S
3 food M
4 food L

作成されるビットマップ

size 1行目 2行目 3行目 4行目
S 0 1 0 0
M 1 0 1 0
L 0 0 0 1

作成されるビットマップの大きさ: カラムのカーディナリティー数 x レコード数

どのscanが使用されるかを実際に確認する

以下、結果を確認するためにstudentという適当なテーブルを用意しています。

                       Table "public.student"
   Column   |         Type          | Collation | Nullable | Default 
------------+-----------------------+-----------+----------+---------
 student_id | integer               |           | not null | 
 name       | character varying(50) |           | not null | 
 gender     | character(1)          |           |          | 
Indexes:
    "student_pkey" PRIMARY KEY, btree (student_id)
Check constraints:
    "student_gender_check" CHECK (gender = ANY (ARRAY['0'::bpchar, '1'::bpchar, '2'::bpchar]))

studentにはランダムツールを用いて作成したレコードが1万件、記録してあります。 また、student_idを主キーとして登録しているので、自動的にB-treeインデックスが作成されています。

動作確認: psql (PostgreSQL) 11.15

Seq scan

フルスキャンが必要になるのは、インデックスが作成されていないカラムを用いた検索をする場合や、テーブルの値を全て取得するような場合です。

examples=# EXPLAIN SELECT * FROM student;
                          QUERY PLAN                          
--------------------------------------------------------------
 Seq Scan on student  (cost=0.00..166.00 rows=10000 width=22)
(1 row)
examples=# EXPLAIN  SELECT * FROM student WHERE name = 'Coy Champlin';
                        QUERY PLAN                        
----------------------------------------------------------
 Seq Scan on student  (cost=0.00..191.00 rows=1 width=22)
   Filter: ((name)::text = 'Coy Champlin'::text)
(2 rows)
 student_id |     name     | gender 
------------+--------------+--------
          1 | Coy Champlin | 1

Index scan

先ほどとは打って変わり、インデックスが作成されているカラムを使用した検索時にはIndex scanが使用されます。Seq scanと比べてテーブルをフルスキャンする必要がないため、同じレコードを取得したにも関わらず、コストが抑えられています。

examples=# EXPLAIN SELECT * FROM student WHERE student_id = 1;
                                 QUERY PLAN                                  
-----------------------------------------------------------------------------
 Index Scan using student_pkey on student  (cost=0.29..8.30 rows=1 width=22)
   Index Cond: (student_id = 1)
(2 rows)
 student_id |     name     | gender 
------------+--------------+--------
          1 | Coy Champlin | 1

BETWEENINでもIndex scanとなりました。

examples=# EXPLAIN SELECT * FROM student WHERE student_id BETWEEN 1 AND 100;
                                   QUERY PLAN                                   
--------------------------------------------------------------------------------
 Index Scan using student_pkey on student  (cost=0.29..10.29 rows=100 width=22)
   Index Cond: ((student_id >= 1) AND (student_id <= 100))
(2 rows)
examples=# EXPLAIN SELECT * FROM student WHERE student_id IN (1, 2, 3);
                                  QUERY PLAN                                  
------------------------------------------------------------------------------
 Index Scan using student_pkey on student  (cost=0.29..16.91 rows=3 width=22)
   Index Cond: (student_id = ANY ('{1,2,3}'::integer[]))
(2 rows)

そして、OR検索時には...なんとプランナーがビットマップを作成してBitmap scanを使用してくれました。

examples=# EXPLAIN SELECT * FROM student WHERE student_id = 1 OR student_id = 2 OR student_id = 3;
                                   QUERY PLAN                                    
---------------------------------------------------------------------------------
 Bitmap Heap Scan on student  (cost=12.88..23.01 rows=3 width=22)
   Recheck Cond: ((student_id = 1) OR (student_id = 2) OR (student_id = 3))
   ->  BitmapOr  (cost=12.88..12.88 rows=3 width=0)
         ->  Bitmap Index Scan on student_pkey  (cost=0.00..4.29 rows=1 width=0)
               Index Cond: (student_id = 1)
         ->  Bitmap Index Scan on student_pkey  (cost=0.00..4.29 rows=1 width=0)
               Index Cond: (student_id = 2)
         ->  Bitmap Index Scan on student_pkey  (cost=0.00..4.29 rows=1 width=0)
               Index Cond: (student_id = 3)
(9 rows)

コストを見る限り、対象のレコードに対してだけ、アクセスをしてビットマップを作っているようです。

Bitmap scan

ビットマップインデックスが必要になりますが、postgresqlではインデックスの作成時に明示的にビットマップインデックスを作成することが出来ません。ただし、インデックスを作成したカラムに対して検索を実施すると、Bitmap scanとなっています。どういった条件でBitmap scanに切り替わるのかは分かりませんでしたが、プランナーが非常に賢いです。

examples=# CREATE INDEX ON student(gender);
examples=# EXPLAIN SELECT * FROM student WHERE gender = '1';
                                     QUERY PLAN                                      
-------------------------------------------------------------------------------------
 Bitmap Heap Scan on student  (cost=65.87..173.13 rows=3301 width=22)
   Recheck Cond: (gender = '1'::bpchar)
   ->  Bitmap Index Scan on student_gender_idx  (cost=0.00..65.04 rows=3301 width=0)
         Index Cond: (gender = '1'::bpchar)
(4 rows)

Index only scan

インデックスが作成されたカラムだけで作成して、SELECT区で取得するカラムをインデックスが作成されたカラムだけにすればIndex only scanとなります。

examples=# EXPLAIN SELECT student_id FROM student WHERE student_id = 1;
                                   QUERY PLAN                                    
---------------------------------------------------------------------------------
 Index Only Scan using student_pkey on student  (cost=0.29..8.30 rows=1 width=4)
   Index Cond: (student_id = 1)
(2 rows)
examples=# EXPLAIN SELECT student_id, gender FROM student WHERE student_id = 1;
                                 QUERY PLAN                                 
----------------------------------------------------------------------------
 Index Scan using student_pkey on student  (cost=0.29..8.30 rows=1 width=6)
   Index Cond: (student_id = 1)
(2 rows)

インデックスが作成されていないnameを取得するとIndex scanとなる。

examples=# EXPLAIN SELECT * FROM student WHERE student_id = 1;
                                 QUERY PLAN                                  
-----------------------------------------------------------------------------
 Index Scan using student_pkey on student  (cost=0.29..8.30 rows=1 width=22)
   Index Cond: (student_id = 1)
(2 rows)

また、Bitmap scanの場合にはIndex only scanにならないよう。

examples=# EXPLAIN SELECT student_id, gender FROM student WHERE gender = '1';
                                     QUERY PLAN                                      
-------------------------------------------------------------------------------------
 Bitmap Heap Scan on student  (cost=65.87..173.13 rows=3301 width=6)
   Recheck Cond: (gender = '1'::bpchar)
   ->  Bitmap Index Scan on student_gender_idx  (cost=0.00..65.04 rows=3301 width=0)
         Index Cond: (gender = '1'::bpchar)
(4 rows)

Tid scan

使うことがほとんどないと思いますが、試してみます。

examples=# SELECT ctid FROM student WHERE student_id = 1;
 ctid  
-------
 (0,1)
examples=# EXPLAIN SELECT * FROM student WHERE ctid = '(0,1)';
                       QUERY PLAN                       
--------------------------------------------------------
 Tid Scan on student  (cost=0.00..4.01 rows=1 width=22)
   TID Cond: (ctid = '(0,1)'::tid)
(2 rows)

あとがき

僕は所属企業でHR向けのアプリケーションを作っています。今から半年前までの約2年間、サービス拡大のために様々な新機能を作成しては公開してを繰り返しました。しかしながら、当時はアプリケーションは多くの問題を抱えていました。

  • コードが複雑で手を加えにくい
  • 仕様が複雑で認識違いやバグが多発する
  • パフォーマンスが低い(スロークエリ)処理が複数確認された

とても、新機能を開発出来る状態ではなくなりました。
そんな中で、バグ修正をメインで行なってきた自分ですが、最近はデータベースへの関心もあり、スロークエリを探して修正方法はないものかと思案することに夢中になっています。その際に、役に立つのがEXPLAINというクエリでした。

例えば、EXPLAIN SELECT * FROM hoge;とすると、このクエリがどのように実行されるのかというプランナーが作成した結果を見ることが出来ます。しかし、出力結果には何やら意味の分からない言葉がたくさん羅列しています。

examples=# EXPLAIN SELECT * FROM hoge;
                          QUERY PLAN                          
--------------------------------------------------------------
 Seq Scan on hoge  (cost=0.00..xx.00 rows=xxx width=xx)
(1 row)

特にテーブルのスキャンに最もコストがかかるため、いつどんな時にどのスキャンが実行されるのかを判断出来るようにと思い、今回の記事の執筆に至りました。

参考文献