2026年8月19日
2026年8月19日
MySQLの文字列関数の使い方(CONCAT・SUBSTRING・REPLACEなど) 日本語の扱いとWordPressでの実例
MySQL の文字列関数を使うと、SQL の中で文字列の連結(CONCAT)、切り出し(SUBSTRING / SUBSTR)、置換(REPLACE)、長さの取得(CHAR_LENGTH)などができます。日本語を扱う場合は「バイト数と文字数の違い」と「NULL の扱い」を押さえておくと、思わぬ結果を避けられます。この記事では、よく使う関数を用途別に整理し、WordPress のテーブルで使える実例と、PHP から使うときの安全な書き方、インデックスとの関係までを説明します。
例は MySQL 8.0 以降を想定しています。MariaDB でもほとんどの関数は同じように使えますが、一部の関数の有無や既定値が異なります(2026年10月現在)。
連結する:CONCATとCONCAT_WS
SELECT CONCAT('WordPress', ' ', '博士'); -- 'WordPress 博士'
SELECT CONCAT('ID:', 123); -- 'ID:123'(数値は文字列に変換)
SELECT CONCAT('a', NULL, 'b'); -- NULL
SELECT CONCAT_WS(', ', '東京', NULL, '大阪'); -- '東京, 大阪'
CONCAT は引数に1つでも NULL があると結果全体が NULL になります。列を連結するときに、値の入っていない行だけ結果が消えるのはこのためです。CONCAT_WS は最初の引数を区切り文字として使い、NULL の引数は飛ばします。空文字列('')は飛ばされないため、区切り文字が続くことがあります。NULL を別の値に置き換えたい場合は IFNULL(列, '') や COALESCE と組み合わせます。
ほかのデータベースで連結に使う || は、MySQL の既定の設定では論理演算子の OR として扱われます。文字列の連結には CONCAT を使ってください。
切り出す:SUBSTRING・LEFT・RIGHT・SUBSTRING_INDEX
SELECT SUBSTRING('ワードプレス博士', 1, 5); -- 'ワードプレ'
SELECT SUBSTRING('ワードプレス博士', 7); -- '博士'
SELECT SUBSTRING('ワードプレス博士', -2); -- '博士'(末尾から2文字)
SELECT LEFT('2026-10-18', 4); -- '2026'
SELECT RIGHT('image.jpeg', 4); -- 'jpeg'
SELECT SUBSTRING_INDEX('a.b.c.d', '.', 2); -- 'a.b'
SELECT SUBSTRING_INDEX('a.b.c.d', '.', -1); -- 'd'
SUBSTRING(文字列, 開始位置, 長さ) の開始位置は1から数えます。0を指定すると空文字列になる点が、0始まりのプログラミング言語と異なります。SUBSTR と MID は SUBSTRING の別名です。位置と長さは文字単位なので、utf8mb4 の列であれば日本語でも文字の途中で切れることはありません。
SUBSTRING_INDEX は区切り文字の何番目までを取り出すかを指定する関数で、正の数なら先頭から、負の数なら末尾から数えます。メールアドレスのドメイン部分やファイルの拡張子を取り出すときに便利です。
長さを調べる:CHAR_LENGTHとLENGTH
SELECT CHAR_LENGTH('博士'), LENGTH('博士'); -- 2, 6
SELECT CHAR_LENGTH('😀'), LENGTH('😀'); -- 1, 4
LENGTH はバイト数、CHAR_LENGTH は文字数を返します。utf8mb4 では日本語の多くの文字が3バイト、絵文字は4バイトです。「タイトルが何文字か」を知りたいときは CHAR_LENGTH を使います。LENGTH で文字数を判定すると、日本語の記事だけ約3倍の長さとして扱われてしまいます。
置換する:REPLACEとREGEXP_REPLACE
SELECT REPLACE('http://example.com/a', 'http://', 'https://');
-- 'https://example.com/a'
SELECT REGEXP_REPLACE('電話: 03-1234-5678', '[0-9]', '*');
-- '電話: **-****-****'
REPLACE は指定した文字列をすべて置き換えます。Collation の設定に関係なく大文字と小文字を区別して比較する点に注意してください。正規表現で置き換えたい場合は REGEXP_REPLACE を使います。MySQL では 8.0 から使える関数で、それより前の版にはありません。
余白を取る・大文字小文字・検索・桁埋め
SELECT TRIM(' WordPress '); -- 'WordPress'
SELECT TRIM(BOTH ' ' FROM ' 博士 '); -- '博士'(全角スペースを除く)
SELECT UPPER('wp'), LOWER('WP'); -- 'WP', 'wp'
SELECT LOCATE('プレス', 'ワードプレス'); -- 4
SELECT INSTR('ワードプレス', 'プレス'); -- 4
SELECT LPAD('7', 3, '0'); -- '007'
TRIM が既定で取り除くのは半角スペースだけです。全角スペースやタブ、改行は残るため、取り除きたい文字を明示します。LOCATE と INSTR はどちらも位置を返しますが、引数の順番が逆です。見つからなければ0を返します。LPAD は指定した長さより元の文字列が長いと、その長さで切り詰める点に注意してください。
複数行をまとめる:GROUP_CONCAT
SELECT p.ID, p.post_title,
GROUP_CONCAT(t.name ORDER BY t.name SEPARATOR ', ') AS tags
FROM wp_posts p
JOIN wp_term_relationships tr ON tr.object_id = p.ID
JOIN wp_term_taxonomy tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
JOIN wp_terms t ON t.term_id = tt.term_id
WHERE p.post_type = 'post' AND p.post_status = 'publish'
AND tt.taxonomy = 'post_tag'
GROUP BY p.ID, p.post_title;
投稿ごとにタグを1行にまとめる例です。GROUP_CONCAT の結果は group_concat_max_len の長さで切り詰められ、MySQL の既定値は1024バイトと小さめです。結果が途中で切れていたら、この値を確認します。MariaDB は既定値が異なります。
WordPressのテーブルで使う実例
接頭辞が wp_ 以外のサイトでは、テーブル名を読み替えてください。読み取りの SELECT は安全ですが、本番のデータベースに負荷をかけないよう、大きなサイトでは LIMIT を付けるか、ステージング環境で試します。
-- タイトルが長い公開記事を文字数順に20件
SELECT ID, post_title, CHAR_LENGTH(post_title) AS title_len
FROM wp_posts
WHERE post_type = 'post' AND post_status = 'publish'
ORDER BY title_len DESC
LIMIT 20;
-- 登録ユーザーのメールドメイン別の人数
SELECT SUBSTRING_INDEX(user_email, '@', -1) AS domain, COUNT(*) AS users
FROM wp_users
GROUP BY domain
ORDER BY users DESC;
ユーザーのメールアドレスは個人情報です。集計結果を共有する場合も、個々のアドレスが分かる形で書き出さないようにします。
UPDATEでREPLACEを使うときの注意
UPDATE wp_posts SET post_content = REPLACE(post_content, '旧', '新') のように本文を一括置換することはできますが、取り消しはバックアップからの復元しかありません。実行前にバックアップを取り、同じ条件の SELECT で対象件数を確かめてください。また、wp_options や wp_postmeta にはシリアライズされた値(文字数を含む形式)が保存されており、REPLACE で文字数が変わると値が壊れます。URL の置き換えなどは、シリアライズを考慮する WP-CLI の wp search-replace を使うほうが安全です。
PHPから使うときは$wpdb->prepareで
検索語のように外部から受け取った値を SQL に入れるときは、文字列を直接埋め込まず $wpdb->prepare() を使います。LIKE に使う値は $wpdb->esc_like() で % と _ をエスケープしてから渡します。
global $wpdb;
$keyword = isset( $_GET['q'] ) ? sanitize_text_field( wp_unslash( $_GET['q'] ) ) : '';
if ( '' !== $keyword ) {
$like = '%' . $wpdb->esc_like( $keyword ) . '%';
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID, post_title FROM {$wpdb->posts}
WHERE post_status = %s AND post_title LIKE %s
ORDER BY post_date DESC LIMIT 20",
'publish',
$like
)
);
foreach ( $rows as $row ) {
echo '<li>' . esc_html( $row->post_title ) . '</li>';
}
}
出力時は esc_html() でエスケープします。SQL の中で CONCAT('%', %s, '%') と書いて連結する方法もありますが、その場合も値は esc_like() を通してから渡します。
文字列関数とインデックス
WHERE LEFT(post_name, 4) = 'test' のように、列に関数をかけて比較すると、その列のインデックスが使われず全行を調べることになります。同じ条件は WHERE post_name LIKE 'test%' と書けば、前方一致としてインデックスを使える場合があります。LIKE の _ は「任意の1文字」を表すため、_wp_ で始まる値を探すなら LIKE '\_wp\_%' のようにエスケープします。
MySQL 8.0 の新しい版では、関数の結果に対するインデックス(関数インデックス)も作れますが、WordPress のコアテーブルの構造を変えるのは避け、まずはクエリの書き方で対処してください。実行計画は EXPLAIN で確認できます。
まとめ
CONCATは NULL が1つでもあると NULL になり、CONCAT_WSは NULL を飛ばします。MySQL の||は既定では連結になりませんSUBSTRINGの位置は1から数え、SUBSTRとMIDは別名です。文字数はCHAR_LENGTH、バイト数はLENGTHで調べますREPLACEは大文字小文字を区別し、正規表現の置換は MySQL 8.0 以降のREGEXP_REPLACEを使います。TRIMは既定で全角スペースを取りません- WordPress の本文やオプションを一括置換するときは、バックアップとシリアライズへの配慮が必要で、URL の置換は
wp search-replaceが安全です - PHP からは
$wpdb->prepare()と$wpdb->esc_like()を使い、出力はエスケープします。列に関数をかけた条件はインデックスが効きにくくなります