Why Postgresql using sec scan in my query - sql
I have a problem with one sql query - it became run too long.
That problem had place one month ago, before everything was OK.
EXPLAIN tell me that Postgresql using Sec scan, well, problem is clear... But why it using sec scan? DB has indexes for all needed keys.
I have spend a lot of time to fix it, but all attemps have failed :(
Please, help!
There is SQL:
EXPLAIN ANALYZE SELECT
t.id,t.category_id,t.category_root_id,t.title,t.deadtime,
t.active,t.initial_cost,t.remote,t.created,t.work_type,
a.city_id,city.city_name_ru AS city_label, curr.short_name AS currency,
l1.val AS root_category_title, l2.val AS category_title, t.service_tasktotop,
t.service_taskcolor,t.service_tasktime, m.name AS metro,
count(tb.id) AS offers_cnt, t.contact_phone_cnt
FROM tasks AS t
LEFT JOIN tasks_address AS a ON ( a.task_id=t.id )
LEFT JOIN geo.cities AS city ON ( a.city_id=city.id_city )
LEFT JOIN catalog_categories AS r ON ( r.id=t.category_root_id )
LEFT JOIN catalog_categories AS c ON ( c.id=t.category_id)
LEFT JOIN localizations AS l1 ON (l1.lang='ru' AND l1.component='catalog' AND l1.subcomponent='categories' AND l1.var=r.name)
LEFT JOIN localizations AS l2 ON (l2.lang='ru' AND l2.component='catalog' AND l2.subcomponent='categories' AND l2.var=c.name)
LEFT JOIN tasks_bets AS tb ON ( tb.task_id=t.id )
LEFT JOIN paym.currencies AS curr ON ( t.currency_id=curr.id )
LEFT JOIN geo.metro AS m ON ( a.metro_id=m.id )
WHERE t.trust_level > 0
AND (a.region_id IN (1, 0) OR a.region_id IS NULL)
AND (a.country_id IN (1, 0) OR a.country_id IS NULL)
AND t.task_type=1
GROUP BY t.id,t.category_id,t.category_root_id,t.title,t.deadtime,t.active,t.initial_cost,t.remote,t.created,t.work_type, a.city_id, city.city_name_ru, curr.short_name, l1.val, l2.val, t.contact_phone_cnt, t.service_tasktotop,t.service_taskcolor,t.service_tasktime, m.name
ORDER BY
CASE
WHEN t.active=1 THEN
CASE
WHEN t.service_tasktotop > 1392025702 THEN 100
ELSE 150
END
WHEN t.active=2 THEN
CASE
WHEN t.service_tasktotop > 1392025702 THEN 200
ELSE 250
END
WHEN t.active=3 THEN 300
WHEN t.active=4 THEN 400
WHEN t.active=5 THEN 500
WHEN t.active=-1 THEN 600
WHEN t.active=-2 THEN 700
WHEN t.active=-3 THEN 800
WHEN t.active=-4 THEN 900
ELSE 1000
END,
CASE
WHEN t.service_tasktotop>1392025702 THEN t.service_tasktotop
ELSE t.created
END
DESC
LIMIT 30 OFFSET 0
There is EXPLAIN dump:
Limit (cost=17101.17..17101.24 rows=30 width=701) (actual time=248.486..248.497 rows=30 loops=1)
-> Sort (cost=17101.17..17156.12 rows=21982 width=701) (actual time=248.484..248.487 rows=30 loops=1)
Sort Key: (CASE WHEN (t.active = 1) THEN CASE WHEN (t.service_tasktotop > 1392025702) THEN 100 ELSE 150 END WHEN (t.active = 2) THEN CASE WHEN (t.service_tasktotop > 1392025702) THEN 200 ELSE 250 END WHEN (t.active = 3) THEN 300 WHEN (t.active = 4) THEN 400 WHEN (t.active = 5) THEN 500 WHEN (t.active = (-1)) THEN 600 WHEN (t.active = (-2)) THEN 700 WHEN (t.active = (-3)) THEN 800 WHEN (t.active = (-4)) THEN 900 ELSE 1000 END), (CASE WHEN (t.service_tasktotop > 1392025702) THEN t.service_tasktotop ELSE t.created END)
Sort Method: top-N heapsort Memory: 35kB
-> GroupAggregate (cost=14363.65..16451.94 rows=21982 width=701) (actual time=212.801..233.808 rows=6398 loops=1)
-> Sort (cost=14363.65..14418.61 rows=21982 width=701) (actual time=212.777..216.111 rows=18347 loops=1)
Sort Key: t.id, t.category_id, t.category_root_id, t.title, t.deadtime, t.active, t.initial_cost, t.remote, t.created, t.work_type, a.city_id, city.city_name_ru, curr.short_name, l1.val, l2.val, t.contact_phone_cnt, t.service_tasktotop, t.service_taskcolor, t.service_tasktime, m.name
Sort Method: quicksort Memory: 6388kB
-> Hash Left Join (cost=2392.33..5939.31 rows=21982 width=701) (actual time=18.986..64.598 rows=18347 loops=1)
Hash Cond: (a.metro_id = m.id)
-> Hash Left Join (cost=2384.20..5628.92 rows=21982 width=681) (actual time=18.866..57.534 rows=18347 loops=1)
Hash Cond: (t.currency_id = curr.id)
-> Hash Left Join (cost=2383.09..5325.56 rows=21982 width=678) (actual time=18.846..50.126 rows=18347 loops=1)
Hash Cond: (t.id = tb.task_id)
-> Hash Left Join (cost=809.08..2760.18 rows=5935 width=674) (actual time=10.987..32.460 rows=6398 loops=1)
Hash Cond: (a.city_id = city.id_city)
-> Hash Left Join (cost=219.25..2029.39 rows=5935 width=158) (actual time=2.883..20.952 rows=6398 loops=1)
Hash Cond: (t.category_root_id = r.id)
-> Hash Left Join (cost=203.42..1969.65 rows=5935 width=125) (actual time=2.719..18.048 rows=6398 loops=1)
Hash Cond: (t.category_id = c.id)
-> Hash Left Join (cost=187.60..1909.91 rows=5935 width=92) (actual time=2.522..15.021 rows=6398 loops=1)
Hash Cond: (t.id = a.task_id)
Filter: (((a.region_id = ANY ('{1,0}'::integer[])) OR (a.region_id IS NULL)) AND ((a.country_id = ANY ('{1,0}'::integer[])) OR (a.country_id IS NULL)))
-> Seq Scan on tasks t (cost=0.00..1548.06 rows=7337 width=84) (actual time=0.008..6.337 rows=7337 loops=1)
Filter: ((trust_level > 0) AND (task_type = 1))
-> Hash (cost=124.49..124.49 rows=5049 width=18) (actual time=2.505..2.505 rows=5040 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 256kB
-> Seq Scan on tasks_address a (cost=0.00..124.49 rows=5049 width=18) (actual time=0.002..1.201 rows=5040 loops=1)
-> Hash (cost=14.91..14.91 rows=73 width=37) (actual time=0.193..0.193 rows=74 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 5kB
-> Hash Left Join (cost=6.46..14.91 rows=73 width=37) (actual time=0.113..0.168 rows=74 loops=1)
Hash Cond: ((c.name)::text = (l2.var)::text)
-> Seq Scan on catalog_categories c (cost=0.00..7.73 rows=73 width=17) (actual time=0.001..0.017 rows=74 loops=1)
-> Hash (cost=5.42..5.42 rows=84 width=46) (actual time=0.105..0.105 rows=104 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 8kB
-> Seq Scan on localizations l2 (cost=0.00..5.42 rows=84 width=46) (actual time=0.005..0.056 rows=104 loops=1)
Filter: (((lang)::text = 'ru'::text) AND ((component)::text = 'catalog'::text) AND ((subcomponent)::text = 'categories'::text))
-> Hash (cost=14.91..14.91 rows=73 width=37) (actual time=0.155..0.155 rows=74 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 5kB
-> Hash Left Join (cost=6.46..14.91 rows=73 width=37) (actual time=0.085..0.130 rows=74 loops=1)
Hash Cond: ((r.name)::text = (l1.var)::text)
-> Seq Scan on catalog_categories r (cost=0.00..7.73 rows=73 width=17) (actual time=0.002..0.016 rows=74 loops=1)
-> Hash (cost=5.42..5.42 rows=84 width=46) (actual time=0.080..0.080 rows=104 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 8kB
-> Seq Scan on localizations l1 (cost=0.00..5.42 rows=84 width=46) (actual time=0.004..0.046 rows=104 loops=1)
Filter: (((lang)::text = 'ru'::text) AND ((component)::text = 'catalog'::text) AND ((subcomponent)::text = 'categories'::text))
-> Hash (cost=363.26..363.26 rows=18126 width=520) (actual time=8.093..8.093 rows=18126 loops=1)
Buckets: 2048 Batches: 1 Memory Usage: 882kB
-> Seq Scan on cities city (cost=0.00..363.26 rows=18126 width=520) (actual time=0.002..3.748 rows=18126 loops=1)
-> Hash (cost=1364.56..1364.56 rows=16756 width=8) (actual time=7.844..7.844 rows=16757 loops=1)
Buckets: 2048 Batches: 1 Memory Usage: 655kB
-> Seq Scan on tasks_bets tb (cost=0.00..1364.56 rows=16756 width=8) (actual time=0.005..4.180 rows=16757 loops=1)
-> Hash (cost=1.05..1.05 rows=5 width=9) (actual time=0.008..0.008 rows=5 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 1kB
-> Seq Scan on currencies curr (cost=0.00..1.05 rows=5 width=9) (actual time=0.003..0.005 rows=5 loops=1)
-> Hash (cost=5.28..5.28 rows=228 width=28) (actual time=0.112..0.112 rows=228 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 14kB
-> Seq Scan on metro m (cost=0.00..5.28 rows=228 width=28) (actual time=0.004..0.050 rows=228 loops=1)
Total runtime: 248.990 ms
Table:
id serial NOT NULL,
author_id integer DEFAULT 0,
category_id integer DEFAULT 0,
category_root_id integer DEFAULT 0,
title character varying,
description text,
deadtime integer DEFAULT 0,
helper_date integer DEFAULT 0,
active integer DEFAULT 1,
initial_cost integer DEFAULT 0,
conditional integer DEFAULT 0,
remote integer DEFAULT 0,
tariff_id integer DEFAULT 0,
created integer DEFAULT 0,
views integer DEFAULT 0,
accepted_helper_id integer DEFAULT 0,
accept_date integer DEFAULT 0,
auction_bet_id integer DEFAULT 0,
token character varying,
repute2helper text,
repute2author text,
active_dated integer DEFAULT 0,
bot integer,
repute_level integer,
seo_body text,
seo_body_active smallint DEFAULT 0,
service_tasktotop integer DEFAULT 0,
service_taskcolor integer DEFAULT 0,
service_tasktime integer DEFAULT 0,
type_id smallint DEFAULT 1,
partner_id integer NOT NULL DEFAULT 0,
trust_level smallint NOT NULL DEFAULT 1,
trust_date integer NOT NULL DEFAULT 0,
active_cause character varying(1500),
admin_notes text,
currency_id smallint NOT NULL DEFAULT 0,
work_type smallint NOT NULL DEFAULT 0,
helpers_gender smallint NOT NULL DEFAULT 0,
helpers_langs integer[],
service_notifyhelpers integer NOT NULL DEFAULT 0,
contact_phone character varying(50),
contact_email character varying(50),
fastclose smallint NOT NULL DEFAULT 0,
access_code character varying(250),
contact_phone_cnt integer NOT NULL DEFAULT 0,
author_in_task integer NOT NULL DEFAULT 0,
task_type smallint NOT NULL DEFAULT 1
Indexes:
CREATE INDEX tasks_author_in_task_idx
ON tasks
USING btree
(author_in_task );
CREATE INDEX tasks_deadtime_bot_created_active_dated_currency_id_idx
ON tasks
USING btree
(deadtime , bot , created , active_dated , currency_id );
CREATE INDEX tasks_idxs
ON tasks
USING btree
(id , active , category_id , category_root_id , remote , type_id , partner_id , trust_level );
CREATE INDEX tasks_service_tasktotop_service_taskcolor_service_tasktime_idx
ON tasks
USING btree
(service_tasktotop , service_taskcolor , service_tasktime );
CREATE INDEX tasks_task_type_idx
ON tasks
USING btree
(task_type );
i recommend to use explain.depesz.com, with this service it's much easier to see where is the problem.
Here http://explain.depesz.com/s/vHT is your explain, as you can see on stats tab seq scans are not a problem. Only 3.1 % of total runtime duration. On other hand sorting operations take a lot time (67%). Do you really need to sort by so many columns?
Sort Key: t.id, t.category_id, t.category_root_id, t.title, t.deadtime, t.active, t.initial_cost, t.remote, t.created, t.work_type, a.city_id, city.city_name_ru, curr.short_name, l1.val, l2.val, t.contact_phone_cnt, t.service_tasktotop, t.service_taskcolor, t.service_tasktime, m.name
last thing, do you have index on every column which is used with JOINS? look here for my simple example, i'm doing simple left join on table with table itself. First with indexed column, than without index. Look at plans (merge joins vs hash joins) and times. Remember that both table columns used in join should be in some index.
P.S. and always do ANALYZE on table, to be sure if planner has actual statistics!
Table "public.people"
Column | Type | Modifiers
------------+---------+-----------------------------------------------------
id | integer | not null default nextval('people_id_seq'::regclass)
username | text |
department | text |
salary | integer |
deleted | boolean | not null default false
Indexes:
"people_pkey" PRIMARY KEY, btree (id)
"people_department_idx" btree (department)
"people_department_salary_idx" btree (department, salary)
"people_salary_idx" btree (salary)
sebpa=# explain analyze select * from people a left join people b on a.id = b.id where a.salary < 30000;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
Merge Left Join (cost=0.57..2540.51 rows=19995 width=82) (actual time=0.022..19.710 rows=19995 loops=1)
Merge Cond: (a.id = b.id)
-> Index Scan using people_pkey on people a (cost=0.29..1145.29 rows=19995 width=41) (actual time=0.011..6.645 rows=19995 loops=1)
Filter: (salary < 30000)
Rows Removed by Filter: 10005
-> Index Scan using people_pkey on people b (cost=0.29..1070.29 rows=30000 width=41) (actual time=0.008..3.769 rows=19996 loops=1)
Total runtime: 20.969 ms
(7 rows)
sebpa=# alter table people drop constraint people_pkey;
ALTER TABLE
sebpa=# vacuum analyze people;
VACUUM
sebpa=# explain analyze select * from people a left join people b on a.id = b.id where a.salary < 30000;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------
Hash Right Join (cost=1081.94..2829.39 rows=19995 width=82) (actual time=10.767..47.147 rows=19995 loops=1)
Hash Cond: (b.id = a.id)
-> Seq Scan on people b (cost=0.00..581.00 rows=30000 width=41) (actual time=0.001..2.989 rows=30000 loops=1)
-> Hash (cost=656.00..656.00 rows=19995 width=41) (actual time=10.753..10.753 rows=19995 loops=1)
Buckets: 2048 Batches: 2 Memory Usage: 733kB
-> Seq Scan on people a (cost=0.00..656.00 rows=19995 width=41) (actual time=0.007..5.827 rows=19995 loops=1)
Filter: (salary < 30000)
Rows Removed by Filter: 10005
Total runtime: 48.884 ms
OK - so you've run an explain on a complicated query and seen a seq-scan then jumped to conclusions.
Explain output can be tricky to read on a small screen, but there's a nice chap who's built a tool for us. Let's post it to explain.depesz.com
http://explain.depesz.com/s/DTCz
This shows you nicely coloured output. Those sequential scans? Take only milliseconds.
The big time consumer seems to be that sort (151ms by itself). It's sorting 18,000 rows by a lot of fields and uses about 6.4MB of memory to do so.
There's nothing worth concentrating on apart from this sort. There are only three plausible options:
Make sure your work_mem is > 6.4MB for this query (set work_mem=...)
Add an index that matches the fields you want to sort by (might work/might not, but it will be a big index that's expensive to update).
Rewrite the query - use a subquery to filter + group your tasks then join to the other tables. Difficult to say if/how much it will help.
Start with #1 - that'll only take a few minutes to test and is a likely candidate if the query used to be quicker.
Related
Postgres: Slow query when using OR statement in a join query
We run a join query between 2 tables. The query has an OR statement that compares one column from the left table and one column from the right table. The query performance is very low, and we fixed it by changing the OR to UNION. Why is this happening? I'm looking for a detailed explanation or a reference to the documentation that might shed a light on the issue. Query with Or Statment: db1=# explain analyze select count(*) from conversations join agents on conversations.agent_id=agents.id where conversations.id=1 or agents.id = '123'; **Query plan** ---------------------------------------------------------------------------------------------------------------------------------- Finalize Aggregate (cost=**11017.95..11017.96** rows=1 width=8) (actual time=54.088..54.088 rows=1 loops=1) -> Gather (cost=11017.73..11017.94 rows=2 width=8) (actual time=53.945..57.181 rows=3 loops=1) Workers Planned: 2 Workers Launched: 2 -> Partial Aggregate (cost=10017.73..10017.74 rows=1 width=8) (actual time=48.303..48.303 rows=1 loops=3) -> Hash Join (cost=219.26..10016.69 rows=415 width=0) (actual time=5.292..48.287 rows=130 loops=3) Hash Cond: (conversations.agent_id = agents.id) Join Filter: ((conversations.id = 1) OR ((agents.id)::text = '123'::text)) Rows Removed by Join Filter: 80035 -> Parallel Seq Scan on conversations (cost=0.00..9366.95 rows=163995 width=8) (actual time=0.017..14.972 rows=131196 loops=3) -> Hash (cost=143.56..143.56 rows=6056 width=16) (actual time=2.686..2.686 rows=6057 loops=3) Buckets: 8192 Batches: 1 Memory Usage: 353kB -> Seq Scan on agents (cost=0.00..143.56 rows=6056 width=16) (actual time=0.011..1.305 rows=6057 loops=3) Planning time: 0.710 ms Execution time: 57.276 ms (15 rows) Changing the OR to UNION: db1=# explain analyze select count(*) from ( select * from conversations join agents on conversations.agent_id=agents.id where conversations.installation_id=1 union select * from conversations join agents on conversations.agent_id=agents.id where agents.source_id = '123') as subquery; **Query plan:** ---------------------------------------------------------------------------------------------------------------------------------- Aggregate (**cost=1114.31..1114.32** rows=1 width=8) (actual time=8.038..8.038 rows=1 loops=1) -> HashAggregate (cost=1091.90..1101.86 rows=996 width=1437) (actual time=7.783..8.009 rows=390 loops=1) Group Key: conversations.id, conversations.created, conversations.modified, conversations.source_created, conversations.source_id, conversations.installation_id, bra in_conversation.resolution_reason, conversations.solve_time, conversations.agent_id, conversations.submission_reason, conversations.is_marked_as_duplicate, conversations.n um_back_and_forths, conversations.is_closed, conversations.is_solved, conversations.conversation_type, conversations.related_ticket_source_id, conversations.channel, brain_convers ation.last_updated_from_platform, conversations.csat, agents.id, agents.created, agents.modified, agents.name, agents.source_id, organizati on_agent.installation_id, agents.settings -> Append (cost=219.68..1027.16 rows=996 width=1437) (actual time=5.517..6.307 rows=390 loops=1) -> Hash Join (cost=219.68..649.69 rows=931 width=224) (actual time=5.516..6.063 rows=390 loops=1) Hash Cond: (conversations.agent_id = agents.id) -> Index Scan using conversations_installation_id_b3ff5c00 on conversations (cost=0.42..427.98 rows=931 width=154) (actual time=0.039..0.344 rows=879 loops=1) Index Cond: (installation_id = 1) -> Hash (cost=143.56..143.56 rows=6056 width=70) (actual time=5.394..5.394 rows=6057 loops=1) Buckets: 8192 Batches: 1 Memory Usage: 710kB -> Seq Scan on agents (cost=0.00..143.56 rows=6056 width=70) (actual time=0.014..1.938 rows=6057 loops=1) -> Nested Loop (cost=0.70..367.52 rows=65 width=224) (actual time=0.210..0.211 rows=0 loops=1) -> Index Scan using agents_source_id_106c8103_like on agents agents_1 (cost=0.28..8.30 rows=1 width=70) (actual time=0.210..0.210 rows=0 loops=1) Index Cond: ((source_id)::text = '123'::text) -> Index Scan using conversations_agent_id_de76554b on conversations conversations_1 (cost=0.42..358.12 rows=110 width=154) (never executed) Index Cond: (agent_id = agents_1.id) Planning time: 2.024 ms Execution time: 9.367 ms (18 rows)
Yes. or has a way of killing the performance of queries. For this query: select count(*) from conversations c join agents a on c.agent_id = a.id where c.id = 1 or a.id = 123; Note I removed the quotes around 123. It looks like a number so I assume it is. For this query, you want an index on conversations(agent_id). Probably the most effective way to write the query is: select count(*) from ((select 1 from conversations c join agents a on c.agent_id = a.id where c.id = 1 ) union all (select 1 from conversations c join agents a on c.agent_id = a.id where a.id = 123 and c.id <> 1 ) ) ac; Note the use of union all rather than union. The additional where condition eliminates duplicates. This can take advantage of the following indexes: conversations(id, agent_id) agents(id) conversations(agent_id, id)
Postgresql Query Slows Inexplicably with Addition of WHERE Constraint
I have the following PostgreSQL query, which contains a few subqueries. The query runs almost instantly until I add the WHERE lb.type = 'Marketing' constraint, which causes it to take about 3 minutes. I find it inexplicable that the addition of such a simple constraint causes such an extreme slowdown, but I'm guessing it must point to a fundamental flaw in my approach. I'm hoping for assistance on a few fronts: Is my use of subqueries to select the latest records from specific tables appropriate, or could it cause performance issues? What should I be looking for in the execution plan when trying to diagnose issues? How should I go about determining what indexes to create for complex queries such as these? Why could the additional WHERE constraint be causing such a massive slowdown? The table structure is as follows: CREATE TABLE sales.leads ( lead_id integer NOT NULL DEFAULT nextval('sales.leads_lead_id_seq'::regclass), batch_id integer, expired integer NOT NULL DEFAULT 0, closed integer NOT NULL DEFAULT 0, merged integer NOT NULL DEFAULT 0, CONSTRAINT leads_pkey PRIMARY KEY (lead_id) ) CREATE TABLE sales.lead_batches ( batch_id integer NOT NULL DEFAULT nextval('sales.lead_batches_batch_id_seq'::regclass), inserted_datetime timestamp without time zone, type character varying(100) COLLATE pg_catalog."default", uploaded smallint NOT NULL DEFAULT '0'::smallint, CONSTRAINT lead_batches_pkey PRIMARY KEY (batch_id) ) CREATE TABLE sales.lead_results ( lead_result_id integer NOT NULL DEFAULT nextval('sales.lead_results_lead_result_id_seq'::regclass), lead_id integer, assigned_datetime timestamp without time zone NOT NULL, user_id character varying(255) COLLATE pg_catalog."default" NOT NULL, resulted_datetime timestamp without time zone, result character varying(255) COLLATE pg_catalog."default", CONSTRAINT lead_results_pkey PRIMARY KEY (lead_result_id) ) CREATE TABLE sales.personal_details ( lead_id integer, title character varying(50) COLLATE pg_catalog."default", first_name character varying(100) COLLATE pg_catalog."default", surname character varying(255) COLLATE pg_catalog."default", email_address character varying(100) COLLATE pg_catalog."default", updated_date date NOT NULL ) CREATE TABLE sales.users ( user_id character varying(50) COLLATE pg_catalog."default" NOT NULL, surname character varying(255) COLLATE pg_catalog."default", name character varying(255) COLLATE pg_catalog."default" ) Query: SELECT l.*, pd.*, lr.resulted_datetime, u.name FROM sales.leads l INNER JOIN sales.lead_batches lb ON l.batch_id = lb.batch_id LEFT JOIN ( SELECT pd_sub.* FROM sales.personal_details pd_sub INNER JOIN ( SELECT lead_id, MAX(updated_date) AS updated_date FROM sales.personal_details GROUP BY lead_id ) sub ON pd_sub.lead_id = sub.lead_id AND pd_sub.updated_date = sub.updated_date ) pd ON l.lead_id = pd.lead_id LEFT JOIN ( SELECT lr_sub.* FROM sales.lead_results lr_sub INNER JOIN ( SELECT lead_id, MAX(resulted_datetime) AS resulted_datetime FROM sales.lead_results GROUP BY lead_id) sub ON lr_sub.lead_id = sub.lead_id AND lr_sub.resulted_datetime = sub.resulted_datetime ) lr ON l.lead_id = lr.lead_id LEFT JOIN sales.users u ON u.user_id = lr.user_id WHERE lb.type = 'Marketing' AND lb.uploaded = 1 AND l.merged = 0 Execution plan: Nested Loop Left Join (cost=10485.51..17604.18 rows=34 width=158) (actual time=717.862..168709.593 rows=18001 loops=1) Join Filter: (l.lead_id = pd_sub.lead_id) Rows Removed by Join Filter: 687818215 -> Nested Loop Left Join (cost=6487.82..12478.42 rows=34 width=135) (actual time=658.141..64951.950 rows=18001 loops=1) Join Filter: (l.lead_id = lr_sub.lead_id) Rows Removed by Join Filter: 435482960 -> Hash Join (cost=131.01..1816.10 rows=34 width=60) (actual time=1.948..126.067 rows=17998 loops=1) Hash Cond: (l.batch_id = lb.batch_id) -> Seq Scan on leads l (cost=0.00..1273.62 rows=32597 width=44) (actual time=0.032..69.763 rows=32621 loops=1) Filter: (merged = 0) Rows Removed by Filter: 5595 -> Hash (cost=130.96..130.96 rows=4 width=20) (actual time=1.894..1.894 rows=4 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 9kB -> Seq Scan on lead_batches lb (cost=0.00..130.96 rows=4 width=20) (actual time=1.078..1.884 rows=4 loops=1) Filter: (((type)::text = 'Marketing'::text) AND (uploaded = 1)) Rows Removed by Filter: 3866 -> Materialize (cost=6356.81..10661.81 rows=1 width=79) (actual time=0.006..1.362 rows=24197 loops=17998) -> Nested Loop Left Join (cost=6356.81..10661.81 rows=1 width=79) (actual time=96.246..633.701 rows=24197 loops=1) Join Filter: ((u.user_id)::text = (lr_sub.user_id)::text) Rows Removed by Join Filter: 1742184 -> Gather (cost=6356.81..10659.19 rows=1 width=72) (actual time=96.203..202.086 rows=24197 loops=1) Workers Planned: 2 Workers Launched: 2 -> Hash Join (cost=5356.81..9659.09 rows=1 width=72) (actual time=134.595..166.341 rows=8066 loops=3) Hash Cond: ((lr_sub.lead_id = lead_results.lead_id) AND (lr_sub.resulted_datetime = (max(lead_results.resulted_datetime)))) -> Parallel Seq Scan on lead_results lr_sub (cost=0.00..3622.05 rows=44605 width=72) (actual time=0.033..17.333 rows=35684 loops=3) -> Hash (cost=5110.36..5110.36 rows=16430 width=12) (actual time=134.260..134.260 rows=24194 loops=3) Buckets: 32768 Batches: 1 Memory Usage: 1391kB -> HashAggregate (cost=4781.76..4946.06 rows=16430 width=12) (actual time=122.823..129.022 rows=24204 loops=3) Group Key: lead_results.lead_id -> Seq Scan on lead_results (cost=0.00..4246.51 rows=107051 width=12) (actual time=0.020..71.768 rows=107051 loops=3) -> Seq Scan on users u (cost=0.00..1.72 rows=72 width=23) (actual time=0.002..0.007 rows=73 loops=24197) -> Materialize (cost=3997.68..5030.85 rows=187 width=31) (actual time=0.003..2.033 rows=38211 loops=18001) -> Hash Join (cost=3997.68..5029.92 rows=187 width=31) (actual time=52.802..85.774 rows=38211 loops=1) Hash Cond: ((personal_details.lead_id = pd_sub.lead_id) AND ((max(personal_details.updated_date)) = pd_sub.updated_date)) -> HashAggregate (cost=1811.38..2186.06 rows=37468 width=8) (actual time=23.330..35.345 rows=38212 loops=1) Group Key: personal_details.lead_id -> Seq Scan on personal_details (cost=0.00..1623.92 rows=37492 width=8) (actual time=0.014..4.636 rows=38232 loops=1) -> Hash (cost=1623.92..1623.92 rows=37492 width=35) (actual time=29.058..29.058 rows=38211 loops=1) Buckets: 65536 Batches: 1 Memory Usage: 2809kB -> Seq Scan on personal_details pd_sub (cost=0.00..1623.92 rows=37492 width=35) (actual time=0.026..17.231 rows=38232 loops=1) Planning time: 1.966 ms Execution time: 168731.769 ms I have an index on lead_id on all tables, and an additional index on (type, uploaded) in lead_batches. Thanks very much in advance for any assistance! EDIT: The execution plan without the additional WHERE constraint: Hash Left Join (cost=15861.46..17780.37 rows=30972 width=158) (actual time=765.076..844.512 rows=32053 loops=1) Hash Cond: (l.lead_id = pd_sub.lead_id) -> Hash Left Join (cost=10829.21..12630.45 rows=30972 width=135) (actual time=667.460..724.297 rows=32053 loops=1) Hash Cond: (l.lead_id = lr_sub.lead_id) -> Hash Join (cost=167.39..1852.48 rows=30972 width=60) (actual time=2.579..36.683 rows=32050 loops=1) Hash Cond: (l.batch_id = lb.batch_id) -> Seq Scan on leads l (cost=0.00..1273.62 rows=32597 width=44) (actual time=0.034..22.166 rows=32623 loops=1) Filter: (merged = 0) Rows Removed by Filter: 5595 -> Hash (cost=121.40..121.40 rows=3679 width=20) (actual time=2.503..2.503 rows=3679 loops=1) Buckets: 4096 Batches: 1 Memory Usage: 234kB -> Seq Scan on lead_batches lb (cost=0.00..121.40 rows=3679 width=20) (actual time=0.011..1.809 rows=3679 loops=1) Filter: (uploaded = 1) Rows Removed by Filter: 193 -> Hash (cost=10661.81..10661.81 rows=1 width=79) (actual time=664.855..664.855 rows=24197 loops=1) Buckets: 32768 (originally 1024) Batches: 1 (originally 1) Memory Usage: 2821kB -> Nested Loop Left Join (cost=6356.81..10661.81 rows=1 width=79) (actual time=142.634..647.146 rows=24197 loops=1) Join Filter: ((u.user_id)::text = (lr_sub.user_id)::text) Rows Removed by Join Filter: 1742184 -> Gather (cost=6356.81..10659.19 rows=1 width=72) (actual time=142.590..241.913 rows=24197 loops=1) Workers Planned: 2 Workers Launched: 2 -> Hash Join (cost=5356.81..9659.09 rows=1 width=72) (actual time=141.250..171.403 rows=8066 loops=3) Hash Cond: ((lr_sub.lead_id = lead_results.lead_id) AND (lr_sub.resulted_datetime = (max(lead_results.resulted_datetime)))) -> Parallel Seq Scan on lead_results lr_sub (cost=0.00..3622.05 rows=44605 width=72) (actual time=0.027..15.322 rows=35684 loops=3) -> Hash (cost=5110.36..5110.36 rows=16430 width=12) (actual time=140.917..140.917 rows=24194 loops=3) Buckets: 32768 Batches: 1 Memory Usage: 1391kB -> HashAggregate (cost=4781.76..4946.06 rows=16430 width=12) (actual time=127.911..135.076 rows=24204 loops=3) Group Key: lead_results.lead_id -> Seq Scan on lead_results (cost=0.00..4246.51 rows=107051 width=12) (actual time=0.020..74.626 rows=107051 loops=3) -> Seq Scan on users u (cost=0.00..1.72 rows=72 width=23) (actual time=0.002..0.006 rows=73 loops=24197) -> Hash (cost=5029.92..5029.92 rows=187 width=31) (actual time=97.561..97.561 rows=38213 loops=1) Buckets: 65536 (originally 1024) Batches: 1 (originally 1) Memory Usage: 2660kB -> Hash Join (cost=3997.68..5029.92 rows=187 width=31) (actual time=52.712..85.099 rows=38213 loops=1) Hash Cond: ((personal_details.lead_id = pd_sub.lead_id) AND ((max(personal_details.updated_date)) = pd_sub.updated_date)) -> HashAggregate (cost=1811.38..2186.06 rows=37468 width=8) (actual time=23.831..35.015 rows=38214 loops=1) Group Key: personal_details.lead_id -> Seq Scan on personal_details (cost=0.00..1623.92 rows=37492 width=8) (actual time=0.012..4.995 rows=38234 loops=1) -> Hash (cost=1623.92..1623.92 rows=37492 width=35) (actual time=28.468..28.468 rows=38213 loops=1) Buckets: 65536 Batches: 1 Memory Usage: 2809kB -> Seq Scan on personal_details pd_sub (cost=0.00..1623.92 rows=37492 width=35) (actual time=0.024..17.089 rows=38234 loops=1) Planning time: 2.058 ms Execution time: 849.460 ms The execution plan with nested_loops disabled: Hash Left Join (cost=13088.17..17390.71 rows=34 width=158) (actual time=277.646..343.924 rows=18001 loops=1) Hash Cond: (l.lead_id = pd_sub.lead_id) -> Hash Right Join (cost=8055.91..12358.31 rows=34 width=135) (actual time=181.614..238.365 rows=18001 loops=1) Hash Cond: (lr_sub.lead_id = l.lead_id) -> Hash Left Join (cost=6359.43..10661.82 rows=1 width=79) (actual time=156.498..201.533 rows=24197 loops=1) Hash Cond: ((lr_sub.user_id)::text = (u.user_id)::text) -> Gather (cost=6356.81..10659.19 rows=1 width=72) (actual time=156.415..190.934 rows=24197 loops=1) Workers Planned: 2 Workers Launched: 2 -> Hash Join (cost=5356.81..9659.09 rows=1 width=72) (actual time=143.387..178.653 rows=8066 loops=3) Hash Cond: ((lr_sub.lead_id = lead_results.lead_id) AND (lr_sub.resulted_datetime = (max(lead_results.resulted_datetime)))) -> Parallel Seq Scan on lead_results lr_sub (cost=0.00..3622.05 rows=44605 width=72) (actual time=0.036..22.404 rows=35684 loops=3) -> Hash (cost=5110.36..5110.36 rows=16430 width=12) (actual time=143.052..143.052 rows=24194 loops=3) Buckets: 32768 Batches: 1 Memory Usage: 1391kB -> HashAggregate (cost=4781.76..4946.06 rows=16430 width=12) (actual time=131.793..137.760 rows=24204 loops=3) Group Key: lead_results.lead_id -> Seq Scan on lead_results (cost=0.00..4246.51 rows=107051 width=12) (actual time=0.023..78.918 rows=107051 loops=3) -> Hash (cost=1.72..1.72 rows=72 width=23) (actual time=0.061..0.061 rows=73 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 12kB -> Seq Scan on users u (cost=0.00..1.72 rows=72 width=23) (actual time=0.031..0.039 rows=73 loops=1) -> Hash (cost=1696.05..1696.05 rows=34 width=60) (actual time=25.068..25.068 rows=17998 loops=1) Buckets: 32768 (originally 1024) Batches: 1 (originally 1) Memory Usage: 2084kB -> Hash Join (cost=10.96..1696.05 rows=34 width=60) (actual time=0.208..18.630 rows=17998 loops=1) Hash Cond: (l.batch_id = lb.batch_id) -> Seq Scan on leads l (cost=0.00..1273.62 rows=32597 width=44) (actual time=0.043..13.065 rows=32623 loops=1) Filter: (merged = 0) Rows Removed by Filter: 5595 -> Hash (cost=10.91..10.91 rows=4 width=20) (actual time=0.137..0.137 rows=4 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 9kB -> Index Scan using lead_batches_type_idx on lead_batches lb (cost=0.28..10.91 rows=4 width=20) (actual time=0.091..0.129 rows=4 loops=1) Index Cond: ((type)::text = 'Marketing'::text) Filter: (uploaded = 1) -> Hash (cost=5029.92..5029.92 rows=187 width=31) (actual time=96.005..96.005 rows=38213 loops=1) Buckets: 65536 (originally 1024) Batches: 1 (originally 1) Memory Usage: 2660kB -> Hash Join (cost=3997.68..5029.92 rows=187 width=31) (actual time=52.166..84.592 rows=38213 loops=1) Hash Cond: ((personal_details.lead_id = pd_sub.lead_id) AND ((max(personal_details.updated_date)) = pd_sub.updated_date)) -> HashAggregate (cost=1811.38..2186.06 rows=37468 width=8) (actual time=23.785..34.403 rows=38214 loops=1) Group Key: personal_details.lead_id -> Seq Scan on personal_details (cost=0.00..1623.92 rows=37492 width=8) (actual time=0.013..4.680 rows=38234 loops=1) -> Hash (cost=1623.92..1623.92 rows=37492 width=35) (actual time=27.960..27.960 rows=38213 loops=1) Buckets: 65536 Batches: 1 Memory Usage: 2809kB -> Seq Scan on personal_details pd_sub (cost=0.00..1623.92 rows=37492 width=35) (actual time=0.019..15.350 rows=38234 loops=1) Planning time: 2.469 ms Execution time: 346.590 ms
You are basically missing some important indexes here. For testing improvements I've set up the tables myself and tried to fill them with test data with similar distribution as read from the explain plans. My baseline performance was ~160 seconds: https://explain.depesz.com/s/WlKO The first thing I did was creating indexes for the foreign key references (although not all will be necessary): CREATE INDEX idx_personal_details_leads ON sales.personal_details (lead_id); CREATE INDEX idx_leads_batches ON sales.leads (batch_id); CREATE INDEX idx_lead_results_users ON sales.lead_results (user_id); That brought us down to ~112 seconds: https://explain.depesz.com/s/aRcf Now, most of the time get's actually spend on the self-joins (table personal_details using latest updated_date and table lead_results using latest resulted_datetime). Based on this, I came up with the following two indexes: CREATE INDEX idx_personal_details_updated ON sales.personal_details (lead_id, updated_date DESC); CREATE INDEX idx_lead_results_resulted ON sales.lead_results (lead_id, resulted_datetime DESC); ...which then immediately brings us down to ~110 milliseconds: https://explain.depesz.com/s/dDfk Debugging help What has helped me in debugging which indexes where most effective, I first rewrote the query to eliminate any sub-select and instead use a dedicated CTE for each of them: WITH leads_update_latest AS ( SELECT lead_id, MAX(updated_date) AS updated_date FROM sales.personal_details GROUP BY lead_id ), pd AS ( SELECT pd_sub.* FROM sales.personal_details pd_sub INNER JOIN leads_update_latest sub ON (pd_sub.lead_id = sub.lead_id AND pd_sub.updated_date = sub.updated_date) ), leads_result_latest AS ( SELECT lead_id, MAX(resulted_datetime) AS resulted_datetime FROM sales.lead_results GROUP BY lead_id ), lr AS ( SELECT lr_sub.* FROM sales.lead_results lr_sub INNER JOIN leads_result_latest sub ON (lr_sub.lead_id = sub.lead_id AND lr_sub.resulted_datetime = sub.resulted_datetime) ), leads AS ( SELECT l.* FROM sales.leads l INNER JOIN sales.lead_batches lb ON (l.batch_id = lb.batch_id) WHERE lb.type = 'Marketing' AND lb.uploaded = 1 AND l.merged = 0 ) SELECT l.*, pd.*, lr.resulted_datetime, u.name FROM leads l LEFT JOIN pd ON l.lead_id = pd.lead_id LEFT JOIN lr ON l.lead_id = lr.lead_id LEFT JOIN sales.users u ON u.user_id = lr.user_id ; Surprisingly, by rewriting the query alone into CTE's, the PostgreSQL planner was way faster and took just ~2.3 seconds without any of the indexes: https://explain.depesz.com/s/lqzq ...with optimization: FK-indexes down to ~230 milliseconds: https://explain.depesz.com/s/a6wT However, with the other combined indexes, the CTE version degraded: combined reverse indexes up to ~270 milliseconds: https://explain.depesz.com/s/TNNm However, as these combined indexes speed up the original query a lot, they also grow a lot faster than single-column indexes and they are an additional write cost to account for in regards to the DB scalability. As a result, it might make sense to go for a CTE-version performing a bit slower but fast enough to be able omit two additional indexes that the DB has to maintain.
Improve performance on SQL query with Nested Loop - PostgreSQL
I am using PostgreSQL and I have a weird problem with my SQL query. Depending on wich date paramter I'm using. My request doesn't do the same operation. This is my working query : SELECT DISTINCT app.id_application FROM stat sj LEFT OUTER JOIN groupe gp ON gp.id_groupe = sj.id_groupe LEFT OUTER JOIN application app ON app.id_application = gp.id_application WHERE date_stat >= '2016/3/01' AND date_stat <= '2016/3/31' AND ( date_stat = date_gen-1 or (date_gen = '2016/04/01' AND date_stat = '2016/3/31')) AND app.id_application IS NOT NULL This query takes around 2 secondes (which is OKAY for me because I have a lots of rows). When I run EXPLAIN ANALYSE for this query I have this: HashAggregate (cost=375486.95..375493.62 rows=667 width=4) (actual time=2320.541..2320.656 rows=442 loops=1) -> Hash Join (cost=254.02..375478.99 rows=3186 width=4) (actual time=6.144..2271.984 rows=263274 loops=1) Hash Cond: (gp.id_application = app.id_application) -> Hash Join (cost=234.01..375415.17 rows=3186 width=4) (actual time=5.926..2200.671 rows=263274 loops=1) Hash Cond: (sj.id_groupe = gp.id_groupe) -> Seq Scan on stat sj (cost=0.00..375109.47 rows=3186 width=8) (actual time=3.196..2068.357 rows=263274 loops=1) Filter: ((date_stat >= '2016-03-01'::date) AND (date_stat <= '2016-03-31'::date) AND ((date_stat = (date_gen - 1)) OR ((date_gen = '2016-04-01'::date) AND (date_stat = '2016-03-31'::date)))) Rows Removed by Filter: 7199514 -> Hash (cost=133.45..133.45 rows=8045 width=12) (actual time=2.677..2.677 rows=8019 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 345kB -> Seq Scan on groupe gp (cost=0.00..133.45 rows=8045 width=12) (actual time=0.007..1.284 rows=8019 loops=1) -> Hash (cost=11.67..11.67 rows=667 width=4) (actual time=0.206..0.206 rows=692 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 25kB -> Seq Scan on application app (cost=0.00..11.67 rows=667 width=4) (actual time=0.007..0.101 rows=692 loops=1) Filter: (id_application IS NOT NULL) Total runtime: 2320.855 ms Now, When I'm trying the same query for the current month (we are the 6th of April, so I'm trying to get all the application_id of April) with the same query SELECT DISTINCT app.id_application FROM stat sj LEFT OUTER JOIN groupe gp ON gp.id_groupe = sj.id_groupe LEFT OUTER JOIN application app ON app.id_application = gp.id_application WHERE date_stat >= '2016/04/01' AND date_stat <= '2016/04/30' AND ( date_stat = date_gen-1 or ( date_gen = '2016/05/01' AND date_job = '2016/04/30')) AND app.id_application IS NOT NULL This query takes now 120 seconds. So I also ran EXPLAIN ANALYZE on this query and now it doesn't have the same operations: HashAggregate (cost=375363.50..375363.51 rows=1 width=4) (actual time=186716.468..186716.532 rows=490 loops=1) -> Nested Loop (cost=0.00..375363.49 rows=1 width=4) (actual time=1.945..186619.404 rows=118990 loops=1) Join Filter: (gp.id_application = app.id_application) Rows Removed by Join Filter: 82222090 -> Nested Loop (cost=0.00..375343.49 rows=1 width=4) (actual time=1.821..171458.237 rows=118990 loops=1) Join Filter: (sj.id_groupe = gp.id_groupe) Rows Removed by Join Filter: 954061820 -> Seq Scan on stat sj (cost=0.00..375109.47 rows=1 width=8) (actual time=0.235..1964.423 rows=118990 loops=1) Filter: ((date_stat >= '2016-04-01'::date) AND (date_stat <= '2016-04-30'::date) AND ((date_stat = (date_gen - 1)) OR ((date_gen = '2016-05-01'::date) AND (date_stat = '2016-04-30'::date)))) Rows Removed by Filter: 7343798 -> Seq Scan on groupe gp (cost=0.00..133.45 rows=8045 width=12) (actual time=0.002..0.736 rows=8019 loops=118990) -> Seq Scan on application app (cost=0.00..11.67 rows=667 width=4) (actual time=0.003..0.073 rows=692 loops=118990) Filter: (id_application IS NOT NULL) Total runtime: 186716.635 ms So I decided to search where the problem came from by reducing the number of conditions from my query until the performances is acceptable again. So with only this parameter WHERE date_stat >= '2016/04/01' It takes only 1.9secondes (like the first working query) and it's also working with 2 parameters : WHERE date_stat >= '2016/04/01' AND app.id_application IS NOT NULL BUT when I try to add one of those line I have the Nested loop in the Explain AND date_stat <= '2016/04/30' AND ( date_stat = date_gen-1 or ( date_gen = '2016/05/01' AND date_stat = '2016/04/30')) Does someone have any idea where it could come from?
Ok, it looks like there's problem with optimizer estimations. He thiks that for april there will be only 1 row so he choose NESTED LOOP which is very inefficient for big number of rows (118,990 in that case). Perform VACUUM ANALYZE for every table. This will clean up dead tuples and refresh statistics. consider adding index based on dates like CREATE INDEX date_stat_idx ON <table with date_stat> USING btree (date_stat); Rerun the query,
Slow OR statement in postgresql
I currently have a postgresql query that is slow because of an OR statement. It's apparently not using an index because of it. Rewriting this query failed so far. The query: EXPLAIN ANALYZE SELECT a0_.id AS id0 FROM advert a0_ INNER JOIN advertcategory a1_ ON a0_.advert_category_id = a1_.id WHERE a0_.advert_category_id IN ( 1136 ) OR a1_.parent_id IN ( 1136 ) ORDER BY a0_.created_date DESC LIMIT 15; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Limit (cost=0.00..27542.49 rows=15 width=12) (actual time=1.658..50.809 rows=15 loops=1) -> Nested Loop (cost=0.00..1691109.07 rows=921 width=12) (actual time=1.657..50.790 rows=15 loops=1) -> Index Scan Backward using advert_created_date_idx on advert a0_ (cost=0.00..670300.17 rows=353804 width=16) (actual time=0.013..16.449 rows=12405 loops=1) -> Index Scan using advertcategory_pkey on advertcategory a1_ (cost=0.00..2.88 rows=1 width=8) (actual time=0.002..0.002 rows=0 loops=12405) Index Cond: (id = a0_.advert_category_id) Filter: ((a0_.advert_category_id = 1136) OR (parent_id = 1136)) Rows Removed by Filter: 1 Total runtime: 50.860 ms Cause of slowness: Filter: ((a0_.advert_category_id = 1136) OR (parent_id = 1136)) I've tried using INNER JOIN instead of the WHERE statement: EXPLAIN ANALYZE SELECT a0_.id AS id0 FROM advert a0_ INNER JOIN advertcategory a1_ ON a0_.advert_category_id = a1_.id AND ( a0_.advert_category_id IN ( 1136 ) OR a1_.parent_id IN ( 1136 ) ) ORDER BY a0_.created_date DESC LIMIT 15; QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Limit (cost=0.00..27542.49 rows=15 width=12) (actual time=4.667..139.955 rows=15 loops=1) -> Nested Loop (cost=0.00..1691109.07 rows=921 width=12) (actual time=4.666..139.932 rows=15 loops=1) -> Index Scan Backward using advert_created_date_idx on advert a0_ (cost=0.00..670300.17 rows=353804 width=16) (actual time=0.019..100.765 rows=12405 loops=1) -> Index Scan using advertcategory_pkey on advertcategory a1_ (cost=0.00..2.88 rows=1 width=8) (actual time=0.002..0.002 rows=0 loops=12405) Index Cond: (id = a0_.advert_category_id) Filter: ((a0_.advert_category_id = 1136) OR (parent_id = 1136)) Rows Removed by Filter: 1 Total runtime: 140.048 ms The query speeds up when I remove one of the OR criteria. So I've made a UNION to see the results. It's very fast! But I do not consider this as a solution: EXPLAIN ANALYZE (SELECT a0_.id AS id0 FROM advert a0_ INNER JOIN advertcategory a1_ ON a0_.advert_category_id = a1_.id WHERE a0_.advert_category_id IN ( 1136 ) ORDER BY a0_.created_date DESC LIMIT 15) UNION (SELECT a0_.id AS id0 FROM advert a0_ INNER JOIN advertcategory a1_ ON a0_.advert_category_id = a1_.id WHERE a1_.parent_id IN ( 1136 ) ORDER BY a0_.created_date DESC LIMIT 15); QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- HashAggregate (cost=4125.70..4126.00 rows=30 width=12) (actual time=7.945..7.951 rows=15 loops=1) -> Append (cost=1120.82..4125.63 rows=30 width=12) (actual time=6.811..7.929 rows=15 loops=1) -> Subquery Scan on "*SELECT* 1" (cost=1120.82..1121.01 rows=15 width=12) (actual time=6.810..6.840 rows=15 loops=1) -> Limit (cost=1120.82..1120.86 rows=15 width=12) (actual time=6.809..6.825 rows=15 loops=1) -> Sort (cost=1120.82..1121.56 rows=295 width=12) (actual time=6.807..6.813 rows=15 loops=1) Sort Key: a0_.created_date Sort Method: top-N heapsort Memory: 25kB -> Nested Loop (cost=10.59..1113.59 rows=295 width=12) (actual time=1.151..6.639 rows=220 loops=1) -> Index Only Scan using advertcategory_pkey on advertcategory a1_ (cost=0.00..8.27 rows=1 width=4) (actual time=1.030..1.033 rows=1 loops=1) Index Cond: (id = 1136) Heap Fetches: 1 -> Bitmap Heap Scan on advert a0_ (cost=10.59..1102.37 rows=295 width=16) (actual time=0.099..5.287 rows=220 loops=1) Recheck Cond: (advert_category_id = 1136) -> Bitmap Index Scan on idx_54f1f40bd4436821 (cost=0.00..10.51 rows=295 width=0) (actual time=0.073..0.073 rows=220 loops=1) Index Cond: (advert_category_id = 1136) -> Subquery Scan on "*SELECT* 2" (cost=3004.43..3004.62 rows=15 width=12) (actual time=1.072..1.072 rows=0 loops=1) -> Limit (cost=3004.43..3004.47 rows=15 width=12) (actual time=1.071..1.071 rows=0 loops=1) -> Sort (cost=3004.43..3005.99 rows=626 width=12) (actual time=1.069..1.069 rows=0 loops=1) Sort Key: a0_.created_date Sort Method: quicksort Memory: 25kB -> Nested Loop (cost=22.91..2989.07 rows=626 width=12) (actual time=1.056..1.056 rows=0 loops=1) -> Index Scan using idx_d84ab8ea727aca70 on advertcategory a1_ (cost=0.00..8.27 rows=1 width=4) (actual time=1.054..1.054 rows=0 loops=1) Index Cond: (parent_id = 1136) -> Bitmap Heap Scan on advert a0_ (cost=22.91..2972.27 rows=853 width=16) (never executed) Recheck Cond: (advert_category_id = a1_.id) -> Bitmap Index Scan on idx_54f1f40bd4436821 (cost=0.00..22.70 rows=853 width=0) (never executed) Index Cond: (advert_category_id = a1_.id) Total runtime: 8.940 ms (28 rows) Tried reversing the IN statement: EXPLAIN ANALYZE SELECT a0_.id AS id0 FROM advert a0_ INNER JOIN advertcategory a1_ ON a0_.advert_category_id = a1_.id WHERE 1136 IN ( a0_.advert_category_id, a1_.parent_id ) ORDER BY a0_.created_date DESC LIMIT 15; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Limit (cost=0.00..27542.49 rows=15 width=12) (actual time=1.848..62.461 rows=15 loops=1) -> Nested Loop (cost=0.00..1691109.07 rows=921 width=12) (actual time=1.847..62.441 rows=15 loops=1) -> Index Scan Backward using advert_created_date_idx on advert a0_ (cost=0.00..670300.17 rows=353804 width=16) (actual time=0.028..27.316 rows=12405 loops=1) -> Index Scan using advertcategory_pkey on advertcategory a1_ (cost=0.00..2.88 rows=1 width=8) (actual time=0.002..0.002 rows=0 loops=12405) Index Cond: (id = a0_.advert_category_id) Filter: ((1136 = a0_.advert_category_id) OR (1136 = parent_id)) Rows Removed by Filter: 1 Total runtime: 62.506 ms (8 rows) Tried using EXISTS: EXPLAIN ANALYZE SELECT a0_.id AS id0 FROM advert a0_ INNER JOIN advertcategory a1_ ON a0_.advert_category_id = a1_.id WHERE EXISTS(SELECT test.id FROM advert test INNER JOIN advertcategory test_cat ON test_cat.id = test.advert_category_id WHERE test.id = a0_.id AND ( test.advert_category_id IN ( 1136 ) OR test_cat.parent_id IN ( 1136 ) )) ORDER BY a0_.created_date DESC LIMIT 15; QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------------------------------------- Limit (cost=45538.18..45538.22 rows=15 width=12) (actual time=524.654..524.673 rows=15 loops=1) -> Sort (cost=45538.18..45540.48 rows=921 width=12) (actual time=524.651..524.658 rows=15 loops=1) Sort Key: a0_.created_date Sort Method: top-N heapsort Memory: 25kB -> Hash Join (cost=39803.59..45515.58 rows=921 width=12) (actual time=497.362..524.436 rows=220 loops=1) Hash Cond: (a0_.advert_category_id = a1_.id) -> Nested Loop (cost=39786.88..45486.21 rows=921 width=16) (actual time=496.748..523.501 rows=220 loops=1) -> HashAggregate (cost=39786.88..39796.09 rows=921 width=4) (actual time=496.705..496.872 rows=220 loops=1) -> Hash Join (cost=16.71..39784.58 rows=921 width=4) (actual time=1.210..496.294 rows=220 loops=1) Hash Cond: (test.advert_category_id = test_cat.id) Join Filter: ((test.advert_category_id = 1136) OR (test_cat.parent_id = 1136)) Rows Removed by Join Filter: 353584 -> Seq Scan on advert test (cost=0.00..33134.04 rows=353804 width=8) (actual time=0.002..177.953 rows=353804 loops=1) -> Hash (cost=9.65..9.65 rows=565 width=8) (actual time=0.622..0.622 rows=565 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 22kB -> Seq Scan on advertcategory test_cat (cost=0.00..9.65 rows=565 width=8) (actual time=0.005..0.327 rows=565 loops=1) -> Index Scan using advert_pkey on advert a0_ (cost=0.00..6.17 rows=1 width=16) (actual time=0.117..0.118 rows=1 loops=220) Index Cond: (id = test.id) -> Hash (cost=9.65..9.65 rows=565 width=4) (actual time=0.604..0.604 rows=565 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 20kB -> Seq Scan on advertcategory a1_ (cost=0.00..9.65 rows=565 width=4) (actual time=0.010..0.285 rows=565 loops=1) Total runtime: 524.797 ms Advert table (stripped down): 353804 rows Table "public.advert" Column | Type | Modifiers | Storage | Stats target | Description -----------------------------+--------------------------------+-----------------------------------------------------+----------+--------------+------------- id | integer | not null default nextval('advert_id_seq'::regclass) | plain | | advert_category_id | integer | not null | plain | | Indexes: "idx_54f1f40bd4436821" btree (advert_category_id) "advert_created_date_idx" btree (created_date) Foreign-key constraints: "fk_54f1f40bd4436821" FOREIGN KEY (advert_category_id) REFERENCES advertcategory(id) ON DELETE RESTRICT Has OIDs: no Category table (stripped down): 565 rows Table "public.advertcategory" Column | Type | Modifiers -----------+---------+------------------------------------------------------------- id | integer | not null default nextval('advertcategory_id_seq'::regclass) parent_id | integer | active | boolean | not null system | boolean | not null Indexes: "advertcategory_pkey" PRIMARY KEY, btree (id) "idx_d84ab8ea727aca70" btree (parent_id) Foreign-key constraints: "fk_d84ab8ea727aca70" FOREIGN KEY (parent_id) REFERENCES advertcategory(id) ON DELETE RESTRICT Short server config: version -------------------------------------------------------------------------------------------------------------- PostgreSQL 9.2.4 on x86_64-unknown-linux-gnu, compiled by gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-3), 64-bit name | current_setting | source ----------------------------+--------------------+---------------------- shared_buffers | 1800MB | configuration file work_mem | 4MB | configuration file As you can see, none of the proper solutions give speed improvement. Only the UNION solution to split the OR statement improves performance. But I can't use that because this query is being used through my ORM framework with alot of other filter options. Plus if I can do this, why doesn't the optimizer do it? It seems a really simple optimization. Any hints on this? A solution to this small problem would greatly appreciated!
Entirely new approach. Your where condition is on two tables, but that seems unnecessary. The first change would be: where a1_.id = 1136 or a1_.parent_id = 1136 I think the structure you want is a scan on the category table and then fetches from the advert table. To help, you can create an index on advert(advert_category_id, created_date). I would be tempted to write the query by moving the where clause into a subquery. I don't know if this would effect performance: SELECT a0_.id AS id0 FROM advert a0_ INNER JOIN (select ac.* from advertcategory ac where ac.id = 1136 or ac.parent_id = 1136 ) ac ON a0_.advert_category_id = ac.id ORDER BY a0_.created_date DESC LIMIT 15;
"Plus if I can do this, why doesn't the optimizer do it?" -- Because there are all sorts of cases where it's not necessarily valid (due to aggregates in subqueries) or interesting (due to a better index) to do so. The best query plan you'll possibly get is given in Gordon's answer, using union all instead of union to avoid the sort (I take it that a category is never its own parent, eliminating any possibility of dups). Otherwise, note that your query could be rewritten like so: SELECT a0_.id AS id0 FROM advert a0_ INNER JOIN advertcategory a1_ ON a0_.advert_category_id = a1_.id WHERE a1_.id IN ( 1136 ) OR a1_.parent_id IN ( 1136 ) ORDER BY a0_.created_date DESC LIMIT 15; In other words you're filtering based on a criteria from one table, and sort/limit based on another. The way you wrote it precludes you from being able to use a good index, because the planner isn't recognizing that the filter criteria is all from the same table, so it'll nestloop over created_date with a filter like you're currently doing. It's not a bad plan, mind you... It's actually the right thing to do if e.g. 1136 isn't very selective a criteria. By making it explicit that the second table is the one of interest, you may end up with a bitmap heap scan when the category is selective enough, if you've indexes on advertcategory (id) (which you already have if it's the primary key) and on advertcategory (parent_id) (which you probably do not have at the moment). Don't count too much on it, though -- PG doesn't collect correlated column information insofar as I'm aware. Another possibility might be to maintain an array with the aggregate categories (using triggers) in advert directly, and to use a GIST index on it: SELECT a0_.id AS id0 FROM advert a0_ WHERE ARRAY[1136, 1137] && a0_.category_ids -- 1136 OR 1137; use <# for AND ORDER BY a0_.created_date DESC LIMIT 15; It's technically redundant, but it works well to optimize this sort of query (i.e. filters over a nested category tree that yield complicated join criterias)... When PG decides to use it, you'll end up top-n sorting applicable ads. (In older PG versions, the selectivity of && was arbitrary from lack of stats; I vaguely remember reading a changelog whereby 9.1, 9.2 or 9.3 improved things, presumably by using code similar to that used by the tsvector content statistics collector for generic array types. At any rate, be sure to use the latest PG version, and be sure to not rewrite that query using operators that the gin/gist index won't be able to use.)
Optimise the PG query
The query is used very often in the app and is too expensive. What are the things I can do to optimise it and bring the total time to milliseconds (rather than hundreds of ms)? NOTES: removing DISTINCT improves (down to ~460ms), but I need to to get rid of cartesian product :( (yeah, show better way of avoiding it) removing OREDER BY name improves, but not significantly. The query: SELECT DISTINCT properties.* FROM properties JOIN developments ON developments.id = properties.development_id -- Development allocations LEFT JOIN allocation_items AS dev_items ON dev_items.development_id = properties.development_id LEFT JOIN allocations AS dev_allocs ON dev_items.allocation_id = dev_allocs.id -- Group allocations LEFT JOIN properties_property_groupings ppg ON ppg.property_id = properties.id LEFT JOIN property_groupings pg ON pg.id = ppg.property_grouping_id LEFT JOIN allocation_items prop_items ON prop_items.property_grouping_id = pg.id LEFT JOIN allocations prop_allocs ON prop_allocs.id = prop_items.allocation_id WHERE (properties.status <> 'deleted') AND (( properties.status <> 'inactive' AND ( (dev_allocs.receiving_company_id = 175 OR prop_allocs.receiving_company_id = 175) AND developments.status = 'active' ) OR developments.company_id = 175 ) AND EXISTS ( SELECT 1 FROM development_participations dp JOIN participations p ON p.id = dp.participation_id WHERE dp.allowed AND p.user_id = 387 AND p.company_id = 175 AND dp.development_id = properties.development_id LIMIT 1 ) ) ORDER BY properties.name EXPLAIN ANALYZE Unique (cost=72336.86..72517.53 rows=1606 width=4336) (actual time=703.766..710.920 rows=219 loops=1) -> Sort (cost=72336.86..72340.87 rows=1606 width=4336) (actual time=703.765..704.698 rows=5091 loops=1) Sort Key: properties.name, properties.id, properties.status, properties.level, etc etc (all columns) Sort Method: external sort Disk: 1000kB -> Nested Loop Left Join (cost=0.00..69258.84 rows=1606 width=4336) (actual time=25.230..366.489 rows=5091 loops=1) Filter: ((((properties.status)::text <> 'inactive'::text) AND ((dev_allocs.receiving_company_id = 175) OR (prop_allocs.receiving_company_id = 175)) AND ((developments.status)::text = 'active'::text)) OR (developments.company_id = 175)) -> Nested Loop Left Join (cost=0.00..57036.99 rows=41718 width=4355) (actual time=25.122..247.587 rows=99567 loops=1) -> Nested Loop Left Join (cost=0.00..47616.39 rows=21766 width=4355) (actual time=25.111..163.827 rows=39774 loops=1) -> Nested Loop Left Join (cost=0.00..41508.16 rows=21766 width=4355) (actual time=25.101..112.452 rows=39774 loops=1) -> Nested Loop Left Join (cost=0.00..34725.22 rows=21766 width=4351) (actual time=25.087..68.104 rows=19887 loops=1) -> Nested Loop Left Join (cost=0.00..28613.00 rows=21766 width=4351) (actual time=25.076..39.360 rows=19887 loops=1) -> Nested Loop (cost=0.00..27478.54 rows=1147 width=4347) (actual time=25.059..29.966 rows=259 loops=1) -> Index Scan using developments_pkey on developments (cost=0.00..25.17 rows=49 width=15) (actual time=0.048..0.127 rows=48 loops=1) Filter: (((status)::text = 'active'::text) OR (company_id = 175)) -> Index Scan using index_properties_on_development_id on properties (cost=0.00..559.95 rows=26 width=4336) (actual time=0.534..0.618 rows=5 loops=48) Index Cond: (development_id = developments.id) Filter: (((status)::text <> 'deleted'::text) AND (SubPlan 1)) SubPlan 1 -> Limit (cost=0.00..10.00 rows=1 width=0) (actual time=0.011..0.011 rows=0 loops=2420) -> Nested Loop (cost=0.00..10.00 rows=1 width=0) (actual time=0.011..0.011 rows=0 loops=2420) Join Filter: (dp.participation_id = p.id) -> Seq Scan on development_participations dp (cost=0.00..1.71 rows=1 width=4) (actual time=0.004..0.008 rows=1 loops=2420) Filter: (allowed AND (development_id = properties.development_id)) -> Index Scan using index_participations_on_user_id on participations p (cost=0.00..8.27 rows=1 width=4) (actual time=0.001..0.001 rows=1 loops=3148) Index Cond: (user_id = 387) Filter: (company_id = 175) -> Index Scan using index_allocation_items_on_development_id on allocation_items dev_items (cost=0.00..0.70 rows=23 width=8) (actual time=0.003..0.016 rows=77 loops=259) Index Cond: (development_id = properties.development_id) -> Index Scan using allocations_pkey on allocations dev_allocs (cost=0.00..0.27 rows=1 width=8) (actual time=0.001..0.001 rows=1 loops=19887) Index Cond: (dev_items.allocation_id = id) -> Index Scan using index_properties_property_groupings_on_property_id on properties_property_groupings ppg (cost=0.00..0.29 rows=2 width=8) (actual time=0.001..0.001 rows=2 loops=19887) Index Cond: (property_id = properties.id) -> Index Scan using property_groupings_pkey on property_groupings pg (cost=0.00..0.27 rows=1 width=4) (actual time=0.001..0.001 rows=1 loops=39774) Index Cond: (id = ppg.property_grouping_id) -> Index Scan using index_allocation_items_on_property_grouping_id on allocation_items prop_items (cost=0.00..0.36 rows=6 width=8) (actual time=0.001..0.001 rows=2 loops=39774) Index Cond: (property_grouping_id = pg.id) -> Index Scan using allocations_pkey on allocations prop_allocs (cost=0.00..0.27 rows=1 width=8) (actual time=0.001..0.001 rows=1 loops=99567) Index Cond: (id = prop_items.allocation_id) Total runtime: 716.692 ms (39 rows)
Answering my own question. This query has 2 big issues: 6 LEFT JOINs that produce cartesian product (resulting in billion-s of records even on small dataset). DISTINCT that has to sort that billion records dataset. So I had to eliminate those. The way I did it is by replacing JOINs with 2 subqueries (won't provide it here since it should be pretty obvious). As a result, the actual time went from ~700-800ms down to ~45ms which is more or less acceptable.
Most time is spend in the disk sort, you should use RAM by changing work_mem: SET work_mem TO '20MB'; And check EXPLAIN ANALYZE again