Skip to content

Commit ddc2b93

Browse files
authored
Merge pull request #103 from macbre/having_clause
Report queries with HAVING clause
2 parents 08d413a + bce09fd commit ddc2b93

File tree

6 files changed

+109
-2
lines changed

6 files changed

+109
-2
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Analyses your database queries and schema and suggests indices improvements. You
1313
* reports text columns with character set different than `utf`
1414
* reports queries that do not use indices
1515
* reports queries that use filesort, temporary file or full table scan
16-
* reports queries that are not quite kosher (e.g. `LIKE "%foo%"`, `INSERT IGNORE`, `SELECT *`)
16+
* reports queries that are not quite kosher (e.g. `LIKE "%foo%"`, `INSERT IGNORE`, `SELECT *`, `HAVING` clause)
1717

1818
This tool **supports MySQL 5.5, 5.6, 5.7, 8.0 and MariaDB 10.0, 10.2** and runs under **Python 2.7, 3.4, 3.5 and 3.6**.
1919

@@ -237,6 +237,13 @@ select_star → table affected: bar
237237
238238
- query: SELECT t.* FROM bar AS t;
239239
240+
------------------------------------------------------------
241+
having_clause → table affected: sales
242+
243+
✗ "SELECT s.cust_id,count(s.cust_id) FROM SH.sales s ..." query uses HAVING clause
244+
245+
- query: SELECT s.cust_id,count(s.cust_id) FROM SH.sales s GROUP BY s.cust_id HAVING s.cust_id != '1660' AND s.cust_id != '2'
246+
240247
(...)
241248
242249
------------------------------------------------------------
@@ -280,6 +287,8 @@ Outputs YML file with results and metadata.
280287

281288
## Checks
282289

290+
> **Number of checks**: 17
291+
283292
* `redundant_indices`: reports indices that are redundant and covered by other
284293
* `non_utf_columns`: reports text columns that have characters encoding set to `latin1` (utf is the way to go)
285294
* `missing_primary_index`: reports tables with no primary or unique key (see [MySQL bug #76252](https://bugs.mysql.com/bug.php?id=76252) and [Wikia/app#9863](https://github.com/Wikia/app/pull/9863))
@@ -301,6 +310,7 @@ Outputs YML file with results and metadata.
301310
* `selects_with_like`: reports SELECT queries that use `LIKE '%foo'` conditions (they can not use an index)
302311
* `insert_ignore`: reports [queries using `INSERT IGNORE`](https://medium.com/legacy-systems-diary/things-to-avoid-episode-1-insert-ignore-535b4c24406b)
303312
* `select_star`: reports [queries using `SELECT *`](https://github.com/jarulraj/sqlcheck/blob/master/docs/query/3001.md)
313+
* `having_clause`: reports [queries using `HAVING` clause](https://github.com/jarulraj/sqlcheck/blob/master/docs/query/3012.md)
304314

305315
## Success stories
306316

indexdigest/cli/script.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@
4848
check_insert_ignore_queries, \
4949
check_single_column, \
5050
check_empty_tables, \
51-
check_select_star
51+
check_select_star, \
52+
check_having_clause
5253

5354

5455
def main():
@@ -101,6 +102,7 @@ def main():
101102
check_selects_with_like(database, queries=queries),
102103
check_insert_ignore_queries(database, queries=queries),
103104
check_select_star(database, queries=queries),
105+
check_having_clause(database, queries=queries),
104106
)
105107

106108
# handle --format

indexdigest/linters/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@
1717
from .linter_0075_test_tables import check_test_tables
1818
from .linter_0089_empty_tables import check_empty_tables
1919
from .linter_0092_select_star import check_select_star
20+
from .linter_0093_having_clause import check_having_clause
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""
2+
This linter checks for select queries with HAVING clause
3+
"""
4+
from sqlparse.tokens import Keyword
5+
from sql_metadata import preprocess_query, get_query_tables, get_query_tokens
6+
7+
from indexdigest.utils import LinterEntry, shorten_query, is_select_query
8+
9+
10+
def query_has_having_clause(query):
11+
"""
12+
Checks if provided query uses HAVING clause
13+
:type query str
14+
:rtype bool
15+
"""
16+
if not is_select_query(query):
17+
return False
18+
19+
query = preprocess_query(query)
20+
tokens = get_query_tokens(query)
21+
22+
for token in tokens:
23+
if token.ttype is Keyword and str(token).upper() == 'HAVING':
24+
return True
25+
26+
return False
27+
28+
29+
def check_having_clause(_, queries):
30+
"""
31+
:type queries list[str]
32+
:rtype: list[LinterEntry]
33+
"""
34+
queries_with_having_clause = [
35+
query for query in queries
36+
if query_has_having_clause(query)
37+
]
38+
39+
for query in queries_with_having_clause:
40+
table_name = get_query_tables(query)[0]
41+
42+
yield LinterEntry(linter_type='having_clause', table_name=table_name,
43+
message='"{}" query uses HAVING clause'.
44+
format(shorten_query(query)),
45+
context={"query": query})
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from __future__ import print_function
2+
3+
from unittest import TestCase
4+
5+
from indexdigest.linters.linter_0093_having_clause import query_has_having_clause, check_having_clause
6+
from indexdigest.test import DatabaseTestMixin, read_queries_from_log
7+
8+
9+
class TestLinter(TestCase, DatabaseTestMixin):
10+
11+
def test_query_has_having_clause(self):
12+
assert query_has_having_clause('SELECT * FROM foo having bar = 2')
13+
assert query_has_having_clause('SELECT * FROM foo HAVING bar = 2')
14+
15+
assert query_has_having_clause("SELECT * FROM 0019_queries_not_using_indices "
16+
"WHERE foo = 'foo' HAVING bar = 'test'")
17+
assert query_has_having_clause("SELECT s.cust_id,count(s.cust_id) FROM SH.sales s "
18+
"GROUP BY s.cust_id HAVING s.cust_id != '1660' AND s.cust_id != '2'")
19+
20+
assert query_has_having_clause('SELECT * FROM foo') is False
21+
assert query_has_having_clause('SELECT * FROM foo_having LIMIT 10') is False
22+
assert query_has_having_clause('SELECT /* having */ id FROM foo') is False
23+
24+
assert query_has_having_clause('INSERT 42 INTO having') is False
25+
26+
def test_having_clause(self):
27+
reports = list(check_having_clause(self.connection, read_queries_from_log('0093-having-clause-log')))
28+
29+
print(list(map(str, reports)))
30+
31+
assert len(reports) == 3
32+
33+
assert str(reports[0]) == 'foo: "SELECT * FROM foo HAVING bar = 2" query uses HAVING clause'
34+
assert reports[0].table_name == 'foo'
35+
assert reports[0].context['query'] == 'SELECT * FROM foo HAVING bar = 2;'
36+
37+
assert str(reports[1]) == 'sales: "SELECT s.cust_id,count(s.cust_id) ' \
38+
'FROM SH.sales s ..." query uses HAVING clause'
39+
assert reports[1].table_name == 'sales'
40+
41+
assert str(reports[2]) == '0019_queries_not_using_indices: "SELECT * FROM ' \
42+
'`0019_queries_not_using_indices` WHE..." query uses HAVING clause'
43+
assert reports[2].table_name == '0019_queries_not_using_indices'
44+
45+
# assert False

sql/0093-having-clause-log

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- Rewriting the query's HAVING clause into a predicate will enable the use of indexes during query processing.
2+
SELECT * FROM foo HAVING bar = 2;
3+
SELECT s.cust_id,count(s.cust_id) FROM SH.sales s GROUP BY s.cust_id HAVING s.cust_id != '1660' AND s.cust_id != '2'
4+
SELECT * FROM `0019_queries_not_using_indices` WHERE foo = 'foo' HAVING bar = 'test';

0 commit comments

Comments
 (0)