用于查找外键不存在的记录的索引

时间:2014-01-02 00:00:56

标签: sql postgresql indexing materialized-views postgresql-performance

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. 我需要经常查询(每分钟最多一次)并且需要始终知道当前的孤立产品集是什么

2 个答案:

答案 0 :(得分:2)

我能想到的最好的是你在评论中的最后一个想法:materialized view

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的替代品 - 显然对性能有很大影响(几百行)而不是1亿)。物化视图需要 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 使用trigger 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 custom settings for aggressive vacuuming,因为它会产生大量死行。

答案 1 :(得分:0)

我尝试了一些测试数据并找到了一种我觉得更快的方法,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())

表2中的过滤索引

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.

两种查询的执行计划

Execution Plans