Skip to content

Commit e614cf6

Browse files
committed
Support disabling indexes for MySQL and MariaDB
For MySQL version 8.0.0 and above, and MariaDB version 10.6.0 and above Allows to disable an index to be ignored by queries while still being maintained.
1 parent 0477527 commit e614cf6

18 files changed

+314
-8
lines changed

activerecord/CHANGELOG.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,41 @@
1+
* Support disabling indexes for MySQL v8.0.0+ and MariaDB v10.6.0+
2+
3+
MySQL 8.0.0 added an option to disable indexes from being used by the query
4+
optimizer by making them "invisible". This allows the index to still be maintained
5+
and updated but no queries will be permitted to use it. This can be useful for adding
6+
new invisible indexes or making existing indexes invisible before dropping them
7+
to ensure queries are not negatively affected.
8+
See https://dev.mysql.com/blog-archive/mysql-8-0-invisible-indexes/ for more details.
9+
10+
MariaDB 10.6.0 also added support for this feature by allowing indexes to be "ignored"
11+
in queries. See https://mariadb.com/kb/en/ignored-indexes/ for more details.
12+
13+
Active Record now supports this option for MySQL 8.0.0+ and MariaDB 10.6.0+ for
14+
index creation and alteration where the new index option `enabled: true/false` can be
15+
passed to column and index methods as below:
16+
17+
```ruby
18+
add_index :users, :email, enabled: false
19+
enable_index :users, :email
20+
add_column :users, :dob, :string, index: { enabled: false }
21+
22+
change_table :users do |t|
23+
t.index :name, enabled: false
24+
t.index :dob
25+
t.disable_index :dob
26+
t.column :username, :string, index: { enabled: false }
27+
t.references :account, index: { enabled: false }
28+
end
29+
30+
create_table :users do |t|
31+
t.string :name, index: { enabled: false }
32+
t.string :email
33+
t.index :email, enabled: false
34+
end
35+
```
36+
37+
*Merve Taner*
38+
139
* Respect `implicit_order_column` in `ActiveRecord::Relation#reverse_order`.
240

341
*Joshua Young*

activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,19 @@ def rename_column(table_name, column_name, new_column_name)
912912
# Concurrently adding an index is not supported in a transaction.
913913
#
914914
# For more information see the {"Transactional Migrations" section}[rdoc-ref:Migration].
915+
#
916+
# ====== Creating an index that is not used by queries
917+
#
918+
# add_index(:developers, :name, enabled: false)
919+
#
920+
# generates:
921+
#
922+
# CREATE INDEX index_developers_on_name ON developers (name) INVISIBLE -- MySQL
923+
#
924+
# CREATE INDEX index_developers_on_name ON developers (name) IGNORED -- MariaDB
925+
#
926+
# Note: only supported by MySQL version 8.0.0 and greater, and MariaDB version 10.6.0 and greater.
927+
#
915928
def add_index(table_name, column_name, **options)
916929
create_index = build_create_index_definition(table_name, column_name, **options)
917930
execute schema_creation.accept(create_index)
@@ -1475,7 +1488,7 @@ def update_table_definition(table_name, base) # :nodoc:
14751488
end
14761489

14771490
def add_index_options(table_name, column_name, name: nil, if_not_exists: false, internal: false, **options) # :nodoc:
1478-
options.assert_valid_keys(:unique, :length, :order, :opclass, :where, :type, :using, :comment, :algorithm, :include, :nulls_not_distinct)
1491+
options.assert_valid_keys(valid_index_options)
14791492

14801493
column_names = index_column_names(column_name)
14811494

@@ -1484,7 +1497,7 @@ def add_index_options(table_name, column_name, name: nil, if_not_exists: false,
14841497

14851498
validate_index_length!(table_name, index_name, internal)
14861499

1487-
index = IndexDefinition.new(
1500+
index = create_index_definition(
14881501
table_name, index_name,
14891502
options[:unique],
14901503
column_names,
@@ -1539,6 +1552,20 @@ def change_column_comment(table_name, column_name, comment_or_changes)
15391552
raise NotImplementedError, "#{self.class} does not support changing column comments"
15401553
end
15411554

1555+
# Enables an index to be used by queries.
1556+
#
1557+
# enable_index(:users, :email)
1558+
def enable_index(table_name, index_name)
1559+
raise NotImplementedError, "#{self.class} does not support enabling indexes"
1560+
end
1561+
1562+
# Prevents an index from being used by queries.
1563+
#
1564+
# disable_index(:users, :email)
1565+
def disable_index(table_name, index_name)
1566+
raise NotImplementedError, "#{self.class} does not support disabling indexes"
1567+
end
1568+
15421569
def create_schema_dumper(options) # :nodoc:
15431570
SchemaDumper.create(self, options)
15441571
end
@@ -1627,6 +1654,10 @@ def add_index_sort_order(quoted_columns, **options)
16271654
end
16281655
end
16291656

1657+
def valid_index_options
1658+
[:unique, :length, :order, :opclass, :where, :type, :using, :comment, :algorithm, :include, :nulls_not_distinct]
1659+
end
1660+
16301661
def options_for_index_columns(options)
16311662
if options.is_a?(Hash)
16321663
options.symbolize_keys
@@ -1703,6 +1734,10 @@ def create_table_definition(name, **options)
17031734
TableDefinition.new(self, name, **options)
17041735
end
17051736

1737+
def create_index_definition(table_name, name, unique, columns, **options)
1738+
IndexDefinition.new(table_name, name, unique, columns, **options)
1739+
end
1740+
17061741
def create_alter_table(name)
17071742
AlterTable.new create_table_definition(name)
17081743
end

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,10 @@ def supports_nulls_not_distinct?
555555
false
556556
end
557557

558+
def supports_disabling_indexes?
559+
false
560+
end
561+
558562
def return_value_after_insert?(column) # :nodoc:
559563
column.auto_populated?
560564
end

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,16 @@ def return_value_after_insert?(column) # :nodoc:
178178
supports_insert_returning? ? column.auto_populated? : column.auto_increment?
179179
end
180180

181+
# See https://dev.mysql.com/doc/refman/8.0/en/invisible-indexes.html for more details on MySQL feature.
182+
# See https://mariadb.com/kb/en/ignored-indexes/ for more details on the MariaDB feature.
183+
def supports_disabling_indexes?
184+
if mariadb?
185+
database_version >= "10.6.0"
186+
else
187+
database_version >= "8.0.0"
188+
end
189+
end
190+
181191
def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
182192
query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
183193
end
@@ -457,6 +467,24 @@ def build_create_index_definition(table_name, column_name, **options) # :nodoc:
457467
CreateIndexDefinition.new(index, algorithm)
458468
end
459469

470+
def enable_index(table_name, index_name) # :nodoc:
471+
raise NotImplementedError unless supports_disabling_indexes?
472+
473+
query = <<~SQL
474+
ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{index_name} #{mariadb? ? "NOT IGNORED" : "VISIBLE"}
475+
SQL
476+
execute(query)
477+
end
478+
479+
def disable_index(table_name, index_name) # :nodoc:
480+
raise NotImplementedError unless supports_disabling_indexes?
481+
482+
query = <<~SQL
483+
ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{index_name} #{mariadb? ? "IGNORED" : "INVISIBLE"}
484+
SQL
485+
execute(query)
486+
end
487+
460488
def add_sql_comment!(sql, comment) # :nodoc:
461489
sql << " COMMENT #{quote(comment)}" if comment.present?
462490
sql

activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ def visit_IndexDefinition(o, create = false)
4949
sql << "USING #{o.using}" if o.using
5050
sql << "ON #{quote_table_name(o.table)}" if create
5151
sql << "(#{quoted_columns(o)})"
52+
sql << "INVISIBLE" if o.disabled? && !mariadb?
53+
sql << "IGNORED" if o.disabled? && mariadb?
5254

5355
add_sql_comment!(sql.join(" "), o.comment)
5456
end

activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,25 @@ module ColumnMethods
5050
deprecate :unsigned_float, :unsigned_decimal, deprecator: ActiveRecord.deprecator
5151
end
5252

53+
# = Active Record MySQL Adapter \Index Definition
54+
class IndexDefinition < ActiveRecord::ConnectionAdapters::IndexDefinition
55+
attr_accessor :enabled
56+
57+
def initialize(*args, **kwargs)
58+
@enabled = kwargs.key?(:enabled) ? kwargs.delete(:enabled) : true
59+
super
60+
end
61+
62+
def defined_for?(columns = nil, name: nil, unique: nil, valid: nil, include: nil, nulls_not_distinct: nil, enabled: nil, **options)
63+
super(columns, name:, unique:, valid:, include:, nulls_not_distinct:, **options) &&
64+
(enabled.nil? || self.enabled == enabled)
65+
end
66+
67+
def disabled?
68+
!@enabled
69+
end
70+
end
71+
5372
# = Active Record MySQL Adapter \Table Definition
5473
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
5574
include ColumnMethods
@@ -99,6 +118,28 @@ def integer_like_primary_key_type(type, options)
99118
# = Active Record MySQL Adapter \Table
100119
class Table < ActiveRecord::ConnectionAdapters::Table
101120
include ColumnMethods
121+
122+
# Enables an index to be used by query optimizers.
123+
#
124+
# t.enable_index(:email)
125+
#
126+
# Note: only supported by MySQL version 8.0.0 and greater, and MariaDB version 10.6.0 and greater.
127+
#
128+
# See {connection.enable_index}[rdoc-ref:SchemaStatements#enable_index]
129+
def enable_index(index_name)
130+
@base.enable_index(name, index_name)
131+
end
132+
133+
# Disables an index not to be used by query optimizers.
134+
#
135+
# t.disable_index(:email)
136+
#
137+
# Note: only supported by MySQL version 8.0.0 and greater, and MariaDB version 10.6.0 and greater.
138+
#
139+
# See {connection.disable_index}[rdoc-ref:SchemaStatements#disable_index]
140+
def disable_index(index_name)
141+
@base.disable_index(name, index_name)
142+
end
102143
end
103144
end
104145
end

activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def indexes(table_name)
2121
index_using = mysql_index_type
2222
end
2323

24-
indexes << [
24+
index = [
2525
row["Table"],
2626
row["Key_name"],
2727
row["Non_unique"].to_i == 0,
@@ -30,8 +30,14 @@ def indexes(table_name)
3030
orders: {},
3131
type: index_type,
3232
using: index_using,
33-
comment: row["Index_comment"].presence
33+
comment: row["Index_comment"].presence,
3434
]
35+
36+
if supports_disabling_indexes?
37+
index[-1][:enabled] = mariadb? ? row["Ignored"] == "NO" : row["Visible"] == "YES"
38+
end
39+
40+
indexes << index
3541
end
3642

3743
if expression = row["Expression"]
@@ -63,8 +69,7 @@ def indexes(table_name)
6369
columns, order: orders, length: lengths
6470
).values.join(", ")
6571
end
66-
67-
IndexDefinition.new(*index, **options)
72+
MySQL::IndexDefinition.new(*index, **options)
6873
end
6974
rescue StatementInvalid => e
7075
if e.message.match?(/Table '.+' doesn't exist/)
@@ -74,6 +79,16 @@ def indexes(table_name)
7479
end
7580
end
7681

82+
def create_index_definition(table_name, name, unique, columns, **options)
83+
MySQL::IndexDefinition.new(table_name, name, unique, columns, **options)
84+
end
85+
86+
def add_index_options(table_name, column_name, name: nil, if_not_exists: false, internal: false, **options) # :nodoc:
87+
index, algorithm, if_not_exists = super
88+
index.enabled = options[:enabled] unless options[:enabled].nil?
89+
[index, algorithm, if_not_exists]
90+
end
91+
7792
def remove_column(table_name, column_name, type = nil, **options)
7893
if foreign_key_exists?(table_name, column: column_name)
7994
remove_foreign_key(table_name, column: column_name)
@@ -234,6 +249,12 @@ def add_index_length(quoted_columns, **options)
234249
end
235250
end
236251

252+
def valid_index_options
253+
index_options = super
254+
index_options << :enabled if supports_disabling_indexes?
255+
index_options
256+
end
257+
237258
def add_options_for_index_columns(quoted_columns, **options)
238259
quoted_columns = add_index_length(quoted_columns, **options)
239260
super

activerecord/lib/active_record/migration/command_recorder.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ class Migration
4444
# * rename_enum_value (must supply a +:from+ and +:to+ option)
4545
# * rename_index
4646
# * rename_table
47+
# * enable_index
48+
# * disable_index
4749
class CommandRecorder
4850
ReversibleAndIrreversibleMethods = [
4951
:create_table, :create_join_table, :rename_table, :add_column, :remove_column,
@@ -58,7 +60,8 @@ class CommandRecorder
5860
:add_unique_constraint, :remove_unique_constraint,
5961
:create_enum, :drop_enum, :rename_enum, :add_enum_value, :rename_enum_value,
6062
:create_schema, :drop_schema,
61-
:create_virtual_table, :drop_virtual_table
63+
:create_virtual_table, :drop_virtual_table,
64+
:enable_index, :disable_index
6265
]
6366
include JoinTable
6467

@@ -183,6 +186,16 @@ def invert_#{method}(args, &block) # def invert_create_table(args, &block)
183186

184187
include StraightReversions
185188

189+
def invert_enable_index(args)
190+
table_name, index_name = args
191+
[:disable_index, [table_name, index_name]]
192+
end
193+
194+
def invert_disable_index(args)
195+
table_name, index_name = args
196+
[:enable_index, [table_name, index_name]]
197+
end
198+
186199
def invert_transaction(args, &block)
187200
sub_recorder = CommandRecorder.new(delegate)
188201
sub_recorder.revert(&block)

activerecord/lib/active_record/schema_dumper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ def index_parts(index)
277277
index_parts << "nulls_not_distinct: #{index.nulls_not_distinct.inspect}" if index.nulls_not_distinct
278278
index_parts << "type: #{index.type.inspect}" if index.type
279279
index_parts << "comment: #{index.comment.inspect}" if index.comment
280+
index_parts << "enabled: #{index.enabled.inspect}" if @connection.supports_disabling_indexes? && index.disabled?
280281
index_parts
281282
end
282283

activerecord/test/cases/active_record_schema_test.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
1818
@connection.drop_table :fruits rescue nil
1919
@connection.drop_table :has_timestamps rescue nil
2020
@connection.drop_table :multiple_indexes rescue nil
21+
@connection.drop_table :disabled_index rescue nil
2122
@schema_migration.delete_all_versions
2223
ActiveRecord::Migration.verbose = @original_verbose
2324
end
@@ -120,6 +121,20 @@ def test_schema_load_with_multiple_indexes_for_column_of_different_names
120121
assert_equal ["multiple_indexes_foo_1", "multiple_indexes_foo_2"], indexes.collect(&:name).sort
121122
end
122123

124+
if ActiveRecord::Base.lease_connection.supports_disabling_indexes?
125+
def test_schema_load_for_index_visibility
126+
ActiveRecord::Schema.define do
127+
create_table :disabled_index do |t|
128+
t.string "foo"
129+
t.index ["foo"], name: "disabled_foo_index", enabled: false
130+
end
131+
end
132+
133+
indexes = @connection.indexes("disabled_index").find { |index| index.name == "disabled_foo_index" }
134+
assert_predicate indexes, :disabled?
135+
end
136+
end
137+
123138
if current_adapter?(:PostgreSQLAdapter)
124139
def test_timestamps_with_and_without_zones
125140
ActiveRecord::Schema.define do

0 commit comments

Comments
 (0)