PostgreSQL - работа с массивом из тысяч элементов

8

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

Вот запрос, который я сейчас использую:

SELECT item_id, other_stuff, ...
FROM (
    SELECT
        -- Partitioned row number as we only want N rows per id
        ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY start_date) AS r,
        item_id, other_stuff, ...
    FROM mytable
    WHERE
        item_id = ANY ($1) -- Integer array
        AND end_date > $2
    ORDER BY item_id ASC, start_date ASC, allowed ASC
) x
WHERE x.r <= 12

Таблица построена так:

    Column     |            Type             | Collation | Nullable | Default 
---------------+-----------------------------+-----------+----------+---------
 item_id       | integer                     |           | not null | 
 allowed       | boolean                     |           | not null | 
 start_date    | timestamp without time zone |           | not null | 
 end_date      | timestamp without time zone |           | not null | 
 ...


 Indexes:
    "idx_dtr_query" btree (item_id, start_date, allowed, end_date)
    ...

Я пришел с этим индексом после того, как попробовал разные и запустил EXPLAINзапрос. Этот был наиболее эффективным как для запросов, так и для сортировки. Вот объяснение анализа запроса:

Subquery Scan on x  (cost=0.56..368945.41 rows=302230 width=73) (actual time=0.021..276.476 rows=168395 loops=1)
  Filter: (x.r <= 12)
  Rows Removed by Filter: 90275
  ->  WindowAgg  (cost=0.56..357611.80 rows=906689 width=73) (actual time=0.019..248.267 rows=258670 loops=1)
        ->  Index Scan using idx_dtr_query on mytable  (cost=0.56..339478.02 rows=906689 width=73) (actual time=0.013..130.362 rows=258670 loops=1)
              Index Cond: ((item_id = ANY ('{/* 15,000 integers */}'::integer[])) AND (end_date > '2018-03-30 12:08:00'::timestamp without time zone))
Planning time: 30.349 ms
Execution time: 284.619 ms

Проблема в том, что массив int может содержать до 15 000 элементов или около того, и в этом случае запрос выполняется довольно медленно (около 800 мс на моем ноутбуке, недавний Dell XPS).

Я думал, что передача массива int в качестве параметра может быть медленной, и, учитывая, что список идентификаторов может быть заранее сохранен в базе данных, я попытался сделать это. Я сохранил их в массиве в другой таблице и использовал item_id = ANY (SELECT UNNEST(item_ids) FROM ...), что было медленнее, чем мой текущий подход. Я также пытался хранить их item_id IN (SELECT item_id FROM ...)построчно и использовать , что было еще медленнее, даже с учетом только тех строк, которые соответствуют моему тесту в таблице.

Есть ли лучший способ сделать это?

Обновление: следуя комментариям Эвана , я попробовал другой подход: каждый элемент является частью нескольких групп, поэтому вместо передачи идентификаторов элементов группы я попытался добавить идентификаторы групп в mytable:

    Column     |            Type             | Collation | Nullable | Default 
---------------+-----------------------------+-----------+----------+---------
 item_id       | integer                     |           | not null | 
 allowed       | boolean                     |           | not null | 
 start_date    | timestamp without time zone |           | not null | 
 end_date      | timestamp without time zone |           | not null | 
 group_ids     | integer[]                   |           | not null | 
 ...

 Indexes:
    "idx_dtr_query" btree (item_id, start_date, allowed, end_date)
    "idx_dtr_group_ids" gin (group_ids)
    ...

Новый запрос ($ 1 - идентификатор целевой группы):

SELECT item_id, other_stuff, ...
FROM (
    SELECT
        -- Partitioned row number as we only want N rows per id
        ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY start_date) AS r,
        item_id, other_stuff, ...
    FROM mytable
    WHERE
        $1 = ANY (group_ids)
        AND end_date > $2
    ORDER BY item_id ASC, start_date ASC, allowed ASC
) x
WHERE x.r <= 12

Объясните, проанализируйте:

Subquery Scan on x  (cost=123356.60..137112.58 rows=131009 width=74) (actual time=811.337..1087.880 rows=172023 loops=1)
  Filter: (x.r <= 12)
  Rows Removed by Filter: 219726
  ->  WindowAgg  (cost=123356.60..132199.73 rows=393028 width=74) (actual time=811.330..1040.121 rows=391749 loops=1)
        ->  Sort  (cost=123356.60..124339.17 rows=393028 width=74) (actual time=811.311..868.127 rows=391749 loops=1)
              Sort Key: item_id, start_date, allowed
              Sort Method: external sort  Disk: 29176kB
              ->  Seq Scan on mytable (cost=0.00..69370.90 rows=393028 width=74) (actual time=0.105..464.126 rows=391749 loops=1)
                    Filter: ((end_date > '2018-04-06 12:00:00'::timestamp without time zone) AND (2928 = ANY (group_ids)))
                    Rows Removed by Filter: 1482567
Planning time: 0.756 ms
Execution time: 1098.348 ms

Возможно, есть место для улучшений с индексами, но мне трудно понять, как Postgres использует их, поэтому я не уверен, что изменить.

Jukurrpa
источник
Сколько строк в «mytable»? Сколько там разных значений "item_id"?
Ник
Кроме того, разве вы не должны иметь ограничение уникальности (возможно, еще не определенный уникальный индекс) в item_id в mytable? ... Отредактировано: о, я вижу "PARTITION BY item_id", поэтому этот вопрос трансформируется в "Каков естественный, реальный ключ для ваших данных? Что должно образовывать уникальный индекс там?"
Ник
Около 12 миллионов строк mytable, около 500 000 разных item_id. Для этой таблицы нет реального естественного уникального ключа, это данные, которые автоматически генерируются для повторяющихся событий. Я думаю, что item_id+ start_date+ name(поле не показано здесь) может составлять какой-то ключ.
Jukurrpa
Можете ли вы опубликовать план выполнения, который вы получаете?
Colin 't Hart
Конечно, добавили объяснение анализа к вопросу.
Jukurrpa

Ответы:

1

Есть ли лучший способ сделать это?

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

BEGIN;
  CREATE TEMP TABLE myitems ( item_id int PRIMARY KEY );
  INSERT INTO myitems(item_id) VALUES (1), (2); -- and on and on
  CREATE INDEX ON myitems(item_id);
COMMIT;

ANALYZE myitems;

SELECT item_id, other_stuff, ...
FROM (
  SELECT
      -- Partitioned row number as we only want N rows per id
      ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY start_date) AS r,
      item_id, other_stuff, ...
  FROM mytable
  INNER JOIN myitems USING (item_id)
  WHERE end_date > $2
  ORDER BY item_id ASC, start_date ASC, allowed ASC
) x
WHERE x.r <= 12;

Но даже лучше, чем это ...

"500k Different item_id" ... "Массив int может содержать до 15000 элементов"

Вы выбираете 3% своей базы данных индивидуально. Я должен задаться вопросом, не лучше ли вам создавать группы / теги и т.д. в самой схеме. Мне никогда не приходилось отправлять 15 000 различных идентификаторов в запросе.

Эван Кэрролл
источник
Просто попытался использовать временную таблицу, и она работает медленнее, по крайней мере, в случае 15000 идентификаторов. Что касается создания групп в самой схеме, вы имеете в виду таблицу с идентификаторами, которые я передаю в качестве аргумента? Я пробовал что-то вроде этого, но производительность была похожа или хуже, чем мой нынешний подход. Я
обновлю
Нет, я имею в виду Если у вас есть 15 000 идентификаторов, как правило, вы сохраняете что-то в идентификаторе, например, является ли элемент кухонным продуктом, и вместо того, чтобы хранить group_id, соответствующий «кухонному продукту», вы пытаетесь найти все кухонные продукты. по их идентификаторам. (что плохо по любой причине) Что представляют собой эти 15 000 идентификаторов? Почему он не хранится в самой строке?
Эван Кэрролл
Каждый элемент принадлежит нескольким группам (обычно 15-20 из них), поэтому я попытался сохранить их как массив int в mytable, но не смог понять, как правильно их проиндексировать. Я обновил вопрос со всеми деталями.
Jukurrpa