PostgreSQL
 sql >> Teknologi Basis Data >  >> RDS >> PostgreSQL

Optimalkan kueri GROUP BY untuk mengambil baris terbaru per pengguna

Untuk kinerja membaca terbaik, Anda memerlukan indeks multikolom:

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

Untuk membuat pemindaian indeks saja mungkin, tambahkan kolom payload jika tidak diperlukan dalam indeks penutup dengan INCLUDE klausa (Postgres 11 atau lebih baru):

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

Lihat:

  • Apakah menutupi indeks di PostgreSQL membantu GABUNG kolom?

Pengganti untuk versi yang lebih lama:

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

Mengapa DESC NULLS LAST ?

  • Indeks yang tidak digunakan dalam kueri rentang tanggal

Untuk sedikit baris per user_id atau meja kecil DISTINCT ON biasanya tercepat dan paling sederhana:

  • Pilih baris pertama di setiap grup GROUP BY?

Untuk banyak baris per user_id pemindaian lewati indeks (atau pemindaian indeks longgar ) adalah (jauh) lebih efisien. Itu tidak diterapkan hingga Postgres 12 - pekerjaan sedang berlangsung untuk Postgres 14. Tetapi ada cara untuk menirunya secara efisien.

Ekspresi Tabel Umum memerlukan Postgres 8.4+ .
LATERAL membutuhkan Postgres 9.3+ .
Solusi berikut melampaui apa yang tercakup dalam Wiki Postgres .

1. Tidak ada tabel terpisah dengan pengguna unik

Dengan users yang terpisah tabel, solusi di 2. di bawah ini biasanya lebih sederhana dan lebih cepat. Lewati saja.

1a. CTE rekursif dengan LATERAL bergabung

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;

Ini mudah untuk mengambil kolom arbitrer dan mungkin yang terbaik di Postgres saat ini. Penjelasan lebih lanjut di bab 2a. di bawah.

1b. CTE rekursif dengan subquery yang berkorelasi

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;

Nyaman untuk mengambil kolom tunggal atau seluruh baris . Contoh menggunakan seluruh jenis baris tabel. Varian lain dimungkinkan.

Untuk menegaskan baris yang ditemukan pada iterasi sebelumnya, uji satu kolom NOT NULL (seperti kunci utama).

Penjelasan lebih lanjut untuk kueri ini di bab 2b. di bawah.

Terkait:

  • Kueri N baris terkait terakhir per baris
  • GROUP BY satu kolom, sambil menyortir menurut yang lain di PostgreSQL

2. Dengan users yang terpisah tabel

Tata letak tabel hampir tidak penting selama tepat satu baris per user_id yang relevan dijamin. Contoh:

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

Idealnya, tabel secara fisik diurutkan secara sinkron dengan log meja. Lihat:

  • Optimalkan rentang kueri cap waktu Postgres

Atau cukup kecil (kardinalitas rendah) sehingga tidak terlalu penting. Selain itu, pengurutan baris dalam kueri dapat membantu mengoptimalkan kinerja lebih lanjut. Lihat tambahan Gang Liang. Jika urutan sortir fisik users tabel kebetulan cocok dengan indeks pada log , ini mungkin tidak relevan.

2a. LATERAL bergabung

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 memungkinkan untuk referensi FROM sebelumnya item pada tingkat kueri yang sama. Lihat:

  • Apa perbedaan antara LATERAL JOIN dan subquery di PostgreSQL?

Menghasilkan satu pencarian indeks (-saja) per pengguna.

Tidak mengembalikan baris untuk pengguna yang hilang di users meja. Biasanya, kunci asing kendala menegakkan integritas referensial akan mengesampingkan hal itu.

Juga, tidak ada baris untuk pengguna tanpa entri yang cocok di log - sesuai dengan pertanyaan awal. Untuk mempertahankan pengguna tersebut dalam hasil, gunakan LEFT JOIN LATERAL ... ON true bukannya CROSS JOIN LATERAL :

  • Memanggil fungsi pengembalian-set dengan argumen array beberapa kali

Gunakan LIMIT n bukannya LIMIT 1 untuk mengambil lebih dari satu baris (tetapi tidak semua) per pengguna.

Secara efektif, semua ini melakukan hal yang sama:

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

Yang terakhir memiliki prioritas yang lebih rendah. JOIN eksplisit mengikat sebelum koma. Perbedaan halus itu bisa menjadi masalah dengan lebih banyak tabel gabungan. Lihat:

  • "referensi tidak valid untuk entri klausa FROM untuk tabel" dalam kueri Postgres

2b. Subkueri terkait

Pilihan bagus untuk mengambil satu kolom dari satu baris . Contoh kode:

  • Optimalkan kueri maksimum berdasarkan grup

Hal yang sama dimungkinkan untuk beberapa kolom , tetapi Anda membutuhkan lebih banyak kecerdasan:

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;

Sukai LEFT JOIN LATERAL di atas, varian ini mencakup semua pengguna, bahkan tanpa entri di log . Anda mendapatkan NULL untuk combo1 , yang dapat Anda filter dengan mudah dengan WHERE klausa dalam kueri luar jika perlu.
Nitpick:dalam kueri luar Anda tidak dapat membedakan apakah subkueri tidak menemukan baris atau semua nilai kolom kebetulan NULL - hasil yang sama. Anda memerlukan NOT NULL kolom di subquery untuk menghindari ambiguitas ini.

Subkueri yang berkorelasi hanya dapat mengembalikan nilai tunggal . Anda dapat membungkus beberapa kolom menjadi tipe komposit. Tetapi untuk menguraikannya nanti, Postgres menuntut tipe komposit yang terkenal. Catatan anonim hanya dapat diuraikan dengan menyediakan daftar definisi kolom.
Gunakan tipe terdaftar seperti tipe baris dari tabel yang ada. Atau daftarkan tipe komposit secara eksplisit (dan permanen) dengan CREATE TYPE . Atau buat tabel sementara (dijatuhkan secara otomatis di akhir sesi) untuk mendaftarkan jenis barisnya sementara. Sintaks cast:(log_date, payload)::combo

Akhirnya, kami tidak ingin menguraikan combo1 pada tingkat kueri yang sama. Karena kelemahan dalam perencana kueri, ini akan mengevaluasi subkueri satu kali untuk setiap kolom (masih berlaku di Postgres 12). Sebagai gantinya, buat subquery dan dekomposisi di outer query.

Terkait:

  • Dapatkan nilai dari baris pertama dan terakhir per grup

Mendemonstrasikan semua 4 kueri dengan 100 ribu entri log dan 1.000 pengguna:
db<>biola di sini - hal 11
sqlfiddle lama



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. PSQLException:transaksi saat ini dibatalkan, perintah diabaikan hingga akhir blok transaksi

  2. Petunjuk di PostgreSQL

  3. Tentukan nama tabel dan kolom sebagai argumen dalam fungsi plpgsql?

  4. Dapatkan Nama Bulan Pendek di PostgreSQL

  5. PostgreSQL mengembalikan fungsi dengan Tipe Data Kustom