AnalyticDB for PostgreSQL menyediakan sejumlah alat untuk mendiagnosis dan memperbaiki kueri lambat. Gunakan panduan ini untuk mengidentifikasi akar permasalahan dan menerapkan perbaikan yang tepat.
| Symptom | Where to look |
|---|---|
| Kueri menjadi lambat setelah pemuatan data massal | Kumpulkan statistik tabel |
| Kueri kompleks multi-tabel lambat | Pilih query optimizer |
| Pencarian titik (point lookups) atau kueri rentang (range queries) lambat | Gunakan indeks untuk mempercepat kueri |
| Tidak yakin apa yang sedang dilakukan database | Lihat rencana kueri |
| Operasi join memicu lalu lintas jaringan tinggi | Hapus operator distribute |
| Kolom join menggunakan tipe data berbeda | Gunakan tipe data yang sama pada kolom join |
| Satu node komputasi melakukan seluruh pekerjaan | Temukan data skew |
| Terlalu banyak kueri konkuren menumpuk | Lihat Pernyataan SQL yang sedang berjalan |
| Kueri macet dalam keadaan menunggu | Periksa status lock |
| Kueri sangat selektif tetap lambat meskipun sudah ada indeks | Aktifkan nested loop joins |
Kumpulkan statistik tabel
Pengoptimal kueri menghasilkan rencana eksekusi berdasarkan statistik tabel. Statistik yang usang atau tidak tersedia menyebabkan pengoptimal membuat estimasi yang tidak akurat, sehingga menghasilkan rencana yang tidak efisien.
Jalankan ANALYZE setelah pemuatan data besar atau setelah lebih dari 20% baris tabel diperbarui.
-- Kumpulkan statistik pada semua tabel
ANALYZE;
-- Kumpulkan statistik pada semua kolom tabel t
ANALYZE t;
-- Kumpulkan statistik pada kolom tertentu
ANALYZE t(a);
Untuk sebagian besar workload, menjalankan ANALYZE t pada tabel yang dimodifikasi sudah cukup. Gunakan ANALYZE tingkat kolom hanya jika Anda memerlukan kontrol lebih ketat—misalnya, pada kolom yang digunakan sebagai kunci gabungan, kondisi filter, atau kolom indeks.
Pilih query optimizer
AnalyticDB for PostgreSQL menyertakan dua query optimizer, masing-masing dioptimalkan untuk workload yang berbeda.
| Legacy optimizer | ORCA query optimizer | |
|---|---|---|
| Default in | V4.3 | V6.0 |
| Best for | Kueri simple dengan konkurensi tinggi; join hingga 3 tabel; workload INSERT, UPDATE, DELETE | Kueri kompleks; join lebih dari 3 tabel; workload extract, transform, load (ETL) dan pelaporan; SQL dengan subkueri (menghilangkan kebutuhan join tabel dalam subkueri); kueri tabel partisi dengan kondisi filter berparameter (dynamic partition filtering) |
| Trade-off | Pembuatan rencana lebih cepat | Mengeksplorasi lebih banyak jalur eksekusi; membutuhkan waktu lebih lama untuk menghasilkan rencana |
Ganti optimizer pada tingkat sesi:
-- Aktifkan Legacy query optimizer
SET optimizer = off;
-- Aktifkan ORCA query optimizer
SET optimizer = on;
-- Periksa optimizer saat ini
SHOW optimizer;
-- on = ORCA query optimizer
-- off = Legacy optimizer
Untuk mengubah optimizer pada tingkat instans, ajukan tiket.
Gunakan indeks untuk mempercepat kueri
Indeks mempercepat kueri yang memindai sebagian kecil tabel berdasarkan kondisi filter. AnalyticDB for PostgreSQL mendukung tiga jenis indeks.
| Jenis indeks | Gunakan saat |
|---|---|
| B-tree index | Kolom memiliki banyak nilai unik dan digunakan untuk memfilter, menggabungkan, atau mengurutkan data |
| Bitmap index | Kolom memiliki sedikit nilai unik dan dirujuk oleh lebih dari satu kondisi filter |
| GiST index | Kueri melibatkan lokasi geografis, rentang (ranges), fitur gambar, atau nilai Geometry |
Contoh: menambahkan B-tree index
Tanpa indeks, pengoptimal melakukan pemindaian tabel penuh:
postgres=# EXPLAIN SELECT * FROM t WHERE b = 1;
QUERY PLAN
-------------------------------------------------------------------------------
Gather Motion 3:1 (slice1; segments: 3) (cost=0.00..431.00 rows=1 width=16)
-> Table Scan on t (cost=0.00..431.00 rows=1 width=16)
Filter: b = 1
Settings: optimizer=on
Optimizer status: PQO version 1.609
(5 rows)
Buat B-tree index pada kolom b:
postgres=# CREATE INDEX i_t_b ON t USING btree (b);
CREATE INDEX
Dengan indeks tersebut, pengoptimal menggunakan Pemindaian indeks sebagai gantinya:
postgres=# EXPLAIN SELECT * FROM t WHERE b = 1;
QUERY PLAN
-----------------------------------------------------------------------------
Gather Motion 3:1 (slice1; segments: 3) (cost=0.00..2.00 rows=1 width=16)
-> Index Scan using i_t_b on t (cost=0.00..2.00 rows=1 width=16)
Index Cond: b = 1
Settings: optimizer=on
Optimizer status: PQO version 1.609
(5 rows)
Lihat rencana kueri
Rencana kueri adalah struktur pohon operator yang menjelaskan cara database mengeksekusi kueri. Membaca rencana tersebut membantu Anda mengidentifikasi penyebab kueri lambat.
Cara membaca rencana kueri
-
Rencana tersebut berupa pohon. Eksekusi dimulai dari node daun (paling bawah) dan mengalir ke akar.
-
cost=<startup>..<total>adalah estimasi biaya oleh pengoptimal, bukan milidetik aktual. Estimasi biaya tinggi berarti pengoptimal memperkirakan operasi tersebut mahal—gunakan untuk perbandingan, bukan sebagai ukuran absolut. -
rows=adalah perkiraan jumlah baris. Selisih besar antara estimasi dan jumlah baris aktual sering kali menunjukkan statistik usang—jalankanANALYZEuntuk memperbaikinya.
EXPLAIN vs EXPLAIN ANALYZE
-
EXPLAINmenampilkan rencana tanpa menjalankan kueri. -
EXPLAIN ANALYZEmenjalankan kueri dan menambahkan waktu eksekusi aktual serta jumlah baris ke dalam rencana.
-- Tampilkan hanya rencana (tanpa eksekusi)
postgres=# EXPLAIN SELECT a, b FROM t;
QUERY PLAN
------------------------------------------------------------------------------
Gather Motion 3:1 (slice1; segments: 3) (cost=0.00..4.00 rows=100 width=8)
-> Seq Scan on t (cost=0.00..4.00 rows=34 width=8)
Optimizer status: legacy query optimizer
(3 rows)
-- Jalankan kueri dan tampilkan waktu aktual
postgres=# EXPLAIN ANALYZE SELECT a, b FROM t;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------
Gather Motion 3:1 (slice1; segments: 3) (cost=0.00..4.00 rows=100 width=8)
Rows out: 100 rows at destination with 2.728 ms to first row, 2.838 ms to end, start offset by 0.418 ms.
-> Seq Scan on t (cost=0.00..4.00 rows=34 width=8)
Rows out: Avg 33.3 rows x 3 workers. Max 37 rows (seg2) with 0.088 ms to first row, 0.107 ms to end, start offset by 2.887 ms.
Slice statistics:
(slice0) Executor memory: 131K bytes.
(slice1) Executor memory: 163K bytes avg x 3 workers, 163K bytes max (seg0).
Statement statistics:
Memory used: 128000K bytes
Optimizer status: legacy query optimizer
Total runtime: 3.739 ms
(11 rows)
Identifikasi bottleneck dari output EXPLAIN ANALYZE
Gunakan data waktu aktual dan jumlah baris untuk mengklasifikasikan bottleneck:
| Apa yang Anda amati | Kemungkinan bottleneck | Langkah selanjutnya |
|---|---|---|
| Waktu tinggi pada operator Redistribute Motion atau Broadcast Motion | Jaringan — data dipindahkan antar node | Sejajarkan kunci distribusi pada kolom join (lihat Hapus operator distribute) |
| Estimasi jumlah baris sangat berbeda dari jumlah baris aktual | Statistik usang | Jalankan ANALYZE pada tabel yang terpengaruh |
| Waktu tinggi pada Seq Scan atau Table Scan dengan jumlah baris besar | I/O — pemindaian tabel penuh | Tambahkan indeks pada kolom filter (lihat Gunakan indeks untuk mempercepat kueri) |
| Penggunaan memori tinggi dalam statistik Slice | Tekanan memori | Kurangi konkurensi atau tingkatkan resource instans |
Jenis operator yang didukung
| Kategori | Operator |
|---|---|
| Pemindaian data | Seq Scan, Table Scan, Index Scan, Bitmap Scan |
| Join | Hash Join, Nested Loop, Merge Join |
| Aggregate | Hash Aggregate, Group Aggregate |
| Distribute | Redistribute Motion, Broadcast Motion, Gather Motion |
| Lainnya | Hash, Sort, Limit, Append |
Contoh: membaca rencana kueri join
postgres=# EXPLAIN SELECT * FROM t1, t2 WHERE t1.b = t2.b;
QUERY PLAN
-------------------------------------------------------------------------------------------------------
Gather Motion 3:1 (slice3; segments: 3) (cost=0.00..862.00 rows=1 width=32)
-> Hash Join (cost=0.00..862.00 rows=1 width=32)
Hash Cond: t1.b = t2.b
-> Redistribute Motion 3:3 (slice1; segments: 3) (cost=0.00..431.00 rows=1 width=16)
Hash Key: t1.b
-> Table Scan on t1 (cost=0.00..431.00 rows=1 width=16)
-> Hash (cost=431.00..431.00 rows=1 width=16)
-> Redistribute Motion 3:3 (slice2; segments: 3) (cost=0.00..431.00 rows=1 width=16)
Hash Key: t2.b
-> Table Scan on t2 (cost=0.00..431.00 rows=1 width=16)
Settings: optimizer=on
Optimizer status: PQO version 1.609
(12 rows)
Membaca rencana ini dari bawah ke atas:
-
Table Scan — memindai
t1dant2. -
Redistribute Motion — mengacak baris dari kedua tabel di seluruh node komputasi berdasarkan nilai hash dari
b, sehingga baris yang cocok berada di node yang sama. -
Hash — membangun tabel hash pada
t2untuk operasi join. -
Hash Join — menggabungkan
t1dant2berdasarkan kunci hash. -
Gather Motion — mengirim hasil dari node komputasi ke node koordinator, yang kemudian mengembalikannya ke klien.
Hapus operator distribute
Saat AnalyticDB for PostgreSQL melakukan join atau agregasi data di seluruh node komputasi, sistem menyisipkan operator Redistribute Motion atau Broadcast Motion untuk mengacak data. Operator-operator ini mengonsumsi lebar pita jaringan yang signifikan. Menyelaraskan kunci distribusi pada kolom join menghilangkan kebutuhan redistribusi data.
Cara kerjanya
Jika dua tabel di-join berdasarkan kolom a dan keduanya didistribusikan berdasarkan a, setiap node komputasi sudah menyimpan baris yang cocok—tidak diperlukan perpindahan data.
Contoh
SELECT * FROM t1, t2 WHERE t1.a = t2.a;
t1 didistribusikan berdasarkan kolom a. Jika t2 didistribusikan berdasarkan kolom b (tidak cocok), AnalyticDB for PostgreSQL harus mendistribusikan ulang t2:
postgres=# EXPLAIN SELECT * FROM t1, t2 WHERE t1.a=t2.a;
QUERY PLAN
-------------------------------------------------------------------------------------------------------
Gather Motion 3:1 (slice2; segments: 3) (cost=0.00..862.00 rows=1 width=32)
-> Hash Join (cost=0.00..862.00 rows=1 width=32)
Hash Cond: t1.a = t2.a
-> Table Scan on t1 (cost=0.00..431.00 rows=1 width=16)
-> Hash (cost=431.00..431.00 rows=1 width=16)
-> Redistribute Motion 3:3 (slice1; segments: 3) (cost=0.00..431.00 rows=1 width=16)
Hash Key: t2.a
-> Table Scan on t2 (cost=0.00..431.00 rows=1 width=16)
Settings: optimizer=on
Optimizer status: PQO version 1.609
(10 rows)
Jika t2 juga didistribusikan berdasarkan a, operasi join berjalan tanpa redistribusi:
postgres=# EXPLAIN SELECT * FROM t1, t2 WHERE t1.a=t2.a;
QUERY PLAN
-------------------------------------------------------------------------------
Gather Motion 3:1 (slice1; segments: 3) (cost=0.00..862.00 rows=1 width=32)
-> Hash Join (cost=0.00..862.00 rows=1 width=32)
Hash Cond: t1.a = t2.a
-> Table Scan on t1 (cost=0.00..431.00 rows=1 width=16)
-> Hash (cost=431.00..431.00 rows=1 width=16)
-> Table Scan on t2 (cost=0.00..431.00 rows=1 width=16)
Settings: optimizer=on
Optimizer status: PQO version 1.609
(8 rows)
Untuk memperbaiki ketidakcocokan tersebut, ubah kunci distribusi t2:
ALTER TABLE t2 SET DISTRIBUTED BY (a);
Gunakan tipe data yang sama pada kolom join
Menggabungkan kolom dengan tipe data berbeda memicu konversi tipe data, yang memaksa redistribusi data. Gunakan tipe data yang sama pada kedua sisi kunci join.
Konversi tipe eksplisit
Cast eksplisit dalam pernyataan SQL mengubah fungsi hash yang diterapkan pada kolom, menyebabkan pengoptimal mendistribusikan ulang kedua tabel:
-- Tanpa konversi tipe: tidak ada redistribusi
postgres=# EXPLAIN SELECT * FROM t1, t2 WHERE t1.a=t2.a;
QUERY PLAN
-------------------------------------------------------------------------------
Gather Motion 3:1 (slice1; segments: 3) (cost=0.00..862.00 rows=1 width=32)
-> Hash Join (cost=0.00..862.00 rows=1 width=32)
Hash Cond: t1.a = t2.a
-> Table Scan on t1 (cost=0.00..431.00 rows=1 width=16)
-> Hash (cost=431.00..431.00 rows=1 width=16)
-> Table Scan on t2 (cost=0.00..431.00 rows=1 width=16)
Settings: optimizer=on
Optimizer status: PQO version 1.609
(8 rows)
-- Cast eksplisit ke numeric: memicu redistribusi kedua tabel
postgres=# EXPLAIN SELECT * FROM t1, t2 WHERE t1.a=t2.a::numeric;
QUERY PLAN
-------------------------------------------------------------------------------------------------------
Gather Motion 3:1 (slice3; segments: 3) (cost=0.00..862.00 rows=1 width=32)
-> Hash Join (cost=0.00..862.00 rows=1 width=32)
Hash Cond: t1.a::numeric = t2.a::numeric
-> Redistribute Motion 3:3 (slice1; segments: 3) (cost=0.00..431.00 rows=1 width=16)
Hash Key: t1.a::numeric
-> Table Scan on t1 (cost=0.00..431.00 rows=1 width=16)
-> Hash (cost=431.00..431.00 rows=1 width=16)
-> Redistribute Motion 3:3 (slice2; segments: 3) (cost=0.00..431.00 rows=1 width=16)
Hash Key: t2.a::numeric
-> Table Scan on t2 (cost=0.00..431.00 rows=1 width=16)
Settings: optimizer=on
Optimizer status: PQO version 1.609
(12 rows)
Konversi tipe implisit
Saat dua kolom join menggunakan tipe yang tidak kompatibel, database secara otomatis mengonversi salah satu tipe. Misalnya, timestamp without time zone dan timestamp with time zone menggunakan fungsi hash berbeda. Pengoptimal kembali menggunakan Broadcast Motion alih-alih join berbasis hash:
postgres=# CREATE TABLE t1 (a timestamp without time zone);
CREATE TABLE
postgres=# CREATE TABLE t2 (a timestamp with time zone);
CREATE TABLE
postgres=# EXPLAIN SELECT * FROM t1, t2 WHERE t1.a=t2.a;
QUERY PLAN
-------------------------------------------------------------------------------------------------
Gather Motion 3:1 (slice2; segments: 3) (cost=0.04..0.11 rows=4 width=16)
-> Nested Loop (cost=0.04..0.11 rows=2 width=16)
Join Filter: t1.a = t2.a
-> Seq Scan on t1 (cost=0.00..0.00 rows=1 width=8)
-> Materialize (cost=0.04..0.07 rows=1 width=8)
-> Broadcast Motion 3:3 (slice1; segments: 3) (cost=0.00..0.04 rows=1 width=8)
-> Seq Scan on t2 (cost=0.00..0.00 rows=1 width=8)
(7 rows)
Untuk menghilangkan konversi implisit, sejajarkan tipe kolom saat definisi tabel. Misalnya, definisikan t1.a dan t2.a sebagai timestamp without time zone.
Temukan data skew
Data skew terjadi ketika baris tersebar tidak merata di seluruh node komputasi. Tabel yang skewed menyebabkan satu node komputasi melakukan sebagian besar pekerjaan sementara node lain menganggur, sehingga memperlambat seluruh kueri.
Periksa distribusi baris di seluruh node komputasi:
postgres=# SELECT gp_segment_id, count(1) FROM t1 GROUP BY 1 ORDER BY 2 DESC;
gp_segment_id | count
---------------+-------
0 | 16415
2 | 37
1 | 32
(3 rows)
Output ini menunjukkan skew parah: node 0 menyimpan 99% baris. Perbaiki dengan mengganti kunci distribusi:
-- Opsi 1: ubah kunci distribusi langsung
ALTER TABLE t1 SET DISTRIBUTED BY (b);
-- Opsi 2: buat ulang tabel dengan kunci distribusi yang lebih baik
-- Buat tabel baru, muat data secara massal, lalu tukar posisinya dengan tabel asli
Pilih kunci distribusi dengan kardinalitas tinggi dan tanpa nilai dominan untuk memastikan penyebaran yang merata.
Lihat Pernyataan SQL yang sedang berjalan
Saat banyak kueri dijalankan secara konkuren, instans mungkin melaporkan respons lambat atau sumber daya tidak mencukupi. Gunakan tampilan pg_stat_activity untuk melihat apa yang sedang berjalan.
postgres=# SELECT * FROM pg_stat_activity;
Bidang utama:
| Bidang | Deskripsi |
|---|---|
procpid |
ID proses master yang mengeksekusi kueri |
usename |
Nama pengguna sesi |
current_query |
Teks kueri saat ini |
waiting |
Apakah kueri sedang menunggu lock |
query_start |
Kapan kueri dimulai |
backend_start |
Kapan proses backend dimulai |
xact_start |
Kapan transaksi saat ini dimulai |
waiting_reason |
Alasan kueri menunggu |
Filter hanya untuk kueri aktif:
SELECT * FROM pg_stat_activity WHERE current_query != '<IDLE>';
Temukan lima kueri dengan waktu eksekusi terlama:
SELECT current_timestamp - query_start AS runtime,
datname,
usename,
current_query
FROM pg_stat_activity
WHERE current_query != '<IDLE>'
ORDER BY runtime DESC
LIMIT 5;
Periksa status lock
Jika suatu kueri memegang lock dalam waktu lama, kueri lain pada objek yang sama akan menunggu tanpa batas. Jalankan kueri berikut untuk melihat tabel mana yang dikunci dan sesi mana yang memegang lock tersebut:
SELECT pgl.locktype AS locktype,
pgl.database AS database,
pgc.relname AS relname,
pgl.relation AS relation,
pgl.transaction AS transaction,
pgl.pid AS pid,
pgl.mode AS mode,
pgl.granted AS granted,
pgsa.current_query AS query
FROM pg_locks pgl
JOIN pg_class pgc ON pgl.relation = pgc.oid
JOIN pg_stat_activity pgsa ON pgl.pid = pgsa.procpid
ORDER BY pgc.relname;
Untuk membuka blokir kueri yang menunggu, batalkan atau hentikan sesi yang memegang lock:
-- Batalkan kueri (tidak berfungsi jika sesi IDLE)
SELECT pg_cancel_backend(pid);
-- Hentikan sesi dan rollback transaksi yang belum dikomit
SELECT pg_terminate_backend(pid);
pg_cancel_backendtidak berpengaruh ketikapg_stat_activity.current_querymenampilkanIDLE. Gunakanpg_terminate_backenddalam kasus tersebut.
Aktifkan nested loop joins
Nested loop joins dinonaktifkan secara default. Untuk kueri yang mengembalikan jumlah baris kecil—karena kondisi filter sangat selektif dan klausa LIMIT—mengaktifkan nested loop joins dapat secara signifikan mengurangi waktu kueri.
Kapan mengaktifkan
Optimasi ini efektif ketika:
-
Kondisi filter pada satu tabel mengembalikan sangat sedikit baris.
-
Klausa LIMIT lebih lanjut membatasi set hasil.
-
Indeks tersedia pada kolom join kedua tabel.
Contoh
SELECT *
FROM t1 JOIN t2 ON t1.c1 = t2.c1
WHERE t1.c2 >= '230769548' AND t1.c2 < '230769549'
LIMIT 100;
Periksa dan aktifkan nested loop joins untuk sesi tersebut:
SHOW enable_nestloop;
enable_nestloop
-----------------
off
SET enable_nestloop = on;
SHOW enable_nestloop;
enable_nestloop
-----------------
on
EXPLAIN SELECT * FROM t1 JOIN t2 ON t1.c1 = t2.c1
WHERE t1.c2 >= '230769548' AND t1.c2 < '23432442'
LIMIT 100;
QUERY PLAN
-----------------------------------------------------------------------------------------------
Limit (cost=0.26..16.31 rows=1 width=18608)
-> Nested Loop (cost=0.26..16.31 rows=1 width=18608)
-> Index Scan using t1 on c2 (cost=0.12..8.14 rows=1 width=12026)
Filter: ((c2 >= '230769548'::bpchar) AND (c2 < '230769549'::bpchar))
-> Index Scan using t2 on c1 (cost=0.14..8.15 rows=1 width=6582)
Index Cond: ((c1)::text = (T1.c1)::text)
Rencana tersebut menggunakan pemindaian indeks pada kedua tabel dan nested loop join, menghindari pemindaian tabel penuh dan redistribusi data.
enable_nestloop adalah pengaturan tingkat sesi. Atur ulang setelah kueri jika kueri lain dalam sesi yang sama lebih diuntungkan oleh hash join.