私のテーブルには、タイプchar(11)
主キーがあります。 このテーブルをクエリするとき、Dieselはパラメータ化されたクエリとbpchar
代わりにタイプtext
値を送信します。 これにより、インデックスのみのスキャンを実行する代わりに、低速のシーケンシャルスキャンが強制されます。 psqlから同様のクエリを実行する場合、PostgreSQLはインデックスを使用します。
インデックス付きのchar(n)
すばやく見つける
結果は数十ミリ秒になります
結果は1秒以上
番号
これを観察する必要があるのは、インデックス付きのchar(n)
列、ディーゼルの.find(
または.filter(id.eq(
、サーバー側のauto_explain
スキャンします。 しかし、これを実証するために完全なテストケースを設定しています。
完全を期すために、私は新しいDebian9.5でこれを行いました。
apt-get update
apt-get install --no-install-recommends libpq-dev curl git gcc libc-dev
adduser user
su user
curl https://sh.rustup.rs -sSf | sh
cargo install diesel_cli --no-default-features --features postgres
それから
su postgres
createuser -S -D -R -P diesel_bpchar
createdb -O diesel_bpchar diesel_bpchar
git clone https://github.com/ludios/diesel_bpchar
cd diesel_bpchar
cargo build
echo "DATABASE_URL=postgres://diesel_bpchar:PASS<strong i="49">@localhost</strong>:5432/diesel_bpchar" > .env
diesel setup
diesel migration run
for i in {00000000001..0005000000}; do echo $i >> /tmp/ids; done
psql postgres://diesel_bpchar:PASS<strong i="50">@localhost</strong>:5432/diesel_bpchar -c "COPY videos FROM STDIN" < /tmp/ids
代わりに、クイックインデックスルックアップは1秒かかります。
# time ./target/debug/find_video 00005000000
Found 1 videos
./target/debug/find_video 00005000000 0.01s user 0.00s system 1% cpu 0.998 total
これらのpostgresqlサーバー設定を使用すると、サーバー側で何が起こっているかを確認できます。
shared_preload_libraries = 'auto_explain' # (change requires restart)
auto_explain.log_min_duration = 0
auto_explain.log_analyze = true
auto_explain.log_verbose = true
log_statement = 'all' # none, ddl, mod, all
(注: auto_explain
はDebianのpostgresql-contrib
パッケージに含まれています。)
Dieselがパラメーター化されたクエリを送信すると、PostgreSQLは順次スキャンをログに記録します。
2018-11-10 17:35:00.414 UTC [29344] diesel_bpchar<strong i="15">@diesel_bpchar</strong> LOG: execute __diesel_stmt_0: SELECT "videos"."id" FROM "videos" WHERE "videos"."id" = $1
2018-11-10 17:35:00.414 UTC [29344] diesel_bpchar<strong i="16">@diesel_bpchar</strong> DETAIL: parameters: $1 = '00005000000'
2018-11-10 17:35:01.390 UTC [29344] diesel_bpchar<strong i="17">@diesel_bpchar</strong> LOG: duration: 976.167 ms plan:
Query Text: SELECT "videos"."id" FROM "videos" WHERE "videos"."id" = $1
Seq Scan on public.videos (cost=0.00..102028.00 rows=25000 width=12) (actual time=976.154..976.154 rows=1 loops=1)
Output: id
Filter: ((videos.id)::text = '00005000000'::text)
Rows Removed by Filter: 4999999
psql
から同様のクエリを実行すると、PostgreSQLはIndex Only Scan
ログに記録します。
# time psql postgres://diesel_bpchar:PASS<strong i="23">@localhost</strong>:5432/diesel_bpchar -c "SELECT videos.id FROM videos WHERE videos.id = '00005000000';"
id
-------------
00005000000
(1 row)
psql postgres://diesel_bpchar:PASS<strong i="24">@localhost</strong>:5432/diesel_bpchar 0.03s user 0.01s system 91% cpu 0.041 total
2018-11-10 17:39:55.150 UTC [31473] diesel_bpchar<strong i="27">@diesel_bpchar</strong> LOG: statement: SELECT videos.id FROM videos WHERE videos.id = '00005000000';
2018-11-10 17:39:55.150 UTC [31473] diesel_bpchar<strong i="28">@diesel_bpchar</strong> LOG: duration: 0.033 ms plan:
Query Text: SELECT videos.id FROM videos WHERE videos.id = '00005000000';
Index Only Scan using videos_pkey on public.videos (cost=0.43..8.45 rows=1 width=12) (actual time=0.028..0.028 rows=1 loops=1)
Output: id
Index Cond: (videos.id = '00005000000'::bpchar)
Heap Fetches: 1
考えられる回避策:
Text
変更しますChar(n)
マップする独自のSqlType
を定義します。 例については、このテストケースを参照してください(列挙型を文字列に置き換えるだけで、 #[postgres(type_name = "char")]
代わりに#[postgres(oid = "18", array_oid = "1002")]
を使用することもできます)。興味深いことに、そのSqlType
回避策に感謝します。 私は.filter(sql(&format!("id = '{}'", id)))
を使用していたため、SQLインジェクションを処理する必要がありました。
FWIW、私はこの正確な問題に遭遇し、PostgreSQLwikiの「Don'tDoThis」リストに追加の警告が追加されました: https :
制約付きでTEXT
に切り替えました。 この場合、ディーゼルがbpchar
を送るのはまだ正しいことだと思います。
ここにはいくつかのより深い問題があります。 このすべての背後にある歴史は少し散らばっているので、ここでいくつかの要約/コンテキストを示しましょう。
非常に昔、ディーゼルにはVarchar
とText
が別々のタイプとしてありました。 これはさまざまな理由で苦痛でした。 それらはPGでは別々のタイプとして扱われますが(varcharは実際にはもうありませんが、 varchar
を含むクエリを説明すると、常に無条件にtext
キャストされることがわかります)、PGそれらをほとんど交換可能にする暗黙の強制があります。
ただし、Rustの制限とアーキテクチャの組み合わせにより、ディーゼルでこれらの強制を簡単にモデル化することはできません。 impl<T: AsExpression<Text>> AsExpression<VarChar> for T
と言う簡単な方法はありません。 したがって、これは、 text_col.eq(varchar_col)
を記述できない、 lower
ような関数が一方の型でのみ機能し、もう一方の型では機能しない、独自のコードベースでの大量の重複など、非常にファンキーなことにつながります。
varcharをタイプエイリアスにするだけで、#288でこれを修正しました。 これは回避策であり、根本的な問題の修正ではありません。 それ以来、この回避策を使用できない同様の問題が発生しました。 数値型はSQLが強制される主要なケースですが、それはできません(ただし、これが実際に苦痛を伴うことはそれほど一般的ではありません)。 タイムスタンプとタイムスタンプは、型が相互に強制的に変換されるが、同じセマンティクスを持たず、「型エイリアスを使用する」ソリューションが機能しない別のケースです。
ただし、文字列は特別です。 これは実際には(ほとんど)ほとんどすべての文字列型で機能します。 これらのタイプはすべて同じ表現を持っています(一部の人々が信じていることに反して、 char
、 varchar
、 bpchar
などを使用してもtext
を使用するよりも利点はありません。 'すべてが内部でbytea
だけです)。 さらに、これらすべてのタイプには、それらに対応する同じRustタイプのセットがあります。 String
およびstr
。
しかし、これはいくつかの方法で私たちを噛むために戻ってきました。 最初に遭遇したのは、これらの強制はアレイには適用されないということでした。 varchar[]
が予想される場所ではtext[]
使用できません。 そのタイプの列に遭遇した場合、 diesel print-schema
とその友人に警告するのはこのためです。
私たちが遭遇した2番目の問題は、 text
がこれらの強制で常に「勝つ」ということです。 PGがsome_string_col = ''::text
または''::text = some_string_col
に遭遇すると、その列のタイプにテキストではなく、テキストにsome_string_col
をキャストします。 これと同じタイプのエイリアスの回避策をcitext
に使用しようとしたときに、最初にこれに遭遇しました。 バインドパラメータをtext
として送信すると、大文字と小文字を区別しない比較ではなく、大文字と小文字を区別する比較が行われます。 ここの場合も同様です。
とはいえ、ここで「根本的な」問題をどのように修正できるかはまだわかりません。 私がこれまでに思いついた最高のものは、これに明示的なキャストを追加することです。これは、少なくとも私たちにアウトを与えますが、それでも文字列型を分割することは非常に苦痛になります。
とはいえ、弦は「特別」だと私は信じており、ここでは特に弦の状況を改善できるかもしれないと思います。 送信するタイプのOIDをすべての場合に実際に知る必要はありません。また、「これを型なし文字列リテラルとして解釈する」ことを意味する0を送信することもできます。 これは、実際には、文字列型に対して送信されるバインドパラメータに必要なものとまったく同じである可能性があります。 (注:これを行う実装は、 Text
のOIDを0
変更するほど簡単ではありません。特に、の一部として送信する場合は、実際のOIDが必要になる場合があります。配列または複合型)
最も参考になるコメント
ここにはいくつかのより深い問題があります。 このすべての背後にある歴史は少し散らばっているので、ここでいくつかの要約/コンテキストを示しましょう。
非常に昔、ディーゼルには
Varchar
とText
が別々のタイプとしてありました。 これはさまざまな理由で苦痛でした。 それらはPGでは別々のタイプとして扱われますが(varcharは実際にはもうありませんが、varchar
を含むクエリを説明すると、常に無条件にtext
キャストされることがわかります)、PGそれらをほとんど交換可能にする暗黙の強制があります。ただし、Rustの制限とアーキテクチャの組み合わせにより、ディーゼルでこれらの強制を簡単にモデル化することはできません。
impl<T: AsExpression<Text>> AsExpression<VarChar> for T
と言う簡単な方法はありません。 したがって、これは、text_col.eq(varchar_col)
を記述できない、lower
ような関数が一方の型でのみ機能し、もう一方の型では機能しない、独自のコードベースでの大量の重複など、非常にファンキーなことにつながります。varcharをタイプエイリアスにするだけで、#288でこれを修正しました。 これは回避策であり、根本的な問題の修正ではありません。 それ以来、この回避策を使用できない同様の問題が発生しました。 数値型はSQLが強制される主要なケースですが、それはできません(ただし、これが実際に苦痛を伴うことはそれほど一般的ではありません)。 タイムスタンプとタイムスタンプは、型が相互に強制的に変換されるが、同じセマンティクスを持たず、「型エイリアスを使用する」ソリューションが機能しない別のケースです。
ただし、文字列は特別です。 これは実際には(ほとんど)ほとんどすべての文字列型で機能します。 これらのタイプはすべて同じ表現を持っています(一部の人々が信じていることに反して、
char
、varchar
、bpchar
などを使用してもtext
を使用するよりも利点はありません。 'すべてが内部でbytea
だけです)。 さらに、これらすべてのタイプには、それらに対応する同じRustタイプのセットがあります。String
およびstr
。しかし、これはいくつかの方法で私たちを噛むために戻ってきました。 最初に遭遇したのは、これらの強制はアレイには適用されないということでした。
varchar[]
が予想される場所ではtext[]
使用できません。 そのタイプの列に遭遇した場合、diesel print-schema
とその友人に警告するのはこのためです。私たちが遭遇した2番目の問題は、
text
がこれらの強制で常に「勝つ」ということです。 PGがsome_string_col = ''::text
または''::text = some_string_col
に遭遇すると、その列のタイプにテキストではなく、テキストにsome_string_col
をキャストします。 これと同じタイプのエイリアスの回避策をcitext
に使用しようとしたときに、最初にこれに遭遇しました。 バインドパラメータをtext
として送信すると、大文字と小文字を区別しない比較ではなく、大文字と小文字を区別する比較が行われます。 ここの場合も同様です。とはいえ、ここで「根本的な」問題をどのように修正できるかはまだわかりません。 私がこれまでに思いついた最高のものは、これに明示的なキャストを追加することです。これは、少なくとも私たちにアウトを与えますが、それでも文字列型を分割することは非常に苦痛になります。
とはいえ、弦は「特別」だと私は信じており、ここでは特に弦の状況を改善できるかもしれないと思います。 送信するタイプのOIDをすべての場合に実際に知る必要はありません。また、「これを型なし文字列リテラルとして解釈する」ことを意味する0を送信することもできます。 これは、実際には、文字列型に対して送信されるバインドパラメータに必要なものとまったく同じである可能性があります。 (注:これを行う実装は、
Text
のOIDを0
変更するほど簡単ではありません。特に、の一部として送信する場合は、実際のOIDが必要になる場合があります。配列または複合型)