Forgive me for the vague title. I couldn't think of anything better, do feel free to suggest a better title.
I have been using this query in a web application with PostgreSQL for a really long time (7 Years).
asset_snapshot is the read data from a lot of IoT devices, and it truncates and reinserts data into that table every 1 minute. The maximum size (now of rows) of asset_snapshot data is 5000-10000 rows.
The aim of this query to is to return the most recent time of devices that has never been seen before.
query 1
select *
from location_aasset_snapshots t1
INNER JOIN (
SELECT unique_identifiter, max(last_seen) as last_seen from
location_asset_snapshots
where unique_identifier not in (select unique_identifier from location_assets)
group by unique_identifier
) t2
on t1.unique_identifier = t2.unique_identifier and t1.last_seen = t2.last_seen
A couple of weeks ago the time to execute this query started to become 10 seconds. The reason we found that is the size of the assets table crossed to 200,000 records.
We found out that if we used except instead of not in the query retuned back to its normal speed again.
query 2:
select *
from location_asset_snapshots t1
INNER JOIN (
SELECT unique_identifier, max(last_seen) as last_seen from
location_asset_snapshots
group by unique_identifier
) t2
on t1.unique_identifier = t2.unique_identifier and t1.last_seen = t2.last_seen
except
select *
from location_asset_snapshots t1
INNER JOIN (
SELECT unique_identifier, max(last_seen) as last_seen from
location_asset_snapshots
group by unique_identifier
) t2
on t1.unique_identifier = t2.unique_identifier and t1.last_seen = t2.last_seen
where t1.unique_identifier in
(select unique_identifier from location_assets)
Was curious to learn if there was something built in PostgreSQL that makes the second query runs faster with a growing dataset.
plan for query 1:
Hash Join (cost=4413874.90..4413897.95 rows=2 width=90)
Hash Cond: (((t1.unique_identifuer)::text = (location_asset_snapshots.unique_identifuer)::text) AND (t1.last_seen = (max(location_asset_snapshots.last_seen))))
-> Seq Scan on location_asset_snapshots t1 (cost=0.00..19.93 rows=593 width=57)
-> Hash (cost=4413870.76..4413870.76 rows=276 width=33)
-> GroupAggregate (cost=4413863.02..4413868.00 rows=276 width=33)
Group Key: location_asset_snapshots.unique_identifuer
-> Sort (cost=4413863.02..4413863.76 rows=296 width=33)
Sort Key: location_asset_snapshots.unique_identifuer
-> Seq Scan on location_asset_snapshots (cost=0.00..4413850.87 rows=296 width=33)
Filter: (NOT (SubPlan 1))
SubPlan 1
-> Materialize (cost=0.00..14344.52 rows=216768 width=25)
-> Seq Scan on location_assets (cost=0.00..11778.68 rows=216768 width=25)
Plan for query 2:
HashSetOp Except (cost=39.88..141.00 rows=3 width=176)
-> Append (cost=39.88..140.88 rows=6 width=176)
-> Subquery Scan on "*SELECT* 1" (cost=39.88..63.02 rows=3 width=94)
-> Hash Join (cost=39.88..62.99 rows=3 width=90)
Hash Cond: (((t1.unique_identifuer)::text = (location_asset_snapshots.unique_identifuer)::text) AND (t1.last_seen = (max(location_asset_snapshots.last_seen))))
-> Seq Scan on location_asset_snapshots t1 (cost=0.00..19.98 rows=598 width=57)
-> Hash (cost=32.63..32.63 rows=483 width=33)
-> HashAggregate (cost=22.97..27.80 rows=483 width=33)
Group Key: location_asset_snapshots.unique_identifuer
-> Seq Scan on location_asset_snapshots (cost=0.00..19.98 rows=598 width=33)
-> Subquery Scan on "*SELECT* 2" (cost=40.30..77.82 rows=3 width=94)
-> Nested Loop (cost=40.30..77.79 rows=3 width=90)
-> Hash Join (cost=39.88..62.99 rows=3 width=90)
Hash Cond: (((t1_1.unique_identifuer)::text = (location_asset_snapshots_1.unique_identifuer)::text) AND (t1_1.last_seen = (max(location_asset_snapshots_1.last_seen))))
-> Seq Scan on location_asset_snapshots t1_1 (cost=0.00..19.98 rows=598 width=57)
-> Hash (cost=32.63..32.63 rows=483 width=33)
-> HashAggregate (cost=22.97..27.80 rows=483 width=33)
Group Key: location_asset_snapshots_1.unique_identifuer
-> Seq Scan on location_asset_snapshots location_asset_snapshots_1 (cost=0.00..19.98 rows=598 width=33)
-> Index Only Scan using for_upsert_unique_identifuer on location_assets (cost=0.42..4.93 rows=1 width=25)
Index Cond: (unique_identifuer = (t1_1.unique_identifuer)::text)
NOT EXISTS
is faster than aNOT IN
(plus it has the added benefit, that the NOT EXISTS doesn't have nasty surprises with NULL values) – a_horse_with_no_name Jan 17 at 19:57