SQLProgrammingSenior Database Engineer

PostgreSQLでの.interval型に対する.RANGEフレームでウィンドウ関数を評価するとき、プランナーがフルパーティションをメモリにマテリアライズする理由と、特定の.ORDER BY.式のどのプロパティが結果を変更せずに.ROWS.フレーミングに置き換えることを可能にするか?

Hintsage AIアシスタントで面接を突破

質問への回答

PostgreSQLは、現在の行の順序付け列から論理的な値オフセットを評価することでRANGEフレームを実装しています。フレーム境界に.interval型(例:INTERVAL '1 hour' PRECEDING)が関与する場合、エグゼキュータは、データセット全体でその時間ウィンドウ内に収まる行数が動的に変化するため、単純な物理行数を使ってフレームメンバーシップを決定することができません。正確性を確保するために、エンジンはすべてのソートされたパーティションを作業テーブルにマテリアライズし(work_memまたはディスクにスピル)、指定された範囲に収まる値を特定するためにすべての行をスキャンし、**O(partition size)**のメモリ複雑性をもたらします。

ORDER BY式がパーティション内の各行に対して一意のキーを構成する場合にのみ、ROWSフレーミングに安全に置き換えることができます。順序付け列に重複がない場合(または主キーのような二次的な一意の列で拡張されている場合)、物理的な行オフセット(ROWS)は論理的な値オフセット(RANGE)と意味的に同じになります。この一意性保証により、エンジンが値の一致する仲間をスキャンせずに、フレームに意図した行が正確に含まれることが保証され、**O(frame size)**のメモリを使用して固定サイズのリングバッファを用いたストリーミング実行モデルが可能になります。

生活での状況

高頻度取引プラットフォームは、ナノ秒精度の市場ティックデータを処理し、過去50ミリ秒の入札-提示スプレッドの移動平均を必要としました。最初の分析クエリはAVG(spread) OVER (PARTITION BY symbol ORDER BY nanos_ts RANGE BETWEEN INTERVAL '50 ms' PRECEDING AND CURRENT ROW)を利用しました。市場のボラティリティの間に、これはwork_memの枯渇を引き起こし、PostgreSQLが作業テーブルをディスクにスピルさせ、クエリのレイテンシがミリ秒から数十秒に低下し、リアルタイムのアルゴリズミックトレーディングには受け入れられないものでした。

エンジニアリングチームは、最初にデータベースサーバーを垂直にスケールアップして、最大のパーティション(高ボリュームシンボル)をメモリ全体に保持するのに十分なRAMを提供することを検討しました。これにより、ディスクのスピルは解消されましたが、コストは非常に高くつきました。最大のシンボルには数億のティックが含まれ、データベース接続ごとにテラバイトのRAMが必要であり、解決策は数千の同時トレーディングアルゴリズムに水平にスケールしませんでした。

二つ目の提案は、平均ティック密度(例:1000行が50msに相当するという仮定)から計算された固定ROWSオフセットを使用して50ミリ秒のウィンドウを近似することでした。このアプローチは、パーティションサイズに関係なく一定のメモリ使用を保証します。しかし、ティック密度は市場のクラッシュ中(ミリ秒あたり数千のティック)と静かな期間中(ティック間の数分)で大きく変化するため、行数の近似は恣意的に不正確で、監査トレイルのための正確な時間ウィンドウ計算を要求する財務規制に違反する可能性があります。

選ばれた解決策は、nanos_tstick_idが合成一意のキーを形成するという事実を利用しました。チームはクエリをORDER BY nanos_ts, tick_idを使用して再構築し、ROWS BETWEEN 1000 PRECEDING AND CURRENT ROWに切り替えました。タイムスタンプの一意性により、論理的な50ミリ秒の境界が通常の市場条件下で予測可能な物理行オフセットと常に整合していることが保証されたため、計算は正確なままで、PostgreSQLは制約のあるバッファを通じて行をストリームできました。クエリのレイテンシはミリ秒未満に低下し、メモリフットプリントは**O(1)**で安定し、システムはディスクにスピルすることなく10億行のパーティションを処理しました。

候補者がよく見逃すこと

ORDER BY列に重複値が含まれている場合、なぜデフォルトのフレーム句(RANGE UNBOUNDED PRECEDING)がROWS UNBOUNDED PRECEDINGとは異なる累積合計を生成するのか?

ウィンドウ関数が明示的なフレーム句を省略すると、PostgreSQLはデフォルトでRANGE UNBOUNDED PRECEDINGを使用します。このモードでは、同じORDER BY値を共有するすべての行が1つの仲間グループとして扱われ、すべてがフレームに同時に含まれます。したがって、ユーザーが同じ日に3回のトランザクションを持っている場合、すべての3行の累積合計は同じになり、すべての3行と前日の合計が表示されます。対照的に、ROWS UNBOUNDED PRECEDINGは合計を段階的に計算します。日の最初のトランザクションは、自身と前の日のみを含み、2番目は最初の2つを含むというように進んでいきます。候補者はこのデフォルトの動作を見逃すことが多く、日中の累積合計がその日の最終合計で「固定」されているように見える報告を生じさせ、時系列分析を破ってしまいます。

RANGEフレームを評価する際にORDER BY列のNULL値をPostgreSQLがどのように処理し、なぜ計算から行が静かに省かれる可能性があるのか?

SQLの三値論理において、NULLとの比較は等価ではなくUNKNOWNをもたらします。RANGEフレームでは、PostgreSQLは通常、有限範囲ウィンドウ(例:BETWEEN 1 PRECEDING AND 1 FOLLOWING)からNULL順位付け値を持つ行を除外します。これらの行は、隣接する行のフレームには見えない孤立した仲間グループを形成する可能性があります。データセットにNULLタイムスタンプ(レガシーデータまたは保留データを表す)が含まれている場合、RANGEを使用した移動平均はこれらの行を静かにドロップしますが、ROWSフレーミングはNULL値に関係なく物理的位置に基づいてそれらを含むため、分析集計に歪みを引き起こす可能性があります。

ORDER BY列が一意であることが保証されているとき、なぜ明示的なROWSフレーミングが大規模なデータセットではRANGEよりも望ましいのか、そしてこれが回避する内部操作は何か?

一意性がROWSRANGE間の意味的な同等性を保証している場合でも、RANGEキーワードの存在自体がPostgreSQLのエグゼキュータに仲間グループスキャンの可能性に備えさせます。これにより、Materializeノードがトリガーされ、すべてのソートされたパーティションを作業テーブルにバッファリングします(O(N)メモリを消費)してから行を出力します。「ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW」と明示的に宣言することで、プランナーに対して物理行のスライディングウィンドウのみが必要であることを示します。これにより、コストのかかるマテリアライゼーションステップを回避するストリーミングWindowAggノードが可能になり、メモリ使用量が**O(frame size)**に削減され、ディスクのスピルなしで10億行のパーティションを処理するのに重要です。