Interesting query plan change linked to the LIMIT parameter

From: Yannick Le Guédart <yannick(at)over-blog(dot)com>
To: pgsql-performance(at)postgresql(dot)org
Subject: Interesting query plan change linked to the LIMIT parameter
Date: 2009-01-20 15:45:45
Message-ID: 4f5470ad0901200745tf90a63bid859e9fd267c98af@mail.gmail.com
Views: Raw Message | Whole Thread | Download mbox | Resend email
Thread:
Lists: pgsql-performance

Greetings,

I'm experiencing a strange query plan change on a "simple" request, based on
the LIMIT parameter. I present here two tables, named _article and _comment.
they are part of a much larger database.

It's a tree-like database (making use of the ltree to keep the coherence),
and in our case, an article can have any number of comments (linked by _
article.id = _comment.parent_id), and a _comment can itself have ONE comment
(at most).

The _article table contains 12 millions t-uples, and the _comment table
around 26 millions. The server runs a postgresql 8.3.5 in its 64bits
version.

Here are the tables definition :

-- _article table
>
> CREATE TABLE "ob2"."_article" (
> "id" bigint NOT NULL DEFAULT nextval('element_id_sequence'::regclass),
> "parent_id" bigint,
> "path" ltree,
> "data" text,
> "date_creation" timestamp without time zone NOT NULL DEFAULT now(),
> "date_publishing" timestamp without time zone NOT NULL DEFAULT now(),
> "date_modification" timestamp without time zone NOT NULL DEFAULT now(),
> "counters" hstore,
> "reference" integer NOT NULL DEFAULT
> nextval('_article_reference_seq'::regclass),
> "title" character varying NOT NULL,
> "text" text,
> "blog_id" bigint,
> "user_id" bigint,
> "site_id" bigint,
> "topic_id" bigint,
> "community_id" bigint,
> CONSTRAINT "_article_pkey" PRIMARY KEY (id)
> ) WITHOUT OIDS;
> ALTER TABLE ONLY "ob2"."_article" ALTER COLUMN "path" SET STORAGE PLAIN;
> ALTER TABLE ONLY "ob2"."_article" ALTER COLUMN "title" SET STORAGE PLAIN;
>
> -- Indexes
>
> CREATE UNIQUE INDEX _article_pkey ON _article USING btree (id);
> CREATE INDEX gist_idx_article_path ON _article USING gist (path);
> CREATE INDEX idx_article_blog_id ON _article USING btree (blog_id);
> CREATE INDEX idx_article_community_id ON _article USING btree
> (community_id);
> CREATE INDEX idx_article_date_creation ON _article USING btree
> (date_creation);
> CREATE INDEX idx_article_date_modification ON _article USING btree
> (date_modification);
> CREATE INDEX idx_article_date_publishing ON _article USING btree
> (date_publishing);
> CREATE INDEX idx_article_parent_id ON _article USING btree (parent_id);
> CREATE UNIQUE INDEX idx_article_reference_unique ON _article USING btree
> (reference);
> CREATE INDEX idx_article_site_id ON _article USING btree (site_id);
> CREATE INDEX idx_article_topic_id ON _article USING btree (topic_id);
> CREATE INDEX idx_article_user_id ON _article USING btree (user_id);
>
> -- _comment table
>
> CREATE TABLE "ob2"."_comment" (
> "id" bigint NOT NULL DEFAULT nextval('element_id_sequence'::regclass),
> "parent_id" bigint,
> "path" ltree,
> "data" text,
> "date_creation" timestamp without time zone NOT NULL DEFAULT now(),
> "date_publishing" timestamp without time zone NOT NULL DEFAULT now(),
> "date_modification" timestamp without time zone NOT NULL DEFAULT now(),
> "counters" hstore,
> "reference" integer NOT NULL DEFAULT
> nextval('_comment_reference_seq'::regclass),
> "text" text,
> "article_id" bigint,
> "blog_id" bigint,
> "user_id" bigint,
> "site_id" bigint,
> CONSTRAINT "_comment_pkey" PRIMARY KEY (id)
> ) WITHOUT OIDS;
> ALTER TABLE ONLY "ob2"."_comment" ALTER COLUMN "path" SET STORAGE PLAIN;
>
> -- Indexes
>
> CREATE UNIQUE INDEX _comment_pkey ON _comment USING btree (id);
> CREATE INDEX gist_idx_comment_path ON _comment USING gist (path);
> CREATE INDEX idx_comment_date_creation ON _comment USING btree
> (date_creation);
> CREATE INDEX idx_comment_date_publishing ON _comment USING btree
> (date_publishing);
> CREATE INDEX idx_comment_parent_id ON _comment USING btree (parent_id);
> CREATE INDEX idx_comment_reference ON _comment USING btree (reference);
>

Now I created a function to get simply the comment reply to a given comment
:

CREATE OR REPLACE FUNCTION get_comment_response (BIGINT) RETURNS _comment AS
> $$
> SELECT * FROM _comment WHERE parent_id = $1;
> $$
> STABLE
> COST 1
> LANGUAGE SQL;
>

Ok, now, all is set. I'd like to get with a simple query the comments of a
given article, ordered by publishing date, as well as their replies if they
exists. So I write this request :

SELECT
_comment.id,
(get_comment_response(_comment.id)).id AS r_id
FROM _comment
INNER JOIN _article
ON _article.id = _comment.parent_id
WHERE _comment.parent_id = '17355952'
ORDER BY _comment.date_publishing ASC
OFFSET 0
LIMIT 10;

Results are good, quite fast, BUT, when executing tests I discovered
something very strange. The query was fast for 3+ comments, but very slow
with a limit of 1 or 2 ! Just because the query plan change :

EXPLAIN
> SELECT _comment.id,
> (get_comment_response(_comment.id)).id AS r_id
> FROM _comment
> INNER JOIN _article
> ON _article.id = _comment.parent_id
> WHERE _comment.parent_id = '17355952'
> ORDER BY _comment.id ASC
> OFFSET 0
> LIMIT 1000;
>
> QUERY
> PLAN
>
> ---------------------------------------------------------------------------------------------------------------
> Limit (cost=10261.19..10263.69 rows=1000 width=8)
> -> Sort (cost=10261.19..10281.06 rows=7949 width=8)
> Sort Key: _comment.id
> -> Nested Loop (cost=0.00..9825.35 rows=7949 width=8)
> -> Index Scan using _article_pkey on _article
> (cost=0.00..9.55 rows=1 width=8)
> Index Cond: (id = 17355952::bigint)
> -> Index Scan using idx_comment_parent_id on _comment
> (cost=0.00..9716.44 rows=7949 width=16)
> Index Cond: (_comment.parent_id = 17355952::bigint)
> (8 rows)
>

EXPLAIN
> SELECT _comment.id,
> (get_comment_response(_comment.id)).id AS r_id
> FROM _comment
> INNER JOIN _article
> ON _article.id = _comment.parent_id
> WHERE _comment.parent_id = '17355952'
> ORDER BY _comment.id ASC
> OFFSET 0
> LIMIT 1;
> QUERY
> PLAN
>
> -----------------------------------------------------------------------------------------------------
> Limit (cost=0.00..3588.42 rows=1 width=8)
> -> Nested Loop (cost=0.00..28524312.40 rows=7949 width=8)
> -> Index Scan using _comment_pkey on _comment
> (cost=0.00..28448324.73 rows=7949 width=16)
> Filter: (parent_id = 17355952::bigint)
> -> Index Scan using _article_pkey on _article (cost=0.00..9.55
> rows=1 width=8)
> Index Cond: (_article.id = 17355952::bigint)
> (6 rows)
>

The second query scans the whole comment table which is very dangerous for
production servers.

So did I do something wrong ? Is there a way to handle this issue smoothly ?

Thanks in advance

Yannick

Responses

Browse pgsql-performance by date

  From Date Subject
Next Message David Wilson 2009-01-20 16:28:33 Re: Interesting query plan change linked to the LIMIT parameter
Previous Message Chris 2009-01-19 22:12:04 Re: left join + case - how is it processed?