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