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

Posted

技术标签:

【中文标题】索引以查找外键不存在的记录【英文标题】:Index to find records where the foreign key does not exist 【发布时间】:2014-01-19 09:45:10 【问题描述】:
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;

以上可能吗?我该怎么做?

注意: 该数据集的一些特征:

    交易永远不会被删除,只会被添加。

    产品永远不会被删除,而是以每分钟 100 秒的速度添加(显然这是一个复杂得多的实际用例背后的虚构示例)。其中一小部分是暂时的孤儿

    我需要经常查询(最多每分钟一次)并且需要始终知道当前的孤立产品集是什么

【问题讨论】:

您可以在 products 表中添加“last_transaction_id”列,在插入时设置触发器,然后在 products 表中搜索 last_transaction_id 是否为空。 我试图不触及现有架构。创建索引是不可能的吗?或者,我可以创建任何索引,只要它不涉及更改架构或需要编写插入触发器 实际上,只要我能快速查找,我可以接受不涉及更改架构的任何事情 FK 约束自动为 product_id 列构建索引。但是:您正在寻找孤立的行,这需要时间。它们可以放在任何地方,至少必须查阅整个索引加上产品表(或它的索引)才能找到它们。您正在 100M 的大海捞针中寻找一百根针。你为什么要找到它们,如果这是你的核心逻辑的一部分,那么你的数据模型中的某些东西是严重错误的,恕我直言。如果只是维护:处理它。 我冒昧地根据您的问题修复查询以匹配您的表定义。 【参考方案1】:

我能想到的最好的就是你在 cmets 中的最后一个想法: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 的直接替换 - 显然对性能有很大影响(几 100 行而不是1 亿)。具体化视图需要 Postgres 9.3,但根据 cmets,这就是您使用的。您可以在早期版本中轻松地手动实现它。

但是,物化视图是快照,不会动态更新。 (无论如何,这可能会使任何性能优势无效。)要更新,您运行(昂贵的)操作:

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 实现。

在表 transactions 中出现引用行时,

orphaned_products2 中删除产品。 使用触发器AFTER UPDATE OF product_id ON transations 实现。 如果您的模型允许更新 transations.products_id - 这将是非常规的事情。 还有一个AFTER INSERT ON transations

所有相对便宜的操作。

如果事务也可以删除,您需要另一个触发器来添加孤立产品AFTER DELETE ON transations - 这会有点贵。对于每个已删除的交易,您需要检查它是否是最后一次引用相关产品,并在这种情况下添加一个孤儿。可能仍然比刷新整个物化视图便宜很多。

VACUUM

根据您的补充信息,我还建议custom settings for aggressive vacuuming 或orphaned_products2,因为它会产生很多死行。

【讨论】:

嘿欧文,这太棒了。我可以确认几件事。交易永远不会被删除,只会被添加。产品也永远不会被删除,而是以每分钟 100 次的速度添加(显然这是一个复杂得多的实际用例背后的虚构示例)。所以我需要它保持新鲜,并且始终知道当前的孤儿产品是什么。 顺便说一句,您的回答看起来很棒,我对物化视图不熟悉,所以我仍在消化您的回答 @wildplasser 似乎不喜欢物化视图的想法。我想知道他是如何看待这种方法的。对我来说它看起来不错,因为物化视图表很小 @alumns:触发器使写操作变得更加昂贵。您应该注意使它们尽可能有效,并且您需要评估由此产生的成本是否可以接受。并且附加表引入了额外的复杂性,为错误腾出空间,并可能削弱参照完整性的严格性。这一切都取决于细节.. 感谢您的详细回答。我每分钟大约写 100 次。所以稍微慢一点的写入实际上很好!你的解决方案真的很好。我去试试看效果好不好【参考方案2】:

我尝试了一些测试数据并找到了一种我认为更快的方法,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.

两个查询的执行计划

【讨论】:

看起来像 SQL Server 代码,并不完全适用于 Postgres。你看到[postgresql] 标签了吗? @ErwinBrandstetter [facepalm] 错过了:( @ErwinBrandstetter 但你认为我的发现是可以接受的,甚至值得一看吗?我不是真正的 DBA 人,但最近一直在尝试了解有关性能优化的更多信息。请给我一些反馈,谢谢。 我对 SQL Server 代码不太熟悉,所以我很难对此发表评论。 EXCEPT 在 Postgres 中不会给你带来太多收益,而 NOT EXISTS 通常执行得更快。现在也没有时间了。 @ErwinBrandstetter 我猜EXCEPT ...NOT EXISTS (...) 会产生相同或相似的查询计划。

以上是关于索引以查找外键不存在的记录的主要内容,如果未能解决你的问题,请参考以下文章

MySQL中如何在关联表中查询出其中一个外键不存在的数据?

单个查询中的 Postgresql 多个连接,其中连接的外键不存在于所有表中

Laravel:未定义的索引 - 数组键不存在

CloudFront + S3网站:应显示隐式索引文档时“指定的键不存在”

如果键不存在,C# Dictionary<int, int> 查找会发生啥?

查询排除了json值且json键不存在的记录