# Improve performance of Topic load - ORDER BY RANDOM

**URL:** https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619
**Category:** Bug
**Tags:** slow-sql
**Created:** [2015年二月24日 16:48 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619 "2015-02-24T16:48:29Z")
**Posts on this page:** 20
**Page:** 1

<div class="post-metadata">

### Author: ![DeanMarkTaylor](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/deanmarktaylor/32/102462_2.png) [@DeanMarkTaylor](https://meta.discourse.org/u/DeanMarkTaylor)
#### Post date: [2015年二月24日 16:48 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/1 "2015-02-24T16:48:29Z")

</div>

Continuing the discussion from [Slow sql queries](https://meta.discourse.org/t/slow-sql-queries/16604/10):

> [@sam](#):
>
> order by random over the entire result set is a bad strategy. we should change this to maybe just select N random rows or something

##The Problem  
Clicking between topics is slow - for a few reasons - one of which is the generation of suggested topics.

For me I currently consider this a bug because suggested topics currently adds a ~640ms additional query delay to the initial display of a topic which is not critical to that topics display.

##What’s the impact?  
For me with 40K topics and 430K posts on a 2GB Digital Ocean instance I’m seeing **almost 500ms added to each topic load** just from the “random” query for suggested topics.

##Problem Detail  
Currently the function `TopicQuery::list_suggested_for` generates a list of `random` topics to augment the `unread` and `new` topics listed at the bottom of the each topic page.

The `random` element of this uses `ORDER BY RANDOM` in the SQL query, this is an easy but slow way of getting a random list of rows from the database.

Here is the function:

> <https://github.com/discourse/discourse/blob/738f2cb72c295c63c3581ae69d990344d9b947a8/lib/topic_query.rb#L55-L63>

##The Suggestions Queries  
As an example here are the numbers for a slightly faster than typical load - perhaps due to being cached.

###New Topic Suggestions  
So new topic suggestions query is sorted as follows:

```SQL
ORDER BY CASE WHEN topics.category_id = 79 THEN 0 ELSE 1 END, topics.bumped_at DESC LIMIT 5 

```

**This “new” query takes 120.1 ms**

> **:straight\_ruler: Screen capture of "new" query times (click to expand)**
>
> ![](https://global.discourse-cdn.com/meta/original/3X/e/a/ea383975d179c0d838e78a0e2318f0bb16b7b883.png)

###Random Suggestions

```SQL
ORDER BY CASE WHEN topics.category_id = 79 THEN 0 ELSE 1 END, RANDOM() LIMIT 5 

```

**This “random” query takes 479.3 ms** - this was a quite fast example.

> **:straight\_ruler: Screen capture of "random" query times (click to expand)**
>
> ![](https://global.discourse-cdn.com/meta/original/3X/4/1/4190ca8c381ec951d91d72ce18432c9b42bc3531.png)

##Solution / Alternative to `ORDER BY RANDOM`  
There are several alternatives to `ORDER BY RANDOM`, one of which is this:

- [ORDER BY RAND() - ~jk](http://jan.kneschke.de/projects/mysql/order-by-rand/)

Linked from here:

> <https://stackoverflow.com/questions/4329396/mysql-select-10-random-rows-from-600k-rows-fast>

---

<div class="post-metadata">

### Author: ![codinghorror](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/codinghorror/32/110067_2.png) [@codinghorror](https://meta.discourse.org/u/codinghorror)
#### Post date: [2015年二月24日 16:59 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/2 "2015-02-24T16:59:31Z")

</div>

I believe @sam talked about adding some intermediate cache tables here. It is more difficult for us because we are doing random within a category, not random across all topics.

---

<div class="post-metadata">

### Author: ![riking](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/riking/32/170938_2.png) [@riking](https://meta.discourse.org/u/riking)
#### Post date: [2015年二月24日 22:11 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/3 "2015-02-24T22:11:35Z")

</div>

The proposed solution is something like this:

- have a Redis key named after the category, for example `suggested:support/wordpress` (category.url)
- if we have N suggested topic slots to fill, try to RPOP n times
- if a RPOP returns nil (the list ran out), refill it:
  - Query for a thousand topics

  - LPUSH them all into the list 100 at a time (`LPUSH suggested:support/wordpress 123 54 87 35 78 15 64 79 ...`)

  - Expire the key in 2 days or so

- Continue RPOPing until either we have N unique topic ids or it returns nil again.
- If RPOP returned nil again, and the category was specified, repeat the fill attempt, but use `suggested:@all` instead of a category-specific one
- Pass our topic ID list to Topic.find and then on to the serializer

This is also known as a “tetris random”.

---

<div class="post-metadata">

### Author: ![sam](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sam/32/102149_2.png) [@sam](https://meta.discourse.org/u/sam)
#### Post date: [2015年二月24日 22:13 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/4 "2015-02-24T22:13:25Z")

</div>

That is actually quite efficient and simple, I like it.

---

<div class="post-metadata">

### Author: ![sam](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sam/32/102149_2.png) [@sam](https://meta.discourse.org/u/sam)
#### Post date: [2015年二月25日 06:22 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/5 "2015-02-25T06:22:23Z")

</div>

This is now implemented per @riking’s suggestion

[https://github.com/discourse/discourse/commit/b760d22460ef5eef4c81b4ba4fbf1a8cdd384558](https://github.com/discourse/discourse/commit/b760d22460ef5eef4c81b4ba4fbf1a8cdd384558)

we backfill up to 3000 random items in the list and trigger a background refresh if the list is shorter than 500.

this technique works very efficiently for categories with a large number of topics, if categories are short then we round trip some extra data to redis, but its still more efficient cause we stabalize on issuing the backfill in the background.

---

<div class="post-metadata">

### Author: ![DeanMarkTaylor](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/deanmarktaylor/32/102462_2.png) [@DeanMarkTaylor](https://meta.discourse.org/u/DeanMarkTaylor)
#### Post date: [2015年二月25日 14:24 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/6 "2015-02-25T14:24:18Z")

</div>

Thanks so much @sam - this update is a great improvement!

Here is a little more detail about what I get now.

###Initial Random Backfill

> **Executed in ~64.5ms (click to expand)**
>
> ![](https://global.discourse-cdn.com/meta/original/3X/f/6/f6c48125485a771c6649092e15683c5189944813.png)

###Random Selection

> **Executed in ~6.2ms (click to expand)**
>
> ![](https://global.discourse-cdn.com/meta/original/3X/8/7/87f95ccac1e27c3ffcda6fb488168efbc9bb4b8a.png)

###Total Topic Load Time  
The total topic load time is now far lower at ~390ms to ~450ms

 ![](https://global.discourse-cdn.com/meta/original/3X/b/7/b7d9a619219a0e8d8ea1d7a127010920e906602e.png)  
 ![](https://global.discourse-cdn.com/meta/original/3X/d/a/da3e5a3568baad8355c01b0cf68938743e33d67a.png)

###In-category `ORDER BY topics.bumped_at`  
This is now the slowest query in a topic load - loading somewhere between ~112ms and ~140ms.

> **Executed in ~112ms (click to expand)**
>
> ![](https://global.discourse-cdn.com/meta/original/3X/5/f/5f56af13568d1061d2d1c4b73dd20977b21a8030.png)

###question  
Is there any chance of the `topics.bumped_at` query being cached?

---

<div class="post-metadata">

### Author: ![codinghorror](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/codinghorror/32/110067_2.png) [@codinghorror](https://meta.discourse.org/u/codinghorror)
#### Post date: [2015年二月26日 00:11 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/7 "2015-02-26T00:11:22Z")

</div>

Remind me again what the topic / post / user count is for your Discourse dataset? Visiting /about should be fine.

---

<div class="post-metadata">

### Author: ![DeanMarkTaylor](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/deanmarktaylor/32/102462_2.png) [@DeanMarkTaylor](https://meta.discourse.org/u/DeanMarkTaylor)
#### Post date: [2015年二月26日 00:12 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/8 "2015-02-26T00:12:31Z")

</div>

![](https://global.discourse-cdn.com/meta/original/3X/e/8/e8479d1af30bfcf6e2a2409dd0ddf0a9c7e6572f.png)

---

<div class="post-metadata">

### Author: ![codinghorror](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/codinghorror/32/110067_2.png) [@codinghorror](https://meta.discourse.org/u/codinghorror)
#### Post date: [2015年二月26日 00:17 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/9 "2015-02-26T00:17:25Z")

</div>

Hmm, surprising, does not seem that much higher than other sites we have, though all Discourse sites (that are not imports) cannot be too large as they have not had sufficient years to grow.

I know @sam was testing with a dataset that had 1 million topics (but not so many posts, proportionally) just to experience maximum pain. Surprised it is an issue with 40k..

---

<div class="post-metadata">

### Author: ![DeanMarkTaylor](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/deanmarktaylor/32/102462_2.png) [@DeanMarkTaylor](https://meta.discourse.org/u/DeanMarkTaylor)
#### Post date: [2015年二月26日 00:19 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/10 "2015-02-26T00:19:24Z")

</div>

> [@codinghorror](#):
>
> Surprised it is an issue with 40k

Does the count on the about page include PM’s?

**There are also 101K of PM’s** - but that’s posts right?  
 ![](https://global.discourse-cdn.com/meta/original/3X/8/3/83972515023248bcec44fd660131c72b8be5dc99.png)

---

<div class="post-metadata">

### Author: ![cpradio](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/cpradio/32/4970_2.png) [@cpradio](https://meta.discourse.org/u/cpradio)
#### Post date: [2015年二月26日 00:19 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/11 "2015-02-26T00:19:59Z")

</div>

That’s a lot of PMs…

---

<div class="post-metadata">

### Author: ![codinghorror](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/codinghorror/32/110067_2.png) [@codinghorror](https://meta.discourse.org/u/codinghorror)
#### Post date: [2015年二月26日 00:20 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/12 "2015-02-26T00:20:51Z")

</div>

Why are there 101,000 PMs? Yes that means your topic count is effectively 140k. Still nowhere near a million.

---

<div class="post-metadata">

### Author: ![DeanMarkTaylor](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/deanmarktaylor/32/102462_2.png) [@DeanMarkTaylor](https://meta.discourse.org/u/DeanMarkTaylor)
#### Post date: [2015年二月26日 00:22 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/13 "2015-02-26T00:22:22Z")

</div>

> [@codinghorror](#):
>
> Why are there 101,000 PMs?

Users like their PM’s - over 11 years of users being active.

---

<div class="post-metadata">

### Author: ![sam](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sam/32/102149_2.png) [@sam](https://meta.discourse.org/u/sam)
#### Post date: [2015年二月26日 00:27 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/14 "2015-02-26T00:27:02Z")

</div>

stuff was way worse with a million, on my desktop it was 800ms for that query which is about 2 seconds on digital ocean

---

<div class="post-metadata">

### Author: ![DeanMarkTaylor](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/deanmarktaylor/32/102462_2.png) [@DeanMarkTaylor](https://meta.discourse.org/u/DeanMarkTaylor)
#### Post date: [2015年二月28日 01:36 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/15 "2015-02-28T01:36:49Z")

</div>

> [@DeanMarkTaylor](#):
>
> Is there any chance of the topics.bumped\_at query being cached?

Out of interest in improving the `topics.bumped_at` `list_suggested_for` query I tested adding an index.

###Without Index

> **Executed in ~112ms (click to expand)**
>
> ![](https://global.discourse-cdn.com/meta/original/3X/5/f/5f56af13568d1061d2d1c4b73dd20977b21a8030.png)

> **Executed in ~141.3ms (click to expand)**
>
> ![](https://global.discourse-cdn.com/meta/original/3X/d/b/dbbc33f0ee35ecb734a3db6b31073f00aaba453c.JPG)

###With Index

> **Executed in ~13.2ms (click to expand)**
>
> ![](https://global.discourse-cdn.com/meta/original/3X/4/5/456c0d7946961efb4ebc6c6dd9acbc6bd9462c84.JPG)

> **Executed in ~18.7ms (click to expand)**
>
> ![](https://global.discourse-cdn.com/meta/original/3X/5/c/5cdb1b4a9224a6f16d5994e5be269207d0a4c9fc.JPG)

###index tested

```SQL
CREATE INDEX idx_topic_load_suggested_for
    ON topics
    USING btree
    (deleted_at, visible, archetype COLLATE pg_catalog."default", created_at);

```

###Tested table / index sizes  
`topics` row count: 66,373  
`topics` table size: 29MB  
`idx_topic_load_suggested_for` index size: 3,488KB

###Thoughts and questions  
To me it seems far better to scan only 3.5MB of data instead of 29MB per topic page load considering how frequently it occurs.

This was just a quick pass at what I thought was a matching index for the query - I’m sure someone with more knowledge of the details could do better.

**Is this an index the Discourse Team can get behind and perhaps include?**  
Or is there a more cached solution that’s needed?  
Perhaps just a good stop-gap?

---

<div class="post-metadata">

### Author: ![sam](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sam/32/102149_2.png) [@sam](https://meta.discourse.org/u/sam)
#### Post date: [2015年二月28日 01:54 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/16 "2015-02-28T01:54:35Z")

</div>

Totally open to improving indexing, but in this case I think a filtered index ׳where deleted at is null and visible׳ should work better

---

<div class="post-metadata">

### Author: ![DeanMarkTaylor](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/deanmarktaylor/32/102462_2.png) [@DeanMarkTaylor](https://meta.discourse.org/u/DeanMarkTaylor)
#### Post date: [2015年二月28日 02:06 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/17 "2015-02-28T02:06:48Z")

</div>

I assume you mean this?

```SQL
CREATE INDEX idx_topic_load_not_deleted_and_visible
    ON topics
    USING btree
    (deleted_at, visible)
    WHERE (deleted_at IS NULL AND visible)

```

I tested this and the index doesn’t get used, it still does a Seq Scan of the table.

edit  
I tried the original index with the addition of the `WHERE (deleted_at IS NULL AND visible)` constraint, it had no performance or index size impact as far as I could tell. But it did prevent the table scan.

---

<div class="post-metadata">

### Author: ![riking](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/riking/32/170938_2.png) [@riking](https://meta.discourse.org/u/riking)
#### Post date: [2015年二月28日 02:48 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/18 "2015-02-28T02:48:38Z")

</div>

I think sam meant this:

```sql
CREATE INDEX idx_topic_load_suggested_for
    ON topics
    USING btree
    (archetype COLLATE pg_catalog."default", created_at)
    WHERE (deleted_at IS NULL AND visible);

```

---

<div class="post-metadata">

### Author: ![sam](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sam/32/102149_2.png) [@sam](https://meta.discourse.org/u/sam)
#### Post date: [2015年二月28日 03:01 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/19 "2015-02-28T03:01:55Z")

</div>

> [@DeanMarkTaylor](#):
>
> CREATE INDEX idx\_topic\_load\_suggested\_for  
> ON topics  
> USING btree  
> (deleted\_at, visible, archetype COLLATE pg\_catalog.“default”, created\_at);

By default indexes in postgres are using btree so we don’t need that, also collation should be good so we don’t need that… What I want you to test is:

```plaintext
create index idx_topics_suggested_for
on topics (visible, created_at) 
where archetype <> 'private_message' and deleted_at is null

```

This is a bit odd cause we are ordering on bumped\_at so I would assume

```plaintext
create index idx_topics_suggested_for
on topics (bumped_at) 
where archetype <> 'private_message' and deleted_at is null and visible

```

Should work best, however admins see non-visible topics so perhaps adding it in makes sense.

What I am really struggling here with is why would you be adding deleted\_at to the actual index when the only valid value for it on selection is NULL. We are never ordering on deleted\_at

---

<div class="post-metadata">

### Author: ![DeanMarkTaylor](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/deanmarktaylor/32/102462_2.png) [@DeanMarkTaylor](https://meta.discourse.org/u/DeanMarkTaylor)
#### Post date: [2015年二月28日 20:50 UTC](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619/20 "2015-02-28T20:50:54Z")

</div>

> [@sam](#):
>
> What I want you to test is:

###Best Results  
Yes this gives the best results:

```SQL
create index idx_topics_suggested_for
on topics (visible, created_at) 
where archetype <> 'private_message' and deleted_at is null

```

Index size: 1248KB  
Method: Uses Index scan  
Reduces the query time to: 6.7-5.8ms from 141.3-112ms

###Other Notes  
####Note 1  
In this query the index is not used because of the join reducing the result set before `bumped_at` is used / useful.

```SQL
create index idx_topics_suggested_for
on topics (bumped_at) 
where archetype <> 'private_message' and deleted_at is null and visible

```

Index size: 896KB

####Note 2  
The following index causes “recheck condition” on the `topics` table using bitmap heap scan after initially reducing the result set, causing slower total query time.

```SQL
create index idx_topics_suggested_for
on topics (created_at) 
where archetype <> 'private_message' and deleted_at is null and visible

```

Index size: 896KB

####Note 3

> [@sam](#):
>
> What I am really struggling here with is why would you be adding deleted\_at to the actual index when the only valid value for it on selection is NULL. We are never ordering on deleted\_at

Yeah that would be because I’m used to not using INDEX’s that support WHERE conditions, you don’t need to be ordering on an item for it to help with index lookup performance in those cases.

[下一頁](https://meta.discourse.org/t/improve-performance-of-topic-load-order-by-random/25619.md?page=2)
