Oracle の JOIN の書き方 — 旧記法 (+) と ANSI 構文の対応表

古い Oracle のコードを読んでいると、FROM にテーブルをカンマで並べて WHERE で結合したり、WHERE a.id = b.id(+) のように (+) が付いていたりする SQL に出会います。これは Oracle が ANSI の JOIN 構文に対応する前から使われてきた旧記法で、(+) は Oracle 独自の外部結合演算子です。

結論から言うと、カンマ結合+WHERE は INNER JOIN、(+) は LEFT JOIN / RIGHT JOIN に対応します。この記事では実際の Oracle Database で両者の結果が一致することを確認しながら、書き換えの対応表と、書き換え時に踏みやすい Oracle 固有の癖をまとめます。なお、Oracle 公式マニュアルも新しく書くなら ANSI 構文を推奨しています(後述)。

実測環境は Docker の gvenzl/oracle-free:23-slim-faststart(バナー表記は Oracle AI Database 26ai Free Release 23.26.2.0.0)です。比較用の PostgreSQL 17.10 / MySQL 8.4.10 も Docker で動かしています。サンプルは次の2表です。

CREATE TABLE emp (id INT, name VARCHAR(20), dept_id INT);
CREATE TABLE dept (dept_id INT, dept_name VARCHAR(20));
INSERT INTO emp VALUES (1,'佐藤',10),(2,'鈴木',20),(3,'高橋',NULL),(4,'田中',40);
INSERT INTO dept VALUES (10,'営業部'),(20,'開発部'),(30,'総務部');

高橋は dept_id が NULL、田中の dept_id 40 は dept 側に存在せず、総務部(30)は emp 側に誰もいない、という配置です。なお、複数行をまとめて書く VALUES (…),(…) 形式の INSERT を Oracle が受け付けるのは 23ai 以降です。それより古い Oracle で手元に再現する場合は、INSERT 文を1行ずつに分けて実行してください。

対応表: 旧記法と ANSI 構文

旧 Oracle 記法ANSI 構文意味
FROM emp, dept WHERE emp.dept_id = dept.dept_idFROM emp INNER JOIN dept ON emp.dept_id = dept.dept_id内部結合
FROM emp, dept WHERE emp.dept_id = dept.dept_id(+)FROM emp LEFT JOIN dept ON emp.dept_id = dept.dept_id左外部結合
FROM emp, dept WHERE emp.dept_id(+) = dept.dept_idFROM emp RIGHT JOIN dept ON emp.dept_id = dept.dept_id右外部結合
(書けない → 後述)FROM emp FULL OUTER JOIN dept ON emp.dept_id = dept.dept_id完全外部結合

(+) は「こちら側に一致する行がなくても NULL で埋めて残す」側、つまり NULL でパディングされる側の列に付けます。dept 側に付ければ「emp は全行残す」= LEFT JOIN です。

カンマ結合 ⇔ INNER JOIN の結果一致

SELECT * FROM emp, dept WHERE emp.dept_id = dept.dept_id;

結果は次の2行で、INNER JOIN … ON に書き換えても同じ2行が返ります(実測)。

IDNAMEDEPT_IDDEPT_IDDEPT_NAME
1佐藤1010営業部
2鈴木2020開発部

SELECT * だと DEPT_ID が2回現れる点はカンマ結合も ON も同じです(USING を使うと1回にマージされます)。

(+) ⇔ LEFT JOIN の結果一致

SELECT * FROM emp, dept WHERE emp.dept_id = dept.dept_id(+);

dept 側の列に (+) を付けたので emp は全行残り、結果は次の4行です。

IDNAMEDEPT_IDDEPT_IDDEPT_NAME
1佐藤1010営業部
2鈴木2020開発部
3高橋(NULL)(NULL)(NULL)
4田中40(NULL)(NULL)
SELECT * FROM emp LEFT JOIN dept ON emp.dept_id = dept.dept_id;

こちらも同一の4行で、両者の結果が一致することを実測で確認しています。部署が引けなかった高橋(dept_id が NULL)と田中(dept_id 40 が dept に無い)が、dept 側 NULL のまま残るのが外部結合です。

(+) では書けないもの: FULL OUTER JOIN

「両方の表の行を全部残したい」からと (+) を両側に付けると、エラーになります(実測)。

SELECT * FROM emp, dept WHERE emp.dept_id(+) = dept.dept_id(+);
-- ORA-01468: a predicate may reference only one outer-joined table

(+) はひとつの条件でどちらか片側にしか付けられません。完全外部結合が必要なら ANSI 構文で書きます。

SELECT * FROM emp FULL OUTER JOIN dept ON emp.dept_id = dept.dept_id;

これは LEFT JOIN の4行に、emp 側に誰もいない総務部(ID・NAME が NULL)を加えた5行を返します(実測)。

公式マニュアルには他にも (+) の制限が列挙されています。主なものだけ挙げると、同じ問い合わせブロックで ANSI JOIN 構文と混在できない、(+) 付きの条件を OR で他の条件とつなげない、IN 条件に使えない、自己外部結合に使えない、などです。旧記法は「書けそうで書けない」パターンが多く、これが ANSI 構文を推奨する理由にもなっています。

ANSI 構文に書き換えるときの Oracle の癖

書き換え先の ANSI 構文側にも、Oracle 固有の引っかかりが2つあります。

ひとつ目は USING です。USING(dept_id) で結合した場合、その列に表修飾子(emp. など)を付けるとエラーになります(実測)。

SELECT emp.dept_id FROM emp JOIN dept USING (dept_id);
-- ORA-25154: column part of USING clause cannot have qualifier

USING の列は修飾なしの dept_id と書く必要があります。同じクエリが PostgreSQL 17 / MySQL 8.4 ではエラーにならず通る(どちらも 10, 20 の2行)ことも確認したので、他の DB から来た人ほど踏みやすい癖です。

ふたつ目は CROSS JOIN です。直積を取る CROSS JOIN に結合条件の ON を付けると構文エラーになります(実測)。

SELECT * FROM emp CROSS JOIN dept ON emp.dept_id = dept.dept_id;
-- ORA-03048: SQL reserved word 'ON' is not syntactically valid following ...

これは PostgreSQL と同じ挙動で、逆に MySQL や SQLite は通してしまいます(この違いは SQL の JOIN の種類と違いまとめ — INNER/LEFT/RIGHT/FULL/CROSS【2026年更新】 で扱っています)。条件を付けたいなら素直に INNER JOIN … ON と書きます。

なお JOIN = INNER JOIN のキーワード省略は Oracle でも実測で結果が同一になることを確認しています(LEFT OUTER JOINOUTER 省略なども、Oracle が ANSI 構文にフル対応している以上同様に有効です)。どこまで省略するかの整理は SQL の JOIN は省略できる — INNER/OUTER キーワードとカンマ結合の整理 にまとめました。

新しく書くなら ANSI 構文

Oracle の SQL Language Reference(Joins 節)は、外部結合について次のように明言しています。

Oracle recommends that you use the FROM clause OUTER JOIN syntax rather than the Oracle join operator.

つまり (+) ではなく FROM 句の OUTER JOIN 構文(ANSI 構文)を使うこと、という公式の推奨です。前述のとおり (+) には FULL OUTER が書けない・OR や IN と組み合わせられないなどの制限が多く、Oracle 以外の DB では通用しない方言でもあります。既存コードを読むための知識としては必要ですが、新規に書く SQL で選ぶ理由はありません。

さいごに

(+) は「付けた側が NULL でパディングされる側」と覚えれば、旧コードの読み替えはこの記事の対応表どおりです。書き換えの際は、FULL OUTER 相当は (+) では書けないこと(ORA-01468)、USING 列に修飾子を付けられないこと(ORA-25154)、CROSS JOINON を付けられないこと(ORA-03048)の3点に気をつけてください。

あわせて読みたい:

INNER JOIN と LEFT JOIN の違い — 結果がどう変わるか実例で確認

INNER JOIN と LEFT JOIN の違いは一言でいうと、相手のいない行を結果に残すかどうかです。INNER JOIN(内部結合)は両方のテーブルに対応する行があるものだけを返し、LEFT JOIN(左外部結合)は左のテーブルの行を全部残して、相手がいない行は右側の列を NULL で埋めます(これを NULL パディングと呼びます)。

この記事では同じデータに両方を実行して結果を見比べたあと、条件を ON に書くか WHERE に書くかで LEFT JOIN の結果が変わる罠と、「LEFT JOIN は遅いのか」という疑問を扱います。実行結果はすべて PostgreSQL 17.10 で実際に確認したもので、ON と WHERE の罠については MySQL 8.4.10 でも同じ結果になることを確認しています。JOIN 全種類のまとめは SQL の JOIN の種類と違いまとめ — INNER/LEFT/RIGHT/FULL/CROSS【2026年更新】 にあります。

サンプルデータ

社員表 emp と部署表 dept を使います。

CREATE TABLE emp (id INT, name VARCHAR(20), dept_id INT);
CREATE TABLE dept (dept_id INT, dept_name VARCHAR(20));
INSERT INTO emp VALUES (1,'佐藤',10),(2,'鈴木',20),(3,'高橋',NULL),(4,'田中',40);
INSERT INTO dept VALUES (10,'営業部'),(20,'開発部'),(30,'総務部');

わざと対応の崩れた行を入れてあります。高橋は dept_id が NULL(部署未定)、田中の dept_id 40 は dept 側に存在しません。この2人の扱いに INNER と LEFT の差が出ます。

同じデータでの結果比較

まず INNER JOIN です。

SELECT * FROM emp INNER JOIN dept USING(dept_id);
dept_ididnamedept_name
101佐藤営業部
202鈴木開発部

2行だけになりました。dept_id が両方のテーブルで一致した行、つまり佐藤と鈴木だけです。高橋(dept_id が NULL)と田中(dept_id 40 は dept に無い)は結合相手がいないので、結果から消えます。

次に、同じクエリの INNER を LEFT に変えます。

SELECT * FROM emp LEFT JOIN dept USING(dept_id);
dept_ididnamedept_name
101佐藤営業部
202鈴木開発部
NULL3高橋NULL
404田中NULL

今度は4行です。左のテーブル emp の行は相手がいなくても全部残り、高橋と田中は dept 側の列(dept_name)が NULL で埋められています。これが LEFT JOIN です。

なお USING(dept_id) で結合しているため、結合キーの dept_id は結果の先頭に1回だけ現れます。ON e.dept_id = d.dept_id と書いた場合は両テーブルの dept_id が2回現れます。INNER JOIN なら2列とも同じ値ですが、LEFT JOIN の相手なし行では右テーブル側の dept_id は NULL になる(田中なら emp 側は 40、dept 側は NULL)ので、どちらの dept_id を SELECT するかに注意してください。

ちなみに LEFT JOIN は LEFT OUTER JOIN の省略形で、OUTER を書いても書かなくても意味は変わりません。キーワードの省略ルールは SQL の JOIN は省略できる — INNER/OUTER キーワードとカンマ結合の整理 にまとめました。

RIGHT JOIN は LEFT JOIN の左右反転で、右のテーブルの行を全部残します。FROM emp RIGHT JOIN deptFROM dept LEFT JOIN emp とテーブル順を入れ替えれば同じ行の組を LEFT で書けるので(SELECT * では列の並び順だけ変わります)、実務では LEFT に統一して書くことが多いです。

罠:条件を ON に書くか WHERE に書くかで結果が変わる

ここが本題です。INNER JOIN では結合条件以外の条件を ON に足しても WHERE に書いても結果は同じですが、LEFT JOIN では ON と WHERE で結果が変わります

「営業部」に関する条件を、まず ON に書いてみます。

SELECT e.name, d.dept_name FROM emp e
  LEFT JOIN dept d ON e.dept_id = d.dept_id AND d.dept_name = '営業部';
namedept_name
佐藤営業部
鈴木NULL
高橋NULL
田中NULL

4行です。ON の条件は「どの行を結合相手として採用するか」の選別に使われるだけなので、左テーブルの行は落ちません。条件に合わなかった鈴木は、相手なし扱いになって NULL が付くだけです。

同じ条件を WHERE に移すとこうなります。

SELECT e.name, d.dept_name FROM emp e
  LEFT JOIN dept d ON e.dept_id = d.dept_id WHERE d.dept_name = '営業部';
namedept_name
佐藤営業部

1行だけになりました。WHERE は結合が終わったあとの結果に対するフィルタです。dept_name が NULL の行は = '営業部' の比較を満たせないので全部落ち、結果として INNER JOIN と同じになります。せっかく LEFT JOIN にした意味が消えるわけです。

PostgreSQL の公式マニュアルにも、ON の条件は結合前に、WHERE の条件は結合後に処理される、内部結合では違いがないが外部結合では大きく違う、と明記されています。

使い分けはこう覚えてください。左テーブルの行を残したまま右テーブル側を絞りたいなら ON に書く。結合後の結果を絞りたいなら WHERE に書く(そしてその場合、実は INNER JOIN で足りていることが多い)。「LEFT JOIN したのに行が減った」というときは、まず WHERE に右テーブルの条件が紛れていないかを疑ってください。

LEFT JOIN は INNER JOIN より遅い?

「LEFT JOIN は遅いから INNER JOIN に書き換えるべき」という話を見かけますが、そのままでは正しくありません。

先ほどの「WHERE に右テーブルの条件を書いた LEFT JOIN」を PostgreSQL 17 で EXPLAIN すると、実行計画は Hash Join になります。Hash Left Join ではありません。WHERE の条件で NULL パディングされた行がどうせ残れないとオプティマイザが判断すると、LEFT JOIN は内部的に INNER JOIN へ変換されるからです。

つまり、同じ結果を返すクエリであれば INNER と LEFT に原理的な速度差はほぼありません。逆に結果が違うクエリ同士(2行返るものと4行返るもの)の速度を比べても意味がありません。書き分けの基準は速度ではなく、前の節までで見てきた「どの行を残したいか」です。

使い分けの指針

  • 結合相手が必ず存在するデータ(外部キー制約などで対応が保証されている親子)を突き合わせるなら INNER JOIN。対応の無い行が混ざりようがないので、意図がそのまま伝わります。
  • 相手がいない行も見たいなら LEFT JOIN。「部署未定の社員も一覧に出す」「注文が1件も無い顧客も集計に含める」といった場面です。相手がいない行だけを探す(dept_name IS NULL で絞る)という欠損チェックにも使えます。

迷ったら「相手のいない行を結果に出したいか」を自問すれば決まります。

さいごに

INNER JOIN は両方に対応する行だけ、LEFT JOIN は左の全行+相手なしは NULL、というのが基本の違いです。それに加えて、LEFT JOIN では条件を ON に置くか WHERE に置くかで結果が変わること、速度は書き分けの基準にならないこと、この2点を押さえておけば実務で困ることはほぼありません。

あわせて読みたい:

SQL の JOIN は省略できる — INNER/OUTER キーワードとカンマ結合の整理


JOIN とだけ書いたら INNER JOIN になるのか」「LEFT JOINLEFT OUTER JOIN は違うのか」という疑問への答えを先に書くと、どちらもまったく同じものです。INNEROUTER は書いても書かなくても意味の変わらないキーワードで、省略しても結果は 1 行も変わりません。

この記事では、省略形の対応表と「では実際どこまで省いて書くか」の指針、そして JOIN の省略とよく混同される「カンマ結合(FROM a, b)」がなぜ別物なのかを、実際に動かした結果つきで整理します。動作確認は 2026年7月に PostgreSQL 17.10 / MySQL 8.4.10 / SQLite 3.51.0 と Oracle Database Free(23ai系, 23.26.2.0.0)で行いました。JOIN そのものの種類と結果の違いは SQL の JOIN の種類と違いまとめ — INNER/LEFT/RIGHT/FULL/CROSS【2026年更新】 にまとめているので、そちらもどうぞ。

省略形の対応表

続きを読む SQL の JOIN は省略できる — INNER/OUTER キーワードとカンマ結合の整理

PEM ⇄ DER / CRT / KEY / PFX(p12) 変換 opensslワンライナー集

サーバや機器によって、欲しい証明書ファイルの形式はバラバラです。 「Windows(IIS)は pfx、nginx は pem、Java は DER」と、同じ証明書を形式だけ変えたい場面がよくあります。

この記事は、形式変換の openssl ワンライナーを「変換元 → 変換先」で逆引きできるようにまとめたものです。 形式そのものの違いがあやしい人は、先に 証明書ファイルの形式まとめ を読んでおくと迷いません。

動作確認は OpenSSL 3.x(手元 3.6.2)。秘密鍵の出力は 3.x 既定で PKCS#8(-----BEGIN PRIVATE KEY-----) になります。

続きを読む PEM ⇄ DER / CRT / KEY / PFX(p12) 変換 opensslワンライナー集

opensslで証明書・鍵・CSR・接続を確認するコマンド総まとめ

証明書や鍵まわりで困ったら、だいたい openssl コマンドで中身を確認すれば解決します。 この記事は、「確認」系の openssl コマンドを用途別に総ざらいしたチートシートです。コピペで使ってください。実際の出力例と、つまずきやすいエラー例も載せました。

この記事は OpenSSL 3.x(手元は 3.6.2)で実際に実行して確認しています。openssl version で自分の版を確認できます。古い 1.0.x 系だと一部オプションの既定が違うことがあります。出力例の証明書はこの記事用に作ったテスト用(www.example.com)なので、値はお手元のものに読み替えてください。

続きを読む opensslで証明書・鍵・CSR・接続を確認するコマンド総まとめ

公開鍵・秘密鍵・証明書の関係をやさしく図解(PKI入門)

SSL証明書や .pem を触っていると、「秘密鍵」「公開鍵」「証明書」「認証局(CA)」「中間証明書」と言葉が次々出てきて、結局それぞれどういう関係なのか分からなくなりがちです。

この記事は、コマンドの前に知っておくとラクになる「鍵と証明書の関係」を、図を使ってかみ砕いて説明します。

続きを読む 公開鍵・秘密鍵・証明書の関係をやさしく図解(PKI入門)

証明書ファイルの形式まとめ — PEM / DER / PFX(p12) / CRT / CER / KEY / CSR の違い

SSL証明書を扱っていると、.pem .crt .cer .der .key .pfx .p12 .csr と似たような拡張子がいろいろ出てきて、何が違うのか混乱しますよね。

結論から言うと、「中身のエンコード方式(PEM か DER か)」と「中に何が入っているか(証明書か・鍵か・両方か)」の2軸で整理すると分かりやすくなります。この記事ではその地図を示します。

続きを読む 証明書ファイルの形式まとめ — PEM / DER / PFX(p12) / CRT / CER / KEY / CSR の違い

[Apache] .git, .gitignore等をWebからアクセスできないようにする

gitで管理しているwebサイトでうっかり.gitや.gitignoreなどの Git 関連ファイルが漏れてしまわないようにするには、httpd.conf で以下のように設定します。

# .git から始まるファイルへのアクセスを禁止
<Files ~ "^\.git">
    Order allow,deny
    Deny from all
    Satisfy All
</Files>
# .git/ フォルダ以下のファイル・ディレクトリへのアクセスを禁止
<Directorymatch "^/.*/\.git/">
Order deny,allow
Deny from all
</Directorymatch>

.git は外してアップしているよ、という方も、保険として入れておくと万一の時に .git や .gitignore などが
公開ディレクトリに混ざってしまってもアクセスできないようになるので安全ですね。

[PHP][Yii] モデルでタイムスタンプを自動更新する

Yii 1.1 のモデル(CModel)で自動更新する方法のメモ。

参考: Yii 1.1: How to automate timestamps in ActiveRecord models

CActiveRecord::rules()で設定する

/**
 * @return array validation rules for model attributes.
 */
public function rules()
{
    return array(
        // 略
        array('modified','default',
              'value'=>new CDbExpression('NOW()'),  // PHPのタイムスタンプを使うならdate('Y-m-d H:i:s')など
              'setOnEmpty'=>false,'on'=>'update'),
        array('created,modified','default',
              'value'=>new CDbExpression('NOW()'),
              'setOnEmpty'=>false,'on'=>'insert')
    );
}

CActiveRecord::beforeSave()で更新する

public function beforeSave() {
    if ($this-&gt;isNewRecord)
        $this->created = new CDbExpression('NOW()');

<pre><code>$this-&gt;modified = new CDbExpression('NOW()');

return parent::beforeSave();
</code></pre>

}

CTimestampBehaviorを使う

    // CActiveRecord::behaviors()
    public function behaviors(){
        return array(
            'CTimestampBehavior' =&gt; array(
                'class' =&gt; 'zii.behaviors.CTimestampBehavior',
                'createAttribute' => 'created', // 作成日時のカラム名(デフォルト: create_time)
                'updateAttribute' => 'modified', // 更新日時のカラム名(デフォルト: update_time)
                'setUpdateOnCreate' => true,    // 作成時にupdateカラムも更新するか(デフォルト: false)
            )
        );
    }

どれを使うかはお好みですね。

Google Calendar API のタイムゾーン設定方法

Google カレンダー API で時間が9時間ずれて表示されたり、取得範囲が指定したよりも9時間ずれたりしていて散々はまった。
続きを読む Google Calendar API のタイムゾーン設定方法