diff --git a/advisor/heuristic.go b/advisor/heuristic.go index caf4e2069f3b4726e5e7f9b3b6baab663973cbb0..faca2d3651aa89cc100aba8624dfbe22606bcc1c 100644 --- a/advisor/heuristic.go +++ b/advisor/heuristic.go @@ -3179,6 +3179,38 @@ func (q *Query4Audit) RuleVarcharLength() Rule { return rule } +// RuleColumnNotAllowType COL.018 +func (q *Query4Audit) RuleColumnNotAllowType() Rule { + var rule = q.RuleOK() + + if len(common.Config.ColumnNotAllowType) == 0 { + return rule + } + + switch s := q.Stmt.(type) { + case *sqlparser.DDL: + switch s.Action { + case "create", "alter": + tks := ast.Tokenize(q.Query) + for _, tk := range tks { + if tk.Type == ast.TokenTypeWord { + for _, tp := range common.Config.ColumnNotAllowType { + if len(tk.Val) <= len(tp)+1 && + strings.HasPrefix(strings.ToLower(tk.Val), strings.ToLower(tp)) { + rule = HeuristicRules["COL.018"] + break + } + } + } + if rule.Item != "OK" { + break + } + } + } + } + return rule +} + // RuleNoOSCKey KEY.002 func (q *Query4Audit) RuleNoOSCKey() Rule { var rule = q.RuleOK() diff --git a/advisor/heuristic_test.go b/advisor/heuristic_test.go index 0744e4757bd5542382235fecead77ba494eb73c1..6ddb7a29aea1d3939c5a3795e0122319cab707a6 100644 --- a/advisor/heuristic_test.go +++ b/advisor/heuristic_test.go @@ -2988,6 +2988,48 @@ func TestRuleVarcharLength(t *testing.T) { common.Log.Debug("Exiting function: %s", common.GetFunctionName()) } +// COL.018 +func TestRuleColumnNotAllowType(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + + sqls := [][]string{ + { + "CREATE TABLE tab (a BOOLEAN);", + "CREATE TABLE tab (a BOOLEAN );", + "ALTER TABLE `tb` add column `a` BOOLEAN;", + }, + { + "CREATE TABLE `tb` ( `id` varchar(1024));", + "ALTER TABLE `tb` add column `id` varchar(35);", + }, + } + + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleColumnNotAllowType() + if rule.Item != "COL.018" { + t.Error("Rule not match:", rule.Item, "Expect : COL.018") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleColumnNotAllowType() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + // KEY.002 func TestRuleNoOSCKey(t *testing.T) { common.Log.Debug("Entering function: %s", common.GetFunctionName()) diff --git a/advisor/rules.go b/advisor/rules.go index 0c882c9c043d7a9a315ca7a820cf6cf4eabd0c94..64e4354fa7bff1acf17b0a4d8e7e06941de0a4e6 100644 --- a/advisor/rules.go +++ b/advisor/rules.go @@ -553,6 +553,14 @@ func init() { Case: "CREATE TABLE tab (a varchar(3500));", Func: (*Query4Audit).RuleVarcharLength, }, + "COL.018": { + Item: "COL.018", + Severity: "L1", + Summary: "建表语句中使用了不推荐的字段类型", + Content: "以下字段类型不被推荐使用:" + strings.Join(common.Config.ColumnNotAllowType, ","), + Case: "CREATE TABLE tab (a BOOLEAN);", + Func: (*Query4Audit).RuleColumnNotAllowType, + }, "DIS.001": { Item: "DIS.001", Severity: "L1", diff --git a/advisor/testdata/TestListHeuristicRules.golden b/advisor/testdata/TestListHeuristicRules.golden index 765dbcf585bdabf90f3edfb1ea54f66074ac8a3c..7346b5ed97419592d19a72959db2ef3f5ba06436 100644 --- a/advisor/testdata/TestListHeuristicRules.golden +++ b/advisor/testdata/TestListHeuristicRules.golden @@ -512,6 +512,16 @@ CREATE TABLE tab (a INT(1)); ```sql CREATE TABLE tab (a varchar(3500)); ``` +## 建表语句中使用了不推荐的字段类型 + +* **Item**:COL.018 +* **Severity**:L1 +* **Content**:以下字段类型不被推荐使用:boolean +* **Case**: + +```sql +CREATE TABLE tab (a BOOLEAN); +``` ## 消除不必要的 DISTINCT 条件 * **Item**:DIS.001 diff --git a/advisor/testdata/TestMergeConflictHeuristicRules.golden b/advisor/testdata/TestMergeConflictHeuristicRules.golden index 6986d1cf483a2d32981b90289bf1c1f7f182e497..9dd8a26aaebd09545cfcaba0520b1aef6d4a9f38 100644 --- a/advisor/testdata/TestMergeConflictHeuristicRules.golden +++ b/advisor/testdata/TestMergeConflictHeuristicRules.golden @@ -48,6 +48,7 @@ advisor.Rule{Item:"COL.014", Severity:"L5", Summary:"为列指定了字符集", advisor.Rule{Item:"COL.015", Severity:"L4", Summary:"TEXT 和 BLOB 类型的字段不可指定非 NULL 的默认值", Content:"MySQL 数据库中 TEXT 和 BLOB 类型的字段不可指定非 NULL 的默认值。TEXT最大长度为2^16-1个字符,MEDIUMTEXT最大长度为2^32-1个字符,LONGTEXT最大长度为2^64-1个字符。", Case:"CREATE TABLE `tbl` (`c` blob DEFAULT NULL);", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} advisor.Rule{Item:"COL.016", Severity:"L1", Summary:"整型定义建议采用 INT(10) 或 BIGINT(20)", Content:"INT(M) 在 integer 数据类型中,M 表示最大显示宽度。 在 INT(M) 中,M 的值跟 INT(M) 所占多少存储空间并无任何关系。 INT(3)、INT(4)、INT(8) 在磁盘上都是占用 4 bytes 的存储空间。", Case:"CREATE TABLE tab (a INT(1));", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} advisor.Rule{Item:"COL.017", Severity:"L2", Summary:"VARCHAR 定义长度过长", Content:"varchar 是可变长字符串,不预先分配存储空间,长度不要超过1024,如果存储长度过长 MySQL 将定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。", Case:"CREATE TABLE tab (a varchar(3500));", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.018", Severity:"L1", Summary:"建表语句中使用了不推荐的字段类型", Content:"以下字段类型不被推荐使用:boolean", Case:"CREATE TABLE tab (a BOOLEAN);", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} advisor.Rule{Item:"DIS.001", Severity:"L1", Summary:"消除不必要的 DISTINCT 条件", Content:"太多DISTINCT条件是复杂的裹脚布式查询的症状。考虑将复杂查询分解成许多简单的查询,并减少DISTINCT条件的数量。如果主键列是列的结果集的一部分,则DISTINCT条件可能没有影响。", Case:"SELECT DISTINCT c.c_id,count(DISTINCT c.c_name),count(DISTINCT c.c_e),count(DISTINCT c.c_n),count(DISTINCT c.c_me),c.c_d FROM (select distinct id, name from B) as e WHERE e.country_id = c.country_id", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} advisor.Rule{Item:"DIS.002", Severity:"L3", Summary:"COUNT(DISTINCT) 多列时结果可能和你预想的不同", Content:"COUNT(DISTINCT col) 计算该列除NULL之外的不重复行数,注意 COUNT(DISTINCT col, col2) 如果其中一列全为 NULL 那么即使另一列有不同的值,也返回0。", Case:"SELECT COUNT(DISTINCT col, col2) FROM tbl;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} advisor.Rule{Item:"DIS.003", Severity:"L3", Summary:"DISTINCT * 对有主键的表没有意义", Content:"当表已经有主键时,对所有列进行 DISTINCT 的输出结果与不进行 DISTINCT 操作的结果相同,请不要画蛇添足。", Case:"SELECT DISTINCT * FROM film;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} diff --git a/common/config.go b/common/config.go index 9c257de090a101ed685db2561f7a80a57d6c7710..29a6998ee072b9e976e10d9300c94f5796eb1963 100644 --- a/common/config.go +++ b/common/config.go @@ -107,6 +107,7 @@ type Configuration struct { UkPrefix string `yaml:"unique-key-prefix"` // 唯一键建议使用的前缀 MaxSubqueryDepth int `yaml:"max-subquery-depth"` // 子查询最大尝试 MaxVarcharLength int `yaml:"max-varchar-length"` // varchar最大长度 + ColumnNotAllowType []string `yaml:"column-not-allow-type"` // 字段不允许使用的数据类型 // ++++++++++++++EXPLAIN检查项+++++++++++++ ExplainSQLReportType string `yaml:"explain-sql-report-type"` // EXPLAIN markdown 格式输出 SQL 样式,支持 sample, fingerprint, pretty 等 @@ -189,6 +190,7 @@ var Config = &Configuration{ UkPrefix: "uk_", MaxSubqueryDepth: 5, MaxVarcharLength: 1024, + ColumnNotAllowType: []string{"boolean"}, MarkdownExtensions: 94, MarkdownHTMLFlags: 0, @@ -542,6 +544,7 @@ func readCmdFlags() error { ukPrefix := flag.String("unique-key-prefix", Config.UkPrefix, "UkPrefix") maxSubqueryDepth := flag.Int("max-subquery-depth", Config.MaxSubqueryDepth, "MaxSubqueryDepth") maxVarcharLength := flag.Int("max-varchar-length", Config.MaxVarcharLength, "MaxVarcharLength") + columnNotAllowType := flag.String("column-not-allow-type", strings.Join(Config.ColumnNotAllowType, ","), "ColumnNotAllowType") // ++++++++++++++EXPLAIN检查项+++++++++++++ explainSQLReportType := flag.String("explain-sql-report-type", strings.ToLower(Config.ExplainSQLReportType), "ExplainSQLReportType [pretty, sample, fingerprint]") explainType := flag.String("explain-type", strings.ToLower(Config.ExplainType), "ExplainType [extended, partitions, traditional]") @@ -674,6 +677,9 @@ func readCmdFlags() error { Config.DryRun = *dryrun Config.MaxPrettySQLLength = *maxPrettySQLLength Config.MaxVarcharLength = *maxVarcharLength + if *columnNotAllowType != "" { + Config.ColumnNotAllowType = strings.Split(strings.ToLower(*columnNotAllowType), ",") + } PrintVersion = *printVersion PrintConfig = *printConfig diff --git a/common/testdata/TestPrintConfiguration.golden b/common/testdata/TestPrintConfiguration.golden index af687104cd0d9bbfcc98260827ace1b2e475c6b5..6b7bf9e4faba18d95d28d4cc9489247c9c0dd3e1 100644 --- a/common/testdata/TestPrintConfiguration.golden +++ b/common/testdata/TestPrintConfiguration.golden @@ -68,6 +68,8 @@ index-prefix: idx_ unique-key-prefix: uk_ max-subquery-depth: 5 max-varchar-length: 1024 +column-not-allow-type: +- boolean explain-sql-report-type: pretty explain-type: extended explain-format: traditional diff --git a/doc/FAQ.md b/doc/FAQ.md index 5c2c5be2a8d64185101b34059ebd8776c366731f..fdec55155f951e92e5736f50b44a20c4c8fb3d9d 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -48,6 +48,16 @@ bash: ./soar.linux-amd64: cannot execute binary file 请注意您操作系统类型,`soar.linux-amd64` 为 Linux 系统使用的二进制文件,`soar.darwin-amd64` 为苹果系统使用的二进制文件,`soar.windows-amd64` 是微软用户使用的二进制文件。下载文件后 Linux 和苹果用户需要为文件添加可执行权限 `chmod a+x filename`。 +## 命令无法找到 + +```bash +bash: soar: command not found +``` + +直接执行 `soar` 命令提示命令无法找到,请先将 soar 文件添加可执行权限 `chmod a+x soar` 然后将可以将 soar 所在路径加到[PATH](https://linuxconfig.org/linux-path-environment-variable)中,也可以将 soar 移动到已有 PATH 中。 + +当然在 Linux 环境下,在 soar 二进制文件所在路径运行 `./soar` 也同样可以解决您的问题。 + ## 提示语法错误 * 请检查SQL语句中是否出现了不配对的引号,如 `, ", ' diff --git a/doc/heuristic.md b/doc/heuristic.md index 765dbcf585bdabf90f3edfb1ea54f66074ac8a3c..7346b5ed97419592d19a72959db2ef3f5ba06436 100644 --- a/doc/heuristic.md +++ b/doc/heuristic.md @@ -512,6 +512,16 @@ CREATE TABLE tab (a INT(1)); ```sql CREATE TABLE tab (a varchar(3500)); ``` +## 建表语句中使用了不推荐的字段类型 + +* **Item**:COL.018 +* **Severity**:L1 +* **Content**:以下字段类型不被推荐使用:boolean +* **Case**: + +```sql +CREATE TABLE tab (a BOOLEAN); +``` ## 消除不必要的 DISTINCT 条件 * **Item**:DIS.001