- SQL
SQL 文字列関数 一覧|4DB比較表とコピペSQL・実行結果つき
SQLの文字列関数とは、文字列の連結・切り出し・検索・置換・変換などを行う関数で、名前だけでなく「NULLの扱い」「文字数の数え方」がデータベースごとに異なります。 たとえば連結は、MySQLではCONCAT、PostgreSQLとOracleは||、SQL Serverは+が定番ですが、途中にNULLが入ったときの結果が同じとは限りません。
この記事では、よく使う文字列操作を「やりたいこと」別に4DBで横並びにした早見表と、コピペ用のSQLをまとめました。MySQL 8.0.46とPostgreSQL 16.13は実際に実行し、出力をそのまま掲載しています。OracleとSQL Serverは公式ドキュメント(Oracle Database SQL Language Reference 23ai、Microsoft Learn)に沿って記載し、表の見出しで「実行確認済み」「ドキュメント準拠」を区別しています。日本語(マルチバイト文字)の文字数とバイト数、Oracleで空文字''がNULLになる点、照合順序による大文字小文字の区別も、実行結果で確認します。
SQL文字列関数の早見表(4DB比較)
sは文字列の列または値です。位置はすべて1始まりです。
| やりたいこと | MySQL 8.0(実行確認済み) | PostgreSQL 16(実行確認済み) | Oracle(ドキュメント準拠) | SQL Server(ドキュメント準拠) |
|---|---|---|---|---|
| 連結 | CONCAT(a, b) |
a || b / CONCAT(a, b) |
a || b / CONCAT(a, b) |
a + b / CONCAT(a, b) |
| 区切り付き連結 | CONCAT_WS('-', a, b) |
CONCAT_WS('-', a, b) |
なし(||で組み立て) |
CONCAT_WS('-', a, b)※2017以降 |
| 文字数 | CHAR_LENGTH(s) |
length(s) / char_length(s) |
LENGTH(s) |
LEN(s)※末尾空白は数えない |
| バイト数 | LENGTH(s) |
octet_length(s) |
LENGTHB(s) |
DATALENGTH(s) |
| 部分取得 | SUBSTRING(s, 5, 4) |
substr(s, 5, 4) |
SUBSTR(s, 5, 4) |
SUBSTRING(s, 5, 4) |
| 左から・右から | LEFT(s, 3) RIGHT(s, 4) |
left(s, 3) right(s, 4) |
SUBSTR(s, 1, 3) SUBSTR(s, -4) |
LEFT(s, 3) RIGHT(s, 4) |
| 位置検索 | LOCATE('-', s) / INSTR(s, '-') |
strpos(s, '-') / position('-' IN s) |
INSTR(s, '-') |
CHARINDEX('-', s) |
| 置換 | REPLACE(s, '/', '-') |
replace(s, '/', '-') |
REPLACE(s, '/', '-') |
REPLACE(s, '/', '-') |
| 1文字ずつ置換・削除 | なし(REPLACEを重ねる) |
translate(s, '-', '') |
TRANSLATE(s, '-x', 'x')※後述 |
TRANSLATE(s, '-', '/')※2017以降、同じ長さ |
| 前後の空白除去 | TRIM(s) |
trim(s) |
TRIM(s) |
TRIM(s)※2017以降 |
| 指定文字の除去 | TRIM(LEADING '0' FROM s) |
ltrim(s, '0') |
LTRIM(s, '0') |
LTRIM(s, '0')※2022以降 |
| 大文字・小文字 | UPPER(s) LOWER(s) |
upper(s) lower(s) initcap(s) |
UPPER LOWER INITCAP |
UPPER(s) LOWER(s) |
| ゼロ埋め | LPAD(s, 5, '0') |
lpad(s, 5, '0') |
LPAD(s, 5, '0') |
RIGHT('00000' + s, 5) / FORMAT(n, '00000') |
| 繰り返し | REPEAT(s, 3) |
repeat(s, 3) |
RPAD(s, 長さ, s)で代用 |
REPLICATE(s, 3) |
| 反転 | REVERSE(s) |
reverse(s) |
REVERSEはドキュメント外※ |
REVERSE(s) |
| n番目の要素(分割) | SUBSTRING_INDEXを2回 |
split_part(s, ',', 2) |
REGEXP_SUBSTR(s, '[^,]+', 1, 2) |
STRING_SPLIT(s, ',', 1)※2022以降で順序番号 |
| 行へ分割 | JSON_TABLEで代用 |
string_to_array + unnest / regexp_split_to_table |
CONNECT BY + REGEXP_SUBSTR |
STRING_SPLIT(s, ',') |
| 集約連結 | GROUP_CONCAT(x ORDER BY x) |
string_agg(x, ',' ORDER BY x) |
LISTAGG(x, ',') WITHIN GROUP (ORDER BY x) |
STRING_AGG(x, ',') WITHIN GROUP (ORDER BY x) |
| 正規表現で判定 | REGEXP_LIKE(s, p) / s REGEXP p |
s ~ p(大小無視は~*) |
REGEXP_LIKE(s, p) |
REGEXP_LIKE(s, p)※2025以降 |
| 正規表現で抽出 | REGEXP_SUBSTR(s, p) |
substring(s FROM p) / regexp_match |
REGEXP_SUBSTR(s, p) |
REGEXP_SUBSTR(s, p)※2025以降 |
| 正規表現で置換 | REGEXP_REPLACE(s, p, r)(全置換) |
regexp_replace(s, p, r, 'g') |
REGEXP_REPLACE(s, p, r)(全置換) |
REGEXP_REPLACE(s, p, r)※2025以降 |
| 数値→文字列 | CAST(n AS CHAR) |
n::text / CAST(n AS text) |
TO_CHAR(n) |
CAST(n AS varchar(20)) |
| 文字列→数値 | CAST(s AS SIGNED) |
s::integer |
TO_NUMBER(s) |
CAST(s AS int) / TRY_CAST |
| 3桁カンマ | FORMAT(n, 2) |
to_char(n, 'FM9,999,999.00') |
TO_CHAR(n, 'FM9,999,999.00') |
FORMAT(n, 'N2') |
| 大文字小文字を無視して比較 | 既定で無視(_ci照合) |
ILIKE / lower(a) = lower(b) |
UPPER(a) = UPPER(b) |
既定で無視(_CI_照合) |
迷ったら「NULLを含む連結」「文字数とバイト数」「空文字とNULL」「大文字小文字の区別」の4点がDBで違うと覚えておくと、移行やレビューで役立ちます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「SQL 文字列関数 一覧|4DB比較表とコピペSQL・実行結果つき」
検証環境と前提
| 項目 | 内容 |
|---|---|
| MySQL | 8.0.46(文字コードutf8mb4、照合順序utf8mb4_0900_ai_ci、既定のsql_mode) |
| PostgreSQL | 16.13(サーバー文字コードUTF8) |
| Oracle | SQL Language Reference(23ai)に準拠。未実行 |
| SQL Server | Microsoft Learn(SQL Server 2022/2025)に準拠。未実行 |
実行結果として載せているのはMySQLとPostgreSQLだけです。MySQLの最新LTSは8.4、PostgreSQLの最新メジャーは18ですが、この記事の関数は8.4・18でも同じ名前で使えます(PostgreSQL 18で増えた点は該当箇所で補足します)。
MySQLクライアントは--default-character-set=utf8mb4で接続してください。 接続の文字コードが合っていないと、日本語が二重に変換され、CHAR_LENGTH('データベース')が6ではなく18になる、REVERSEで文字化けするといった現象が起きます(実際に最初の実行でこの状態になりました)。
文字列を連結する(CONCAT・||・+)とNULLの扱い
連結で一番差が出るのは、途中にNULLがあるときです。
-- PostgreSQL 16
SELECT 'SQL' || '入門' AS pipe, CONCAT('SQL', '入門') AS concat_fn, CONCAT_WS('-', '2026', '09', '28') AS ws;
SELECT 'A' || NULL AS pipe_null, CONCAT('A', NULL, 'B') AS concat_null, CONCAT_WS(',', 'A', NULL, 'B') AS ws_null;
pipe | concat_fn | ws
---------+-----------+------------
SQL入門 | SQL入門 | 2026-09-28
pipe_null | concat_null | ws_null
-----------+-------------+---------
| AB | A,B
PostgreSQLは||はNULLが1つでもあると結果がNULL(空欄に見えている部分)、CONCATはNULLを無視します。
-- MySQL 8.0
SELECT CONCAT('SQL', '入門') AS concat_fn, CONCAT_WS('-', '2026', '09', '28') AS ws, 'SQL' || '入門' AS pipe;
SELECT CONCAT('A', NULL, 'B') AS concat_null, CONCAT_WS(',', 'A', NULL, 'B') AS ws_null, CONCAT('A', IFNULL(NULL, ''), 'B') AS ifnull_fix;
+-----------+------------+------+
| concat_fn | ws | pipe |
+-----------+------------+------+
| SQL入門 | 2026-09-28 | 0 |
+-----------+------------+------+
+-------------+---------+------------+
| concat_null | ws_null | ifnull_fix |
+-------------+---------+------------+
| NULL | A,B | AB |
+-------------+---------+------------+
MySQLは逆に、CONCATはNULLが1つでもあるとNULLです。CONCAT_WSはNULLを飛ばします。また、MySQLの||は既定では論理和(OR)なので、'SQL' || '入門'は0になります。 sql_modeにPIPES_AS_CONCATを加えると連結になります。
-- MySQL 8.0:|| を連結として使う(セッション単位)
SET SESSION sql_mode = CONCAT(@@sql_mode, ',PIPES_AS_CONCAT');
SELECT 'SQL' || '入門' AS pipe_concat; -- SQL入門
| DB | || とNULL |
CONCAT とNULL |
NULLを空文字にする |
|---|---|---|---|
| MySQL(実行確認済み) | 既定は論理和 | NULLになる | IFNULL(x, '') / COALESCE |
| PostgreSQL(実行確認済み) | NULLになる | 無視する | COALESCE(x, '') |
| Oracle(ドキュメント準拠) | 無視する(NULLは空文字と同じ扱い) | 無視する。23aiは3引数以上可、19cまでは2引数 | NVL(x, '')は不要 |
| SQL Server(ドキュメント準拠) | +はNULLになる(CONCAT_NULL_YIELDS_NULLがON) |
無視する | ISNULL(x, '') / COALESCE |
DBをまたいで同じ結果にしたいなら、COALESCE(x, '')で先にNULLを消してから連結するのが確実です。
文字数とバイト数を数える(日本語の違い)
MySQLのLENGTHはバイト数、PostgreSQLのlengthは文字数です。 名前が同じなので、移行時に一番取り違えやすい関数です。
-- MySQL 8.0(utf8mb4)
SELECT LENGTH('データベース') AS len_bytes, CHAR_LENGTH('データベース') AS char_len,
OCTET_LENGTH('データベース') AS octet, LENGTH('abc ') AS trailing_sp;
+-----------+----------+-------+-------------+
| len_bytes | char_len | octet | trailing_sp |
+-----------+----------+-------+-------------+
| 18 | 6 | 18 | 5 |
+-----------+----------+-------+-------------+
-- PostgreSQL 16(UTF8)
SELECT length('データベース') AS len, char_length('データベース') AS char_len,
octet_length('データベース') AS bytes, length('abc ') AS trailing_sp,
length('abc '::char(5)) AS char5;
len | char_len | bytes | trailing_sp | char5
-----+----------+-------+-------------+-------
6 | 6 | 18 | 5 | 3
UTF-8ではカタカナ1文字が3バイトなので、6文字で18バイトです。PostgreSQLのchar(n)型は末尾の空白を意味のないものとして扱うため、lengthが3になります。
| DB | 文字数 | バイト数 | 注意点(ドキュメント準拠を含む) |
|---|---|---|---|
| MySQL | CHAR_LENGTH |
LENGTH |
VARCHAR(10)の10は文字数 |
| PostgreSQL | length / char_length |
octet_length |
varchar(10)の10は文字数 |
| Oracle | LENGTH |
LENGTHB |
VARCHAR2(10)は既定でバイト数(NLS_LENGTH_SEMANTICSでCHARに変更可) |
| SQL Server | LEN(末尾空白を除く) |
DATALENGTH |
nvarcharは1文字2バイト(サロゲートペアは4バイト) |
「何文字まで入るか」の検証には文字数、「列の容量」の確認にはバイト数を使います。
部分文字列を取り出す(SUBSTRING・LEFT・RIGHT)
-- PostgreSQL 16
SELECT substring('ORD-2026-0042' FROM 5 FOR 4) AS sub, substr('ORD-2026-0042', 5, 4) AS substr_fn,
left('ORD-2026-0042', 3) AS l, right('ORD-2026-0042', 4) AS r,
substr('東京都千代田区', 1, 3) AS ja, left('ORD', -1) AS left_neg;
sub | substr_fn | l | r | ja | left_neg
------+-----------+-----+------+--------+----------
2026 | 2026 | ORD | 0042 | 東京都 | OR
-- MySQL 8.0
SELECT SUBSTRING('ORD-2026-0042', 5, 4) AS sub, LEFT('ORD-2026-0042', 3) AS l,
RIGHT('ORD-2026-0042', 4) AS r, SUBSTRING('東京都千代田区', 1, 3) AS ja,
SUBSTRING('ORD-2026-0042', -4) AS neg;
+------+------+------+-----------+------+
| sub | l | r | ja | neg |
+------+------+------+-----------+------+
| 2026 | ORD | 0042 | 東京都 | 0042 |
+------+------+------+-----------+------+
どちらも文字単位で切り出すので、日本語でも文字化けしません。 開始位置のマイナス指定はDBで意味が違います。MySQLとOracleのSUBSTR(s, -4)は「末尾から4文字目以降」、PostgreSQLのleft(s, -1)は「最後の1文字を除いた全部」です。SQL ServerのSUBSTRINGは開始位置を省略できないので、末尾からはRIGHTを使います。
文字の位置を検索する(LOCATE・strpos・INSTR・CHARINDEX)
-- MySQL 8.0
SELECT LOCATE('-', 'ORD-2026-0042') AS loc, INSTR('ORD-2026-0042', '2026') AS instr_fn,
POSITION('-' IN 'ORD-2026-0042') AS pos, LOCATE('-', 'ORD-2026-0042', 5) AS loc_from5,
LOCATE('X', 'ORD') AS notfound;
+-----+----------+-----+-----------+----------+
| loc | instr_fn | pos | loc_from5 | notfound |
+-----+----------+-----+-----------+----------+
| 4 | 5 | 4 | 9 | 0 |
+-----+----------+-----+-----------+----------+
-- PostgreSQL 16
SELECT position('-' IN 'ORD-2026-0042') AS pos, strpos('ORD-2026-0042', '2026') AS strpos_fn,
position('X' IN 'ORD') AS notfound;
-- pos=4, strpos_fn=5, notfound=0
見つからないときは4DBとも0を返します。 引数の順番は、MySQLのLOCATEとSQL ServerのCHARINDEXが「探す文字, 対象」、INSTRとstrposが「対象, 探す文字」で逆なので注意します。2番目の出現を探すなら、OracleはINSTR(s, '-', 1, 2)、MySQLはLOCATE('-', s, 前回位置 + 1)、SQL ServerはCHARINDEX('-', s, 開始位置)です。PostgreSQLには開始位置の引数がないため、strpos(substr(s, 5), '-')のように切り出してから探します。
置換する・一部を伏せる(REPLACE・TRANSLATE・OVERLAY)
-- PostgreSQL 16
SELECT replace('2026/09/28', '/', '-') AS rep,
translate('090-1234-5678', '-', '') AS tr,
overlay('090-1234-5678' PLACING '****' FROM 5 FOR 4) AS ov;
rep | tr | ov
------------+-------------+---------------
2026-09-28 | 09012345678 | 090-****-5678
-- MySQL 8.0:OVERLAY の代わりは INSERT 関数
SELECT REPLACE('2026/09/28', '/', '-') AS rep, INSERT('090-1234-5678', 5, 4, '****') AS ins;
-- rep=2026-09-28, ins=090-****-5678
translateは「1文字ずつの置き換え表」です。PostgreSQLでは置換先が短いと、あふれた文字は削除されます。OracleのTRANSLATEは置換先を空文字にすると結果がNULLになる(空文字がNULL扱いのため)ので、TRANSLATE(s, 'x-', 'x')のように残す文字を1つ入れて削除します。SQL ServerのTRANSLATEは置換元と置換先が同じ長さである必要があります。
前後の空白や指定文字を取り除く(TRIM・LTRIM・RTRIM)
-- PostgreSQL 16
SELECT '[' || trim(' abc ') || ']' AS t, '[' || ltrim(' abc ') || ']' AS lt,
'[' || rtrim(' abc ') || ']' AS rt, trim(BOTH '0' FROM '000123000') AS t0,
ltrim('000123', '0') AS lt0, '[' || trim(' 全角 ') || ']' AS zenkaku;
t | lt | rt | t0 | lt0 | zenkaku
-------+---------+---------+-----+-----+------------
[abc] | [abc ] | [ abc] | 123 | 123 | [ 全角 ]
MySQLも同じ結果でした(LTRIMに文字指定はできないのでTRIM(LEADING '0' FROM s)を使います)。TRIMが消すのは半角スペースだけで、全角スペースは残ります。 全角も消すには、消す文字を明示します。
-- PostgreSQL 16:半角・全角スペースの両方を除去
SELECT '[' || trim(BOTH ' ' FROM ' 全角 ') || ']' AS both_sp; -- [全角]
-- MySQL 8.0:TRIM は1つの文字列しか指定できないので正規表現を使う
SELECT CONCAT('[', REGEXP_REPLACE(' 全角 ', '^[[:space:] ]+|[[:space:] ]+$', ''), ']') AS re; -- [全角]
入力フォームから来たデータは、登録時に全角・半角の空白をそろえて除去しておくと、検索や重複チェックでの取りこぼしを防げます。
大文字・小文字に変換する(UPPER・LOWER・INITCAP)
-- PostgreSQL 16
SELECT upper('Hello SQL') AS up, lower('Hello SQL') AS low,
initcap('hello sql world') AS initcap_fn, upper('abc') AS fullwidth;
up | low | initcap_fn | fullwidth
-----------+-----------+-----------------+-----------
HELLO SQL | hello sql | Hello Sql World | ABC
全角英字も大文字に変わります(MySQLもABCでした)。INITCAPはPostgreSQLとOracleにあり、MySQLとSQL Serverにはありません。PostgreSQL 18では、大文字小文字を区別しない比較をより正確に行うcasefold()が追加されています。
ゼロ埋め・桁そろえ(LPAD・RPAD)と繰り返し・反転
-- PostgreSQL 16
SELECT lpad('42', 5, '0') AS lp, rpad('ab', 5, '*') AS rp,
lpad('123456', 4, '0') AS truncated, to_char(42, 'FM00000') AS tochar;
SELECT repeat('ab', 3) AS rpt, reverse('SQL入門') AS rev;
lp | rp | truncated | tochar
-------+-------+-----------+--------
00042 | ab*** | 1234 | 00042
rpt | rev
--------+---------
ababab | 門入LQS
LPADは指定した長さより長い文字列を切り詰めます(123456が1234になる)。MySQLも同じ結果でした。桁あふれを検出したいときは、先にCHAR_LENGTHで長さを確かめます。
-- SQL Server(ドキュメント準拠):LPAD がないので RIGHT と REPLICATE で組み立てる
SELECT RIGHT(REPLICATE('0', 5) + CAST(42 AS varchar(5)), 5) AS lp, -- 00042
FORMAT(42, '00000') AS fmt, -- 00042
REPLICATE('ab', 3) AS rpt, REVERSE('SQL') AS rev; -- ababab, LQS
※OracleのREVERSEはSQL Language Referenceに載っていない関数です。文字単位の反転が必要なら、アプリケーション側で行うほうが確実です。
区切り文字で分割する(SPLIT_PART・SUBSTRING_INDEX・STRING_SPLIT)
「n番目の要素がほしい」のか「行に展開したい」のかで書き方が変わります。
-- PostgreSQL 16:n番目の要素
SELECT split_part('tokyo,osaka,nagoya', ',', 2) AS part2,
split_part('tokyo,osaka,nagoya', ',', -1) AS last_part,
split_part('a,b', ',', 5) AS out_of_range;
part2 | last_part | out_of_range
-------+-----------+--------------
osaka | nagoya |
-- PostgreSQL 16:行に展開(順番つき)
SELECT * FROM unnest(string_to_array('tokyo,osaka,nagoya', ',')) WITH ORDINALITY AS t(val, n);
val | n
--------+---
tokyo | 1
osaka | 2
nagoya | 3
-- MySQL 8.0:n番目の要素は SUBSTRING_INDEX を2回
SELECT SUBSTRING_INDEX('tokyo,osaka,nagoya', ',', 1) AS first_part,
SUBSTRING_INDEX(SUBSTRING_INDEX('tokyo,osaka,nagoya', ',', 2), ',', -1) AS part2,
SUBSTRING_INDEX('tokyo,osaka,nagoya', ',', -1) AS last_part,
SUBSTRING_INDEX(SUBSTRING_INDEX('a,b', ',', 5), ',', -1) AS out_of_range;
+------------+-------+-----------+--------------+
| first_part | part2 | last_part | out_of_range |
+------------+-------+-----------+--------------+
| tokyo | osaka | nagoya | b |
+------------+-------+-----------+--------------+
範囲外の番号を指定すると、PostgreSQLは空文字、MySQLの2回SUBSTRING_INDEXは最後の要素bを返します。 MySQLで要素数を超えたかどうかを判定したいときは、CHAR_LENGTH(s) - CHAR_LENGTH(REPLACE(s, ',', '')) + 1で要素数を数えて比べます。
-- MySQL 8.0:行に展開(JSON配列に変換して JSON_TABLE)
SELECT j.n, j.val
FROM JSON_TABLE(CONCAT('["', REPLACE('tokyo,osaka,nagoya', ',', '","'), '"]'),
'$[*]' COLUMNS (n FOR ORDINALITY, val VARCHAR(50) PATH '$')) AS j;
+------+--------+
| n | val |
+------+--------+
| 1 | tokyo |
| 2 | osaka |
| 3 | nagoya |
+------+--------+
値に"や\が含まれる場合はJSONとして壊れるので、先にエスケープするか、再帰CTEで分割します。
-- SQL Server 2022以降(ドキュメント準拠):ordinal 列で順番を取る
SELECT value, ordinal FROM STRING_SPLIT('tokyo,osaka,nagoya', ',', 1) ORDER BY ordinal;
-- Oracle(ドキュメント準拠):2番目の要素
SELECT REGEXP_SUBSTR('tokyo,osaka,nagoya', '[^,]+', 1, 2) AS part2 FROM dual; -- osaka
SQL ServerのSTRING_SPLITは出力順が保証されないため、順番が必要なら第3引数1でordinal列を出してORDER BYします(互換性レベル130以上)。Oracleの'[^,]+'は空の要素(a,,b)を飛ばす点に注意します。
複数行を1つの文字列にまとめる(STRING_AGG・GROUP_CONCAT・LISTAGG)
-- 共通のサンプルデータ(記事ごとのタグ)
CREATE TABLE tags (article_id int, tag varchar(20));
INSERT INTO tags VALUES (1,'SQL'),(1,'DB'),(1,'入門'),(2,'Java'),(2,NULL),(3,'SQL'),(3,'SQL');
-- PostgreSQL 16
SELECT article_id, string_agg(tag, ',' ORDER BY tag) AS tags, string_agg(DISTINCT tag, ',') AS uniq
FROM tags GROUP BY article_id ORDER BY article_id;
-- MySQL 8.0
SELECT article_id, GROUP_CONCAT(tag ORDER BY tag SEPARATOR ',') AS tags, GROUP_CONCAT(DISTINCT tag) AS uniq
FROM tags GROUP BY article_id ORDER BY article_id;
-- 両DBとも同じ結果
article_id | tags | uniq
------------+-------------+-------------
1 | DB,SQL,入門 | DB,SQL,入門
2 | Java | Java
3 | SQL,SQL | SQL
NULLは両DBとも自動で除外されます。 MySQLのGROUP_CONCATは既定で1024バイトまでで、超えた分は警告付きで切り捨てられます。
-- MySQL 8.0:上限を小さくして切り捨てを再現
SET SESSION group_concat_max_len = 10;
SELECT GROUP_CONCAT(n) AS g FROM (SELECT 1 n UNION SELECT 22 UNION SELECT 333 UNION SELECT 4444) t;
SHOW WARNINGS;
+------------+
| g |
+------------+
| 1,22,333,4 |
+------------+
+---------+------+---------------------------------+
| Level | Code | Message |
+---------+------+---------------------------------+
| Warning | 1260 | Row 4 was cut by GROUP_CONCAT() |
+---------+------+---------------------------------+
長くなりそうならSET SESSION group_concat_max_len = 1000000;のように先に広げます。 OracleのLISTAGGは結果がVARCHAR2の上限を超えるとエラーになるため、ON OVERFLOW TRUNCATEを付けると切り詰めて返せます(ドキュメント準拠)。
-- Oracle 19c以降(ドキュメント準拠)
SELECT article_id, LISTAGG(DISTINCT tag, ',') WITHIN GROUP (ORDER BY tag) AS tags
FROM tags GROUP BY article_id;
-- SQL Server 2017以降(ドキュメント準拠)
SELECT article_id, STRING_AGG(tag, ',') WITHIN GROUP (ORDER BY tag) AS tags
FROM tags GROUP BY article_id;
正規表現で判定・抽出・置換する(REGEXP)
-- MySQL 8.0
SELECT REGEXP_SUBSTR('注文番号: ORD-2026-0042 です', '[A-Z]+-[0-9]{4}-[0-9]+') AS extract1,
REGEXP_REPLACE('user@example.com', '^[^@]+@', '') AS domain,
REGEXP_REPLACE('090-1234-5678', '[^0-9]', '') AS digits,
REGEXP_REPLACE('a1b2c3', '[0-9]', 'X', 1, 1) AS first_only,
'ORD-0042' REGEXP '^ORD-[0-9]+$' AS matches,
REGEXP_LIKE('abc', 'ABC') AS ci_default, REGEXP_LIKE('abc', 'ABC', 'c') AS cs_flag;
+---------------+-------------+-------------+------------+---------+------------+---------+
| extract1 | domain | digits | first_only | matches | ci_default | cs_flag |
+---------------+-------------+-------------+------------+---------+------------+---------+
| ORD-2026-0042 | example.com | 09012345678 | aXb2c3 | 1 | 1 | 0 |
+---------------+-------------+-------------+------------+---------+------------+---------+
-- PostgreSQL 16
SELECT substring('注文番号: ORD-2026-0042 です' FROM '[A-Z]+-[0-9]{4}-[0-9]+') AS extract1,
(regexp_match('user@example.com', '^([^@]+)@(.+)$'))[2] AS domain,
regexp_replace('090-1234-5678', '[^0-9]', '', 'g') AS digits,
regexp_replace('a1b2c3', '[0-9]', 'X') AS first_only,
'ORD-0042' ~ '^ORD-[0-9]+$' AS matches, 'abc' ~* 'ABC' AS ci_match,
regexp_count('a1b22c333', '[0-9]+') AS cnt;
extract1 | domain | digits | first_only | matches | ci_match | cnt
---------------+-------------+-------------+------------+---------+----------+-----
ORD-2026-0042 | example.com | 09012345678 | aXb2c3 | t | t | 3
| 違い | MySQL | PostgreSQL |
|---|---|---|
REGEXP_REPLACEの既定 |
すべて置換 | 最初の1つだけ(全置換は'g') |
| 大文字小文字 | 列の照合順序に従う(_ciなら無視) |
区別する(無視は~*や'i') |
| グループの取り出し | なし(REGEXP_SUBSTRで全体のみ) |
regexp_match(...)[n] |
PostgreSQLで'g'を付け忘れると最初の1か所しか置換されない点が、移行時によくつまずくところです。SQL Serverは2025からREGEXP_LIKEなどの正規表現関数が追加されました(REGEXP_LIKEは互換性レベル170以上が必要、ドキュメント準拠)。それ以前のバージョンではLIKEとPATINDEXのワイルドカードで代用します。
数値と文字列を変換する・書式を付ける
-- PostgreSQL 16
SELECT CAST(123 AS text) AS to_text, 123::text || '円' AS yen, CAST('0042' AS integer) AS to_int,
'3.14'::numeric AS to_num, to_char(1234567.891, 'FM9,999,999.00') AS fmt,
to_number('1,234,567', '9,999,999') AS tonum;
SELECT CAST('abc' AS integer);
to_text | yen | to_int | to_num | fmt | tonum
---------+-------+--------+--------+--------------+---------
123 | 123円 | 42 | 3.14 | 1,234,567.89 | 1234567
ERROR: invalid input syntax for type integer: "abc"
LINE 1: SELECT CAST('abc' AS integer);
-- MySQL 8.0
SELECT CAST('abc' AS SIGNED) AS bad, CAST('12abc' AS SIGNED) AS partial, '10' + 5 AS implicit;
SHOW WARNINGS;
+-----+---------+----------+
| bad | partial | implicit |
+-----+---------+----------+
| 0 | 12 | 15 |
+-----+---------+----------+
+---------+------+--------------------------------------------+
| Level | Code | Message |
+---------+------+--------------------------------------------+
| Warning | 1292 | Truncated incorrect INTEGER value: 'abc' |
| Warning | 1292 | Truncated incorrect INTEGER value: '12abc' |
+---------+------+--------------------------------------------+
変換できない文字列に対して、PostgreSQLはエラー、MySQLはSELECTでは警告付きで0や先頭の数字を返します。 ただしMySQLでも、厳格モード(既定のSTRICT_TRANS_TABLES)でINSERTするとERROR 1292 (22007): Truncated incorrect INTEGER value: 'abc'になりました。
| DB | 変換できないときにNULLを返す書き方 |
|---|---|
| MySQL | CASE WHEN s REGEXP '^-?[0-9]+$' THEN CAST(s AS SIGNED) END |
| PostgreSQL | 同様にCASE WHEN s ~ '^-?[0-9]+$' THEN s::integer END |
| Oracle(ドキュメント準拠) | TO_NUMBER(s DEFAULT NULL ON CONVERSION ERROR)(12c R2以降) |
| SQL Server(ドキュメント準拠) | TRY_CAST(s AS int) / TRY_CONVERT |
数値の3桁カンマは、MySQLがFORMAT(1234567.891, 2)(結果1,234,567.89)、PostgreSQLとOracleがto_char、SQL ServerがFORMAT(n, 'N2')です。
空文字('')とNULLの違い(Oracleは同じ扱い)
-- PostgreSQL 16 / MySQL 8.0 で同じ結果
SELECT '' IS NULL AS empty_is_null, length('') AS len_empty,
COALESCE(NULLIF('', ''), '(空)') AS nullif_empty;
empty_is_null | len_empty | nullif_empty
---------------+-----------+--------------
f | 0 | (空)
MySQL・PostgreSQL・SQL Serverでは空文字とNULLは別物ですが、Oracleでは長さ0の文字列はNULLとして扱われます(ドキュメント準拠)。そのためOracleでは次のようになります。
| 書き方(Oracle、ドキュメント準拠) | 結果 |
|---|---|
WHERE col = '' |
どの行にも一致しない(NULL = NULLは不明) |
WHERE col IS NULL |
空文字で登録した行も一致する |
LENGTH('') |
NULL(0ではない) |
'A' || NULL || 'B' |
AB |
4DBで同じ意味にしたいなら、「値なし」はNULLに統一し、NULLIF(TRIM(col), '')で空文字や空白だけの値をNULLにそろえると扱いやすくなります。
LIKEで % や _ を文字として検索する(エスケープ)
-- 共通のサンプル
CREATE TABLE items (name varchar(20));
INSERT INTO items VALUES ('100%オレンジ'),('1000円'),('file_a'),('fileXa');
SELECT name FROM items WHERE name LIKE '100%'; -- ① % がワイルドカード
SELECT name FROM items WHERE name LIKE '100\%%'; -- ② \ でエスケープ
SELECT name FROM items WHERE name LIKE 'file!_a' ESCAPE '!'; -- ③ ESCAPE で指定
-- MySQL 8.0 / PostgreSQL 16 で同じ結果
① 100%オレンジ, 1000円
② 100%オレンジ
③ file_a
MySQLとPostgreSQLは\が既定のエスケープ文字ですが、OracleとSQL Serverには既定のエスケープ文字がありません。 どのDBでも動かすなら、③のようにESCAPEを明示します。SQL ServerはLIKE '100[%]%'のように角かっこで囲む書き方もできます(ドキュメント準拠)。
アプリケーションから検索語を渡すときは、% _ とエスケープ文字自体をエスケープしてから、プレースホルダーで渡します。
大文字小文字・濁点・全角半角を区別するか(照合順序)
文字の比較結果は、関数ではなく照合順序(collation)で決まります。
-- MySQL 8.0(utf8mb4_0900_ai_ci)
SELECT 'abc' = 'ABC' AS eq, 'abc' = 'ABC' COLLATE utf8mb4_0900_as_cs AS eq_cs,
BINARY 'abc' = 'ABC' AS eq_bin, 'ハ' = 'パ' AS dakuten, 'ア' = 'ア' AS hankaku,
'a ' = 'a' AS trailing_sp;
+----+-------+--------+---------+---------+-------------+
| eq | eq_cs | eq_bin | dakuten | hankaku | trailing_sp |
+----+-------+--------+---------+---------+-------------+
| 1 | 0 | 0 | 1 | 1 | 0 |
+----+-------+--------+---------+---------+-------------+
MySQL 8.0の既定utf8mb4_0900_ai_ciは、大文字小文字(ci)だけでなくアクセント(ai)も区別しないため、「ハ」と「パ」、半角「ア」と全角「ア」も等しくなりました。一方、末尾の空白は区別します(NO PAD)。会員IDやコードのように厳密に比べたい列は、utf8mb4_binやutf8mb4_0900_as_csを列に指定しておきます。
-- PostgreSQL 16:既定は区別する。区別しない比較が必要なら ICU の非決定的照合順序を使う
SELECT 'abc' = 'ABC' AS eq, 'abc' ILIKE 'ABC' AS ilike, lower('abc') = lower('ABC') AS lower_eq;
-- eq=f, ilike=t, lower_eq=t
CREATE COLLATION IF NOT EXISTS ci (provider = icu, locale = 'und-u-ks-level2', deterministic = false);
CREATE TABLE members (email text COLLATE ci);
INSERT INTO members VALUES ('Taro@Example.com');
SELECT * FROM members WHERE email = 'taro@example.com'; -- Taro@Example.com が一致
SELECT * FROM members WHERE email LIKE 'taro%';
email
------------------
Taro@Example.com
ERROR: nondeterministic collations are not supported for LIKE
PostgreSQL 16では、非決定的照合順序の列にLIKEを使うとエラーになります(PostgreSQL 18のリリースノートでLIKEに対応)。16で前方一致も行うなら、lower(email)の式インデックスとlower(email) LIKE 'taro%'の組み合わせが扱いやすいです。
| DB | 既定の動き | 区別したい/区別したくないとき |
|---|---|---|
| MySQL | 区別しない(_ci) |
列にutf8mb4_binや_as_csを指定 |
| PostgreSQL | 区別する | ILIKE、lower()、ICU非決定的照合順序、citext拡張 |
| Oracle(ドキュメント準拠) | 区別する | UPPER()で比較、またはNLS_COMP=LINGUISTICとNLS_SORT |
| SQL Server(ドキュメント準拠) | 照合順序次第(日本語環境はJapanese_CI_ASが一般的で区別しない) |
COLLATE Japanese_CS_ASなどを指定 |
よくある失敗と直し方
| よくある失敗 | 起きること | 直し方 |
|---|---|---|
MySQLで'a' || 'b' |
論理和で0になる |
CONCATを使う、またはPIPES_AS_CONCAT |
MySQLのCONCATにNULL列 |
全体がNULL | CONCAT_WSかIFNULL(col, '') |
MySQLのLENGTHで文字数チェック |
日本語で3倍の値 | CHAR_LENGTH |
PostgreSQLのregexp_replace |
1か所しか置換されない | 第4引数に'g' |
TRIMで全角空白 |
残る | 消す文字を明示、または正規表現 |
GROUP_CONCATで長いリスト |
1024バイトで切れる | group_concat_max_lenを広げる |
Oracleでcol = '' |
1件も返らない | col IS NULL |
LPADで長い値 |
黙って切り詰められる | 事前に文字数を確認 |
| MySQLで「ハ」で検索 | 「パ」も一致する | 列の照合順序を_as_csや_binに |
| 列に関数をかけて検索 | インデックスが使われにくい | 式インデックス(PostgreSQL)や生成列+インデックス(MySQL) |
よくある質問
Q. 4DBすべてで使える文字列関数はどれですか?
A. UPPER、LOWER、REPLACE、TRIM(SQL Serverは2017以降)、SUBSTRING/SUBSTR(名前は2種類)が共通して使えます。連結はCONCAT(a, b)の2引数なら4DBで動きますが、NULLの扱いが違うため、COALESCEで空文字にしてから渡すと結果がそろいます。
Q. 文字数を数えるのはLENGTHとCHAR_LENGTHのどちらですか?
A. 文字数ならMySQLはCHAR_LENGTH、PostgreSQLはlengthかchar_length、OracleはLENGTH、SQL ServerはLENです。MySQLのLENGTHだけはバイト数で、UTF-8の日本語は1文字3バイト(絵文字などは4バイト)になります。
Q. カンマ区切りの文字列を行に分けるには?
A. PostgreSQLはunnest(string_to_array(s, ','))、SQL ServerはSTRING_SPLIT、MySQL 8.0はJSON_TABLEか再帰CTE、OracleはREGEXP_SUBSTRとCONNECT BYの組み合わせです。頻繁に分割するなら、テーブル設計を1行1値に正規化することも検討します。
Q. 正規表現関数はどのバージョンから使えますか?
A. MySQLは8.0からREGEXP_LIKE・REGEXP_REPLACE・REGEXP_SUBSTR・REGEXP_INSTR、PostgreSQLは古くから~演算子とregexp_replace(regexp_countなどは15以降)、Oracleは10g以降、SQL Serverは2025からです。
まとめ
- 連結のNULLはDBで逆:MySQLの
CONCATはNULLになり、PostgreSQLのCONCATは無視する。COALESCEで先にそろえる - MySQLの
LENGTHはバイト数。日本語の文字数はCHAR_LENGTH、PostgreSQLはlength - Oracleでは空文字がNULL。「値なし」はNULLに統一し、
NULLIF(TRIM(col), '')でそろえる - 分割は
split_part・SUBSTRING_INDEX・STRING_SPLIT、集約はstring_agg・GROUP_CONCAT・LISTAGG・STRING_AGG - PostgreSQLの
regexp_replaceは'g'で全置換。MySQLは既定で全置換 - 大文字小文字や濁点の区別は照合順序で決まる。MySQLの既定
_0900_ai_ciは「ハ」と「パ」も等しい - LIKEで
%_を探すならESCAPEを明示すると4DBで同じ動きになる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「SQL 文字列関数 一覧|4DB比較表とコピペSQL・実行結果つき」
株式会社RJCでは、DBごとの小さな違いを実際に動かして確かめ、レビューやドキュメントで共有しています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。