SQL cho phân tích dữ liệu · Hướng dẫn · 30 tháng 10, 2026 · 11 phút đọc
Hàm chuỗi trong SQL: CONCAT, SUBSTRING, REPLACE, UPPER, TRIM và hơn thế

Hàm chuỗi giúp bạn làm sạch và biến đổi văn bản ngay trong truy vấn, thay vì xuất ra bảng tính rồi sửa tay. Chỉ một nhóm nhỏ hàm đã đủ cho phần lớn công việc thực tế: || và CONCAT nối văn bản, SUBSTRING, LEFT, RIGHT và SPLIT_PART cắt ra từng phần, REPLACE và REGEXP_REPLACE thay ký tự, UPPER, LOWER và TRIM chuẩn hóa giá trị để chúng khớp nhau, còn LENGTH và POSITION đo độ dài và tìm kiếm. Bài này đi qua từng hàm trên dữ liệu thật và chỉ ra những chỗ PostgreSQL, DuckDB và các hệ quản trị khác xử lý khác nhau.
Mọi truy vấn ở đây đều chạy trên cơ sở dữ liệu hãng hàng không của khóa SQL trên Nomto, theo cú pháp PostgreSQL trên DuckDB (engine chạy phía sau phòng học SQL của Nomto). Các bảng kết quả là kết quả chạy thật. Những cột văn bản chúng ta sẽ dùng:
| Bảng | Cột | Giá trị ví dụ |
|---|---|---|
airports |
airport_code, airport_name, city |
SVO, Sheremetyevo, Moscow |
aircrafts |
model |
Sukhoi Superjet-100 |
flights |
flight_no |
PG0405 |
seats |
seat_no |
10A |
tickets |
passenger_name, contact_data (JSON) |
VALERIY TIKHONOV, {"phone": "+70127117011"} |
Nối chuỗi: || và CONCAT
Trong PostgreSQL và DuckDB, cách chuẩn để nối chuỗi là toán tử ||. Hàm CONCAT() làm cùng việc đó dưới dạng hàm:
SELECT airport_code,
city,
airport_name,
city || ' (' || airport_code || ')' AS label,
CONCAT(city, ' - ', airport_name) AS full_name
FROM airports
WHERE city IN ('Moscow', 'Sochi')
ORDER BY airport_code;
| airport_code | city | airport_name | label | full_name |
|---|---|---|---|---|
| AER | Sochi | Sochi | Sochi (AER) | Sochi - Sochi |
| DME | Moscow | Domodedovo | Moscow (DME) | Moscow - Domodedovo |
| SVO | Moscow | Sheremetyevo | Moscow (SVO) | Moscow - Sheremetyevo |
| VKO | Moscow | Vnukovo | Moscow (VKO) | Moscow - Vnukovo |
Hai cách này không hoàn toàn giống nhau. Chúng xử lý NULL khác nhau:
SELECT 'PG' || NULL AS pipes,
CONCAT('PG', NULL) AS concat_fn,
concat_ws(' / ', 'SVO', NULL, 'LED') AS ws;
| pipes | concat_fn | ws |
|---|---|---|
| NULL | PG | SVO / LED |
Với ||, chỉ một giá trị NULL cũng biến cả kết quả thành NULL. CONCAT bỏ qua các NULL. concat_ws (concatenate with separator, nối kèm dấu phân cách) đặt dấu phân cách giữa các giá trị và cũng bỏ qua NULL, rất tiện khi ghép địa chỉ hay nhãn tuyến bay từ những phần không bắt buộc. Nếu bạn tạo nhãn bằng || và một số dòng ra rỗng, nguyên nhân thường gặp là một cột NULL; hãy bọc cột đó trong COALESCE(col, '') hoặc chuyển sang CONCAT.
Các hệ quản trị khác. || và CONCAT đều chạy được trong PostgreSQL, DuckDB, Oracle và SQLite. MySQL dùng CONCAT (ở đó || nghĩa là phép OR logic, trừ khi bật một chế độ riêng), còn SQL Server dùng + hoặc CONCAT.
UPPER, LOWER và viết hoa chữ cái đầu của tên
UPPER và LOWER đổi chữ hoa, chữ thường. Tên hành khách được lưu bằng chữ in hoa:
SELECT passenger_name,
lower(passenger_name) AS lower_name,
upper(left(split_part(passenger_name, ' ', 1), 1))
|| lower(substring(split_part(passenger_name, ' ', 1), 2)) AS first_name,
length(passenger_name) AS len
FROM tickets
LIMIT 3;
| passenger_name | lower_name | first_name | len |
|---|---|---|---|
| VALERIY TIKHONOV | valeriy tikhonov | Valeriy | 16 |
| EVGENIYA ALEKSEEVA | evgeniya alekseeva | Evgeniya | 18 |
| ARTUR GERASIMOV | artur gerasimov | Artur | 15 |
PostgreSQL có initcap(passenger_name), viết hoa chữ cái đầu của mọi từ trong một lần (Valeriy Tikhonov). DuckDB không có initcap, nên truy vấn trên tự làm việc đó: chữ cái đầu viết hoa, phần còn lại viết thường.
Công dụng lớn hơn của LOWER là so khớp. So sánh chuỗi phân biệt hoa thường: WHERE city = 'moscow' không tìm thấy gì. Hãy so sánh lower(city) = 'moscow', hoặc dùng ILIKE, tức là LIKE không phân biệt hoa thường:
SELECT COUNT(*) FILTER (WHERE airport_name LIKE '%ovo') AS like_ovo,
COUNT(*) FILTER (WHERE city ILIKE 'novo%') AS ilike_novo
FROM airports;
| like_ovo | ilike_novo |
|---|---|
| 11 | 2 |
ILIKE chạy được trong PostgreSQL và DuckDB nhưng không có trong MySQL hay SQL Server; ở đó việc phân biệt hoa thường phụ thuộc vào collation của cột.
SUBSTRING, LEFT, RIGHT và SPLIT_PART: cắt văn bản
substring(text, start, length) trả về một phần của chuỗi. Vị trí bắt đầu từ 1, và nếu bỏ qua độ dài, bạn nhận được mọi thứ đến hết chuỗi. Số hiệu chuyến bay gồm mã hãng PG cộng bốn chữ số:
SELECT flight_no,
substring(flight_no, 3) AS num,
CAST(substring(flight_no, 3) AS INTEGER) AS num_int,
right(flight_no, 4) AS r
FROM flights
WHERE flight_id IN (1, 2);
| flight_no | num | num_int | r |
|---|---|---|---|
| PG0405 | 0405 | 405 | 0405 |
| PG0404 | 0404 | 404 | 0404 |
num vẫn là văn bản ('0405'); hãy ép kiểu nếu bạn cần sắp xếp hay so sánh theo số, nếu không '1000' sẽ đứng trước '999'. LEFT(text, n) và RIGHT(text, n) là cách viết tắt để lấy n ký tự đầu hoặc cuối. PostgreSQL cũng chấp nhận dạng chuẩn substring(text FROM 3 FOR 4); MySQL và SQL Server cũng dùng SUBSTRING(text, 3, 4), còn Oracle gọi nó là SUBSTR.
Khi văn bản có dấu phân cách, split_part(text, delimiter, n) dễ hơn đếm vị trí. Tách tên hành khách thành tên và họ, kèm cách làm tương tự bằng POSITION:
SELECT passenger_name,
split_part(passenger_name, ' ', 1) AS first_name,
split_part(passenger_name, ' ', 2) AS last_name,
position(' ' IN passenger_name) AS space_at,
substring(passenger_name FROM 1 FOR position(' ' IN passenger_name) - 1) AS first2,
left(passenger_name, 1) AS initial
FROM tickets
LIMIT 3;
| passenger_name | first_name | last_name | space_at | first2 | initial |
|---|---|---|---|---|---|
| VALERIY TIKHONOV | VALERIY | TIKHONOV | 8 | VALERIY | V |
| EVGENIYA ALEKSEEVA | EVGENIYA | ALEKSEEVA | 9 | EVGENIYA | E |
| ARTUR GERASIMOV | ARTUR | GERASIMOV | 6 | ARTUR | A |
position(' ' IN x) và strpos(x, ' ') là một trong PostgreSQL và DuckDB; cả hai trả về 0 khi không tìm thấy chuỗi con. split_part có trong PostgreSQL, DuckDB và Snowflake; MySQL dùng SUBSTRING_INDEX thay thế, còn SQL Server không có cả hai.
Khi đã tách được một phần, bạn có thể nhóm theo nó. Những tên phổ biến nhất trên vé:
SELECT split_part(passenger_name, ' ', 1) AS first_name,
COUNT(*) AS tickets
FROM tickets
GROUP BY 1
ORDER BY 2 DESC
LIMIT 5;
| first_name | tickets |
|---|---|
| ALEKSANDR | 20328 |
| SERGEY | 15133 |
| VLADIMIR | 12806 |
| TATYANA | 12058 |
| ELENA | 11291 |
REPLACE và REGEXP_REPLACE
replace(text, from, to) thay mọi lần xuất hiện của một chuỗi con:
SELECT model,
replace(model, 'Sukhoi ', '') AS short,
replace(model, '-', ' ') AS no_dash
FROM aircrafts
WHERE model LIKE 'S%' OR model LIKE 'Bombardier%';
| model | short | no_dash |
|---|---|---|
| Bombardier CRJ-200 | Bombardier CRJ-200 | Bombardier CRJ 200 |
| Sukhoi Superjet-100 | Superjet-100 | Sukhoi Superjet 100 |
Nếu chuỗi con không có trong giá trị, giá trị được trả về nguyên vẹn. Thay bằng '' nghĩa là xóa.
Khi thứ cần thay là một mẫu chứ không phải văn bản cố định, hãy dùng biểu thức chính quy (regex). Số ghế ghép từ số hàng và một chữ cái (10A); tách chúng bằng cách xóa chữ số hoặc chữ cái:
SELECT seat_no,
regexp_replace(seat_no, '[0-9]', '', 'g') AS letter,
CAST(regexp_replace(seat_no, '[A-Z]', '', 'g') AS INTEGER) AS row_no
FROM seats
WHERE aircraft_code = '319'
ORDER BY row_no, letter
LIMIT 4;
| seat_no | letter | row_no |
|---|---|---|
| 1A | A | 1 |
| 1C | C | 1 |
| 1D | D | 1 |
| 1F | F | 1 |
Cờ 'g' nghĩa là “global”: thay mọi kết quả khớp, không chỉ kết quả đầu tiên. Không có nó, regexp_replace('12A', '[0-9]', '') cho ra 2A. Điều này giống nhau trong PostgreSQL và DuckDB. Để lấy phần khớp ra thay vì thay thế, PostgreSQL dùng substring(text FROM 'pattern') hoặc regexp_substr (từ phiên bản 15), còn DuckDB dùng regexp_extract(text, 'pattern').
TRIM, LTRIM, RTRIM và LPAD
trim xóa khoảng trắng ở đầu và cuối chuỗi; khoảng trắng thừa là lý do phổ biến nhất khiến hai giá trị trông giống nhau lại không khớp khi JOIN:
SELECT '|' || trim(' SVO ') || '|' AS t,
'|' || ltrim(' SVO ') || '|' AS l,
'|' || rtrim(' SVO ') || '|' AS r,
trim(BOTH '0' FROM '0405') AS z,
ltrim('0405', '0') AS lz;
| t | l | r | z | lz |
|---|---|---|---|---|
| |SVO| | |SVO | | | SVO| | 405 | 405 |
Các dấu gạch đứng giúp nhìn thấy khoảng trắng. trim còn có thể xóa ký tự khác, ví dụ các số 0 ở đầu. length(' SVO ') bằng 6 trong khi length(trim(' SVO ')) bằng 3; đây là cách nhanh để tìm giá trị bẩn: WHERE length(code) <> length(trim(code)).
lpad(text, length, fill) làm ngược lại: đệm thêm ký tự vào bên trái. Số hiệu chuyến bay tiếp theo sau PG0405, giữ đủ bốn chữ số:
SELECT flight_no,
lpad(CAST(CAST(substring(flight_no, 3) AS INTEGER) + 1 AS VARCHAR), 4, '0') AS next_no
FROM flights
WHERE flight_id = 1;
| flight_no | next_no |
|---|---|
| PG0405 | 0406 |
Văn bản bên trong JSON: ->>
Thông tin liên hệ nằm trong một cột JSON. Toán tử ->> trả về một trường dưới dạng văn bản, và từ đó mọi hàm chuỗi đều dùng được:
SELECT ticket_no,
contact_data ->> 'phone' AS phone,
contact_data ->> 'email' AS email,
right(contact_data ->> 'phone', 4) AS last4
FROM tickets
LIMIT 3;
| ticket_no | phone | last4 | |
|---|---|---|---|
| 0005432000987 | +70127117011 | NULL | 7011 |
| 0005432000988 | +70378089255 | NULL | 9255 |
| 0005432000989 | +70760429203 | NULL | 9203 |
Khóa không tồn tại trả về NULL, không phải chuỗi rỗng. 199.083 trong số 366.733 vé có email, và bạn đếm chúng bằng COUNT(*) FILTER (WHERE contact_data ->> 'email' IS NOT NULL). ->> chạy được trên json và jsonb trong PostgreSQL và trên JSON trong DuckDB.
STRING_AGG: gộp nhiều dòng thành một chuỗi
Ngược lại với tách chuỗi: gộp các giá trị của một nhóm thành một ô, ngăn cách bằng dấu phẩy. Những thành phố có nhiều hơn một sân bay:
SELECT city,
string_agg(airport_code, ', ' ORDER BY airport_code) AS airports,
COUNT(*) AS n
FROM airports
GROUP BY city
HAVING COUNT(*) > 1
ORDER BY city;
| city | airports | n |
|---|---|---|
| Moscow | DME, SVO, VKO | 3 |
| Ulyanovsk | ULV, ULY | 2 |
string_agg chạy được trong PostgreSQL, DuckDB và SQL Server (từ 2017). MySQL gọi nó là GROUP_CONCAT; Oracle và Snowflake gọi là LISTAGG. HAVING được giải thích trong bài GROUP BY và HAVING.
Tra cứu nhanh: PostgreSQL, DuckDB và các hệ khác
| Việc cần làm | PostgreSQL | DuckDB | MySQL | SQL Server |
|---|---|---|---|---|
| Nối chuỗi | ||, CONCAT |
||, CONCAT |
CONCAT |
+, CONCAT |
| Độ dài | length, char_length |
length |
CHAR_LENGTH |
LEN |
| Tìm vị trí | position(x IN s), strpos |
position, strpos |
LOCATE, INSTR |
CHARINDEX |
| Tách theo dấu phân cách | split_part |
split_part |
SUBSTRING_INDEX |
STRING_SPLIT (trả về bảng) |
| Viết hoa chữ cái đầu mỗi từ | initcap |
không có | không có | không có |
| Trích xuất bằng regex | substring(s FROM re), regexp_substr |
regexp_extract |
REGEXP_SUBSTR |
không có sẵn |
| Gộp dòng thành chuỗi | string_agg |
string_agg |
GROUP_CONCAT |
STRING_AGG |
| LIKE không phân biệt hoa thường | ILIKE |
ILIKE |
tùy collation | tùy collation |
Các lỗi thường gặp
- Dùng
||với cột có thểNULL. MộtNULLlàm rỗng cả nhãn. Hãy dùngCONCAT,concat_wshoặcCOALESCE. - So sánh mà không chuẩn hóa.
'SVO 'có khoảng trắng ở cuối và'svo'viết thường sẽ không bao giờ bằng'SVO'. Hãytrimvàlower/uppercả hai vế trước khi JOIN theo khóa văn bản. - Sắp xếp số được lưu dưới dạng văn bản. Ép các chữ số đã tách sang
INTEGERtrước khiORDER BYhayMAX. - Lệch một vị trí. Chuỗi trong SQL bắt đầu từ vị trí 1, không phải 0, và
positiontrả về 0 khi không tìm thấy, nênsubstring(x, 1, position(...) - 1)trả về chuỗi rỗng với những giá trị không có dấu phân cách. - Quên cờ
'g'trongregexp_replace, nên chỉ kết quả khớp đầu tiên bị thay. - Dùng nháy kép cho văn bản. Trong PostgreSQL,
"SVO"là tên cột; văn bản phải đặt trong nháy đơn.
Ngày giờ có bộ hàm riêng, được trình bày trong bài hàm ngày giờ trong SQL. Muốn luyện thêm với các bài kiểu phỏng vấn thật, hãy xem bài tập SQL có lời giải.
Câu hỏi thường gặp
Nối chuỗi trong SQL như thế nào?
Trong PostgreSQL và DuckDB, dùng first || ' ' || last hoặc CONCAT(first, ' ', last). CONCAT bỏ qua giá trị NULL, còn || trả về NULL nếu có bất kỳ phần nào là NULL. Trong MySQL dùng CONCAT; trong SQL Server dùng + hoặc CONCAT.
SUBSTRING và SPLIT_PART khác nhau thế nào?
SUBSTRING cắt theo vị trí (ký tự thứ 3 đến thứ 6). SPLIT_PART cắt theo dấu phân cách (từ thứ hai sau khi tách theo khoảng trắng). Hãy dùng SPLIT_PART khi các phần có độ dài khác nhau nhưng được ngăn cách bằng một ký tự đã biết.
Xóa khoảng trắng trong chuỗi SQL như thế nào?
trim(x) xóa khoảng trắng ở cả hai đầu, ltrim và rtrim chỉ xóa ở một phía. Để xóa mọi khoảng trắng, kể cả ở giữa, dùng replace(x, ' ', '').
Làm sao so sánh chuỗi không phân biệt hoa thường?
So sánh giá trị đã chuyển về chữ thường, lower(city) = 'moscow', hoặc dùng ILIKE trong PostgreSQL và DuckDB. Trong MySQL và SQL Server, phép so sánh thường đã không phân biệt hoa thường sẵn, tùy theo collation.
PostgreSQL có hàm LEN không?
Không. Hãy dùng length(x) hoặc char_length(x). LEN là hàm của SQL Server; lưu ý LEN của SQL Server bỏ qua khoảng trắng ở cuối, còn length của PostgreSQL trên kiểu text thì vẫn đếm chúng.
Ngôn ngữ khác: English, Español, Português (Brasil), 简体中文