ActiveRecord count optimizations (#5478) [deploy]

* Do not load messages in memory, just to count them
* Add Model.estimated_count
* Use Model.estimated_count in task record_db_table_counts
* Use size and estimated_count
* Use table_name so that .estimated_count works with all models
* Add comment to explain the .load.size + .each pattern
This commit is contained in:
rhymes 2020-01-14 18:03:38 +01:00 committed by Molly Struve
parent dba7e6ca60
commit 9b2886979a
10 changed files with 65 additions and 29 deletions

View file

@ -1,6 +1,5 @@
class EmailLogic
attr_reader :open_percentage, :last_email_sent_at,
:days_until_next_email, :articles_to_send
attr_reader :open_percentage, :last_email_sent_at, :days_until_next_email, :articles_to_send
def initialize(user)
@user = user
@ -28,13 +27,19 @@ class EmailLogic
def get_articles_to_send
fresh_date = get_fresh_date
articles = if user_has_followings?
experience_level_rating = (@user.experience_level || 5)
experience_level_rating_min = experience_level_rating - 3.6
experience_level_rating_max = experience_level_rating + 3.6
@user.followed_articles.
where("published_at > ?", fresh_date).
where(published: true, email_digest_eligible: true).
where.not(user_id: @user.id).
where("score > ?", 12).
where("experience_level_rating > ? AND experience_level_rating < ?", (@user.experience_level || 5) - 3.6, (@user.experience_level || 5) + 3.6).
where("experience_level_rating > ? AND experience_level_rating < ?",
experience_level_rating_min, experience_level_rating_max).
order("score DESC").
limit(8)
else
@ -46,7 +51,9 @@ class EmailLogic
order("score DESC").
limit(8)
end
@ready_to_receive_email = false if articles.length < 3
articles
end
@ -62,10 +69,11 @@ class EmailLogic
def get_open_rate
past_sent_emails = @user.email_messages.where(mailer: "DigestMailer#digest_email").limit(10)
# Will stick with 50% open rate if @user has no/not-enough email digest history
return 0.5 if past_sent_emails.length < 10
past_sent_emails_count = past_sent_emails.count
# Will stick with 50% open rate if @user has no/not-enough email digest history
return 0.5 if past_sent_emails_count < 10
past_opened_emails_count = past_sent_emails.where("opened_at IS NOT NULL").count
past_opened_emails_count / past_sent_emails_count
end

View file

@ -1,3 +1,27 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
QUERY_ESTIMATED_COUNT = <<~SQL.squish.freeze
SELECT (
(reltuples / GREATEST(relpages, 1)) *
(pg_relation_size($1) / (GREATEST(current_setting('block_size')::integer, 1)))
)::bigint AS count
FROM pg_class WHERE relname = $2;
SQL
# Computes an estimated count of the number of rows using stats collected by VACUUM
# inspired by <https://www.citusdata.com/blog/2016/10/12/count-performance/#dup_counts_estimated_full>
# and <https://stackoverflow.com/a/48391562/4186181>
def self.estimated_count
query_name = "SQL COUNT ESTIMATE: #{table_name}"
table_name_attr = ActiveRecord::Relation::QueryAttribute.new(
"relname", table_name, ActiveRecord::Type::String.new
)
result = connection.exec_query(
QUERY_ESTIMATED_COUNT, query_name, [table_name_attr, table_name_attr]
)
result.first["count"]
end
end

View file

@ -1,7 +1,11 @@
<div class="row">
<div class="col-12">
<h2>Notes</h2>
<% if @user.notes.count > 0 %>
<%# notes are going to be iterated upon in the following line,
thus we can preload them, to avoid a second SELECT COUNT(*)
that would instead happen when ActiveRecord calls .each on a collection #%>
<% if @user.notes.load.size > 0 %>
<% @user.notes.each do |note| %>
<p><em><%= note.created_at.strftime("%d %B %Y %H:%M UTC") %> by <%= User.find(note.author_id).username if note.author_id.present? %></em> -
<% if !note.reason.blank? %><strong><%= note.reason %>:</strong>

View file

@ -28,7 +28,7 @@
<p>
<b style="font-size:1.3em">All negative reactions are private.</b>
</p>
<hr/>
<hr>
<p>
Use <b>Thumbsdown</b> to move this content "down" for any reason (quality, usefulness, etc.).
</p>
@ -53,13 +53,13 @@
<% end %>
<%= f.hidden_field :article_id, value: @moderatable.id %>
<% if current_user.any_admin? || @tag_moderator_tags.any? { |tag| @moderatable.tag_list.include?(tag.name) } %>
<div class="tag-mod-addition-radio" >
<div class="tag-mod-addition-radio">
<%= f.radio_button :adjustment_type, "removal", required: true %>
<%= f.label :adjustment_type, "Remove", value: "removal" %>
</div>
<% end %>
<% if @moderatable.tag_list.count < 4 %>
<div class="tag-mod-addition-radio" >
<% if @moderatable.tag_list.size < 4 %>
<div class="tag-mod-addition-radio">
<%= f.radio_button :adjustment_type, "addition", required: true %>
<%= f.label :adjustment_type, "Add", value: "addition" %>
</div>

View file

@ -10,7 +10,7 @@
<h1>Partner With <img src="<%= asset_path "rainbowdev.svg" %>" /> </h1>
<h3>🚀 Reach</h3>
<p>
DEV serves millions of unique visitors per month. This highly-targeted developer audience is comprised of registered members (<%= User.count %>) and visitors from the open web — many of whom visit DEV as part of their daily routine. You may be interested in the <a href="https://www.similarweb.com/website/dev.to">public analytics available on SimilarWeb</a>.
DEV serves millions of unique visitors per month. This highly-targeted developer audience is comprised of registered members (<%= User.estimated_count %>) and visitors from the open web — many of whom visit DEV as part of their daily routine. You may be interested in the <a href="https://www.similarweb.com/website/dev.to">public analytics available on SimilarWeb</a>.
</p>
<h3>❤️ Community</h3>
<p>

View file

@ -3,7 +3,7 @@
<img class="rainbowdevimage" src="<%= asset_path "rainbowdev.svg" %>" alt="rainbow DEV logo" />
<h2>
<a href="/">DEV</a> is a community of <br /><%= number_with_delimiter User.count %> amazing humans who code.
<a href="/">DEV</a> is a community of <br /><%= number_with_delimiter User.estimated_count %> amazing humans who code.
</h2>
<h3>

View file

@ -53,7 +53,7 @@
<% end %>
<h2 style="color: #ff0000;">Danger Zone</h2>
<% if @user.identities.count == 2 %>
<% if @user.identities.size == 2 %>
<h3>Remove OAuth Associations</h3>
<h4 style="font-weight:400;">
You can remove one of your authentication methods. We'll still need one to authenticate you.

View file

@ -113,19 +113,11 @@ task fix_credits_count_cache: :environment do
end
task record_db_table_counts: :environment do
table_names = %w[users articles organizations comments podcasts classified_listings page_views]
table_names.each do |table_name|
estimate = ActiveRecord::Base.connection.execute("SELECT reltuples::bigint AS estimate FROM pg_class where relname='#{table_name}'").first["estimate"]
Rails.logger.info(
"db_table_size",
table_info: {
table_name: table_name,
table_size: estimate
},
)
DataDogStatsClient.gauge(
"postgres.db_table_size", estimate, tags: { table_name: table_name }
)
models = [User, Article, Organization, Comment, Podcast, ClassifiedListing, PageView]
models.each do |model|
estimate = model.estimated_count
Rails.logger.info("db_table_size", table_info: { table_name: model.table_name, table_size: estimate })
DataDogStatsClient.gauge("postgres.db_table_size", estimate, tags: { table_name: model.table_name })
end
end

View file

@ -3,8 +3,6 @@ require "rails_helper"
RSpec.describe EmailLogic, type: :labor do
let(:user) { create(:user) }
# TODO: improve this test suite, and improve it's speed
describe "#analyze" do
context "when user is brand new with no-follow" do
it "returns 0.5 for open_percentage" do

View file

@ -0,0 +1,10 @@
require "rails_helper"
# ApplicationRecord is an abstract class, tests will use one of the core models
RSpec.describe ApplicationRecord, type: :model do
describe ".estimated_count" do
it "does not raise errors if there are no rows" do
expect { User.estimated_count }.not_to raise_error
end
end
end