Сам спросил - сам ответил. Я нашел решение, но мне оно совсем не нравится, может, у %username% найдется чего поинтереснее.
Проблема заключается в том, что для простейшего решения нужно иметь возможность хранить состояние, чего PostgreSql прямо не позволяет. Кажется, лучшим решением было бы написание собственной оконной функции, имеющей возможность смотреть на соседние строки, вроде lead, но интернет не изобилует примерами написания оных.
Однако, обнаружилось, что postgres все же может позволить хранить какие-то данные, причем даже не вылезая за пределы транзакции. "Статичные переменные" можно реализовать с помощью функций
current_setting / set_config. Невероятный костыль, конечно, но что есть.
Итак, с помощью "статики" создаем функцию, которая по понятной логике щелкает страницы сортированного списка по изменению строкового ключа:
CREATE OR REPLACE FUNCTION grouped_pagination_page(current_key VARCHAR, per_page INT4) RETURNS INT4 AS $$
DECLARE
last_key VARCHAR;
last_row_count INT4;
last_page INT4;
BEGIN
SELECT COALESCE(current_setting('GPP.last_key', TRUE), '') INTO last_key;
SELECT CAST(COALESCE(NULLIF(current_setting('GPP.last_row_count', TRUE),''),'0') AS INT) INTO last_row_count;
SELECT CAST(COALESCE(NULLIF(current_setting('GPP.last_page', TRUE),''),'1') AS INT) INTO last_page;
IF current_key <> last_key THEN
PERFORM set_config('GPP.last_key', current_key, TRUE);
IF last_row_count >= per_page THEN
last_page = last_page + 1;
last_row_count = 0;
PERFORM set_config('GPP.last_page', last_page::VARCHAR, TRUE);
END IF;
END IF;
last_row_count = last_row_count + 1;
PERFORM set_config('GPP.last_row_count', last_row_count::VARCHAR, TRUE);
RETURN last_page;
END;
$$ LANGUAGE 'plpgsql';
И вот, собственно, использование ее в запросе (у меня ключ группы - вендор-модель-кузов):
SELECT *,
MIN(price) OVER win_vendor min_price_vendor,
MIN(price) OVER win_model min_price_model,
MIN(price) OVER win_body min_price_body,
grouped_pagination_page((vendor_name || model_name || body_type)::VARCHAR, 10) page_number
FROM
car
WINDOW
win_vendor AS (PARTITION BY vendor_name),
win_model AS (PARTITION BY vendor_name, model_name),
win_body AS (PARTITION BY vendor_name, model_name, body_type)
ORDER BY min_price_vendor,
min_price_model,
min_price_body,
price,
specifications_name