มุมมองเป็นอันตรายต่อประสิทธิภาพใน PostgreSQL หรือไม่


44

ต่อไปนี้เป็นข้อความที่ตัดตอนมาจากหนังสือเกี่ยวกับการออกแบบ db (การออกแบบฐานข้อมูลเริ่มต้น ISBN: 0-7645-7490-6):

อันตรายจากการใช้มุมมองคือการกรองคิวรีกับมุมมองโดยคาดว่าจะอ่านส่วนเล็ก ๆ ของตารางที่มีขนาดใหญ่มาก การกรองใด ๆ ที่ควรทำในมุมมองเพราะการกรองใด ๆ กับตัวเองถูกนำไปใช้หลังจากแบบสอบถามในมุมมองได้ดำเนินการเสร็จสมบูรณ์ โดยทั่วไปมุมมองมีประโยชน์สำหรับการเร่งกระบวนการพัฒนา แต่ในระยะยาวสามารถฆ่าประสิทธิภาพของฐานข้อมูลได้อย่างสมบูรณ์

ต่อไปนี้เป็นข้อความที่ตัดตอนมาจากเอกสารประกอบ PostgreSQL 9.5:

การใช้มุมมองแบบเสรีเป็นสิ่งสำคัญในการออกแบบฐานข้อมูล SQL ที่ดี มุมมองช่วยให้คุณสามารถแค็ปซูลรายละเอียดของโครงสร้างของตารางของคุณซึ่งอาจเปลี่ยนแปลงได้เมื่อแอปพลิเคชันของคุณพัฒนาขึ้นหลังอินเทอร์เฟซที่สอดคล้องกัน

แหล่งที่มาทั้งสองดูเหมือนจะขัดแย้งกัน ("ไม่ออกแบบด้วยมุมมอง" กับ "ทำออกแบบด้วยมุมมอง")

อย่างไรก็ตามในมุมมอง PG จะดำเนินการโดยใช้ระบบกฎ ดังนั้นอาจเป็นไปได้ (และนี่คือคำถามของฉัน) การกรองใด ๆ กับมุมมองจะถูกเขียนใหม่เป็นตัวกรองภายในมุมมองทำให้เกิดการเรียกใช้แบบสอบถามแบบครั้งเดียวเทียบกับตารางต้นแบบ

การตีความของฉันถูกต้องหรือไม่และ PG ได้รวมเอาส่วนคำสั่งเข้าและออกจากที่ไหน? หรือมันแยกกันทีละอัน? ตัวอย่างสั้น ๆ ที่มีอยู่ในตัวถูกต้อง (คอมไพล์ได้)?


ฉันคิดว่าคำถามไม่ถูกต้องเพราะทั้งสองแหล่งไม่ได้พูดถึงสิ่งเดียวกัน อันแรกเกี่ยวข้องกับการสืบค้นจากมุมมองและหลังจากใช้ตัวกรอง: SELECT * FROM my_view WHERE my_column = 'blablabla';ในขณะที่ตัวที่สองกำลังใช้มุมมองเพื่อทำให้ตัวแบบข้อมูลของคุณโปร่งใสกับแอปพลิเคชันที่ใช้ แหล่งที่มาแรกชี้ให้คุณรวมตัวกรองWHERE my_column = 'blablabla'ในคำจำกัดความของมุมมองเนื่องจากจะทำให้แผนการดำเนินการดีขึ้น
EAmez

คำตอบ:


48

หนังสือเล่มนี้ผิด

เลือกจากมุมมองเป็นว่าเร็วหรือช้าเรียกใช้คำสั่ง SQL พื้นฐาน - explain analyzeคุณสามารถตรวจสอบว่าการใช้

เครื่องมือเพิ่มประสิทธิภาพ Postgres (และเครื่องมือเพิ่มประสิทธิภาพสำหรับ DBMS ที่ทันสมัยอื่น ๆ ) จะสามารถกดภาคแสดงในมุมมองลงในคำสั่งมุมมองที่เกิดขึ้นจริง - ให้นี่เป็นคำสั่งง่ายๆ (อีกครั้งนี้สามารถตรวจสอบได้โดยใช้explain analyze)

"ชื่อเสียงที่ไม่ดี" เกี่ยวกับประสิทธิภาพที่เกิดจากการที่คุณใช้มุมมองมากเกินไปและเริ่มสร้างมุมมองที่ใช้มุมมองที่ใช้มุมมอง บ่อยครั้งที่ผลลัพธ์ในข้อความที่ทำมากเกินไปเมื่อเทียบกับข้อความที่ปรับแต่งแบบมือโดยไม่มีมุมมองเช่นเนื่องจากบางตารางกลางไม่จำเป็น ในเกือบทุกกรณีเครื่องมือเพิ่มประสิทธิภาพไม่ฉลาดพอที่จะลบตาราง / ตัวเชื่อมที่ไม่จำเป็นออกหรือเพื่อเลื่อนภาคแสดงลงในหลาย ๆ ระดับของมุมมอง (นี่เป็นเรื่องจริงสำหรับ DBMS อื่น ๆ เช่นกัน)


3
ได้รับบางส่วนของเคาน์เตอร์คำตอบที่เสนอคุณอาจต้องการที่จะอธิบายนิดในสิ่งที่เป็นคำสั่งง่ายๆ
RDFozz

คุณสามารถอธิบายวิธีใช้explain analyzeคำสั่งได้หรือไม่
ดัสตินยะส์

@DustinMichels: ดูคู่มือ: postgresql.org/docs/current/using-explain.html
a_horse_with_no_name

19

ในการให้ตัวอย่างสิ่งที่@a_horse อธิบายให้คุณ:

Postgres ใช้ schema ของข้อมูลซึ่งประกอบด้วยมุมมอง (บางครั้งซับซ้อน) ให้ข้อมูลเกี่ยวกับวัตถุ DB ในรูปแบบมาตรฐาน สะดวกและเชื่อถือได้ - และอาจมีราคาแพงกว่าการเข้าถึงตารางแค็ตตาล็อก Postgres โดยตรง

ตัวอย่างที่ง่ายมากในการรับคอลัมน์ที่มองเห็นได้ทั้งหมดของตาราง
... จาก schema ของข้อมูล:

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

... จากแคตตาล็อกระบบ:

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

EXPLAIN ANALYZEเปรียบเทียบแบบสอบถามแผนและการดำเนินการเวลาสำหรับทั้งที่มี

  • แบบสอบถามแรกขึ้นอยู่กับมุมมองinformation_schema.columnsซึ่งเชื่อมต่อกับหลาย ๆ ตารางที่เราไม่จำเป็นต้องทำ

  • การสืบค้นที่สองจะสแกนเพียงหนึ่งตารางpg_catalog.pg_attributeเท่านั้น (แต่แบบสอบถามแรกยังต้องการเพียงไม่กี่มิลลิวินาทีในฐานข้อมูลทั่วไป)

รายละเอียด:


7

แก้ไข:

ด้วยคำขอโทษฉันต้องถอนการยืนยันของฉันว่าคำตอบที่ยอมรับนั้นไม่ถูกต้องเสมอไป - ระบุว่ามุมมองนั้นเหมือนกันเสมอกับสิ่งเดียวกันที่เขียนเป็นแบบสอบถามย่อย ฉันคิดว่ามันเถียงไม่ได้และฉันคิดว่าตอนนี้ฉันรู้ว่าสิ่งที่เกิดขึ้นในกรณีของฉัน

ตอนนี้ฉันก็คิดว่ามีคำตอบที่ดีกว่าสำหรับคำถามเดิม

คำถามเดิมเกี่ยวกับว่ามันควรจะเป็นแนวทางปฏิบัติที่จะใช้มุมมอง (ตรงข้ามกับตัวอย่างเช่นการทำซ้ำ SQL ในการปฏิบัติที่อาจต้องได้รับการบำรุงรักษาสองครั้งหรือมากกว่า)

คำตอบของฉันจะ "ไม่ถ้าแบบสอบถามของคุณใช้ฟังก์ชั่นหน้าต่างหรือสิ่งอื่นใดที่ทำให้เครื่องมือเพิ่มประสิทธิภาพในการรักษาแบบสอบถามที่แตกต่างกันเมื่อมันกลายเป็นแบบสอบถามย่อยเพราะการกระทำมากของการสร้างแบบสอบถามย่อย (ไม่ว่าจะแสดงเป็นมุมมองหรือไม่) อาจลดประสิทธิภาพ หากคุณกำลังกรองด้วยพารามิเตอร์ที่รันไทม์

ความซับซ้อนของฟังก์ชั่นหน้าต่างของฉันไม่จำเป็น แผนอธิบายสำหรับสิ่งนี้:

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';

มีค่าใช้จ่ายน้อยกว่านี้มาก

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';

หวังว่าจะมีความเฉพาะเจาะจงและเป็นประโยชน์มากกว่านี้อีกเล็กน้อย

จากประสบการณ์ล่าสุดของฉัน (ทำให้ฉันพบคำถามนี้) คำตอบที่ยอมรับด้านบนไม่ถูกต้องในทุกรอบ ฉันมีคำถามง่ายๆที่มีฟังก์ชั่นหน้าต่าง:

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))

ถ้าฉันเพิ่มตัวกรองนี้:

where assembly_key = '185132'

แผนการอธิบายที่ฉันได้รับมีดังนี้:

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))

นี่คือการใช้ดัชนีคีย์หลักในตารางบริการรถไฟและดัชนีที่ไม่ซ้ำกันในตารางส่วนที่ มันดำเนินการใน 90ms

ฉันสร้างมุมมอง (วางที่นี่เพื่อให้ชัดเจนอย่างแน่นอน แต่มันเป็นคำถามในมุมมอง):

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))

เมื่อฉันสอบถามมุมมองนี้ด้วยตัวกรองที่เหมือนกัน:

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

นี่คือแผนอธิบาย:

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)

นี่เป็นการสแกนแบบเต็มรูปแบบบนทั้งสองตารางและใช้เวลา 17 วินาที

จนกว่าฉันจะเจอสิ่งนี้ฉันใช้มุมมองกับ PostgreSQL อย่างอิสระ (ต้องเข้าใจมุมมองที่จัดขึ้นอย่างกว้างขวางที่แสดงในคำตอบที่ยอมรับ) ฉันจะหลีกเลี่ยงการใช้มุมมองเป็นพิเศษหากฉันต้องการการกรองแบบรวมล่วงหน้าซึ่งฉันจะใช้ฟังก์ชั่นการคืนค่า

ฉันยังทราบด้วยว่า CTEs ใน PostgreSQL ได้รับการประเมินแยกต่างหากโดยการออกแบบดังนั้นฉันจึงไม่ใช้มันในวิธีเดียวกับที่ฉันใช้กับ SQL Server ตัวอย่างเช่นที่ซึ่งพวกเขาดูเหมือนจะได้รับการปรับให้เหมาะสมที่สุดในแบบสอบถามย่อย

ดังนั้นคำตอบของฉันคือมีบางกรณีที่มุมมองไม่ทำงานตรงตามคำถามที่พวกเขาใช้ดังนั้นขอแนะนำให้ใช้ความระมัดระวัง ฉันใช้ Amazon Aurora ตาม PostgreSQL 9.6.6


2
หมายเหตุข้อแม้ในคำตอบอื่น ๆ - " ให้นี่เป็นคำสั่งง่าย ๆ "
RDFozz

ในฐานะที่เป็นบันทึกย่อด้านข้างโดยCASE WHEN (NOT ts.primary_direction) THEN '-1' :: INTEGER ELSE 1 ENDไม่จำเป็นจะทำให้คิวรีช้าลงกว่าที่คุณจะดีกว่าถ้าคุณเขียนเงื่อนไขเพิ่มเติมอีกสองลำดับ
Evan Carroll

@EvanCarroll ฉันต่อสู้กับสิ่งนี้มาระยะหนึ่งแล้ว เพิ่งพบว่าเร็วกว่าเล็กน้อยเพื่อดึง CASE ออกหนึ่งระดับ: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

ไม่ใช่ความคิดที่ดีเช่นกัน .. คุณมีปัญหาเล็กน้อยที่นี่ ฉันหมายถึงสิ่งที่ยิ่งใหญ่คือมุมมองของคุณไม่สมเหตุสมผลและมันก็ทำสิ่งต่าง ๆ เพราะคุณใช้dense_rank()ดังนั้นมันจึงไม่ใช่ปัญหาด้านประสิทธิภาพ
Evan Carroll

1
@EvanCarroll ความคิดเห็นของคุณแจ้งให้ฉันไปถึงที่นั่นด้วยตัวเอง (ดังนั้นคำตอบที่แก้ไขของฉัน) ขอขอบคุณ.
enjayaitch

-1

(ฉันเป็นแฟนตัวยงของการดู แต่คุณต้องระวังอย่างมากกับ PG ที่นี่และฉันอยากจะสนับสนุนให้ทุกคนใช้มุมมองโดยทั่วไปใน PGเพื่อความเข้าใจและการบำรุงรักษาของแบบสอบถาม / รหัสที่ดีขึ้น)

ที่จริงและน่าเศร้า (คำเตือน :) การใช้มุมมองใน Postgres ทำให้เราเกิดปัญหาจริงและทำให้ประสิทธิภาพการทำงานลดลงขึ้นอยู่กับคุณลักษณะที่เราใช้ภายใน :-( (อย่างน้อยกับ v10.1) (สิ่งนี้จะไม่เหมือนกับที่อื่น ๆ ระบบฐานข้อมูลที่ทันสมัยเช่น Oracle)

ดังนั้นอาจเป็นไปได้ (และนี่คือคำถามของฉัน) การกรองใด ๆ กับมุมมอง ... ส่งผลให้เกิดการดำเนินการค้นหาแบบเดี่ยวต่อตารางต้นแบบ

(ขึ้นอยู่กับสิ่งที่คุณหมายถึง - ไม่มี - ตารางชั่วคราวกลางอาจปรากฏให้เห็นว่าคุณอาจไม่ต้องการที่จะเป็นหรือในกรณีที่ไม่มีการกดลงบน ...

ฉันรู้ "คุณสมบัติ" ที่สำคัญอย่างน้อยสองอย่างซึ่งทำให้เราอยู่ในระหว่างการย้ายข้อมูลจาก Oracle ไปยัง Postgresดังนั้นเราจึงต้องละทิ้ง PG ในโครงการ:

  • CTEs (- withแบบสอบถามย่อย / นิพจน์ตารางทั่วไป ) มี (โดยปกติ) มีประโยชน์สำหรับการสร้างเคียวรีที่ซับซ้อนมากขึ้น (แม้ในแอปพลิเคชันขนาดเล็ก) แต่ใน PG ได้รับการออกแบบโดยใช้เป็นคำแนะนำเครื่องมือเพิ่มประสิทธิภาพ "ซ่อน" (การสร้างตารางชั่วคราวเช่น ดังนั้นจึงเป็นการละเมิดแนวคิด (สำหรับฉันและผู้อื่นที่สำคัญ) ของการประกาศ SQL ( Oracle docu ): เช่น

    • แบบสอบถามง่ายๆ:

      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)
        ...
      */
    • เขียนใหม่โดยใช้ 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)  
      ...
      */
    • แหล่งข้อมูลเพิ่มเติมที่มีการสนทนา ฯลฯ : https://blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences/

  • ฟังก์ชั่นหน้าต่างกับover-statements กำลังอาจใช้ไม่ได้ (โดยปกติจะใช้ในมุมมองเช่นเป็นแหล่งสำหรับรายงานขึ้นอยู่กับคำสั่งที่ซับซ้อนมากขึ้น)


วิธีแก้ปัญหาของเราสำหรับwith-clauses

เราจะแปลง "มุมมองแบบอินไลน์" ทั้งหมดให้เป็นมุมมองจริงด้วยคำนำหน้าพิเศษเพื่อไม่ให้สับสนกับรายการ / เนมสเปซของมุมมองและสามารถเชื่อมโยงกับ "มุมมองด้านนอก" ดั้งเดิมได้อย่างง่ายดาย: - /


โซลูชันของเราสำหรับฟังก์ชั่นหน้าต่าง

เราติดตั้งสำเร็จแล้วโดยใช้ฐานข้อมูล Oracle


1
พูดคุยเรื่องย้ายไปแชท
ypercubeᵀᴹ
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.