Вредны ли представления для производительности в PostgreSQL?

45

Ниже приведен отрывок из книги о дизайне БД (Начальный номер базы данных ISBN: 0-7645-7490-6):

Опасность использования представлений заключается в фильтрации запроса к представлению, ожидающего чтения очень маленькой части очень большой таблицы. Любая фильтрация должна выполняться в представлении, поскольку любая фильтрация для самого представления применяется после того, как запрос в представлении завершил выполнение. Представления обычно полезны для ускорения процесса разработки, но в долгосрочной перспективе могут полностью снизить производительность базы данных.

Ниже приводится выдержка из документации PostgreSQL 9.5:

Либеральное использование представлений является ключевым аспектом хорошего дизайна базы данных SQL. Представления позволяют инкапсулировать детали структуры ваших таблиц, которые могут меняться по мере развития вашего приложения, за согласованными интерфейсами.

Два источника, кажется, противоречат друг другу («не проектируйте с помощью представлений» и «не проектируйте с помощью представлений»).

Однако в PG представления реализованы с использованием системы правил. Таким образом, возможно (и это мой вопрос) любая фильтрация по представлению переписывается как фильтр внутри представления, что приводит к одному выполнению запроса к базовым таблицам.

Верна ли моя интерпретация, и PG комбинирует предложения WHERE в поле зрения и из поля зрения? Или он запускает их отдельно, один за другим? Какие-нибудь короткие, самодостаточные, правильные (компилируемые) примеры?

ARX
источник
Я думаю, что вопрос не правильный, потому что оба источника не говорят об одном и том же. Первый связан с запросом из представления, и ПОСЛЕ применения фильтра SELECT * FROM my_view WHERE my_column = 'blablabla';:. Второй - об использовании представлений, чтобы сделать вашу модель данных прозрачной для приложения, которое ее использует. Первые источники указывают, что вы должны включить фильтр WHERE my_column = 'blablabla'в определение представления, так как это приведет к лучшему плану выполнения.
EAmez

Ответы:

51

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

Выбор в представлении происходит так же быстро или медленно, как и выполнение основного оператора SQL - вы можете легко проверить это, используя explain analyze.

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

«Плохая репутация» в отношении производительности возникает, я думаю, из-за того, что вы злоупотребляете представлениями и начинаете создавать представления, использующие представления, использующие представления. Очень часто это приводит к операторам, которые делают слишком много по сравнению с оператором, который был составлен вручную без представлений, например, потому что некоторые промежуточные таблицы не понадобятся. Практически во всех случаях оптимизатор недостаточно умен, чтобы удалять эти ненужные таблицы / объединения или опускать предикаты для нескольких уровней представлений (это верно и для других СУБД).

a_horse_with_no_name
источник
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))

При этом используется индекс первичного ключа в таблице обслуживания поездов и неуникальный индекс в таблице part_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.

enjayaitch
источник
2
Обратите внимание на предостережение в другом ответе - « при условии, что это простое утверждение ».
RDFozz
Напомним, CASE WHEN (NOT ts.primary_direction) THEN '-1' :: INTEGER ELSE 1 ENDчто излишне делать запрос медленнее, чем нужно, если вам лучше написать еще два условия в порядке.
Эван Кэрролл
@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()так что это не проблема производительности.
Эван Кэрролл
1
@EvanCarroll ваш комментарий побудил меня туда попасть (отсюда и мой отредактированный ответ). Спасибо.
Enjayaitch
0

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

На самом деле и к сожалению (ПРЕДУПРЕЖДЕНИЕ :) использование представлений в Postgres вызвало у нас реальные проблемы и сильно снизило нашу производительность в зависимости от функций, которые мы использовали внутри него :-( (по крайней мере, с v10.1). (Это было бы не так с другими современные системы баз данных, такие как Oracle.)

Таким образом, возможно (и это мой вопрос) любая фильтрация по представлению ... приводящая к выполнению одного запроса к базовым таблицам.

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

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

  • CTE ( withподзапросы -clause / общие табличные выражения ) (обычно) полезны для структурирования более сложных запросов (даже в небольших приложениях), но в PG они спроектированы как «скрытые» подсказки оптимизатора (генерирующие, например, неиндексированные временные таблицы) и таким образом нарушают (для меня и многих других важных) концепцию декларативного SQL ( документ Oracle ): например,

    • простой запрос:

      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клаузул

Мы преобразуем все «встроенные представления» в реальные представления со специальным префиксом, чтобы они не мешали списку / пространству имен представлений и могли быть легко связаны с исходным «внешним видом»: - /


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

Мы успешно реализовали его, используя базу данных Oracle.

Андреас Дитрих
источник