Чи шкідливі представлення для продуктивності в PostgreSQL?


45

Далі - уривок із книги про дизайн дизайну 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

Відповіді:


49

Книга неправильна.

Вибір із представлення виглядає так само швидко або повільно, як і запуск базового оператора SQL - ви можете легко перевірити це за допомогою explain analyze.

Оптимізатор Postgres (і оптимізатор для багатьох інших сучасних СУБД) зможе витіснити предикати на представлення даних у фактичний вигляд перегляду, за умови, що це просте твердження (знову це можна перевірити за допомогою explain analyze).

Думаю, що "погана репутація" щодо продуктивності пов'язана з тим, коли ви надто зловживаєте поданнями та починаєте створювати представлення, що використовують представлення, що використовують представлення. Дуже часто це призводить до тверджень, які роблять занадто багато в порівнянні з твердженням, яке було розроблено вручну без думок, наприклад, тому що деякі проміжні таблиці не знадобляться. Майже у всіх випадках оптимізатор недостатньо розумний для видалення цих непотрібних таблиць / об'єднань або для витіснення предикатів на декілька рівнів переглядів (це справедливо і для інших СУБД).


3
Враховуючи деякі запропоновані зустрічні відповіді, ви можете трохи пояснити, що таке просте твердження .
RDFozz

Чи можете ви пояснити, як використовувати explain analyzeоператор?
Дастін Міхельс

@DustinMichels: подивіться посібник: postgresql.org/docs/current/using-explain.html
a_horse_with_no_name

19

Щоб навести приклад того, що пояснив @a_horse :

Postgres реалізує інформаційну схему, яка складається з (іноді складних) поглядів, що надають інформацію про об'єкти БД у стандартизованому вигляді. Це зручно і надійно - і може бути значно дорожчим, ніж безпосередньо звертатися до таблиць каталогу Postgres.

Дуже простий приклад, щоб отримати всі видимі стовпці таблиці
... з інформаційної схеми:

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

Для цього використовується індекс первинного ключа в таблиці служб поїздів і не унікальний індекс таблиці por_consist. Він виконується за 90мс.

Я створив подання (вставивши його тут, щоб він був абсолютно зрозумілим, але це буквально запит у представленні):

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 (розуміючи широко поширені погляди, виражені у прийнятій відповіді). Я спеціально уникаю використання представлень, якщо мені потрібна попередня сукупність фільтрації, для якої я б використовував функції повернення набору.

Я також знаю, що CTE в PostgreSQL суворо оцінюються окремо, тому я не використовую їх так само, як це було б, наприклад, із SQL Server, де вони, схоже, оптимізовані як підзапити.

Моя відповідь, отже, є випадки, коли погляди виконують не так, як запит, на якому вони ґрунтуються, тому рекомендується обережність. Я використовую Amazon Aurora на основі PostgreSQL 9.6.6.


2
Зверніть увагу на застереження в іншій відповіді - "за умови, що це просте твердження ".
RDFozz

Як бічна примітка, CASE WHEN (NOT ts.primary_direction) THEN '-1' :: INTEGER ELSE 1 ENDзайве робити запит повільніше, ніж потрібно, вам краще написати ще два умови в порядку.
Еван Керролл

@EvanCarroll Я деякий час боровся з цим. Щойно виявив, що вивести 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
КАРТЕ на деякий

Це теж не гарна ідея .. у вас тут є кілька проблем. Я маю на увазі, головне - це те, що ваш погляд не має сенсу, і він робить різні речі через ваше використання, dense_rank()тому це насправді не є проблемою продуктивності.
Еван Керролл

1
@EvanCarroll ваш коментар спонукав мене сам туди потрапити (звідси моя відредагована відповідь). Дякую.
enjayaitch

0

(Я величезна прихильниця поглядів, але ви повинні бути дуже обережними з PG тут, і я хотів би заохотити всіх використовувати погляди, як правило, також в PG для кращої зрозумілості та ремонтопридатності запитів / коду)

Власне і сумно (ПОПЕРЕДЖЕННЯ :) використання поглядів у Postgres спричинило нам справжні проблеми та погано знизило нашу ефективність залежно від функцій, якими ми користувалися всередині нього :-( (принаймні, з v10.1). (Це було б не так, як з іншими сучасні системи БД, такі як Oracle.)

Таким чином, можливо (і це моє запитання) будь-яке фільтрування проти подання ... в результаті чого виконується один запит проти базових таблиць.

(Залежно від того, що ви точно маєте на увазі - ні - можуть бути матеріалізовані проміжні таблиці темпів, якими ви, можливо, не хочете бути, або де предикати не натиснуті на ...)

Я знаю щонайменше дві основні "особливості", які підвели нас під час міграції з Oracle до Postgres, тому нам довелося відмовитися від PG в проекті:

  • CTE ( -запити -загадки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-виконаннями потенційно непридатні (зазвичай використовуються в представленнях, наприклад, як джерело для звітів на основі більш складних запитів)


наше вирішення для with-клауз

Ми перетворимо всі "вбудовані погляди" в реальні погляди зі спеціальним префіксом, щоб вони не псували список / простір імен поглядів і легко могли бути пов'язані з початковим "зовнішнім видом": - /


наше рішення для віконних функцій

Ми успішно реалізували його за допомогою бази даних Oracle.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.