66 人が閲覧(直近 30 日) データベース

データベースの属性とは?カラム・フィールド・項目の違いと種類・制約をSQL例で解説

データベースの属性とは?カラム・フィールド・項目の違いと種類・制約をSQL例で解説

データベースの「属性」とは、テーブルの列が表すデータ項目の種類のことです。usersテーブルなら、ユーザー名・メールアドレス・登録日時の一つひとつが属性にあたります。ただし同じ「列」でも、設計書では属性、SQLではカラム、Accessではフィールド、仕様書では項目と呼び名が変わり、phpMyAdminの画面にはUNSIGNEDなどを選ぶ「属性」という別の欄まであります。この記事は呼び名の違いを製品ごとに整理したうえで、属性の種類・データ型・キー・制約、既存テーブルの属性一覧を取り出すSQLまでを、PostgreSQL 18.6とMySQL 8.4.8で実行した結果つきで説明します。データベース全体の種類と選び方はデータベースとは?種類・DBMS・RDBとNoSQLの選び方を実装目線で解説で整理しています。

まとめ:データベースの属性の定義とカラム・フィールドとの違い・設計判断の要点

  • 属性とは、テーブルの各行が共通して持つデータ項目の種類。関係モデルでは関係(テーブル)の見出しを構成する要素で、1行分の値の組をタプル、属性の数を次数と呼ぶ。
  • 属性とカラムは同じ列を指す。属性は設計・理論の呼び名、カラムはSQLでの呼び名。項目は仕様書での言い換え。
  • フィールドは文脈で意味が割れる。Microsoft Accessでは列そのもの(属性と同じ粒度)、入力フォームや表計算の比喩では1つのセルを指す。
  • phpMyAdminの「属性」欄は列の付加指定(BINARY・UNSIGNED・UNSIGNED ZEROFILL・on update CURRENT_TIMESTAMP)を選ぶ欄。MySQL 8.4ではUNSIGNEDを整数に付ける用途とon update以外は非推奨の警告が出る。
  • 属性には種類(単一値・複合・多値・導出・キー属性)とデータ型がある。金額は浮動小数でなく numeric(DECIMAL)で持ち、導出属性は生成列で持てる。
  • NOT NULL・UNIQUE・CHECK・外部キーをテーブル定義に設定すれば、値の整合性をデータベース側で守れる。MySQL 8.4では列定義内のREFERENCESは無視されるので、外部キーは独立したFOREIGN KEY句で書く。単一参照テーブル・カンマ詰め・EAVは、型や参照の整合性を通常の列定義だけで表しにくくなるので避ける。
  • 既存システムの属性は information_schema.columns で一覧にできる。属性設計の見直しやDB移行はここから始める。

用語を確認する場合は次節へ、phpMyAdminの画面操作を調べる場合は「属性」欄の節へ進んでください。

データベースの属性とは:テーブルの列が表すデータ項目と定義域の関係

属性は、テーブルの縦の列に入る値が「何を表すか」を決めるものです。社員テーブルなら社員番号・氏名・所属部署・入社日がそれぞれ属性で、すべての行がこの4つを共通して持ちます。氏名の列にメールアドレスを混ぜないように、1つの属性は1つの意味だけを担当します。

下のusersテーブルでは、id・username・email・created_atの4つが属性です。各行はこの4属性の値の組で1人のユーザーを表します。

CREATE TABLE users (
    id         INT PRIMARY KEY,
    username   VARCHAR(50)  NOT NULL,
    email      VARCHAR(100) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

このSQLはPostgreSQL 18.6とMySQL 8.4.8のどちらでもそのまま通ります。列名の右に書いた型と制約が、その属性に入ってよい値の範囲を決めています。

関係モデルにおける属性の定義と定義域(ドメイン)の役割

関係モデル(リレーショナルモデル)では、テーブルを「関係(リレーション)」と呼び、その見出しを構成するのが属性です。各属性には定義域(ドメイン)があり、属性が取りうる値の範囲を表します。created_atの定義域は日時、idの定義域は整数、という具合です。

SQLでは型と制約の組み合わせが定義域の実装にあたります。PostgreSQLには CREATE DOMAIN があり、「0以上の金額」のような定義域に名前を付けて複数のテーブルで使い回せます。属性と定義域を先に決めておけば、後段の型や制約の判断がぶれません。

属性・タプル・次数・濃度・属性値の対応と試験で問われる用語の区別

関係モデルの用語は、表の見た目に置き換えると覚えやすくなります。基本情報技術者試験で問われるのもこの対応です。

関係モデルの用語 表で見たときの位置 usersテーブルでの例
関係(リレーション) テーブル全体 users
属性 列(見出し) id・username・email・created_at
属性値 1つのセルの値 usernameの「yamada」
タプル(組) 行 1人分のユーザー情報
次数 属性の数 4
濃度(基数) タプルの数 登録ユーザー数

次数は設計で決まり、運用中はほとんど変わりません。濃度は行が増えるたびに変わります。属性を1つ足すと次数が1増え、既存の全タプルにその属性値の欄ができます。列追加のたびに「既存行に何を入れるか」を決める必要があるのはこのためです。

属性とカラム・フィールド・項目・レコードの違い:製品と文脈で変わる呼び名

これらは重なりの大きい用語で、区別の基準は「どの立場から呼んでいるか」です。注意が要るのはフィールドで、製品によって列を指したりセルを指したりします。

用語 指すもの 主に使う場面
属性 列が表すデータ項目の種類 設計・関係モデル・ER図
カラム(列) テーブルの列そのもの SQL・DBMSのマニュアル
項目 属性の言い換え 仕様書・テーブル定義書
フィールド 列、または1つのセル Access・フォーム・表計算
レコード(行) 1件分の値の組 SQL・Access・業務
プロパティ 対象が持つ性質 GIS・ツール類

設計やデータモデルの話では「属性」、SQLでテーブルを作る話なら「カラム」で通じます。属性をエンティティの枠内に並べる形式のER図では、その項目がカラム設計の出発点になります。複合属性・多値属性・導出属性は、分割や別テーブル化、計算による取得も検討します。設計書とDDLで呼び名が分かれていても、指しているものは同じです。

フィールドは列かセルか:Accessの定義と入力フォームの用法

Microsoft Accessでは、フィールドは列そのものです。Microsoftサポートのデータベース設計の基本は「各行は正しくは レコード と呼ばれ、各列は正しくは フィールド と呼ばれます」と書き、フィールドを「すべてのレコードが持つ、アイテムの種類 (1 項目の情報)」と定義しています。これは属性の定義とほぼ同じです。

一方、Webの入力フォームの入力欄や、表計算ソフトにたとえた解説では、フィールドは1件のレコードの中の1つの値(セル)を指します。「カラムとフィールドの違い」を聞かれたら、Accessの文脈なら同じもの、フォームやセルの文脈ならカラムは縦一列・フィールドはその中の1マス、と答えるのが正確です。「フィールドに入力する」という言い方だけでは、列と個々の値を区別できません。迷ったら使っている製品と画面を確かめ、列の定義の話か1件の値の話かを言葉にして確認します。

カラム型(列指向)データベースは別の話:「カラム」が付く用語の取り違え

「カラム型データベース」「列指向データベース」は、属性の呼び名ではなくデータの保存方式の名前です。行単位で保存する通常のRDBに対し、同じ列の値をまとめて保存し、集計で必要な列だけを読む方式を指します。分析用のデータベースが採用しており、Google CloudのBigQueryのストレージ解説も、テーブルデータを列指向形式で各列を別々に保存すると説明しています。

列指向でもテーブルの列が属性であることは変わらず、違うのはディスク上の並べ方です。分析基盤の選び方はデータウェアハウス(DWH)とは?仕組み・製品比較・選び方で扱っています。

phpMyAdmin・MySQLの「属性」欄:UNSIGNED・ZEROFILL・BINARY・on updateの意味

MySQLを触っていて「属性」という言葉に出会う場面の多くは、phpMyAdminのテーブル構造の編集画面です。ここでの「属性」は列そのものではなく、型に付け足す指定を選ぶ欄を指します。phpMyAdminのソースのTypes::getAttributes()を見ると、選択肢は空欄・BINARY・UNSIGNED・UNSIGNED ZEROFILL・on update CURRENT_TIMESTAMPの5つで、接続先がMariaDB 10.1以上のときだけCOMPRESSED=zlibが加わります。日本語版の画面ではこの欄の英語名 Attributes が「属性」と訳されています。

選択肢 意味 MySQL 8.4.8での扱い
UNSIGNED 負の数を禁止 整数は可・DECIMALは非推奨
UNSIGNED ZEROFILL 桁数まで0で埋めて表示 非推奨(警告)
BINARY 大文字小文字を区別して比較 非推奨・_bin照合順序に置換
on update CURRENT_TIMESTAMP 更新時に現在時刻を自動設定 使用可

MySQL 8.4.8で次のテーブルを作ると、4件の警告が返りました。警告の詳細は、CREATE TABLEの直後に SHOW WARNINGS; を実行して確認できます。下のコメントでは長い警告文の一部を省略しています。

CREATE TABLE items (
  id         INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  code       INT(5) UNSIGNED ZEROFILL,
  price      DECIMAL(10,0) UNSIGNED,
  stock      INT UNSIGNED NOT NULL DEFAULT 0,
  name       VARCHAR(50) BINARY,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Warning 1681: The ZEROFILL attribute is deprecated and will be removed in a future release.
--   Use the LPAD function to zero-pad numbers, or store the formatted numbers in a CHAR column.
-- Warning 1681: Integer display width is deprecated and will be removed in a future release.
-- Warning 1681: UNSIGNED for decimal and floating point data types is deprecated ...
-- Warning 1287: 'BINARY as attribute of a type' is deprecated ...
--   Please use a CHARACTER SET clause with _bin collation instead

警告が出ても列は作られ、ZEROFILLを付けたcodeに42を入れると 00042 と表示されます。SHOW CREATE TABLE で確かめると、BINARYを付けたnameは CHARACTER SET utf8mb4 COLLATE utf8mb4_bin に書き換わっていました。BINARYは型ではなく照合順序の指定に変換される、ということです。UNSIGNEDのstockに-1を入れる更新は、既定のSQLモード(STRICT_TRANS_TABLESを含む)では ERROR 1264 (22003): Out of range value for column 'stock' で止まりました。updated_atは、同じ行の他の列の値が実際に変わると自動更新されます。他の列に同じ値を再設定するだけでは更新されません。

新しく作るテーブルでは、整数のUNSIGNEDとon update CURRENT_TIMESTAMPだけを使い、ゼロ埋めは表示側の LPAD() で、大文字小文字の区別は COLLATE utf8mb4_bin で書くのが無難です。非推奨の扱いはMySQL 8.4リファレンスの数値型属性にも書かれています。PostgreSQLにはUNSIGNEDもZEROFILLも無く、負の数の禁止は CHECK (stock >= 0) で表します。両者の型の違いはPostgreSQLとMySQLの違いを徹底比較|性能・データ型・全文検索・移行と使い分けで比べています。

属性の種類とデータ型:単一値・複合・多値・導出属性と型の選び方

属性の種類は、値の個数、構成要素、導出方法、識別上の役割という観点で整理します。これらは排他的な分類ではなく、たとえば生年月日は単一値属性であり、年齢の導出元にもなります。データ型は、それらとは別に値の表現を決める実装上の選択です。

属性の種類5つ:単一値・複合・多値・導出・キー属性の見分け方

  • 単一値属性:1つの値だけを持つ。生年月日など、多くの属性はこれ。
  • 複合属性:複数の意味を含む。住所(都道府県・市区町村・番地)が典型で、検索や集計に使うなら分割する。
  • 多値属性:1件に対し値が複数。電話番号を2つ以上持つ場合などで、別テーブルに切り出すのが基本。
  • 導出属性:他の属性から計算で求まる。生年月日から出す年齢や、単価×数量の小計が代表。ただし現在日付で変わる年齢は生成列にできない(PostgreSQL 18.6では generation expression is not immutable で拒否された)ので、取得時のクエリで計算する。
  • キー属性:行を一意に識別する。社員番号のように主キーの候補になる属性。

複合属性の構成要素や多値属性の各値を個別に検索・更新する場合、1列への詰め込みは文字列の分解や部分更新の処理を増やします。最初の設計で「この属性は単一値か」を確認しておけば、後からの作り直しを避けられます。

属性のデータ型:数値・文字列・日付・真偽の対応と金額にnumericを使う理由

値の性質 代表的な型 例の属性
整数 INT、BIGINT id、数量
小数(金額) DECIMAL(NUMERIC) 価格
文字列 VARCHAR、TEXT 氏名、本文
日付・日時 DATE、TIMESTAMP 生年月日、created_at
真偽 BOOLEAN(MySQLはTINYINT(1)の別名) 有効フラグ

金額に浮動小数を使わないのは誤差が出るからです。PostgreSQL 18.6で SELECT 0.1::double precision + 0.2::double precision = 0.3::double precision, 0.1::numeric + 0.2::numeric = 0.3::numeric; を実行すると、double precisionでは偽、numericでは真になりました。数値型の公式ドキュメントも、real と double precision は不正確な可変精度型だとして、金額のように正確さが要る値には numeric を勧めています。MySQLのBOOLEANはTINYINT(1)の別名で、8.4.8では5を入れても通ったため、0と1に限るならCHECKを足します。文字列は最大長が読める範囲ならVARCHAR、長文ならTEXTを選びます。型の名前や上限値はDBMSごとに違うため、移行を伴う設計では対象製品のドキュメントで1つずつ確認します。

導出属性を生成列で持つ方法:PostgreSQL 18のVIRTUALとSTORED

導出属性をアプリ側で毎回計算すると、画面ごとに計算式がずれる事故が起きます。式をテーブル定義に置けるのが生成列(generated column)です。PostgreSQL 18(2025年9月25日リリース)では、リリースノートのとおり読み出し時に計算する VIRTUAL が加わり、キーワードを省いたときの既定になりました。2026年10月時点の最新はこの18系の18.6です。

CREATE TABLE order_items (
    id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    unit_price NUMERIC(10,0) NOT NULL,
    quantity   INT NOT NULL CHECK (quantity > 0),
    subtotal   NUMERIC(12,0) GENERATED ALWAYS AS (unit_price * quantity) STORED,
    tax_incl   NUMERIC(12,0) GENERATED ALWAYS AS (unit_price * quantity * 1.1)
);
INSERT INTO order_items (unit_price, quantity) VALUES (1200, 3);
SELECT unit_price, quantity, subtotal, tax_incl FROM order_items;
--  unit_price | quantity | subtotal | tax_incl
--        1200 |        3 |     3600 |     3960

INSERT INTO order_items (unit_price, quantity, subtotal) VALUES (1200, 1, 999);
-- ERROR:  cannot insert a non-DEFAULT value into column "subtotal"
-- DETAIL:  Column "subtotal" is a generated column.

キーワードを書かなかったtax_inclは、システムカタログ pg_attribute.attgenerated が v(VIRTUAL)、STOREDと書いたsubtotalは s でした。検索条件や並べ替えに使う導出属性はSTORED、表示にしか使わないものはVIRTUALにするのが目安です。生成列には値を直接書き込めないので、計算式と違う値が紛れ込む余地もありません。生成列の公式ドキュメントは、VIRTUALをビューに、STOREDをマテリアライズドビューに近いものと説明しています。MySQLでもAS式とVIRTUAL・STOREDによる生成列の定義が使えます。ただし、上のDDLにあるIDENTITYによる採番はAUTO_INCREMENTへ書き換える必要があります。phpMyAdminの章で作ったitemsに subtotal DECIMAL(12,0) AS (price * stock) VIRTUAL を足すと、information_schemaのEXTRA列に VIRTUAL GENERATED と出ます。

キーとなる属性と制約:主キー・外部キー・NOT NULL・CHECKの書き方

行を識別したりテーブル同士をつないだりする「キー」の役割を担う属性があり、さらに各属性には値のルールを課す制約を付けられます。

主キー・外部キー・候補キーの役割の違いと主キーに選ぶ属性の条件

  • 主キー(PRIMARY KEY):行を一意に識別する1つ以上の属性。キー全体の重複と、構成する各属性のNULLを許さない。usersのidが該当する。
  • 候補キー:行を一意に識別でき、構成する属性をそれ以上減らせない属性の集合。1属性の場合も複数属性の場合もある。メールアドレスは、必須かつ重複不可という業務ルールがある場合に候補になる。
  • 外部キー(FOREIGN KEY):同じテーブルまたは別テーブルの主キーや一意キーを参照する、1つ以上の属性。下のpostsのuser_idがusersのidを指し、1対多の関係を作る。
CREATE TABLE posts (
    id      INT PRIMARY KEY,
    user_id INT NOT NULL,
    title   VARCHAR(200) NOT NULL,
    body    TEXT,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

postsのように外部キーを独立した FOREIGN KEY 句で書くのは、MySQLで確実に効かせるためでもあります。MySQL 8.4.8で user_id INT NOT NULL REFERENCES u(id) と列定義の中に書くと、エラーも警告も出ないまま外部キーは作られず、存在しない999を入れるINSERTも通りました。PostgreSQLはどちらの書き方でも外部キーになります。

主キーの候補が複数あるときは、値が変わらず短いものを選びます。このusersテーブルではemailにNOT NULLとUNIQUEを指定しているため、メールアドレスも候補キーになります。ただし変更されうるため、主キーには変更しない運用のidを選びます。業務上の意味を持たない連番やUUIDを主キーにする代理キーでは、UUIDの版によって挿入性能やインデックスの膨らみ方が変わります。採番方式の比較はUUIDv7とは?UUIDv4との違い・言語別の生成方法と主キー設計の注意点で扱っています。

属性の制約:NOT NULL・UNIQUE・DEFAULT・CHECKの選択基準

  • NOT NULL:NULLを禁止する。氏名やメールの必須列に付ける。ただし、空文字列や空白だけの値は別途検査する。
  • UNIQUE:重複を禁止する。メールアドレスを一意にしたいときに使う。
  • DEFAULT:未指定時の既定値を入れる。created_atに現在時刻を入れる用途が代表。
  • CHECK:値の条件を課す。数量を1以上に限る、といった範囲制限ができる。

制約の公式ドキュメントは、CHECKを最も汎用的な制約と位置づけ、UNIQUE制約を付けると対象列にB-treeの一意インデックスが自動で作られると説明しています。メールアドレスにUNIQUEを付ければ、重複の防止とメールでの検索の高速化が同時に手に入ります。

PostgreSQL 18からは、NOT NULLも名前を持つ制約として pg_constraint に記録されるようになりました。18.6でusersとpostsを作って制約一覧を引くと、主キー・一意・外部キーに加えて users_username_not_null のようなNOT NULL制約が並びます。17以前のカタログを読む移行スクリプトや監視SQLは、件数が増える点に注意が要ります。

制約だけでは守れない整合性もあります。複数の処理が同時に同じ行を更新する場面では、トランザクションとロックによる整合性管理が別途必要です。デッドロックの原因と解消方法はデッドロックとは?DBトランザクションで起きる原因・例と解消方法で扱っています。制約は値の条件・行間の一意性・テーブル間の参照整合性を守り、ロックやトランザクション分離は並行処理を制御します。

制約違反をpsqlで試す実行例:エラーメッセージから違反した属性を読む

制約が効いているかは、わざと違反する値を入れれば確かめられます。先のusersとpostsを作ったPostgreSQL 18.6で実行した結果です。

INSERT INTO users (id, username, email) VALUES (1, 'yamada', 'mail-001');

-- 1. NOT NULL違反:usernameを省略
INSERT INTO users (id, email) VALUES (2, 'mail-002');
-- ERROR:  null value in column "username" of relation "users" violates not-null constraint
-- DETAIL:  Failing row contains (2, null, mail-002, 2026-10-14 02:55:48.123029).

-- 2. UNIQUE違反:同じemailを登録
INSERT INTO users (id, username, email) VALUES (3, 'yamada2', 'mail-001');
-- ERROR:  duplicate key value violates unique constraint "users_email_key"
-- DETAIL:  Key (email)=(mail-001) already exists.

-- 3. 外部キー違反:存在しないuser_idで投稿
INSERT INTO posts (id, user_id, title) VALUES (1, 999, 'test');
-- ERROR:  insert or update on table "posts" violates foreign key constraint "posts_user_id_fkey"
-- DETAIL:  Key (user_id)=(999) is not present in table "users".

ERRORの行には違反した属性名か制約名が、DETAILの行には弾かれた値が入ります。制約名は付けなければ「テーブル名_列名_key」「テーブル名_列名_fkey」の形で自動で決まるため、本番のログから原因の属性をすぐ特定できます。チームで運用するなら CONSTRAINT users_email_uq UNIQUE (email) のように意味の分かる名前を付けておくと、ログがさらに読みやすくなります。

既存テーブルの属性一覧をSQLで確認する手順:information_schemaの使い方

引き継いだDBの調査、仕様書と実装のずれの確認、移行前の棚卸しでは、まず属性の一覧を取り出します。SQL標準の情報スキーマ(information_schema)を使えば、DBMSをまたいで同じ書き方で取れます。columnsビューの公式ドキュメントのとおり、このビューには、現在のユーザーが所有権または何らかの権限を持つ列の情報が表示されます。ctidなどのシステム列は含まれません。

SELECT table_name,
       ordinal_position AS no,
       column_name,
       data_type,
       character_maximum_length AS max_len,
       is_nullable,
       column_default
FROM   information_schema.columns
WHERE  table_schema = 'public'
  AND  table_name IN ('users', 'posts')
ORDER  BY table_name, ordinal_position;
--  table_name | no | column_name |          data_type          | max_len | is_nullable |  column_default
--  posts      |  1 | id          | integer                     |         | NO          |
--  posts      |  2 | user_id     | integer                     |         | NO          |
--  posts      |  3 | title       | character varying           |     200 | NO          |
--  posts      |  4 | body        | text                        |         | YES         |
--  users      |  1 | id          | integer                     |         | NO          |
--  users      |  2 | username    | character varying           |      50 | NO          |
--  users      |  3 | email       | character varying           |     100 | NO          |
--  users      |  4 | created_at  | timestamp without time zone |         | YES         | CURRENT_TIMESTAMP

MySQLでは table_schema にデータベース名を指定すれば同じSQLが通ります。MySQLで属性欄の指定まで見たいときは、COLUMN_TYPEとEXTRAの2列を足します。phpMyAdminの章で作ったitemsでは、COLUMN_TYPEに int(5) unsigned zerofill、EXTRAに auto_increment や DEFAULT_GENERATED on update CURRENT_TIMESTAMP が出ました。UNSIGNED・ZEROFILL・on updateの指定はこの2列で確認できます。BINARY属性の効果はCOLLATION_NAMEで照合順序を確認しますが、元の指定がBINARYだったか、明示的なCOLLATEだったかは区別できません。

psqlなら \d users で、属性・型・NULL可否・既定値に加えてインデックスと外部キーまでまとめて表示されます。メタコマンドとSQLの使い分けはPostgreSQLのテーブル一覧を取得する方法|メタコマンドとinformation_schema・pg_catalogの使い分けにまとめています。一覧を取ったら、is_nullableがYESなのに業務上は必須の属性、金額なのにdouble precisionの属性、意味の読めない予備カラムの3つを探します。見つかれば属性設計を見直す余地があります。

良い属性設計の判断基準:1属性1事実と関数従属で置き場所を決める

属性設計の核は「1属性1事実」です。1つの属性は1つの意味だけを持ち、複数の事実を1列に混ぜません。第1正規形では、各属性に、その定義域の単一の値を持たせます。住所を「東京都千代田区…」と1列に入れると、都道府県別の集計に文字列の抽出・分類処理が必要になります。市区町村での絞り込みが要件にあるなら分け、要件に無ければ分けない、という判断も同じ基準から出てきます。

もう1つの軸が関数従属です。「ある属性の値が決まれば、別の属性の値が一意に決まる」関係を指し、決め手になる側を決定項と呼びます。社員番号が決まれば氏名が決まるなら、社員番号は氏名の決定項です。正規化とは、この関数従属を手がかりに属性を正しいテーブルへ振り分ける作業です。迷ったら「この属性は、このテーブルの主キーだけに従属しているか」を問います。第1〜第3正規形の分解手順と、あえて正規化を崩す場面はデータベース正規化とは?第1〜第3正規形の手順と非正規化の判断で例題付きで解説しています。

属性設計で避けるべきアンチパターン:単一参照テーブル・カンマ詰め・EAV

原則の裏返しとして、やってはいけない設計があります。どれも、業務上の意味に対応する型や参照制約を単純な列定義で表しにくくなる点に注意が必要です。

単一参照テーブルで複雑になる参照定義とカンマ詰めで効かない外部キー

単一参照テーブルは、区分値や種別といった性質の異なるマスタを1つの汎用テーブルにまとめる手法で、One True Lookup Tableとも呼ばれます。下の設計でも、参照側に区分種別と値を持たせ、複合外部キーと区分種別を固定するCHECK制約を組み合わせれば、異なる区分への参照を防げます。ただし、参照する各テーブルにその定義が必要になります。

-- 避けたい設計:意味の異なる区分を1テーブルに集約
CREATE TABLE all_codes (
    code_type  VARCHAR(50),
    code_value VARCHAR(50),
    label      VARCHAR(100),
    PRIMARY KEY (code_type, code_value)
);

区分ごとに専用のマスタテーブルを作り、外部キーで縛る設計を採るべきです。専用マスタなら単一列の外部キーだけで参照先が決まり、参照側に区分種別の列とCHECKを毎回足す必要がありません。テーブルは増えても、定義が単純なぶん誤りが入りにくくなります。

多値のカンマ詰めも同類です。「タグ」属性に 赤,青,緑 のように複数値を入れると、特定タグでの検索や件数集計が文字列処理になり、外部キーも効きません。多値属性は中間テーブルに切り出します。意味が定義されないまま肥大化する「予備カラム」も避けます。現場で繰り返される失敗の一覧はDB設計のアンチパターン一覧にまとめています。

EAV(属性を行で持つ設計)の型・必須性・参照整合性の制限と採否

EAV(Entity-Attribute-Value)は、属性そのものを列ではなく行として持つ設計です。「商品ID・属性名・値」の3列だけのテーブルに、色もサイズも重量も1行ずつ入れます。属性を後から自由に足せるため、商品ごとに項目が違うECサイトや、顧客がフォーム項目を増やせるSaaSで採用されがちです。

CREATE TABLE product_attributes (
    product_id INT          NOT NULL,
    attr_name  VARCHAR(50)  NOT NULL,  -- 'color', 'weight' など
    attr_value VARCHAR(255),           -- 型はすべて文字列になる
    PRIMARY KEY (product_id, attr_name)
);
INSERT INTO product_attributes VALUES (1, 'color', 'red'), (1, 'weight', '-5kg'), (2, 'color', 'blue');
SELECT product_id,
       max(attr_value) FILTER (WHERE attr_name = 'color')  AS color,
       max(attr_value) FILTER (WHERE attr_name = 'weight') AS weight
FROM product_attributes GROUP BY product_id ORDER BY 1;
--  product_id | color | weight
--           1 | red   | -5kg
--           2 | blue  |

PostgreSQL 18.6で実行すると、重量に「-5kg」という負の文字列が入り、商品2は重量が無いまま通りました。このDDLには重量の形式や範囲を検査するCHECKが無いからです。属性名に応じたCHECKは追加できますが、重量の行自体が必ず存在することは、attr_valueのNOT NULLだけでは保証できません。1商品を横に並べるだけで、属性の数だけ集約か自己結合が要ります。検索・集計の条件に使う属性や、必須性・参照整合性を厳密に管理する属性は、EAVではなく通常の列に置くべきです。可変の項目をJSONBで持つ方法もあり、JSONBは検索用のGINインデックスも使えるので、項目の可変性と検索・更新の要件で採否を決めます。JSONBと通常の列の境界はPostgreSQL JSONBの使い方|json型との格納差・GIN索引と正規化列の設計境界で整理しています。

属性と関係(1:1・1:N・N:N)の対応:外部キー属性の置き場所

外部キー属性をどこに置くかで、テーブル間の関係の種類が決まります。

  • 1:1:社員と社員証のように、対応先を高々1件に制限する関係。一方に相手の主キーを参照する外部キーとUNIQUEを付ける。必須ならNOT NULLも付け、すべての社員に社員証が存在することは別途保証する。
  • 1:N:ユーザーと投稿のように1件が複数を持つ。「多」側(投稿)に外部キー属性を置く。
  • N:N:著者と書籍のように双方が複数を持つ。両者の主キーを属性に持つ中間テーブルで分解する。
-- 検証用の参照先テーブルを先に作成する
CREATE TABLE authors (id INT PRIMARY KEY);
CREATE TABLE books (id INT PRIMARY KEY);

-- N:N を中間テーブルで分解する
CREATE TABLE author_book (
    author_id INT,
    book_id   INT,
    PRIMARY KEY (author_id, book_id),
    FOREIGN KEY (author_id) REFERENCES authors(id),
    FOREIGN KEY (book_id)   REFERENCES books(id)
);

PostgreSQL 18.6で、authorsに無いauthor_id=3を中間テーブルに入れると author_book_author_id_fkey 違反で拒否されました。N:Nを中間テーブルなしで表そうとすると、カンマ詰めに逆戻りします。関係の種類を見極めてから外部キー属性の置き場所を決める、という順序を守ってください。多重度をER図でどう描くかはER図とは?書き方・エンティティやリレーション・カーディナリティで解説しています。

属性を決めるデータベース設計の手順:要件からテーブル定義書まで6工程

  1. 要件を洗い出し、システムが扱う「もの・こと」を言葉で書き出す。
  2. そこからエンティティ(テーブルの候補)を抽出する。「ユーザー」「投稿」「商品」など。
  3. 各エンティティが持つ属性を列挙し、1属性1事実になっているか確認する。
  4. 属性ごとにデータ型と制約(NOT NULL・UNIQUE等)を決める。
  5. 関数従属を手がかりに正規化し、属性を正しいテーブルへ振り分ける。
  6. 主キー・外部キーを定め、テーブル定義書にまとめる。

1〜3の工程は、要件定義の段階でER図に起こすのが一般的です。要件定義から設計までに作る図の種類と担当は要件定義と設計で作る7つの図|画面遷移図・ER図とデザイナーの担当範囲にまとめています。テーブル定義書の叩き台は、information_schemaで取った属性一覧を表計算ソフトに貼れば作れます。

属性設計を社内で進めるか外部に相談するか:判断の条件と見送る場面

社内で進める条件は、担当者が業務ルールを型・キー・制約に落とし込み、変更の影響と復旧手順まで検証できることです。テーブル数や利用者の範囲だけでは判断できません。CRUD中心の小規模アプリでも、誤更新の業務影響や社内のレビュー体制を確認して、外部レビューの要否を決めます。

一方、稼働中のシステムでカンマ詰め・EAV・単一参照テーブルがすでに使われていてデータを壊さずに列へ戻す場合、Accessや表計算の台帳を型と制約のあるRDBへ移す場合、MySQLからPostgreSQLのように製品をまたいでUNSIGNEDやZEROFILLの置き換えが要る場合は、属性設計の段階で専門家のレビューを入れる価値があります。比べる費用は、レビュー料と、変換・検証・手戻りの工数の合計です。どれも属性を1つ誤ると既存データの変換をやり直すことになる作業です。information_schemaで取った属性一覧が手元にあれば、相談の初回から具体的な見積もりに進めます。既存DBの棚卸しから移行計画まではデータベース設計・移行支援で対応しています。

よくある質問

データベースの属性とは何ですか?

テーブルの列が表す、各レコードが共通して持つデータ項目の種類です。社員テーブルなら社員番号・氏名・入社日がそれぞれ属性です。関係モデルでは関係(テーブル)の見出しを構成する要素を指し、各属性には取りうる値の範囲である定義域(ドメイン)があります。

属性とカラムの違いは何ですか?

指すものは同じ列で、呼ぶ立場が違います。属性は設計やデータモデルの言葉、カラムはSQLやDBMSの言葉です。設計の会話では「属性」、テーブル作成の会話では「カラム」と使い分ければ通じます。

カラムとフィールドの違いは何ですか?

Microsoft Accessではフィールドは列そのもので、カラムと同じものを指します。入力フォームや表計算にたとえた説明では、フィールドは1件のレコードの中の1つの値(セル)を指し、カラムはその縦一列です。どちらの意味かは文脈で判断します。

phpMyAdminの「属性」欄には何を設定しますか?

型に付け足す指定を選ぶ欄で、選択肢はBINARY・UNSIGNED・UNSIGNED ZEROFILL・on update CURRENT_TIMESTAMPです。MySQL 8.4では整数のUNSIGNEDとon update CURRENT_TIMESTAMP以外は非推奨の警告が出るため、通常は空欄のままにし、負の数を禁止したい整数列だけUNSIGNEDを選びます。

属性と項目は同じ意味ですか?

多くの場面で同じ意味です。仕様書やテーブル定義書では、属性を「項目」と言い換えるのが一般的です。項目名・型・桁数・必須かどうかを並べた一覧は、属性一覧と同じものです。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.07.21 テックブログ Apache Tomcatの脆弱性一覧【2026年10月】最新版11.0.27・10.1.61・9.0.123とCVE-2026-59083ほかの影響確認・対応手順
  2. 2026.10.21 テックブログ NEC委託先の不正アクセスとは|顧客システム暗号化・情報漏えいの経緯と委託元が点検する正規アカウント
  3. 2026.10.21 テックブログ ふるさとチョイス不正アクセスで445万件流出:対象確認・パスワード変更と再開時期
  4. 2026.10.22 テックブログ Tomcatバージョン一覧とサポート期限【2026年10月】9.0系は2027年3月末で終了・9.1系と11.0系への移行判断
  5. 2024.07.05 コラム XAMPPの脆弱性とは?危険性とCVE-2024-4577への対策をわかりやすく解説【2026年10月】

RELATED POSTS 関連記事

目次