PostgreSQL performance drops when adding limit

When postgreSQL executes the following query it will take about 30ms

select crm_contact_id from crm_contact where crm_contact_id in
(select co.crm_contact_id from crm_contact co join crm_department d on d.crm_department_id=co.crm_department_id where d.crm_company_id=1607)
and owner_id='cf0df836-ddf4-4ecd-8b8e-b602d4f8303c'
order by contact_start_dt

But when adding limit 200 to this query the performance drops, the query takes about 30000ms!!!

select crm_contact_id from crm_contact where crm_contact_id in
(select co.crm_contact_id from crm_contact co join crm_department d on d.crm_department_id=co.crm_department_id where d.crm_company_id=1607)
and owner_id='cf0df836-ddf4-4ecd-8b8e-b602d4f8303c'
order by contact_start_dt
limit 200

Our problem is that servoy adds ‘limit ?’ to certain postgresql queries (it didn’t happen with sybase) and this is the reason why our performance drops dramatically. We also cannot get rid of the in-clause because it is a result of using filter-params.

Can we in one way or another do something that prevents servoy adding the ‘limit’? Or perhaps can we force postgreSQL using another query-plan?

Hi Michel,

michel:
But when adding limit 200 to this query the performance drops, the query takes about 30000ms!!!

How did you measure that ? Have you tried to run it in PgAdmin/psql and time it ?

michel:
Our problem is that servoy adds ‘limit ?’ to certain postgresql queries (it didn’t happen with sybase) and this is the reason why our performance drops dramatically.

In Sybase/MSSQL you wouldn’t see a LIMIT 200 at the end of your query but instead you see SELECT TOP 200 at the beginning of the query. Both do the same thing.
A 30 second difference between the 2 queries doesn’t make much sense. Both queries should have the same query plan, only the fetched result is smaller when using a limit. So overall (with fetching) the second query should be faster so I wonder what really is going on.
I suggest you run both queries in a query tool and analyze the query plan (EXPLAIN ANALYZE ) to rule out it’s the database server.

Hope this helps.

Hello Robert,

This was already timed in PgAdmin. Both queries have a different query plan, that’s the problem. We found out that if we change the query to:

select crm_contact_id from crm_contact where crm_contact_id in
(select distinct co.crm_contact_id from crm_contact co join crm_department d on d.crm_department_id=co.crm_department_id where d.crm_company_id=1607)
and owner_id='cf0df836-ddf4-4ecd-8b8e-b602d4f8303c'
order by contact_start_dt
limit 200

the query plan is changed and it will run in 15ms!!!

By the way: I don’t see the select top 200 in sybase, there is no limitation in the query.

Hi Robert,

You said :

Both queries should have the same query plan, only the fetched result is smaller when using a limit.

FYI : introducing the limit clause in the SQL query changes the query plan completely ! In PgAdmin you can see it nicely in a graphical way.

The Postgres manual says :

The query planner takes LIMIT into account when generating a query plan, so you are very likely to get different plans (yielding different row orders) depending on what you use for LIMIT and OFFSET.

PS. We are migrating a large database from Sybase to Postgresql and noticed a huge performance loss. As we know now due to the LIMIT query plan selection Postgres does…

Regards,

lwjwillemsen:
Hi Robert,

You said :

Both queries should have the same query plan, only the fetched result is smaller when using a limit.

FYI : introducing the limit clause in the SQL query changes the query plan completely ! In PgAdmin you can see it nicely in a graphical way.

The Postgres manual says :

The query planner takes LIMIT into account when generating a query plan, so you are very likely to get different plans (yielding different row orders) depending on what you use for LIMIT and OFFSET.

Correct. But since you are also using an ORDER BY it should handle the whole dataset anyway. Something that is touched on in the next line in the manual after the one you quoted I believe.
In any case I will look into this a little deeper.

Robert,

I was talking about Postgresql query plans and performance and not about the returned data. The returned data is in both cases the same and correct (thanks to the order by, I know).

Regards,

Michel,

Have a look at which indexes are defined and used in both query plans.
I expect you can enforce a better query plan by dropping an index or creating a new one.

Rob

Hi Rob,

I doubt it, don’t know what index to set then. These are the query plans:

with limit clause (30000 ms):
[attachment=1]limit.png[/attachment]

without limit clause (30 ms):
[attachment=0]nolimit.png[/attachment]

Michel,

Just to make sure the statistics are up-to-date you could first run an ANALYZE command for each table involved:

ANALYZE crm_contact
ANALYZE crm_department

If the query is still slow you could try dropping index idx_crm_contact_contact_start_… (could not read the rest)

Of course, when this helps, you need to check if this index is helpful for some other queries.

Rob

rgansevles:
…If the query is still slow you could try dropping index idx_crm_contact_contact_start_…

Yes, that kicks the query plan in the right direction. But, as you already mentioned, we are using that index for other queries.

Michel,

Good it helped, but strange that the index was used.
How is the index defined?

Rob

This is how the index is defined:

create index idx_crm_contact_contact_start_dt on crm_contact (crm_contact_start_dt)

Hi Rob,

The index ‘idx_crm_contact_contact_start_dt’ is (very probable) used because the field crm_contact_start_dt is used in the order by clause.

Regards,

This goes very deep in postgres tuning.
Maybe you get some more help on a postgres forum?

Rob

Hi Rob,

  1. Servoy ships Postgresql as bundled database.
  2. ‘simple’ and ‘common’ sql query is fired to postgresql through Servoy commands (including the limit clause).
  3. Performance drops dramatically compared to Sybase.

=>

Some Servoy action/effort on this (towards Postgresql) is not too much asked IMHO.

Regards,

Hi Michel/Lambert,

Can you provide a textual output of an EXPLAIN ANALYZE of both queries (preferably between [ code][ /code] tags)? It contains more info than the graphical output does in the screenshot.
Also what version of PostgreSQL is this (SELECT version()) ?
And how large is your work_mem (show work_mem)?

Hello Robert,

This is the output of the first (fast) query:

user_data=> explain analyze select crm_contact_id from crm_contact where crm_contact_id in
user_data-> (select co.crm_contact_id from crm_contact co join crm_department d on d.crm_department_id=co.crm_department_id where d.crm_company_id=1607)
user_data-> and owner_id='cf0df836-ddf4-4ecd-8b8e-b602d4f8303c'
user_data-> order by contact_start_dt;
                                                                                  QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=67285.00..68577.07 rows=516827 width=12) (actual time=19.081..19.304 rows=923 loops=1)
   Sort Key: crm_contact.contact_start_dt
   Sort Method:  quicksort  Memory: 53kB
   ->  Nested Loop  (cost=187.80..571.87 rows=516827 width=12) (actual time=7.382..18.091 rows=923 loops=1)
         ->  HashAggregate  (cost=187.80..188.26 rows=46 width=4) (actual time=7.352..7.804 rows=923 loops=1)
               ->  Nested Loop  (cost=0.00..187.69 rows=46 width=4) (actual time=0.127..6.577 rows=923 loops=1)
                     ->  Index Scan using idx_crm_department_crm_company_id on crm_department d  (cost=0.00..8.27 rows=1 width=4) (actual time=0.048..0.050 rows=1 loops=1)
                           Index Cond: (crm_company_id = 1607)
                     ->  Index Scan using idx_crm_contact_crm_department_id on crm_contact co  (cost=0.00..178.84 rows=46 width=8) (actual time=0.074..5.863 rows=923 loops=1)
                           Index Cond: (co.crm_department_id = d.crm_department_id)
         ->  Index Scan using crm_contact_pkey on crm_contact  (cost=0.00..8.33 rows=1 width=12) (actual time=0.009..0.009 rows=1 loops=923)
               Index Cond: (crm_contact.crm_contact_id = co.crm_contact_id)
               Filter: ((crm_contact.owner_id)::text = 'cf0df836-ddf4-4ecd-8b8e-b602d4f8303c'::text)
 Total runtime: 19.655 ms
(14 rows)

and this is the output of the second (slow) query:

user_data=> explain analyze select crm_contact_id from crm_contact where crm_contact_id in
user_data-> (select co.crm_contact_id from crm_contact co join crm_department d on d.crm_department_id=co.crm_department_id where d.crm_company_id=1607)
user_data-> and owner_id='cf0df836-ddf4-4ecd-8b8e-b602d4f8303c'
user_data-> order by contact_start_dt
user_data-> limit 200;
                                                                                   QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=187.74..710.08 rows=200 width=12) (actual time=2451.987..153862.793 rows=200 loops=1)
   ->  Nested Loop Semi Join  (cost=187.74..1349993.47 rows=516827 width=12) (actual time=2451.986..153862.606 rows=200 loops=1)
         Join Filter: (crm_contact.crm_contact_id = co.crm_contact_id)
         ->  Index Scan using idx_crm_contact_contact_start_dt on crm_contact  (cost=0.00..814889.79 rows=516827 width=12) (actual time=0.096..760.033 rows=278253 loops=1)
               Filter: ((owner_id)::text = 'cf0df836-ddf4-4ecd-8b8e-b602d4f8303c'::text)
         ->  Materialize  (cost=187.74..188.20 rows=46 width=4) (actual time=0.000..0.239 rows=923 loops=278253)
               ->  Nested Loop  (cost=0.00..187.69 rows=46 width=4) (actual time=0.254..11.367 rows=923 loops=1)
                     ->  Index Scan using idx_crm_department_crm_company_id on crm_department d  (cost=0.00..8.27 rows=1 width=4) (actual time=0.139..0.140 rows=1 loops=1)
                           Index Cond: (crm_company_id = 1607)
                     ->  Index Scan using idx_crm_contact_crm_department_id on crm_contact co  (cost=0.00..178.84 rows=46 width=8) (actual time=0.111..10.364 rows=923 loops=1)
                           Index Cond: (co.crm_department_id = d.crm_department_id)
 Total runtime: 153863.141 ms
(12 rows)

postgresql version and work_mem:

user_data=> select version();
                           version
-------------------------------------------------------------
 PostgreSQL 8.4.3, compiled by Visual C++ build 1400, 32-bit
(1 row)

user_data=> show work_mem;
 work_mem
----------
 1MB
(1 row)

Hi Michel,

I checked with the IRC channel (#postgresql) on FreeNode and the word is the following:

The planner is faced with a decision between the fast startup plan using indexes for ordering, and the fast-completion plan using sorting.
It has to estimate which is faster based on statistics, and it’s often wrong.
Fixing the query might be easier than addressing the performance issue other ways.

Is this query generated by Servoy or is it a custom one? Because you can simply use the subselect as your main query like so:

SELECT DISTINCT co.crm_contact_id 
FROM crm_contact co JOIN crm_department d ON d.crm_department_id=co.crm_department_id 
WHERE d.crm_company_id=1607 AND co.owner_id='cf0df836-ddf4-4ecd-8b8e-b602d4f8303c'
ORDER BY co.contact_start_dt

Hope this helps.

Hi Robert,

The sub query is ours (parent - child query we defined ourselves in our servoy framework program data). The child table (crm_contact) holds ca. 500,000 rows.

We only can make that filter query persistent within Servoy through the addFoundSetFilterParam command with pkField IN subquery.

Servoy fires that on loadRecords() to postgresql with the (very important) addition limit 200

We discovered ourselves that with adding the distinct clause in the sub query the postgresql query planner takes another route (for the better performance)…

The Sybase query planner took the right route without the distinct clause.

Regards,