Postgresql join – too long when no results found

indexjoin;performancepostgresqlpostgresql-performance

I have two tables: controller_monitor and controller_monitor_reading.

controller_monitor

         Column         |            Type             |                            Modifiers
------------------------+-----------------------------+-----------------------------------------------------------------
 id                     | integer                     | not null default nextval('controller_monitor_id_seq'::regclass)
 sid                    | character varying(100)      |
 node_type              | character varying(20)       | not null
 name                   | character varying(255)      | not null
 tag                    | character varying(255)      | not null
 denotation             | character varying(255)      | not null
 alias                  | character varying(80)       |
 controller_id          | integer                     |
 value                  | integer                     |
 silent                 | boolean                     |
 last_reading_timestamp | timestamp without time zone | not null
 payload                | character varying(50)       |
Indexes:
    "controller_monitor_pkey" PRIMARY KEY, btree (id)
    "controller_monitor_sid_key" UNIQUE CONSTRAINT, btree (sid)
    "controller_monitor_tag_key" UNIQUE CONSTRAINT, btree (tag)
    "ix_controller_monitor_id" hash (id)
    "ix_controller_monitor_silent" btree (silent)
    "ix_controller_monitor_tag" hash (tag)
Foreign-key constraints:
    "controller_monitor_controller_id_fkey" FOREIGN KEY (controller_id) REFERENCES controller(id)
Referenced by:
    TABLE "controller_monitor_reading" CONSTRAINT "controller_monitor_reading_monitor_id_fkey" FOREIGN KEY (monitor_id) REFERENCES controller_monitor(id) ON DELETE CASCADE
    TABLE "controller_monitor_reading_p2015_03" CONSTRAINT "controller_monitor_reading_p2015_03_monitor_id_fkey" FOREIGN KEY (monitor_id) REFERENCES controller_monitor(id) ON DELETE CASCADE
    TABLE "controller_monitor_reading_p2015_04" CONSTRAINT "controller_monitor_reading_p2015_04_monitor_id_fkey" FOREIGN KEY (monitor_id) REFERENCES controller_monitor(id) ON DELETE CASCADE
    TABLE "controller_monitor_reading_p2015_05" CONSTRAINT "controller_monitor_reading_p2015_05_monitor_id_fkey" FOREIGN KEY (monitor_id) REFERENCES controller_monitor(id) ON DELETE CASCADE
    TABLE "controller_monitor_reading_p2015_06" CONSTRAINT "controller_monitor_reading_p2015_06_monitor_id_fkey" FOREIGN KEY (monitor_id) REFERENCES controller_monitor(id) ON DELETE CASCADE
    TABLE "controller_monitor_reading_p2015_07" CONSTRAINT "controller_monitor_reading_p2015_07_monitor_id_fkey" FOREIGN KEY (monitor_id) REFERENCES controller_monitor(id) ON DELETE CASCADE
    TABLE "controller_monitor_reading_p2015_08" CONSTRAINT "controller_monitor_reading_p2015_08_monitor_id_fkey" FOREIGN KEY (monitor_id) REFERENCES controller_monitor(id) ON DELETE CASCADE
    TABLE "controller_monitor_reading_p2015_09" CONSTRAINT "controller_monitor_reading_p2015_09_monitor_id_fkey" FOREIGN KEY (monitor_id) REFERENCES controller_monitor(id) ON DELETE CASCADE
    TABLE "controller_monitor_reading_p2015_10" CONSTRAINT "controller_monitor_reading_p2015_10_monitor_id_fkey" FOREIGN KEY (monitor_id) REFERENCES controller_monitor(id) ON DELETE CASCADE
    TABLE "controller_monitor_reading_p2015_11" CONSTRAINT "controller_monitor_reading_p2015_11_monitor_id_fkey" FOREIGN KEY (monitor_id) REFERENCES controller_monitor(id) ON DELETE CASCADE
    TABLE "controller_node_monitor" CONSTRAINT "controller_node_monitor_monitor_id_fkey" FOREIGN KEY (monitor_id) REFERENCES controller_monitor(id) ON DELETE CASCADE

controller_monitor_reading

    Column     |            Type             |                                Modifiers                                | Storage  | Stats target | Description
---------------+-----------------------------+-------------------------------------------------------------------------+----------+--------------+-------------
 id            | integer                     | not null default nextval('controller_monitor_reading_id_seq'::regclass) | plain    |              |
 m_denotation  | character varying(255)      | not null                                                                | extended |              |
 timestamp     | timestamp without time zone | not null                                                                | plain    |              |
 qual          | integer                     | not null                                                                | plain    |              |
 value         | integer                     | not null                                                                | plain    |              |
 silent        | boolean                     |                                                                         | plain    |              |
 silenced_by   | character varying(255)      |                                                                         | extended |              |
 monitor_id    | integer                     | not null                                                                | plain    |              |
 state_id      | integer                     |                                                                         | plain    |              |
 comment       | character varying(140)      |                                                                         | extended |              |
 registered_at | timestamp without time zone | default timezone('utc'::text, now())                                    | plain    |              |
 issue_id      | integer                     |                                                                         | plain    |              |
Indexes:
    "controller_monitor_reading_pkey" PRIMARY KEY, btree (id)
    "ix_controller_monitor_reading_id" hash (monitor_id)
    "ix_controller_monitor_reading_silent" btree (silent)
    "ix_controller_monitor_reading_timestamp" btree ("timestamp")
Foreign-key constraints:
    "controller_monitor_reading_issue_id_fkey" FOREIGN KEY (issue_id) REFERENCES issue(id)
    "controller_monitor_reading_monitor_id_fkey" FOREIGN KEY (monitor_id) REFERENCES controller_monitor(id) ON DELETE CASCADE
    "controller_monitor_reading_state_id_fkey" FOREIGN KEY (state_id) REFERENCES state(id)
Triggers:
    controller_monitor_reading_part_trig BEFORE INSERT ON controller_monitor_reading FOR EACH ROW EXECUTE PROCEDURE controller_monitor_reading_part_trig_func()
Child tables: controller_monitor_reading_p2015_03,
              controller_monitor_reading_p2015_04,
              controller_monitor_reading_p2015_05,
              controller_monitor_reading_p2015_06,
              controller_monitor_reading_p2015_07,
              controller_monitor_reading_p2015_08,
              controller_monitor_reading_p2015_09,
              controller_monitor_reading_p2015_10,
              controller_monitor_reading_p2015_11
Has OIDs: no

There are some monitors that have zero readings, and joins for these tables are much slower
than joins for monitors that do have some readings.

for example, this query (selecting latest reading for some monitor) takes 40ms because
'DPVESELAYA.PP14905.LOSTU' monitor has some readings
(i.e. there are readings with monitor_id that correspond to id of this monitor):

EXPLAIN ANALYZE
SELECT controller_monitor_reading.id, controller_monitor_reading.timestamp,
controller_monitor_reading.value
FROM controller_monitor_reading
JOIN controller_monitor
ON (controller_monitor.id = controller_monitor_reading.monitor_id)
WHERE controller_monitor.tag = 'DPVESELAYA.PP14905.LOSTU'
AND controller_monitor_reading.timestamp < '2015-08-13T13:54:35.139702'::timestamp
ORDER BY controller_monitor_reading.timestamp DESC LIMIT 1                                              QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=1.78..11859.58 rows=1 width=16) (actual time=40.593..40.594 rows=1 loops=1)
   ->  Nested Loop  (cost=1.78..47432.99 rows=4 width=16) (actual time=40.591..40.591 rows=1 loops=1)
         Join Filter: (controller_monitor.id = controller_monitor_reading.monitor_id)
         Rows Removed by Join Filter: 12197
         ->  Merge Append  (cost=1.78..39715.40 rows=513971 width=20) (actual time=0.060..21.255 rows=12198 loops=1)
               Sort Key: controller_monitor_reading."timestamp"
               ->  Index Scan Backward using ix_controller_monitor_reading_timestamp on controller_monitor_reading  (cost=0.28..42.45 rows=673 width=20) (actual time=0.016..0.016 rows=1 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
               ->  Index Scan Backward using controller_monitor_reading_p2015_03_timestamp_idx on controller_monitor_reading_p2015_03  (cost=0.14..44.44 rows=17 width=20) (actual time=0.001..0.001 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
               ->  Index Scan Backward using controller_monitor_reading_p2015_04_timestamp_idx on controller_monitor_reading_p2015_04  (cost=0.14..44.44 rows=17 width=20) (actual time=0.002..0.002 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
               ->  Index Scan Backward using controller_monitor_reading_p2015_05_timestamp_idx on controller_monitor_reading_p2015_05  (cost=0.14..44.44 rows=17 width=20) (actual time=0.001..0.001 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
               ->  Index Scan Backward using controller_monitor_reading_p2015_06_timestamp_idx on controller_monitor_reading_p2015_06  (cost=0.14..44.44 rows=17 width=20) (actual time=0.001..0.001 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
               ->  Index Scan Backward using controller_monitor_reading_p2015_07_timestamp_idx on controller_monitor_reading_p2015_07  (cost=0.42..6254.67 rows=159638 width=20) (actual time=0.010..0.010 rows=1 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
               ->  Index Scan Backward using controller_monitor_reading_p2015_08_timestamp_idx on controller_monitor_reading_p2015_08  (cost=0.42..17526.52 rows=353592 width=20) (actual time=0.026..12.941 rows=12198 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
         ->  Materialize  (cost=0.00..8.02 rows=1 width=4) (actual time=0.000..0.001 rows=1 loops=12198)
               ->  Index Scan using ix_controller_monitor_tag on controller_monitor  (cost=0.00..8.02 rows=1 width=4) (actual time=0.016..0.018 rows=1 loops=1)
                     Index Cond: ((tag)::text = 'DPVESELAYA.PP14905.LOSTU'::text)
 Planning time: 1.398 ms
 Execution time: 40.713 ms
(25 rows)

But the same query with a monitor that doesn't have any readings takes up to 2 seconds!

EXPLAIN ANALYZE
SELECT controller_monitor_reading.id, controller_monitor_reading.timestamp,
controller_monitor_reading.value
FROM controller_monitor
JOIN controller_monitor_reading
ON (controller_monitor.id = controller_monitor_reading.monitor_id)
WHERE controller_monitor.tag = 'DPVESELAYA.PP14905.BOXDOOR'
AND controller_monitor_reading.timestamp < '2015-08-13T13:54:35.139702'::timestamp
ORDER BY controller_monitor_reading.timestamp DESC LIMIT 1                                                                                                                                                                                            QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=1.78..11859.58 rows=1 width=16) (actual time=1604.720..1604.720 rows=0 loops=1)
   ->  Nested Loop  (cost=1.78..47432.99 rows=4 width=16) (actual time=1604.719..1604.719 rows=0 loops=1)
         Join Filter: (controller_monitor.id = controller_monitor_reading.monitor_id)
         Rows Removed by Join Filter: 518537
         ->  Merge Append  (cost=1.78..39715.40 rows=513971 width=20) (actual time=0.080..847.699 rows=518537 loops=1)
               Sort Key: controller_monitor_reading."timestamp"
               ->  Index Scan Backward using ix_controller_monitor_reading_timestamp on controller_monitor_reading  (cost=0.28..42.45 rows=673 width=20) (actual time=0.022..0.442 rows=673 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
               ->  Index Scan Backward using controller_monitor_reading_p2015_03_timestamp_idx on controller_monitor_reading_p2015_03  (cost=0.14..44.44 rows=17 width=20) (actual time=0.002..0.002 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
               ->  Index Scan Backward using controller_monitor_reading_p2015_04_timestamp_idx on controller_monitor_reading_p2015_04  (cost=0.14..44.44 rows=17 width=20) (actual time=0.002..0.002 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
               ->  Index Scan Backward using controller_monitor_reading_p2015_05_timestamp_idx on controller_monitor_reading_p2015_05  (cost=0.14..44.44 rows=17 width=20) (actual time=0.001..0.001 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
               ->  Index Scan Backward using controller_monitor_reading_p2015_06_timestamp_idx on controller_monitor_reading_p2015_06  (cost=0.14..44.44 rows=17 width=20) (actual time=0.002..0.002 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
               ->  Index Scan Backward using controller_monitor_reading_p2015_07_timestamp_idx on controller_monitor_reading_p2015_07  (cost=0.42..6254.67 rows=159638 width=20) (actual time=0.015..108.979 rows=159649 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
               ->  Index Scan Backward using controller_monitor_reading_p2015_08_timestamp_idx on controller_monitor_reading_p2015_08  (cost=0.42..17526.52 rows=353592 width=20) (actual time=0.033..430.380 rows=358215 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
         ->  Materialize  (cost=0.00..8.02 rows=1 width=4) (actual time=0.000..0.001 rows=1 loops=518537)
               ->  Index Scan using ix_controller_monitor_tag on controller_monitor  (cost=0.00..8.02 rows=1 width=4) (actual time=0.018..0.019 rows=1 loops=1)
                     Index Cond: ((tag)::text = 'DPVESELAYA.PP14905.BOXDOOR'::text)
 Planning time: 1.343 ms
 Execution time: 1604.855 ms
(25 rows)

I believe this is because when a join is performed, postgresql tries to find a reading
with monitor_id correspondning to my monitor's id,' finds none and somehow explodes with unnecessary extra scans.

How do I fix it, except for making dummy readings for each and every monitor?

The size of controller_monitor table is about 140k rows. The size of controller_monitor_reading table (including partitioned tables) is about 700k rows, and supposed to be much more over time. My Postgresql version is 9.4.4.

UPDATE

About Vladimir Baranov's suggested answer: I added an index on (monitor_id, timestamp), and tried the suggested query. It sped up the slower query, but slowed down the faster query.

faster query (which was 40ms):


 Nested Loop  (cost=1.78..36.68 rows=1 width=16) (actual time=66.797..66.805 rows=1 loops=1)
   ->  Index Scan using ix_controller_monitor_tag on controller_monitor  (cost=0.00..8.02 rows=1 width=4) (actual time=0.022..0.028 rows=1 loops=1)
         Index Cond: ((tag)::text = 'DPVESELAYA.PP14905.LOSTU'::text)
   ->  Limit  (cost=1.78..28.64 rows=1 width=16) (actual time=66.769..66.770 rows=1 loops=1)
         ->  Merge Append  (cost=1.78..25282.92 rows=941 width=16) (actual time=66.766..66.766 rows=1 loops=1)
               Sort Key: controller_monitor_reading."timestamp"
               ->  Index Scan Backward using ix_monitor_id_timestamp on controller_monitor_reading  (cost=0.28..11.87 rows=2 width=16) (actual time=0.011..0.011 rows=0 loops=1)
                     Index Cond: ((controller_monitor.id = monitor_id) AND ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone))
               ->  Index Scan Backward using controller_monitor_reading_p2015_03_timestamp_idx on controller_monitor_reading_p2015_03  (cost=0.14..44.48 rows=1 width=16) (actual time=0.002..0.002 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
                     Filter: (controller_monitor.id = monitor_id)
               ->  Index Scan Backward using controller_monitor_reading_p2015_04_timestamp_idx on controller_monitor_reading_p2015_04  (cost=0.14..44.48 rows=1 width=16) (actual time=0.002..0.002 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
                     Filter: (controller_monitor.id = monitor_id)
               ->  Index Scan Backward using controller_monitor_reading_p2015_05_timestamp_idx on controller_monitor_reading_p2015_05  (cost=0.14..44.48 rows=1 width=16) (actual time=0.002..0.002 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
                     Filter: (controller_monitor.id = monitor_id)
               ->  Index Scan Backward using controller_monitor_reading_p2015_06_timestamp_idx on controller_monitor_reading_p2015_06  (cost=0.14..44.48 rows=1 width=16) (actual time=0.002..0.002 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
                     Filter: (controller_monitor.id = monitor_id)
               ->  Index Scan Backward using controller_monitor_reading_p2015_07_timestamp_idx on controller_monitor_reading_p2015_07  (cost=0.42..6653.76 rows=902 width=16) (actual time=59.497..59.497 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
                     Filter: (controller_monitor.id = monitor_id)
                     Rows Removed by Filter: 159649
               ->  Index Scan Backward using controller_monitor_reading_p2015_08_timestamp_idx on controller_monitor_reading_p2015_08  (cost=0.42..18410.50 rows=33 width=16) (actual time=7.237..7.237 rows=1 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
                     Filter: (controller_monitor.id = monitor_id)
                     Rows Removed by Filter: 12197
 Planning time: 1.287 ms
 Execution time: 66.954 ms
(30 rows)

slower query (which was 1.6sec):

                                                                                                     QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=1.78..36.68 rows=1 width=16) (actual time=311.481..311.481 rows=0 loops=1)
   ->  Index Scan using ix_controller_monitor_tag on controller_monitor  (cost=0.00..8.02 rows=1 width=4) (actual time=0.022..0.027 rows=1 loops=1)
         Index Cond: ((tag)::text = 'DPVESELAYA.PP14905.BOXDOOR'::text)
   ->  Limit  (cost=1.78..28.64 rows=1 width=16) (actual time=311.447..311.447 rows=0 loops=1)
         ->  Merge Append  (cost=1.78..25282.92 rows=941 width=16) (actual time=311.445..311.445 rows=0 loops=1)
               Sort Key: controller_monitor_reading."timestamp"
               ->  Index Scan Backward using ix_monitor_id_timestamp on controller_monitor_reading  (cost=0.28..11.87 rows=2 width=16) (actual time=0.010..0.010 rows=0 loops=1)
                     Index Cond: ((controller_monitor.id = monitor_id) AND ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone))
               ->  Index Scan Backward using controller_monitor_reading_p2015_03_timestamp_idx on controller_monitor_reading_p2015_03  (cost=0.14..44.48 rows=1 width=16) (actual time=0.001..0.001 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
                     Filter: (controller_monitor.id = monitor_id)
               ->  Index Scan Backward using controller_monitor_reading_p2015_04_timestamp_idx on controller_monitor_reading_p2015_04  (cost=0.14..44.48 rows=1 width=16) (actual time=0.002..0.002 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
                     Filter: (controller_monitor.id = monitor_id)
               ->  Index Scan Backward using controller_monitor_reading_p2015_05_timestamp_idx on controller_monitor_reading_p2015_05  (cost=0.14..44.48 rows=1 width=16) (actual time=0.002..0.002 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
                     Filter: (controller_monitor.id = monitor_id)
               ->  Index Scan Backward using controller_monitor_reading_p2015_06_timestamp_idx on controller_monitor_reading_p2015_06  (cost=0.14..44.48 rows=1 width=16) (actual time=0.001..0.001 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
                     Filter: (controller_monitor.id = monitor_id)
               ->  Index Scan Backward using controller_monitor_reading_p2015_07_timestamp_idx on controller_monitor_reading_p2015_07  (cost=0.42..6653.76 rows=902 width=16) (actual time=61.642..61.642 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
                     Filter: (controller_monitor.id = monitor_id)
                     Rows Removed by Filter: 159649
               ->  Index Scan Backward using controller_monitor_reading_p2015_08_timestamp_idx on controller_monitor_reading_p2015_08  (cost=0.42..18410.50 rows=33 width=16) (actual time=249.768..249.768 rows=0 loops=1)
                     Index Cond: ("timestamp" < '2015-08-13 13:54:35.139702'::timestamp without time zone)
                     Filter: (controller_monitor.id = monitor_id)
                     Rows Removed by Filter: 358215
 Planning time: 1.316 ms
 Execution time: 311.597 ms
(30 rows)

This is already much better, thanks Vladimir! But is there a way to do even better?

Best Answer

I would create an index on (monitor_id, timestamp). It should be enough.

If not, I'd use LATERAL JOIN.

SELECT
    T.id
    ,T.timestamp
    ,T.value
FROM
    controller_monitor
    INNER JOIN LATERAL
    (
        SELECT
            controller_monitor_reading.id
            ,controller_monitor_reading.timestamp
            ,controller_monitor_reading.value
        FROM
            controller_monitor_reading
        WHERE
            controller_monitor.id = controller_monitor_reading.monitor_id
            AND controller_monitor_reading.timestamp < '2015-08-13T13:54:35.139702'::timestamp
        ORDER BY
            controller_monitor_reading.timestamp DESC LIMIT 1
    ) AS T ON true
WHERE
    controller_monitor.tag = 'DPVESELAYA.PP14905.BOXDOOR'

Since controller_monitor.tag is unique, there should be one seek in controller_monitor table plus one seek in controller_monitor_reading table using index on (monitor_id, timestamp).

update

I'm not familiar with partitioned tables and it looks like you have your main table controller_monitor_reading partitioned into controller_monitor_reading_p2015_03, controller_monitor_reading_p2015_04, ... etc. Make sure that index on (monitor_id, timestamp) exists on all of these tables.