桌子
products
:
id int primary_key
桌子
transactions
:
product_id int 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 秒的速度添加(显然这是一个更复杂的实际用例背后的虚构示例)。其中一小部分暂时成为孤儿。
我需要经常查询(最多每分钟一次)并且需要始终知道当前的孤立产品集是什么。
MATERIALIZED VIEW
:
CREATE MATERIALIZED VIEW orphaned_products AS
SELECT * -- or just the columns you need
FROM products p
WHERE NOT EXISTS (SELECT 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 * -- or just the columns you need
FROM products p
WHERE NOT EXISTS (SELECT FROM transactions t WHERE t.product_id = p.id);
您可以像第一个一样通过截断和重新填充来刷新“物化视图”。但关键是要避免昂贵的操作。你真正需要的是:
添加新产品到
orphaned_products2
.AFTER INSERT ON products
.
一旦引用行出现在表orphaned_products2
中,就从
transactions
中删除产品。
AFTER UPDATE OF product_id ON transactions
实现。 Only如果你的模型允许transactions.products_id
更新 - 这对于交易来说似乎很奇怪。AFTER INSERT ON transactions
.
所有相对便宜的操作。
如果可以删除交易,则需要另一个触发器来添加孤立产品
AFTER DELETE ON transactions
——这会有点贵。对于每笔已删除的交易,您需要检查它是否是最后一次引用相关产品,并在这种情况下添加一个孤儿。可能还是比刷新整个物化视图便宜很多
VACUUM
在您提供更多信息后,我还建议自定义设置以进行orphaned_products2
的积极吸尘,因为它会产生很多死行。
我尝试了一些测试数据并找到了一种我认为更快的方法,
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.
两个查询的执行计划