A SQL query on the left with real schema, table, column and alias names, and on the right the same query with every name replaced by a role-named placeholder such as schema_1, table_1, col_1, alias_1 and str_1, line for line in the same layout.
The same eight lines before and after. Every name becomes a placeholder that says what it was, schema, table, column, alias, parameter or string. The keywords, the built-in SUM and the number in LIMIT stay, and so does every line break. That is what a reviewer gets, and the key that turns it back is what they do not.

Nobody leaks a schema on purpose. It leaks one pasted query at a time, a SELECT in a ChatGPT tab, a JOIN in a Stack Overflow question, a WHERE clause in a vendor ticket, each carrying table names, column names and a literal or two of real data.

A query is documentation

Read someone else's query and you learn what their business stores, how the tables relate, which columns exist and what the data looks like. That is precisely why queries are so useful to share, and why sharing them raw is a problem. The names are the sensitive part. The structure, the part a reviewer or a model actually needs, is not tied to them at all.

Anonymize, ask, restore

The tool above runs a round trip with three steps and one key.

  1. Paste the query, or drop a .sql file. Names become placeholders labelled by role. crm.customers turns into schema_1.table_1, company_name into col_4, a user-defined fn_churn_risk( into func_1(, and 'Acme GmbH' into 'str_2'. Whitespace, line breaks and keywords are untouched, so the result reads like your query with the names filed off.
  2. Send the result wherever the question lives. ChatGPT, Claude, a forum post, a Slack thread with an external consultant. The recipient sees a working query shape and zero vocabulary from your database.
  3. When the answer comes back, switch the tool to restore names and paste it, edits and all. Every placeholder in the key becomes the real name again, including placeholders in lines the reviewer wrote from scratch.

The key panel between input and stats shows each replacement with its role and how often it occurs in the current input. New names are only written into the stored key once the input settles, otherwise typing customers character by character would leave cus and custo behind as entries.

A table listing the parts of a SQL query, table and schema names, columns, string literals, comments, aliases and functions, numbers, keywords and the overall shape, with what each one tells a reader and whether the anonymizer replaces it.
Names are the leak that a placeholder fixes. Numbers and the shape of the query are the two things it leaves in place, and both are decisions rather than gaps: a number cannot be replaced without changing what the query does, and a query without its join structure is not something anyone can help with. Long numbers are listed under the output so they can be edited by hand.

The replacement rules

In the queryExampleComes out as
Schema, table, CTEcrm.customers, WITH paid ASschema_1.table_1, cte_1
Columncompany_name, "Account Manager"col_4, "col_5" with quotes kept
Aliasinvoices inv, AS revenuetable_1 alias_1, AS alias_2
Own functionfn_churn_risk(c.id)func_1(alias_3.col_2)
Named parameter:period_start, @tenant:param_1, @param_2
String literal'%acme%''%str_3%', wildcards outside
Keyword, type, built-inSELECT, varchar(20), COALESCE(unchanged
Keyword-shaped columnname, date, typecol_7, decided by position
NumberLIMIT 20, id = 48213unchanged, long ones flagged
Comment-- Q3 board deckremoved unless --comments is on

The keyword-shaped row is the hard one. A column named date, value, count or key looks exactly like syntax, and a tool built on word lists must leave it alone. Position resolves it here. After a dot, after FROM, or between two expression tokens the word is a name and gets a placeholder, while in CAST(x AS date) or COUNT(*) it is syntax and stays. Only hard reserved words like SELECT and WHERE can never be a bare name.

There is deliberately no whitelist of harmless names, and every alias goes too, down to the single letter. The reasoning sits in the FAQ below.

Numbers are the one thing left in place, because no placeholder keeps a query valid. LIMIT n1 is a syntax error and LIMIT 1 lies about the query. Anything with five or more digits, the shape of a record id or an account number, is flagged under the output for a manual decision.

String literals are masked by default because they are where actual data lives. The exceptions are literals that work as syntax rather than data. The unit in DATE_TRUNC('month', ...), the pattern in TO_CHAR(d, 'YYYY-MM') and a bare ', ' separator inside CONCAT stay readable, and the tool reports them as kept.

Why replace-all backfires

The obvious approach is the editor's find and replace, or sed in a shell. Both operate on characters, and names are not characters.

replace-all works on substrings
sed 's/user/t1/g'

SELECT t1_id, t1name
FROM t1s u
WHERE u.email = 'sam@t1s.io'
  -- t1 export
a tokenizer works on names
SELECT col_1, col_2
FROM table_1 alias_1
WHERE alias_1.col_3 = 'str_1'

Five things go wrong at once, all silently. The pattern matches inside user_id and username. It rewrites the email inside the string literal into a new, equally real-looking address instead of masking it. It treats "Users", Users and users as three different words when the engine treats them as one table. It never touches the comment, which names the report and the quarter. And after twenty replacements nothing records which name became what, so the answer you get back cannot be mapped onto your schema.

A tokenizer has none of these problems because it knows where a name starts, where it ends, and whether it currently sits inside a string, a comment or a keyword position.

The key stays here

The key is the list of placeholder, original name and role. It lives in this browser's localStorage and nowhere else. There is no account, no sync, no server copy. It survives page reloads, which is what makes multi-query sessions work. customers is table_1 in today's query and still table_1 in next week's migration, so an answer that touches both restores cleanly.

Because the key contains your real names, the exported JSON deserves the same handling as the schema itself. Sending it to a colleague is the intended way to let them restore your queries. Importing a key adds entries you are missing, unless it comes from a different browser and reuses your placeholder numbers for other names, in which case merging would be dishonest and the import replaces the key instead, with a notice.

The reset button forgets everything and therefore asks for a second click.

What it cannot promise

The tool tokenizes rather than parses. A dialect keyword missing from its lists gets a placeholder it did not need, which reads oddly but round-trips fine. The opposite, a name mistaken for syntax, is rarer and confined to unusual DDL positions. Check the key panel before sharing anything that matters, it lists exactly what was and was not replaced.

The larger limit is structural. Five joins around a table with a paid_at column look like invoicing to anyone who has built invoicing, placeholders or not. We paste anonymized queries into language models ourselves and accept that trade, and it costs us the occasional answer that misses because the model could not tell col_3 was a timestamp. A query whose structure is itself the secret should not be shared in any form.

For pipelines there is sqlglot

This page is built for the query that is about to leave your machine right now. The same job inside a CI step or a data pipeline wants a real parser instead: sqlglot parses into a typed tree per dialect, renaming exp.Table and exp.Column nodes is exact rather than heuristic, and the cost is a Python dependency plus output printed sqlglot's way. Your IDE's rename refactoring is the one tool to avoid here. It renames the schema itself, the opposite of what sharing a copy needs.

Asked before the query leaves the database

What can someone learn from a SQL query posted online?

More than most people expect. The table names describe what the business stores, the joins describe how it fits together, and the literals in the WHERE clause are often actual customer data, an email address or a company name. Whoever operates the chat service or forum can read all of it, and public posts get indexed.

How do you anonymize a SQL query before sending it to ChatGPT?

Swap every identifier that is not a keyword for a placeholder, mask the string literals, drop the comments, and write the mapping down so the answer can be translated back. By hand that takes minutes and breaks on the second occurrence of a name. The tool on this page does the whole pass in the browser and keeps the mapping as a key you can reverse.

Can an anonymized query be de-anonymized without the key?

No. table_1 carries no trace of the name it replaced.

Does swapping the names change what a query does?

No. Each name maps to exactly one placeholder, so joins, filters and grouping keep their structure.

Is formatting or minifying SQL enough to hide anything?

It hides nothing. A formatter rearranges whitespace and a minifier removes it, but every table name, column name and literal is still there word for word. Anonymizing is a different operation, it replaces the names themselves and keeps a private mapping for the way back.

Why replace harmless column names like id or created_at?

Because a rule with exceptions has to be right about every single name, and the kept ones betray the replaced ones. A user_id sitting next to table_1 tells any reader what table_1 is. Replacing everything is the only rule that never needs judgement, and a reader who needs to know that col_3 is a timestamp can be told so in the message next to the query.

What happens when the AI invents its own placeholder in the answer?

Restore mode swaps back every placeholder the key knows and lists the ones it never issued, so an invented col_99 stays visible instead of silently pretending to be restored. A placeholder that got wrapped into a longer name, like table_1_idx in a suggested index, is left alone on purpose.

How do I anonymize SQL programmatically in Python?

With sqlglot: parse_one(query) gives you a syntax tree, find_all(exp.Table) and find_all(exp.Column) give you the identifier nodes, and you rename each one from a dict you keep for the reverse step, then print with .sql(). You get an exact parse for one dialect at a time. You also get a dependency, dialect quirks to handle, and output formatted the way sqlglot prints it rather than the way the query was written.

Where does the mapping between placeholders and real names live?

In your own browser, in localStorage of this site. It is never transmitted. Download it as a JSON file if you want a copy or need it on another machine.

Can a colleague restore a query I anonymized?

Yes, if you send them the key file along with it. They import the JSON on the same page, paste the anonymized query into restore mode, and get your real names. Without the file the placeholders stay placeholders, which is exactly the point.

Are the values in a WHERE clause personal data?

Often yes. A clause like email = 'anna@firma.at' or name LIKE '%maier%' contains information about an identifiable person, which is what data protection law cares about. Masking literals turns the value into str_1 while the comparison stays readable, so the question can be asked without the person travelling along.