Kueri yang efisien untuk mendapatkan nilai terbaik per grup dari tabel besar

13

Diberikan tabel:

    Column    |            Type             
 id           | integer                     
 latitude     | numeric(9,6)                
 longitude    | numeric(9,6)                
 speed        | integer                     
 equipment_id | integer                     
 created_at   | timestamp without time zone
Indexes:
    "geoposition_records_pkey" PRIMARY KEY, btree (id)

Tabel ini memiliki 20 juta catatan yang secara relatif tidak banyak. Tapi itu membuat pemindaian berurutan lambat.

Bagaimana saya bisa mendapatkan catatan terakhir ( max(created_at)) dari masing-masing equipment_id?

Saya sudah mencoba kedua pertanyaan berikut, dengan beberapa varian yang telah saya baca melalui banyak jawaban dari topik ini:

select max(created_at),equipment_id from geoposition_records group by equipment_id;

select distinct on (equipment_id) equipment_id,created_at 
  from geoposition_records order by equipment_id, created_at desc;

Saya juga telah mencoba membuat indeks btree untuk equipment_id,created_attetapi Postgres menemukan bahwa menggunakan seqscan lebih cepat. Pemaksaan enable_seqscan = offtidak ada gunanya karena membaca indeks selambat pemindaian seq, mungkin lebih buruk.

Permintaan harus berjalan secara berkala, selalu kembali yang terakhir.

Menggunakan Postgres 9.3.

Jelaskan / analisis (dengan 1,7 juta catatan):

set enable_seqscan=true;
explain analyze select max(created_at),equipment_id from geoposition_records group by equipment_id;
"HashAggregate  (cost=47803.77..47804.34 rows=57 width=12) (actual time=1935.536..1935.556 rows=58 loops=1)"
"  ->  Seq Scan on geoposition_records  (cost=0.00..39544.51 rows=1651851 width=12) (actual time=0.029..494.296 rows=1651851 loops=1)"
"Total runtime: 1935.632 ms"

set enable_seqscan=false;
explain analyze select max(created_at),equipment_id from geoposition_records group by equipment_id;
"GroupAggregate  (cost=0.00..2995933.57 rows=57 width=12) (actual time=222.034..11305.073 rows=58 loops=1)"
"  ->  Index Scan using geoposition_records_equipment_id_created_at_idx on geoposition_records  (cost=0.00..2987673.75 rows=1651851 width=12) (actual time=0.062..10248.703 rows=1651851 loops=1)"
"Total runtime: 11305.161 ms"
Feyd
sumber
nah terakhir kali saya mengecek tidak ada NULLnilai dalam equipment_idpersentase yang diharapkan di bawah 0,1%
Feyd

Jawaban:

10

Indeks b-tree multicolumn polos seharusnya bisa digunakan:

CREATE INDEX foo_idx
ON geoposition_records (equipment_id, created_at DESC NULLS LAST);

Mengapa DESC NULLS LAST?

Fungsi

Jika Anda tidak dapat berbicara masuk akal ke perencana kueri, perulangan fungsi melalui tabel peralatan harus melakukan trik. Mencari satu equipment_id sekaligus menggunakan indeks. Untuk sejumlah kecil (57 menilai dari EXPLAIN ANALYZEoutput Anda ), itu cepat.
Aman untuk menganggap Anda memiliki equipmentmeja?

CREATE OR REPLACE FUNCTION f_latest_equip()
  RETURNS TABLE (equipment_id int, latest timestamp) AS
$func$
BEGIN
FOR equipment_id IN
   SELECT e.equipment_id FROM equipment e ORDER BY 1
LOOP
   SELECT g.created_at
   FROM   geoposition_records g
   WHERE  g.equipment_id = f_latest_equip.equipment_id
                           -- prepend function name to disambiguate
   ORDER  BY g.created_at DESC NULLS LAST
   LIMIT  1
   INTO   latest;

   RETURN NEXT;
END LOOP;
END  
$func$  LANGUAGE plpgsql STABLE;

Membuat panggilan yang bagus juga:

SELECT * FROM f_latest_equip();

Subquery terkait

Kalau dipikir-pikir itu, menggunakan equipmenttabel ini , Anda bisa ke pekerjaan kotor dengan subqueries berkorelasi rendah untuk efek besar:

SELECT equipment_id
     ,(SELECT created_at
       FROM   geoposition_records
       WHERE  equipment_id = eq.equipment_id
       ORDER  BY created_at DESC NULLS LAST
       LIMIT  1) AS latest
FROM   equipment eq;

Performanya sangat bagus.

LATERAL bergabung dengan Postgres 9.3+

SELECT eq.equipment_id, r.latest
FROM   equipment eq
LEFT   JOIN LATERAL (
   SELECT created_at
   FROM   geoposition_records
   WHERE  equipment_id = eq.equipment_id
   ORDER  BY created_at DESC NULLS LAST
   LIMIT  1
   ) r(latest) ON true;

Penjelasan detail:

Kinerja serupa dengan subquery yang berkorelasi. Membandingkan kinerja max(), DISTINCT ON, fungsi, berkorelasi subquery dan LATERALdalam hal ini:

SQL Fiddle .

Erwin Brandstetter
sumber
1
@ErwinBrandstetter ini adalah sesuatu yang saya coba setelah jawaban dari Colin, tapi saya tidak bisa berhenti berpikir bahwa ini adalah solusi yang menggunakan jenis sisi database n + 1 query (tidak yakin apakah itu termasuk dalam antipattern karena ada tidak ada koneksi overhead) ... Saya bertanya-tanya sekarang mengapa kelompok ada, jika tidak dapat menangani beberapa juta rekaman dengan benar ... Itu tidak masuk akal, kan? menjadi sesuatu yang kita lewatkan. Akhirnya, pertanyaannya telah sedikit berubah dan kami mengasumsikan kehadiran meja peralatan ... Saya ingin tahu apakah sebenarnya ada cara lain
Feyd
3

Percobaan 1

Jika

  1. Saya punya equipmentmeja terpisah , dan
  2. Saya memiliki indeks geoposition_records(equipment_id, created_at desc)

maka berikut ini berfungsi untuk saya:

select id as equipment_id, (select max(created_at)
                            from geoposition_records
                            where equipment_id = equipment.id
                           ) as max_created_at
from equipment;

Saya tidak bisa memaksa PG untuk melakukan query cepat untuk menentukan kedua daftar equipment_iddan terkait max(created_at). Tapi saya akan coba lagi besok!

Percobaan 2

Saya menemukan tautan ini: http://zogovic.com/post/44856908222/optimizing-postgresql-query-for-distinct-values Menggabungkan teknik ini dengan permintaan saya dari upaya 1, saya dapatkan:

WITH RECURSIVE equipment(id) AS (
    SELECT MIN(equipment_id) FROM geoposition_records
  UNION
    SELECT (
      SELECT equipment_id
      FROM geoposition_records
      WHERE equipment_id > equipment.id
      ORDER BY equipment_id
      LIMIT 1
    )
    FROM equipment WHERE id IS NOT NULL
)
SELECT id AS equipment_id, (SELECT MAX(created_at)
                            FROM geoposition_records
                            WHERE equipment_id = equipment.id
                           ) AS max_created_at
FROM equipment;

dan ini bekerja CEPAT! Tapi kamu butuh

  1. formulir permintaan ultra-contorted ini, dan
  2. indeks aktif geoposition_records(equipment_id, created_at desc).
Colin 't Hart
sumber