Context
We have two tables (not the real tables used in the examples below. They are from a toy DB im using for testing):
- Incidents_2 (Columns of interest are a geom, and a reported_at (int8))
- tmp_points (Columns of interest are a geom, timeframe integer representing days, radius integer in meters)
Each row in the tmp_points table has a location and we are looking for incidents near it, within a timeframe. Each could have a different radius and timeframe.
For my dummy data I have 350000 incidents, and 1500 tmp_points.
I have a gist index on both area columns, and a btree on incidents_2.reported_at
.
The incidents table contains 6 years of data. The tmp_points maximum timeframe is 30 days.
The first query was returning in around 6 seconds on a cold run, and 600ms ish for subsequent. I tried partitioning the incidents table in two partitions. One that would cover the effective range of the query, and one for the rest. This was partitioned on reported_at.
The first query still scan BOTH partitions. The second query scans only the smaller partition for most recent incidents.
explain analyze
select to_timestamp(i.reported_at), i.id, i.description, i.area, tp.point, tp."name", tp.radius
from incidents_2 i
join tmp_points tp
on to_timestamp(i.reported_at) >= now() - (tp.days*2 || 'days')::interval
and ST_Dwithin(i.area, tp.point, tp.radius)
explain analyze
select reported_at, i.id, i.description, i.area, tp.point, tp."name", tp.radius
from incidents_2 i
join tmp_points tp
on i.reported_at > 1583586702
and ST_Dwithin(i.area, tp.point, tp.radius )
Problem
Whilst I am aware that the second query is taking a fixed figure so the planner knows it can knock out a partition, the first query which is actually what I need is not.
Ive tried a few ways of rewriting this but cant think of a way of getting the same result but accessing only one partition. Other than accessing the partition directly.
QUERY PLAN |
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
Nested Loop (cost=0.41..185299.97 rows=51 width=319) (actual time=102.313..662.713 rows=2 loops=1) |
-> Seq Scan on tmp_points tp (cost=0.00..28.33 rows=1333 width=61) (actual time=0.008..0.259 rows=1333 loops=1) |
-> Append (cost=0.41..138.97 rows=2 width=262) (actual time=0.497..0.497 rows=0 loops=1333) |
-> Index Scan using incidents2_old_area_idx on incidents2_old i (cost=0.41..137.65 rows=1 width=262) (actual time=0.479..0.479 rows=0 loops=1333) |
Index Cond: (area && _st_expand((tp.point)::geography, (tp.radius)::double precision)) |
Filter: ((to_timestamp((reported_at)::double precision) >= (now() - ((((tp.days * 2))::text || 'days'::text))::interval)) AND ((tp.point)::geography && _st_expand(area, (tp.radius)::double precision)) AND _st_dwithin(area, (tp.point)::geogra|
Rows Removed by Filter: 90 |
-> Index Scan using incidents2_new_area_idx on incidents2_new i_1 (cost=0.27..1.31 rows=1 width=299) (actual time=0.015..0.015 rows=0 loops=1333) |
Index Cond: (area && _st_expand((tp.point)::geography, (tp.radius)::double precision)) |
Filter: ((to_timestamp((reported_at)::double precision) >= (now() - ((((tp.days * 2))::text || 'days'::text))::interval)) AND ((tp.point)::geography && _st_expand(area, (tp.radius)::double precision)) AND _st_dwithin(area, (tp.point)::geogra|
Rows Removed by Filter: 1 |
Planning Time: 0.717 ms |
Execution Time: 662.747 ms |
My only other thought is to create a materialized view of the query and periodically refresh it. This would enable me to keep sub 50ms responses, but create stale data. I'm negotiating with the business over the freshness of the data but I'd prefer to do this at query time anyway if possible!
UPDATE 16/05 Based on some feedback I have tidied this up a little.
PG Version: 11.2.
Incidents Table
CREATE TABLE public.incidents_tz (
id varchar(255) NOT NULL,
description text NOT NULL,
area geography NULL,
reported_at_tz timestamptz NOT NULL,
CONSTRAINT incidents_tz_pkey PRIMARY KEY (reported_at_tz, id)
)
PARTITION BY RANGE (reported_at_tz);
CREATE INDEX incidents_tz_area_gist_index ON ONLY public.incidents_tz USING gist (area);
CREATE INDEX incidentstz_started_at_index ON ONLY public.incidents_tz USING btree (reported_at_tz);
Tmp points table
CREATE TABLE public.tmp_points (
point geometry NULL,
"name" varchar NULL,
radius int4 NULL,
days int4 NULL
);
CREATE INDEX tmp_points_st_expand_idx ON public.tmp_points USING gist (st_expand(point, (radius)::double precision));
I am now using the example given in the first answer:
explain analyze
SELECT i.reported_at_tz, i.id, i.description, i.area, tp.point, tp."name", tp.radius, tp.days
FROM incidents_tz i
JOIN tmp_points tp
ON i.reported_at_tz >= now() - interval '1 day' * tp.days -- 1 day?
AND ST_Dwithin(i.area, tp.point, tp.radius)
Which still unfortunately results in the plan (which is using both partitions):
UERY PLAN |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
ested Loop (cost=0.41..57673.48 rows=22 width=298) (actual time=0.241..178.554 rows=6111 loops=1) |
-> Seq Scan on tmp_points tp (cost=0.00..27.79 rows=1279 width=61) (actual time=0.007..0.159 rows=1279 loops=1) |
-> Append (cost=0.41..45.05 rows=2 width=238) (actual time=0.094..0.138 rows=5 loops=1279) |
-> Index Scan using incidents_tz_old_area_idx on incidents_tz_old i (cost=0.41..39.30 rows=1 width=245) (never executed) |
Index Cond: (area && _st_expand((tp.point)::geography, (tp.radius)::double precision)) |
Filter: ((reported_at_tz >= (now() - ('1 day'::interval * (tp.days)::double precision))) AND ((tp.point)::geography && _st_expand(area, (tp.radius)::double precision)) AND _st_dwithin(area, (tp.point)::geography, (tp.radius)::double precisio|
-> Index Scan using incidents_tz_new_area_idx on incidents_tz_new i_1 (cost=0.41..5.74 rows=1 width=211) (actual time=0.093..0.136 rows=5 loops=1279) |
Index Cond: (area && _st_expand((tp.point)::geography, (tp.radius)::double precision)) |
Filter: ((reported_at_tz >= (now() - ('1 day'::interval * (tp.days)::double precision))) AND ((tp.point)::geography && _st_expand(area, (tp.radius)::double precision)) AND _st_dwithin(area, (tp.point)::geography, (tp.radius)::double precisio|
Rows Removed by Filter: 12 |
lanning Time: 0.314 ms |
xecution Time: 178.857 ms |
CREATE TABLE
andCREATE INDEX
statements are the source of truth. Verbose description can never make up for it, even when done well like you did. Also, please lead with your version of Postgres.id varchar(255)
... is there a reason for a max length of 255 characters? Because, typically, that's a misunderstanding.