diff --git a/advisor/explainer_test.go b/advisor/explainer_test.go index eb37f44a2108b2885707fe040bafc29281f3eaef..413939e751757711845d148896aa89ed2ce70a20 100644 --- a/advisor/explainer_test.go +++ b/advisor/explainer_test.go @@ -31,7 +31,13 @@ func TestDigestExplainText(t *testing.T) { | 1 | SIMPLE | city | ref | idx_fk_country_id,idx_country_id_city,idx_all,idx_other | idx_fk_country_id | 2 | sakila.country.country_id | 2 | Using index | +----+-------------+---------+-------+---------------------------------------------------------+-------------------+---------+---------------------------+------+-------------+` common.Config.ReportType = "explain-digest" - err := common.GoldenDiff(func() { DigestExplainText(text) }, t.Name(), update) + err := common.GoldenDiff(func() { + DigestExplainText(text) + orgReportType := common.Config.ReportType + common.Config.ReportType = "html" + DigestExplainText(text) + common.Config.ReportType = orgReportType + }, t.Name(), update) if nil != err { t.Fatal(err) } diff --git a/advisor/testdata/TestDigestExplainText.golden b/advisor/testdata/TestDigestExplainText.golden index b0311993f107f4163e6a4e204f001a5f3ccf7657..c3da0fb4a937305c52a60d52e5f612b96ca9e96d 100644 --- a/advisor/testdata/TestDigestExplainText.golden +++ b/advisor/testdata/TestDigestExplainText.golden @@ -24,3 +24,112 @@ * **Using index**: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询. + + +SQL优化分析报告 + + + + + +

Explain信息

+ +

★ ★ ★ ★ ★ 100分

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredscalabilityExtra
1SIMPLEcountryNULLindexPRIMARY,
country_id
country152NULL00.00%☠️ O(n)Using index
1SIMPLEcityNULLrefidx_fk_country_id,
idx_country_id_city,
idx_all,
idx_other
idx_fk_country_id2sakila.country.country_id00.00%☠️ O(n)Using index
+ +

Explain信息解读

+ +

SelectType信息解读

+ + + +

Type信息解读

+ + + +

Extra信息解读

+ + + diff --git a/ast/rewrite.go b/ast/rewrite.go index c7a07cc1409e62efb1650739cd55fee3ec3e1e91..32cc9e78f70c98ffe6d2f93381c71be4c5a5c692 100644 --- a/ast/rewrite.go +++ b/ast/rewrite.go @@ -312,7 +312,7 @@ func (rw *Rewrite) RewriteStandard() *Rewrite { return rw } -// RewriteAlwaysTrue alwaystrue: 删除恒真条件 +// RewriteAlwaysTrue always true: 删除恒真条件 func (rw *Rewrite) RewriteAlwaysTrue() (reWriter *Rewrite) { array := NewNodeList(rw.Stmt) tNode := array.Head @@ -340,7 +340,7 @@ func isAlwaysTrue(expr *sqlparser.ComparisonExpr) bool { expr.Operator = "!=" case "<=>": expr.Operator = "=" - case ">=", "<=", "!=", "=": + case ">=", "<=", "!=", "=", ">", "<": default: return false } diff --git a/ast/rewrite_test.go b/ast/rewrite_test.go index 817e2eb43699b4bfdaee95238105a78824bea6aa..d95433dc8420636a2d1a6bde2ec2f3424ba789ac 100644 --- a/ast/rewrite_test.go +++ b/ast/rewrite_test.go @@ -433,10 +433,18 @@ func TestRewriteAlwaysTrue(t *testing.T) { "input": "SELECT count(col) FROM tbl where 1>=1;", "output": "select count(col) from tbl", }, + { + "input": "SELECT count(col) FROM tbl where 2>1;", + "output": "select count(col) from tbl", + }, { "input": "SELECT count(col) FROM tbl where 1<=1;", "output": "select count(col) from tbl", }, + { + "input": "SELECT count(col) FROM tbl where 1<2;", + "output": "select count(col) from tbl", + }, { "input": "SELECT count(col) FROM tbl where 1=1 and 2=2;", "output": "select count(col) from tbl", @@ -461,6 +469,10 @@ func TestRewriteAlwaysTrue(t *testing.T) { "input": "SELECT count(col) FROM tbl where (1=1);", "output": "select count(col) from tbl", }, + { + "input": "SELECT count(col) FROM tbl where a=1;", + "output": "select count(col) from tbl where a = 1", + }, { "input": "SELECT count(col) FROM tbl where ('a'= 'a' or 'b' = 'b') and a = 'b';", "output": "select count(col) from tbl where a = 'b'", @@ -777,6 +789,10 @@ func TestListRewriteRules(t *testing.T) { common.Log.Debug("Entering function: %s", common.GetFunctionName()) err := common.GoldenDiff(func() { ListRewriteRules(RewriteRules) + orgReportType := common.Config.ReportType + common.Config.ReportType = "json" + ListRewriteRules(RewriteRules) + common.Config.ReportType = orgReportType }, t.Name(), update) if err != nil { t.Error(err) diff --git a/ast/testdata/TestListRewriteRules.golden b/ast/testdata/TestListRewriteRules.golden index 80e6fb3a056bb16780d7eaea19146625e187c232..e855f915a27d209ce8bc72a9eabd143673607874 100644 --- a/ast/testdata/TestListRewriteRules.golden +++ b/ast/testdata/TestListRewriteRules.golden @@ -270,3 +270,143 @@ use sakila ```sql use sakila; ``` +[ + { + "Name": "dml2select", + "Description": "将数据库更新请求转换为只读查询请求,便于执行EXPLAIN", + "Original": "DELETE FROM film WHERE length \u003e 100", + "Suggest": "select * from film where length \u003e 100" + }, + { + "Name": "star2columns", + "Description": "为SELECT *补全表的列信息", + "Original": "SELECT * FROM film", + "Suggest": "select film.film_id, film.title from film" + }, + { + "Name": "insertcolumns", + "Description": "为INSERT补全表的列信息", + "Original": "insert into film values(1,2,3,4,5)", + "Suggest": "insert into film(film_id, title, description, release_year, language_id) values (1, 2, 3, 4, 5)" + }, + { + "Name": "having", + "Description": "将查询的 HAVING 子句改写为 WHERE 中的查询条件", + "Original": "SELECT state, COUNT(*) FROM Drivers GROUP BY state HAVING state IN ('GA', 'TX') ORDER BY state", + "Suggest": "select state, COUNT(*) from Drivers where state in ('GA', 'TX') group by state order by state asc" + }, + { + "Name": "orderbynull", + "Description": "如果 GROUP BY 语句不指定 ORDER BY 条件会导致无谓的排序产生,如果不需要排序建议添加 ORDER BY NULL", + "Original": "SELECT sum(col1) FROM tbl GROUP BY col", + "Suggest": "select sum(col1) from tbl group by col order by null" + }, + { + "Name": "unionall", + "Description": "可以接受重复的时间,使用 UNION ALL 替代 UNION 以提高查询效率", + "Original": "select country_id from city union select country_id from country", + "Suggest": "select country_id from city union all select country_id from country" + }, + { + "Name": "or2in", + "Description": "将同一列不同条件的 OR 查询转写为 IN 查询", + "Original": "select country_id from city where col1 = 1 or (col2 = 1 or col2 = 2 ) or col1 = 3;", + "Suggest": "select country_id from city where (col2 in (1, 2)) or col1 in (1, 3);" + }, + { + "Name": "innull", + "Description": "如果 IN 条件中可能有 NULL 值而又想匹配 NULL 值时,建议添加OR col IS NULL", + "Original": "暂不支持", + "Suggest": "暂不支持" + }, + { + "Name": "or2union", + "Description": "将不同列的 OR 查询转为 UNION 查询,建议结合 unionall 重写策略一起使用", + "Original": "暂不支持", + "Suggest": "暂不支持" + }, + { + "Name": "dmlorderby", + "Description": "删除 DML 更新操作中无意义的 ORDER BY", + "Original": "DELETE FROM tbl WHERE col1=1 ORDER BY col", + "Suggest": "delete from tbl where col1 = 1" + }, + { + "Name": "sub2join", + "Description": "将子查询转换为JOIN查询", + "Original": "暂不支持", + "Suggest": "暂不支持" + }, + { + "Name": "join2sub", + "Description": "将JOIN查询转换为子查询", + "Original": "暂不支持", + "Suggest": "暂不支持" + }, + { + "Name": "distinctstar", + "Description": "DISTINCT *对有主键的表没有意义,可以将DISTINCT删掉", + "Original": "SELECT DISTINCT * FROM film;", + "Suggest": "SELECT * FROM film" + }, + { + "Name": "standard", + "Description": "SQL标准化,如:关键字转换为小写", + "Original": "SELECT sum(col1) FROM tbl GROUP BY 1;", + "Suggest": "select sum(col1) from tbl group by 1" + }, + { + "Name": "mergealter", + "Description": "合并同一张表的多条ALTER语句", + "Original": "ALTER TABLE t2 DROP COLUMN c;ALTER TABLE t2 DROP COLUMN d;", + "Suggest": "ALTER TABLE t2 DROP COLUMN c, DROP COLUMN d;" + }, + { + "Name": "alwaystrue", + "Description": "删除无用的恒真判断条件", + "Original": "SELECT count(col) FROM tbl where 'a'= 'a' or ('b' = 'b' and a = 'b');", + "Suggest": "select count(col) from tbl where (a = 'b');" + }, + { + "Name": "countstar", + "Description": "不建议使用COUNT(col)或COUNT(常量),建议改写为COUNT(*)", + "Original": "SELECT count(col) FROM tbl GROUP BY 1;", + "Suggest": "SELECT count(*) FROM tbl GROUP BY 1;" + }, + { + "Name": "innodb", + "Description": "建表时建议使用InnoDB引擎,非 InnoDB 引擎表自动转 InnoDB", + "Original": "CREATE TABLE t1(id bigint(20) NOT NULL AUTO_INCREMENT);", + "Suggest": "create table t1 (\n\tid bigint(20) not null auto_increment\n) ENGINE=InnoDB;" + }, + { + "Name": "autoincrement", + "Description": "将autoincrement初始化为1", + "Original": "CREATE TABLE t1(id bigint(20) NOT NULL AUTO_INCREMENT) ENGINE=InnoDB AUTO_INCREMENT=123802;", + "Suggest": "create table t1(id bigint(20) not null auto_increment) ENGINE=InnoDB auto_increment=1;" + }, + { + "Name": "intwidth", + "Description": "整型数据类型修改默认显示宽度", + "Original": "create table t1 (id int(20) not null auto_increment) ENGINE=InnoDB;", + "Suggest": "create table t1 (id int(10) not null auto_increment) ENGINE=InnoDB;" + }, + { + "Name": "truncate", + "Description": "不带 WHERE 条件的 DELETE 操作建议修改为 TRUNCATE", + "Original": "DELETE FROM tbl", + "Suggest": "truncate table tbl" + }, + { + "Name": "rmparenthesis", + "Description": "去除没有意义的括号", + "Original": "select col from table where (col = 1);", + "Suggest": "select col from table where col = 1;" + }, + { + "Name": "delimiter", + "Description": "补全DELIMITER", + "Original": "use sakila", + "Suggest": "use sakila;" + } +] diff --git a/database/explain.go b/database/explain.go index 1ca0b7ff35e879784867d0e48bbfbaaef4fdf50a..6ed5e08962f607bca29a5ecefab51019288f431d 100644 --- a/database/explain.go +++ b/database/explain.go @@ -604,6 +604,9 @@ func MySQLExplainWarnings(exp *ExplainInfo) string { // MySQLExplainQueryCost 将last_query_cost信息补充到评审结果中 func MySQLExplainQueryCost(exp *ExplainInfo) string { var content string + if exp == nil { + return content + } if exp.QueryCost > 0 { tmp := fmt.Sprintf("%.3f\n", exp.QueryCost) @@ -819,7 +822,7 @@ func parseTraditionalExplainText(content string) (explainRows []*ExplainRow, err } // filtered may larger than 100.00 // https://bugs.mysql.com/bug.php?id=34124 - if filtered > 100.00 { + if filtered >= 100.00 { filtered = 100.00 } @@ -1039,6 +1042,7 @@ func ParseExplainResult(res QueryResult, formatType int) (exp *ExplainInfo, err // Explain 获取 SQL 的 explain 信息 func (db *Connector) Explain(sql string, explainType int, formatType int) (exp *ExplainInfo, err error) { + exp = &ExplainInfo{SQL: sql} if explainType != TraditionalExplainType { formatType = TraditionalFormatExplain } @@ -1054,13 +1058,14 @@ func (db *Connector) Explain(sql string, explainType int, formatType int) (exp * }() // 执行EXPLAIN请求 - sql = db.explainQuery(sql, explainType, formatType) - res, err := db.Query(sql) + exp.SQL = db.explainQuery(sql, explainType, formatType) + res, err := db.Query(exp.SQL) + if err != nil { + return exp, err + } // 解析mysql结果,输出ExplainInfo exp, err = ParseExplainResult(res, formatType) - exp.SQL = sql - return exp, err } diff --git a/database/explain_test.go b/database/explain_test.go index c3fa31b4c7133450830ebfa7d2f02e069d972167..54320b74eec1e27c9276135c9146a0bdb6e9aab6 100644 --- a/database/explain_test.go +++ b/database/explain_test.go @@ -17,6 +17,7 @@ package database import ( + "fmt" "testing" "github.com/XiaoMi/soar/common" @@ -2439,12 +2440,13 @@ func TestMySQLExplainWarnings(t *testing.T) { func TestMySQLExplainQueryCost(t *testing.T) { common.Log.Debug("Entering function: %s", common.GetFunctionName()) - expInfo, err := connTest.Explain("select 1", TraditionalExplainType, TraditionalFormatExplain) - if err != nil { - t.Error(err) - } - err = common.GoldenDiff(func() { - MySQLExplainQueryCost(expInfo) + err := common.GoldenDiff(func() { + expInfo, err := connTest.Explain("select 1", TraditionalExplainType, TraditionalFormatExplain) + fmt.Println(err, MySQLExplainQueryCost(expInfo)) + expInfo, err = connTest.Explain("select 1", ExtendedExplainType, TraditionalFormatExplain) + fmt.Println(err, MySQLExplainQueryCost(expInfo)) + expInfo, err = connTest.Explain("select 1", TraditionalExplainType, JSONFormatExplain) + fmt.Println(err, MySQLExplainQueryCost(expInfo)) }, t.Name(), update) if err != nil { t.Error(err) diff --git a/database/mysql.go b/database/mysql.go index 06388a6b4b0037f9c97a0d4416bb8b24071db926..520c532cdd7badf71f007438b134afa332129cea 100644 --- a/database/mysql.go +++ b/database/mysql.go @@ -279,6 +279,7 @@ func (db *Connector) dangerousQuery(query string) bool { "show", "explain", "describe", + "desc", } for _, prefix := range whiteList { diff --git a/database/profiling_test.go b/database/profiling_test.go index d1e71ee26dbf0ee07132eed398334ac828815bd0..d1948ab1ef55439ad74c36d87b5991eda1bf25c0 100644 --- a/database/profiling_test.go +++ b/database/profiling_test.go @@ -30,6 +30,10 @@ func TestProfiling(t *testing.T) { t.Error(err) } pretty.Println(rows) + _, err = connTest.Profiling("delete from film") + if err == nil { + t.Error(err) + } common.Log.Debug("Exiting function: %s", common.GetFunctionName()) } diff --git a/database/testdata/TestMySQLExplainQueryCost.golden b/database/testdata/TestMySQLExplainQueryCost.golden index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1fae1e5478dfd21137f9f1c32f919075603ac246 100644 --- a/database/testdata/TestMySQLExplainQueryCost.golden +++ b/database/testdata/TestMySQLExplainQueryCost.golden @@ -0,0 +1,3 @@ + + +