Home Functions POSITION()
SQL String Function

POSITION()

POSITION() returns the 1-based index of the first occurrence of a substring inside a string, or 0 when the substring is not present.

MySQLPostgreSQLSQL Server
Returns: An integer giving the 1-based position where the substring first appears. Returns 0 when the substring is not found, and NULL when either argument is NULL.

Syntax

POSITION(substring IN string)INSTR(string, substring)LOCATE(substring, string)LOCATE(substring, string, start)CHARINDEX(substring, string [, start])
ParameterTypeRequiredDescription
substring string yes The text you are searching for. The function returns the index where this first appears inside string.
string string yes The text to search within.
start integer no Optional 1-based index that LOCATE and CHARINDEX begin searching from. Useful for finding a second or later occurrence.

How it works

POSITION() is a string function that reports where a substring first appears inside another string. Standard SQL spells it POSITION(substring IN string), and it sits in the same family of text tools as SUBSTRING() and REPLACE(). You can browse the whole set in the SQL functions reference.

The catch is that every engine names this operation differently. The SQL standard form POSITION(substring IN string) works in PostgreSQL and MySQL. MySQL, Oracle and SQLite also expose INSTR(string, substring). MySQL adds LOCATE(substring, string [, start]), and SQL Server uses CHARINDEX(substring, string [, start]). Despite the different spellings they behave the same way: all are 1-based, and all return 0 when the substring is not found.

The two everyday uses are splitting and testing. Combine POSITION() with SUBSTRING() to cut a value around a delimiter, such as pulling the domain out of an email address. Or compare the result against zero, as in POSITION('@' IN email) > 0, to check whether a string contains something. For a categorised tour of the rest, see the SQL functions list.

Examples

Find the position of a character in an email

SELECT email, POSITION('@' IN email) AS at_pos
FROM users;
Result
email        | at_pos
-------------+-------
ali@shop.com |      4
noreply@x.io |      8

Split around a delimiter with POSITION and SUBSTRING

-- everything after the '@' is the email domain
SELECT email,
       SUBSTRING(email FROM POSITION('@' IN email) + 1) AS domain
FROM users;
Result
email        | domain
-------------+---------
ali@shop.com | shop.com
noreply@x.io | x.io

The same search in every dialect

-- PostgreSQL and MySQL (SQL standard)
SELECT POSITION('@' IN email) FROM users;

-- MySQL, Oracle, SQLite
SELECT INSTR(email, '@') FROM users;

-- MySQL only
SELECT LOCATE('@', email) FROM users;

-- SQL Server
SELECT CHARINDEX('@', email) FROM users;
Result
engine     | call                   | result
-----------+------------------------+-------
PostgreSQL | POSITION('@' IN email) |      4
MySQL      | LOCATE('@', email)     |      4
SQLite     | INSTR(email, '@')      |      4
SQL Server | CHARINDEX('@', email)  |      4

Check for existence by testing the result

-- keep only rows whose value contains an '@'
SELECT email
FROM users
WHERE POSITION('@' IN email) > 0;
Result
email
------------
ali@shop.com
noreply@x.io

Find a later occurrence with a start position

-- MySQL: find the second dot by starting after the first
SELECT LOCATE('.', 'a.b.c', LOCATE('.', 'a.b.c') + 1) AS second_dot;
Result
second_dot
----------
         4

Common mistakes

Wrong
-- LOCATE takes the substring first, then the string.
-- These arguments are the wrong way round.
SELECT LOCATE(email, '@') FROM users;
Right
-- Substring first, then the string to search in.
SELECT LOCATE('@', email) FROM users;

Argument order is the biggest cross dialect trap. Standard POSITION uses substring IN string. INSTR is (string, substring). LOCATE and CHARINDEX are (substring, string). Swap them and you get 0 or a wrong index with no error to warn you.

Wrong
-- Feeding a possible 0 straight into SUBSTRING.
-- When 'X' is missing, POSITION returns 0.
SELECT SUBSTRING(code FROM POSITION('X' IN code)) FROM parts;
Right
-- 0 means not found, so guard for it first.
SELECT CASE WHEN POSITION('X' IN code) > 0
            THEN SUBSTRING(code FROM POSITION('X' IN code) + 1)
            ELSE code
       END AS suffix
FROM parts;

A return of 0 means the substring was not found. It is not an error and it is not a valid index, because positions are 1-based and start at 1. Always check for 0 before you use the result as an offset.

Wrong
-- Assuming a NULL column gives 0.
SELECT POSITION('@' IN email) FROM users;  -- NULL email yields NULL
Right
-- Coalesce first when the column can be NULL.
SELECT POSITION('@' IN COALESCE(email, '')) FROM users;

If either argument is NULL the whole result is NULL, not 0. Wrap the input in COALESCE when a column is nullable and you need a numeric answer for every row.

Performance

POSITION() and its cousins scan a string character by character, so a standard B-tree index cannot speed up an arbitrary substring search. A predicate like POSITION('abc' IN name) > 0 forces a full scan of the column, exactly like LIKE '%abc%'. If the match is anchored to the start of the value, prefer LIKE 'abc%' so the optimiser can use the index.

For repeated substring lookups on large tables, reach for a purpose built tool: a full text index in MySQL or SQL Server, or a trigram index (pg_trgm) in PostgreSQL. If you only ever slice out a fixed part of a value, a generated column plus an index on it lets the engine answer without recomputing POSITION() each time.

Interview questions

How do you find the position of a substring in MySQL, PostgreSQL and SQL Server?

The SQL standard form POSITION(substring IN string) works in PostgreSQL and MySQL. MySQL also has LOCATE(substring, string) and INSTR(string, substring). SQL Server uses CHARINDEX(substring, string). All are 1-based and return 0 when the substring is absent.

Is the returned index 0-based or 1-based?

It is 1-based. The first character sits at position 1. A result of 0 is reserved to mean the substring was not found, so 0 is never a real position.

What does POSITION return when the substring is not present?

It returns 0. That is a normal value, not an error. NULL only appears when one of the arguments is itself NULL.

How would you extract the domain from an email address?

Find the @ with POSITION and slice everything after it with SUBSTRING. Adding 1 to the position skips the @ itself.

SELECT SUBSTRING(email FROM POSITION('@' IN email) + 1) AS domain
FROM users;

What is the difference between INSTR, LOCATE and CHARINDEX?

They all locate a substring but differ in argument order and availability. INSTR(string, substring) is in MySQL, Oracle and SQLite. LOCATE(substring, string [, start]) is MySQL only and accepts an optional start position. CHARINDEX(substring, string [, start]) is the SQL Server spelling. All are 1-based and return 0 when there is no match.

How do you check whether a string contains a substring?

Test the position against zero, for example POSITION('@' IN email) > 0. LIKE with wildcards also works, but POSITION is handy when you need the actual index as well as a yes or no answer.

Master SQL, one function at a time

Browse the full SQL functions library, or learn the fundamentals with our free, structured courses.