PostgreSQL 9.3 Что нового?


    Здравствуйте, хабрачеловеки! Не так уж давно вышел релиз PostgreSQL 9.3 и я хотел бы ознакомить Вас с наиболее важными новшествами, касающимися клиентской части, которые, возможно, пригодятся Вам. В этой статье рассмотрено следующее:
    • материализированные представления
    • обновляемые представления
    • триггеры к событиям
    • рекурсивные представления
    • латеральное присоединение
    • изменяемые внешние таблицы
    • функции и операторы для работы с типом JSON


    Материализированные представления



    Материализированное представление — физический объект базы данных, содержащий в себе результаты некоторого запроса. Бесспорно, одно из самых ожидаемых новшеств. Посмотрим, каким образом работать с ним в PostgreSQL.

    Создадим справочник авторов и справочник книг, имеющий ссылку на автора:
    CREATE TABLE author
    (
    	id serial NOT NULL,
    	first_name text NOT NULL,
    	last_name text NOT NULL,
    
    	CONSTRAINT pk_author_id PRIMARY KEY ( id ),
    	CONSTRAINT uk_author_name UNIQUE ( first_name, last_name )
    );
    
    CREATE TABLE book
    (
    	id serial NOT NULL,
    	author_id integer NOT NULL,
    	name text NOT NULL,
    
    	CONSTRAINT pk_book_id PRIMARY KEY ( id ),
    	CONSTRAINT fk_book_author_id FOREIGN KEY ( author_id ) REFERENCES author ( id ),
    	CONSTRAINT uk_book_name UNIQUE ( author_id, name )
    );
    


    Наполним эти таблицы данными — добавим пару авторов и сгенерируем много-много книг:
    INSERT INTO author ( first_name, last_name ) VALUES ( 'Иван',  'Иванов' ); -- сгенерирован id = 1
    INSERT INTO author ( first_name, last_name ) VALUES ( 'Пётр', 'Петров' ); -- сгенерирован id = 2
    
    INSERT INTO book ( author_id, name ) VALUES
    ( 1, 'Трактат о пустоте (часть ' || generate_series ( 1, 100000 ) || ')' );
    
    INSERT INTO book ( author_id, name ) VALUES
    ( 2, 'Невыносимость бытия' ),
    ( 2, 'Счастливый финал' );
    


    Для сравнения, создадим обычное и материализированное представление (обратите внимание, что для создания последнего требуется немного больше времени, для выборки и записи результата):
    CREATE VIEW vw_book AS
    SELECT book.id, author.first_name || ' ' || author.last_name AS author_name, book.name
    FROM book
    INNER JOIN author ON author.id = book.author_id;
    
    CREATE MATERIALIZED VIEW mvw_book AS
    SELECT book.id, author.first_name || ' ' || author.last_name AS author_name, book.name
    FROM book
    INNER JOIN author ON author.id = book.author_id;
    


    Теперь, давайте взглянем на план запроса с условием для обычного и материализированного представления:
    EXPLAIN ANALYZE SELECT * FROM vw_book WHERE author_name = 'Пётр Петров';
    --
    Hash Join  (cost=24.58..2543.83 rows=482 width=119) (actual time=19.389..19.390 rows=2 loops=1)
      Hash Cond: (book.author_id = author.id)
      ->  Seq Scan on book  (cost=0.00..2137.02 rows=100002 width=59) (actual time=0.017..9.231 rows=100002 loops=1)
      ->  Hash  (cost=24.53..24.53 rows=4 width=68) (actual time=0.026..0.026 rows=1 loops=1)
            Buckets: 1024  Batches: 1  Memory Usage: 1kB
            ->  Seq Scan on author  (cost=0.00..24.53 rows=4 width=68) (actual time=0.019..0.020 rows=1 loops=1)
                  Filter: (((first_name || ' '::text) || last_name) = 'Пётр Петров'::text)
                  Rows Removed by Filter: 1
    Total runtime: 19.452 ms
    
    EXPLAIN ANALYZE SELECT * FROM mvw_book WHERE author_name = 'Пётр Петров';
    --
    Seq Scan on mvw_book  (cost=0.00..2584.03 rows=7 width=77) (actual time=15.869..15.870 rows=2 loops=1)
      Filter: (author_name = 'Пётр Петров'::text)
      Rows Removed by Filter: 100000
    Total runtime: 15.905 ms
    


    Данные для материализированного представления лежат кучно и их не приходится собирать из разных таблиц. Но это еще не всё, так как для них есть возможность создавать индексы. Улучшаем результат:
    CREATE INDEX idx_book_name ON mvw_book ( author_name );
    
    EXPLAIN ANALYZE SELECT * FROM mvw_book WHERE author_name = 'Пётр Петров';
    --
    Index Scan using idx_book_name on mvw_book  (cost=0.42..8.54 rows=7 width=77) (actual time=0.051..0.055 rows=2 loops=1)
      Index Cond: (author_name = 'Пётр Петров'::text)
    Total runtime: 0.099 ms
    


    Неплохо, поиск ведётся по индексу и время поиска существенно сократилось.

    Но есть и нюанс при использовании материализированных представлений — после DML операций над таблицами, из которых состоит представление, представление приходится обновлять:
    INSERT INTO book ( author_id, name ) VALUES
    ( 2, 'Потерянный во мгле' );
    
    REFRESH MATERIALIZED VIEW mvw_book;
    


    Это можно автоматизировать триггером:
    CREATE OR REPLACE FUNCTION mvw_book_refresh ( )
    RETURNS trigger AS
    $BODY$
    BEGIN
    	REFRESH MATERIALIZED VIEW mvw_book;
    	RETURN NULL;
    END
    $BODY$
    LANGUAGE plpgsql VOLATILE;
    
    CREATE TRIGGER tr_book_refresh AFTER INSERT OR UPDATE OR DELETE
    ON book FOR EACH STATEMENT EXECUTE PROCEDURE mvw_book_refresh ( );
    
    CREATE TRIGGER tr_author_refresh AFTER INSERT OR UPDATE OR DELETE
    ON author FOR EACH STATEMENT EXECUTE PROCEDURE mvw_book_refresh ( );
    


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

    Обновляемые представления



    К обновляемым представлениями можно применять DML-операции. Правда вот требования к таким представлениям высокие: только одна сущность (таблица, представление) в списке FROM, без операторов WITH, DISTINCT, GROUP BY, HAVING, LIMIT и OFFSET, без операций над множествами (UNION, INTERSECT и EXCEPT) и к полям не должны применятся никакие функции или операции.

    Обновляемые представления в действии:
    CREATE TABLE employee
    (
    	id serial NOT NULL,
    	fullname text NOT NULL,
    	birthday date,
    	salary numeric NOT NULL DEFAULT 0.0,
    
    	CONSTRAINT pk_employee_id PRIMARY KEY ( id ),
    	CONSTRAINT uk_employee_fullname UNIQUE ( fullname ),
    	CONSTRAINT ch_employee_salary CHECK ( salary >= 0.0 )
    );
    
    INSERT INTO employee ( fullname, salary ) VALUES ( 'Иван Иванов', 800.0 );
    INSERT INTO employee ( fullname, salary ) VALUES ( 'Пётр Петров', 2000.0 );
    INSERT INTO employee ( fullname, salary ) VALUES ( 'Неизвестный', 1500.0 );
    
    CREATE VIEW vw_employee_top_salary AS
    SELECT employee.fullname AS name, employee.salary
    FROM employee
    WHERE employee.salary >= 1000.0;
    
    -- работаем с представлением
    INSERT INTO vw_employee_top_salary ( name, salary ) VALUES ( 'Сёмён Сидоров', 2500.0 );
    UPDATE vw_employee_top_salary SET salary = 2200.0 WHERE name = 'Пётр Петров';
    DELETE FROM vw_employee_top_salary WHERE name = 'Неизвестный';
    
    -- вывод результатов
    SELECT * FROM vw_employee_top_salary;
    


    Обратите внимание, что INSERT в представление можно сделать в любом случае, а UPDATE и DELETE — только, когда набор из базовой таблицы попадает попадает под условие в представлении:
    INSERT INTO vw_employee_top_salary ( name, salary ) VALUES ( 'Анонимус', 0.0 ); -- добавится строка
    UPDATE vw_employee_top_salary SET salary = 3000.0 WHERE name = 'Анонимус'; -- ничего не изменится, так как salary равно 0.0
    DELETE FROM vw_employee_top_salary WHERE name = 'Анонимус'; -- ничего не изменится, так как salary равно 0.0
    


    Более продвинутые вещи можно делать, используя правила к представлениям.

    Триггеры к событиям



    Тоже, довольно ожидаемое нововведение. Позволяют перехватывать DDL команды в БД. Отличаются от обычных триггеров в первую очередь тем, что они глобальные, без привязки к конкретной таблице, но можно указать, на какие команды реагировать.

    Создаются так:
    CREATE OR REPLACE FUNCTION event_trigger_begin ( )
    RETURNS event_trigger AS
    $BODY$
    BEGIN
    	RAISE NOTICE '(begin) tg_event = %, tg_tag = %', TG_EVENT, TG_TAG;
    END;
    $BODY$
    LANGUAGE plpgsql;
    
    CREATE OR REPLACE FUNCTION event_trigger_end ( )
    RETURNS event_trigger AS
    $BODY$
    BEGIN
    	RAISE NOTICE '(end) tg_event = %, tg_tag = %', TG_EVENT, TG_TAG;
    END;
    $BODY$
    LANGUAGE plpgsql;
    
    CREATE EVENT TRIGGER tr_event_begin ON ddl_command_start EXECUTE PROCEDURE event_trigger_begin ( );
    CREATE EVENT TRIGGER tr_event_end ON ddl_command_end EXECUTE PROCEDURE event_trigger_end ( );
    


    Проводим разные DDL-манипуляции с таблицей:
    CREATE TABLE article
    (
    	id SERIAL NOT NULL,
    	name text NOT NULL,
    
    	CONSTRAINT pk_article_id PRIMARY KEY ( id ),
    	CONSTRAINT uk_article_name UNIQUE ( name )
    );
    
    ALTER TABLE article ADD COLUMN misc numeric;
    ALTER TABLE article ALTER COLUMN misc TYPE text;
    ALTER TABLE article DROP COLUMN misc;
    
    DROP TABLE article;
    


    Вывод должен быть таким:
    tg_event = ddl_command_start, tg_tag = CREATE TABLE
    tg_event = ddl_command_end, tg_tag = CREATE TABLE
    tg_event = ddl_command_start, tg_tag = ALTER TABLE
    tg_event = ddl_command_end, tg_tag = ALTER TABLE
    tg_event = ddl_command_start, tg_tag = ALTER TABLE
    tg_event = ddl_command_end, tg_tag = ALTER TABLE
    tg_event = ddl_command_start, tg_tag = ALTER TABLE
    tg_event = ddl_command_end, tg_tag = ALTER TABLE
    tg_event = ddl_command_start, tg_tag = DROP TABLE
    tg_event = ddl_command_end, tg_tag = DROP TABLE
    


    Через plpgsql доступна только информация о событии (TG_EVENT) и, собственно, о команде (TG_TAG), но, надеюсь, в будущем будет лучше.

    Рекурсивные представления



    Позволяют упростить конструкцию WITH RECURSIVE, если необходимо построить по ней представление.

    Создадим таблицу и наполним ее тестовыми данными:
    CREATE TABLE directory
    (
    	id serial NOT NULL,
    	parent_id integer,
    	name text NOT NULL,
    
    	CONSTRAINT pk_directory_id PRIMARY KEY ( id ),
    	CONSTRAINT fk_directory_parent_id FOREIGN KEY ( parent_id ) REFERENCES directory ( id ),
    	CONSTRAINT uk_directory_name UNIQUE ( parent_id, name )
    );
    
    INSERT INTO directory ( parent_id, name ) VALUES ( NULL, 'usr' ); -- сгенерирован id = 1
    INSERT INTO directory ( parent_id, name ) VALUES ( 1, 'lib' );
    INSERT INTO directory ( parent_id, name ) VALUES ( 1, 'include' );
    INSERT INTO directory ( parent_id, name ) VALUES ( NULL, 'var' ); -- сгенерирован id = 4
    INSERT INTO directory ( parent_id, name ) VALUES ( 4, 'opt' ); -- сгенерирован id = 5
    INSERT INTO directory ( parent_id, name ) VALUES ( 5, 'tmp' );
    INSERT INTO directory ( parent_id, name ) VALUES ( 4, 'log' ); -- сгенерирован id = 7
    INSERT INTO directory ( parent_id, name ) VALUES ( 7, 'samba' ); 
    INSERT INTO directory ( parent_id, name ) VALUES ( 7, 'news' );
    


    Запрос через WITH RECURSIVE и аналогичный ему, через рекурсивное представление:
    WITH RECURSIVE vw_directory ( id, parent_id, name, path ) AS
    (
    	SELECT id, parent_id, name, '/' || name
    	FROM directory
    	WHERE parent_id IS NULL AND name = 'var'
    	UNION ALL
    	SELECT d.id, d.parent_id, d.name, t.path || '/' || d.name
    	FROM directory d
    	INNER JOIN vw_directory t ON d.parent_id = t.id
    )
    SELECT * FROM vw_directory ORDER BY path;
    
    CREATE RECURSIVE VIEW vw_directory ( id, parent_id, name, path ) AS
    SELECT id, parent_id, name, '/' || name
    FROM directory
    WHERE parent_id IS NULL AND name = 'var'
    UNION ALL
    SELECT d.id, d.parent_id, d.name, t.path || '/' || d.name
    FROM directory d
    INNER JOIN vw_directory t ON d.parent_id = t.id;
    
    SELECT * FROM vw_directory ORDER BY path;
    


    На самом деле, рекурсивное представление — это обёртка над WITH RECURSIVE, в чем можно убедится, просмотрев текст сфорированного представления:
    CREATE OR REPLACE VIEW vw_directory AS
     WITH RECURSIVE vw_directory(id, parent_id, name, path) AS (
                     SELECT directory.id,
                        directory.parent_id,
                        directory.name,
                        '/'::text || directory.name
                       FROM directory
                      WHERE directory.parent_id IS NULL AND directory.name = 'var'::text
            UNION ALL
                     SELECT d.id,
                        d.parent_id,
                        d.name,
                        (t.path || '/'::text) || d.name
                       FROM directory d
                  JOIN vw_directory t ON d.parent_id = t.id
            )
     SELECT vw_directory.id,
        vw_directory.parent_id,
        vw_directory.name,
        vw_directory.path
       FROM vw_directory;
    


    Латеральное присоединение



    Позволяет обращатся из подзапроса к сущностями из внешнего запроса. Пример использования (подсчет количества полей только для сущностей из схемы public):

    SELECT t.table_schema || '.' || t.table_name,
    	   q.columns_count
    FROM information_schema.tables t,
    LATERAL (
    			SELECT sum ( 1 ) AS columns_count
    			FROM information_schema.columns c
    			WHERE t.table_schema IN ( 'public' ) AND
    				  t.table_schema || '.' || t.table_name = c.table_schema || '.' || c.table_name
    		) q
    ORDER BY 1;
    


    Изменяемые внешние таблицы



    Новый модуль postgres_fdw, позволяющий получить read/write доступ к данным, расположенным в другой БД. Ранее такая функциональность была в dblink, но в postgres_fdw всё прозрачнее, стандартизированный синтаксис и можно получить лучшую производительность. Посмотрим каким способом можно использовать postgres_fdw.

    Создадим новую БД fdb и в ней тестовую таблицу (она будет внешней по отношению к текущей БД):
    CREATE TABLE city
    (
    	country text NOT NULL,
    	name text NOT NULL,
    
    	CONSTRAINT uk_city_name UNIQUE ( country, name )
    );
    


    Вернемся в текущую БД и настроим внешний источник данных:
    -- создание расширения
    CREATE EXTENSION postgres_fdw;
    
    -- добавление внешнего сервера
    CREATE SERVER fdb_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS ( host 'localhost', dbname 'fdb' );
    
    -- отображения пользователя
    CREATE USER MAPPING FOR PUBLIC SERVER fdb_server OPTIONS ( password 'pwd' );
    
    -- создание внешней таблицы
    CREATE FOREIGN TABLE fdb_city ( country text, name text ) SERVER fdb_server OPTIONS ( table_name 'city' );
    


    Теперь мы можем работать с внешней таблицей:
    -- добавляем запись
    INSERT INTO fdb_city ( country, name ) VALUES ( 'USA', 'Las Vegas' );
    
    -- изменяем ее
    UPDATE fdb_city SET name = 'New Vegas' WHERE name = 'New Vegas';
    
    -- смотрим, что получилось
    SELECT * FROM fdb_city;
    


    Чтобы удостоверится, что данные действительно попали туда, куда надо, переключаемся в БД fdb и проверяем:
    SELECT * FROM city;
    


    Функции и операторы для работы с типом JSON



    Тип JSON появился в PostgreSQL 9.2, но функций было лишь две — array_to_json (конвертация массива в JSON) и row_to_json (конвертация записи в JSON). Теперь функций стало больше и можно вполне работать с этим типом:
    CREATE TYPE t_link AS
    (
    	"from" text,
    	"to" text
    );
    
    CREATE TABLE param
    (
    	id serial NOT NULL,
    	name text NOT NULL,
    	value json NOT NULL,
    
    	CONSTRAINT pk_param_id PRIMARY KEY ( id ),
    	CONSTRAINT uk_param_name UNIQUE ( name )
    );
    
    INSERT INTO param ( name, value ) VALUES
    ( 'connection', '{ "username" : "Administrator", "login" : "root", "databases" : [ "db0", "db1" ], "enable" : { "day" : 0, "night" : 1 } }'::json ),
    ( 'link', '{ "from" : "db0", "to" : "db1" }'::json );
    
    -- значение поля (запрос)
    SELECT value ->> 'username' FROM param WHERE name = 'connection'; 
    
    -- результат
    Administrator
    
    -- значение поля (по заданному пути) (запрос)
    SELECT value #>> '{databases,0}' FROM param WHERE name = 'connection';
    
    -- результат
    db0
    
    -- преобразование в SETOF ( key, value ) с типом text (запрос)
    SELECT json_each_text ( value ) FROM param;
    
    -- результат
    (username,Administrator)
    (login,root)
    (databases,"[ ""db0"", ""db1"" ]")
    (enable,"{ ""day"" : 0, ""night"" : 1 }")
    (from,db0)
    (to,db1)
    
    -- значения ключей (запрос)
    SELECT json_object_keys ( value ) FROM param;
    
    -- результат
    username
    login
    databases
    enable
    from
    to
    
    -- значение в виде записи (запрос)
    SELECT * FROM json_populate_record ( null::t_link, ( SELECT value FROM param WHERE name = 'link' ) );
    
    -- результат
    db0;db1
    
    -- значения массива (запрос)
    SELECT json_array_elements ( value -> 'databases' ) FROM param;
    
    -- результат
    "db0"
    "db1"
    


    Подводя итог, хочу сказать, что рад развитию PostgreSQL, проект развивается, хоть и есть еще сырые вещи.

    P.S. Спасибо, если дочитали до конца.

    Ссылки:
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 30
    • +28
      Отлично, так и надо писать статьи про обновления — подробно, с примерами, а не просто перечисление новых фич.
    • 0
      Интересно, спасибо.

      Материализированное представление — интересная штука, но где их можно применить на практике? Судя по плану запроса и индексам, это просто аналог обычной таблицы, данные в которой приходится так же обновлять. Хотя проще иметь один объект-представление, нежели создавать таблицу + функцию для её обновления.
      • 0
        Первое, что пришло в голову в момент прочтения статьи: есть база регионов присутствия провайдера, есть база районов в них, есть населенных пунктов, есть улиц и домов. По house_id формирую строку адреса, но пока приходится хранить в абонентах как строку-адрес, так и house_id для упрощения поиска. Далее, мы пишем улицы без сокращений, но некоторые названия так и хочется сократить, т.к. длина названия стремится к ширише монитора )) (погуглите улицы в Вышнем Волочке). С помощью материализованного представления я смогу собрать различные написания адресов в строки и подкрепить их к house_id. Плюсы очевидны.

        Оффтоп: слоны зачетные ))
        • 0
          Там где используются тяжёлые запросы, а данные меняются сравнительно редко. Как у нас в проекте, например. Нам просто обязательно приходится эмулировать материализованные представления, пока нельзя будет перейти на 9.3, где это делается так просто и лаконично.
          • 0
            > Материализированное представление — интересная штука, но где их можно применить на практике?

            Например, в таблицах, собирающих статистику. Если статистика нетривиальная (или собирать ее надо на большом объеме данных), то обычное представление будет выдавать результат за совершенно неудовлетворительное время.
            • 0
              Во, есть такое.
              Обычно делается сводная таблица, которая заполняется длительное время, и это никак нельзя сделать по клиентскому запросу (время ожидания составляет десятки минут), формируем её обычно в конце отчётного периода, либо, если очень срочно нужно, в любое время по запросу пользователей. А клиент просто берёт из неё готовые данные.

              Реализовано с помощью обычной таблицы и функции, её заполняющей.
            • 0
              В __Оракле__ используем хранимые представления с обновлением как по ON UPDATE так и по таймеру (раз в час).

              В какой то мере заменяет сфинкс для поиска в большом объёме данных по «многоджойнутым» запросам и, по сравнении со сфинкс, решает проблемы доступа к отдельным записям путём дополнительного join на таблицы привилегий, которые у каждого пользователя могут отличаться.

              В общем, мощная и удобная штука.
              Хорошо, если такое же теперь есть теперь в PostgreSQL.
              • 0
                Да, я уже понял, куда её можно применить. Смущает только то, что смену версии на текущем проекте проблематично делать.
                • 0
                  Простите, а в чем проблема смены версии? Там же «на лету» все можно поменять; я как раз недавно переходил на 9.3, очень порадовала простота процедуры.
            • +3
              Великолепная статья. Спасибо. А почему триггер на обновление представления из примера выполняется «for each row»? Разве недостаточно будет выполнить обновление для операции в целом?
              • 0
                Верно, спасибо, FOR EACH STATEMENT в данном случае более подходит. Машинально написал FOR EACH ROW… Исправил.
              • +1
                Да уж, после прочтения таких новостей крайне грустно возвращаться к MySQL.
                Oracle совершенно не шевелится с добавлением чего-то действительно полезного.
                • +2
                  У Oracle есть Oracle Database, продаваемый за деньги (Express Edition не беру во внимание), где есть много хороших вещей. Мне кажется, им не целесообразно добавлять такой функционал в open source проект.
                • –1
                  Ну всё, ребята, сейчас будут PHP гуру насиловать JSONом postgres.
                  Как по мне, это они перестарались. Может мне может кто-нибудь объяснить, зачем в базе данных этот тип?
                  • +1
                    А почему бы и нет. У многих NoSQL баз данных отпал такой аргумент «schema-less» против PostgreSQL :)
                    • 0
                      В этом случае я думаю что человек ошибся инструментом.
                      • 0
                        По-моему, два инструмента с проблемами синхронизации данных хуже, чем один инструмент, поддерживающий аналогичные механизмы, даже в ущерб скорости.
                    • +1
                      Я могу. Мне его сильно не хватало. Ибо обычного hstore маловато будет.

                      Есть такая штука как OpenStreetMap. Точнее его база. У узлов описывающие географические объекты есть таги. К примеру маяки.
                      Нужно описать их тип, световые сектора их цвета, частоту мерцания. А у моста или порта этоти таги совсем другие.
                      Т.е. конкретно выделить схему атрибутов различных типов объектов нельзя. Точнее можно и нужно но спроецировать ее на базу очень гемморойно.

                      Сейчас атрибуты можно хранить в плоском виде т.к hstore не может в качестве значения использовать другой hstore. в результате чего ключи выглядят так seamark:light:1:color=>red. Что не очень удобно и для индексации и для разбора. JSON тут более естественен.
                      • 0
                        В принципе раньше можно было использовать тип XML и даже строить по нему индексы. А так же делать разные XPath запросы к этим данным.
                        • +3
                          JSON «легче» читается и занимает меньший объем памяти, по сравнению с XML.
                      • 0
                        Затем чтобы в таблице не держать 100 полей. Например атрибуты пользователя, коих может быть оооочень много.
                      • –1
                        очень интересно, спасибо.
                        • –1
                          Еще года 3 назад, когда я активно юзал постгрес, он уже был таким матёрым слонярой с бивнями по полтора метра. А тут такие несерьёзные слоники на картинках…
                          • 0
                            LATERAL больше всего радует, с этой конструкцией много формулировок запросов должно упроститься.
                            В следующей версии обещают условия внутри вызова агрегатных функций — совсем прелесть.
                            • +1
                              А я хочу пакеты… а их всё нет и нет =(
                            • +1
                              > Note that materialized views cannot be auto-refreshed; refreshes are not incremental; and the base table cannot be manipulated
                              21 год назад (1992 г), у Oracle 7 уже был fast refresh (incremental refresh в терминах пострге).
                              Пруф — docs.oracle.com/cd/A57673_01/DOC/server/doc/SQL73/ch4a.htm#toc100

                              Постгре, шажочек за шажочком, гонится за ораклом, отставая на десятилетия. Всё-равно молодцы.
                              • 0
                                В частных случаях incremental refresh несложно сделать руками. По настоящему ракетная наука, это Qwery Rewrite, позволяющий автоматически подставлять Materialized View в SQL-запросы пользователя, переписывая их.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.