新任Git管理者のための歴史改変入門
この講義について
役割としてGit管理者をしていたり、もしくは特段評価に反映されないのに他に詳しい人がいないからという理由からGit管理者みたいなことをする羽目になった人向けの履歴改変入門です。
前提
ここでの歴史改変とは、Gitリポジトリの各コミットに対して特定のファイルの除去もしくは追加、コミット情報を改変する事を指します。
歴史改変する前に
filter-branch
を使用して歴史改変を実施すると、各コミットのSHAハッシュが書き換わります。
つまり、改変前後で(master
などの)ブランチが指し示すコミットが変わってしまうため、リモートにプッシュしていないコミットがあった場合はプッシュできなくなります。
一般的な運用ではそのリポジトリを触っている関係者全員に歴史改変を実施する旨を伝え、都合の良いタイミングで作業を止めてもらい、すべてのコミットをプッシュしてから実施すべきです。
静止点を作らなくてはいけない都合上、そのリポジトリに関係する関係者が多ければ多いほどスケジュールの調整コストが増大します。
歴史改変手順
関係者とのスケジュールの調整
上記で述べた通り、歴史改変ではコミットハッシュが書き換わるためその旨を関係者全員に通知します。
また、どのタイミングのリモートの情報を使用して歴史改変をするか、歴史改変作業中に作業を止めてもらうのか、それともチェリーピックで対応してもらうのかなどを通知します。
関係者がGitに詳しくない場合は作業を止めてもらうのが一番確実です。
リモートブランチのクローン
作業に自信がない場合は、日々の作業で使用しているローカルブランチとは別に今回の作業用のローカルブランチをプルしてくることをお勧めします。
作業用のローカルブランチとは別に、インスタントなバックアップとして--bare
オプション付きでクローンしておくと安心かもしれません。
歴史改変
作業用のローカルブランチを使用して歴史改変を実行します。
確認・プッシュ
改変後のコミットを確認し、意図した内容に書き換わっているか確認します。
意図した内容になっている事が確認出来たら、既存のリモートリポジトリに--force
オプション付きでプッシュするか、新規のリモートリポジトリにプッシュします。
関係者に通知
作業が完了し、リモートへのプッシュが完了したら関係者に通知します。
その際、既存のローカルブランチを削除して再度リモートからクローンする必要がある旨を合わせて伝えると良いでしょう。
--tree-filter <command>
オプションについて
この講座では--tree-filter
を使用します。
--tree-filter
は以下の原理で動作します。
.git-rewrite/t
ディレクトリ(デフォルトから変更しない場合)以下にコミット時点のワーキングツリーの状態を再現する<command>
を実行する- オリジナルのコミットと
.git-rewrite/t
の内容を比較し、差分を抽出しコミットする - 上記の手順を指定されたブランチの全てのコミットに対して実行する
コマンド例
履歴から特定のファイルを消す場合
$ git filter-branch --tree-filter 'rm -f README_jp.md' -- HEAD
履歴にファイルを追加する場合
$ git filter-branch --tree-filter 'cp -p ~/source/orig/msg.txt .' -- HEAD
特定の人物の存在を無かったことにしたい場合
git filter-branch --env-filter ' if test "$GIT_AUTHOR_EMAIL" = "jyuch@users.noreply.github.com" then GIT_AUTHOR_NAME=nobody GIT_AUTHOR_EMAIL=nobody@jyuch.dev fi if test "$GIT_COMMITTER_EMAIL" = "jyuch@users.noreply.github.com" then GIT_COMMITTER_NAME=nobody GIT_COMMITTER_EMAIL=nobody@jyuch.dev fi ' -- HEAD
おわり
Postgresでも複数のインデックスが使用可能な状況ならカーディナリティが高いカラムのが優先的に使われるんだよね その2
はじめに
前回に引き続き、Postgresのオプティマイザの挙動を確認しようの会です。
tablename | attname | avg_width | n_distinct |
---|---|---|---|
table1 | c1 | 11 | -1 |
table1 | c2 | 8 | -0.55219007 |
table1 | c3 | 7 | 8332669 |
table1 | c4 | 6 | 930908 |
table1 | c5 | 5 | 98600 |
table1 | c6 | 4 | 9990 |
table1 | c7 | 3 | 1000 |
table1 | c8 | 2 | 100 |
table1 | c9 | 2 | 10 |
みたいなデータ分布のテーブルがあるとして、
create index table1_c1_c6 on table1 (c1, c6); create index table1_c6_c1 on table1 (c6, c1);
みたいなインデックスを貼ってc1 like '+%' and c6 = '5000'
のような大味なlike
演算子のクエリをブッパしたらどちらのインデクスが使われるんだろうねというお話です。
環境
今回からPostgreSQL 14.5をソースコードからコンパイルしてインストールする方法で検証します。
select version();
PostgreSQL 14.5 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 8.5.0 20210514 (Red Hat 8.5.0-10), 64-bit
みんな大好きRed Hat Enterprise Linuxです。
あとはクエリヒントを与えられるようにpg_hint_planをソースコードからコンパイルしてインストールしています。
環境構築
こんな感じにテーブルを作成し、
create table table1 ( id bigint primary key, c1 varchar(100), c2 varchar(100), c3 varchar(100), c4 varchar(100), c5 varchar(100), c6 varchar(100), c7 varchar(100), c8 varchar(100), c9 varchar(100) ); create index table1_c1_c6 on table1 (c1, c6); create index table1_c6_c1 on table1 (c6, c1);
こんな感じにデータを投入します。
synchronous_commit
パラメータとかを変えずにデータ投入をしたらなんか12時間くらいかかってました。つらい
using Dapper; using Npgsql; namespace ConsoleApp3 { internal class Program { private static readonly string[] Symbols = { "+", "-", "*", "/", "@", "" }; private static string GetSymbol(Random random) { var symbol = Symbols[random.Next(Symbols.Length)]; return symbol; } static void Main(string[] args) { using var db = new NpgsqlConnection("Host=192.168.2.100;Username=testdb;Database=testdb"); db.Open(); var tx = db.BeginTransaction(); var rand = new Random(); for (var i = 0; i < 100_000_000; i++) { if (i != 0 && i % 10000 == 0) { Console.WriteLine("{0}", i); tx.Commit(); tx = db.BeginTransaction(); } var n = rand.Next(); var p = new { Id = i, C1 = $"{GetSymbol(rand)}{i:0000000000}", C2 = n % 100_000_000, C3 = n % 10_000_000, C4 = n % 1_000_000, C5 = n % 100_000, C6 = n % 10_000, C7 = n % 1_000, C8 = n % 100, C9 = n % 10, }; db.Execute("insert into table1 (id, c1, c2, c3, c4, c5, c6, c7, c8, c9) values (@Id, @C1, @C2, @C3, @C4, @C5, @C6, @C7, @C8, @C9)", p, transaction: tx); } tx.Commit(); } } }
検証
以下のクエリを想定した場合、カーディナリティ自体が高いのはc1
ですが、このlike
演算子の使い方であればc6
が先頭に来ているインデックスを使用したほうが有利なのはデータの傾向を知っている人間であればすぐにわかります。
explain analyse select * from table1 t where c1 like '+%' and c6 = '5000';
この場合はちゃんとtable1_c6_c1
(c6
が先頭のインデックス)を使用してくれます。
Index Scan using table1_c6_c1 on table1 t (cost=0.57..1888.12 rows=1716 width=56) (actual time=0.027..2.256 rows=1728 loops=1) Index Cond: (((c6)::text = '5000'::text) AND ((c1)::text >= '+'::text) AND ((c1)::text < ','::text)) Filter: ((c1)::text ~~ '+%'::text) Planning Time: 0.109 ms Execution Time: 2.310 ms
また、c6
の条件をlike
演算子に変えてもちゃんとtable1_c6_c1
を優先的に使ってくれます。
explain analyse select * from table1 t where c1 like '+%' and c6 like '500%';
Bitmap Heap Scan on table1 t (cost=2312.27..23275.90 rows=1714 width=56) (actual time=14.581..355.768 rows=18441 loops=1) Filter: (((c1)::text ~~ '+%'::text) AND ((c6)::text ~~ '500%'::text)) Heap Blocks: exact=18289 -> Bitmap Index Scan on table1_c6_c1 (cost=0.00..2311.84 rows=19180 width=0) (actual time=12.230..12.231 rows=18441 loops=1) Index Cond: (((c6)::text >= '500'::text) AND ((c6)::text < '501'::text) AND ((c1)::text >= '+'::text) AND ((c1)::text < ','::text)) Planning Time: 0.091 ms Execution Time: 357.301 ms
まとめ
細かい挙動は追えていませんが、ちゃんと使ってほしいインデックスを選択してくれるようです。えらい
おまけ
pg_hint_planを使うと強制的に使うインデックスとかを弄れます。まぁ使わないに越したことはないですけど
explain analyse /*+ IndexScan(t table1_c1_c6) */ select * from table1 t where c1 like '+%' and c6 like '500%';
Index Scan using table1_c1_c6 on table1 t (cost=0.57..357692.13 rows=1714 width=56) (actual time=0.071..1810.166 rows=18441 loops=1) Index Cond: (((c1)::text >= '+'::text) AND ((c1)::text < ','::text) AND ((c6)::text >= '500'::text) AND ((c6)::text < '501'::text)) Filter: (((c1)::text ~~ '+%'::text) AND ((c6)::text ~~ '500%'::text)) Planning Time: 0.128 ms Execution Time: 1811.353 ms
おわり
Postgresでも複数のインデックスが使用可能な状況ならカーディナリティが高いカラムのが優先的に使われるんだよね
はじめに
タイトル通りです。
挙動ベースのお話でソースを追ったわけではないのです。
環境
select version();
PostgreSQL 14.5, compiled by Visual C++ build 1914, 64-bit
環境構築
こんな感じにクラスタを作成して
initdb --encoding=UTF8 --no-locale --username=postgres --pgdata=C:\pgdata\14
こんな感じにテーブルを作ります。
create table table1 ( id bigint primary key, c1 varchar(10) not null, c2 varchar(10) not null, c3 bigint not null, c4 bigint not null ); create index idx_table1_c1 on table1 (c1); create index idx_table1_c2 on table1 (c2); create index idx_table1_c3 on table1 (c3); create index idx_table1_c4 on table1 (c4);
そうしたらこんな感じにダミーデータをぶち込みます。
using Dapper; using Npgsql; namespace ConsoleApp1 { internal class Program { static void Main(string[] args) { using var db = new NpgsqlConnection("Host=localhost;Username=testdb;Database=testdb"); db.Open(); var tx = db.BeginTransaction(); var rand = new Random(); for (var i = 0; i < 10_000_000; i++) { if (i != 0 && i % 10000 == 0) { Console.WriteLine("{0}", i); tx.Commit(); tx = db.BeginTransaction(); } var n = rand.Next(); var p = new { Id = i, C1 = n, C2 = n % 1_000, C3 = n % 100_000, C4 = n % 1_000_000, }; db.Execute("insert into table1 (id, c1, c2, c3, c4) values (@Id, @C1, @C2, @C3, @C4)", p, transaction: tx); } tx.Commit(); } } }
あとは念のため
analyse table1;
しておきましょうか。
検証
統計情報のうち、個別値の推定値を見てみましょう。
select tablename, attname, n_distinct from pg_stats where tablename = 'table1' and attname <> 'id' order by attname;
tablename | attname | n_distinct |
---|---|---|
table1 | c1 | -1 |
table1 | c2 | 1000 |
table1 | c3 | 97508 |
table1 | c4 | 919503 |
n_distinct
の説明は以下の感じです。
c1は統計情報的には完全に固有の値とみなされています。あとはc4 → c3 → c2の順で個別値の数が多いと推定されています。
つまり、統計情報的にはc1 → c4 → c3 → c2の順でインデックスを使ったほうが有利になるはずです。
explain analyse select * from table1 where c4 = 1 and c3 = 1 and c2 = 1 and c1 = 1;
Index Scan using idx_table1_c1 on table1 (cost=0.43..2.66 rows=1 width=40) (actual time=0.017..0.017 rows=0 loops=1) Index Cond: (c1 = 1) Filter: ((c4 = 1) AND (c3 = 1) AND (c2 = 1)) Planning Time: 0.151 ms Execution Time: 0.035 ms
explain analyse select * from table1 where c4 = 1 and c3 = 1 and c2 = 1;
Bitmap Heap Scan on table1 (cost=4.18..5.29 rows=1 width=40) (actual time=0.106..0.193 rows=16 loops=1) Recheck Cond: ((c4 = 1) AND (c3 = 1)) Filter: (c2 = 1) Heap Blocks: exact=16 -> BitmapAnd (cost=4.18..4.18 rows=1 width=0) (actual time=0.094..0.095 rows=0 loops=1) -> Bitmap Index Scan on idx_table1_c4 (cost=0.00..1.62 rows=11 width=0) (actual time=0.068..0.068 rows=16 loops=1) Index Cond: (c4 = 1) -> Bitmap Index Scan on idx_table1_c3 (cost=0.00..2.31 rows=103 width=0) (actual time=0.023..0.023 rows=93 loops=1) Index Cond: (c3 = 1)
explain analyse select * from table1 where c3 = 1 and c2 = 1;
Bitmap Heap Scan on table1 (cost=86.17..87.29 rows=1 width=40) (actual time=1.511..1.682 rows=93 loops=1) Recheck Cond: ((c3 = 1) AND (c2 = 1)) Heap Blocks: exact=93 -> BitmapAnd (cost=86.17..86.17 rows=1 width=0) (actual time=1.498..1.499 rows=0 loops=1) -> Bitmap Index Scan on idx_table1_c3 (cost=0.00..2.31 rows=103 width=0) (actual time=0.019..0.019 rows=93 loops=1) Index Cond: (c3 = 1) -> Bitmap Index Scan on idx_table1_c2 (cost=0.00..83.61 rows=9917 width=0) (actual time=1.353..1.353 rows=10098 loops=1) Index Cond: (c2 = 1) Planning Time: 0.112 ms
explain analyse select * from table1 where c2 = 1;
Bitmap Heap Scan on table1 (cost=86.09..10193.41 rows=9917 width=40) (actual time=2.318..30.969 rows=10098 loops=1) Recheck Cond: (c2 = 1) Heap Blocks: exact=9530 -> Bitmap Index Scan on idx_table1_c2 (cost=0.00..83.61 rows=9917 width=0) (actual time=1.287..1.287 rows=10098 loops=1) Index Cond: (c2 = 1) Planning Time: 0.095 ms Execution Time: 31.413 ms
c1が条件に入っている場合はc1のインデックが使われています。
c1を条件から外すとc4とc3が個別にBitmap Index ScanされてからBitmapAndでなんかいい感じに処理されているっぽいです。
おわり