web-dev-qa-db-ja.com

テーブルに既に適切なインデックスがある場合にビットマップヒープスキャンを削除する方法

私はPostgreSQL9.6を実行しています。これらは関連する定義です:

_CREATE TABLE IF NOT EXISTS instagram.profiles_1000 (
    id                          SERIAL PRIMARY KEY,
    username                    VARCHAR(255) NOT NULL UNIQUE,
    followers                   BIGINT,
    tsv                         TSVECTOR
);

CREATE UNIQUE INDEX IF NOT EXISTS instagram_username_index
    ON instagram.profiles_1000(username);
CREATE INDEX IF NOT EXISTS instagram_followers_index
    ON instagram.profiles_1000(followers);
CREATE INDEX IF NOT EXISTS instagram_textsearch_index
    ON instagram.profiles_1000 USING GIN(tsv);
_

そして、テキストベクトルはトリガーによって更新されます。

_CREATE FUNCTION instagram_documents_search_trigger() RETURNS trigger AS $$
begin
  new.tsv :=
        setweight(to_tsvector(COALESCE(new.username, '')), 'D') || ' ' ||
        setweight(to_tsvector(COALESCE(new.full_name, '')), 'C') || ' ' ||
        setweight(to_tsvector(COALESCE(new.location_country, '')), 'B') || ' ' ||
        setweight(to_tsvector(COALESCE(new.location_region, '')), 'B') || ' ' ||
        setweight(to_tsvector(COALESCE(new.biography, '')), 'A') || ' ' ||
        setweight(to_tsvector(COALESCE(new.location_city, '')), 'A');
  return new;
end
$$ LANGUAGE plpgsql;


CREATE TRIGGER instagram_tsvectorupdate BEFORE INSERT OR UPDATE
    ON instagram.profiles_1000 FOR EACH ROW
    EXECUTE PROCEDURE instagram_documents_search_trigger();
_

これはクエリです:

_select instagram.profiles_1000.*, categories, followers as rank                                                                                            
from instagram.profiles_1000
join plainto_tsquery('arts') as q on q @@ tsv
left outer join instagram.profile_categories_agg on instagram.profiles_1000.username = instagram.profile_categories_agg.username
where followers is not null and followers > 0
order by (followers, -id) desc
limit 50;
_

これはEXPLAIN (ANALYZE, BUFFERS)の出力です:

https://explain.depesz.com/s/ceCd

原因は、合計実行時間の大部分を占めるビットマップヒープスキャンです。率直に言って、特に_instagram_textsearch_index_のビットマップインデックススキャンがすでに検索語に従って行をフィルタリングしているため、なぜ必要なのかわかりません。

誰かが光を当てることはできますか?

[〜#〜] edit [〜#〜]説明出力を誤って読んだことが指摘されました。確かに、左外部結合には多くの時間がかかっていました。私はそれを次のように削除しようとしました:

_select instagram.profiles_1000.*, followers as rank
from instagram.profiles_1000
join plainto_tsquery('arts') as q on q @@ tsv                                              
where followers is not null and followers > 0
order by (followers, -id) desc
limit 50;
_

しかし、クエリはまだ13秒かかります!これはEXPLAIN (ANALYZE, BUFFERS)出力です:

https://explain.depesz.com/s/awfH

現在、ボトルネックは全文検索のようです。本当に遅いですか?テーブルには500万行しかなく、tsv(タイプTSVECTORを持つ)には次のインデックスでインデックスが付けられます。

_CREATE INDEX IF NOT EXISTS instagram_textsearch_index_1000
    ON instagram.profiles_1000 USING GIN(tsv);
_

EDIT 2検索に一致するプロファイル(常に最大50)のみを処理すれば、無駄のないクエリを作成できることに気付きました。このクエリを使用する:

_select p.*, categories
from
    (select id
    from instagram.profiles_1000, plainto_tsquery('arts') as q
    where q @@ tsv and followers is not null and followers > 0
    order by (followers, -id) desc
    limit 50) as ids
inner join instagram.profiles_1000 as p on
    p.id = ids.id
left outer join instagram.profile_categories_agg as c on
    c.username = p.username;
_

私はこの結果を得ることができます: https://explain.depesz.com/s/OvG

これにより、検索が約3秒になります。少なくとも1秒に到達した方がいいでしょう。

2
rubik

タイミングをさらに改善したい場合は、少なくとも@@一致条件が多くの結果を返す場合は、FTSインデックスの使用を中止するのが最善の方法です。

まず、ORDER BYをorder by (followers, -id) descからorder by followers desc, idに変更する必要があります。このバージョンは意味的には同等です(ただし、NULL値の処理方法を除いて)。2つの列を疑似行にパッケージ化し、それらの行の値を並べ替える必要はありません。列の値を直接並べ替えます。この直接ソートははるかに高速ですが、より重要なのは、ORDER BYを満たすために、ソートではなくインデックスを使用する可能性を広げることです。

次に、(followers desc, id)にインデックスを作成すると、クエリはそのインデックスをステップ実行して、@@条件を満たす行を探し、50個が見つかったら停止します。この方法で行うと、@@の一致である100,000を超える行を引き出し、それらを並べ替えて上位50を取り出すよりもはるかに高速になります。

1
jjanes

ここでの「犯人」は、合計時間の1/4未満を占めます。実際のボトルネックは、instagram_categories_username_category_aggを使用したインデックスのみのスキャンであり、0.200 * 118453 = 23690.6ミリ秒、ほぼ24秒かかりますが、これはほとんどの場合です。

驚くべき偶然ではない限り、すべてのユーザーが1つのカテゴリを持っているように見えます。なぜ、profiles_1000の属性ではなく、ユーザーカテゴリ用の別のテーブルがあるのですか。そのデザインが真の犯人のようです。

とにかく、ビットマップヒープスキャンを実行する必要があるのは、それが正しい答えに到達する方法だからです。ビットマップインデックススキャンのみを実行した場合、一致する行に関するデータはなく、アドレスのみが含まれます。また、可視性、再チェック、および不可逆なビットマップ圧縮のため、そのアドレスが実際に一致する行であるかどうかもわかりません。

通常のインデックススキャンもインデックスとテーブルの両方にアクセスします。EXPLAINの2つの異なるエントリがビットマップスキャンと同じように計画しているため、これらの操作は分離されません。

1
jjanes