はじめに
SQLは共通言語のように見えますが、データベース製品によって書き方や動作が異なります。
例えば、検索結果を先頭10件だけ取得する処理でも、Oracle、PostgreSQL、MySQL、SQL Serverでは構文が違います。また、構文は通るのに検索結果だけが変わるという違いもあります。
この記事では、主要な4つのリレーショナルデータベース(RDBMS)を比較し、実務で遭遇しやすい7つの違いを整理します。
- Oracle Database:業務システムでよく利用されるRDBMS
- PostgreSQL:オープンソースのRDBMS
- MySQL:Webアプリケーションなどで広く利用されるRDBMS
- SQL Server:Microsoft製のRDBMS。SQL方言はT-SQL
対象読者は、SQLのSELECT・INSERT・UPDATEをある程度読める方、DB移行や別製品での開発を担当する方です。
検証対象・前提:Oracle Database 19c、PostgreSQL 18、MySQL 8.4、SQL Server 2025の公式ドキュメントを参照しています。記載例は構文の比較を目的としており、DBの設定、照合順序、列の型、バージョンによって動作が変わる場合があります。記事内のSQLはすべて同一環境で実行できるものではありません。
まずは一覧で比較
| 処理 | Oracle | PostgreSQL | MySQL | SQL Server |
|---|---|---|---|---|
| 先頭10件を取得 | FETCH FIRST 10 ROWS ONLY |
LIMIT 10 |
LIMIT 10 |
TOP (10) |
NULLを別の値に置換 |
NVL() / COALESCE()
|
COALESCE() |
IFNULL() / COALESCE()
|
ISNULL() / COALESCE()
|
| 文字列の連結 | 連結演算子 | 連結演算子 | CONCAT() |
+ / CONCAT()
|
| 現在日時の取得例 | SYSDATE |
CURRENT_TIMESTAMP |
NOW() |
GETDATE() |
| 1日加算する例 | 日付 + 1 |
日付 + INTERVAL '1 day' |
DATE_ADD() |
DATEADD() |
| 自動採番の指定例 | GENERATED ... AS IDENTITY |
GENERATED ... AS IDENTITY |
AUTO_INCREMENT |
IDENTITY(1,1) |
| 更新または追加(UPSERT) | MERGE |
ON CONFLICT |
ON DUPLICATE KEY UPDATE |
MERGEなど |
※ OracleとPostgreSQLの文字列連結演算子は||です。なお、SQL Serverにもバージョンによって追加の構文がありますが、ここでは一般的に利用されている書き方を示します。
1. 取得件数の制限:LIMIT・FETCH・TOP
**「社員テーブルからIDの小さい順に10件取得する」**という処理です。ここではemployeesテーブルにid列が存在するものとします。
Oracle
SELECT *
FROM employees
ORDER BY id
FETCH FIRST 10 ROWS ONLY;
PostgreSQL / MySQL
SELECT *
FROM employees
ORDER BY id
LIMIT 10;
SQL Server
SELECT TOP (10) *
FROM employees
ORDER BY id;
ページングする場合
例えば、先頭20件を飛ばして次の10件を取得する場合は次のようになります。
-- Oracle
SELECT * FROM employees
ORDER BY id
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
-- PostgreSQL / MySQL
SELECT * FROM employees
ORDER BY id
LIMIT 10 OFFSET 20;
-- SQL Server
SELECT * FROM employees
ORDER BY id
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
注意:
ORDER BYがなければ「先頭10件」は安定しません。ページングでは、一意になる列(例:主キーのid)まで含めて並べ替えることが重要です。Oracleの古いコードにあるROWNUMは、ORDER BYと組み合わせたときの評価順序にも注意が必要です。
2. NULLを別の値に置き換える:NVL・COALESCE・IFNULL・ISNULL
例えば、bonusがNULLなら0と表示する場合です。
-- Oracle
SELECT NVL(bonus, 0) FROM employees;
-- PostgreSQL
SELECT COALESCE(bonus, 0) FROM employees;
-- MySQL
SELECT IFNULL(bonus, 0) FROM employees;
-- SQL Server
SELECT ISNULL(bonus, 0) FROM employees;
実は、4製品ともCOALESCE(bonus, 0)を利用できます。移植性を意識するなら、まずCOALESCEを検討するとよいでしょう。
ただし、COALESCEと各製品固有の関数は、戻り値のデータ型や暗黙的な型変換まで完全に同じではありません。
また、MySQLのISNULL(x)は「xがNULLかを判定する関数」です。SQL ServerのISNULL(x, y)と同じ意味ではありません。
3. 文字列の結合:||・CONCAT・+
**「姓と名を連結する」**処理を比較します。
-- Oracle
SELECT last_name || first_name AS full_name FROM employees;
-- PostgreSQL
SELECT last_name || first_name AS full_name FROM employees;
-- MySQL
SELECT CONCAT(last_name, first_name) AS full_name FROM employees;
-- SQL Server
SELECT last_name + first_name AS full_name FROM employees;
特に注意したいのがMySQLです。通常の設定では、||は文字列結合ではなく論理ORとして解釈されます(PIPES_AS_CONCATモードは例外)。
さらに、NULLを含む場合の結果も同じではありません。
- PostgreSQLの
'A' || NULLはNULL - MySQLの
CONCAT('A', NULL)はNULL - SQL Serverの
CONCAT('A', NULL)は'A'(NULLを空文字として処理) - Oracleの
'A' || NULLは'A'(ただし両方NULLならNULL)
DBを切り替える際は、**「連結できるか」だけでなく「NULLを連結したときどうなるか」**まで確認しましょう。
4. 日付の加算・現在日時:DATE_ADD・DATEADDなど
1日後の日付を求める
-- Oracle
SELECT DATE '2026-01-01' + 1 FROM dual;
-- PostgreSQL
SELECT DATE '2026-01-01' + INTERVAL '1 day';
-- MySQL
SELECT DATE_ADD('2026-01-01', INTERVAL 1 DAY);
-- SQL Server
SELECT DATEADD(day, 1, CAST('2026-01-01' AS date));
PostgreSQLでもDATE '2026-01-01' + 1という書き方はできます。ただし、INTERVALで加算する場合は、式の結果の型がtimestampになることがあります。同じ日付に見えても結果の型が異なる点に注意してください。
現在日時を取得する
-- Oracle(サーバーの現在日時をDATE型で返す)
SELECT SYSDATE FROM dual;
-- PostgreSQL
SELECT CURRENT_TIMESTAMP;
-- MySQL
SELECT NOW();
-- SQL Server
SELECT GETDATE();
これらは取得できる値の型、タイムゾーンの扱い、日時が評価されるタイミングが一致するとは限りません。バッチ処理や締め処理では特に確認が必要です。
5. 自動採番:IDENTITY・AUTO_INCREMENT
新規登録時に主キーidを自動生成したいときの指定例です。
-- Oracle
CREATE TABLE sample_users (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR2(100)
);
-- PostgreSQL
CREATE TABLE sample_users (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(100)
);
-- MySQL
CREATE TABLE sample_users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100)
);
-- SQL Server
CREATE TABLE sample_users (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
name NVARCHAR(100)
);
GENERATED BY DEFAULTとGENERATED ALWAYSでは、明示的にIDを指定した場合の扱いなどが異なります。また、自動採番だからといって番号が欠番なく連続する保証はありません。採番値を業務上の連番として扱う設計には注意しましょう。
6. あれば更新、なければ追加:UPSERT
CSV取込や外部データ連携などでは、同じIDのレコードがあれば更新し、なければ追加する処理がよく出てきます。
ここでは、employees(id, name)にid = 1, name = '山田'を反映する例です。idには主キーまたは適切な一意制約がある前提です。
Oracle:MERGE
MERGE INTO employees e
USING (SELECT 1 AS id, '山田' AS name FROM dual) src
ON (e.id = src.id)
WHEN MATCHED THEN
UPDATE SET e.name = src.name
WHEN NOT MATCHED THEN
INSERT (id, name) VALUES (src.id, src.name);
PostgreSQL:ON CONFLICT
INSERT INTO employees (id, name)
VALUES (1, '山田')
ON CONFLICT (id)
DO UPDATE SET name = EXCLUDED.name;
EXCLUDED.nameは、登録しようとした行のnameを表します。
MySQL:ON DUPLICATE KEY UPDATE
INSERT INTO employees (id, name)
VALUES (1, '山田') AS new
ON DUPLICATE KEY UPDATE name = new.name;
MySQLはPRIMARY KEYまたはUNIQUEキーの重複によって更新が発生します。複数の一意制約がある場合は、想定と違うキーの重複が更新を引き起こす可能性があります。
SQL Server:MERGE
MERGE INTO employees AS target
USING (SELECT 1 AS id, N'山田' AS name) AS src
ON target.id = src.id
WHEN MATCHED THEN
UPDATE SET name = src.name
WHEN NOT MATCHED THEN
INSERT (id, name) VALUES (src.id, src.name);
SQL ServerのMERGEでは文末の;が必要です。またMicrosoftの公式ドキュメントでも、同時実行が多い処理ではMERGEの競合や性能を検証し、個別のINSERT・UPDATEなどの採用を検討するよう案内しています。
注意:UPSERT構文があれば同時実行時の整合性を無条件に保証できる、というわけではありません。主キー・一意制約、トランザクション分離レベル、ロック設計、エラー時の再試行を含めて設計してください。
7. 同じSQLに見えて結果が違う:空文字・日付型・文字数
ここがDB移行で特に見落としやすい部分です。
7-1. Oracleでは空文字がNULLになる
例えば、次の条件です。
WHERE name = ''
Oracleでは長さ0の文字列がNULLとして扱われるため、一般的なNULL比較と同様、この条件でNULL行を検索することはできません。NULLを検索するなら次のように書きます。
WHERE name IS NULL
一方、PostgreSQL・MySQL・SQL Serverでは、通常の文字列型で''とNULLを区別します。Oracleから移行すると、空文字を入れていたつもりのデータがNULLとして扱われていたことが問題になる場合があります。
7-2. OracleのDATEには時刻も含まれる
OracleのDATE型は、年月日だけでなく時・分・秒を保持します。PostgreSQL・MySQL・SQL ServerのDATE型は日付のみです。
そのため、Oracleで次の条件を指定しても、対象日のすべての時刻を取得できるとは限りません。
WHERE created_at = DATE '2026-01-01'
Oracleのcreated_atが2026-01-01 12:30:00であれば一致しません。日時列から1日分を取得するなら、例えば次のように開始日時以上・翌日未満で絞る方法が有効です。
WHERE created_at >= DATE '2026-01-01'
AND created_at < DATE '2026-01-02'
7-3. LENGTHが「文字数」とは限らない
| 製品 | 文字数の例 | バイト数の例 |
|---|---|---|
| Oracle | LENGTH(name) |
LENGTHB(name) |
| PostgreSQL | LENGTH(name) |
OCTET_LENGTH(name) |
| MySQL | CHAR_LENGTH(name) |
LENGTH(name) |
| SQL Server | LEN(name) |
DATALENGTH(name) |
MySQLのLENGTHはバイト数を返します。例えばUTF-8(utf8mb4)で保存した'あ'は、CHAR_LENGTH('あ') = 1、LENGTH('あ') = 3となります。
なお、SQL ServerのLENは末尾のスペースを数えません。文字数の制約やデータ移行の照合では、これらの違いも考慮しましょう。
移行・レビュー時に確認したいチェックリスト
-
LIMIT・TOP・FETCHとORDER BYが適切か -
NVL・IFNULL・ISNULLなどの関数を置き換えたか -
文字列連結時の
NULLの動作が変わらないか - 日付型の時刻・タイムゾーン・精度を確認したか
- ID自動採番と既存データのID投入方法を確認したか
- UPSERTのキーと同時実行時の動作を検証したか
-
''とNULL、文字数とバイト数の違いを確認したか
おわりに
SQLの基本構文であるSELECT・JOIN・WHEREは多くのRDBMSで共通しています。しかし、件数制限、関数、日付、UPSERTには製品ごとの「SQL方言」があります。
特に気をつけたいのは、単純に構文エラーになる違いよりも、SQLは実行できるのに検索・更新結果が異なる違いです。
新しいDBを触るときやシステムを移行するときは、まずこの記事の比較表とチェックリストを確認し、使用する製品・バージョンの公式ドキュメントで詳細を確認してみてください。