Diesel: PostgreSQLのインデックス付きchar(n)列のクエリは、シーケンシャルスキャンを使用します

作成日 2018年11月10日  ·  4コメント  ·  ソース: diesel-rs/diesel

設定

バージョン

  • さび: rustc 1.30.1(1433507eb 2018-11-07)
  • ディーゼル: 1.3.3
  • データベース: postgresql-9.6 9.6.10-0 + deb9u1またはpostgresl- 1111.1-1.pgdg90 + 1 (同様の結果)
  • オペレーティングシステムDebian9.5

機能フラグ

  • ディーゼル: postgres

問題の説明

私のテーブルには、タイプ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

チェックリスト

  • [x]私はすでに同様の問題について課題追跡システムを調べました。
  • [x]この問題は、Rustの安定したチャネルで再現できます。 (あなたの問題は
    そうでない場合は閉じます)
bug postgres

最も参考になるコメント

ここにはいくつかのより深い問題があります。 このすべての背後にある歴史は少し散らばっているので、ここでいくつかの要約/コンテキストを示しましょう。

非常に昔、ディーゼルにはVarcharTextが別々のタイプとしてありました。 これはさまざまな理由で苦痛でした。 それらはPGでは別々のタイプとして扱われますが(varcharは実際にはもうありませんが、 varcharを含むクエリを説明すると、常に無条件にtextキャストされることがわかります)、PGそれらをほとんど交換可能にする暗黙の強制があります。

ただし、Rustの制限とアーキテクチャの組み合わせにより、ディーゼルでこれらの強制を簡単にモデル化することはできません。 impl<T: AsExpression<Text>> AsExpression<VarChar> for Tと言う簡単な方法はありません。 したがって、これは、 text_col.eq(varchar_col)を記述できない、 lowerような関数が一方の型でのみ機能し、もう一方の型では機能しない、独自のコードベースでの大量の重複など、非常にファンキーなことにつながります。

varcharをタイプエイリアスにするだけで、#288でこれを修正しました。 これは回避策であり、根本的な問題の修正ではありません。 それ以来、この回避策を使用できない同様の問題が発生しました。 数値型はSQLが強制される主要なケースですが、それはできません(ただし、これが実際に苦痛を伴うことはそれほど一般的ではありません)。 タイムスタンプとタイムスタンプは、型が相互に強制的に変換されるが、同じセマンティクスを持たず、「型エイリアスを使用する」ソリューションが機能しない別のケースです。

ただし、文字列は特別です。 これは実際には(ほとんど)ほとんどすべての文字列型で機能します。 これらのタイプはすべて同じ表現を持っています(一部の人々が信じていることに反して、 charvarcharbpcharなどを使用しても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が必要になる場合があります。配列または複合型)

全てのコメント4件

考えられる回避策:

  • 可能であれば、列のタイプを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を送るのはまだ正しいことだと思います。

ここにはいくつかのより深い問題があります。 このすべての背後にある歴史は少し散らばっているので、ここでいくつかの要約/コンテキストを示しましょう。

非常に昔、ディーゼルにはVarcharTextが別々のタイプとしてありました。 これはさまざまな理由で苦痛でした。 それらはPGでは別々のタイプとして扱われますが(varcharは実際にはもうありませんが、 varcharを含むクエリを説明すると、常に無条件にtextキャストされることがわかります)、PGそれらをほとんど交換可能にする暗黙の強制があります。

ただし、Rustの制限とアーキテクチャの組み合わせにより、ディーゼルでこれらの強制を簡単にモデル化することはできません。 impl<T: AsExpression<Text>> AsExpression<VarChar> for Tと言う簡単な方法はありません。 したがって、これは、 text_col.eq(varchar_col)を記述できない、 lowerような関数が一方の型でのみ機能し、もう一方の型では機能しない、独自のコードベースでの大量の重複など、非常にファンキーなことにつながります。

varcharをタイプエイリアスにするだけで、#288でこれを修正しました。 これは回避策であり、根本的な問題の修正ではありません。 それ以来、この回避策を使用できない同様の問題が発生しました。 数値型はSQLが強制される主要なケースですが、それはできません(ただし、これが実際に苦痛を伴うことはそれほど一般的ではありません)。 タイムスタンプとタイムスタンプは、型が相互に強制的に変換されるが、同じセマンティクスを持たず、「型エイリアスを使用する」ソリューションが機能しない別のケースです。

ただし、文字列は特別です。 これは実際には(ほとんど)ほとんどすべての文字列型で機能します。 これらのタイプはすべて同じ表現を持っています(一部の人々が信じていることに反して、 charvarcharbpcharなどを使用しても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が必要になる場合があります。配列または複合型)

このページは役に立ちましたか?
0 / 5 - 0 評価