sql >> データベース >  >> RDS >> PostgreSQL

GROUP BYクエリを最適化して、ユーザーごとに最新の行を取得します

    最高の読み取りパフォーマンスを得るには、複数列のインデックスが必要です:

    CREATE INDEX log_combo_idx
    ON log (user_id, log_date DESC NULLS LAST);
    

    インデックスのみのスキャンを行う 可能であれば、それ以外の場合は不要な列payloadを追加します INCLUDEを使用したカバーインデックス 条項(Postgres 11以降):

    CREATE INDEX log_combo_covering_idx
    ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);
    

    参照:

    • PostgreSQLでインデックスをカバーすると、列の結合に役立ちますか?

    古いバージョンのフォールバック:

    CREATE INDEX log_combo_covering_idx
    ON log (user_id, log_date DESC NULLS LAST, payload);
    

    DESC NULLS LASTが選ばれる理由 ?

    • 日付範囲クエリで未使用のインデックス

    少数の場合 user_idあたりの行数 または小さなテーブルDISTINCT ON 通常、最も速くて簡単です:

    • 各GROUPBYグループの最初の行を選択しますか?

    多くの場合 user_idあたりの行数 インデックススキップスキャン (またはルーズインデックススキャン )は(はるかに)より効率的です。これはPostgres12までは実装されていません-Postgres14の作業は進行中ですが、効率的にエミュレートする方法があります。

    一般的なテーブル式にはPostgres8.4+が必要です 。
    LATERAL Postgresが必要9.3+
    次のソリューションは、 Postgres Wikiでカバーされているものを超えています 。

    1。一意のユーザーを含む個別のテーブルはありません

    別のusers 表、2のソリューション。 以下は通常、より単純で高速です。スキップしてください。

    1a。 LATERALを使用した再帰CTE 参加

    WITH RECURSIVE cte AS (
       (                                -- parentheses required
       SELECT user_id, log_date, payload
       FROM   log
       WHERE  log_date <= :mydate
       ORDER  BY user_id, log_date DESC NULLS LAST
       LIMIT  1
       )
       UNION ALL
       SELECT l.*
       FROM   cte c
       CROSS  JOIN LATERAL (
          SELECT l.user_id, l.log_date, l.payload
          FROM   log l
          WHERE  l.user_id > c.user_id  -- lateral reference
          AND    log_date <= :mydate    -- repeat condition
          ORDER  BY l.user_id, l.log_date DESC NULLS LAST
          LIMIT  1
          ) l
       )
    TABLE  cte
    ORDER  BY user_id;
    

    これは任意の列を取得するのが簡単で、おそらく現在のPostgresで最適です。 2a。の章で詳しく説明します。 以下。

    1b。相関サブクエリを使用した再帰CTE

    WITH RECURSIVE cte AS (
       (                                           -- parentheses required
       SELECT l AS my_row                          -- whole row
       FROM   log l
       WHERE  log_date <= :mydate
       ORDER  BY user_id, log_date DESC NULLS LAST
       LIMIT  1
       )
       UNION ALL
       SELECT (SELECT l                            -- whole row
               FROM   log l
               WHERE  l.user_id > (c.my_row).user_id
               AND    l.log_date <= :mydate        -- repeat condition
               ORDER  BY l.user_id, l.log_date DESC NULLS LAST
               LIMIT  1)
       FROM   cte c
       WHERE  (c.my_row).user_id IS NOT NULL       -- note parentheses
       )
    SELECT (my_row).*                              -- decompose row
    FROM   cte
    WHERE  (my_row).user_id IS NOT NULL
    ORDER  BY (my_row).user_id;
    

    単一の列を取得するのに便利 または行全体 。この例では、テーブルの行タイプ全体を使用しています。他のバリエーションも可能です。

    前の反復で行が見つかったことを表明するには、単一のNOT NULL列(主キーなど)をテストします。

    このクエリの詳細については、第2b章を参照してください。以下。

    関連:

    • 行ごとに最後のN個の関連行をクエリする
    • PostgreSQLで別の列で並べ替えるときに1つの列でGROUPBYする

    2。個別のusers テーブル

    関連するuser_idごとに1行だけであれば、テーブルのレイアウトはほとんど問題になりません。 保証されています。例:

    CREATE TABLE users (
       user_id  serial PRIMARY KEY
     , username text NOT NULL
    );
    

    理想的には、テーブルはlogと同期して物理的に並べ替えられます テーブル。参照:

    • Postgresタイムスタンプクエリ範囲を最適化する

    または、それがほとんど問題にならないほど十分に小さい(カーディナリティが低い)。それ以外の場合、クエリ内の行を並べ替えると、パフォーマンスをさらに最適化するのに役立ちます。 GangLiangの追加を参照してください。 usersの物理的な並べ替え順序の場合 テーブルがたまたまlogのインデックスと一致している 、これは無関係かもしれません。

    2a。 LATERAL 参加

    SELECT u.user_id, l.log_date, l.payload
    FROM   users u
    CROSS  JOIN LATERAL (
       SELECT l.log_date, l.payload
       FROM   log l
       WHERE  l.user_id = u.user_id         -- lateral reference
       AND    l.log_date <= :mydate
       ORDER  BY l.log_date DESC NULLS LAST
       LIMIT  1
       ) l;
    

    JOIN LATERAL 先行するFROMを参照できます 同じクエリレベルのアイテム。参照:

    • LATERAL JOINとPostgreSQLのサブクエリの違いは何ですか?

    ユーザーごとに1つのインデックス(のみ)のルックアップが発生します。

    usersに欠落しているユーザーの行を返しません テーブル。通常、外部キー 参照整合性を強制する制約はそれを除外します。

    また、logのエントリと一致しないユーザーの行はありません -元の質問に準拠しています。これらのユーザーを結果に残すには、 LEFT JOIN LATERAL ... ON trueを使用します CROSS JOIN LATERALの代わりに :

    • 配列引数を使用して集合を返す関数を複数回呼び出す

    LIMIT nを使用します LIMIT 1の代わりに 複数の行を取得する (すべてではありませんが)ユーザーごと。

    事実上、これらはすべて同じことをします:

    JOIN LATERAL ... ON true
    CROSS JOIN LATERAL ...
    , LATERAL ...
    

    ただし、最後のものの優先度は低くなります。明示的なJOIN カンマの前にバインドします。その微妙な違いは、結合テーブルが増えると問題になる可能性があります。参照:

    • Postgresクエリの「テーブルのFROM句エントリへの無効な参照」

    2b。相関サブクエリ

    単一の列を取得するのに適しています 単一行から 。コード例:

    • グループごとの最大クエリを最適化する

    複数の列でも同じことが可能です。 、しかしあなたはもっと賢い必要があります:

    CREATE TEMP TABLE combo (log_date date, payload int);
    
    SELECT user_id, (combo1).*              -- note parentheses
    FROM (
       SELECT u.user_id
            , (SELECT (l.log_date, l.payload)::combo
               FROM   log l
               WHERE  l.user_id = u.user_id
               AND    l.log_date <= :mydate
               ORDER  BY l.log_date DESC NULLS LAST
               LIMIT  1) AS combo1
       FROM   users u
       ) sub;
    

    LEFT JOIN LATERALのように 上記のように、このバリアントにはすべてが含まれます logにエントリがなくてもユーザー 。 NULLを取得します combo1の場合 、WHEREで簡単にフィルタリングできます 必要に応じて、外部クエリの句。
    Nitpick:外部クエリでは、サブクエリで行が見つからなかったのか、すべての列の値がNULLであるのかを区別できません。同じ結果です。 NOT NULLが必要です このあいまいさを避けるために、サブクエリの列。

    相関サブクエリは、単一の値のみを返すことができます 。複数の列を複合型にラップできます。しかし、後でそれを分解するために、Postgresはよく知られている複合型を要求します。匿名レコードは、列定義リストを提供する場合にのみ分解できます。
    既存のテーブルの行タイプのように、登録されたタイプを使用してください。または、複合型をCREATE TYPEに明示的に(そして永続的に)登録します 。または、一時テーブル(セッションの終了時に自動的に削除される)を作成して、その行タイプを一時的に登録します。キャスト構文:(log_date, payload)::combo

    最後に、combo1を分解したくありません 同じクエリレベルで。クエリプランナーの弱点により、これはサブクエリを列ごとに1回評価します(Postgres 12でも当てはまります)。代わりに、それをサブクエリにして、外部クエリで分解します。

    関連:

    • グループごとに最初と最後の行から値を取得します

    10万のログエントリと1kのユーザーを使用した4つのクエリすべてのデモンストレーション:
    db <> fiddle here --pg 11
    古いsqlfiddle



    1. SQLite Rtrim()のしくみ

    2. Access 2016 For Dummies Cheat Sheet

    3. MySQLの単一引用符、二重引用符、逆引用符の使用法の説明

    4. MapReduceがHadoopでどのように機能するか