class AddTagsToNotes < ActiveRecord::Migration
def change
add_column :notes, :tags, :string, array: true
end
end
SELECT notes.*
FROM notes
WHERE 'useful' = ANY(notes.tags)
SELECT notes.*
FROM notes
WHERE notes.tags @> '{useful}'
class AddTagsToNotes < ActiveRecord::Migration
def change
add_index :notes, :tags, using: :gin
end
end
First Run - 370 ms
Nested Loop -> Bitmap Heap Scan on post_tags Recheck Cond: (tag_id = 1) -> Bitmap Index Scan on index_post_tags_on_tag_id Index Cond: (tag_id = 1) -> Index Scan using posts_pkey on posts Index Cond: (id = post_tags.post_id) Total runtime: 370.139 ms
Subsequent calls - 13 ms
Nested Loop -> Bitmap Heap Scan on post_tags Recheck Cond: (tag_id = 2) -> Bitmap Index Scan on index_post_tags_on_tag_id Index Cond: (tag_id = 2) -> Index Scan using posts_pkey on posts Index Cond: (id = post_tags.post_id) Total runtime: 13.511 ms
First run: 503 ms
Aggregate -> Seq Scan on posts Filter: ('Tag #1'::text = ANY ((array_tags)::text[])) Rows Removed by Filter: 98989 Total runtime: 503.517 ms
Subsequent runs: 500 ms
Aggregate -> Seq Scan on posts Filter: ('Tag #1'::text = ANY ((array_tags)::text[])) Rows Removed by Filter: 98989 Total runtime: 499.618 ms
First run - Pre index: 536 ms
Aggregate -> Seq Scan on posts Filter: (array_tags @> '{"Tag #1"}'::character varying[]) Rows Removed by Filter: 98989 Total runtime: 536.183 msFirst run - Post index: 8ms
Aggregate -> Bitmap Heap Scan on posts Recheck Cond: (gin_tags @> '{"Tag #1"}'::character varying[]) -> Bitmap Index Scan on index_posts_on_gin_tags Index Cond: (gin_tags @> '{"Tag #1"}'::character varying[]) Total runtime: 8.056 ms
Method | Run 1 | Run 2 |
---|---|---|
has_many | 370 ms | 13 ms |
Array: ANY | 503 ms | 500 ms |
Array: @< (no index) | 499 ms | 500 ms |
Array: @< (indexed) | 8 ms | 5 ms |
enable_extension
which will allow you to enable an extension via a migrationenable_extension 'hstore'
will enable hstore in your database
class CreateMembers < ActiveRecord::Migration
def change
enable_extension 'hstore' # Let's enable Hstore
create_table :members do |t|
t.string :username
t.string :type
t.hstore :data # We'll store all our attributes in a column
# called data
t.timestamps
end
end
end
module HstoreAccessor
def hstore_accessor(hstore_attribute, *keys)
Array(keys).flatten.each do |key|
define_method("#{key}=") do |value|
send("#{hstore_attribute}=", (send(hstore_attribute) || {}).merge(key.to_s => value))
send("#{hstore_attribute}_will_change!")
end
define_method(key) do
send(hstore_attribute) && send(hstore_attribute)[key.to_s]
end
end
end
end
end
ActiveRecord::Base.send(:extend, HstoreAccessor)
class User < Member
hstore_accessor :data, :email, :age
end
class User < Member
hstore_accessor :data, :email, :age
validates :email, :age, presence: true
end
User.new.email #=> nil
User.new.valid? #=> false
class TwitterUser < Member
hstore_accessor :data, :twitter_handle
end
twitter_user = TwitterUser.new twitter_handle: '_danmcclain', username: 'dan'
twitter_user.twitter_handle #=> "_danmcclain"
twitter_user.username #=> "dan"
twitter_user.email #=> NoMethodError
SELECT rank() OVER(ORDER BY points DESC), username, points
FROM scores
WHERE game = 'Barking Bostons'
┌──────┬──────────┬────────┐ │ rank │ username │ points │ ├──────┼──────────┼────────┤ │ 1 │ Brian │ 300 │ │ 2 │ Dan │ 200 │ │ 3 │ Dan │ 100 │ │ 4 │ Doug │ 30 │ └──────┴──────────┴────────┘
SELECT rank() OVER (ORDER BY scores.points DESC), username, points
FROM scores
WHERE game = 'Barking Bostons' AND username = 'Dan'
┌──────┬──────────┬────────┐ │ rank │ username │ points │ ├──────┼──────────┼────────┤ │ 1 │ Dan │ 200 │ │ 2 │ Dan │ 100 │ └──────┴──────────┴────────┘
WITH top_scores_for_game AS (
SELECT rank() OVER (ORDER BY points DESC), username, points
FROM scores
WHERE scores.game = 'Barking Bostons'
)
SELECT top_scores_for_game.*
FROM top_scores_for_game
WITH
statement
WITH top_scores_for_game AS (
SELECT rank() OVER (ORDER BY points DESC), username, points
FROM scores
WHERE scores.game = 'Barking Bostons'
)
SELECT top_scores_for_game.*
FROM top_scores_for_game
WHERE top_scores_for_game.username = 'Dan'
┌──────┬──────────┬────────┐ │ rank │ username │ points │ ├──────┼──────────┼────────┤ │ 2 │ Dan │ 200 │ │ 3 │ Dan │ 100 │ └──────┴──────────┴────────┘
query = <<-SQL
WITH top_scores_for_game AS (
SELECT rank() OVER (ORDER BY points DESC), username, points
FROM scores
WHERE scores.game = 'Barking Bostons'
)
SELECT top_scores_for_game.*
FROM top_scores_for_game
WHERE top_scores_for_game.username = 'Dan'
SQL
Score.find_by_sql query
=> [#<Score ... >, #<Score ... >]
Score.ranked(points: :desc).where(game: 'Barking Bostons')
Score.from_cte('top_scores_for_game',
Score.ranked(points: :desc).where(game: 'Barking Bostons')).where(username: 'Dan')
{
posts: [
{
id: 1,
title: 'My awesome blog post',
content: 'Loren ipsum',
tag_ids: [1,2]
}
],
tags: [
{
id: 1,
name: 'Rails'
},
{
id: 2,
name: 'PostgreSQL'
}
]
}
WITH notes_ids AS (
SELECT id
FROM "notes"
), tag_ids_by_notes AS (
SELECT "tags"."note_id", array_agg("tags"."id") AS tag_ids
FROM "tags" GROUP BY "tags"."note_id"
HAVING "tags"."note_id" IN (SELECT "notes_ids"."id" FROM "notes_ids")
), tags_ids AS (
SELECT id FROM "tags"
), tags_attributes_filter AS (
SELECT "tags"."id", "tags"."name", "tags"."note_id"
FROM "tags"
WHERE "tags"."note_id" IN (SELECT "notes_ids"."id" FROM "notes_ids")
), tags_as_json_array AS (
SELECT array_to_json(array_agg(row_to_json(tags_attributes_filter))) AS tags, 1 AS match
FROM "tags_attributes_filter"
), notes_attributes_filter AS (
SELECT "notes"."id", "notes"."title", "notes"."content", coalesce("tag_ids_by_notes"."tag_ids", '{}'::int[]) AS tag_ids
FROM "notes"
LEFT OUTER JOIN "tag_ids_by_notes"
ON "notes"."id" = "tag_ids_by_notes"."note_id"
), notes_as_json_array AS (
SELECT array_to_json(array_agg(row_to_json(notes_attributes_filter))) AS notes, 1 AS match
FROM "notes_attributes_filter"
), jsons AS (
SELECT "tags_as_json_array"."tags", "notes_as_json_array"."notes"
FROM "tags_as_json_array"
INNER JOIN "notes_as_json_array"
ON "tags_as_json_array"."match" = "notes_as_json_array"."match"
)
SELECT row_to_json(jsons)
FROM "jsons"
WITH notes_ids AS (
SELECT id
FROM "notes"
-- We'd filter our notes that we want back here
), tag_ids_by_notes AS (
SELECT "tags"."note_id", array_agg("tags"."id") AS tag_ids
FROM "tags" GROUP BY "tags"."note_id"
HAVING "tags"."note_id" IN (SELECT "notes_ids"."id" FROM "notes_ids")
),
tags_attributes_filter AS (
SELECT "tags"."id", "tags"."name", "tags"."note_id"
FROM "tags"
WHERE "tags"."note_id" IN (SELECT "notes_ids"."id" FROM "notes_ids")
), tags_as_json_array AS (
SELECT array_to_json(array_agg(row_to_json(tags_attributes_filter))) AS tags, 1 AS match
FROM "tags_attributes_filter"
)
notes_attributes_filter AS (
SELECT "notes"."id", "notes"."title", "notes"."content", coalesce("tag_ids_by_notes"."tag_ids", '{}'::int[]) AS tag_ids
FROM "notes"
LEFT OUTER JOIN "tag_ids_by_notes"
ON "notes"."id" = "tag_ids_by_notes"."note_id"
), notes_as_json_array AS (
SELECT array_to_json(array_agg(row_to_json(notes_attributes_filter))) AS notes, 1 AS match
FROM "notes_attributes_filter"
)
WITH jsons AS (
SELECT "tags_as_json_array"."tags", "notes_as_json_array"."notes"
FROM "tags_as_json_array"
INNER JOIN "notes_as_json_array"
ON "tags_as_json_array"."match" = "notes_as_json_array"."match"
)
SELECT row_to_json(jsons)
FROM "jsons"
Ruby | PostgreSQL | ||
---|---|---|---|
1000 Notes, 10000 Tags | 4407 ms | 66 ms | 67.2 X |
500 Notes, 5000 Tags |
2071 ms | 26 ms | 78.5 X |
100 Notes, 1000 Tags |
427 ms | 12 ms | 34.2 X |
10 Notes, 100 Tags |
46 ms | 10 ms | 4.6 X |