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