PHP MySQL 创建数据表

CREATE TABLE, choosing column types, keys and indexes.

Beginner
Key PointsToo-small types force migrations, too-large waste space; `VARCHAR(255)` is not a default.

CREATE TABLE

SQL
CREATE TABLE IF NOT EXISTS users (
    id         INT UNSIGNED NOT NULL AUTO_INCREMENT,
    name       VARCHAR(50)  NOT NULL,
    email      VARCHAR(100) NOT NULL,
    age        TINYINT UNSIGNED NULL,
    status     TINYINT      NOT NULL DEFAULT 1,
    created_at DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    UNIQUE KEY uk_email (email),
    KEY idx_status_created (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
PHP
<?php
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

$sql = file_get_contents(__DIR__ . "/create_users.sql");
$pdo->exec($sql);
echo "表 users 已创建\n";

$cols = $pdo->query("SHOW COLUMNS FROM users")->fetchAll();
echo "字段数: " . count($cols) . "\n";
foreach ($cols as $c) {
    echo $c["Field"] . " " . $c["Type"] . "\n";
}
Output
表 users 已创建
字段数: 7
id int unsigned
name varchar(50)
email varchar(100)
age tinyint unsigned
status tinyint
created_at datetime
updated_at datetime

Choosing column types

NeedTypeNote
主键BIGINT UNSIGNED AUTO_INCREMENTINT caps at ~2.1B; use BIGINT for large tables
短字符串VARCHAR(n)按实际长度定,别一律 255
长文本TEXT / LONGTEXT不能有默认值,索引要前缀
金额DECIMAL(10,2) 或整数分**绝不用 FLOAT**
布尔TINYINT(1)MySQL 没有真正的 bool
时间DATETIME / TIMESTAMPTIMESTAMP 有时区转换,DATETIME 没有
JSONJSONMySQL 5.7+,可建函数索引
枚举VARCHAR + 应用层校验ENUM 改值要 ALTER TABLE
FLOAT/DOUBLE for money accumulates rounding error. **Use DECIMAL or integer cents** — an accounting rule.

Indexes

SQL
-- 主键:唯一且非空,聚簇索引
PRIMARY KEY (id)

-- 唯一索引:除了加速还保证唯一
UNIQUE KEY uk_email (email)

-- 普通索引:加速 WHERE / ORDER BY
KEY idx_status_created (status, created_at)

-- 联合索引:最左前缀原则
-- (status, created_at) 能加速 status=1,也能加速 status=1 AND created_at>...
-- 但不能加速只用 created_at 的查询

Column order in a composite index matters: put the most selective first. (status, created_at) works as a prefix even though status has few values.

Altering tables

PHP
<?php
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

// 加字段
$pdo->exec("ALTER TABLE users ADD COLUMN phone VARCHAR(20) NULL AFTER email");
echo "已加字段 phone\n";

// 加索引
$pdo->exec("ALTER TABLE users ADD KEY idx_phone (phone)");
echo "已加索引 idx_phone\n";

$cols = $pdo->query("SHOW COLUMNS FROM users")->fetchAll(PDO::FETCH_COLUMN);
echo "字段数: " . count($cols) . "\n";
Output
已加字段 phone
已加索引 idx_phone
字段数: 8
ALTER TABLE on a big table locks or hammers IO. MySQL 8's ALGORITHM=INPLACE, LOCK=NONE helps, but adding indexes still costs. Rehearse on a replica first.

Table checklist

  • Always a primary key —— InnoDB creates a hidden one you cannot control
  • **utf8mb4** —— consistent across database, table and connection
  • **NOT NULL with defaults** —— nullable columns index poorly and invite bugs
  • **created_at / updated_at** —— invaluable when debugging
  • Foreign keys or app-level checks —— FKs cost locks under load; many teams validate in the app

Friends & Partners