Forked from Bodacious/Benchmark semver ordering in Postgresql.rb
Last active
March 16, 2024 16:44
-
-
Save h-lame/75d409e1c8168f0364a857be696a5833 to your computer and use it in GitHub Desktop.
Benchmarking different SEMVER ordering strategies in PostgreSQL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'bundler/inline' | |
## | |
# Get dependencies | |
gemfile do | |
source 'https://rubygems.org' | |
gem 'pg' | |
gem 'activerecord', require: 'active_record' | |
gem 'benchmark-ips' | |
gem 'pry' | |
end | |
## | |
# Ensure DB exists | |
ActiveRecord::Base.yield_self do |base| | |
base.establish_connection( | |
adapter: 'postgresql', | |
host: 'localhost', | |
database: 'postgres' | |
) | |
begin | |
base.connection.create_database('test_stuff') | |
rescue ActiveRecord::DatabaseAlreadyExists | |
nil | |
end | |
base.establish_connection( | |
adapter: 'postgresql', | |
host: 'localhost', | |
database: 'test_stuff' | |
) | |
end | |
## | |
# Define the schema | |
ActiveRecord::Schema.define do | |
schema_proc = ->(t) { | |
t.string 'name' | |
t.string 'version' | |
t.integer 'version_major' | |
t.integer 'version_minor' | |
t.integer 'version_patch' | |
t.integer 'version_arr', array: true | |
} | |
create_table('side_a', id: :bigint, primary_key: :id, force: true, &schema_proc) | |
create_table('side_b', id: :bigint, primary_key: :id, force: true, &schema_proc) | |
create_table('side_c', id: :bigint, primary_key: :id, force: true, &schema_proc) | |
create_table('side_d', id: :bigint, primary_key: :id, force: true, &schema_proc) | |
create_table('side_e', id: :bigint, primary_key: :id, force: true, &schema_proc) | |
add_index :side_a, :version | |
add_index :side_b, [:version_major, :version_minor, :version_patch], name: :version | |
add_index :side_c, [:version_patch, :version_minor, :version_major], name: :version_rev | |
add_index :side_e, [:version_arr] | |
end | |
class SideA < ActiveRecord::Base | |
self.table_name = 'side_a' | |
scope :ordered, -> { order(Arel.sql("string_to_array(version, '.')::int[]")) } | |
end | |
class SideB < ActiveRecord::Base | |
self.table_name = 'side_b' | |
scope :ordered, -> { order('version_major ASC, version_minor ASC, version_patch ASC') } | |
def version=(value) | |
super | |
self.major_version, self.minor_version, self.patch_version = *value.split('.') | |
end | |
end | |
class SideC < ActiveRecord::Base | |
self.table_name = 'side_c' | |
scope :ordered, -> { order('version_major ASC, version_minor ASC, version_patch ASC') } | |
def version=(value) | |
super | |
self.major_version, self.minor_version, self.patch_version = *value.split('.') | |
end | |
end | |
class SideD < ActiveRecord::Base | |
self.table_name = 'side_d' | |
scope :ordered, -> { order('version_arr ASC') } | |
def version=(value) | |
super | |
self.version_arr = *value.split('.') | |
end | |
end | |
class SideE < ActiveRecord::Base | |
self.table_name = 'side_e' | |
scope :ordered, -> { order('version_arr ASC') } | |
def version=(value) | |
super | |
self.version_arr = *value.split('.') | |
end | |
end | |
## | |
# Build a bunch of fake data | |
maj_versions = (0..2).to_a | |
min_versions = (0..12).to_a | |
patch_versions = (0..15).to_a | |
all_version_strings = maj_versions.product(min_versions, patch_versions).map { _1.join('.') } | |
document_names = 10.times.map.with_index(1) { "Document #{_2}" } | |
names_and_versions = document_names.product(all_version_strings) | |
attributes = names_and_versions.map.with_index(1) do |(name, version_string), index| | |
{ | |
name: name, | |
version: version_string, | |
version_major: version_string.split('.').first, | |
version_minor: version_string.split('.').second, | |
version_patch: version_string.split('.').third, | |
version_arr: version_string.split('.'), | |
id: index | |
} | |
end | |
## | |
# Populate the tables | |
SideA.insert_all(attributes, unique_by: :id) | |
SideB.insert_all(attributes, unique_by: :id) | |
SideC.insert_all(attributes, unique_by: :id) | |
SideD.insert_all(attributes, unique_by: :id) | |
SideE.insert_all(attributes, unique_by: :id) | |
work = -> (klass) { | |
klass.ordered.limit(100).load | |
klass.ordered.count | |
} | |
## | |
# Run the benchmark | |
Benchmark.ips do |test| | |
test.report('Indexed String') { work.(SideA) } | |
test.report('Split index maj,min,patch') { work.(SideB) } | |
test.report('Split index patch,min,max') { work.(SideC) } | |
test.report('Integer array') { work.(SideD) } | |
test.report('Integer array indexed') { work.(SideE) } | |
end | |
ActiveRecord::Base.connection.yield_self do |connection| | |
connection.disconnect! | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Warming up -------------------------------------- | |
Indexed String 14.000 i/100ms | |
Split index maj,min,patch | |
73.000 i/100ms | |
Split index patch,min,max | |
39.000 i/100ms | |
Integer array 28.000 i/100ms | |
Integer array indexed | |
56.000 i/100ms | |
Calculating ------------------------------------- | |
Indexed String 125.508 (±10.4%) i/s - 630.000 in 5.068970s | |
Split index maj,min,patch | |
623.507 (±18.8%) i/s - 3.066k in 5.113163s | |
Split index patch,min,max | |
406.964 (±14.3%) i/s - 1.989k in 5.012564s | |
Integer array 395.207 (± 3.3%) i/s - 1.988k in 5.036056s | |
Integer array indexed | |
751.770 (± 4.4%) i/s - 3.752k in 5.002257s |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment