Получить разбитые на страницы строки и общее количество в одном запросе - PullRequest
2 голосов
/ 11 апреля 2020

Базовое требование:
Найти последние записи для person_id на submission_date для указанных критериев фильтрации type, plan, status. Таких фильтров может быть больше, но логика c для возврата последних по дате отправки одинакова независимо. Два основных способа использования - один для просмотра страниц в пользовательском интерфейсе и второй для создания отчетов.

WITH cte AS (
  SELECT * FROM (
    SELECT my_table.*, rank() OVER (PARTITION BY person_id ORDER BY submission_date DESC, last_updated DESC, id DESC) FROM my_table
    )  rank_filter 
      WHERE RANK=1 AND status in ('ACCEPTED','CORRECTED') AND type != 'CR' AND h_plan_id IN (10000, 20000)
)
SELECT
SELECT count(id) FROM cte group by id,
SELECT * FROM cte limit 10 offset 0;

group by также не работает в CTE. объединение со всеми null в запросе подсчета может работать для объединения, вероятно, но не обязательно.

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

Postgres версия 12.

\d my_table;
                               Table "public.my_table"
                 Column   |            Type             | Collation | Nullable 
--------------------------+-----------------------------+-----------+----------
 id                       | bigint                      |           | not null 
 h_plan_id                | bigint                      |           | not null 
 h_plan_submitter_id      | bigint                      |           |          
 last_updated             | timestamp without time zone |           |          
 date_created             | timestamp without time zone |           |          
 modified_by              | character varying(255)      |           |          
 segment_number           | integer                     |           |          

 -- <bunch of other text columns>

 submission_date          | character varying(255)      |           |          
 person_id                | character varying(255)      |           |          
 status                   | character varying(255)      |           |          
 file_id                  | bigint                      |           | not null 
Indexes:
    "my_table_pkey" PRIMARY KEY, btree (id)
    "my_table_file_idx" btree (file_id)
    "my_table_hplansubmitter_idx" btree (h_plan_submitter_id)
    "my_table_key_hash_idx" btree (key_hash)
    "my_table_person_id_idx" btree (person_id)
    "my_table_segment_number_idx" btree (segment_number)
Foreign-key constraints:
    "fk38njesaryvhj7e3p4thqkq7pb" FOREIGN KEY (h_plan_id) REFERENCES health_plan(id) ON UPDATE CASCADE ON DELETE CASCADE
    "fk6by9668sowmdob7433mi3rpsu" FOREIGN KEY (h_plan_submitter_id) REFERENCES h_plan_submitter(id) ON UPDATE CASCADE ON DELETE CASCADE
    "fkb06gpo9ng6eujkhnes0eco7bj" FOREIGN KEY (file_id) REFERENCES x12file(id) ON UPDATE CASCADE ON DELETE CASCADE

Дополнительная информация Возможные значения для type: EN и CR с EN около 70% данных. Ширина столбцов таблицы select avg_width from pg_stats where tablename='mytable'; составляет в общей сложности 374 для 41 столбца, то есть около 9 на столбец.

Идея состоит в том, чтобы показывать некоторые страницы заранее пользователю, затем они могут фильтроваться по дополнительным параметрам, таким как file_name (каждый файл обычно содержит около 5 тыс. записей), type (очень низкая мощность), member_add_id (высокая мощность), plan_id (низкая мощность, каждые 500 тыс. до миллиона записей будут связаны с идентификатором плана). Бизнес-требование во всех случаях - показывать только последнюю запись для определенного набора идентификаторов плана для submission_date (для отчетов это делается в год). Порядок по идентификатору был просто защитным кодированием, в один и тот же день может быть несколько записей, и даже если кто-то отредактировал вторую последнюю запись и, следовательно, коснулся отметки времени last_updated, мы хотим показать только самую последнюю запись тех же данных. Это, вероятно, никогда не происходит и может быть удалено.

Пользователь может использовать эти данные для создания отчетов CSV.

Результат объяснения для запроса с правым объединением ниже:

 Nested Loop Left Join  (cost=554076.32..554076.56 rows=10 width=17092) (actual time=4530.914..4530.922 rows=10 loops=1)
   CTE cte
     ->  Unique  (cost=519813.11..522319.10 rows=495358 width=1922) (actual time=2719.093..3523.029 rows=422638 loops=1)
           ->  Sort  (cost=519813.11..521066.10 rows=501198 width=1922) (actual time=2719.091..3301.622 rows=423211 loops=1)
                 Sort Key: mytable.person_id, mytable.submission_date DESC NULLS LAST, mytable.last_updated DESC NULLS LAST, mytable.id DESC
                 Sort Method: external merge  Disk: 152384kB
                 ->  Seq Scan on mytable  (cost=0.00..54367.63 rows=501198 width=1922) (actual time=293.953..468.554 rows=423211 loops=1)
                       Filter: (((status)::text = ANY ('{ACCEPTED,CORRECTED}'::text[])) AND (h_plan_id = ANY ('{1,2}'::bigint[])) AND ((type)::text <> 'CR'::text))
                       Rows Removed by Filter: 10158
   ->  Aggregate  (cost=11145.56..11145.57 rows=1 width=8) (actual time=4142.116..4142.116 rows=1 loops=1)
         ->  CTE Scan on cte  (cost=0.00..9907.16 rows=495358 width=0) (actual time=2719.095..4071.481 rows=422638 loops=1)
   ->  Limit  (cost=20611.67..20611.69 rows=10 width=17084) (actual time=388.777..388.781 rows=10 loops=1)
         ->  Sort  (cost=20611.67..21850.06 rows=495358 width=17084) (actual time=388.776..388.777 rows=10 loops=1)
               Sort Key: cte_1.person_id
               Sort Method: top-N heapsort  Memory: 30kB
               ->  CTE Scan on cte cte_1  (cost=0.00..9907.16 rows=495358 width=17084) (actual time=0.013..128.314 rows=422638 loops=1)
 Planning Time: 0.369 ms
 JIT:
   Functions: 9
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 1.947 ms, Inlining 4.983 ms, Optimization 178.469 ms, Emission 110.261 ms, Total 295.660 ms
 Execution Time: 4587.711 ms

Ответы [ 2 ]

3 голосов
/ 11 апреля 2020

Перво-наперво: вы можете использовать результаты из CTE несколько раз в одном запросе, это основная особенность CTE .) Что у вас есть будет работать так (хотя по-прежнему использовать CTE только один раз):

WITH cte AS (
   SELECT * FROM (
      SELECT *, row_number()  -- see below
                OVER (PARTITION BY person_id
                      ORDER BY submission_date DESC NULLS LAST  -- see below
                             , last_updated DESC NULLS LAST  -- see below
                             , id DESC) AS rn
      FROM  tbl
      ) sub
   WHERE  rn = 1
   AND    status IN ('ACCEPTED', 'CORRECTED')
   )
SELECT *, count(*) OVER () AS total_rows_in_cte
FROM   cte
LIMIT  10
OFFSET 0;  -- see below

Предупреждение 1: rank()

rank() может возвращать несколько строк на person_id с помощью rank = 1 , DISTINCT ON (person_id) (как и в случае с Гордоном) - это подходящая замена для row_number(), которая работает для вас, как пояснила дополнительная информация. См .:

Предупреждение 2: ORDER BY submission_date DESC

Ни submission_date ни last_updated определены NOT NULL. Может быть проблема с ORDER BY submission_date DESC, last_updated DESC ... См .:

Должны ли они столбцы действительно будут NOT NULL?

Вы ответили:

Да, все эти столбцы должны быть ненулевыми. Я могу добавить это ограничение. Я назвал это обнуляемым, так как мы получаем данные в файлах, которые не всегда идеальны. Но это очень редкое условие, и я могу вместо этого вставить пустую строку.

Пустые строки не допускаются для типа date. Держите столбцы обнуляемыми. NULL - правильное значение для этих случаев. Используйте NULLS LAST, как показано, чтобы избежать сортировки NULL сверху.

Предупреждение 3: OFFSET

Если OFFSET равно или больше, чем число строк, возвращаемых CTE, вы получаете без строки , поэтому также не существует общего количества. См .:

Промежуточное решение

Обращаясь ко всем предостережениям и основываясь на добавленной информации, мы можем прийти к следующему запросу:

WITH cte AS (
   SELECT DISTINCT ON (person_id) *
   FROM   tbl
   WHERE  status IN ('ACCEPTED', 'CORRECTED')
   ORDER  BY person_id, submission_date DESC NULLS LAST, last_updated DESC NULLS LAST, id DESC
   )
SELECT *
FROM  (
   TABLE  cte
   ORDER  BY person_id  -- ?? see below
   LIMIT  10
   OFFSET 0
   ) sub
RIGHT  JOIN (SELECT count(*) FROM cte) c(total_rows_in_cte) ON true;

Теперь CTE на самом деле используется дважды. RIGHT JOIN гарантирует, что мы получим общее количество, независимо от OFFSET. DISTINCT ON должен выполнить OK-i sh только для нескольких строк на (person_id) в базовом запросе.

Но у вас широкие строки. Насколько широко в среднем? Скорее всего, запрос приведет к последовательному сканированию всей таблицы. Индексы не помогут (сильно). Все это останется крайне неэффективным для подкачки . См .:

Невозможно задействовать индекс для подкачки, поскольку он основан на производной таблице из КТР. А ваши действительные критерии сортировки для подкачки пока неясны (ORDER BY id?). Если целью является подкачка страниц, вам крайне необходим другой стиль запросов. Если вас интересуют только первые несколько страниц, вам нужен другой стиль запроса. Наилучшее решение зависит от информации, которая все еще отсутствует в вопросе ...

Радикально быстрее

Для вашей обновленной цели:

Найти последние записи для person_id на submission_date

(Игнорирование "для указанных критериев фильтра, типа, плана, статуса" для простоты.)

И:

Найти последнюю строку для person_id, только если она имеет status IN ('ACCEPTED','CORRECTED')

На основе этих двух специализированных индексов :

CREATE INDEX ON tbl (submission_date DESC NULLS LAST, last_updated DESC NULLS LAST, id DESC NULLS LAST)
WHERE  status IN ('ACCEPTED', 'CORRECTED'); -- optional

CREATE INDEX ON tbl (person_id, submission_date DESC NULLS LAST, last_updated DESC NULLS LAST, id DESC NULLS LAST);

Запустите этот запрос:

WITH RECURSIVE cte AS (
   (
   SELECT t  -- whole row
   FROM   tbl t
   WHERE  status IN ('ACCEPTED', 'CORRECTED')
   AND    NOT EXISTS (SELECT FROM tbl
                      WHERE  person_id = t.person_id 
                      AND   (  submission_date,   last_updated,   id)
                          > (t.submission_date, t.last_updated, t.id)  -- row-wise comparison
                      )
   ORDER  BY submission_date DESC NULLS LAST, last_updated DESC NULLS LAST, id DESC NULLS LAST
   LIMIT  1
   )

   UNION ALL
   SELECT (SELECT t1  -- whole row
           FROM   tbl t1
           WHERE ( t1.submission_date, t1.last_updated, t1.id)
               < ((t).submission_date,(t).last_updated,(t).id)  -- row-wise comparison
           AND    t1.status IN ('ACCEPTED', 'CORRECTED')
           AND    NOT EXISTS (SELECT FROM tbl
                              WHERE  person_id = t1.person_id 
                              AND   (   submission_date,    last_updated,    id)
                                  > (t1.submission_date, t1.last_updated, t1.id)  -- row-wise comparison
                              )
           ORDER  BY submission_date DESC NULLS LAST, last_updated DESC NULLS LAST, id DESC NULLS LAST
           LIMIT  1)
   FROM   cte c
   WHERE  (t).id IS NOT NULL
   )
SELECT (t).*
FROM   cte
LIMIT  10
OFFSET 0;

Здесь требуется каждый набор круглых скобок.

Этот уровень сложности должен радикально быстрее получить относительно небольшой набор верхних строк с использованием заданных индексов и без последовательных сканирования. См .:

submission_date, скорее всего, должно быть типа timestamptz или date, а не character varying(255) - что является странным определением типа в Postgres в любом случае. См .:

Многие дополнительные детали могут быть оптимизированы, но это выходит из-под контроля. Вы могли бы рассмотреть профессиональный консалтинг.

3 голосов
/ 11 апреля 2020

Вы можете попытаться упростить запрос:

SELECT DISTINCT ON (person_id) t.*
FROM my_table t
WHERE status in ('ACCEPTED', 'CORRECTED')
ORDER BY person_id, submission_date DESC, last_updated DESC, id DESC

Я не уверен, достаточно ли умный Postgres, чтобы использовать индекс в (person_id, submission_date DESC, last_updated DESC, id DESC, status) в этом случае, но стоит попробовать.

Вы можете ускорить это, используя индекс для выражения: (status in ('ACCEPTED', 'CORRECTED'), person_id, submission_date DESC, last_updated DESC, id DESC).

РЕДАКТИРОВАТЬ:

Если вы хотите отсортировать по другому столбцу, вы можете использовать подзапрос:

SELECT t.*
FROM (SELECT DISTINCT ON (person_id) t.*
      FROM my_table t
      WHERE status in ('ACCEPTED', 'CORRECTED')
      ORDER BY person_id, submission_date DESC, last_updated DESC, id DESC
     ) t
ORDER BY submission_date DESC
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...