On this page
Syntax
CONCAT_WS(separator, value1, value2)CONCAT_WS(separator, value1, value2, value3, ...) | Parameter | Type | Required | Description |
|---|---|---|---|
separator |
string | yes | The text placed between each value. This is the FIRST argument, not data. If it is NULL the whole result is NULL. |
value1, value2, ... |
string or any castable | yes | Two or more values to join. Any argument that is NULL is skipped entirely, so no separator is added for it. |
How it works
CONCAT_WS() stands for "concatenate with separator". Unlike plain CONCAT(), you pass the separator once as the first argument and the function places it between every remaining value for you. This makes it the cleanest way to build comma separated lines, full addresses and full names without sprinkling the separator by hand.
The key behaviour is that CONCAT_WS() SKIPS NULL arguments. If a value is NULL, it is left out and no extra separator is produced for it, so you never end up with a doubled or trailing separator. This is different from CONCAT(), which in MySQL simply treats NULL as an empty string, and from the || operator, which in standard SQL turns the whole expression NULL if any part is NULL.
Because it ignores missing pieces, CONCAT_WS() is ideal for optional fields such as a middle name or a second address line. It is available in MySQL, PostgreSQL and SQL Server (2017 and later). Note that SQLite does not have CONCAT_WS(); there you must fall back to the || operator with manual NULL handling.
Examples
Join address parts into one line
SELECT CONCAT_WS(', ', street, city, country) AS full_address
FROM customers;
full_address ---------------------------- 12 Palm St, Dubai, UAE 8 Nile Rd, Cairo, EGY
Build a full name that skips a missing middle name
-- middle_name is NULL for many people
SELECT CONCAT_WS(' ', first_name, middle_name, last_name) AS full_name
FROM people;
full_name ------------------- Aisha Khan John Michael Doe
Build a CSV row from several columns
SELECT CONCAT_WS(',', id, name, email, country) AS csv_line
FROM customers;
csv_line ---------------------------------- 1,Aisha Khan,aisha@x.com,UAE 2,John Doe,john@x.com,EGY
NULL values are dropped, no doubled separator
-- addr2 is NULL, so it is skipped cleanly
SELECT CONCAT_WS(' | ', addr1, addr2, city) AS label
FROM sites;
label ---------------------- Unit 5 | Dubai Unit 9 | Floor 2 | Doha
Separator applies only between values
SELECT CONCAT_WS('-', '2026', '07', '04') AS iso_date;
iso_date ---------- 2026-07-04
Common mistakes
-- Expecting NULL to show as an empty slot
-- between the separators
SELECT CONCAT_WS(', ', city, region, country)
FROM addresses; -- region is NULL
Right
-- If you truly want a placeholder, coalesce first
SELECT CONCAT_WS(', ', city, COALESCE(region, 'N/A'), country)
FROM addresses;
CONCAT_WS() SKIPS NULL arguments; it does not leave a blank slot with separators around it. If you expected "Dubai, , UAE" you will get "Dubai, UAE" instead. Wrap the value in COALESCE() when you actually need a placeholder.
-- Treating the first argument as data
SELECT CONCAT_WS(first_name, last_name, email)
FROM users;
Right
-- The first argument is the separator
SELECT CONCAT_WS(' ', first_name, last_name, email)
FROM users;
The FIRST argument is always the separator, not a value to join. Passing a real column there uses that column as glue between the remaining values, which is almost never what you want.
-- Using CONCAT_WS in SQLite
SELECT CONCAT_WS('-', a, b) FROM t;
Right
-- SQLite has no CONCAT_WS; use || instead
SELECT a || '-' || b FROM t;
SQLite does not provide CONCAT_WS(). Use the || operator, and remember that in SQLite any NULL operand makes the whole result NULL, so wrap parts in COALESCE() if needed.
Performance
CONCAT_WS() is a lightweight scalar function evaluated once per row, so on its own it is cheap. The cost you should watch is applying it to columns you then filter on: wrapping columns in CONCAT_WS() inside a WHERE clause prevents the optimiser from using an index, because the engine must build the string for every row before comparing.
When you only need a display value, compute CONCAT_WS() in the SELECT list rather than in WHERE or JOIN conditions. If you must search on a joined string, store it in a persisted computed column or a generated column and index that instead of building it on the fly for every query.
For very large exports, building CSV lines with CONCAT_WS() in SQL is fine, but pushing millions of rows through string concatenation adds CPU work; measure whether formatting in the application layer is cheaper for your workload.
Interview questions
What is the difference between CONCAT_WS() and CONCAT()?
CONCAT_WS() takes a separator as its first argument and places it between the remaining values, and it SKIPS any NULL argument. CONCAT() has no separator and, in MySQL, treats NULL as an empty string. So CONCAT_WS avoids doubled or trailing separators when values are missing.
How does CONCAT_WS() handle NULL values?
It ignores them. A NULL value argument is skipped and no separator is added for it. The only NULL that matters is the separator itself: if the separator is NULL the entire result is NULL.
SELECT CONCAT_WS('-', 'a', NULL, 'b') AS r; -- 'a-b'
How would you build a full name that omits a missing middle name?
Pass a single space as the separator and list the three name parts. Because CONCAT_WS skips the NULL middle name, you get a clean single space between first and last name with no stray extra space.
SELECT CONCAT_WS(' ', first_name, middle_name, last_name) AS full_name
FROM people;
Does SQLite support CONCAT_WS()?
No. SQLite has no CONCAT_WS() function. You must use the || operator to join strings, and because || yields NULL if any operand is NULL, you often wrap parts in COALESCE() to reproduce the skipping behaviour.
Why might CONCAT_WS() in a WHERE clause hurt performance?
Wrapping indexed columns in CONCAT_WS() forces the engine to compute the combined string for every row before comparing, so it cannot use an index on those columns. Compute it in the SELECT list, or index a persisted computed column, instead of filtering on the built string.