SQLで文字列の一部を取り出す操作は、メールアドレスからドメインを抽出する・コードの先頭桁を確認する・個人情報をマスキングするなど、実務で頻繁に発生します。そのための主要関数が SUBSTR / SUBSTRING です。
関数名・構文・負の開始位置の扱いがDBMSによって異なるため、移植性を意識した書き方が必要です。本記事では SUBSTR / SUBSTRING の全構文・LEFT / RIGHT との使い分け・INSTR / POSITION / CHARINDEX との組み合わせ・実務パターン集まで網羅します。
基本構文と引数
SUBSTR / SUBSTRING は3つの引数を取ります(3つ目は省略可能)。位置は1始まり(先頭文字が1)で、ほぼすべてのDBMSで共通です。
-- 共通構文(SUBSTR / SUBSTRING どちらも同じ引数) SUBSTR(文字列, 開始位置) SUBSTR(文字列, 開始位置, 取り出す文字数) SUBSTRING(文字列, 開始位置) SUBSTRING(文字列, 開始位置, 取り出す文字数) -- SQL標準構文(FROM / FOR キーワード形式) SUBSTRING(文字列 FROM 開始位置) SUBSTRING(文字列 FROM 開始位置 FOR 取り出す文字数)
| 引数 | 説明 | 省略時 |
|---|---|---|
| 文字列 | 切り出し元の文字列またはカラム名 | —(必須) |
| 開始位置 | 切り出し開始の文字位置。先頭は 1(0を指定すると1と同じ扱い※DB依存) | —(必須) |
| 取り出す文字数 | 何文字分取り出すか。省略すると末尾まですべて取り出す | 末尾まで取り出す |
-- 1文字目から5文字取り出す
SELECT SUBSTR('Hello, World!', 1, 5); -- 'Hello'
SELECT SUBSTRING('Hello, World!', 1, 5); -- 'Hello'
-- 8文字目から末尾まで
SELECT SUBSTR('Hello, World!', 8); -- 'World!'
-- カラムに適用
SELECT
customer_code,
SUBSTR(customer_code, 1, 2) AS prefix, -- 先頭2文字(地域コード等)
SUBSTR(customer_code, 3) AS suffix -- 3文字目以降
FROM customers;
DBMS別の関数名と対応状況
| DBMS | SUBSTR | SUBSTRING | SUBSTRING FROM/FOR | LEFT / RIGHT |
|---|---|---|---|---|
| MySQL | ○(SUBSTRINGの別名) | ○ | ○ | ○ |
| PostgreSQL | ○ | ○ | ○(SQL標準) | ○ |
| Oracle | ○ | ×(使えない) | × | ×(SUBSTR代用) |
| SQL Server | ×(使えない) | ○ | × | ○ |
SUBSTRING が使えず SUBSTR のみ対応。SQL Server は SUBSTR が使えず SUBSTRING のみ対応。全DBMS対応が必要な場合は SUBSTR(Oracle共通)を使い、SQL Serverは別途対応が必要です。-- MySQL / PostgreSQL: SUBSTR と SUBSTRING は同じ
SELECT SUBSTR('Hello', 1, 3); -- 'Hel'
SELECT SUBSTRING('Hello', 1, 3); -- 'Hel'
SELECT SUBSTRING('Hello' FROM 1 FOR 3); -- 'Hel'(SQL標準形式)
-- Oracle: SUBSTR のみ(SUBSTRING は存在しない)
SELECT SUBSTR('Hello', 1, 3) FROM dual; -- 'Hel'
-- SQL Server: SUBSTRING のみ(SUBSTR は存在しない)
SELECT SUBSTRING('Hello', 1, 3); -- 'Hel'
開始位置の詳細:1始まり・0指定・負の値
1始まりと0を指定した場合
-- 標準: 1が先頭文字
SELECT SUBSTR('ABCDE', 1, 3); -- 'ABC'(先頭から3文字)
SELECT SUBSTR('ABCDE', 2, 3); -- 'BCD'(2文字目から3文字)
SELECT SUBSTR('ABCDE', 3, 3); -- 'CDE'(3文字目から3文字)
-- 0を指定した場合(DBMSによって動作が異なる)
SELECT SUBSTR('ABCDE', 0, 3);
-- MySQL: 'ABC'(0は1と同じ扱い)
-- Oracle: 'AB'(0を先頭の1つ前と解釈、取り出し文字数が1減る)
-- PostgreSQL: 'AB'(同様に1手前として扱う)
-- SQL Server: SUBSTRING('ABCDE', 0, 3) → 'AB'(同様)
負の開始位置(Oracle独自機能)
-- Oracle のみ: 負の開始位置 → 末尾から数えて開始
SELECT SUBSTR('Hello, World!', -6) FROM dual; -- 'World!'(末尾6文字)
SELECT SUBSTR('Hello, World!', -6, 5) FROM dual; -- 'World'(末尾6文字目から5文字)
-- 末尾3文字を取得(Oracle)
SELECT SUBSTR(product_code, -3) FROM products;
-- MySQL / PostgreSQL / SQL Server では負の開始位置は使えない
-- → 代替: SUBSTR(str, LENGTH(str) - N + 1, N) または RIGHT(str, N)
長さを省略した場合
-- 長さを省略すると指定位置から末尾まで取り出す(全DB共通)
SELECT SUBSTR('Hello, World!', 8); -- 'World!'
SELECT SUBSTRING('Hello, World!', 8); -- 'World!'
SELECT SUBSTRING('Hello, World!' FROM 8); -- 'World!'(SQL標準)
-- カラムの先頭3文字を除いた残り全部
SELECT SUBSTR(description, 4) FROM articles; -- 4文字目以降
LEFT / RIGHT:先頭・末尾N文字を取り出す短縮形
先頭または末尾から固定文字数を取り出す場合は LEFT / RIGHT がシンプルです。MySQL・PostgreSQL・SQL Server で使えますが、Oracle には存在しません。
-- LEFT: 先頭からN文字
SELECT LEFT('Hello, World!', 5); -- 'Hello' (MySQL / PostgreSQL / SQL Server)
SELECT LEFT(product_code, 2); -- 先頭2文字(カテゴリコード等)
-- RIGHT: 末尾からN文字
SELECT RIGHT('Hello, World!', 6); -- 'World!'
SELECT RIGHT(phone, 4); -- 電話番号の下4桁
-- Oracle での代替(LEFT / RIGHT がないため SUBSTR で代用)
SELECT SUBSTR('Hello, World!', 1, 5) FROM dual; -- LEFT相当
SELECT SUBSTR('Hello, World!', -6) FROM dual; -- RIGHT相当(負位置)
SELECT SUBSTR('Hello, World!', LENGTH('Hello, World!') - 5) FROM dual; -- RIGHT相当(全DB移植可)
| 操作 | MySQL / PostgreSQL / SQL Server | Oracle |
|---|---|---|
| 先頭N文字 | LEFT(str, N) |
SUBSTR(str, 1, N) |
| 末尾N文字 | RIGHT(str, N) |
SUBSTR(str, -N) または SUBSTR(str, LENGTH(str)-N+1) |
| M文字目からN文字 | SUBSTR(str, M, N) |
SUBSTR(str, M, N) |
位置検索関数と組み合わせて動的に切り出す
「@より前のユーザー名を取り出す」「最後のスラッシュ以降のファイル名だけ取り出す」など、区切り文字の位置を動的に取得して切り出すパターンでは、SUBSTR と位置検索関数を組み合わせます。
| DBMS | 文字列内の位置を返す関数 | 見つからない場合の戻り値 |
|---|---|---|
| MySQL | INSTR(str, search) / LOCATE(search, str) / POSITION(search IN str) |
0 |
| PostgreSQL | STRPOS(str, search) / POSITION(search IN str) |
0 |
| Oracle | INSTR(str, search [, start [, n]]) |
0 |
| SQL Server | CHARINDEX(search, str [, start]) |
0 |
-- MySQL / Oracle
SELECT
email,
SUBSTR(email, 1, INSTR(email, '@') - 1) AS username, -- @より前
SUBSTR(email, INSTR(email, '@') + 1) AS domain -- @より後
FROM users;
-- 'yamada@example.com' → username='yamada', domain='example.com'
-- PostgreSQL
SELECT
email,
SUBSTR(email, 1, STRPOS(email, '@') - 1) AS username,
SUBSTR(email, STRPOS(email, '@') + 1) AS domain
FROM users;
-- SQL Server
SELECT
email,
SUBSTRING(email, 1, CHARINDEX('@', email) - 1) AS username,
SUBSTRING(email, CHARINDEX('@', email) + 1, LEN(email)) AS domain
FROM users;
-- Oracle: INSTR で最後の / の位置を取得
SELECT
url,
SUBSTR(url, INSTR(url, '/', -1) + 1) AS filename
FROM pages;
-- 'https://example.com/images/photo.jpg' → 'photo.jpg'
-- INSTR(str, '/', -1) は末尾から検索して最初に見つかった位置を返す
-- MySQL: 最後の / の位置を取得(末尾検索は LENGTH + LOCATE の組み合わせ)
SELECT
url,
SUBSTR(url, LENGTH(url) - LOCATE('/', REVERSE(url)) + 2) AS filename
FROM pages;
-- PostgreSQL
SELECT
url,
SUBSTR(url, LENGTH(url) - STRPOS(REVERSE(url), '/') + 2) AS filename
FROM pages;
-- アンダースコアで「カテゴリ_連番」形式のコードを分割
-- 例: 'SQL_001' → カテゴリ='SQL', 連番='001'
-- MySQL / Oracle
SELECT
code,
SUBSTR(code, 1, INSTR(code, '_') - 1) AS category,
SUBSTR(code, INSTR(code, '_') + 1) AS seq_no
FROM products;
-- PostgreSQL(SPLIT_PART が使えるならより簡潔)
SELECT
code,
SPLIT_PART(code, '_', 1) AS category, -- PostgreSQL独自
SPLIT_PART(code, '_', 2) AS seq_no
FROM products;
SPLIT_PART(文字列, 区切り文字, N) は区切り文字で分割したN番目の要素を返します。SUBSTR + STRPOSのネストより格段にシンプルで、CSV形式や _ 区切りのコード分解に便利です。実務でよく使うパターン集
郵便番号の分割(ハイフン前後)
-- '123-4567' → '123' と '4567' に分割
SELECT
zip_code,
SUBSTR(zip_code, 1, 3) AS zip_prefix, -- ハイフン前(固定3桁)
SUBSTR(zip_code, 5, 4) AS zip_suffix -- ハイフン後(固定4桁)
FROM addresses;
-- ハイフンなし形式に変換
SELECT
SUBSTR(zip_code, 1, 3) || '-' || SUBSTR(zip_code, 4) -- ハイフンなしに付与
FROM addresses
WHERE zip_code NOT LIKE '%-%';
電話番号のマスキング(下4桁を****に)
-- 電話番号の下4桁を **** に(全DB共通)
SELECT
phone,
SUBSTR(phone, 1, LENGTH(phone) - 4) || '****' AS masked_phone
FROM customers;
-- '09012345678' → '0901234****'
-- 上4桁だけ残して残りをマスク
SELECT
name,
LEFT(name, 1) || REPEAT('*', LENGTH(name) - 1) AS masked_name -- MySQL/PostgreSQL
FROM customers;
-- '田中太郎' → '田***'
-- SQL Server
SELECT
name,
SUBSTRING(name, 1, 1) + REPLICATE('*', LEN(name) - 1) AS masked_name
FROM customers;
日付文字列から年・月・日を抽出
-- '20240315' → year='2024', month='03', day='15'
SELECT
date_str,
SUBSTR(date_str, 1, 4) AS year_part,
SUBSTR(date_str, 5, 2) AS month_part,
SUBSTR(date_str, 7, 2) AS day_part,
-- ハイフン区切りに変換
SUBSTR(date_str, 1, 4) || '-' || SUBSTR(date_str, 5, 2) || '-' || SUBSTR(date_str, 7, 2)
AS formatted_date
FROM date_strings;
-- 'YYYY/MM/DD' 形式から月だけ取り出す
SELECT SUBSTR(date_str, 6, 2) AS month_part -- 6文字目から2文字(MM部分)
FROM date_strings;
固定幅フィールドのパース
レガシーシステムからのデータや、バッチ連携ファイルには固定幅で複数フィールドを詰めた形式が存在します。SUBSTRで各フィールドを切り出せます。
-- 固定幅データ例(30文字):
-- 位置 1- 4: 年度(4文字)
-- 位置 5- 6: 月(2文字)
-- 位置 7-16: 顧客コード(10文字、右詰めスペース)
-- 位置 17-30: 金額(14文字、右詰めゼロ埋め)
SELECT
raw_line,
TRIM(SUBSTR(raw_line, 1, 4)) AS fiscal_year,
TRIM(SUBSTR(raw_line, 5, 2)) AS month,
TRIM(SUBSTR(raw_line, 7, 10)) AS customer_code,
CAST(TRIM(SUBSTR(raw_line, 17, 14)) AS DECIMAL(14,2)) AS amount
FROM fixed_width_import;
コードの先頭でテーブルを振り分ける(CASE WHEN + SUBSTR)
-- コードの先頭2文字でカテゴリを判定してラベル付け
SELECT
product_code,
CASE SUBSTR(product_code, 1, 2)
WHEN 'PC' THEN 'パソコン'
WHEN 'MO' THEN 'モバイル'
WHEN 'AC' THEN 'アクセサリ'
ELSE 'その他'
END AS category_name
FROM products;
-- 先頭1文字で国コード判定(MySQL)
SELECT
order_no,
LEFT(order_no, 1) AS country_code,
SUBSTR(order_no, 2) AS local_order_no
FROM orders;
SUBSTR + UPDATE でデータを修正する
-- 先頭に誤って追加された接頭辞「XX」を除去 -- 'XXABC001' → 'ABC001' UPDATE products SET product_code = SUBSTR(product_code, 3) WHERE product_code LIKE 'XX%'; -- 確認(UPDATE前に SELECT で確認) SELECT product_code, SUBSTR(product_code, 3) AS new_code FROM products WHERE product_code LIKE 'XX%';
REGEXP_SUBSTRとの使い分け(PostgreSQL・Oracle・MySQL 8.0+)
抽出パターンが固定位置・固定文字数なら SUBSTR、正規表現でマッチした部分を取り出したい場合は REGEXP_SUBSTR を使います。
-- 文字列から数字部分だけを抽出(位置が不定)
-- Oracle
SELECT REGEXP_SUBSTR('ABC123DEF456', '[0-9]+') FROM dual; -- '123'(最初の数字列)
-- PostgreSQL(regexp_match)
SELECT (REGEXP_MATCH('ABC123DEF456', '[0-9]+'))[1]; -- '123'
-- MySQL 8.0+
SELECT REGEXP_SUBSTR('ABC123DEF456', '[0-9]+'); -- '123'
-- メールのドメイン部分を正規表現で抽出
SELECT REGEXP_SUBSTR(email, '@(.+)', 1, 1, 'e', 1) -- Oracle
FROM users;
よくある質問
SUBSTRに統一し、SQL Server専用クエリではSUBSTRINGを使います。'' が返ります(エラーにはなりません)。例: SUBSTR('Hello', 10) → ''。ただしSQL ServerのSUBSTRINGは開始位置が1未満でも動作し(1とみなす)、長さが負だとエラーになります。SUBSTR(str, -N)(負の開始位置)が使えます。MySQL・PostgreSQL・SQL ServerはRIGHT(str, N)またはSUBSTR(str, LENGTH(str) - N + 1, N)を使います。全DB共通の書き方はSUBSTR(str, LENGTH(str) - N + 1)です(Oracleは-N推奨)。SUBSTRはデフォルトで文字数単位で動作します(UTF-8でも1文字=1としてカウント)。OracleはNLS_CHARACTERSETによって動作が変わる場合があります。バイト単位で切り出したい場合はSUBSTRB(Oracle)やSUBSTRING(バイトモード)を明示的に使ってください。通常の文字列操作では文字数単位のSUBSTRで問題ありません。NULLIF(SUBSTR(str, pos, len), '')を使うと、結果が空文字のときにNULLに変換できます。またCASE WHEN SUBSTR(str, pos, len) = '' THEN NULL ELSE SUBSTR(str, pos, len) ENDでも同様の結果が得られます。まとめ
SUBSTR / SUBSTRING は文字列操作の基本中の基本ですが、DBMS間の違いを把握しておくことが重要です。
- 関数名: Oracle は SUBSTR のみ・SQL Server は SUBSTRING のみ・MySQL/PostgreSQL は両方対応
- 開始位置は1始まり: 0の指定はDBMSによって動作が異なるため使わない
- 長さ省略 = 末尾まで: 第3引数を省略すると指定位置から末尾まで取り出す
- 負の開始位置は Oracle のみ: 末尾からN文字は MySQL/PostgreSQL/SQL Server では RIGHT() を使う
- 動的な位置切り出し: INSTR / STRPOS / CHARINDEX と組み合わせて区切り文字での分割に対応
- 実務応用: メール分解・郵便番号分割・マスキング・固定幅パース・コード先頭での振り分け
関連記事:LENGTH関数で文字列の長さを取得する方法、REPLACE関数完全ガイド、文字列結合完全ガイド(CONCAT・GROUP_CONCAT)、TRIM関数完全ガイド