Mengoptimalkan kueri pada rentang cap waktu (dua kolom)

96

Saya menggunakan PostgreSQL 9.1 di Ubuntu 12.04.

Saya perlu memilih catatan dalam rentang waktu: meja saya time_limitsmemiliki dua timestampbidang dan satu integerproperti. Ada kolom tambahan di tabel aktual saya yang tidak terlibat dengan kueri ini.

create table (
   start_date_time timestamp,
   end_date_time timestamp, 
   id_phi integer, 
   primary key(start_date_time, end_date_time,id_phi);

Tabel ini berisi sekitar 2 juta catatan.

Pertanyaan seperti berikut ini membutuhkan banyak waktu:

select * from time_limits as t 
where t.id_phi=0 
and t.start_date_time <= timestamp'2010-08-08 00:00:00'
and t.end_date_time   >= timestamp'2010-08-08 00:05:00';

Jadi saya mencoba menambahkan indeks lain - kebalikan dari PK:

create index idx_inversed on time_limits(id_phi, start_date_time, end_date_time);

Saya mendapat kesan bahwa kinerja meningkat: Waktu untuk mengakses catatan di tengah meja tampaknya lebih masuk akal: di suatu tempat antara 40 dan 90 detik.

Tetapi masih beberapa puluh detik untuk nilai di tengah rentang waktu. Dan dua kali lagi ketika menargetkan ujung meja (berbicara secara kronologis).

Saya mencoba explain analyzeuntuk pertama kalinya untuk mendapatkan paket permintaan ini:

 Bitmap Heap Scan on time_limits  (cost=4730.38..22465.32 rows=62682 width=36) (actual time=44.446..44.446 rows=0 loops=1)
   Recheck Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
   ->  Bitmap Index Scan on idx_time_limits_phi_start_end  (cost=0.00..4714.71 rows=62682 width=0) (actual time=44.437..44.437 rows=0 loops=1)
         Index Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
 Total runtime: 44.507 ms

Lihat hasilnya di depesz.com.

Apa yang bisa saya lakukan untuk mengoptimalkan pencarian? Anda dapat melihat semua waktu yang dihabiskan memindai dua kolom cap waktu setelah id_phidiatur ke 0. Dan saya tidak mengerti pemindaian besar (baris 60K!) Pada cap waktu. Bukankah mereka diindeks oleh kunci primer dan idx_inversedsaya menambahkan?

Haruskah saya mengubah dari jenis stempel waktu ke yang lain?

Saya telah membaca sedikit tentang indeks GIST dan GIN. Saya rasa mereka bisa lebih efisien pada kondisi tertentu untuk jenis kustom. Apakah ini opsi yang layak untuk kasus penggunaan saya?

Stephane Rolland
sumber
1
baik itu 45-an. Saya tidak tahu mengapa dikatakan 45 ms. Saya bahkan tidak akan mulai mengeluh jika itu secepat 45 ms ... :-) Mungkin bug dalam hasil menjelaskan analisis. Atau mungkin sudah saatnya analisis dilakukan. Tidak tahu Tapi 40/50 detik adalah apa yang saya ukur.
Stephane Rolland
2
Waktu yang dilaporkan dalam explain analyzeoutput adalah waktu permintaan yang diperlukan di server . Jika kueri Anda membutuhkan waktu 45 detik, maka waktu tambahan dihabiskan untuk mentransfer data dari database ke program yang menjalankan kueri. Lagipula 62682 baris dan jika setiap baris besar (misalnya panjang varcharatau textkolom), ini dapat memengaruhi waktu transfer secara drastis.
a_horse_with_no_name
@a_horse_with_no_name: rows=62682 rowsadalah perkiraan perencana . Kueri mengembalikan 0 baris. (actual time=44.446..44.446 rows=0 loops=1)
Erwin Brandstetter
@ ErwinBrandstetter: ah, benar. Saya mengabaikan hal itu. Tapi saya masih belum pernah melihat hasil dari menjelaskan analisis kebohongan tentang waktu eksekusi.
a_horse_with_no_name

Jawaban:

162

Untuk Postgres 9.1 atau lebih baru:

CREATE INDEX idx_time_limits_ts_inverse
ON time_limits (id_phi, start_date_time, end_date_time DESC);

Dalam kebanyakan kasus, urutan semacam indeks hampir tidak relevan. Postgres dapat memindai mundur secara praktis secepat. Tetapi untuk kueri rentang pada beberapa kolom, ini bisa membuat perbedaan besar . Erat terkait:

Pertimbangkan permintaan Anda:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    start_date_time <= '2010-08-08 00:00'
AND    end_date_time   >= '2010-08-08 00:05';

Urutan urutan kolom pertama id_phidalam indeks tidak relevan. Karena ini diperiksa untuk persamaan ( =), itu harus didahulukan. Anda punya hak itu. Lebih banyak dalam jawaban terkait ini:

Postgres dapat melompat ke id_phi = 0dalam waktu dekat dan mempertimbangkan dua kolom indeks pencocokan berikut. Ini dipertanyakan dengan berbagai kondisi urutan urutan terbalik ( <=, >=). Dalam indeks saya, baris yang memenuhi kualifikasi didahulukan. Harus menjadi cara tercepat yang mungkin dengan indeks B-Tree 1 :

  • Anda ingin start_date_time <= something: indeks memiliki stempel waktu paling awal terlebih dahulu.
    • Jika memenuhi syarat, periksa juga kolom 3.
      Perulangan hingga baris pertama gagal memenuhi syarat (super cepat).
  • Anda ingin end_date_time >= something: indeks memiliki stempel waktu terakhir lebih dulu.
    • Jika memenuhi syarat, teruskan mengambil baris sampai yang pertama tidak (super cepat).
      Lanjutkan dengan nilai berikutnya untuk kolom 2 ..

Postgres dapat memindai maju atau mundur. Cara Anda memiliki indeks, harus membaca semua baris yang cocok pada dua kolom pertama dan kemudian filter ketiga. Pastikan untuk membaca IndeksORDER BY bab dan dalam manual. Ini cocok dengan pertanyaan Anda dengan cukup baik.

Berapa banyak baris yang cocok pada dua kolom pertama?
Hanya sedikit yang start_date_timemendekati awal rentang waktu tabel. Tapi hampir semua baris dengan id_phi = 0ujung kronologis dari tabel! Jadi kinerja memburuk dengan waktu mulai nanti.

Perkiraan perencana

Perencana memperkirakan rows=62682untuk contoh kueri Anda. Dari mereka, tidak ada yang memenuhi syarat ( rows=0). Anda mungkin mendapatkan perkiraan yang lebih baik jika Anda meningkatkan target statistik untuk tabel. Untuk 2.000.000 baris ...

ALTER TABLE time_limits ALTER start_date_time SET STATISTICS 1000;
ALTER TABLE time_limits ALTER end_date_time   SET STATISTICS 1000;

... mungkin membayar. Atau bahkan lebih tinggi. Lebih banyak dalam jawaban terkait ini:

Saya kira Anda tidak memerlukan itu untuk id_phi(hanya beberapa nilai berbeda, didistribusikan secara merata), tetapi untuk cap waktu (banyak nilai berbeda, didistribusikan tidak merata).
Saya juga tidak menganggapnya penting dengan peningkatan indeks.

CLUSTER / pg_repack

Jika Anda menginginkannya lebih cepat, Anda dapat merampingkan urutan fisik baris di tabel Anda. Jika Anda mampu mengunci meja Anda secara eksklusif untuk jangka waktu pendek (misalnya, di luar jam) untuk menulis ulang tabel Anda dan memesan baris sesuai dengan indeks:

ALTER TABLE time_limits CLUSTER ON idx_time_limits_inversed;

Dengan akses bersamaan, pertimbangkan pg_repack , yang dapat melakukan hal yang sama tanpa kunci eksklusif.

Either way, efeknya adalah bahwa blok lebih sedikit perlu dibaca dari tabel dan semuanya sudah diurutkan. Ini efek satu kali memburuk dari waktu ke waktu dengan menulis di atas meja yang memecah urutan fisik.

Indeks GiST di Postgres 9.2+

1 Dengan hal 9.2+ ada opsi lain yang mungkin lebih cepat: indeks GiST untuk kolom rentang.

  • Ada tipe rentang bawaan untuk timestampdan timestamp with time zone: tsrange,tstzrange . Indeks btree biasanya lebih cepat untuk integerkolom tambahan seperti id_phi. Lebih kecil dan lebih murah untuk dirawat. Tetapi secara keseluruhan kueri mungkin akan lebih cepat dengan indeks gabungan.

  • Ubah definisi tabel Anda atau gunakan indeks ekspresi .

  • Untuk indeks GiST multikolom yang tersedia, Anda juga memerlukan modul tambahan yang btree_gistterpasang (satu kali per basis data) yang menyediakan kelas operator untuk menyertakan integer.

Trifecta! Sebuah indeks GIST multicolumn fungsional :

CREATE EXTENSION IF NOT EXISTS btree_gist;  -- if not installed, yet

CREATE INDEX idx_time_limits_funky ON time_limits USING gist
(id_phi, tsrange(start_date_time, end_date_time, '[]'));

Gunakan operator "berisi rentang"@> dalam kueri Anda sekarang:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    tsrange(start_date_time, end_date_time, '[]')
    @> tsrange('2010-08-08 00:00', '2010-08-08 00:05', '[]')

Indeks SP-GiST di Postgres 9.3+

Sebuah SP-GIST indeks mungkin lebih cepat untuk jenis query - kecuali bahwa, mengutip manual :

Saat ini, hanya tipe indeks B-tree, GiST, GIN, dan BRIN yang mendukung indeks multikolom.

Masih benar di Postgres 12.
Anda harus menggabungkan spgistindeks hanya (tsrange(...))dengan btreeindeks kedua aktif (id_phi). Dengan tambahan overhead, saya tidak yakin ini bisa bersaing.
Jawaban terkait dengan tolok ukur hanya untuk satu tsrangekolom:

Erwin Brandstetter
sumber
78
Saya harus mengatakan ini setidaknya sekali saja, bahwa masing-masing jawaban Anda pada SO dan DBA memiliki nilai tambah yang sangat tinggi / exprertise , dan sebagian besar waktu paling lengkap. Untuk mengatakannya sekali saja: Hormati !.
Stephane Rolland
1
Merci bien! :) Jadi, apakah Anda mendapatkan hasil yang lebih cepat?
Erwin Brandstetter
Saya harus membiarkan menyelesaikan salinan massal besar yang dihasilkan dari permintaan saya yang canggung secara intensif, jadi membuat prosesnya sangat lambat, itu berubah selama berjam-jam sebelum saya mengajukan pertanyaan. Tetapi saya telah menghitung, dan saya memutuskan untuk membiarkannya berubah sampai besok pagi, itu akan selesai, dan meja baru siap untuk diisi besok. Saya telah mencoba untuk membuat indeks Anda secara bersamaan selama pekerjaan, tetapi karena terlalu banyak akses (saya pikir), pembuatan indeks harus dikunci. Saya akan mengulangi waktu tes yang sama ini lagi besok dengan solusi Anda. Saya juga telah melihat bagaimana meningkatkan ke 9.2 ;-) untuk debian / ubuntu.
Stephane Rolland
2
@StephaneRolland: itu masih akan menarik mengapa menjelaskan analisis output menunjukkan 45 milidetik saat Anda melihat permintaan mengambil lebih dari 40 detik.
a_horse_with_no_name
1
@ John: Postgres dapat melintasi indeks maju atau mundur, tetapi tidak dapat mengubah arah dalam pemindaian yang sama. Idealnya, Anda memiliki semua baris yang memenuhi syarat per node terlebih dahulu (atau terakhir), tetapi harus sama rata (pencocokan predikat kueri) untuk semua kolom untuk mendapatkan hasil terbaik.
Erwin Brandstetter
5

Jawaban Erwin sudah komprehensif, namun:

Jenis rentang untuk cap waktu tersedia dalam PostgreSQL 9.1 dengan ekstensi temporal dari Jeff Davis: https://github.com/jeff-davis/PostgreSQL-Temporal

Catatan: memiliki fitur terbatas (menggunakan Timestamptz, dan Anda hanya dapat memiliki gaya '[)' tumpang tindih afaik). Juga, ada banyak alasan bagus lainnya untuk meningkatkan ke PostgreSQL 9.2.

nathan-m
sumber
3

Anda dapat mencoba membuat indeks multikolom dalam urutan yang berbeda:

primary key(id_phi, start_date_time,end_date_time);

Saya memposting sekali pertanyaan serupa juga terkait dengan pemesanan indeks pada indeks multicolumn. Kuncinya adalah mencoba menggunakan terlebih dahulu kondisi yang paling ketat untuk mengurangi ruang pencarian.

Sunting : Kesalahan saya. Sekarang saya melihat bahwa Anda sudah menentukan indeks ini.

jap1968
sumber
Saya sudah memiliki kedua indeks. Kecuali kunci primer adalah yang lain, tetapi indeks yang Anda usulkan sudah ada, dan merupakan salah satu yang digunakan jika Anda melihat penjelasannya:Bitmap Index Scan on idx_time_limits_phi_start_end
Stephane Rolland
1

Saya berhasil meningkat dengan cepat (dari 1 detik ke 70 ms)

Saya memiliki tabel dengan agregasi banyak pengukuran dan banyak level ( lkolom) (30s, 1m, 1h, dll) ada dua kolom rentang terikat: $suntuk awal dan $eakhir.

Saya membuat dua indeks multikolom: satu untuk memulai dan satu untuk akhir.

Saya menyesuaikan kueri pemilihan: pilih rentang di mana batas awal mereka berada dalam kisaran yang diberikan. Selain itu pilih rentang di mana ujungnya terikat dalam kisaran yang diberikan.

Jelaskan menunjukkan dua aliran baris menggunakan indeks kami secara efisien.

Indeks:

drop index if exists agg_search_a;
CREATE INDEX agg_search_a
ON agg (measurement_id, l, "$s");

drop index if exists agg_search_b;
CREATE INDEX agg_search_b
ON agg (measurement_id, l, "$e");

Pilih kueri:

select "$s", "$e", a, t, b, c from agg
where 
    measurement_id=0 
    and l =  '30s'
    and (
        (
            "$s" > '2013-05-01 02:05:05'
            and "$s" < '2013-05-01 02:18:15'
        )
        or 
        (
             "$e" > '2013-05-01 02:00:05'
            and "$e" < '2013-05-01 02:18:05'
        )
    )

;

Menjelaskan:

[
  {
    "Execution Time": 0.058,
    "Planning Time": 0.112,
    "Plan": {
      "Startup Cost": 10.18,
      "Rows Removed by Index Recheck": 0,
      "Actual Rows": 37,
      "Plans": [
    {
      "Startup Cost": 10.18,
      "Actual Rows": 0,
      "Plans": [
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 26,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone))",
          "Plan Rows": 29,
          "Parallel Aware": false,
          "Actual Total Time": 0.016,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.016,
          "Total Cost": 5,
          "Actual Loops": 1,
          "Index Name": "agg_search_a"
        },
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 36,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone))",
          "Plan Rows": 39,
          "Parallel Aware": false,
          "Actual Total Time": 0.011,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.011,
          "Total Cost": 5.15,
          "Actual Loops": 1,
          "Index Name": "agg_search_b"
        }
      ],
      "Node Type": "BitmapOr",
      "Plan Rows": 68,
      "Parallel Aware": false,
      "Actual Total Time": 0.027,
      "Parent Relationship": "Outer",
      "Actual Startup Time": 0.027,
      "Plan Width": 0,
      "Actual Loops": 1,
      "Total Cost": 10.18
    }
      ],
      "Exact Heap Blocks": 1,
      "Node Type": "Bitmap Heap Scan",
      "Plan Rows": 68,
      "Relation Name": "agg",
      "Alias": "agg",
      "Parallel Aware": false,
      "Actual Total Time": 0.037,
      "Recheck Cond": "(((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone)) OR ((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone)))",
      "Lossy Heap Blocks": 0,
      "Actual Startup Time": 0.033,
      "Plan Width": 44,
      "Actual Loops": 1,
      "Total Cost": 280.95
    },
    "Triggers": []
  }
]

Kuncinya adalah bahwa node rencana Anda hanya berisi baris yang diinginkan. Sebelumnya kami mendapat ribuan baris dalam node rencana karena dipilih all points from some point in time to the very end, kemudian node berikutnya menghapus baris yang tidak perlu.

borovsky
sumber