NV ToolkitFormat · Validate · Convert
Guide

How to Escape a Single Quote in SQL

Every developer meets this the first time they insert a name like O'Brien and the parser stops mid-statement. The fix is one character long, but which character depends on the database, on a server setting, and on whether you are writing a literal or a LIKE pattern. Here is the complete map.

In short

Double the quote. Inside a SQL string literal, write two single quotes for every one you want in the value: 'O''Brien'. That is the ANSI SQL rule and it works unchanged in MySQL, PostgreSQL, SQL Server, Oracle and SQLite. MySQL also accepts a backslash escape, PostgreSQL offers dollar quoting, and Oracle offers q-strings — but doubling is the one form that is always safe.

By NaveenKumar T · Software engineer · builds and maintains NV Toolkit Published 2026-09-11 9 min read

Why one apostrophe breaks the whole statement

A SQL string literal is delimited by single quotes. The parser reads the opening quote, consumes characters until it sees the next single quote, and calls that the end of the string. An apostrophe inside your data is indistinguishable from that closing delimiter, so the parser closes the literal early and then tries to interpret the remainder of your value as SQL keywords. You get a syntax error near a word that looks perfectly innocent, which is why the message is rarely helpful.

The remedy is an escape: a way of writing the delimiter character so the parser treats it as data. ANSI SQL specifies exactly one, and it is the same in every engine that claims standard compliance — write the quote twice. PostgreSQL's documentation states it plainly: to include a single-quote character within a string constant, write two adjacent single quotes. The parser sees the pair, emits one quote into the value, and keeps reading.

Two adjacent quotes are not a double quote. A double quote is a different token entirely — in PostgreSQL, Oracle and SQLite it delimits identifiers such as column names, and in SQL Server it does the same when QUOTED_IDENTIFIER is on. Swapping one for the other turns 'O''Brien' into a reference to a column that does not exist. If you are eyeballing a long literal and cannot tell which you typed, paste it into the SQL Escape tool and let it do the doubling.

sql
-- Breaks: the literal ends at the apostrophe
INSERT INTO customers (last_name) VALUES ('O'Brien');
--                                          ^ literal closes here

-- Correct in every mainstream database: double the quote
INSERT INTO customers (last_name) VALUES ('O''Brien');

-- Stored value: O'Brien   (one apostrophe, not two)
SELECT last_name, length(last_name) FROM customers;  -- 7

MySQL: two escapes, and a server setting that removes one

MySQL accepts the standard doubling, and it also accepts C-style backslash escapes inside string literals — so \' produces an apostrophe, \\ produces a backslash, and \n produces a newline. Both 'O''Brien' and 'O\'Brien' store the same seven characters. Because MySQL also lets you delimit strings with double quotes by default, "O'Brien" works too, which is convenient right up until someone enables ANSI_QUOTES and the double quote starts meaning identifier instead.

The trap is the NO_BACKSLASH_ESCAPES SQL mode. When it is enabled, the backslash loses its special meaning and becomes an ordinary character — 'O\'Brien' then fails to parse, because the escape you relied on no longer exists. It is not part of MySQL 8's default sql_mode, but plenty of teams turn it on precisely to move closer to the standard, and replication or a migration to another engine can surface it without warning. Check with SELECT @@sql_mode before you assume.

The practical conclusion is the same one the rest of this guide keeps arriving at: doubling survives every one of these settings, and backslash escapes do not. If you are writing a script that has to run against an unknown MySQL configuration — a seed file, a data-fix migration, something a colleague will paste into a client you have never seen — use two single quotes and move on. Reserve backslash escapes for cases where you genuinely need a newline or tab in the value, and remember that those break under NO_BACKSLASH_ESCAPES too.

sql
-- MySQL: both of these store O'Brien under default settings
SELECT 'O''Brien', 'O\'Brien';

-- Which escapes are live right now?
SELECT @@sql_mode;

-- With NO_BACKSLASH_ESCAPES on, only the doubled form parses
SET SESSION sql_mode = CONCAT(@@sql_mode, ',NO_BACKSLASH_ESCAPES');
SELECT 'O''Brien';    -- OK
-- SELECT 'O\'Brien'; -- syntax error: backslash is now a plain character

PostgreSQL: escape strings and dollar quoting

PostgreSQL is strict by default. Since version 9.1 the standard_conforming_strings parameter has defaulted to on, which means a backslash inside an ordinary literal is just a backslash — 'C:\temp' really contains one backslash, and 'O\'Brien' is a syntax error. Doubling is the only escape an ordinary string constant understands. That is the standards-compliant behaviour and you should not turn it off.

When you do need C-style escapes, PostgreSQL provides escape string constants: put an E immediately before the opening quote, as in E'line one\nline two'. Inside an E-string the backslash regains its meaning, so E'O\'Brien' is valid — though the doubled form still works there too. There is also a Unicode form, U&'...', which lets you write code points as escapes; it is only available while standard_conforming_strings is on, precisely because parsing it under the legacy rules would open injection holes.

For anything with a lot of quotes — a function body, an embedded JSON document, a regular expression — dollar quoting is the feature worth knowing. A dollar-quoted constant runs between a matching pair of delimiters, either bare or tagged, and nothing inside it is ever escaped. Single quotes, backslashes and newlines are all taken literally. You can nest them by choosing different tags at each level, which is why almost every CREATE FUNCTION you will read uses a tagged delimiter rather than a wall of quadrupled backslashes. When you are formatting one of those long statements for review, the SQL Formatter will keep the body readable.

sql
-- Standard doubling (always correct)
SELECT 'Dianne''s horse';

-- Escape string constant: backslash escapes are enabled by the E prefix
SELECT E'line one\nline two';
SELECT E'O\'Brien';

-- Dollar quoting: nothing inside is escaped at all
SELECT $$Dianne's horse$$;
SELECT $tag$He said "it's fine" and typed C:\temp$tag$;

-- Which behaviour is active?
SHOW standard_conforming_strings;  -- on, since PostgreSQL 9.1

SQL Server, Oracle and SQLite in one table

Transact-SQL follows the standard: if a character string enclosed in single quotation marks contains an embedded quotation mark, represent it with two single quotation marks. The wrinkle in SQL Server is not escaping but encoding — a bare literal is varchar and is converted to the database code page, so a literal with accented characters can lose them where the N-prefixed form keeps them. Prefix every literal that might hold non-ASCII text with N. For generated SQL there is also QUOTENAME, which wraps a string in delimiters and doubles whatever is inside, producing a ready-made single-quoted literal. Note the limit — the input is sysname, capped at 128 characters, and anything longer returns NULL, so for long values use REPLACE instead.

Oracle takes the doubling rule and adds an escape hatch for quote-heavy text. A q-string is written with a chosen quote delimiter, which may be any single character other than space, tab or return. If you open with one of the bracket characters, the closing delimiter must be the matching closing bracket; otherwise the same character closes it. Inside a q-string a single quote is just a single quote, so no escaping is needed at all. The one rule to remember is that the delimiter must not appear in the text immediately followed by a single quote.

SQLite is the simplest of the five and the strictest: doubling is the only escape a string literal has. There are no backslash escapes — '\n' in SQLite is a two-character string containing a backslash and the letter n, which surprises people porting fixtures from MySQL. Build the string with char(10) or concatenation if you need a real newline. The table below is the whole landscape on one screen.

Escaping a single quote across the five common engines
DatabaseEscape for a quoteAlternative literal syntaxNotes
MySQL / MariaDB'' or \'"double-quoted" strings (default mode)Backslash escapes disappear under NO_BACKSLASH_ESCAPES; ANSI_QUOTES turns the double quote into an identifier quote
PostgreSQL''E'...' escape strings, $$...$$ dollar quotingstandard_conforming_strings is on by default since 9.1, so backslashes are literal outside E-strings
SQL Server''N'...' for Unicode; QUOTENAME(v, '''')QUOTENAME input is sysname — over 128 characters it returns NULL, so use REPLACE for long values
Oracle''q'[...]', q'{...}', q'!...!'An opening bracket must close with its matching bracket; any other delimiter closes with itself
SQLite''noneNo backslash escapes at all; '\n' is a literal backslash followed by n
sql
-- SQL Server: doubling, Unicode prefix, and generated literals
SELECT 'O''Brien', N'Michel', QUOTENAME('O''Brien', '''');
-- QUOTENAME returns: 'O''Brien'
SELECT REPLACE(@long_value, '''', '''''');  -- for values over 128 chars

-- Oracle: doubling, or a q-string with no escaping at all
SELECT 'O''Brien' FROM dual;
SELECT q'[O'Brien said "hello"]' FROM dual;

-- SQLite: doubling only
SELECT 'O''Brien';

Escaping inside LIKE is a second, separate problem

A LIKE pattern is parsed twice. First the string literal is parsed, so a single quote still has to be doubled exactly as above. Then the resulting text is parsed as a pattern, where the percent sign matches any run of characters and the underscore matches exactly one. A search for a value that genuinely contains a percent sign — a discount code, a Windows path with an underscore — will silently match far more rows than you meant, and no error will tell you.

The portable fix is the ESCAPE clause, which nominates one character as the pattern escape for that expression: LIKE '100\%' ESCAPE '\'. PostgreSQL and MySQL treat the backslash as the pattern escape even when you omit ESCAPE, but SQL Server and Oracle have no default at all — omit the clause there and your backslash is simply another literal character to match. Because the defaults disagree, writing ESCAPE explicitly every time is the habit that ports.

SQL Server offers a second option that is sometimes clearer: the bracket class. Wrapping a wildcard in square brackets strips its special meaning, so a bracketed percent sign matches a literal one, and the same works for the underscore. It is T-SQL only. Whichever form you choose, apply the pattern escape to the user's input before you concatenate it into the pattern — escaping the quote is not enough, and forgetting the wildcard layer is one of the quieter ways a search endpoint turns into a full table scan.

sql
-- Wrong: the % in the value is treated as a wildcard
SELECT * FROM codes WHERE code LIKE '%SAVE20%%';

-- Portable: nominate an escape character explicitly
SELECT * FROM codes WHERE code LIKE '%SAVE20\%%' ESCAPE '\';

-- Underscore is a single-character wildcard, so it needs escaping too
SELECT * FROM files WHERE path LIKE 'log\_2026%' ESCAPE '\';

-- SQL Server only: a bracket class also disables the wildcard
SELECT * FROM codes WHERE code LIKE '%SAVE20[%]%';

In application code, stop escaping and bind instead

Everything above is for SQL you write by hand: a migration, a data fix, a one-off analysis query. In application code, escaping is the wrong tool. A bind parameter sends the statement and the values to the server as separate things, so the value is never parsed as SQL and there is nothing to escape — quotes, backslashes, semicolons and Unicode oddities all arrive as data. It is also usually faster, because the server can cache the plan for the statement text.

In Python the placeholder depends on the driver's paramstyle, and getting it wrong is the most common mistake. psycopg and psycopg2 use %s for every type — that is a placeholder, not Python string formatting, so it takes no quotes around it and you must never build the string with the percent operator or an f-string. The sqlite3 module and most MySQL drivers use a question mark instead. Pass the values as a tuple or list in the second argument and let the driver do the rest; psycopg3 sends them through the extended query protocol server-side, while psycopg2 adapts them client-side, and both are safe.

One detail worth internalising: a placeholder stands for a single value, never for a table name, a column name, or a fragment of SQL. If you need those to be dynamic, that is a different problem with a different answer — an allow-list, or an identifier-quoting function such as PostgreSQL's quote_ident. When you are exporting the resulting rows to a spreadsheet, the same escaping question reappears in a different alphabet; the CSV Escape tool handles that layer.

python
# psycopg (PostgreSQL) - %s is a placeholder, not string formatting
import psycopg

with psycopg.connect("dbname=shop") as conn, conn.cursor() as cur:
    cur.execute(
        "SELECT id, email FROM customers WHERE last_name = %s AND active = %s",
        ("O'Brien", True),          # values travel separately from the SQL
    )
    rows = cur.fetchall()

# sqlite3 (stdlib) and most MySQL drivers use ?
import sqlite3

con = sqlite3.connect("shop.db")
con.execute("SELECT id FROM customers WHERE last_name = ?", ("O'Brien",))

# Never do this, no matter how carefully you escape:
# cur.execute(f"SELECT id FROM customers WHERE last_name = '{name}'")

The same thing in Node: pg and mysql2

node-postgres uses numbered placeholders, matching PostgreSQL's own parameter syntax, and takes the values as an array. The numbering is one-based and a parameter may be repeated, which is handy for a value used in both a WHERE clause and a RETURNING expression. Because the driver forwards the parameters to the server untouched, an apostrophe in the value needs nothing done to it.

mysql2 uses question marks but draws a distinction that catches people out. connection.query() interpolates the values into the SQL client-side using the driver's escaping routine, then sends one finished string; connection.execute() sends a genuine server-side prepared statement with the values separate. Both are safe against injection for values, but execute() is the one that keeps the value out of the SQL text entirely and lets MySQL reuse the plan. There is also a double question mark for identifiers — it applies backtick quoting rather than value escaping, which is the right tool if a column name really must be dynamic and it comes from an allow-list.

If a value is destined for a browser rather than a database, remember that database escaping does nothing for it: an apostrophe that is safe in SQL can still break a JavaScript string literal or an HTML attribute. That is a separate encoding step, handled by tools like JavaScript Escape — one escape per destination, applied at the moment of use, never once at the front door.

javascript
// node-postgres: numbered placeholders with the values in an array
const { rows } = await client.query(
  "SELECT id, email FROM customers WHERE last_name = $1 AND active = $2",
  ["O'Brien", true],
);

// mysql2/promise: execute() is a real server-side prepared statement
const [rows2] = await conn.execute(
  "SELECT id, email FROM customers WHERE last_name = ? AND active = ?",
  ["O'Brien", 1],
);

// query() interpolates client-side; still escaped, but the value ends up in the SQL text
const [rows3] = await conn.query(
  "SELECT id FROM customers WHERE last_name = ?",
  ["O'Brien"],
);

// ?? quotes an IDENTIFIER, not a value - only ever from an allow-list
const sort = ALLOWED.has(req.query.sort) ? req.query.sort : "created_at";
const [rows4] = await conn.query("SELECT id FROM customers ORDER BY ?? DESC", [sort]);
How this was tested: Every escape rule below was checked clause by clause against the official PostgreSQL, Microsoft and Oracle documentation cited above on 2026-09-11; the statements are documented syntax rather than the output of a live server.

Sources

  1. PostgreSQL — Lexical Structure (String Constants)
  2. Microsoft Learn — Constants (Transact-SQL)
  3. Oracle Database SQL Language Reference — Literals

Frequently asked questions

What is the single rule if I only remember one thing?

Replace every single quote with two single quotes inside the literal, so O'Brien is written 'O''Brien'. That is ANSI SQL and it is accepted unchanged by MySQL, PostgreSQL, SQL Server, Oracle and SQLite. Every other form in this guide — backslashes, E-strings, dollar quoting, q-strings — is an engine-specific convenience layered on top of it.

Why does a backslash escape work on my laptop but fail in production?

Almost always a MySQL sql_mode difference. Backslash escapes are enabled by default but disappear when NO_BACKSLASH_ESCAPES is set, and the two servers may not agree. The same symptom appears when moving a script from MySQL to PostgreSQL, where standard_conforming_strings has been on by default since 9.1 and backslashes are literal outside an E-string. Run SELECT @@sql_mode or SHOW standard_conforming_strings to confirm, then switch the script to doubled quotes so it stops mattering.

Does escaping quotes protect me from SQL injection?

It closes one specific hole, not the class of bug. Quote doubling only helps when the value sits inside a quoted string literal — it does nothing for a value spliced into a numeric comparison, an ORDER BY clause, a table name, or a LIMIT, and nothing for a value stored now and concatenated into a query later. Use bind parameters in application code and keep escaping for hand-written scripts.

How do I escape a quote inside a LIKE pattern?

Double it for the string literal as usual, then handle the pattern wildcards separately: the percent sign and underscore are wildcards and need an escape character nominated with the ESCAPE clause, as in LIKE '100\%' ESCAPE '\'. PostgreSQL and MySQL default to backslash, SQL Server and Oracle have no default at all, so write ESCAPE explicitly if the query has to be portable.

What about escaping a double quote or a backslash?

A double quote needs no escaping inside a single-quoted literal in any of these engines — it is ordinary data. A backslash is ordinary data in PostgreSQL (with standard_conforming_strings on), SQLite and Oracle, but is an escape introducer in MySQL under default settings, so a Windows path stored from MySQL needs a doubled backslash to hold one. Doubling the quote and leaving everything else alone is the portable position.

Paste a value into SQL Escape and get the doubled literal back — with an unescape mode for reading escaped scripts.

Escape it now

Keep working

Related tools