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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?