SQL Query Auditor & Optimizer — analyze SQL for performance, security, and best practices
Every SQL query you write is a potential performance bottleneck, security vulnerability, or maintenance headache. Code linters catch bugs in Python, Go, and TypeScript — but who catches bugs in your SQL?
sqlaudit is a zero-dependency SQL static analyzer that catches:
- Performance killers:
SELECT *, missingWHEREonDELETE/UPDATE, leading wildcards inLIKE, excessive JOINs - Security risks: SQL injection patterns,
GRANT ALL, always-true conditions (1=1), comment injection - Correctness bugs:
= NULLinstead ofIS NULL,HAVINGwithoutGROUP BY, implicit column lists - Style issues: inconsistent keyword casing, implicit joins, unnecessary parentheses
No database connection required. No configuration needed. Just point it at SQL and get actionable feedback.
pip install sqlauditOr install from source:
git clone https://github.com/JSLEEKR/sqlaudit.git
cd sqlaudit
pip install -e .# Audit a SQL file
sqlaudit audit -f queries.sql
# Audit a single query
sqlaudit audit -q "SELECT * FROM users WHERE name = NULL"
# Audit from stdin
cat migration.sql | sqlaudit audit --stdin
# JSON output for CI/CD integration
sqlaudit audit -f queries.sql --format json
# Only check security rules
sqlaudit audit -f queries.sql --category security
# Fail CI pipeline on any error or warning
sqlaudit audit -f queries.sql --fail-on warning
# Show fix suggestions
sqlaudit audit -q "DELETE FROM users" --verbose
# List all available rules
sqlaudit rulesfrom sqlaudit import Analyzer
analyzer = Analyzer()
report = analyzer.analyze("""
SELECT * FROM users WHERE name = NULL;
DELETE FROM audit_logs;
INSERT INTO users VALUES (1, 'test');
""")
print(f"Score: {report.score}/100")
print(f"Errors: {report.total_errors}")
print(f"Warnings: {report.total_warnings}")
for query in report.queries:
for finding in query.findings:
print(f" [{finding.severity.value}] {finding.rule_id}: {finding.message}")
print(f" Fix: {finding.suggestion}")from sqlaudit import Analyzer
analyzer = Analyzer()
findings = analyzer.check_query("DELETE FROM important_table")
for f in findings:
print(f"{f.rule_id}: {f.message}")
# P002: DELETE without WHERE clause will affect all rowssqlaudit ships with 27 built-in rules across 5 categories.
| Rule | Severity | Description |
|---|---|---|
| P001 | WARNING | SELECT * retrieves all columns |
| P002 | ERROR | UPDATE/DELETE without WHERE clause |
| P003 | WARNING | Leading wildcard in LIKE prevents index usage |
| P004 | WARNING | Too many JOINs (>5 tables) |
| P005 | WARNING | NOT IN with subquery (NULL handling issues) |
| P006 | INFO | Multiple OR on same column (use IN instead) |
| P007 | HINT | SELECT without LIMIT |
| P008 | WARNING | Function on column in WHERE prevents index usage |
| P009 | HINT | SELECT DISTINCT with JOINs may indicate bad join |
| P010 | INFO | ORDER BY without LIMIT sorts entire result set |
| Rule | Severity | Description |
|---|---|---|
| S001 | ERROR | String concatenation (SQL injection risk) |
| S002 | ERROR | GRANT ALL PRIVILEGES (excessive permissions) |
| S003 | WARNING | DROP without IF EXISTS |
| S004 | WARNING | TRUNCATE detected (no row-level logging) |
| S005 | ERROR | Comment patterns inside string literals |
| S006 | WARNING | UNION injection pattern |
| S007 | WARNING | Always-true condition (1=1, 'a'='a') |
| Rule | Severity | Description |
|---|---|---|
| B001 | WARNING | INSERT without column list |
| B002 | INFO | SELECT INTO (non-standard) |
| B003 | HINT | Multi-table query without aliases |
| B004 | WARNING | Deeply nested subqueries (depth >= 3) |
| B005 | INFO | Implicit join (comma-separated FROM) |
| Rule | Severity | Description |
|---|---|---|
| C001 | ERROR | = NULL instead of IS NULL |
| C002 | WARNING | SELECT * with GROUP BY |
| C003 | ERROR | HAVING without GROUP BY |
| Rule | Severity | Description |
|---|---|---|
| T001 | HINT | Inconsistent keyword casing |
| T002 | HINT | Unnecessary parentheses in simple conditions |
from sqlaudit import Analyzer
from sqlaudit.rules import Category
# Only security checks
analyzer = Analyzer(enable_categories=[Category.SECURITY])
report = analyzer.analyze("SELECT * FROM users")from sqlaudit import Analyzer
from sqlaudit.rules import Severity
# Only errors and warnings (skip info/hint)
analyzer = Analyzer(min_severity=Severity.WARNING)analyzer = Analyzer(disable_rules=["P001", "P007", "T001"])from sqlaudit import Analyzer, Rule, Severity, Category
def check_table_prefix(info):
"""All tables should use 'app_' prefix."""
for table in info.tables:
if not table.startswith("app_"):
return f"Table '{table}' missing 'app_' prefix"
return None
analyzer = Analyzer()
analyzer.register_rule(Rule(
id="CUSTOM001",
name="table-prefix",
description="Tables must use app_ prefix",
severity=Severity.WARNING,
category=Category.BEST_PRACTICE,
check=check_table_prefix,
suggestion="Rename table to use app_ prefix",
))
report = analyzer.analyze("SELECT * FROM users")
# Finds: Table 'users' missing 'app_' prefix# Human-readable text (default)
sqlaudit audit -q "SELECT * FROM t" --format text
# JSON for programmatic consumption
sqlaudit audit -q "SELECT * FROM t" --format json
# CSV for spreadsheet import
sqlaudit audit -q "SELECT * FROM t" --format csv
# Summary only (score + counts)
sqlaudit audit -q "SELECT * FROM t" --format summary# GitHub Actions example
- name: SQL Audit
run: |
pip install sqlaudit
sqlaudit audit -f migrations/*.sql --format json --fail-on warning# Pre-commit hook
#!/bin/bash
SQL_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.sql$')
if [ -n "$SQL_FILES" ]; then
sqlaudit audit -f $SQL_FILES --fail-on error --no-color
fisqlaudit assigns a score from 0-100 based on findings:
| Severity | Point Deduction |
|---|---|
| ERROR | -10 per finding |
| WARNING | -5 per finding |
| INFO | -2 per finding |
| HINT | -1 per finding |
A pass rate is also computed: percentage of queries with zero errors or warnings.
sqlaudit/
parser.py # SQL tokenizer + query parser (zero dependencies)
rules.py # Rule engine with 27 built-in rules
analyzer.py # Main analyzer (parser + rules + report)
report.py # Report data model (Finding, QueryResult, Report)
formatter.py # Output formatters (text, JSON, CSV, summary)
cli.py # Command-line interface
The architecture is designed for extensibility:
- Parser produces a
QueryInfoobject with structured query metadata - Rules are pure functions:
(QueryInfo) -> Optional[str] - Custom rules can be registered at runtime
- Formatters are pluggable for any output format
MIT