◀ 7.

Django/MySQLのUnknown column ...id|主キー・実列・別名の確認

▶

Djangoで「Unknown column ...id」が出る場合、まずモデルが選択する列と、接続している実テーブルを照合します。Djangoの主キー対応と、MySQLの別名/スコープの誤りは異なる問題です。

接続先と実列、Djangoの主キーとdb_column、SQLの表別名を順に照合しDDLを診断として実行しない図

Djangoの暗黙idと実テーブルの不一致

主キーを明示しないDjangoモデルには、通常idという自動主キーが補われます。managed=FalseはDjangoによるテーブル作成/削除等の管理を制御するもので、idをモデルから消す設定ではありません。既存の表にperson_noしかないのに、モデルがidもSELECTするとエラーになります。

元の記事にあった「forで回すと主キー不足のエラー」という観測は、QuerySetの遅延評価でその時にSQLが実行された可能性と分けて理解します。forだけが原因ではなく、list()等でも発生します。valuesでidを選ばない読取が通っても、モデルの対応が直ったとは限りません。

確認するもの確認点
実DB接続alias/DB/表/列と制約。開発機や別DBだけの確認で済ませない。
モデル_meta.pkのname/column、db_table、db_column、managedと実際の設定。
一意キー存在・非NULL・一意性と更新の意味。都合のよい列へprimary_key=Trueを足して済ませない。
SQL実行時の列と別名、query block。ログへURL/SQL/params/例外を丸ごと公開しない。

実在する一意キーへ対応付ける教材

次はperson_noが本当に一意な主キーである合成表だけの例です。Python側をperson_key、DB列をperson_noと明示します。既存のモデル/DBをこの定義で置き換える指示ではありません。複合キーやキーのない表は別の設計が必要で、重複列に主キーを宣言してもDBの一意性は生まれません。

from django.db import models


class LegacyPerson(models.Model):
    person_key = models.BigIntegerField(primary_key=True, db_column="person_no")
    display_name = models.CharField(max_length=50)

    class Meta:
        app_label = "legacy_pk_lab"
        db_table = "legacy_person"
        managed = False

mapping.pyは既知のモデルのメタ情報だけを辞書で返し、DBへ問い合わせたり環境変数を出したりしません。mapping_report(LegacyPerson)のprimary_fieldはperson_key、primary_columnはperson_noです。本番の管理情報を公開する診断URLとして使いません。

def mapping_report(model):
    primary = model._meta.pk
    return {
        "model": model._meta.label,
        "table": model._meta.db_table,
        "managed": model._meta.managed,
        "primary_field": primary.name,
        "primary_column": primary.column,
    }

読取だけで確認するSQL

read_queries.sqlは教材の既存表に対する読取だけです。実環境では許可された対象と読取権限で実施し、未知のDB名/識別子をユーザー入力から連結しません。制約とデータの意味まで確認してから対応を決めます。

SELECT DATABASE() AS current_database, VERSION() AS server_version;
SHOW FULL COLUMNS FROM legacy_person;
SHOW INDEX FROM legacy_person;
SELECT person_no, display_name FROM legacy_person ORDER BY person_no;

inspectdbは既存表からモデル候補を出力する補助であり、そのまま正しい設計が完成する保証ではありません。既存models.pyへ出力を上書きせず、別の確認用出力として照合します。raw()でモデルを得る場合も、対応する主キーがSELECT結果に必要です。managed=Falseを読取専用の認可や書込み禁止機能とも扱いません。

MySQL8.4.13とSQLiteの独立検体では、暗黙idは読取評価時に失敗し、正しい対応の2行は(11, Ada)/(22, Ben)で一致しました。非一意なgroup_noを主キーと偽ってもget(pk=7)は複数件になりました。実テーブルにid列を追加したり、migrationを走らせたりせずに比較しています。試験の表の作成・投入は新しい検体DB内だけです。

この記事の要点
  • 意味: SELECT / WHERE / ON 句で存在しないカラム or テーブル修飾を参照
  • 確認点: Djangoが選ぶ主キー/列と実テーブル、接続先、列名、表の別名とquery blockのスコープを照合する
  • 切り分け: SHOW COLUMNS FROM table で実カラム名確認、DESC table、エラーメッセージのテーブル名がエイリアスか実名か区別
  • JOIN ON 句でエイリアスを使ったら、SELECT/WHERE もエイリアスで統一
  • Django: managed=Falseでも暗黙のidは省略されない。実在する一意キーとdb_columnを確認する

エラー内容

旧例の注意:次はMySQLのエラー表示例です。1054はエラー番号、42S22はSQLSTATE、field list等は参照箇所です。DjangoのPython例外型やSQLiteの文言と同じ分類ではありません。番号だけで主キー不足と決めつけず、実際の接続先と列を照合します。

ERROR 1054 (42S22): Unknown column 'users.id' in 'field list'

# 'field list'    = SELECT 句で起きた
# 'where clause'  = WHERE 句で起きた
# 'on clause'     = JOIN ON 句で起きた
# 'order clause'  = ORDER BY で起きた

MySQL が「指定されたカラムやテーブル.カラム表記が見当たらない」と言っているエラーです。末尾のfield list/where clause等を読みます。実テーブルにidが無い理由がモデルの対応違いなのか、SQLの別名なのかを分けます。

原因 1: カラム名 / テーブル名のタイポ

旧例の注意:次はordersにuser_idがあるという前提の旧SQLです。FROMで存在しないテーブルを指定した場合は1054ではなく1146等になるため、テーブル名の誤りを同じ番号と断定しません。SHOW/DESCも読取権限が必要で、rootへの変更を推奨しません。

-- ❌ 実際のカラムは user_id だが userid と書いた
SELECT userid FROM orders;
-- ERROR 1054: Unknown column 'userid' in 'field list'

-- 確認
SHOW COLUMNS FROM orders;
DESC orders;

-- ✅ 修正
SELECT user_id FROM orders;

原因 2: AS でエイリアスを付けた後に元テーブル名で参照

同じquery blockで表へ別名を付けた場合、その表の列参照も対応する別名へ合わせます。原因の頻度やすべてのSQL階層での有効範囲を断定するものではありません。

旧例の注意:次は同じquery blockでusersへuという別名を付けた例です。SELECT/ONの両方に不備があるため、末尾の句名は旧コメントでなく実際のエラーで確認します。別名の不一致を直すもので、Djangoの暗黙id/実列不一致を直す例ではありません。複数表に同じ列がある場合は曖昧な参照も別に確認します。

-- ❌ エイリアス u を付けたのに users.id を参照
SELECT users.id, users.name
FROM users AS u
JOIN orders AS o ON o.user_id = users.id;
-- ERROR 1054: Unknown column 'users.id' in 'field list'

-- ✅ エイリアスで統一
SELECT u.id, u.name
FROM users AS u
JOIN orders AS o ON o.user_id = u.id;

原因 3: サブクエリのスコープミス

相関サブクエリは外側を参照できます。どのquery blockにどの表/別名があるかを確認します。次の例の問題は、外側をpと名付けたのに内側でproductsと呼んでいることです。

旧例の注意:次の旧コメントは原因を言い切れていません。相関サブクエリは外側を参照できます。この例ではproductsをpと呼んでいるのにproducts.category_idと書くことが問題です。p.category_idに合わせれば参照できます。JOINへの書換えは一致する内側の行数により重複を増やすため、結果を比較せず等価とみなしません。

-- ❌ サブクエリ内に products.category_id は無い
SELECT p.name
FROM products p
WHERE p.id IN (
    SELECT id FROM products_top
    WHERE products.category_id = 5  -- ★ 外側の products を参照
);
-- ERROR 1054: Unknown column 'products.category_id' in 'where clause'

-- ✅ 内側にも参照を入れるか、JOIN に書き換える
SELECT p.name
FROM products p
JOIN products_top pt ON pt.id = p.id
WHERE p.category_id = 5;

結果を変えない相関の例

correlated.sqlは外側をp、内側をptとしてEXISTSで一致の有無を調べます。教材のproducts_topには同じidを2行入れ、IN/EXISTSは1行、単純JOINは2行になることを両DBで確認しました。別名を直すことと、重複を含む結果の等価性は別です。

SELECT p.id, p.name
FROM products AS p
WHERE p.category_id = 5
  AND EXISTS (
      SELECT 1 FROM products_top AS pt WHERE pt.id = p.id
  )
ORDER BY p.id;

原因 4: JOIN の ON 句でテーブル名が違う

旧例の注意:次はONに現れるcustomersという名前が、このFROM/JOINに定義されていない例です。列名だけを修正せず、対象表/別名と結合条件を照合します。

-- ❌ JOIN したテーブル名と ON 句が食い違う
SELECT *
FROM users u
JOIN orders o ON o.user_id = customers.id;
-- ERROR 1054: Unknown column 'customers.id' in 'on clause'

-- ✅ 修正
SELECT *
FROM users u
JOIN orders o ON o.user_id = u.id;

原因 5: GROUP BY / ORDER BY での参照ミス

旧例の注意:次はcntとcountの食い違いの例です。最後のORDER BY cntだけでは独立したSQL文になりません。元のSELECTへ組み込み、同点順や集計の意味も別に確認します。

-- ❌ SELECT で別名を付けたカラムを別名で ORDER BY しているが、
--    その別名と異なる名前を使った
SELECT
  u.name,
  COUNT(*) AS cnt
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.id
ORDER BY count;  -- ★ 別名は cnt
-- ERROR 1054: Unknown column 'count' in 'order clause'

-- ✅
ORDER BY cnt;

切り分けの手順

  1. エラー末尾の in 'xxx clause' でどの句かを特定
  2. SHOW COLUMNS FROM テーブル名 で実カラム確認
  3. SQL の FROM / JOIN でつけたエイリアスを洗い出す
  4. エイリアスを使うなら全箇所エイリアスに統一
  5. 相関の有無、query blockごとの別名、期待する行数と重複を確認する

他製品の旧比較例:Laravel Query Builder

旧例の注意:次はDjangoではなくLaravelのQuery Builderの旧比較例です。DB::tableはEloquentモデルの説明と同一ではありません。toSql()は実行履歴ではなく、dd()を本番へ足して内部情報を公開しません。この作業でPHP/Laravelの実行は検証していません。

<?php
// ❌ 別名 u を付けたのに users.id で参照
$users = DB::table('users as u')
    ->join('orders as o', 'o.user_id', '=', 'u.id')
    ->select('users.id', 'users.name')   // ← ここ
    ->get();
// SQLSTATE[42S22]: Column not found: 1054 Unknown column 'users.id'

// ✅ エイリアスで統一
$users = DB::table('users as u')
    ->join('orders as o', 'o.user_id', '=', 'u.id')
    ->select('u.id', 'u.name')
    ->get();

// デバッグ: 実際に発行される SQL を確認
$sql = DB::table('users as u')
    ->join('orders as o', 'o.user_id', '=', 'u.id')
    ->select('u.id')
    ->toSql();
dd($sql);

他製品の旧比較例:Eloquentモデル

旧例の注意:次は別製品Eloquentの旧比較例です。Djangoのモデル設定へこのコードを移しません。集計別名を直すことと、Djangoの主キーを実テーブルへ対応付けることは別です。

<?php
// ❌ withCount のリレーション名と JOIN 結果の参照が食い違い
$users = User::withCount('orders')
    ->orderBy('order_count')  // ★ 実際は orders_count
    ->get();
// Unknown column 'order_count'

// ✅
$users = User::withCount('orders')
    ->orderBy('orders_count')
    ->get();

マイグレーション直後に出る場合

環境差がある場合も、未実行の変更が原因とは限りません。接続DB/alias、表名/列名、modelと配布版、履歴上の変更を読取で照合します。変更が必要と分かっても、承認済みの手順・退避・互換性・失敗時の対応を別途検討します。次の旧コードを診断として実行しません。

旧例の注意:実行しないでください。次の旧手順にあるphp artisan migrate --forceはスキーマを変更し得る処理で、このDjangoエラーの診断方法ではありません。root接続や自動マイグレーションで解決を試みず、最初は読取権限で接続先・実列・モデル・配布版を照合します。

# Laravel
php artisan migrate:status
php artisan migrate --force

# 該当テーブルのカラム状態を直接確認
mysql -u root -p myapp -e "SHOW COLUMNS FROM users"

FAQ

Q: 大文字小文字が違うだけでも出る?
A: テーブル名の大小扱いはlower_case_table_namesとOS/ファイルシステム等の条件を確認します。Linuxという名称だけで実設定を決めつけず、既存DBの設定を診断目的で変更しません。列名の規則と表名の規則も区別します。

Q: 予約語を使っていると出る?
A: 構文の1064、列参照の1054等を分けます。MySQLのバッククォートは識別子を囲むもので、存在しない列を作りません。シングルクォートの文字列リテラルとも別です。ダブルクォートの扱いもsql_modeに依存するため一律の置換をしません。

Q: 通常のビューで起きる場合は?
A: ビュー上の存在しない列の参照と、定義先の表/列が変わってビュー自体が無効な場合を分けます。後者は1356等になることもあります。SHOW CREATE VIEWで定義と参照先を確認し、1054だけに限定したりDDLで作り直したりしません。

検証範囲と公式資料(2026年10月26日)

Python3.12.10/Django5.2.18/mysqlclient2.2.7で、SQLiteメモリDBとPC上の専用MySQL8.4.13を使い、同じORMモデルとSQLを比較しました。MySQLは一意名/labelの新規container、データはtmpfs、接続はPCの127.0.0.1の自動割当ポートだけで、既存DB/volume/資格情報は使いません。暗黙id・明示pk/db_column・managed=False・遅延評価/投影/raw/inspectdb、実際の1054/1146/1356、表別名・相関・JOIN重複・集計別名を検査しました。

失敗SQLが診断ログへ必ず完全に入るとも限りません。検体ではCaptureQueriesContextのSQL欄がNoneの文字列表現になる失敗経路もあり、コンパイル結果と実行時の番号を別に照合しました。SQL/paramsやエラー文には秘密値・スキーマ情報が含まれ得るため、公開ページやddへ出しません。

実アプリの全モデル/関連・複合キー・権限・全DB版/sql_mode、実サーバーのmigration・性能・Laravel/Doctrineの動作は未検証です。危険な旧例の保持は実行の推奨ではありません。SQL計測はDjangoのSQLログと計測、ページングはDjangoのLIMIT/OFFSETを参照してください。

公式資料:Djangoの自動主キー、managed/db_table、db_column、既存DBとinspectdb、rawの主キー、MySQLのエラー番号/SQLSTATE、相関サブクエリ、識別子と別名。

子ページ

子ページはありません

同階層のページ
  1. Invalid HTTP_HOST header: '...'. You may need to add '...' to ALLOWED_HOSTS
  2. CommandError: You must set settings.ALLOWED_HOSTS if DEBUG is False.
  3. django.utils.datastructures.MultiValueDictKeyError
  4. Forbidden (403) CSRF verification failed. Request aborted.
  5. ModuleNotFoundError: No module named 'MySQLdb'
  6. WARNINGS: ?: (mysql.W002) MySQL Strict Mode is not set for database connection
  7. Unknown column 'table_name.id' in 'field list'
  8. RuntimeError: Model class ~ doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS.
  9. get() returned more than one MynumberRegist -- it returned 2!
  10. django.db.utils.OperationalError: (2006, "Can't connect to MySQL server")
  11. 'include' is not defined