Resep untuk Kueri SQL yang Sakit

Beberapa bulan yang lalu kami mengumumkan jelaskan.tensor.ru - publik layanan untuk mem-parsing dan memvisualisasikan rencana kueri ke PostgreSQL.

Anda telah menggunakannya lebih dari 6000 kali sejak saat itu, namun salah satu fitur berguna yang mungkin luput dari perhatian adalah petunjuk struktural, yang terlihat seperti ini:

Resep untuk Kueri SQL yang Sakit

Dengarkan mereka dan permintaan Anda akan "menjadi selembut sutra". πŸ™‚

Tapi serius, banyak situasi yang membuat permintaan lambat dan "rakus" dalam hal sumber daya, tipikal dan dapat dikenali dari struktur dan data rencana.

Dalam hal ini, setiap pengembang individu tidak perlu mencari opsi pengoptimalan sendiri, hanya mengandalkan pengalamannya sendiri - kami dapat memberi tahu dia apa yang terjadi di sini, apa alasannya, dan bagaimana menemukan solusi. Itulah yang kami lakukan.

Resep untuk Kueri SQL yang Sakit

Mari kita lihat lebih dekat kasus-kasus ini - bagaimana mereka didefinisikan dan rekomendasi apa yang mereka berikan.

Untuk perendaman yang lebih baik dalam topik, pertama-tama Anda dapat mendengarkan blok yang sesuai dari laporan saya di PGConf.Russia 2020, dan baru kemudian pergi ke analisis terperinci dari setiap contoh:

#1: indeks "undersorting"

Kapan?

Tunjukkan faktur terakhir untuk klien "LLC Kolokolchik".

Bagaimana mengidentifikasi

-> Limit
   -> Sort
      -> Index [Only] Scan [Backward] | Bitmap Heap Scan

Rekomendasi

Indeks digunakan perluas dengan bidang sortir.

Contoh:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk  -- 100K "Ρ„Π°ΠΊΡ‚ΠΎΠ²"
, (random() * 1000)::integer fk_cli; -- 1K Ρ€Π°Π·Π½Ρ‹Ρ… Π²Π½Π΅ΡˆΠ½ΠΈΡ… ΠΊΠ»ΡŽΡ‡Π΅ΠΉ

CREATE INDEX ON tbl(fk_cli); -- индСкс для foreign key

SELECT
  *
FROM
  tbl
WHERE
  fk_cli = 1 -- ΠΎΡ‚Π±ΠΎΡ€ ΠΏΠΎ ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½ΠΎΠΉ связи
ORDER BY
  pk DESC -- Ρ…ΠΎΡ‚ΠΈΠΌ всСго ΠΎΠ΄Π½Ρƒ "послСднюю" запись
LIMIT 1;

Resep untuk Kueri SQL yang Sakit
[lihat penjelasan.tensor.ru]

Anda dapat segera melihat bahwa lebih dari 100 catatan dikurangi dengan indeks, yang kemudian diurutkan semuanya, dan kemudian hanya satu yang tersisa.

Kami memperbaiki:

DROP INDEX tbl_fk_cli_idx;
CREATE INDEX ON tbl(fk_cli, pk DESC); -- Π΄ΠΎΠ±Π°Π²ΠΈΠ»ΠΈ ΠΊΠ»ΡŽΡ‡ сортировки

Resep untuk Kueri SQL yang Sakit
[lihat penjelasan.tensor.ru]

Bahkan pada sampel primitif seperti itu - 8.5x lebih cepat dan pembacaan 33x lebih sedikit. Efeknya akan semakin jelas, semakin banyak "fakta" yang Anda miliki untuk setiap nilai. fk.

Saya perhatikan bahwa indeks seperti itu akan berfungsi sebagai indeks "awalan" tidak lebih buruk dari yang sebelumnya untuk kueri lain fk, di mana menyortir pk tidak dan tidak (Anda dapat membaca lebih lanjut tentang ini dalam artikel saya tentang menemukan indeks yang tidak efisien). Secara khusus, itu akan memberikan normal dukungan kunci asing eksplisit oleh bidang ini.

#2: persimpangan indeks (BitmapAnd)

Kapan?

Tampilkan semua kontrak untuk klien "LLC Kolokolchik" menyimpulkan atas nama "NJSC Lyutik".

Bagaimana mengidentifikasi

-> BitmapAnd
   -> Bitmap Index Scan
   -> Bitmap Index Scan

Rekomendasi

membuat indeks komposit menurut bidang dari kedua sumber atau perluas salah satu bidang yang ada dari yang kedua.

Contoh:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk      -- 100K "Ρ„Π°ΠΊΡ‚ΠΎΠ²"
, (random() *  100)::integer fk_org  -- 100 Ρ€Π°Π·Π½Ρ‹Ρ… Π²Π½Π΅ΡˆΠ½ΠΈΡ… ΠΊΠ»ΡŽΡ‡Π΅ΠΉ
, (random() * 1000)::integer fk_cli; -- 1K Ρ€Π°Π·Π½Ρ‹Ρ… Π²Π½Π΅ΡˆΠ½ΠΈΡ… ΠΊΠ»ΡŽΡ‡Π΅ΠΉ

CREATE INDEX ON tbl(fk_org); -- индСкс для foreign key
CREATE INDEX ON tbl(fk_cli); -- индСкс для foreign key

SELECT
  *
FROM
  tbl
WHERE
  (fk_org, fk_cli) = (1, 999); -- ΠΎΡ‚Π±ΠΎΡ€ ΠΏΠΎ ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½ΠΎΠΉ ΠΏΠ°Ρ€Π΅

Resep untuk Kueri SQL yang Sakit
[lihat penjelasan.tensor.ru]

Kami memperbaiki:

DROP INDEX tbl_fk_org_idx;
CREATE INDEX ON tbl(fk_org, fk_cli);

Resep untuk Kueri SQL yang Sakit
[lihat penjelasan.tensor.ru]

Di sini keuntungannya lebih kecil, karena Bitmap Heap Scan sendiri cukup efektif. Tapi bagaimanapun juga 7x lebih cepat dan pembacaan 2.5x lebih sedikit.

#3: Menggabungkan Indeks (BitmapOr)

Kapan?

Tampilkan 20 permintaan terlama "milik sendiri" atau permintaan yang belum ditetapkan untuk diproses, dengan prioritas milik sendiri.

Bagaimana mengidentifikasi

-> BitmapOr
   -> Bitmap Index Scan
   -> Bitmap Index Scan

Rekomendasi

Menggunakan UNI [SEMUA] untuk menggabungkan subkueri untuk setiap kondisi ATAU blok.

Contoh:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk  -- 100K "Ρ„Π°ΠΊΡ‚ΠΎΠ²"
, CASE
    WHEN random() < 1::real/16 THEN NULL -- с Π²Π΅Ρ€ΠΎΡΡ‚Π½ΠΎΡΡ‚ΡŒΡŽ 1:16 запись "Π½ΠΈΡ‡ΡŒΡ"
    ELSE (random() * 100)::integer -- 100 Ρ€Π°Π·Π½Ρ‹Ρ… Π²Π½Π΅ΡˆΠ½ΠΈΡ… ΠΊΠ»ΡŽΡ‡Π΅ΠΉ
  END fk_own;

CREATE INDEX ON tbl(fk_own, pk); -- индСкс с "Π²Ρ€ΠΎΠ΄Π΅ ΠΊΠ°ΠΊ подходящСй" сортировкой

SELECT
  *
FROM
  tbl
WHERE
  fk_own = 1 OR -- свои
  fk_own IS NULL -- ... ΠΈΠ»ΠΈ "Π½ΠΈΡ‡ΡŒΠΈ"
ORDER BY
  pk
, (fk_own = 1) DESC -- сначала "свои"
LIMIT 20;

Resep untuk Kueri SQL yang Sakit
[lihat penjelasan.tensor.ru]

Kami memperbaiki:

(
  SELECT
    *
  FROM
    tbl
  WHERE
    fk_own = 1 -- сначала "свои" 20
  ORDER BY
    pk
  LIMIT 20
)
UNION ALL
(
  SELECT
    *
  FROM
    tbl
  WHERE
    fk_own IS NULL -- ΠΏΠΎΡ‚ΠΎΠΌ "Π½ΠΈΡ‡ΡŒΠΈ" 20
  ORDER BY
    pk
  LIMIT 20
)
LIMIT 20; -- но всСго - 20, большС и нС надо

Resep untuk Kueri SQL yang Sakit
[lihat penjelasan.tensor.ru]

Kami mengambil keuntungan dari fakta bahwa semua 20 record yang diperlukan segera diterima di blok pertama, jadi blok kedua, dengan Bitmap Heap Scan yang lebih "mahal", bahkan tidak dieksekusi - akibatnya 22x lebih cepat, pembacaan 44x lebih sedikit!

Cerita yang lebih rinci tentang metode pengoptimalan ini pada contoh-contoh konkret bisa dibaca di artikel PostgreSQL Antipatterns: GABUNG dan OR yang Berbahaya ΠΈ PostgreSQL Antipatterns: Kisah Penyempurnaan Iteratif Pencarian berdasarkan Nama, atau "Mengoptimalkan Bolak-balik".

Versi umum pemilihan yang dipesan oleh beberapa tombol (dan bukan hanya untuk sepasang const / NULL) dibahas dalam artikel tersebut SQL HowTo: tulis while-loop langsung di kueri, atau "Elementary three-way".

#4: Kami terlalu banyak membaca

Kapan?

Biasanya, ini terjadi saat Anda ingin "melampirkan filter lain" ke permintaan yang ada.

β€œDan kamu tidak memiliki hal yang sama, tapi dengan kancing mutiara? " film "Tangan Berlian"

Misalnya, memodifikasi tugas di atas, menampilkan 20 permintaan "kritis" terlama pertama untuk diproses, apa pun tujuannya.

Bagaimana mengidentifikasi

-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
   && 5 Γ— rows < RRbF -- ΠΎΡ‚Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ΠΎΠ²Π°Π½ΠΎ >80% ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°Π½Π½ΠΎΠ³ΠΎ
   && loops Γ— RRbF > 100 -- ΠΈ ΠΏΡ€ΠΈ этом большС 100 записСй суммарно

Rekomendasi

Buat [lebih] terspesialisasi indeks dengan klausa WHERE atau sertakan bidang tambahan dalam indeks.

Jika kondisi pemfilteran "statis" untuk tugas Anda - yaitu tidak termasuk ekspansi daftar nilai di masa mendatang - lebih baik menggunakan indeks WHERE. Berbagai status boolean/enum cocok dengan kategori ini.

Jika kondisi filtrasi dapat mengambil nilai yang berbeda, lebih baik memperluas indeks dengan bidang-bidang ini - seperti dalam situasi dengan BitmapAnd di atas.

Contoh:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk -- 100K "Ρ„Π°ΠΊΡ‚ΠΎΠ²"
, CASE
    WHEN random() < 1::real/16 THEN NULL
    ELSE (random() * 100)::integer -- 100 Ρ€Π°Π·Π½Ρ‹Ρ… Π²Π½Π΅ΡˆΠ½ΠΈΡ… ΠΊΠ»ΡŽΡ‡Π΅ΠΉ
  END fk_own
, (random() < 1::real/50) critical; -- 1:50, Ρ‡Ρ‚ΠΎ заявка "критичная"

CREATE INDEX ON tbl(pk);
CREATE INDEX ON tbl(fk_own, pk);

SELECT
  *
FROM
  tbl
WHERE
  critical
ORDER BY
  pk
LIMIT 20;

Resep untuk Kueri SQL yang Sakit
[lihat penjelasan.tensor.ru]

Kami memperbaiki:

CREATE INDEX ON tbl(pk)
  WHERE critical; -- Π΄ΠΎΠ±Π°Π²ΠΈΠ»ΠΈ "статичноС" условиС Ρ„ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΠΈ

Resep untuk Kueri SQL yang Sakit
[lihat penjelasan.tensor.ru]

Seperti yang Anda lihat, pemfilteran dari paket benar-benar hilang, dan permintaan telah menjadi 5 kali lebih cepat.

# 5: tabel jarang

Kapan?

Berbagai upaya untuk membuat antrian pemrosesan tugas Anda sendiri, ketika sejumlah besar pembaruan / penghapusan catatan di atas meja menyebabkan situasi sejumlah besar catatan "mati".

Bagaimana mengidentifikasi

-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
   && loops Γ— (rows + RRbF) < (shared hit + shared read) Γ— 8
      -- ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°Π½ΠΎ большС 1KB Π½Π° ΠΊΠ°ΠΆΠ΄ΡƒΡŽ запись
   && shared hit + shared read > 64

Rekomendasi

Lakukan secara manual secara teratur VAKUM [PENUH] atau mencapai pemrosesan yang cukup sering vakum otomatis dengan menyempurnakan parameternya, termasuk untuk meja tertentu.

Dalam banyak kasus, masalah tersebut disebabkan oleh tata letak kueri yang buruk saat dipanggil dari logika bisnis, seperti yang dibahas di PostgreSQL Antipatterns: melawan gerombolan "mati".

Tetapi kita harus memahami bahwa VACUUM FULL pun tidak selalu dapat membantu. Untuk kasus seperti itu, Anda harus membiasakan diri dengan algoritme dari artikel tersebut. DBA: saat VACUUM lewat, kami membersihkan tabel secara manual.

#6: membaca dari "tengah" indeks

Kapan?

Tampaknya mereka membaca sedikit, dan semuanya diindeks, dan mereka tidak memfilter siapa pun secara berlebihan - tetapi tetap saja, halaman yang dibaca jauh lebih banyak daripada yang kami inginkan.

Bagaimana mengidentifikasi

-> Index [Only] Scan [Backward]
   && loops Γ— (rows + RRbF) < (shared hit + shared read) Γ— 8
      -- ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°Π½ΠΎ большС 1KB Π½Π° ΠΊΠ°ΠΆΠ΄ΡƒΡŽ запись
   && shared hit + shared read > 64

Rekomendasi

Perhatikan baik-baik struktur indeks yang digunakan dan bidang kunci yang ditentukan dalam kueri - kemungkinan besar, bagian indeks tidak disetel. Kemungkinan besar Anda perlu membuat indeks serupa, tetapi tanpa bidang awalan, atau belajar untuk mengulangi nilai-nilai mereka.

Contoh:

CREATE TABLE tbl AS
SELECT
  generate_series(1, 100000) pk      -- 100K "Ρ„Π°ΠΊΡ‚ΠΎΠ²"
, (random() *  100)::integer fk_org  -- 100 Ρ€Π°Π·Π½Ρ‹Ρ… Π²Π½Π΅ΡˆΠ½ΠΈΡ… ΠΊΠ»ΡŽΡ‡Π΅ΠΉ
, (random() * 1000)::integer fk_cli; -- 1K Ρ€Π°Π·Π½Ρ‹Ρ… Π²Π½Π΅ΡˆΠ½ΠΈΡ… ΠΊΠ»ΡŽΡ‡Π΅ΠΉ

CREATE INDEX ON tbl(fk_org, fk_cli); -- всС ΠΏΠΎΡ‡Ρ‚ΠΈ ΠΊΠ°ΠΊ Π² #2
-- Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π²ΠΎΡ‚ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ индСкс ΠΏΠΎ fk_cli ΠΌΡ‹ ΡƒΠΆΠ΅ посчитали лишним ΠΈ ΡƒΠ΄Π°Π»ΠΈΠ»ΠΈ

SELECT
  *
FROM
  tbl
WHERE
  fk_cli = 999 -- Π° fk_org Π½Π΅ Π·Π°Π΄Π°Π½ΠΎ, хотя стоит Π² индСксС Ρ€Π°Π½ΡŒΡˆΠ΅
LIMIT 20;

Resep untuk Kueri SQL yang Sakit
[lihat penjelasan.tensor.ru]

Segalanya tampak baik-baik saja, bahkan dalam hal indeks, tetapi entah bagaimana mencurigakan - untuk masing-masing dari 20 catatan yang dibaca, 4 halaman data harus dikurangi, 32KB per catatan - bukankah itu tebal? Ya dan nama indeks tbl_fk_org_fk_cli_idx mengarah pada pemikiran.

Kami memperbaiki:

CREATE INDEX ON tbl(fk_cli);

Resep untuk Kueri SQL yang Sakit
[lihat penjelasan.tensor.ru]

Tiba-tiba - 10 kali lebih cepat dan 4 kali lebih sedikit untuk membaca!

Untuk lebih banyak contoh penggunaan indeks yang tidak efisien, lihat artikel DBA: temukan indeks yang tidak berguna.

#7: KTE Γ— KTE

Kapan?

dalam permintaan mencetak CTE "gemuk". dari tabel yang berbeda, dan kemudian memutuskan untuk melakukannya di antara mereka JOIN.

Kasing ini relevan untuk versi di bawah v12 atau permintaan dengan WITH MATERIALIZED.

Bagaimana mengidentifikasi

-> CTE Scan
   && loops > 10
   && loops Γ— (rows + RRbF) > 10000
      -- слишком большоС Π΄Π΅ΠΊΠ°Ρ€Ρ‚ΠΎΠ²ΠΎ ΠΏΡ€ΠΎΠΈΠ·Π²Π΅Π΄Π΅Π½ΠΈΠ΅ CTE

Rekomendasi

Analisis permintaan dengan hati-hati apakah CTE dibutuhkan di sini sama sekali? Jika ya, maka terapkan "kamus" di hstore/json sesuai dengan model yang dijelaskan di PostgreSQL Antipatterns: Dictionary Hit Heavy JOIN.

#8: tukar ke disk (ditulis sementara)

Kapan?

Pemrosesan satu kali (pengurutan atau keunikan) dari sejumlah besar rekaman tidak sesuai dengan memori yang dialokasikan untuk ini.

Bagaimana mengidentifikasi

-> *
   && temp written > 0

Rekomendasi

Jika jumlah memori yang digunakan oleh operasi tidak melebihi nilai parameter yang ditetapkan pekerjaan_mem, itu harus diperbaiki. Anda dapat langsung di konfigurasi untuk semua orang, atau Anda dapat melalui SET [LOCAL] untuk permintaan/transaksi tertentu.

Contoh:

SHOW work_mem;
-- "16MB"

SELECT
  random()
FROM
  generate_series(1, 1000000)
ORDER BY
  1;

Resep untuk Kueri SQL yang Sakit
[lihat penjelasan.tensor.ru]

Kami memperbaiki:

SET work_mem = '128MB'; -- ΠΏΠ΅Ρ€Π΅Π΄ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ΠΌ запроса

Resep untuk Kueri SQL yang Sakit
[lihat penjelasan.tensor.ru]

Untuk alasan yang jelas, jika hanya memori yang digunakan dan bukan disk, kueri akan jauh lebih cepat. Pada saat yang sama, sebagian beban juga dikeluarkan dari HDD.

Tetapi Anda perlu memahami bahwa mengalokasikan banyak memori juga tidak akan selalu berhasil - itu tidak akan cukup untuk semua orang.

#9: Statistik yang tidak relevan

Kapan?

Banyak yang dituangkan ke pangkalan sekaligus, tetapi mereka tidak punya waktu untuk mengusirnya ANALYZE.

Bagaimana mengidentifikasi

-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
   && ratio >> 10

Rekomendasi

Habiskan sama ANALYZE.

Situasi ini dijelaskan secara lebih rinci di PostgreSQL Antipatterns: statistik adalah kepala dari segalanya.

#10: "ada yang tidak beres"

Kapan?

Ada kunci yang menunggu permintaan bersaing, atau sumber daya perangkat keras CPU/hypervisor tidak cukup.

Bagaimana mengidentifikasi

-> *
   && (shared hit / 8K) + (shared read / 1K) < time / 1000
      -- RAM hit = 64MB/s, HDD read = 8MB/s
   && time > 100ms -- Ρ‡ΠΈΡ‚Π°Π»ΠΈ ΠΌΠ°Π»ΠΎ, Π½ΠΎ слишком Π΄ΠΎΠ»Π³ΠΎ

Rekomendasi

Gunakan eksternal sistem pemantauan server untuk pemblokiran atau konsumsi sumber daya yang tidak normal. Kami telah berbicara tentang versi kami dalam mengatur proses ini untuk ratusan server. di sini ΠΈ di sini.

Resep untuk Kueri SQL yang Sakit
Resep untuk Kueri SQL yang Sakit

Sumber: www.habr.com

Tambah komentar