diff --git a/advisor/explainer_test.go b/advisor/explainer_test.go
index eb37f44a..413939e7 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 b0311993..c3da0fb4 100644
--- a/advisor/testdata/TestDigestExplainText.golden
+++ b/advisor/testdata/TestDigestExplainText.golden
@@ -24,3 +24,112 @@
* **Using index**: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询.
+
+
+SQL优化分析报告
+
+
+
+
+
+Explain信息
+
+★ ★ ★ ★ ★ 100分
+
+
+
+
+id |
+select_type |
+table |
+partitions |
+type |
+possible_keys |
+key |
+key_len |
+ref |
+rows |
+filtered |
+scalability |
+Extra |
+
+
+
+
+
+1 |
+SIMPLE |
+country |
+NULL |
+index |
+PRIMARY, country_id |
+country |
+152 |
+NULL |
+0 |
+0.00% |
+☠️ O(n) |
+Using index |
+
+
+
+1 |
+SIMPLE |
+city |
+NULL |
+ref |
+idx_fk_country_id, idx_country_id_city, idx_all, idx_other |
+idx_fk_country_id |
+2 |
+sakila.country.country_id |
+0 |
+0.00% |
+☠️ O(n) |
+Using index |
+
+
+
+
+Explain信息解读
+
+SelectType信息解读
+
+
+- SIMPLE: 简单SELECT(不使用UNION或子查询等).
+
+
+Type信息解读
+
+
+index: 全表扫描, 只是扫描表的时候按照索引次序进行而不是行. 主要优点就是避免了排序, 但是开销仍然非常大.
+
+ref: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'.
+
+
+Extra信息解读
+
+
+- Using index: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询.
+
+
diff --git a/ast/rewrite.go b/ast/rewrite.go
index c7a07cc1..32cc9e78 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 817e2eb4..d95433dc 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 80e6fb3a..e855f915 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 1ca0b7ff..6ed5e089 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 c3fa31b4..54320b74 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 06388a6b..520c532c 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 d1e71ee2..d1948ab1 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 e69de29b..1fae1e54 100644
--- a/database/testdata/TestMySQLExplainQueryCost.golden
+++ b/database/testdata/TestMySQLExplainQueryCost.golden
@@ -0,0 +1,3 @@
+
+
+