The recurring problem when formatting MySQL is identifiers. The reserved word list has grown with every release — rank, groups, window and system became reserved in 8.0 — so real schemas are full of backtick-quoted columns that only exist because the name collided with a keyword. A formatter that treats backticks as decoration, or that applies keyword casing to the text inside them, will produce SQL that no longer runs.
Selecting the MySQL grammar keeps backtick quoting intact and parses the clauses that are specific to this dialect: index hints between the table and the join, the two-argument LIMIT, and the clause-like arguments of GROUP_CONCAT. The query below is loaded into the editor above.
Before and after
This is the query loaded in the editor above. On the left is what you paste; on the right is what the formatter returns with the default options for this dialect.
select `u`.`id`, `u`.`order`, group_concat(distinct t.name order by t.name separator ', ') as tags, json_extract(u.meta, '$.plan') as plan from `users` `u` force index (idx_users_created) left join `user_tags` ut on ut.user_id = u.id left join `tags` t on t.id = ut.tag_id where u.created_at >= date_sub(now(), interval 30 day) group by `u`.`id`, `u`.`order` having count(t.id) > 0 order by u.created_at desc limit 40, 20;SELECT
`u`.`id`,
`u`.`order`,
GROUP_CONCAT(
DISTINCT t.name
ORDER BY
t.name SEPARATOR ', '
) AS tags,
JSON_EXTRACT(u.meta, '$.plan') AS plan
FROM
`users` `u` FORCE INDEX (idx_users_created)
LEFT JOIN `user_tags` ut ON ut.user_id = u.id
LEFT JOIN `tags` t ON t.id = ut.tag_id
WHERE
u.created_at >= DATE_SUB(NOW(), interval 30 day)
GROUP BY
`u`.`id`,
`u`.`order`
HAVING
COUNT(t.id) > 0
ORDER BY
u.created_at DESC
LIMIT
40, 20;What is specific to MySQL & MariaDB
Backtick identifiers and case sensitivity
The column named `order` in the example is the common case: a perfectly reasonable business term that happens to be a reserved word. Backticks are the only thing keeping that query valid, so they are preserved and never re-cased.
This matters more in MySQL than in most databases because table name case sensitivity depends on the host filesystem. On Linux, Users and users are different tables; on macOS and Windows they usually are not. A formatter that uppercases identifiers will silently work in development and fail in production, which is why identifier case defaults to Preserve here.
LIMIT with two arguments
MySQL accepts both LIMIT count and LIMIT offset, count. The two-argument form has no equivalent in standard SQL — LIMIT 40, 20 means skip 40, return 20, which is the reverse of the order people expect from LIMIT ... OFFSET.
The formatter keeps both arguments on one line rather than splitting them across the comma, because splitting them makes an already confusing clause worse.
Index hints sit between the table and the join
FORCE INDEX, USE INDEX, IGNORE INDEX and STRAIGHT_JOIN attach to a table reference, so they appear after the alias and before the next JOIN. They are parsed as part of the table reference and stay on its line, which keeps the join list readable.
Functions whose arguments contain clauses
GROUP_CONCAT is not an ordinary function call: its argument list can contain DISTINCT, a full ORDER BY and a SEPARATOR. It is expanded as a nested block for that reason, with the ORDER BY indented inside the call.
ON DUPLICATE KEY UPDATE, old and new form
The upsert clause is recognised in both spellings — the long-standing VALUES(col) form and the row alias introduced in MySQL 8.0.20, which deprecated it. The alias form parses cleanly:
INSERT INTO
t (id, v)
VALUES
(1, 'a') AS new
ON DUPLICATE KEY UPDATE
v = new.v;Three comment syntaxes, one of them a trap
MySQL accepts # to end of line, /* */ blocks, and -- to end of line. The last one has a condition that catches people out: MySQL requires whitespace after the double dash, so --comment is not a comment and will usually be read as a subtraction followed by an identifier.
Comments are preserved in place. If a -- comment survives formatting but the query then fails to run, check for the missing space.
Known limitations
No formatter handles every corner of a dialect. These are the cases where this one produces output you may want to correct by hand.
- In DATE_SUB(NOW(), interval 30 day) the INTERVAL keyword and its unit are left in the case you typed them, because they are parsed as part of the function argument rather than as top-level keywords. The query is valid either way.
Conventions worth adopting
Quote only what needs quoting
Backticks around every identifier is a habit picked up from GUI tools that generate them unconditionally. It is not wrong, but it adds noise — the example above quotes `users` and `order` and leaves the aliases bare, which is the more common hand-written style.
MariaDB uses the same setting
MariaDB diverged from MySQL after 5.5, but the formatting-relevant syntax — backticks, index hints, LIMIT, the comment styles — is shared. Use the MySQL dialect for MariaDB queries.
MySQL & MariaDB formatting FAQ
Will uppercasing keywords break my case-sensitive table names?
No. Keyword case and identifier case are independent settings. The default uppercases SELECT, FROM and JOIN while leaving every table and column name exactly as you typed it.
Does this work for MariaDB?
Yes. Select the MySQL dialect. The syntax that affects formatting is the same in both.
Why is my -- comment not being treated as a comment?
MySQL requires a whitespace character after the double dash. --note is parsed as an expression; -- note is a comment. This is a MySQL rule, not a formatter behaviour.
Is my SQL uploaded to a server?
No. Formatting happens in your browser and nothing leaves your machine, so it is safe to paste queries containing real schema names.
Other SQL dialects
Not sure which one you need, or working with more than one? The general SQL formatter lets you switch dialects without leaving the page.