Closed14

Hight Performance Postgresql for Railsを読んでく

ShuSuzukiShuSuzuki
  • 地味に始めて見たけど、schema.rbをsql方式で管理することもできるらしい。
  • bin/rails db:migratepg_dump → db/structre.sqlにダンプの流れらしい。
  • postgres appは便利
  • postgres用語その1:タプル
    • PostgreSQLでの「タプル」というのは、データベースのテーブルにおける一つ一つの行(レコード)くらいの感覚?
    • postgresの場合はそのタプルが直接ではなく、バージョンによって管理されているらしい
  • postgres用語その2:ページ
    • PostgreSQLでは、データはページ(ここではほとんどブロックと相互変更可能)に格納される

あとは1章に関しては、railsとかsqlの簡単な説明だった。クセ強な環境構築だったが無事できたぞ!

ShuSuzukiShuSuzuki
rideshare_development=> SELECT * FROM pg_stat_activity
rideshare_development-> WHERE pid = (SELECT PG_BACKEND_PID());
-[ RECORD 1 ]----+---------------------------------------
datid            | 16396
datname          | rideshare_development
pid              | 22568
leader_pid       | 
usesysid         | 16390
usename          | owner
application_name | psql
client_addr      | 127.0.0.1
client_hostname  | 
client_port      | 55625
backend_start    | 2024-01-29 22:58:56.428045+09
xact_start       | 2024-01-29 23:06:52.481908+09
query_start      | 2024-01-29 23:06:52.481908+09
state_change     | 2024-01-29 23:06:52.48191+09
wait_event_type  | 
wait_event       | 
state            | active
backend_xid      | 
backend_xmin     | 863
query_id         | 
query            | SELECT * FROM pg_stat_activity        +
                 | WHERE pid = (SELECT PG_BACKEND_PID());
backend_type     | client backend
  • postgresは悲観ロック。
    • SELECT * FROM pg_locks;ロック情報が取れる
ShuSuzukiShuSuzuki
  • データスクラブ
    • 不必要なデータを削除しながらデータベースを構築していくらしい。
    • 手元で安全に本番同等のパフォーマンステストをするのに有効
  • psqlの関数使ってテーブルのサイズを以下のように確認できる
SELECT PG_SIZE_PRETTY( 
  PG_TOTAL_RELATION_SIZE('rideshare.users')
);

// 結果
 pg_size_pretty 
----------------
 2629 MB
(1 row)

// データめっちゃいれた
SELECT COUNT(*) From users;
  count   
----------
 10020210
  • memo: 途中のコードメモ
//userテーブルの統計対象データ数をset
ALTER TABLE users
ALTER COLUMN first_name SET STATISTICS 5_000;

//分析開始
ANALYZE users;

//分析結果出力
SELECT
  attname,
  n_distinct,
  most_common_vals
FROM pg_stats
WHERE schemaname = 'rideshare' AND tablename = 'users'
AND attname = 'first_name';
  • PostgreSQLにはデータベースレベルとテーブルレベルのコメントがある
  • CREATE OR REPLACE FUNCTIONでpostgresは関数をつくれそう
  • fxっていうgemを使うといい感じにmigrationでpostgresの関数を管理できる
  • SET LOCAL statement_timeout = '120s';で一時的にtimeout引きあげるのが良さそうだ。俺の個人PCがもたねえぞ。。。
  • 外部キー制約を見つけるコマンド初めて知った。
rideshare_development=> SELECT
rideshare_development-> conrelid::regclass AS table_name, conname AS foreign_key, PG_GET_CONSTRAINTDEF(oid)
rideshare_development-> FROM pg_constraint
rideshare_development-> WHERE contype = 'f'
rideshare_development-> AND connamespace = 'rideshare'::regnamespace ORDER BY conrelid::regclass::text, contype DESC;
      table_name      |     foreign_key     |                    pg_get_constraintdef                    
----------------------+---------------------+------------------------------------------------------------
 trip_positions       | fk_rails_9688ac8706 | FOREIGN KEY (trip_id) REFERENCES trips(id)
 trip_requests        | fk_rails_fa2679b626 | FOREIGN KEY (start_location_id) REFERENCES locations(id)
 trip_requests        | fk_rails_c17a139554 | FOREIGN KEY (rider_id) REFERENCES users(id)
 trip_requests        | fk_rails_3fdebbfaca | FOREIGN KEY (end_location_id) REFERENCES locations(id)
 trips                | fk_rails_6d92acb430 | FOREIGN KEY (trip_request_id) REFERENCES trip_requests(id)
 trips                | fk_rails_e7560abc33 | FOREIGN KEY (driver_id) REFERENCES users(id)
 vehicle_reservations | fk_rails_7edc8e666a | FOREIGN KEY (vehicle_id) REFERENCES vehicles(id)
 vehicle_reservations | fk_rails_59996232fc | FOREIGN KEY (trip_request_id) REFERENCES trip_requests(id)
  • テーブルをコピーしたい場合、基本的には大量データを扱うときはクローン/コピーするのが良さそう。ただindexや制約を削除してから、データをコピーして、cloneするのが一番パフォーマンス良い。
  • VACUUMのは不要領域の削除
  • PL/pgSQL で手続き的にプロシジャを書くことができる
ShuSuzukiShuSuzuki
  • indexつけるときとかはCONCURRENTLYつけるとロックはされないが、総合時間はかかる。
  • unique制約をあとから付ける場合にはCTE(共通テーブル)方式というパターンがあるらしい
  • check制約がrailsでサポートされたのは6.1からしい
  • Check制約は新しい追加更新にだけ適用することができる。それを利用して安全にuniqe制約などを追加していく戦略もある
  • 制約の強制を遅らせることもできる(DEFERRABLEオプション)
    • トランザクションの中で制約を一時的に遅らせて、SQLを単純化できる
  • exclude制約はuniq制約よりも柔軟
    • GISTインデックスというのを使ってる

拡張の追加に慣れてきた。確認方法は\dx\dnでschema確認。

rideshare_development=> CREATE EXTENSION IF NOT EXISTS citext;
CREATE EXTENSION
rideshare_development=> \dx

仮想列というのがposgresで使えるらしい。postgresでは生成列と呼ばれている

```rb
create_table 'temp.customers' do |t| t.string :email
t.virtual :email_downcased,
type: :string,
as: 'LOWER(email)', stored: true
end
  • postgresには型としてenum型がある
create_enum :vehicle_status, [ VehicleStatus::DRAFT, VehicleStatus::PUBLISHED]
add_column :vehicles, :status, :enum, enum_type: :vehicle_status, default: VehicleStatus::DRAFT, null: false
  • Domainというオブジェクト型もあるらしい
    • これは指定された集合の中の文字からデータが成っていることとを制約するらしい。
  • active_record_doctor というgemがある

実行してみた。

~/Desktop/rideshare main !3 ?7 ❯ bin/rake active_record_doctor                                                           Ruby 3.2.2
trip_requests.end_location_id is a foreign key of type integer and references locations.id of type bigint - foreign keys should be of the same type as the referenced column
trip_requests.start_location_id is a foreign key of type integer and references locations.id of type bigint - foreign keys should be of the same type as the referenced column
vehicle_reservations.trip_request_id is a foreign key of type integer and references trip_requests.id of type bigint - foreign keys should be of the same type as the referenced column
vehicle_reservations.vehicle_id is a foreign key of type integer and references vehicles.id of type bigint - foreign keys should be of the same type as the referenced column
use `dependent: :delete_all` or similar on Vehicle.vehicle_reservations - associated model VehicleReservation has no callbacks and can be deleted in bulk
add an index on vehicle_reservations(trip_request_id) - foreign keys are often used in database lookups and should be indexed for performance reasons
add an index on trip_positions(trip_id) - foreign keys are often used in database lookups and should be indexed for performance reasons
SearchResult references a non-existent table or view named search_results
FastSearchResult references a non-existent table or view named fast_search_results
the schema limits locations.state to 2 characters but there's no length validator on Location.state - remove the database limit or add the validator
add a unique index on users(drivers_license_number) - validating uniqueness in Driver without an index can lead to duplicates
add a unique index on trips(trip_request_id) - using `has_one` in TripRequest without an index can lead to duplicates
create a foreign key on trip_requests.rider_id - looks like an association without a foreign key constraint
create a foreign key on trips.driver_id - looks like an association without a foreign key constraint
add a `presence` validator to VehicleReservation.canceled - it's NOT NULL but lacks a validator
ShuSuzukiShuSuzuki
  • Multiversion Concurrency Control (MVCC) - 行の変更と同時アクセスを管理するメカニズム
  • backfilling - 新しいテーブル設計のために新しい空のカラムを追加すること
  • strong_migrationはお世話になってます
  • defaultのみは早いが、手をいれると急に遅くなってロックがかかる
// 早い
ALTER TABLE temp.users ADD COLUMN city_id INTEGER
DEFAULT 1;

// 遅い
ALTER TABLE temp.users ADD COLUMN city_id INTEGER
DEFAULT 1 + FLOOR(RANDOM() * 25);

// ほかセッションで実行したらtimeoutになった
rideshare_development=> SELECT * FROM temp.users LIMIT 1;
ERROR:  canceling statement due to lock timeout
LINE 1: SELECT * FROM temp.users LIMIT 1;
  • 1つの対策がCONCURRENTLYのオプション使用

    • 2倍時間かかるらしいけど。説明はあとででるがindexの追加とかだとほぼ必要。オフライン実行のときは不要。
  • migrationの戦略として大きなテーブルに制約を加える場合

    • 1: add_check_constraintをvalidate: falseで実行。新規データのcheckを有効化
    • 2: validate_check_constraintで検証。 既存のデータの検証
    • というやり方でやるとロックタイムを最小限にできる
  • lock_timeoutを設定すると永遠にトランザクションでロックになるのを防げる

  • log_lock_waits をonにしてdeadlock_timeoutを設定知るとpostgres.logに詳細な情報が記されて、pgbadgerはそれを容易に可視化してくれる

  • statament timeoutはクライアント側から設定する

  • activerecordのキャッシュのせいでアプリケーションエラーになる可能性もあるので、まずはignore_columnをして上げるのが良い。

  • 効率の良いbackfill(データを一括で埋めること)の方法を考える必要がある

    • ダブルライティング(置き換え前と置き換え後のテーブル両方に書き込み、読み込みは後者にする)
    • 場合によっては中間テーブルが必要
    • SET UNLOGGEDにすると書き込みが早くなる。またautovaccumも無効にすると良い。
ShuSuzukiShuSuzuki
N+1 queries detected:
  SELECT COUNT(*) FROM "vehicle_reservations" WHERE "vehicle_reservations"."vehicle_id" = 1 /*application='Rideshare'*/
  SELECT COUNT(*) FROM "vehicle_reservations" WHERE "vehicle_reservations"."vehicle_id" = 2 /*application='Rideshare'*/
  SELECT COUNT(*) FROM "vehicle_reservations" WHERE "vehicle_reservations"."vehicle_id" = 3 /*application='Rideshare'*/
  SELECT COUNT(*) FROM "vehicle_reservations" WHERE "vehicle_reservations"."vehicle_id" = 4 /*application='Rideshare'*/
Call stack:
  (irb):9:in `block (2 levels) in <top (required)>'
  (irb):8:in `block in <top (required)>'
  (irb):8:in `<top (required)>'
  • includsは基本書かないで欲しい派です!
  • rails6からstrict_loadingというものが使えてN+1を完全に防げる
  • rails7で導入されたload_async使ってみたいぞ。
  • RETURNINGキーワードというのが使えるらしい。postgresだと使える
    • insertした行とかが返ってくるらしい。
  • CTEは共通テーブル方式
    • .withを使うとできる。(あらかじめ用意しとくサブクエリ的な)
  • ActiveRecordではviewはサポートされてないけどgem使えばできる
  • クエりchacheはSQLクエリの結果をcontrollerのアクションの間キャッシュする。
  • prepared_statement(プリペアドステートメント)は、データベースで使用される概念で、SQL文をあらかじめコンパイルしておき、実行時にパラメータを渡すことで繰り返し効率的に実行できるようにしたものです。activerecordだとデフォルトで有効になっている。
  • counter cacheでcountクエリを高速化できる(デメリットもあるが)
  • .countではなく.sizeを使うのもパフォーマンス向上の1つ
  • 平均計算とかはSQLの組み込み関数を使ったほうが早い
ShuSuzukiShuSuzuki
  • デフォルトで使用できるActive Support Notificationsでイベントを発行できる
  • pgss(pg_stat_statement)はDBの統計情報を確認できるviewを作成する
    • PostgreSQLで問い合わせを受け取ると、PGSSはそれらをグループに分類し、グループに識別子を付与し、グループレベルの統計情報を取得します
    • pgHeroはパフォーマンスダッシュボード
  • ExplainにAnlyze引数をとると実際の実行時間が返される。
  • Buffers引数を追加するとフェッチされるデータ数(IO)を把握できる
  • indexをサジェストしてくれるgemもいくつかあるそう
  • auto_explainモジュールを使うと、自動的な実行計画取得ができる
  • dbのcountは時間がかかるがいくつかのトリックで対策できるそう

全然違った。すげえ。統計値更新すればもっと正確になるかも?

irb(main):002> User.fast_count
   (38.1ms)  SELECT fast_count('users', 100000) /*application='Rideshare'*/
=> 19949796
irb(main):003> User.count
[sql.active_record] 14.766931 SELECT COUNT(*) FROM "users" /*application='Rideshare'*/
  User Count (14767.0ms)  SELECT COUNT(*) FROM "users" /*application='Rideshare'*/
=> 20040420
irb(main):004> 
  • FILTER句が使える。スコープ付き集約関数(case文とかで使えそう)
  • ActiveRecordにはoptimizer_hintsメソッドがあるらしい
  • Rails7.1からはUser.where("id <= 10").explain(:analyze)みたいにexplainにanalizeを指定できる
  • rails-pg-extrasはSQLを分析できるようなCLIを追加してくれるgem
ShuSuzukiShuSuzuki

postgresのexplain。mysqlに慣れてたからあれって思ったけど、普通に見やすかった。

rideshare_development=> EXPLAIN (ANALYZE, BUFFERS)                                                                                    SELECT * FROM temp.users                                                                                                              WHERE last_name IN ('lname10000', 'lname100000', 'lname1000000');
                                                         QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------------------
 Index Scan using users_last_name_idx on users  (cost=0.43..19.97 rows=3 width=73) (actual time=0.038..0.053 rows=3 loops=1)
   Index Cond: (last_name = ANY ('{lname10000,lname100000,lname1000000}'::text[]))
   Buffers: shared hit=9 read=6
 Planning:
   Buffers: shared hit=15 read=1 dirtied=2
 Planning Time: 0.278 ms
 Execution Time: 0.077 ms
(7 rows)
  • indexについての情報を出力するクエリ

  • multicolumn Indexは冗長になりやすい

  • PostgreSQLはいくつかの操作で個々のインデックススキャン結果を組み合わせることができる(わざわざmultiにする必要がない場合が多い)

  • 部分インデックスは、定義に式を組み込んだB-Treeインデックス

    • CREATE INDEX index_name ON table_name(column_name) WHERE condition;こんなイメージ
    • bool値でdefalutがtrueの場合だとindexにfalseの条件を仕込めば、少ない容量でindexを有効に使える。これはfalseでの検索が多いときに便利
  • 式インデックスは、定義に式を持つインデックスで、格納される前に値を変換します。

    • CREATE UNIQUE INDEX index_temp_users_lower_email_unique ON temp.users (LOWER(email));
    • LOWERでメールアドレスを正規化する。
  • jsonとjsonb型がある

  • jsonbのような1カラムに複数ともいえるデータが入るときはgin indexを使うとよいそう。

  • jsonbだとこんな感じの包む

  • 包含演算は@>で以下のように使う

SELECT * FROM trips
WHERE data @> '{"ride_details":{"water_offered": true}}';
  • jsonb_path_opsを指定すると、JSONデータ内の特定のパスやキーを対象としたクエリが高速化されます
  • postgres-json-schemaは、JSONスキーマ定義をデータベースに追加することをサポートします。
  • ブロック範囲インデックス(BRIN INDEX)というのがあるらしい
    • BRINが有効であるためには、物理レイアウトと対象カラムの順序が強く相関しているテーブルが必要である。

btreeよりbrinのほうが軽い

rideshare_development=*> SELECT PG_SIZE_PRETTY( PG_RELATION_SIZE('trip_positions_created_at_btree'));
 pg_size_pretty 
----------------
 113 MB
(1 row)

rideshare_development=*> SELECT PG_SIZE_PRETTY( PG_RELATION_SIZE('trip_positions_created_at_brin'));
 pg_size_pretty 
----------------
 32 kB
(1 row)
  • hashインデックスというのもあるらしい
    • ハッシュインデックスはカラム値の代わりにソースカラムから計算されたハッシュを格納するため、全体的に使用するスペースが少なくて済む。
  • カバレッジ・インデックスとは、インデックス・エントリがクエリに必要なすべてのデータを提供するインデックスの定義を指す
ShuSuzukiShuSuzuki
  • 3つの重要なメンテナンス操作は、VACUUM、ANALYZE、REINDEXです。この3つをVARと略す
  • Automatic Vacuumは、各テーブルに対してVACUUMワーカーを実行するバックグラウンドスケジューラ
    • ガベージコレクションみたいなもん
    • 行バージョンは以下のような形でみれる(ページ数, タプル番号)
rideshare_development=> SELECT ctid,id FROM users WHERE id = 1;
    ctid    | id 
------------+----
 (41443,11) |  1
  • updateするとタプルが更新され(バージョンみたいなもん)、ページは変わることもあれば変わらないこともある
  • よってupdate等をするとタプルが増えて肥大化し始める。。閾値を修正して対応することができる
    • デフォルトの閾値は0.2(ターブルサイズの20%がデッドタプル)
    • 下げるとよりはやくvacuumが実行される
  • インデックスもデッドタプルのせいで肥大化する
  • VACUUM (ANALYZE, VERBOSE) users;手動でタプルと統計情報を更新する
    最後にanalyzeされたタイミングを知ることもできる
rideshare_development=> SELECT
rideshare_development->  schemaname,
rideshare_development->   relname,
rideshare_development->   last_autoanalyze,
rideshare_development->   last_analyze
rideshare_development-> FROM pg_stat_all_tables WHERE relname = 'vehicles';
 schemaname | relname  | last_autoanalyze |         last_analyze          
------------+----------+------------------+-------------------------------
 rideshare  | vehicles |                  | 2024-02-03 01:06:07.150831+09
(1 row)

  • posgresには未使用indexを追跡できる機能がある
  • 同じindex名はつけられないがindex名を変えた同じindexは作れてしまう。(無駄な重複)
  • pg_cronを使うと定期的なpostgresコマンドの実行ができる
ShuSuzukiShuSuzuki
  • active recordがSQLクエリ作成 → postgresqlアダプタがpostgreSQLとの互換性チェック → pg gemがrubyからPostgresSQLへのクライアント接続を管理 → active recordはクライアント側のconnection poolを使用し、pool数を設定→ active recordはクエリを実行時にプールから接続をcheckout, → postgres側で問い合わせを受取処理する。→ active recordはconnectionを閉じる必要がある。(アイドル状態を維持することで既存の接続を再利用することもできる)

pg_stat_activityでコネクション状態を関しできる

-- Use \watch to continually monitor results
SELECT
pid,
datname,
usename, application_name, client_hostname, client_port, backend_start, query_start, query,
state
FROM pg_stat_activity
  • idle_session_timeoutでアイドル接続に上限を設けることができる
  • コネクション数とpumaやsidekiqのプロセス/スレッド数を対応させる必要がある
  • pg bouncerはossの接続プーラー。railsとpostgresの仲介約で、max接続を超えても拒否せず遅延させたり、接続を効率よく管理する。
  • 設定周りめっちゃつっかかっちゃった。でもpgbouncerに接続できたぞ!!!
~ ❯ psql -p 6432 -U owner pgbouncer                                                                                                6s
Password for user owner: 
psql (16.1, server 1.22.1/bouncer)
WARNING: psql major version 16, server major version 1.22.
         Some psql features might not work.
Type "help" for help.

pgbouncer=# SHOW HELP;
NOTICE:  Console usage
DETAIL:  
	SHOW HELP|CONFIG|DATABASES|POOLS|CLIENTS|SERVERS|USERS|VERSION
	SHOW PEERS|PEER_POOLS
	SHOW FDS|SOCKETS|ACTIVE_SOCKETS|LISTS|MEM|STATE
	SHOW DNS_HOSTS|DNS_ZONES
	SHOW STATS|STATS_TOTALS|STATS_AVERAGES|TOTALS
	SET key = arg
	RELOAD
	PAUSE [<db>]
	RESUME [<db>]
	DISABLE <db>
	ENABLE <db>
	RECONNECT [<db>]
	KILL <db>
	SUSPEND
	SHUTDOWN
	WAIT_CLOSE [<db>]
SHOW
pgbouncer=# 
pgbouncer=# SHOW DATABASES;
pgbouncer=# SHOW CLIENTS;
pgbouncer=# 


  • pgbouncerにはいろいろなpool modeがある
  • statement timeout, locktimeoutを設定するとより管理がしやすいかつ安全
  • 行ブロックは読み取りもブロックする排他ロック
    • lock_timeoutを活用、 no waitキーワードの使用が対策
  • https://www.citusdata.com/blog/2018/02/15/when-postgresql-blocks/
    • いい感じになにと何がlockするか書かれている
  • pgbadgerでlockに関するログ分析dhができる
  • acive record migration などは Advisory Locksが使われる
    • 保留中のマイグレーションを単一のプロセスに適用することを制限
ShuSuzukiShuSuzuki
  • gemを使ったSQLの実行は必ずしも適切なindexが使われているわけではない
  • limitとoffsetのページネーションは同時更新などが起こると順番が崩れる可能性がある。また行数が多く、非効率な可能性もある
  • ページネーションではCURSORキーワードという手段もある
    • ステートフルな設計になるらしい
  • キーセットページネーションというのもある
ShuSuzukiShuSuzuki
  • 読み込みは水平方向にスケールできるが書き込みは垂直方向のみ
  • TOTAL.times.to_a.in_groups_of(BATCH_SIZE), d.attributes.symbolize_keys.slice(ここらへんあんま使わないけど使えそうな処理
  • もうupsert/insertがない世界には戻れない
  • postgresで競合を解決するのは以下の2つがある
    • ON CONFLICT DO NOTHING ON CONFLICT DO UPDATE
  • activerecord-importをあんま使わなくなったなあ
  • PostgreSQLはCSVファイルからのデータの一括読み込みをサポートしています。
  • PostgreSQLでは、外部データラッパ(FDW)と呼ばれる機能を使用して、外部データソースに接続することができる
ShuSuzukiShuSuzuki

モチベがだんだん下がっていったので一旦終了。また最後のほうは別途読みたい。シャーディングとかパーティショニングあたりは別途勉強したいなあ

このスクラップは2024/03/25にクローズされました