PostgreSQL - ambil baris yang memiliki nilai Max untuk kolom

96

Saya berurusan dengan tabel Postgres (disebut "kehidupan") yang berisi catatan dengan kolom untuk time_stamp, usr_id, transaction_id, dan life_remaining. Saya butuh kueri yang akan memberi saya total sisa_hidup terbaru untuk setiap usr_id

  1. Ada beberapa pengguna (usr_id berbeda)
  2. time_stamp bukanlah pengenal unik: terkadang peristiwa pengguna (satu per baris dalam tabel) akan terjadi dengan time_stamp yang sama.
  3. trans_id unik hanya untuk rentang waktu yang sangat kecil: berulang kali
  4. sisa_hidup (untuk pengguna tertentu) dapat meningkat dan menurun seiring waktu

contoh:

time_stamp | Lifes_remaining | usr_id | trans_id
-----------------------------------------
  07:00 | 1 | 1 | 1    
  09:00 | 4 | 2 | 2    
  10:00 | 2 | 3 | 3    
  10:00 | 1 | 2 | 4    
  11:00 | 4 | 1 | 5    
  11:00 | 3 | 1 | 6    
  13:00 | 3 | 3 | 1    

Karena saya perlu mengakses kolom lain dari baris dengan data terbaru untuk setiap usr_id yang diberikan, saya memerlukan kueri yang memberikan hasil seperti ini:

time_stamp | Lifes_remaining | usr_id | trans_id
-----------------------------------------
  11:00 | 3 | 1 | 6    
  10:00 | 1 | 2 | 4    
  13:00 | 3 | 3 | 1    

Seperti yang disebutkan, setiap usr_id bisa mendapatkan atau kehilangan nyawa, dan terkadang peristiwa dengan stempel waktu ini terjadi sangat berdekatan sehingga memiliki stempel waktu yang sama! Oleh karena itu, kueri ini tidak akan berfungsi:

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp) AS max_timestamp 
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp = b.time_stamp

Sebagai gantinya, saya perlu menggunakan time_stamp (first) dan trans_id (second) untuk mengidentifikasi baris yang benar. Saya juga perlu meneruskan informasi itu dari subkueri ke kueri utama yang akan menyediakan data untuk kolom lain dari baris yang sesuai. Ini adalah kueri yang diretas sehingga saya berhasil:

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp || '*' || trans_id) 
       AS max_timestamp_transid
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp_transid = b.time_stamp || '*' || b.trans_id 
ORDER BY b.usr_id

Oke, jadi ini berhasil, tapi saya tidak menyukainya. Ini membutuhkan kueri dalam kueri, bergabung sendiri, dan menurut saya itu bisa jauh lebih sederhana dengan mengambil baris yang menurut MAX memiliki cap waktu dan trans_id terbesar. Tabel "hidup" memiliki puluhan juta baris untuk diurai, jadi saya ingin kueri ini secepat dan seefisien mungkin. Saya baru mengenal RDBM dan Postgres pada khususnya, jadi saya tahu bahwa saya perlu menggunakan indeks yang tepat secara efektif. Saya agak bingung tentang cara mengoptimalkan.

Saya menemukan diskusi serupa di sini . Dapatkah saya melakukan beberapa jenis Postgres yang setara dengan fungsi analitik Oracle?

Saran apa pun tentang mengakses informasi kolom terkait yang digunakan oleh fungsi agregat (seperti MAX), membuat indeks, dan membuat kueri yang lebih baik akan sangat dihargai!

PS Anda dapat menggunakan berikut ini untuk membuat kasus contoh saya:

create TABLE lives (time_stamp timestamp, lives_remaining integer, 
                    usr_id integer, trans_id integer);
insert into lives values ('2000-01-01 07:00', 1, 1, 1);
insert into lives values ('2000-01-01 09:00', 4, 2, 2);
insert into lives values ('2000-01-01 10:00', 2, 3, 3);
insert into lives values ('2000-01-01 10:00', 1, 2, 4);
insert into lives values ('2000-01-01 11:00', 4, 1, 5);
insert into lives values ('2000-01-01 11:00', 3, 1, 6);
insert into lives values ('2000-01-01 13:00', 3, 3, 1);
Joshua Berry
sumber
Josh, Anda mungkin tidak menyukai fakta bahwa kueri bergabung sendiri, dll., Tetapi tidak masalah sejauh menyangkut RDBMS.
vladr
1
Apa self-join yang akhirnya akan diterjemahkan adalah pemetaan indeks sederhana, di mana SELECT bagian dalam (yang dengan MAX) memindai indeks membuang entri yang tidak relevan, dan di mana SELECT luar hanya mengambil sisa kolom dari tabel sesuai dengan indeks yang dipersempit.
vladr
Vlad, terima kasih atas tip dan penjelasannya. Ini membuka mata saya untuk mulai memahami cara kerja bagian dalam database dan cara mengoptimalkan kueri. Quassnoi, terima kasih atas permintaan dan tip yang bagus tentang kunci utama; Bill juga. Sangat membantu.
Joshua Berry
terima kasih telah menunjukkan kepada saya bagaimana mendapatkan MAX BY2 kolom!

Jawaban:

90

Di atas meja dengan 158k baris pseudo-random (usr_id didistribusikan secara seragam antara 0 dan 10k, trans_iddidistribusikan secara seragam antara 0 dan 30),

Berdasarkan biaya kueri, di bawah ini, saya mengacu pada perkiraan biaya pengoptimal berbasis biaya Postgres (dengan xxx_costnilai default Postgres ), yang merupakan perkiraan fungsi yang ditimbang dari sumber daya I / O dan CPU yang diperlukan; Anda bisa mendapatkannya dengan mengaktifkan PgAdminIII dan menjalankan "Query / Explain (F7)" pada kueri dengan "Query / Explain options" yang disetel ke "Analyze"

  • Permintaan Quassnoy memiliki perkiraan biaya 745k (!), Dan selesai dalam 1,3 detik (diberi indeks senyawa pada ( usr_id, trans_id, time_stamp))
  • Kueri Bill memiliki perkiraan biaya 93k, dan selesai dalam 2,9 detik (diberi indeks gabungan pada ( usr_id, trans_id))
  • Query # 1 di bawah ini memiliki perkiraan biaya 16K, dan menyelesaikan di 800ms (diberi indeks senyawa pada ( usr_id, trans_id, time_stamp))
  • Query # 2 di bawah ini memiliki perkiraan biaya 14K, dan menyelesaikan di 800ms (diberi indeks fungsi senyawa pada ( usr_id, EXTRACT(EPOCH FROM time_stamp), trans_id))
    • ini khusus untuk Postgres
  • Query # 3 di bawah (Postgres 8.4+) memiliki perkiraan biaya dan penyelesaian waktu sebanding dengan (atau lebih baik dari) permintaan # 2 (diberi indeks senyawa pada ( usr_id, time_stamp, trans_id)); ini memiliki keuntungan dari memindai livestabel hanya sekali dan, jika Anda meningkatkan sementara (jika perlu) work_mem untuk mengakomodasi jenis dalam memori, ini akan menjadi yang tercepat dari semua kueri.

Semua waktu di atas termasuk pengambilan 10k baris penuh hasil-set.

Sasaran Anda adalah perkiraan biaya minimal dan waktu eksekusi kueri minimal, dengan penekanan pada perkiraan biaya. Eksekusi kueri dapat sangat bergantung pada kondisi runtime (misalnya apakah baris yang relevan sudah sepenuhnya di-cache dalam memori atau belum), sedangkan perkiraan biayanya belum. Di sisi lain, perlu diingat bahwa perkiraan biaya persis seperti itu, perkiraan.

Waktu eksekusi kueri terbaik diperoleh saat menjalankan pada database khusus tanpa beban (misalnya bermain dengan pgAdminIII pada PC pengembangan.) Waktu kueri akan bervariasi dalam produksi berdasarkan pada beban mesin aktual / penyebaran akses data. Ketika satu kueri muncul sedikit lebih cepat (<20%) daripada yang lain tetapi memiliki biaya yang jauh lebih tinggi, umumnya akan lebih bijaksana untuk memilih satu dengan waktu eksekusi lebih tinggi tetapi biaya lebih rendah.

Jika Anda mengharapkan tidak ada persaingan untuk memori di mesin produksi Anda pada saat kueri dijalankan (misalnya, cache RDBMS dan cache sistem file tidak akan dihancurkan oleh kueri bersamaan dan / atau aktivitas sistem file) maka waktu kueri yang Anda peroleh dalam mode mandiri (mis. pgAdminIII pada PC pengembangan) akan mewakili. Jika ada perselisihan pada sistem produksi, waktu kueri akan menurun secara proporsional dengan perkiraan rasio biaya, karena kueri dengan biaya lebih rendah tidak terlalu bergantung pada cache sedangkan kueri dengan biaya lebih tinggi akan mengunjungi kembali data yang sama berulang kali (memicu I / O tambahan jika tidak ada cache yang stabil), misalnya:

              cost | time (dedicated machine) |     time (under load) |
-------------------+--------------------------+-----------------------+
some query A:   5k | (all data cached)  900ms | (less i/o)     1000ms |
some query B:  50k | (all data cached)  900ms | (lots of i/o) 10000ms |

Jangan lupa untuk menjalankan ANALYZE livessekali setelah membuat indeks yang diperlukan.


Pertanyaan # 1

-- incrementally narrow down the result set via inner joins
--  the CBO may elect to perform one full index scan combined
--  with cascading index lookups, or as hash aggregates terminated
--  by one nested index lookup into lives - on my machine
--  the latter query plan was selected given my memory settings and
--  histogram
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
    SELECT
      usr_id,
      MAX(time_stamp) AS time_stamp_max
     FROM
      lives
     GROUP BY
      usr_id
  ) AS l2
 ON
  l1.usr_id     = l2.usr_id AND
  l1.time_stamp = l2.time_stamp_max
 INNER JOIN (
    SELECT
      usr_id,
      time_stamp,
      MAX(trans_id) AS trans_max
     FROM
      lives
     GROUP BY
      usr_id, time_stamp
  ) AS l3
 ON
  l1.usr_id     = l3.usr_id AND
  l1.time_stamp = l3.time_stamp AND
  l1.trans_id   = l3.trans_max

Pertanyaan # 2

-- cheat to obtain a max of the (time_stamp, trans_id) tuple in one pass
-- this results in a single table scan and one nested index lookup into lives,
--  by far the least I/O intensive operation even in case of great scarcity
--  of memory (least reliant on cache for the best performance)
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
   SELECT
     usr_id,
     MAX(ARRAY[EXTRACT(EPOCH FROM time_stamp),trans_id])
       AS compound_time_stamp
    FROM
     lives
    GROUP BY
     usr_id
  ) AS l2
ON
  l1.usr_id = l2.usr_id AND
  EXTRACT(EPOCH FROM l1.time_stamp) = l2.compound_time_stamp[1] AND
  l1.trans_id = l2.compound_time_stamp[2]

Pembaruan 2013/01/29

Akhirnya, pada versi 8.4, Postgres mendukung Fungsi Jendela yang berarti Anda dapat menulis sesuatu yang sederhana dan efisien seperti:

Pertanyaan # 3

-- use Window Functions
-- performs a SINGLE scan of the table
SELECT DISTINCT ON (usr_id)
  last_value(time_stamp) OVER wnd,
  last_value(lives_remaining) OVER wnd,
  usr_id,
  last_value(trans_id) OVER wnd
 FROM lives
 WINDOW wnd AS (
   PARTITION BY usr_id ORDER BY time_stamp, trans_id
   ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
 );
vladr
sumber
Dengan indeks gabungan pada (usr_id, trans_id, times_tamp), apakah yang Anda maksud adalah "BUAT INDEKS LIVE_blah_idx ON kehidupan (usr_id, trans_id, time_stamp)"? Atau haruskah saya membuat tiga indeks terpisah untuk setiap kolom? Saya harus tetap menggunakan default "MENGGUNAKAN btree", bukan?
Joshua Berry
1
Ya untuk pilihan pertama: Maksud saya BUAT INDEKS Live_blah_idx ON nyawa (usr_id, trans_id, time_stamp). :) Bersulang.
vladr
Terima kasih telah melakukan vladr perbandingan biaya! Jawaban yang sangat lengkap!
Adam
@vladr Saya baru saja menemukan jawaban Anda. Saya agak bingung, karena Anda mengatakan kueri 1 memiliki biaya 16k dan kueri 2 biaya 14k. Tetapi lebih jauh di bawah tabel Anda mengatakan kueri 1 memiliki biaya 5k dan kueri 2 memiliki biaya 50k. Jadi kueri mana yang lebih disukai untuk digunakan? :) terima kasih
Houman
1
@Kave, tabel ini untuk sepasang kueri hipotetis untuk mengilustrasikan sebuah contoh, bukan dua kueri OP. Mengganti nama untuk mengurangi kebingungan.
vladr
78

Saya akan mengusulkan versi bersih berdasarkan DISTINCT ON(lihat dokumen ):

SELECT DISTINCT ON (usr_id)
    time_stamp,
    lives_remaining,
    usr_id,
    trans_id
FROM lives
ORDER BY usr_id, time_stamp DESC, trans_id DESC;
Marco
sumber
6
Ini adalah jawaban yang sangat singkat dan masuk akal. Juga memiliki referensi yang bagus! Ini harus menjadi jawaban yang diterima.
Prakhar Agrawal
Ini tampaknya berhasil untuk saya pada aplikasi saya yang sedikit berbeda di mana tidak ada yang lain. Pasti harus dinaikkan agar lebih terlihat.
Jim Factor
8

Berikut metode lain, yang kebetulan tidak menggunakan subkueri terkait atau GROUP BY. Saya tidak ahli dalam penyetelan kinerja PostgreSQL, jadi saya sarankan Anda mencoba ini dan solusi yang diberikan oleh orang lain untuk melihat mana yang bekerja lebih baik untuk Anda.

SELECT l1.*
FROM lives l1 LEFT OUTER JOIN lives l2
  ON (l1.usr_id = l2.usr_id AND (l1.time_stamp < l2.time_stamp 
   OR (l1.time_stamp = l2.time_stamp AND l1.trans_id < l2.trans_id)))
WHERE l2.usr_id IS NULL
ORDER BY l1.usr_id;

Saya berasumsi bahwa trans_iditu unik setidaknya di atas nilai tertentu time_stamp.

Bill Karwin
sumber
4

Saya suka gaya jawaban Mike Woodhouse di halaman lain yang Anda sebutkan. Ini sangat ringkas ketika hal yang dimaksimalkan hanyalah satu kolom, dalam hal ini subkueri hanya dapat menggunakan MAX(some_col)dan GROUP BYkolom lainnya, tetapi dalam kasus Anda, Anda memiliki kuantitas 2 bagian untuk dimaksimalkan, Anda masih dapat melakukannya dengan menggunakan ORDER BYplus LIMIT 1sebagai gantinya (seperti yang dilakukan oleh Quassnoi):

SELECT * 
FROM lives outer
WHERE (usr_id, time_stamp, trans_id) IN (
    SELECT usr_id, time_stamp, trans_id
    FROM lives sq
    WHERE sq.usr_id = outer.usr_id
    ORDER BY trans_id, time_stamp
    LIMIT 1
)

Saya merasa menggunakan sintaks baris-konstruktor WHERE (a, b, c) IN (subquery)bagus karena mengurangi jumlah verbiage yang dibutuhkan.

j_random_hacker
sumber
3

Sebenarnya ada solusi hacky untuk masalah ini. Katakanlah Anda ingin memilih pohon terbesar dari setiap hutan di suatu wilayah.

SELECT (array_agg(tree.id ORDER BY tree_size.size)))[1]
FROM tree JOIN forest ON (tree.forest = forest.id)
GROUP BY forest.id

Ketika Anda mengelompokkan pohon berdasarkan hutan, akan ada daftar pohon yang tidak disortir dan Anda perlu menemukan yang terbesar. Hal pertama yang harus Anda lakukan adalah mengurutkan baris berdasarkan ukurannya dan memilih yang pertama dari daftar Anda. Ini mungkin tampak tidak efisien tetapi jika Anda memiliki jutaan baris, ini akan lebih cepat daripada solusi yang menyertakan JOINs dan WHEREkondisi.

BTW, perhatikan bahwa ORDER_BYuntuk array_aggdiperkenalkan di Postgresql 9.0

burak emre
sumber
Anda mengalami kesalahan. Anda perlu menulis ORDER BY tree_size.size DESC. Selain itu, untuk tugas penulis kodenya akan terlihat seperti ini: SELECT usr_id, (array_agg(time_stamp ORDER BY time_stamp DESC))[1] AS timestamp, (array_agg(lives_remaining ORDER BY time_stamp DESC))[1] AS lives_remaining, (array_agg(trans_id ORDER BY time_stamp DESC))[1] AS trans_id FROM lives GROUP BY usr_id
alexkovelsky
2

Ada opsi baru di Postgressql 9.5 yang disebut DISTINCT ON

SELECT DISTINCT ON (location) location, time, report
    FROM weather_reports
    ORDER BY location, time DESC;

Ini menghilangkan baris duplikat dan hanya menyisakan baris pertama seperti yang didefinisikan klausa ORDER BY saya.

lihat dokumentasi resmi

Eden
sumber
1
SELECT  l.*
FROM    (
        SELECT DISTINCT usr_id
        FROM   lives
        ) lo, lives l
WHERE   l.ctid = (
        SELECT ctid
        FROM   lives li
        WHERE  li.usr_id = lo.usr_id
        ORDER BY
          time_stamp DESC, trans_id DESC
        LIMIT 1
        )

Membuat indeks di (usr_id, time_stamp, trans_id)akan sangat meningkatkan kueri ini.

Anda harus selalu, selalu memiliki beberapa jenis PRIMARY KEYdi tabel Anda.

Quassnoi
sumber
0

Saya pikir Anda punya satu masalah besar di sini: tidak ada "penghitung" yang meningkat secara monoton untuk menjamin bahwa pertikaian tertentu terjadi lebih lambat daripada yang lain. Ambil contoh ini:

timestamp   lives_remaining   user_id   trans_id
10:00       4                 3         5
10:00       5                 3         6
10:00       3                 3         1
10:00       2                 3         2

Anda tidak dapat menentukan dari data ini yang merupakan entri terbaru. Apakah yang kedua atau yang terakhir? Tidak ada fungsi sortir atau max () yang dapat Anda terapkan pada data ini untuk memberikan jawaban yang benar.

Meningkatkan resolusi stempel waktu akan sangat membantu. Karena mesin database membuat permintaan secara serial, dengan resolusi yang memadai Anda dapat menjamin bahwa tidak ada dua stempel waktu yang sama.

Atau, gunakan trans_id yang tidak akan bergulir untuk waktu yang sangat, sangat lama. Memiliki trans_id yang bergulir berarti Anda tidak dapat mengetahui (untuk stempel waktu yang sama) apakah trans_id 6 lebih baru daripada trans_id 1 kecuali Anda melakukan perhitungan yang rumit.

Barry Brown
sumber
Ya, idealnya kolom urutan (peningkatan otomatis) akan berurutan.
vladr
Asumsi di atas adalah bahwa untuk peningkatan waktu yang kecil, trans_id tidak akan bergulir. Saya setuju bahwa tabel memerlukan indeks utama yang unik - seperti trans_id yang tidak berulang. (PS Saya senang bahwa saya sekarang memiliki cukup poin karma / reputasi untuk dikomentari!)
Joshua Berry
Vlad menyatakan bahwa trans_id memiliki siklus yang agak pendek yang sering berubah. Meskipun Anda hanya mempertimbangkan dua baris tengah dari tabel saya (trans_id = 6 dan 1), Anda tetap tidak dapat membedakan mana yang terbaru. Oleh karena itu, menggunakan max (trans_id) untuk stempel waktu tertentu tidak akan berfungsi.
Barry Brown
Ya, saya mengandalkan jaminan dari pembuat aplikasi bahwa tuple (time_stamp, trans_id) unik untuk pengguna tertentu. Jika tidak demikian maka "SELECT l1.usr_id, l1.lives_left, ... FROM ... WHERE ..." harus menjadi "SELECT l1.usr_id, MAX / MIN (l1.lives_left), ... FROM. .. MANA ... GROUP BY l1.usr_id, ...
vladr
0

Solusi lain yang mungkin berguna bagi Anda.

SELECT t.*
FROM
    (SELECT
        *,
        ROW_NUMBER() OVER(PARTITION BY usr_id ORDER BY time_stamp DESC) as r
    FROM lives) as t
WHERE t.r = 1
Turbcool
sumber