The best I can think of is your last idea in the comments: a 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)
Then you can use this table (a materialized view is just a table) as drop-in replacement for the big table products
in queries working with orphaned products - with obviously great impact on performance (a few 100 rows instead of 100 millions). Materialized views require Postgres 9.3, but that's what you are using according to the comments. And you can implement it by hand easily in earlier versions.
However, a materialized view is a snapshot and not updated dynamically. (This might void any performance benefit anyway.) To update, you run the (expensive) operation:
REFRESH MATERIALIZED VIEW orphaned_products;
You could do that at strategically opportune points in time and have multiple subsequent queries benefit from it, depending on your business model.
Of course, you would have an index on orphaned_products.id
, but that would not be very important for a small table of a few hundred rows.
If your model is such that transactions are never deleted, you could exploit that to great effect. Create a similar table by hand:
CREATE TABLE orphaned_products2 AS
SELECT *
FROM products p
WHERE NOT EXISTS (SELECT 1 FROM transactions t WHERE t.product_id = p.id);
Of course you can refresh that "materialized view" just like the first one by truncating and refilling it. But the point is to avoid the expensive operation. All you actually need is:
Add new products to
orphaned_products2
.
Implement with a triggerAFTER INSERT ON products
.Remove products from
orphaned_products2
as soon as a referencing row appears in tabletransactions
.
Implement with a triggerAFTER UPDATE OF product_id ON transations
. Only if your model allowstransations.products_id
to be updated - which would be an unconventional thing.
And another oneAFTER INSERT ON transations
.
All comparatively cheap operations.
- If transactions can be deleted, too, you'd need another trigger to add orphaned products
AFTER DELETE ON transations
- which would a bit be more expensive. For every deleted transaction you need to check whether that was the last referencing the related product, and add an orphan in this case. May still be a lot cheaper than to refresh the whole materialized view.
VACUUM
After your additional information I would also suggest custom settings for aggressive vacuuming of orphaned_products2
, since it is going to produce a lot of dead rows.