Apakah tampilan berbahaya untuk kinerja di PostgreSQL?


45

Berikut ini adalah kutipan dari buku tentang desain db (Permulaan Desain Database ISBN: 0-7645-7490-6):

Bahaya menggunakan tampilan adalah memfilter kueri terhadap tampilan, berharap untuk membaca sebagian kecil dari tabel yang sangat besar. Penyaringan apa pun harus dilakukan dalam tampilan karena setiap penyaringan terhadap tampilan itu sendiri diterapkan setelah kueri dalam tampilan telah menyelesaikan eksekusi. Tampilan biasanya berguna untuk mempercepat proses pengembangan tetapi dalam jangka panjang dapat benar-benar mematikan kinerja basis data.

Berikut ini adalah kutipan dari Dokumentasi PostgreSQL 9.5:

Memanfaatkan pandangan secara bebas adalah aspek kunci dari desain database SQL yang baik. Tampilan memungkinkan Anda untuk merangkum detail struktur tabel Anda, yang mungkin berubah ketika aplikasi Anda berkembang, di belakang antarmuka yang konsisten.

Kedua sumber tersebut tampaknya saling bertentangan ("jangan desain dengan tampilan" vs "desain dengan tampilan").

Namun, dalam pandangan PG diimplementasikan menggunakan sistem aturan. Jadi, mungkin (dan ini adalah pertanyaan saya) setiap pemfilteran terhadap tampilan ditulis ulang sebagai filter dalam tampilan, menghasilkan eksekusi permintaan tunggal terhadap tabel yang mendasarinya.

Apakah interpretasi saya benar dan AM menggabungkan klausa WHERE ke dalam dan ke luar tampilan? Atau apakah itu menjalankannya secara terpisah, satu demi satu? Adakah contoh pendek, mandiri, benar (kompilasi),?


Saya pikir pertanyaannya tidak benar karena kedua sumber tidak berbicara tentang hal yang sama. Yang pertama terkait dengan permintaan dari tampilan dan SETELAH menerapkan filter: SELECT * FROM my_view WHERE my_column = 'blablabla';.Sementara yang kedua adalah tentang menggunakan tampilan untuk membuat model data Anda transparan untuk aplikasi yang menggunakannya. Sumber pertama menunjukkan Anda untuk menyertakan filter WHERE my_column = 'blablabla'di dalam definisi tampilan, karena ini menghasilkan rencana eksekusi yang lebih baik.
EAmez

Jawaban:


49

Buku itu salah.

Memilih dari tampilan persis secepat atau lambat seperti menjalankan pernyataan SQL yang mendasarinya - Anda dapat dengan mudah memeriksa menggunakan itu explain analyze.

Pengoptimal Postgres (dan pengoptimal untuk banyak DBMS modern lainnya) akan dapat menekan predikat tampilan ke pernyataan tampilan aktual - asalkan ini adalah pernyataan sederhana (sekali lagi, ini dapat diverifikasi menggunakan explain analyze).

"Reputasi buruk" berkenaan dengan kinerja bermula - saya pikir - dari saat Anda terlalu sering menggunakan tampilan dan mulai membangun tampilan yang menggunakan tampilan yang menggunakan tampilan. Sangat sering hal itu menghasilkan pernyataan yang terlalu banyak dibandingkan dengan pernyataan yang dirancang sendiri tanpa tampilan misalnya karena beberapa tabel perantara tidak diperlukan. Dalam hampir semua kasus, pengoptimal tidak cukup pintar untuk menghapus tabel / gabungan yang tidak dibutuhkan atau untuk menurunkan predikat pada beberapa level tampilan (ini juga berlaku untuk DBMS lainnya).


3
Mengingat beberapa jawaban balik yang diajukan, Anda mungkin ingin menguraikan sedikit tentang apa itu pernyataan sederhana .
RDFozz

Bisakah Anda menjelaskan cara menggunakan explain analyzepernyataan itu?
Dustin Michels


19

Untuk memberi Anda sebuah contoh tentang apa yang dijelaskan @a_horse :

Postgres mengimplementasikan skema informasi, yang terdiri dari (terkadang kompleks) pandangan yang memberikan informasi tentang objek DB dalam bentuk standar. Ini nyaman dan dapat diandalkan - dan dapat secara substansial lebih mahal daripada mengakses tabel katalog Postgres secara langsung.

Contoh yang sangat sederhana, untuk mendapatkan semua kolom tabel yang terlihat
... dari skema informasi:

SELECT column_name
FROM   information_schema.columns
WHERE  table_name = 'big'
AND    table_schema = 'public';

... dari katalog sistem:

SELECT attname
FROM   pg_catalog.pg_attribute
WHERE  attrelid = 'public.big'::regclass
AND    attnum > 0
AND    NOT attisdropped;

Bandingkan paket permintaan dan waktu eksekusi untuk keduanya dengan EXPLAIN ANALYZE.

  • Kueri pertama didasarkan pada tampilan information_schema.columns, yang bergabung ke beberapa tabel yang tidak kita perlukan untuk ini sama sekali.

  • Kueri kedua hanya memindai satu tabel pg_catalog.pg_attribute, karenanya jauh lebih cepat. (Tetapi permintaan pertama masih hanya membutuhkan beberapa ms dalam DB umum.)

Detail:


7

SUNTING:

Dengan permintaan maaf, saya perlu menarik kembali pernyataan saya bahwa jawaban yang diterima tidak selalu benar - itu menyatakan bahwa pandangan selalu identik dengan hal yang sama yang ditulis sebagai subquery. Saya pikir itu tidak dapat dibantah, dan saya pikir saya sekarang tahu apa yang terjadi dalam kasus saya.

Saya sekarang juga berpikir ada jawaban yang lebih baik untuk pertanyaan awal.

Pertanyaan aslinya adalah tentang apakah seharusnya membimbing praktik untuk menggunakan pandangan (sebagai lawan, misalnya, mengulangi SQL dalam rutinitas yang mungkin perlu dipertahankan dua kali atau lebih).

Jawaban saya adalah "tidak jika kueri Anda menggunakan fungsi jendela atau apa pun yang menyebabkan pengoptimal memperlakukan kueri secara berbeda ketika menjadi subquery, karena tindakan membuat subquery (apakah diwakili sebagai tampilan atau tidak) dapat menurunkan kinerja jika Anda memfilter dengan parameter saat runtime.

Kompleksitas fungsi jendela saya tidak perlu. Rencana jelaskan untuk ini:

SELECT DISTINCT ts.train_service_key,
            pc.assembly_key,
            count(*) OVER 
              (PARTITION BY ts.train_service_key) AS train_records
FROM staging.train_service ts
   JOIN staging.portion_consist pc 
     USING (ds_code, train_service_key)
WHERE assembly_key = '185132';

jauh lebih murah daripada ini:

SELECT *
FROM (SELECT DISTINCT ts.train_service_key,
            pc.assembly_key,
            count(*) OVER
              (PARTITION BY ts.train_service_key) AS train_records
FROM staging.train_service ts
   JOIN staging.portion_consist pc
     USING (ds_code, train_service_key)) AS query
WHERE assembly_key = '185132';

Semoga itu sedikit lebih spesifik dan bermanfaat.

Dalam pengalaman saya baru-baru ini (menyebabkan saya menemukan pertanyaan ini), jawaban yang diterima di atas tidak benar dalam semua keadaan. Saya memiliki permintaan yang relatif sederhana yang mencakup fungsi jendela:

SELECT DISTINCT ts.train_service_key,
                pc.assembly_key,
                dense_rank() OVER (PARTITION BY ts.train_service_key
                ORDER BY pc.through_idx DESC, pc.first_portion ASC,
               ((CASE WHEN (NOT ts.primary_direction)
                 THEN '-1' :: INTEGER
                 ELSE 1
                 END) * pc.first_seq)) AS coach_block_idx
FROM (staging.train_service ts
JOIN staging.portion_consist pc USING (ds_code, train_service_key))

Jika saya menambahkan filter ini:

where assembly_key = '185132'

Rencana jelaskan yang saya dapatkan adalah sebagai berikut:

QUERY PLAN
Unique  (cost=11562.66..11568.77 rows=814 width=43)
  ->  Sort  (cost=11562.66..11564.70 rows=814 width=43)
    Sort Key: ts.train_service_key, (dense_rank() OVER (?))
    ->  WindowAgg  (cost=11500.92..11523.31 rows=814 width=43)
          ->  Sort  (cost=11500.92..11502.96 rows=814 width=35)
                Sort Key: ts.train_service_key, pc.through_idx DESC, pc.first_portion, ((CASE WHEN (NOT ts.primary_direction) THEN '-1'::integer ELSE 1 END * pc.first_seq))
                ->  Nested Loop  (cost=20.39..11461.57 rows=814 width=35)
                      ->  Bitmap Heap Scan on portion_consist pc  (cost=19.97..3370.39 rows=973 width=38)
                            Recheck Cond: (assembly_key = '185132'::text)
                            ->  Bitmap Index Scan on portion_consist_assembly_key_index  (cost=0.00..19.72 rows=973 width=0)
                                  Index Cond: (assembly_key = '185132'::text)
                      ->  Index Scan using train_service_pk on train_service ts  (cost=0.43..8.30 rows=1 width=21)
                            Index Cond: ((ds_code = pc.ds_code) AND (train_service_key = pc.train_service_key))

Ini menggunakan indeks kunci utama pada tabel layanan kereta dan indeks non-unik pada tabel portion_consist. Ini dijalankan dalam 90ms.

Saya membuat tampilan (menempelkannya di sini agar benar-benar jelas tapi secara harfiah kueri dalam tampilan):

CREATE OR REPLACE VIEW staging.v_unit_coach_block AS
SELECT DISTINCT ts.train_service_key,
            pc.assembly_key,
            dense_rank() OVER (PARTITION BY ts.train_service_key
              ORDER BY pc.through_idx DESC, pc.first_portion ASC, (
                (CASE
              WHEN (NOT ts.primary_direction)
                THEN '-1' :: INTEGER
              ELSE 1
              END) * pc.first_seq)) AS coach_block_idx
 FROM (staging.train_service ts
  JOIN staging.portion_consist pc USING (ds_code, train_service_key))

Ketika saya menanyakan tampilan ini dengan filter yang identik:

select * from staging.v_unit_coach_block
where assembly_key = '185132';

Ini adalah paket yang jelas:

QUERY PLAN
Subquery Scan on v_unit_coach_block  (cost=494217.13..508955.10     rows=3275 width=31)
Filter: (v_unit_coach_block.assembly_key = '185132'::text)
 ->  Unique  (cost=494217.13..500767.34 rows=655021 width=43)
    ->  Sort  (cost=494217.13..495854.68 rows=655021 width=43)
          Sort Key: ts.train_service_key, pc.assembly_key, (dense_rank() OVER (?))
          ->  WindowAgg  (cost=392772.16..410785.23 rows=655021 width=43)
                ->  Sort  (cost=392772.16..394409.71 rows=655021 width=35)
                      Sort Key: ts.train_service_key, pc.through_idx DESC, pc.first_portion, ((CASE WHEN (NOT ts.primary_direction) THEN '-1'::integer ELSE 1 END * pc.first_seq))
                      ->  Hash Join  (cost=89947.40..311580.26 rows=655021 width=35)
                            Hash Cond: ((pc.ds_code = ts.ds_code) AND (pc.train_service_key = ts.train_service_key))
                            ->  Seq Scan on portion_consist pc  (cost=0.00..39867.86 rows=782786 width=38)
                            ->  Hash  (cost=65935.36..65935.36 rows=1151136 width=21)
                                  ->  Seq Scan on train_service ts  (cost=0.00..65935.36 rows=1151136 width=21)

Ini melakukan pemindaian penuh pada kedua tabel dan membutuhkan 17 detik.

Sampai saya menemukan ini, saya telah bebas menggunakan pandangan dengan PostgreSQL (setelah memahami pandangan yang dipegang secara luas yang dinyatakan dalam jawaban yang diterima). Saya secara khusus akan menghindari menggunakan tampilan jika saya membutuhkan pemfilteran pra-agregat, untuk itu saya akan menggunakan fungsi set-return.

Saya juga menyadari bahwa CTE di PostgreSQL dievaluasi secara terpisah secara terpisah, berdasarkan desain, jadi saya tidak menggunakannya dengan cara yang sama dengan SQL Server, misalnya, di mana mereka tampaknya dioptimalkan sebagai subqueries.

Oleh karena itu, jawaban saya adalah, ada beberapa contoh di mana tampilan tidak berkinerja persis seperti kueri yang menjadi dasarnya, sehingga disarankan untuk berhati-hati. Saya menggunakan Amazon Aurora berdasarkan PostgreSQL 9.6.6.


2
Perhatikan peringatan di jawaban lain - " asalkan ini adalah pernyataan sederhana ".
RDFozz

Sebagai catatan, CASE WHEN (NOT ts.primary_direction) THEN '-1' :: INTEGER ELSE 1 ENDtidak perlu membuat kueri lebih lambat dari yang seharusnya Anda lebih baik menulis dua persyaratan lainnya dalam urutan.
Evan Carroll

@ EvanCarroll Saya berjuang dengan ini untuk sementara waktu. Baru saja menemukan itu sedikit lebih cepat untuk menarik KASUS keluar satu tingkat:CASE WHEN (NOT ts.primary_direction) THEN dense_rank() OVER (PARTITION BY ts.train_service_key ORDER BY pc.through_idx DESC, pc.first_portion ASC, pc.first_seq DESC) ELSE dense_rank() OVER (PARTITION BY ts.train_service_key ORDER BY pc.through_idx DESC, pc.first_portion ASC, pc.first_seq ASC) END AS coach_block_idx
enjayaitch

Itu bukan ide yang bagus juga .. Anda punya beberapa masalah di sini. Maksud saya yang utama adalah bahwa pandangan Anda tidak masuk akal dan melakukan hal-hal yang berbeda karena penggunaan Anda dense_rank()sehingga itu tidak benar-benar masalah kinerja.
Evan Carroll

1
@EvanCarroll komentar Anda mendorong saya untuk sampai di sana sendiri (maka jawaban saya yang diedit). Terima kasih.
enjayaitch

0

(Saya penggemar pandangan, tetapi Anda harus sangat berhati-hati dengan PG di sini dan saya ingin mendorong semua orang untuk menggunakan pandangan secara umum juga di PG untuk pemahaman yang lebih baik dan pemeliharaan kueri / kode)

Sebenarnya dan sayangnya (PERINGATAN :) menggunakan tampilan di Postgres menyebabkan masalah nyata dan sangat menurunkan kinerja kami tergantung pada fitur yang kami gunakan di dalamnya :-( (setidaknya dengan v10.1). (Ini tidak akan seperti yang lain sistem DB modern seperti Oracle.)

Jadi, mungkin (dan ini adalah pertanyaan saya) penyaringan apa pun terhadap tampilan ... menghasilkan eksekusi kueri tunggal terhadap tabel yang mendasarinya.

(Bergantung pada apa yang Anda maksudkan sebenarnya - tidak - tabel temp antara mungkin terwujud bahwa Anda mungkin tidak ingin berada atau di mana predikat tidak ditekan pada ...)

Saya tahu setidaknya dua "fitur" utama, yang mengecewakan kami di tengah migrasi dari Oracle ke Postgres sehingga kami harus mengabaikan PG dalam sebuah proyek:

  • CTEs ( with-clause subqueries / umum ekspresi tabel ) adalah (biasanya) berguna untuk penataan query yang lebih kompleks (bahkan dalam aplikasi yang lebih kecil), tetapi dalam PG adalah dengan desain diimplementasikan sebagai "tersembunyi" optimizer petunjuk (menghasilkan misalnya non-diindeks tabel temp) dan dengan demikian melanggar (bagi saya dan banyak orang lain yang penting) konsep deklaratif SQL ( Oracle docu ): misalnya

    • permintaan sederhana:

      explain
      
        select * from pg_indexes where indexname='pg_am_name_index'
      
      /* result: 
      
      Nested Loop Left Join  (cost=12.38..26.67 rows=1 width=260)
        ...
        ->  Bitmap Index Scan on pg_class_relname_nsp_index  (cost=0.00..4.29 rows=2 width=0)
                                               Index Cond: (relname = 'pg_am_name_index'::name)
        ...
      */
    • ditulis ulang menggunakan beberapa CTE:

      explain
      
        with 
      
        unfiltered as (
          select * from pg_indexes
        ) 
      
        select * from unfiltered where indexname='pg_am_name_index'
      
      /* result:
      
      CTE Scan on unfiltered  (cost=584.45..587.60 rows=1 width=288)
         Filter: (indexname = 'pg_am_name_index'::name)
         CTE unfiltered
           ->  Hash Left Join  (cost=230.08..584.45 rows=140 width=260)  
      ...
      */
    • sumber lebih lanjut dengan diskusi dll .: https://blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences/

  • fungsi jendela dengan- overpernyataan berpotensi tidak dapat digunakan (biasanya digunakan dalam tampilan, misalnya sebagai sumber untuk laporan berdasarkan permintaan yang lebih kompleks)


solusi kami untuk with-kausa

Kami akan mengubah semua "tampilan sebaris" menjadi tampilan nyata dengan awalan khusus sehingga tidak mengacaukan daftar / namespace tampilan dan dapat dengan mudah dikaitkan dengan "tampilan luar" asli:: - /


solusi kami untuk fungsi jendela

Kami berhasil mengimplementasikannya menggunakan database Oracle.


Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.