Индекс для поиска записей, в которых внешний ключ не существует.

table products
id primary_key

table transactions
product_id foreign_key references products

Приведенный ниже SQL-запрос выполняется очень медленно:

SELECT products.* 
FROM   products 
       LEFT JOIN transactions 
              ON ( products.id = transactions.product_id ) 
WHERE  transactions.product_id IS NULL; 

Из 100 сотен миллионов записей продуктов может быть только 100 записей, в которых у продукта нет соответствующих транзакций.

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

Я хочу создать частичный индекс следующим образом:

CREATE INDEX products_with_no_transactions_index 
ON (Left JOIN TABLE 
    BETWEEN products AND transactions) 
WHERE transactions.product_id IS NULL;

Возможно ли вышеизложенное и как мне это сделать?

Примечание. Некоторые характеристики этого набора данных:

  1. Транзакции никогда не удаляются, а только добавляются.

  2. Продукты никогда не удаляются, а добавляются со скоростью 100 секунд в минуту (очевидно, это вымышленный пример, стоящий за гораздо более сложным реальным вариантом использования). Небольшое количество временно осиротевших

  3. Мне нужно часто запрашивать (до одного раза в минуту) и всегда знать, каков текущий набор потерянных продуктов.


person samol    schedule 02.01.2014    source источник
comment
Вы можете добавить столбец «last_transaction_id» в таблицу продуктов, настроить триггер на вставку, а затем выполнить поиск в таблице продуктов для last_transaction_id равным нулю.   -  person Keith John Hutchison    schedule 02.01.2014
comment
Я стараюсь не трогать существующую схему. Нельзя ли создать индекс? В качестве альтернативы я могу создать любой индекс, если он не требует изменения схемы или необходимости написания триггера вставки.   -  person samol    schedule 02.01.2014
comment
На самом деле, я согласен со всем, что не связано с изменением схемы, если я могу быстро просмотреть ее.   -  person samol    schedule 02.01.2014
comment
Ограничение FK автоматически создает индекс для столбца product_id. Но: вы ищете потерянные строки, что требует времени. Они могут находиться где угодно, и, по крайней мере, весь индекс плюс таблица продуктов (или ее индекс) должны быть просмотрены, чтобы найти их. Вы ищете сто иголок в стоге сена 100М. Зачем вам их искать, если это часть вашей основной логики, что-то в вашей модели данных серьезно не так, ИМХО. Если это только техническое обслуживание: разберитесь с этим.   -  person wildplasser    schedule 02.01.2014
comment
Кстати, у меня postgresql 9.3, так можно ли создать материализованное представление и проиндексировать его? Я лично не материализовал взгляд и не знаком с ним   -  person samol    schedule 02.01.2014
comment
Кстати: The below SQL Query is very slow: не факт. Не обошлось без плана запроса + настройки + popcount таблиц. Бросание материализованных представлений только еще больше запутает вас, ИМХО.   -  person wildplasser    schedule 02.01.2014
comment
в порядке. Справедливо. Это по устаревшей причине, и мне действительно нужно иметь возможность часто запрашивать их.   -  person samol    schedule 02.01.2014
comment
Я позволил себе исправить запрос, чтобы он соответствовал вашему определению таблицы в соответствии с вашим вопросом.   -  person Erwin Brandstetter    schedule 02.01.2014
comment
С твоим ремонтом лучше. Спасибо   -  person samol    schedule 02.01.2014
comment
@wildplasser Я хотел узнать ваше мнение об этом ответе. Это выглядит хорошо для меня, но что вы думаете, поскольку вам не нравится подход с материализованным представлением. stackoverflow.com/questions/20874569/   -  person samol    schedule 02.01.2014
comment
Ответ Эрвина выглядит хорошо для меня. Лично я считаю, что вам следует переосмыслить свою модель данных: создать приют (как в заключительных замечаниях Эрвина) или добавить несколько триггеров и сделать ее более управляемой состоянием.   -  person wildplasser    schedule 02.01.2014


Ответы (2)


Лучшее, что я могу придумать, это ваша последняя идея в комментариях: < strong>материализованное представление.

CREATE MATERIALIZED VIEW orphaned_products AS
SELECT *
FROM   products p
WHERE  NOT EXISTS (SELECT 1 FROM transactions t WHERE t.product_id = p.id)

Затем вы можете использовать эту таблицу (материализованное представление — это просто таблица) в качестве замены большой таблицы products в запросах, работающих с осиротевшими продуктами — с очевидным большим влиянием на производительность (несколько 100 строк вместо 100 миллионов). Для материализованных представлений требуется Postgres 9.3, но это то, что вы используете в соответствии с комментариями. И вы можете легко реализовать это вручную в более ранних версиях.

Однако материализованное представление представляет собой снимок и не обновляется динамически. (В любом случае это может аннулировать любое преимущество в производительности.) Для обновления вы запускаете (дорогостоящую) операцию:

REFRESH MATERIALIZED VIEW orphaned_products;

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

Конечно, у вас будет индекс для orphaned_products.id, но это не будет очень важно для маленькой таблицы из нескольких сотен строк.

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

CREATE TABLE orphaned_products2 AS
SELECT *
FROM   products p
WHERE  NOT EXISTS (SELECT 1 FROM transactions t WHERE t.product_id = p.id);

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

  • Добавить новые продукты в orphaned_products2.
    Реализовать с помощью триггер AFTER INSERT ON products.

  • Удалите продукты из orphaned_products2, как только в таблице transactions появится соответствующая строка.
    Реализуйте с помощью триггера AFTER UPDATE OF product_id ON transations. Только если ваша модель позволяет обновлять transations.products_id, что было бы необычно.
    И еще один AFTER INSERT ON transations.

Все сравнительно дешевые операции.

  • Если транзакции также могут быть удалены, вам понадобится еще один триггер для добавления потерянных продуктов AFTER DELETE ON transations, что будет немного дороже. Для каждой удаленной транзакции вам нужно проверить, была ли она последней ссылкой на связанный продукт, и в этом случае добавить потерянную транзакцию. Все еще может быть намного дешевле, чем обновлять все материализованное представление.

VACUUM

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

person Erwin Brandstetter    schedule 02.01.2014
comment
Эй, Эрвин, это превосходно. Есть пара вещей, которые я могу подтвердить. Транзакции никогда не удаляются, а только добавляются. Продукты также никогда не удаляются, а добавляются со скоростью 100 секунд в минуту (очевидно, это вымышленный пример, лежащий в основе гораздо более сложного фактического варианта использования). Поэтому мне нужно, чтобы он был свежим и всегда знал, что такое текущие бесхозные продукты. - person samol; 02.01.2014
comment
Кстати, ваш ответ выглядит превосходно, я не знаком с материализованными представлениями, поэтому я все еще перевариваю ваш ответ. - person samol; 02.01.2014
comment
@wildplasser, похоже, не был поклонником идеи материализованного представления. Интересно, как он думает об этом подходе. Это выглядит хорошо для меня, так как таблица материализованного представления мала - person samol; 02.01.2014
comment
@alumns: триггеры удорожают операции записи. Вы должны позаботиться о том, чтобы сделать их максимально эффективными, и вам нужно оценить, является ли полученная стоимость приемлемой. А дополнительная таблица вносит дополнительную сложность, создавая места для ошибок и, возможно, ослабляя строгость ссылочной целостности. Все зависит от деталей.. - person Erwin Brandstetter; 02.01.2014
comment
Спасибо за подробный ответ. Я делаю около 100 записей в минуту. Так что немного более медленная запись на самом деле хороша! Ваше решение действительно хорошее. Я собираюсь попробовать это и посмотреть, хорошо ли это работает - person samol; 02.01.2014
comment
Эй, Эрвин, не могли бы вы немного расширить свой ответ, почему в этом случае необходимо настроить агрессивную чистку? - person samol; 02.01.2014
comment
@alumns: Поскольку вы собираетесь вводить около 100 строк в минуту и ​​вскоре после этого удалять большинство из них, у вас будет много версий мертвых строк, раздувающих таблицу. Чтобы удалить раздувание, вам нужно запускать VACUUM и, вероятно, чаще, чем запланировал бы стандартный демон автоочистки. Подробнее в руководстве здесь. - person Erwin Brandstetter; 02.01.2014

Я попытался попробовать некоторые тестовые данные и нашел способ, который, как мне кажется, более быстрый, USING THE EXCEPT OPERATOR

Ниже приведены мои выводы.

Тестовые данные

CREATE TABLE TestTable_1 
(ID INT IDENTITY PRIMARY KEY,
Column1 INT,
Column2 INT
)
GO

CREATE TABLE TestTable_2
(ID INT IDENTITY PRIMARY KEY,
Column1 INT,
Column2 INT,
FK_ID INT references TestTable_1(ID)
)
GO

DECLARE @i INT = 1

WHILE (@i <= 10000)
 BEGIN
   INSERT INTO TestTable_1 (Column1, Column2)
   VALUES (@i , @i + 100)
   SET @i = @i + 1;
 END

 DECLARE @i2 INT = 1

WHILE (@i2 <= 10000)
 BEGIN
   INSERT INTO TestTable_2 (Column1, Column2, FK_ID)
   VALUES (@i2 , @i2 + 100, 1 + CONVERT(INT, (10000-1+1)*RAND()))
   SET @i2 = @i2 + 1;
 END

 UPDATE  TestTable_2
 SET FK_ID = NULL
 WHERE ID IN (SELECT TOP 10 ID FROM TestTable_2 ORDER BY NEWID())

Отфильтрованный индекс во второй таблице

CREATE NONCLUSTERED INDEX FIX_FK_ID
ON TestTable_2(ID, FK_ID)
WHERE FK_ID IS NULL ;
GO

Запрос 1

SET STATISTICS IO ON;
PRINT 'TEST 1'
SELECT T1.*
FROM TestTable_1 T1 LEFT JOIN TestTable_2 T2
ON T1.ID = T2.FK_ID
WHERE FK_ID IS NOT NULL

Запрос 2

PRINT 'TEST 2'
SELECT ID,  Column1,    Column2 FROM TestTable_1
EXCEPT 
SELECT ID,  Column1,    Column2 FROM TestTable_2
WHERE FK_ID IS NULL

TEST 1

(9990 row(s) affected)
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestTable_1'. Scan count 1, logical reads 28, physical reads 0, read-ahead reads 19, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestTable_2'. Scan count 1, logical reads 33, physical reads 3, read-ahead reads 29, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.


TEST 2

(9990 row(s) affected)
Table 'TestTable_1'. Scan count 1, logical reads 28, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestTable_2'. Scan count 1, logical reads 22, physical reads 1, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Планы выполнения для обоих запросов

Планы выполнения

person M.Ali    schedule 02.01.2014
comment
Похоже на код SQL Server, который не полностью применим к Postgres. Вы видели тег [postgresql]? - person Erwin Brandstetter; 02.01.2014
comment
@ErwinBrandstetter [facepalm] пропустил это :( - person M.Ali; 02.01.2014
comment
@ErwinBrandstetter, но как вы считаете, мои выводы приемлемы или даже заслуживают того, чтобы на них посмотреть ?? На самом деле я не являюсь администратором баз данных, но недавно пытался узнать больше об оптимизации производительности. Пожалуйста, я был бы признателен за обратную связь, спасибо. - person M.Ali; 02.01.2014
comment
Я не так хорошо знаком с кодом SQL Server, поэтому вряд ли могу это комментировать. EXCEPT не принесет вам многого в Postgres, где NOT EXISTS обычно работает быстрее. Сейчас тоже вне времени. - person Erwin Brandstetter; 02.01.2014
comment
@ErwinBrandstetter Я предполагаю, что EXCEPT ... и NOT EXISTS (...) дадут одинаковые или похожие планы запросов. - person wildplasser; 02.01.2014
comment
Если вы идете по пути материализованного представления, мой вопрос и ответ по этому поводу могут быть актуальными — postgresql.1045698.n5.nabble.com/ - person Jayadevan; 03.01.2014