query_police 0.1.0.beta
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +487 -0
- data/Rakefile +12 -0
- data/lib/query_police/analysis/dynamic_message.rb +72 -0
- data/lib/query_police/analysis.rb +155 -0
- data/lib/query_police/config.rb +17 -0
- data/lib/query_police/constants.rb +14 -0
- data/lib/query_police/explain.rb +70 -0
- data/lib/query_police/helper.rb +44 -0
- data/lib/query_police/rules.json +171 -0
- data/lib/query_police/version.rb +5 -0
- data/lib/query_police.rb +163 -0
- data/sig/query_police.rbs +4 -0
- metadata +104 -0
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module QueryPolice
|
4
|
+
class Analysis
|
5
|
+
# Module to define methods related to dynamic message
|
6
|
+
module DynamicMessage
|
7
|
+
private
|
8
|
+
|
9
|
+
LISTED_VAR = %w[amount column impact table tag value].freeze
|
10
|
+
|
11
|
+
# to pretty print the analysis with warnings and suggestions
|
12
|
+
# @param opts [Hash] opts to get specifc dyanmic message
|
13
|
+
# eg. {"table" => "users", "column" => "select_type", "tag" => "SIMPLE", "type" => "message"}
|
14
|
+
# @return [String]
|
15
|
+
def dynamic_message(opts)
|
16
|
+
table, column, tag, type = opts.values_at("table", "column", "tag", "type")
|
17
|
+
message = self.tables.dig(table, "analysis", column, "tags", tag, type) || ""
|
18
|
+
|
19
|
+
variables = message.scan(/\$(\w+)/).uniq.map { |var| var[0] }
|
20
|
+
variables.each do |var|
|
21
|
+
value = dynamic_value_of(var, opts)
|
22
|
+
|
23
|
+
message.gsub!(/\$#{var}/, value.to_s) if value.present?
|
24
|
+
end
|
25
|
+
|
26
|
+
message
|
27
|
+
end
|
28
|
+
|
29
|
+
def dynamic_value_of(var, opts)
|
30
|
+
LISTED_VAR.include?(var) ? send(var, opts) : relative_value_of(var, opts.dig("table"))
|
31
|
+
end
|
32
|
+
|
33
|
+
def relative_value_of(var, table)
|
34
|
+
value_type = var.match(/amount_/).present? ? "amount" : "value"
|
35
|
+
self.tables.dig(table, "analysis", var.gsub(/amount_/, ""), value_type)
|
36
|
+
end
|
37
|
+
|
38
|
+
# dynamic variable methods
|
39
|
+
def amount(opts)
|
40
|
+
table, column = opts.values_at("table", "column")
|
41
|
+
|
42
|
+
self.tables.dig(table, "analysis", column, "amount")
|
43
|
+
end
|
44
|
+
|
45
|
+
def column(opts)
|
46
|
+
opts.dig("column")
|
47
|
+
end
|
48
|
+
|
49
|
+
def impact(opts)
|
50
|
+
table, column, tag = opts.values_at("table", "column", "tag")
|
51
|
+
|
52
|
+
impact = self.tables.dig(table, "analysis", column, "tags", tag, "impact")
|
53
|
+
|
54
|
+
opts.dig("colours").present? ? impact.send(IMPACTS[impact].colour) : impact
|
55
|
+
end
|
56
|
+
|
57
|
+
def table(opts)
|
58
|
+
opts.dig("table")
|
59
|
+
end
|
60
|
+
|
61
|
+
def tag(opts)
|
62
|
+
opts.dig("tag")
|
63
|
+
end
|
64
|
+
|
65
|
+
def value(opts)
|
66
|
+
table, column = opts.values_at("table", "column")
|
67
|
+
|
68
|
+
self.tables.dig(table, "analysis", column, "value")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'analysis/dynamic_message'
|
4
|
+
|
5
|
+
module QueryPolice
|
6
|
+
# This class is used to store analysis of a query and provide methods over them
|
7
|
+
class Analysis
|
8
|
+
include DynamicMessage
|
9
|
+
|
10
|
+
IMPACTS = {
|
11
|
+
"negative" => { "colour" => "red" },
|
12
|
+
"positive" => { "colour" => "green" },
|
13
|
+
"caution" => { "colour" => "yellow" }
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
# initialize analysis object
|
17
|
+
# tables [Array] Array of table analysis
|
18
|
+
# Eg.
|
19
|
+
# {
|
20
|
+
# "users" => {
|
21
|
+
# "id"=>1,
|
22
|
+
# "name"=>"users",
|
23
|
+
# "analysis"=>{
|
24
|
+
# "type"=>{
|
25
|
+
# "value" => "all",
|
26
|
+
# "tags" => {
|
27
|
+
# "all" => {
|
28
|
+
# "impact"=>"negative",
|
29
|
+
# "warning"=>"warning to represent the issue",
|
30
|
+
# "suggestions"=>"some follow up suggestions"
|
31
|
+
# }
|
32
|
+
# }
|
33
|
+
# }
|
34
|
+
# }
|
35
|
+
# }
|
36
|
+
# }
|
37
|
+
# summary [Hash] hash of analysis summary
|
38
|
+
# Eg.
|
39
|
+
# {
|
40
|
+
# "cardinality"=>{
|
41
|
+
# "amount"=>10,
|
42
|
+
# "warning"=>"warning to represent the issue",
|
43
|
+
# "suggestions"=>"some follow up suggestions"
|
44
|
+
# }
|
45
|
+
# }
|
46
|
+
def initialize
|
47
|
+
@table_count = 0
|
48
|
+
@tables = {}
|
49
|
+
@summary = {}
|
50
|
+
end
|
51
|
+
|
52
|
+
attr_accessor :tables, :table_count, :summary
|
53
|
+
|
54
|
+
# register a table analysis in analysis object
|
55
|
+
# @param name [String] name of the table
|
56
|
+
# @param table_analysis [Hash] analysis of a table
|
57
|
+
# Eg.
|
58
|
+
# {
|
59
|
+
# "id"=>1,
|
60
|
+
# "name"=>"users",
|
61
|
+
# "analysis"=>{
|
62
|
+
# "type"=>[
|
63
|
+
# {
|
64
|
+
# "tag"=>"all",
|
65
|
+
# "impact"=>"negative",
|
66
|
+
# "warning"=>"warning to represent the issue",
|
67
|
+
# "suggestions"=>"some follow up suggestions"
|
68
|
+
# }
|
69
|
+
# ]
|
70
|
+
# }
|
71
|
+
# }
|
72
|
+
def register_table(name, table_analysis)
|
73
|
+
self.table_count += 1
|
74
|
+
self.tables.merge!(
|
75
|
+
{
|
76
|
+
name => {
|
77
|
+
"id" => self.table_count,
|
78
|
+
"name" => name,
|
79
|
+
"analysis" => table_analysis
|
80
|
+
}
|
81
|
+
}
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
# register summary based in different attributes
|
86
|
+
# @param summary [Hash] hash of summary of analysis
|
87
|
+
def register_summary(summary)
|
88
|
+
self.summary.merge!(summary)
|
89
|
+
end
|
90
|
+
|
91
|
+
# to get analysis in pretty format with warnings and suggestions
|
92
|
+
# @param opts [Hash] - possible options [positive: <boolean>, negative: <boolean>, caution: <boolean>]
|
93
|
+
# @return [String] pretty analysis
|
94
|
+
def pretty_analysis(opts)
|
95
|
+
final_message = ""
|
96
|
+
|
97
|
+
opts.slice(*IMPACTS.keys).each do |impact, value|
|
98
|
+
final_message += pretty_analysis_for(impact) if value.present?
|
99
|
+
end
|
100
|
+
|
101
|
+
final_message
|
102
|
+
end
|
103
|
+
|
104
|
+
# to get analysis in pretty format with warnings and suggestions for a impact
|
105
|
+
# @param impact [String]
|
106
|
+
# @return [String] pretty analysis
|
107
|
+
def pretty_analysis_for(impact)
|
108
|
+
final_message = ""
|
109
|
+
|
110
|
+
self.tables.keys.each do |table|
|
111
|
+
table_message = table_pretty_analysis(table, {impact => true})
|
112
|
+
|
113
|
+
final_message += "table: #{table}\n#{table_message}\n" if table_message.present?
|
114
|
+
end
|
115
|
+
|
116
|
+
final_message
|
117
|
+
end
|
118
|
+
|
119
|
+
# to get analysis in pretty format with warnings and suggestions for a table
|
120
|
+
# @param table [String] - table name
|
121
|
+
# @param opts [Hash] - possible options [positive: <boolean>, negative: <boolean>, caution: <boolean>]
|
122
|
+
# @return [String] pretty analysis
|
123
|
+
def table_pretty_analysis(table, opts)
|
124
|
+
table_message = ""
|
125
|
+
|
126
|
+
self.tables.dig(table, "analysis").each do |column, column_analysis|
|
127
|
+
tags_message = ""
|
128
|
+
column_analysis.dig("tags").each do |tag, tag_analysis|
|
129
|
+
next unless opts.dig(tag_analysis.dig("impact")).present?
|
130
|
+
|
131
|
+
tags_message += tag_pretty_analysis(table, column, tag)
|
132
|
+
end
|
133
|
+
|
134
|
+
table_message += "column: #{column}\n#{tags_message}" if tags_message.present?
|
135
|
+
end
|
136
|
+
|
137
|
+
table_message
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def tag_pretty_analysis(table, column, tag)
|
143
|
+
tag_message = ""
|
144
|
+
|
145
|
+
opts = { "table" => table, "column" => column, "tag" => tag }
|
146
|
+
message = dynamic_message(opts.merge({ "type" => "message" }))
|
147
|
+
suggestion = dynamic_message(opts.merge({ "type" => "suggestion" }))
|
148
|
+
tag_message += "impact: #{impact(opts.merge({ "colours" => true }))}\n"
|
149
|
+
tag_message += "message: #{message}\n"
|
150
|
+
tag_message += "suggestion: #{suggestion}\n" if suggestion.present?
|
151
|
+
|
152
|
+
tag_message
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module QueryPolice
|
4
|
+
# This class is used for configuration of query police
|
5
|
+
class Config
|
6
|
+
def initialize(detailed, rules_path)
|
7
|
+
@detailed = detailed
|
8
|
+
@rules_path = rules_path
|
9
|
+
end
|
10
|
+
|
11
|
+
def detailed?
|
12
|
+
@detailed.present?
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_accessor :detailed, :rules_path
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module QueryPolice
|
4
|
+
module Constants
|
5
|
+
DEFAULT_COLUMN_RULES = {
|
6
|
+
"value_type" => "string"
|
7
|
+
}.freeze
|
8
|
+
DEFAULT_DETAILED = true
|
9
|
+
DEFAULT_LOGGER_CONFIG = {
|
10
|
+
"negative" => true
|
11
|
+
}.freeze
|
12
|
+
DEFAULT_RULES_PATH = File.join(File.dirname(__FILE__), "rules.json")
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "helper"
|
4
|
+
|
5
|
+
module QueryPolice
|
6
|
+
# This module provides tools to explain queries and ActiveRecord::Relation
|
7
|
+
module Explain
|
8
|
+
# to get explain result in parsable format
|
9
|
+
# @param relation [ActiveRecord::Relation, String] active record relation or raw sql query
|
10
|
+
# @return [Array] parsed_result - array of hashes representing EXPLAIN result for each row
|
11
|
+
def full_explain(relation, detailed = true)
|
12
|
+
explain_result = explain(relation)
|
13
|
+
return explain_result unless detailed
|
14
|
+
|
15
|
+
detailed_explain_result = detailed_explain(relation)
|
16
|
+
|
17
|
+
[*explain_result.keys, *detailed_explain_result.keys].uniq.map do |key|
|
18
|
+
(
|
19
|
+
explain_result.dig(key)&.merge(
|
20
|
+
detailed_explain_result.dig(key) || {}
|
21
|
+
) || detailed_explain_result.dig(key)
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# to get explain result in parsable format using "EXPLAIN <query>"
|
27
|
+
# @param relation [ActiveRecord::Relation, String] active record relation or raw sql query
|
28
|
+
# @return [Array] parsed_result - array of hashes representing EXPLAIN result for each row
|
29
|
+
def explain(relation)
|
30
|
+
query = load_query(relation)
|
31
|
+
explain_result = ActiveRecord::Base.connection.execute("EXPLAIN #{query}")
|
32
|
+
parsed_result = {}
|
33
|
+
|
34
|
+
explain_result.each(as: :json) do |ele|
|
35
|
+
parsed_result[ele.dig("table")] = ele
|
36
|
+
end
|
37
|
+
|
38
|
+
parsed_result
|
39
|
+
end
|
40
|
+
|
41
|
+
# to get detailed explain result in parsable format using "EXPLAIN format=JSON <query>"
|
42
|
+
# @param relation [ActiveRecord::Relation, String] active record relation or raw sql query
|
43
|
+
# @param prefix [String] prefix to append before each key "prefix#<key>"
|
44
|
+
# @return [Array] parsed_result - array of flatten hashes representing EXPLAIN result for each row
|
45
|
+
def detailed_explain(relation, prefix = "detailed")
|
46
|
+
query = load_query(relation)
|
47
|
+
explain_result = ActiveRecord::Base.connection.execute("EXPLAIN format=json #{query}")
|
48
|
+
explain_result = parse_detailed_explain(explain_result)
|
49
|
+
|
50
|
+
explain_result.map { |ele| [ele.dig("table_name"), Helper.flatten_hash(ele, prefix)] }.to_h
|
51
|
+
end
|
52
|
+
|
53
|
+
class << self
|
54
|
+
private
|
55
|
+
|
56
|
+
def load_query(relation)
|
57
|
+
relation.class.name == "ActiveRecord::Relation" ? relation.to_sql : relation
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_detailed_explain(explain_result)
|
61
|
+
parsed_result = JSON.parse(explain_result&.first&.first || "{}").dig("query_block")
|
62
|
+
return parsed_result.dig("nested_loop").map { |e| e.dig("table") } if parsed_result.key?("nested_loop")
|
63
|
+
|
64
|
+
parsed_result.key?("table") ? [parsed_result.dig("table")] : []
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
module_function :full_explain, :explain, :detailed_explain
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module QueryPolice
|
4
|
+
# This module define helper methods for query police
|
5
|
+
module Helper
|
6
|
+
def flatten_hash(hash, prefix_key = "")
|
7
|
+
flat_hash = {}
|
8
|
+
|
9
|
+
hash.each do |key, value|
|
10
|
+
key = prefix_key.present? ? "#{prefix_key}##{key}" : key.to_s
|
11
|
+
|
12
|
+
flat_hash.merge!(value.is_a?(Hash) ? flatten_hash(value, key) : { key => value })
|
13
|
+
end
|
14
|
+
|
15
|
+
flat_hash
|
16
|
+
end
|
17
|
+
|
18
|
+
def logger(message, type="info")
|
19
|
+
if defined?(Rails) && Rails.logger
|
20
|
+
Rails.logger.send(type, message)
|
21
|
+
else
|
22
|
+
puts "#{type.upcase}: #{message}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def load_config(rules_path)
|
27
|
+
unless File.exists?(rules_path)
|
28
|
+
raise Error.new(
|
29
|
+
"Failed to load the rule file from '#{rules_path}'. " \
|
30
|
+
"The file may be missing or there is a problem with the path. " \
|
31
|
+
"Please ensure that the file exists and the path is correct."
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
rules_config = JSON.parse(File.read(rules_path))
|
36
|
+
|
37
|
+
rules_config
|
38
|
+
end
|
39
|
+
|
40
|
+
module_function :flatten_hash, :logger, :load_config
|
41
|
+
end
|
42
|
+
|
43
|
+
private_constant :Helper
|
44
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
{
|
2
|
+
"select_type": {
|
3
|
+
"description": "Type of select used in the table.",
|
4
|
+
"value_type": "string",
|
5
|
+
"rules": {
|
6
|
+
"SIMPLE": {
|
7
|
+
"impact": "positive",
|
8
|
+
"message": "A simple query without subqueries or unions.",
|
9
|
+
"suggestion": ""
|
10
|
+
}
|
11
|
+
}
|
12
|
+
},
|
13
|
+
"type": {
|
14
|
+
"description": "Join used in the query for a specific table.",
|
15
|
+
"value_type": "string",
|
16
|
+
"rules": {
|
17
|
+
"system": {
|
18
|
+
"impact": "positive",
|
19
|
+
"message": "Table has zero or one row, no change required.",
|
20
|
+
"suggestion": ""
|
21
|
+
},
|
22
|
+
"const": {
|
23
|
+
"impact": "positive",
|
24
|
+
"message": "Table has only one indexed matching row, fastest join type.",
|
25
|
+
"suggestion": ""
|
26
|
+
},
|
27
|
+
"eq_ref": {
|
28
|
+
"impact": "positive",
|
29
|
+
"message": "All index parts used in join and index is primary_key or unique not null.",
|
30
|
+
"suggestion": ""
|
31
|
+
},
|
32
|
+
"ref": {
|
33
|
+
"impact": "caution",
|
34
|
+
"message": "All matching rows of an indexed column read for each combination of rows from previous table.",
|
35
|
+
"suggestion": "Ensure the referenced column is indexed and look for null values, dupilcates. Upgrade to eq_ref join type if possible.\nYou can acheive eq_ref by adding unique and not null to the index - $key used in $table table table."
|
36
|
+
},
|
37
|
+
"fulltext": {
|
38
|
+
"impact": "caution",
|
39
|
+
"message": "Join uses table FULLTEXT index, index key used - $key.",
|
40
|
+
"suggestion": "Should only be used for text heavy columns."
|
41
|
+
},
|
42
|
+
"ref_or_null": {
|
43
|
+
"impact": "caution",
|
44
|
+
"message": "Using ref index with Null value in $table table.",
|
45
|
+
"suggestion": "Please check if you can upgrade to eq_ref, you can acheive eq_ref by adding unique and not null to the index - $key used in $table table."
|
46
|
+
},
|
47
|
+
"index_merge": {
|
48
|
+
"impact": "caution",
|
49
|
+
"message": "Join uses list of indexes, keys used: $key.",
|
50
|
+
"suggestion": "Slow if the indexes are not well-chosen or if there are too many indexes being used."
|
51
|
+
},
|
52
|
+
"range": {
|
53
|
+
"impact": "caution",
|
54
|
+
"message": "Index used to find matching rows in specific range.",
|
55
|
+
"suggestion": "Please check the range it should not be too broad."
|
56
|
+
},
|
57
|
+
"index": {
|
58
|
+
"impact": "caution",
|
59
|
+
"message": "Entire index tree scanned to find matching rows.",
|
60
|
+
"suggestion": "Can be slow for large indexes(Your key length: $key_len), use carefully."
|
61
|
+
},
|
62
|
+
"ALL": {
|
63
|
+
"impact": "negative",
|
64
|
+
"message": "Entire $table table is scanned to find matching rows, you have $amount_possible_keys possible keys to use.",
|
65
|
+
"suggestion": "Use index here. You can use index from possible key: $possible_keys or add new one to $table table as per the requirements."
|
66
|
+
}
|
67
|
+
}
|
68
|
+
},
|
69
|
+
"rows": {
|
70
|
+
"description": "Estimated number of rows scanned to find matching rows.",
|
71
|
+
"value_type": "number",
|
72
|
+
"rules": {
|
73
|
+
"threshold": {
|
74
|
+
"amount": 100,
|
75
|
+
"impact": "negative",
|
76
|
+
"message": "$value rows are being scanned per join for $table table.",
|
77
|
+
"suggestion": "Please see if it is possible to use index from $possible_keys or add new one to $table table as per the requirements to reduce the number of rows scanned."
|
78
|
+
}
|
79
|
+
}
|
80
|
+
},
|
81
|
+
"possible_keys": {
|
82
|
+
"description": "Index keys possible for a specifc table",
|
83
|
+
"value_type": "array",
|
84
|
+
"delimiter": ",",
|
85
|
+
"rules": {
|
86
|
+
"absent": {
|
87
|
+
"impact": "negative",
|
88
|
+
"message": "There are no possible keys for $table table to be used, can result into full scan",
|
89
|
+
"suggestion": "Please add index keys for $table table"
|
90
|
+
},
|
91
|
+
"threshold": {
|
92
|
+
"amount": 5,
|
93
|
+
"impact": "negative",
|
94
|
+
"message": "There are $amount possible keys for $table table, having too many index keys can be unoptimal",
|
95
|
+
"suggestion": "Please check if there are extra indexes in $table table."
|
96
|
+
}
|
97
|
+
}
|
98
|
+
},
|
99
|
+
"key": {
|
100
|
+
"description": "",
|
101
|
+
"value_type": "string",
|
102
|
+
"rules": {
|
103
|
+
"absent": {
|
104
|
+
"impact": "negative",
|
105
|
+
"message": "There is no index key used for $table table, and can result into full scan of the $table table",
|
106
|
+
"suggestion": "Please use index from possible_keys: $possible_keys or add new one to $table table as per the requirements."
|
107
|
+
}
|
108
|
+
}
|
109
|
+
},
|
110
|
+
"key_len": {
|
111
|
+
"description": "Length of the key index used",
|
112
|
+
"value_type": "number",
|
113
|
+
"rules": {}
|
114
|
+
},
|
115
|
+
"filtered": {
|
116
|
+
"description": "Indicates percentage of rows appearing from the total.",
|
117
|
+
"value_type": "number",
|
118
|
+
"rules": {}
|
119
|
+
},
|
120
|
+
"extra": {
|
121
|
+
"description": "Additional information about the plan",
|
122
|
+
"value_type": "array",
|
123
|
+
"delimiter": ";",
|
124
|
+
"rules": {
|
125
|
+
"Using temporary": {
|
126
|
+
"impact": "",
|
127
|
+
"message": "",
|
128
|
+
"suggestion": ""
|
129
|
+
},
|
130
|
+
"Using filesort": {
|
131
|
+
"impact": "negative",
|
132
|
+
"message": "A file-based algorithm in being applied over your result, This can be inefficient and result into long query time.",
|
133
|
+
"suggestion": "Please ensure either result set is small or use proper index."
|
134
|
+
},
|
135
|
+
"Using join buffer": {
|
136
|
+
"impact": "",
|
137
|
+
"message": "",
|
138
|
+
"suggestion": ""
|
139
|
+
},
|
140
|
+
"Using index condition": {
|
141
|
+
"impact": "",
|
142
|
+
"message": "",
|
143
|
+
"suggestion": ""
|
144
|
+
}
|
145
|
+
}
|
146
|
+
},
|
147
|
+
"detailed#used_columns": {
|
148
|
+
"description": "",
|
149
|
+
"value_type": "array",
|
150
|
+
"rules": {
|
151
|
+
"threshold": {
|
152
|
+
"amount": 7,
|
153
|
+
"impact": "negative",
|
154
|
+
"message": "You have selected $amount columns, You should not select too many columns.",
|
155
|
+
"suggestion": "Please only select required columns."
|
156
|
+
}
|
157
|
+
}
|
158
|
+
},
|
159
|
+
"cardinality": {
|
160
|
+
"description": "",
|
161
|
+
"value_type": "number",
|
162
|
+
"rules": {
|
163
|
+
"threshold": {
|
164
|
+
"amount": 100,
|
165
|
+
"impact": "negative",
|
166
|
+
"message": "The cardinality of table is $amount, and its too high.",
|
167
|
+
"suggestion": "Please use proper index, query only requried data and ensure you are using proper joins."
|
168
|
+
}
|
169
|
+
}
|
170
|
+
}
|
171
|
+
}
|