9

I am using postgres 9.4.

The messages has the following schema: messages belongs to feed_id, and has posted_at, also messages can have a parent message (in case of replies).

                    Table "public.messages"
            Column            |            Type             | Modifiers
------------------------------+-----------------------------+-----------
 message_id                   | character varying(255)      | not null
 feed_id                      | integer                     |
 parent_id                    | character varying(255)      |
 posted_at                    | timestamp without time zone |
 share_count                  | integer                     |
Indexes:
    "messages_pkey" PRIMARY KEY, btree (message_id)
    "index_messages_on_feed_id_posted_at" btree (feed_id, posted_at DESC NULLS LAST)

I want to return all messages ordered by share_count, but for each parent_id, I only want to return one message. ie, if multiple messages have the same parent_id, then only the latest one (posted_at) is returned. The parent_id can be null, messages with null parent_id should all return.

The query I used is:

WITH filtered_messages AS (SELECT * 
                           FROM messages
                           WHERE feed_id IN (7) 
                           AND (posted_at >= '2015-01-01 04:00:00.000000') 
                           AND (posted_at < '2015-04-28 04:00:00.000000'))
    SELECT *
    FROM (SELECT DISTINCT ON(COALESCE(parent_id, message_id)) parent_id,
                          message_id, 
                          posted_at, 
                          share_count
          FROM filtered_messages
          ORDER BY COALESCE(parent_id, message_id), posted_at DESC NULLS LAST
         ) messages
    ORDER BY share_count DESC NULLS LAST, posted_at DESC NULLS LAST;

Here's the http://sqlfiddle.com/#!15/588e5/1/0, in the SQL Fiddle, I have defined the schema, the exact query, and the expected result.

But the performance of the query is slow once the messages table gets big. I tried add multiple sorting indexes, but it does not seem to use the index. Here's the explain: http://explain.depesz.com/s/Sv2

How can I create a correct index?

  • At a first glance, the ORDER BY in the subquery is totally useless. Furthermore, the linked plan cannot be a result of the posted query - there is no mention of metadata, for example. – dezso Apr 27 '15 at 21:35
  • Your description does not cover the role of feed_id and posted_at and you did not mention metadata at all, which seems to be a JSON type? Please repair your question to make it consistent. You select > 500k rows in the CTE ... How many rows are in the table? What percentage of rows do you typically select in the CTE? What percentage of rows has parent_id IS NULL? Consider the info in the [postgresql-performance] tag for performance questions. – Erwin Brandstetter Apr 28 '15 at 3:25
  • Also important: How many rows for each parent_id? (min / avg / max) – Erwin Brandstetter Apr 28 '15 at 3:42
  • sorry, I was trying to make the question more clear by reducing some of the columns, share_count was actually in hstore metadata. Currently the messages table has 10 mil data, but increasing fast. I am think to separate into partition tables for each feed_id. Since I am only fetching per feed id. the percentage of parent_id null vs not null is about 60% / 40%. a typical fetch is around 1-2% of the table. ( around 100K messages) The performance for 100K is around 1s, but once gets to 500K+ it uses bitmap index and normally takes 10s. – Zhaohan Weng Apr 28 '15 at 15:26
9

Query

This query should be substantially faster in any case:

SELECT parent_id, message_id, posted_at, share_count
FROM   messages
WHERE  feed_id = 7
AND    posted_at >= '2015-01-01 4:0:0'
AND    posted_at <  '2015-04-28 4:0:0'
AND    parent_id IS NULL  -- match index condition
UNION ALL
(
SELECT DISTINCT ON(parent_id)
       parent_id, message_id, posted_at, share_count
FROM   messages
WHERE  feed_id = 7
AND    posted_at >= '2015-01-01 4:0:0'
AND    posted_at <  '2015-04-28 4:0:0'
AND    parent_id IS NOT NULL  -- match index condition
ORDER  BY parent_id, posted_at DESC NULLS LAST
)
ORDER  BY share_count DESC NULLS LAST, posted_at DESC NULLS LAST;
  • The CTE does nothing here that a plain subquery could not deliver also. And a CTE introduces an optimization barrier since it is executed separately and its result is materialized.

  • You have one more subquery-level than you actually need.

  • The expression (COALESCE(parent_id, message_id) is not compatible with a plain index, you would need an index on that expression. But that may not be very useful either, depending on data distribution. Follow my links below for detailed information.

  • Splitting the simple case of parent_id IS NULL into a separate SELECT may or may not deliver the optimum. Especially not, if that's a rare case anyway, in which case a combined query with an index on (COALESCE(parent_id, message_id) may perform better. Other considerations apply ...

Indices

Especially when supported with these indices:

CREATE INDEX messages_idx_null ON messages (
  feed_id
, posted_at DESC NULLS LAST
, share_count DESC NULLS LAST
, parent_id, message_id
)
WHERE parent_id IS NULL;

CREATE INDEX messages_idx_notnull ON messages (
  feed_id
, posted_at DESC NULLS LAST
, share_count DESC NULLS LAST
, parent_id, message_id
)
WHERE parent_id IS NOT NULL;

The two partial indices cover the whole table together and are about the same size together as a single total index.

The last two columns parent_id, message_id only make sense if you get index-only scans out of it. Else remove them from both indices.

SQL Fiddle.

Depending on missing details, DISTINCT ON may or may not be the best query technique for the purpose. Reade detailed explanation here:

And possibly faster alternatives here:

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.