Оптимизация запроса count для PostgreSQL

У меня есть таблица в postgresql, которая содержит массив, который постоянно обновляется.

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

select count(id) 
from table 
where not (ARRAY['parameter value'] <@ table.array_column)

но при увеличении количества строк и количества выполнений этого запроса (несколько раз в секунду, возможно, сотни или тысячи) производительность сильно снижается, мне кажется, что подсчет в postgresql может иметь линейный порядок выполнения (я не совсем уверен в этом).

в основном мой вопрос:

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

любое предложение, которое вы могли бы дать мне, было бы действительно оценено.

3 ответов


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

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

CREATE TABLE demo( id integer primary key );
INSERT INTO demo (id) SELECT id FROM arrtable;
CREATE TABLE properties (
  demo_id integer not null references demo(id),
  property integer not null,
  primary key (demo_id, property)
);
CREATE INDEX properties_property_idx ON properties(property);

затем вы можете запросить свойства:

SELECT count(id) 
FROM demo 
WHERE NOT EXISTS (
  SELECT 1 FROM properties WHERE demo.id = properties.demo_id AND property = 1
)

Я ожидал, что это будет намного быстрее, чем исходный запрос, но на самом деле это то же самое с теми же образцами данных; он работает в том же диапазоне от 2 до 3s, что и ваш исходный запрос. Это та же проблема, где поиск того, что не существует гораздо медленнее, чем поиск того, что is там; если мы ищем строки, содержащие свойство, мы можем избежать seqscan demo и просто сканировать properties для сопоставления идентификаторов напрямую.

опять же, сканирование seq в таблице, содержащей массив делает работу так же хорошо.


PostgreSQL фактически поддерживает индексы GIN в Столбцах массива. К сожалению, он не кажется пригодным для NOT ARRAY[...] <@ indexed_col и GIN индексы не подходят для часто обновляемых таблиц в любом случае.

демо:

CREATE TABLE arrtable (id integer primary key, array_column integer[]);

INSERT INTO arrtable(1, ARRAY[1,2,3,4]);

CREATE INDEX arrtable_arraycolumn_gin_arr_idx
ON arrtable USING GIN(array_column);

-- Use the following *only* for testing whether Pg can use an index
-- Do not use it in production.
SET enable_seqscan = off;

explain (buffers, analyze) select count(id) 
from arrtable 
where not (ARRAY[1] <@ arrtable.array_column);

к сожалению, это показывает, что мы не можем использовать индекс. Если вы не отрицаете условие, его можно использовать, поэтому вы можете искать и подсчитывать строки, которые do содержит элемент поиска (путем удаления NOT).

вы можете использовать индекс для подсчета записей, которые do содержит целевое значение, затем вычтите этот результат из числа всех записей. С counting все строки в таблице довольно медленно в PostgreSQL (9.1 и старше) и требует последовательного сканирования это на самом деле будет медленнее, чем ваш текущий запрос. Возможно, что в 9.2 для подсчета строк можно использовать только сканирование индекса, если у вас есть индекс b-дерева на id в этом случае это может быть OK:

SELECT (
  SELECT count(id) FROM arrtable
) - (
  SELECT count(id) FROM arrtable 
  WHERE (ARRAY[1] <@ arrtable.array_column)
);

он гарантированно будет работать хуже, чем ваша оригинальная версия для Pg 9.1 и ниже, потому что в дополнение к seqscan ваш оригинал требует этого и требуется сканирование индекса Джина. Теперь я протестировал это на 9.2, и, похоже, он использует индекс для подсчета, поэтому стоит изучить 9.2. С некоторыми менее тривиальными фиктивными данными:

drop index arrtable_arraycolumn_gin_arr_idx ;
truncate table arrtable;
insert into arrtable (id, array_column)
select s, ARRAY[1,2,s,s*2,s*3,s/2,s/4] FROM generate_series(1,1000000) s;
CREATE INDEX arrtable_arraycolumn_gin_arr_idx
ON arrtable USING GIN(array_column);

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

хуже, запрос, использующий этот индекс, занимает в два раза больше времени, чем ваш исходный запрос, и в лучшем случае вдвое меньше на том же наборе данных. Это хуже всего для случаев, когда индекс не очень избирательный, как ARRAY[1] - 4s против 2s для исходного запроса. Где индекс очень избирательный (т. е. не так много совпадений, как ARRAY[199]) он работает примерно за 1,2 секунды против 3S оригинала. Этот индекс просто не стоит иметь для этого запроса.

урок здесь? Иногда правильный ответ - просто выполнить последовательное сканирование.

так как это не будет делать для ваших ставок попадания, либо поддерживать материализованное представление с триггером, как предлагает @debenhur, либо попытаться инвертировать массив, чтобы быть списком параметров, которые делает запись не есть, поэтому вы можете использовать индекс GiST, как предлагает @maniek.


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

можете ли вы расположить столбец так, чтобы он хранил инверсию данных (так, чтобы запрос был select count(id) from table where ARRAY[‘parameter value’] <@ table.array_column) ? Этот запрос будет использовать индекс gin/gist.