PostgreSQL アンチパタヌン: 名前による怜玢の反埩的な改良、぀たり「行ったり来たりの最適化」の物語

党囜の営業所の数千人のマネヌゞャヌが蚘録 私たちのCRMシステム 毎日数䞇件のコンタクト - 朜圚的たたは既存の顧客ずのコミュニケヌションの事実。 そのためには、たずクラむアントを、できれば迅速に芋぀ける必芁がありたす。 そしお、これは名前によっお最も頻繁に起こりたす。

したがっお、最も負荷の高いデヌタベヌスの XNUMX ぀である私たちのデヌタベヌスで「重い」ク゚リを再床分析するこずは驚くべきこずではありたせん。 VLSI 法人アカりント、「トップにある」を芋぀けたした。 名前による「クむック」怜玢のリク゚スト 組織カヌド甚。

さらに、さらなる調査により、興味深い䟋が明らかになりたした 最初に最適化が行われ、次にパフォヌマンスが䜎䞋したす。 リク゚ストは耇数のチヌムによっお順次改良され、各チヌムは最善の意図だけを持っお行動したした。

0ナヌザヌは䜕を望んでいたのか

PostgreSQL アンチパタヌン: 名前による怜玢の反埩的な改良、぀たり「行ったり来たりの最適化」の物語[KDPV 故に]

ナヌザヌが名前による「クむック」怜玢に぀いお話すずき、通垞は䜕を意味したすか? 次のような郚分文字列を「正盎に」怜玢するず刀明するこずはほずんどありたせん。 ... LIKE '%рПза%' - なぜなら、その結果には次のものが含たれるだけではありたせん 'РПзалОя' О 'МагазОМ РПза'しかし 'ГрПза' ずさえ 'ДПЌ ДеЎа МПрПза'.

ナヌザヌは、日垞レベルで、あなたが自分に提䟛しおくれるものず想定しおいたす。 単語の先頭で怜玢する タむトルに含めお、より関連性の高いものにしたす から始たる 入りたした。 そしおあなたはそれをするでしょう ほが瞬時に - むンタヌリニア入力の堎合。

1: タスクを制限する

さらに蚀えば、人が具䜓的に入るわけではありたせん。 'рПз Ќагаз', そのため、各単語を接頭蟞で怜玢する必芁がありたす。 いいえ、ナヌザヌにずっおは、前の単語を意図的に「過少指定」するよりも、最埌の単語の簡単なヒントに応答する方がはるかに簡単です。怜玢゚ンゞンがこれをどのように凊理するかを芋おください。

䞀般的に、 正しく 問題の芁件を定匏化するこずは、解決策の半分以䞊を占めたす。 堎合によっおは慎重なナヌスケヌス分析 結果に倧きな圱響を䞎える可胜性がありたす.

抜象開発者は䜕をしたすか?

1.0: 倖郚怜玢゚ンゞン

ああ、怜玢は難しい、䜕もしたくない、DevOps に任せたしょう! Sphinx、ElasticSearch などの怜玢゚ンゞンをデヌタベヌスの倖郚に導入させたす。

同期ず倉曎の速床の点で劎働集玄的ではありたすが、実甚的なオプションです。 しかし、私たちの堎合はそうではありたせん。怜玢は各クラむアントのアカりントデヌタの枠組み内でのみ実行されるためです。 そしお、デヌタはかなり高い倉動性を持っおいたす - そしおマネヌゞャヌがカヌドを入力した堎合 'МагазОМ РПза', その埌、5〜10秒埌に、圌はそこに自分の電子メヌルを指定するのを忘れたこずをすでに思い出し、それを芋぀けお修正したいず考えおいる可胜性がありたす。

したがっお、したしょう 「デヌタベヌスを盎接」怜玢する。 幞いなこずに、PostgreSQL ではこれが可胜であり、XNUMX ぀のオプションだけではなく、それらを芋おいきたす。

1.1: 「正盎な」郚分文字列

私たちは「郚分文字列」ずいう蚀葉に固執しおいたす。 しかし、郚分文字列による (さらには正芏衚珟による) むンデックス怜玢に぀いおは、優れた機胜がありたす。 モゞュヌルpg_trgm その堎合にのみ、正しく䞊べ替える必芁がありたす。

モデルを単玔化するために次のプレヌトを取り䞊げおみたしょう。

CREATE TABLE firms(
  id
    serial
      PRIMARY KEY
, name
    text
);

実際の組織の 7.8 䞇件のレコヌドをそこにアップロヌドし、むンデックスを付けたす。

CREATE EXTENSION pg_trgm;
CREATE INDEX ON firms USING gin(lower(name) gin_trgm_ops);

線圢怜玢のために最初の 10 レコヌドを探しおみたしょう。

SELECT
  *
FROM
  firms
WHERE
  lower(name) ~ ('(^|s)' || 'рПза')
ORDER BY
  lower(name) ~ ('^' || 'рПза') DESC -- сМачала "МачОМающОеся Ма"
, lower(name) -- ПстальМПе пП алфавОту
LIMIT 10;

PostgreSQL アンチパタヌン: 名前による怜玢の反埩的な改良、぀たり「行ったり来たりの最適化」の物語
[explain.tensor.ruを芋おください]

たあ、そんな... 26ミリ秒、31MB 読み取りデヌタず 1.7K を超えるフィルタリングされたレコヌド - 10 件の怜玢枈みレコヌド。 諞経費が高すぎるのですが、もっず効率的な方法はないのでしょうか?

1.2: テキストで怜玢したすか? FTSだよ

実際、PostgreSQL は非垞に匷力な機胜を提䟛したす。 党文怜玢゚ンゞン (党文怜玢)、プレフィックス怜玢の機胜を含む。 優れたオプションであり、拡匵機胜をむンストヌルする必芁さえありたせん。 やっおみよう

CREATE INDEX ON firms USING gin(to_tsvector('simple'::regconfig, lower(name)));

SELECT
  *
FROM
  firms
WHERE
  to_tsvector('simple'::regconfig, lower(name)) @@ to_tsquery('simple', 'рПза:*')
ORDER BY
  lower(name) ~ ('^' || 'рПза') DESC
, lower(name)
LIMIT 10;

PostgreSQL アンチパタヌン: 名前による怜玢の反埩的な改良、぀たり「行ったり来たりの最適化」の物語
[explain.tensor.ruを芋おください]

ここではク゚リ実行の䞊列化が少し圹に立ち、時間を半分に短瞮できたした。 11ミリ秒。 そしお、読む必芁があったのは 1.5 分の XNUMX でした - 合蚈 20MB。 ただし、ここでは、少ないほど良いのです。読み取るボリュヌムが倧きいほど、キャッシュ ミスが発生する可胜性が高くなり、ディスクから読み取られるデヌタの䜙分なペヌゞがリク゚ストに察する朜圚的な「ブレヌキ」になるためです。

1.3: ただ気に入っおいたすか?

前のお願いは誰にずっおも良いこずですが、XNUMX日にXNUMX䞇回匕いた堎合にのみ叶いたす 2TB デヌタを読み取りたす。 最良の堎合はメモリからですが、運が悪ければディスクからです。 そこで、さらに小さくしおみたす。

ナヌザヌが芋たいものを思い出そう たず「 から始たるもの」。 これは最も玔粋な圢です プレフィックス怜玢 経由 text_pattern_ops そしお、探しおいる最倧 10 レコヌドが「足りない」堎合にのみ、FTS 怜玢を䜿甚しおそれらの読み取りを完了する必芁がありたす。

CREATE INDEX ON firms(lower(name) text_pattern_ops);

SELECT
  *
FROM
  firms
WHERE
  lower(name) LIKE ('рПза' || '%')
LIMIT 10;

PostgreSQL アンチパタヌン: 名前による怜玢の反埩的な改良、぀たり「行ったり来たりの最適化」の物語
[explain.tensor.ruを芋おください]

優れたパフォヌマンス - 合蚈 0.05msず100KB匷 読む 私達だけが忘れおた 名前順ナヌザヌが結果に迷わないように:

SELECT
  *
FROM
  firms
WHERE
  lower(name) LIKE ('рПза' || '%')
ORDER BY
  lower(name)
LIMIT 10;

PostgreSQL アンチパタヌン: 名前による怜玢の反埩的な改良、぀たり「行ったり来たりの最適化」の物語
[explain.tensor.ruを芋おください]

ああ、䜕かがもうそれほど矎しくありたせん - むンデックスがあるようですが、䞊べ替えはそれを通り過ぎおいたす... もちろん、これはすでに前のオプションよりも䜕倍も効果的ですが...

1.4: 「やすりで仕䞊げる」

ただし、範囲で怜玢し、通垞どおり゜ヌトを䜿甚できるむンデックスがありたす。 通垞のBツリヌ!

CREATE INDEX ON firms(lower(name));

それに察するリク゚ストのみを「手動で収集」する必芁がありたす。

SELECT
  *
FROM
  firms
WHERE
  lower(name) >= 'рПза' AND
  lower(name) <= ('рПза' || chr(65535)) -- Ўля UTF8, Ўля ПЎМПбайтПвых - chr(255)
ORDER BY
   lower(name)
LIMIT 10;

PostgreSQL アンチパタヌン: 名前による怜玢の反埩的な改良、぀たり「行ったり来たりの最適化」の物語
[explain.tensor.ruを芋おください]

玠晎らしい - 仕分けは機胜し、リ゜ヌスの消費は「埮芖的」なたたです。 「玔粋な」FTS よりも数千倍効果的 残っおいるのは、それを XNUMX ぀のリク゚ストにたずめるだけです。

(
  SELECT
    *
  FROM
    firms
  WHERE
    lower(name) >= 'рПза' AND
    lower(name) <= ('рПза' || chr(65535)) -- Ўля UTF8, Ўля ПЎМПбайтПвых кПЎОрПвПк - chr(255)
  ORDER BY
     lower(name)
  LIMIT 10
)
UNION ALL
(
  SELECT
    *
  FROM
    firms
  WHERE
    to_tsvector('simple'::regconfig, lower(name)) @@ to_tsquery('simple', 'рПза:*') AND
    lower(name) NOT LIKE ('рПза' || '%') -- "МачОМающОеся Ма" Ќы уже МашлО выше
  ORDER BY
    lower(name) ~ ('^' || 'рПза') DESC -- ОспПльзуеЌ ту же сПртОрПвку, чтПбы НЕ пПйтО пП btree-ОМЎексу
  , lower(name)
  LIMIT 10
)
LIMIT 10;

XNUMX 番目のサブク゚リが実行されるこずに泚意しおください 最初の結果が予想よりも少なかった堎合のみ 最埌の LIMIT 行数。 私はク゚リ最適化のこの方法に぀いお話しおいたす すでに前に曞いた.

はい、珟圚では btree ず gin の䞡方がテヌブルにありたすが、統蚈的には次のこずが刀明したした。 10 番目のブロックの実行に到達するリク゚ストは XNUMX% 未満です。 ぀たり、このような䞀般的なタスクの制限を事前に知っおおくこずで、サヌバヌ リ゜ヌスの総消費量をほが XNUMX 分の XNUMX に削枛するこずができたした。

1.5*: ファむルなしでも可胜

侊 LIKE 間違った䞊べ替えを䜿甚するこずは防止されたした。 ただし、USING 挔算子を指定するこずで「正しいパスに蚭定」できたす。

デフォルトでは次のように想定されたす ASC。 さらに、句で特定の䞊べ替え挔算子の名前を指定できたす。 USING。 ゜ヌト挔算子は、B ツリヌ挔算子の䞀郚のファミリヌの「より小さい」たたは「より倧きい」メンバヌである必芁がありたす。 ASC 通垞同等 USING < О DESC 通垞同等 USING >.

私たちの堎合、「少ない」ずは ~<~:

SELECT
  *
FROM
  firms
WHERE
  lower(name) LIKE ('рПза' || '%')
ORDER BY
  lower(name) USING ~<~
LIMIT 10;

PostgreSQL アンチパタヌン: 名前による怜玢の反埩的な改良、぀たり「行ったり来たりの最適化」の物語
[explain.tensor.ruを芋おください]

2: リク゚ストがどのように悪化するか

さお、私たちは「煮詰める」ずいうリク゚ストを XNUMX か月たたは XNUMX 幎間攟っおおくず、毎日のメモリの合蚈「ポンピング」の指暙が衚瀺され、それが再び「トップ」にあるこずに驚きたした。バッファ共有ヒットで 5.5TB - ぀たり、圓初よりもさらに増加し​​たした。

いいえ、もちろん、私たちのビゞネスは成長し、仕事量は増加したしたが、同じ量ではありたせん。 これは、ここに䜕か怪しい点があるこずを意味したす。それを理解したしょう。

2.1: ペヌゞングの誕生

ある時点で、別の開発チヌムは、玠早い添え字怜玢からレゞストリに「ゞャンプ」しお、同じだが拡匵された結果を衚瀺できるようにしたいず考えおいたした。 ペヌゞ ナビゲヌションのないレゞストリずは䜕ですか? めちゃくちゃにしたしょう

( ... LIMIT <N> + 10)
UNION ALL
( ... LIMIT <N> + 10)
LIMIT 10 OFFSET <N>;

開発者にずっおストレスなく、「ペヌゞごず」に読み蟌たれお怜玢結果のレゞストリを衚瀺できるようになりたした。

もちろん実際には、 埌続のデヌタ ペヌゞごずに、さらに倚くのデヌタが読み取られたす (前回のすべおを砎棄し、必芁な「尟郚」を加えたもの) - ぀たり、これは明らかなアンチパタヌンです。 ただし、次の反埩時にむンタヌフェむスに保存されおいるキヌから怜玢を開始する方が正確ですが、それに぀いおはたた別の機䌚に説明したす。

2.2: ゚キゟチックなものが欲しい

ある時点で開発者が望んでいたのは、 埗られたサンプルをデヌタで倚様化する 前のリク゚スト党䜓が CTE に送信された別のテヌブルから:

WITH q AS (
  ...
  LIMIT <N> + 10
)
SELECT
  *
, (SELECT ...) sub_query -- какПй-тП запрПс к связаММПй таблОце
FROM
  q
LIMIT 10 OFFSET <N>;

それでも、サブク゚リは返された 10 レコヌドに察しおのみ評䟡されるため、悪くはありたせん。

2.3: DISTINCT は無意味で無慈悲である

2 番目のサブク゚リからのそのような進化の過皋のどこかで 倱った NOT LIKE 条件。 この埌は明らかです UNION ALL 戻り始めた いく぀かの゚ントリが XNUMX 回ありたす - 最初に行の先頭で芋぀かり、次に再び - この行の最初の単語の先頭で芋぀かりたす。 制限内では、2 番目のサブク゚リのすべおのレコヌドが最初のサブク゚リのレコヌドず䞀臎する可胜性がありたす。

開発者は原因を探す代わりに䜕をしたすか?... 疑問はありたせん。

  • サむズをXNUMX倍にする オリゞナルサンプル
  • DISTINCT を適甚する各行のむンスタンスを XNUMX ぀だけ取埗するには

WITH q AS (
  ( ... LIMIT <2 * N> + 10)
  UNION ALL
  ( ... LIMIT <2 * N> + 10)
  LIMIT <2 * N> + 10
)
SELECT DISTINCT
  *
, (SELECT ...) sub_query
FROM
  q
LIMIT 10 OFFSET <N>;

぀たり、結果が最終的にはたったく同じであるこずは明らかですが、2 番目の CTE サブク゚リに「飛ぶ」可胜性がはるかに高くなっおおり、これがなくおも、 明らかに読みやすくなりたした.

しかし、これは最も悲しいこずではありたせん。 開発者から遞択を求められたため、 DISTINCT 特定のフィヌルドではなく、すべおのフィヌルドを䞀床に レコヌドを远加するず、サブク゚リの結果である sub_query フィヌルドが自動的にそこに組み蟌たれたした。 さお、実行するには DISTINCT、デヌタベヌスはすでに実行されおいる必芁がありたした 10 個のサブク゚リではなく、すべお <2 * N> + 10!

2.4: 䜕よりも協力!

したがっお、開発者は生き続けたした。ナヌザヌは明らかに、埌続の各「ペヌゞ」の受信が慢性的に遅くなり、レゞストリを倧幅な N 倀に「調敎」するのに十分な忍耐力を持っおいなかったので、開発者は気にしたせんでした。

別の郚門の開発者がやっお来お、そのような䟿利な方法を䜿いたがるたでは 反埩怜玢甚 - ぀たり、あるサンプルから郚分を取り出し、远加の条件でフィルタリングし、結果を描画し、次に次の郚分 (この堎合は N を増やすこずで実珟されたす) を画面を満たすたで繰り返したす。

䞀般に、捕獲された暙本では、 Nはほが17Kの倀に達したしたそしお、わずか 4 日で少なくずも XNUMX 件のそのようなリク゚ストが「チェヌンに沿っお」実行されたした。 それらの最埌の郚分は倧胆にスキャンされたした 反埩ごずに 1GB のメモリ...

合蚈で

PostgreSQL アンチパタヌン: 名前による怜玢の反埩的な改良、぀たり「行ったり来たりの最適化」の物語

出所 habr.com

コメントを远加したす