一、前端眼中的"数据请求" vs 后端的"数据查询"
1.1 前端思维模型
作为前端,你习惯的数据获取方式是:
javascript
// 前端:调用 API,拿到 JSON
const res = await fetch('/api/users?status=active&page=1');
const data = await res.json();
// data = [{ id: 1, name: '张三', ... }, { id: 2, name: '李四', ... }]
// 然后在前端内存中做筛选/排序
const vipUsers = data.filter(u => u.level === 'vip');
前端的数据操作特点:
- 数据已经在内存中(JS 数组/对象)
- 数据量小(单次请求通常几十到几百条)
- 用
filter、map、sort、find等方法操作
1.2 后端 SQL 思维模型
后端面对的根本问题是:数据在磁盘上,不在内存里,总数据量可能上百万甚至上亿条。
sql
-- 后端:直接告诉数据库你想要什么,数据库帮你精确查找
SELECT id, name, email
FROM users
WHERE status = 'active' AND level = 'vip'
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;
核心区别:前端操作的是内存数组,SQL 操作的是磁盘上的数据库。
如果后端把 100 万条数据全部加载到内存再筛选:
- 内存爆炸
- 网络传输几百 MB,超时
- 无意义的 CPU 消耗
SQL 的作用就是:只把你需要的那几条数据从磁盘中捞出来,其他数据根本不碰。
1.3 类比理解
sql
前端思维(不行的):
把整个仓库的货搬出来 → 在门口挑你要的那几件 → 把剩下的搬回去
SQL 思维(正确的):
直接走进仓库 → 走到对应货架 → 拿走你要的那几件 → 出来
SQL 就是那个"知道货架在哪、怎么走的导航系统"
二、SQL 是什么
2.1 定义
SQL(Structured Query Language,结构化查询语言)是操作关系型数据库 的标准语言。它本质上是一种声明式语言------你告诉数据库"我要什么",而不是"怎么去拿"。
ini
声明式(SQL): SELECT name FROM users WHERE id = 1;
命令式(JS): const user = users.find(u => u.id === 1); const name = user.name;
2.2 关系型数据库是什么
将数据以**表格(Table)的形式组织,表与表之间通过外键(Foreign Key)**建立关联:
scss
┌──────────────────────┐ ┌──────────────────────┐
│ users │ │ orders │
├──────────────────────┤ ├──────────────────────┤
│ id (主键 Primary Key) │←──┐ │ id (主键) │
│ name │ └──│ user_id (外键) │
│ email │ │ product_name │
│ created_at │ │ amount │
└──────────────────────┘ │ created_at │
└──────────────────────┘
一个 user 可以有多个 order(一对多关系)
通过 user_id 关联
类比前端:
- 表 ≈ 一个 JSON 数组
- 行(Row)≈ 数组中的一项(一个对象)
- 列(Column)≈ 对象的 key
- 主键 ≈ 唯一的
id字段 - 外键 ≈ 引用另一个数组中的对象 id
2.3 SQL 能做什么(CRUD)
| 操作 | SQL 关键字 | 前端类比 |
|---|---|---|
| Create(创建) | INSERT INTO |
array.push({...}) |
| Read(读取) | SELECT |
array.filter(...) / array.find(...) |
| Update(更新) | UPDATE |
obj.name = 'new' |
| Delete(删除) | DELETE |
array.splice(index, 1) |
三、SQL 本质上在查什么
3.1 数据存储的物理现实
less
前端视角:
data = [{ id: 1, name: '张三' }, { id: 2, name: '李四' }]
数据已经在内存里,用 JS 引擎直接操作
后端视角:
数据在磁盘文件里(例如 /var/lib/mysql/mydb/users.ibd)
磁盘结构(简化):
┌─────────────────────────────────────────────┐
│ 数据页1 (16KB) │
│ ┌─────────────────────────────────────────┐ │
│ │ Row: 1 | 张三 | zhangsan@mail.com | ... │ │
│ │ Row: 2 | 李四 | lisi@mail.com | ... │ │
│ │ Row: 3 | 王五 | wangwu@mail.com | ... │ │
│ │ ... │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 数据页2 (16KB) │
│ ... │
└─────────────────────────────────────────────┘
B+ 树索引(一个独立的树状结构,快速定位数据在哪一页):
[100]
/ \
[50] [150]
/ \ / \
[1..][51..][101..][151..] ← 叶子节点指向实际数据页
3.2 SQL 的本质:让数据库引擎代劳
当你写 SELECT * FROM users WHERE id = 520,实际上发生的事:
markdown
1. 解析 SQL → 数据库把 "我要查 users 表 id=520 的行" 翻译成执行计划
2. 走索引 → 从 B+ 树根节点开始,50 → 100 → ... 快速定位到 id=520 所在的数据页
3. 读磁盘 → 只读取包含 id=520 的那个 16KB 数据页到内存
4. 过滤 → 在内存的 16KB 数据页中找到 id=520 的那一行
5. 返回 → 把这一行的数据通过网络返回给应用
核心本质:SQL 是一个让数据库引擎帮你做"大海捞针"的 DSL(领域特定语言),你不需要关心数据在哪一页、怎么做二分查找、怎么索引跳转。
3.3 索引(Index):SQL 快速查找的核心
sql
-- 没有索引:全表扫描,100万条数据一条条比对,耗时秒级
SELECT * FROM users WHERE email = 'zhangsan@mail.com';
-- 有索引:从 B+ 树直接定位,通常 3~5 次磁盘 IO,耗时毫秒级
CREATE INDEX idx_email ON users(email);
前端类比:
arduino
无索引 ≈ 在一个没有目录的 1000 页书里找"珠穆朗玛峰"
有索引 ≈ 翻到书末尾的索引页,看到"珠穆朗玛峰 → 第 342 页",直接翻过去
索引是后端性能优化的核心。绝大部分慢查询,都是因为没走索引 或者索引设计不合理。
四、什么场景下必须用 SQL 而不是内存操作
4.1 必须用 SQL 的典型场景
| 场景 | 为什么不能用内存操作 | SQL 怎么做 |
|---|---|---|
| 用户登录验证 | 千万用户,不能全加载到内存 | SELECT * FROM users WHERE email=? AND password_hash=? |
| 分页列表 | 百万条数据,只需 20 条 | SELECT ... LIMIT 20 OFFSET 0 |
| 多表关联查询 | 用户 + 订单 + 商品,三表联动 | JOIN 让数据库做关联,而不是在代码里循环嵌套 |
| 聚合统计 | 计算总数/平均值/分组,数据库有原生支持 | SELECT dept, COUNT(*), AVG(salary) FROM users GROUP BY dept |
| 事务操作 | 转账:A 扣钱 + B 加钱,要么都成功要么都失败 | BEGIN; UPDATE ...; UPDATE ...; COMMIT; |
| 搜索/排序/过滤 | 数据库有索引,比内存操作快几个数量级 | WHERE email LIKE '%keyword%' ORDER BY created_at DESC |
4.2 可以用内存操作的场景(不需要 SQL)
| 场景 | 说明 |
|---|---|
| 配置/字典数据 | 几百条,启动时加载到内存,定时刷新 |
| 会话/Token | 存在 Redis(内存数据库),不存 MySQL |
| 计数器/排行榜 | Redis 的 INCR/ZADD 比 SQL 的 UPDATE COUNT 快很多 |
| 临时计算结果 | 已经在内存中的数据,直接操作 |
核心原则:数据量小 + 需要频繁访问 → 考虑缓存/内存;数据量大 + 持久存储 → 用 SQL + 数据库。
五、SQL 的核心语法速览
5.1 基础查询
sql
-- 查所有(★ 生产环境慎用,数据量大时可能拖垮数据库)
SELECT * FROM users;
-- 指定字段(推荐)
SELECT id, name, email FROM users;
-- 条件筛选
SELECT * FROM users WHERE status = 'active' AND age > 18;
-- 排序
SELECT * FROM users ORDER BY created_at DESC;
-- 分页
SELECT * FROM users LIMIT 20 OFFSET 40; -- 第3页(跳过前40条,取20条)
-- 模糊搜索
SELECT * FROM users WHERE name LIKE '%张%'; -- % 是通配符
-- 去重
SELECT DISTINCT city FROM users;
-- 聚合
SELECT dept, COUNT(*) AS count, AVG(salary) AS avg_salary
FROM users
GROUP BY dept
HAVING count > 5; -- HAVING 过滤分组后的结果(WHERE 在分组前过滤)
5.2 多表关联
sql
-- INNER JOIN:只返回两表都匹配的行
SELECT u.name, o.product_name, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- 结果:只有下过订单的用户
-- LEFT JOIN:左表全保留,右表没匹配的填 NULL
SELECT u.name, o.product_name
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
-- 结果:所有用户,没订单的显示 NULL
前端类比:
javascript
// INNER JOIN ≈
const result = users
.filter(u => orders.some(o => o.user_id === u.id))
.map(u => {
const userOrders = orders.filter(o => o.user_id === u.id);
return userOrders.map(o => ({ name: u.name, product: o.product_name }));
}).flat();
// LEFT JOIN ≈
const result = users.map(u => {
const userOrders = orders.filter(o => o.user_id === u.id);
return userOrders.length > 0
? userOrders.map(o => ({ name: u.name, product: o.product_name }))
: [{ name: u.name, product: null }];
}).flat();
5.3 增删改
sql
-- 插入
INSERT INTO users (name, email, status) VALUES ('赵六', 'zhao@mail.com', 'active');
-- 批量插入(比逐条插入快很多)
INSERT INTO users (name, email) VALUES
('A', 'a@mail.com'),
('B', 'b@mail.com'),
('C', 'c@mail.com');
-- 更新(★ 忘写 WHERE 就全表更新,生产事故!)
UPDATE users SET status = 'inactive' WHERE id = 3;
-- 删除(★ 同理,忘写 WHERE 全删)
DELETE FROM users WHERE id = 3;
5.4 事务
sql
-- 转账:A 转 100 给 B
BEGIN; -- 开始事务
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- A 扣钱
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- B 加钱
COMMIT; -- 提交(确认)
-- 如果中间出错,执行 ROLLBACK(回滚,全部撤销)
ROLLBACK;
六、Go 语言操作 SQL 实战
6.1 标准库 database/sql + MySQL 驱动
Go 官方只提供接口,具体数据库需要引入驱动:
go
package main
import (
"database/sql"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql" // MySQL 驱动(init 注册)
)
func main() {
// 1. 连接数据库
dsn := "root:password@tcp(127.0.0.1:3306)/mydb?charset=utf8mb4&parseTime=true"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 连接池配置
db.SetMaxOpenConns(25) // 最大打开连接数
db.SetMaxIdleConns(10) // 最大空闲连接数
db.SetConnMaxLifetime(5 * time.Minute) // 连接最大存活时间
// 验证连接
if err := db.Ping(); err != nil {
log.Fatal(err)
}
// 2. 查询单条
var user User
err = db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", 1).
Scan(&user.ID, &user.Name, &user.Email)
if err == sql.ErrNoRows {
fmt.Println("用户不存在")
} else if err != nil {
log.Fatal(err)
}
// 3. 查询多条
rows, err := db.Query("SELECT id, name, email FROM users WHERE status = ? LIMIT ?", "active", 20)
if err != nil {
log.Fatal(err)
}
defer rows.Close() // ★ 必须关闭,否则连接泄漏
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
log.Fatal(err)
}
users = append(users, u)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
// 4. 插入
result, err := db.Exec(
"INSERT INTO users (name, email, created_at) VALUES (?, ?, NOW())",
"新用户", "new@mail.com",
)
if err != nil {
log.Fatal(err)
}
lastID, _ := result.LastInsertId()
fmt.Printf("新插入的 ID: %d\n", lastID)
// 5. 更新
_, err = db.Exec("UPDATE users SET status = ? WHERE id = ?", "inactive", 1)
if err != nil {
log.Fatal(err)
}
// 6. 删除
_, err = db.Exec("DELETE FROM users WHERE id = ?", 99)
if err != nil {
log.Fatal(err)
}
// 7. 事务
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
// defer tx.Rollback() // 如果 commit 之前出错,自动回滚
_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", 100, 1)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", 100, 2)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
if err := tx.Commit(); err != nil {
log.Fatal(err)
}
}
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
6.2 使用 GORM(Go 最流行的 ORM)
对于前端转后端的同学,GORM 的写法更像你熟悉的 JS 链式调用:
go
package main
import (
"fmt"
"log"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// 模型定义(对应数据库表)
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"column:name;type:varchar(100)"`
Email string `gorm:"uniqueIndex"`
Status string `gorm:"default:active"`
// gorm.Model 会自带 ID、CreatedAt、UpdatedAt、DeletedAt
}
func main() {
dsn := "root:password@tcp(127.0.0.1:3306)/mydb?charset=utf8mb4&parseTime=true"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
// 自动建表(开发环境用,生产环境用 Migration)
db.AutoMigrate(&User{})
// ────────── CRUD 操作 ──────────
// 查询单条
var user User
db.First(&user, 1) // 按主键查
db.Where("email = ?", "a@mail.com").First(&user)
// 查询多条
var users []User
db.Where("status = ?", "active").
Order("created_at DESC").
Limit(20).Offset(0).
Find(&users)
// 条件链式查询
db.Where("age > ?", 18).
Where("status = ?", "active").
Or("vip = ?", true).
Find(&users)
// 创建
newUser := User{Name: "张三", Email: "zhangsan@mail.com"}
db.Create(&newUser) // 创建后 newUser.ID 自动填充
// 批量创建
db.Create(&[]User{
{Name: "李四", Email: "lisi@mail.com"},
{Name: "王五", Email: "wangwu@mail.com"},
})
// 更新
db.Model(&user).Update("status", "inactive")
db.Model(&user).Updates(User{Name: "新名字", Status: "active"}) // 多字段更新
// 删除(GORM 默认软删除,设置 deleted_at)
db.Delete(&user, 1)
// 硬删除
db.Unscoped().Delete(&user, 1)
// ────────── 关联查询 ──────────
// 定义关联
type Order struct {
ID uint
UserID uint
ProductName string
Amount float64
User User `gorm:"foreignKey:UserID"` // 外键关联
}
// 预加载(解决 N+1 查询问题)
var orders []Order
db.Preload("User").Where("amount > ?", 100).Find(&orders)
// 2 条 SQL:
// SELECT * FROM orders WHERE amount > 100;
// SELECT * FROM users WHERE id IN (1, 2, 3); ← IN 查询,一次性加载所有关联用户
// ────────── 事务 ──────────
err = db.Transaction(func(tx *gorm.DB) error {
// 所有操作在事务中
if err := tx.Model(&user1).Update("balance", gorm.Expr("balance - ?", 100)).Error; err != nil {
return err // 返回 error 自动回滚
}
if err := tx.Model(&user2).Update("balance", gorm.Expr("balance + ?", 100)).Error; err != nil {
return err
}
return nil // 返回 nil 自动提交
})
}
// TableName 自定义表名
func (User) TableName() string {
return "users"
}
6.3 原生 SQL vs ORM 选择
| 原生 SQL(database/sql) | ORM(GORM) | |
|---|---|---|
| 学习成本 | 需要写 SQL | 像 JS 链式调用,上手快 |
| 控制力 | 100% 精确控制 | ORM 生成的 SQL 可能不够优 |
| 复杂查询 | 手写 SQL,灵活 | 复杂关联可能很绕 |
| 迁移成本 | 改业务=改 SQL | 改字段=改 struct tag |
| 防注入 | 手动用 ? 占位符 |
自动参数绑定 |
| 适合场景 | 对性能要求高、复杂查询 | CRUD 为主,快速开发 |
前端转后端建议: GORM 上手最快,语法像 JS Lodash/filter。但建议掌握原生 SQL,因为排查性能问题时你需要看懂数据库慢查询日志里的 SQL。
七、PHP 语言操作 SQL 实战
7.1 使用 PDO(PHP Data Objects)
php
<?php
class Database
{
private static ?PDO $instance = null;
// 单例连接
public static function getInstance(): PDO
{
if (self::$instance === null) {
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=mydb;charset=utf8mb4';
self::$instance = new PDO($dsn, 'root', 'password', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 抛异常
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 默认返回关联数组
PDO::ATTR_EMULATE_PREPARES => false, // 使用数据库原生预处理
]);
}
return self::$instance;
}
}
// ────────── 基础 CRUD ──────────
// 查询单条
$stmt = Database::getInstance()->prepare("SELECT id, name, email FROM users WHERE id = ?");
$stmt->execute([1]);
$user = $stmt->fetch();
// $user = ['id' => 1, 'name' => '张三', 'email' => 'zhangsan@mail.com']
// 查询多条
$stmt = Database::getInstance()->prepare(
"SELECT id, name, email FROM users WHERE status = ? ORDER BY created_at DESC LIMIT ? OFFSET ?"
);
$stmt->execute(['active', 20, 0]);
$users = $stmt->fetchAll();
// $users = [['id' => 1, ...], ['id' => 2, ...], ...]
// 插入 + 获取自增 ID
$stmt = Database::getInstance()->prepare(
"INSERT INTO users (name, email, created_at) VALUES (?, ?, NOW())"
);
$stmt->execute(['新用户', 'new@mail.com']);
$lastId = Database::getInstance()->lastInsertId();
// 更新
$stmt = Database::getInstance()->prepare("UPDATE users SET status = ? WHERE id = ?");
$stmt->execute(['inactive', 1]);
$affectedRows = $stmt->rowCount();
// 删除
$stmt = Database::getInstance()->prepare("DELETE FROM users WHERE id = ?");
$stmt->execute([99]);
// ────────── 事务 ──────────
$pdo = Database::getInstance();
$pdo->beginTransaction();
try {
$pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ?")
->execute([100, 1]);
$pdo->prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?")
->execute([100, 2]);
$pdo->commit();
echo "转账成功";
} catch (Exception $e) {
$pdo->rollBack();
echo "转账失败: " . $e->getMessage();
}
// ────────── 动态查询构建 ──────────
function getUsers(array $filters, int $page = 1, int $perPage = 20): array
{
$pdo = Database::getInstance();
$where = [];
$params = [];
if (!empty($filters['status'])) {
$where[] = 'status = ?';
$params[] = $filters['status'];
}
if (!empty($filters['keyword'])) {
$where[] = '(name LIKE ? OR email LIKE ?)';
$params[] = '%' . $filters['keyword'] . '%';
$params[] = '%' . $filters['keyword'] . '%';
}
if (!empty($filters['min_age'])) {
$where[] = 'age >= ?';
$params[] = (int)$filters['min_age'];
}
$whereClause = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$offset = ($page - 1) * $perPage;
// 查总数
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM users $whereClause");
$countStmt->execute($params);
$total = $countStmt->fetchColumn();
// 查数据
$params[] = $perPage;
$params[] = $offset;
$stmt = $pdo->prepare(
"SELECT id, name, email FROM users $whereClause ORDER BY id DESC LIMIT ? OFFSET ?"
);
$stmt->execute($params);
return [
'list' => $stmt->fetchAll(),
'total' => (int)$total,
'page' => $page,
'per_page' => $perPage,
];
}
7.2 使用 Laravel Eloquent ORM
如果你用 Laravel 框架,Eloquent 让数据库操作更像操作 JS 对象:
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $table = 'users';
protected $fillable = ['name', 'email', 'status']; // 允许批量赋值
protected $hidden = ['password']; // JSON 输出时隐藏
// 关联定义:一个用户有多个订单
public function orders()
{
return $this->hasMany(Order::class);
}
}
class Order extends Model
{
protected $fillable = ['user_id', 'product_name', 'amount'];
// 反向关联:一个订单属于一个用户
public function user()
{
return $this->belongsTo(User::class);
}
}
// ────────── CRUD ──────────
// 查询单条
$user = User::find(1);
$user = User::where('email', 'a@mail.com')->first();
// 查询多条
$users = User::where('status', 'active')
->orderBy('created_at', 'desc')
->limit(20)
->offset(0)
->get();
// 条件链式查询
$users = User::where('age', '>', 18)
->where('status', 'active')
->orWhere('vip', true)
->get();
// 创建
$user = User::create([
'name' => '赵六',
'email' => 'zhao@mail.com',
'status' => 'active',
]);
// 更新
$user->update(['status' => 'inactive']);
User::where('status', 'inactive')->update(['status' => 'active']); // 批量更新
// 删除
$user->delete();
User::destroy([1, 2, 3]); // 批量删除
// ────────── 关联查询 ──────────
// 预加载(解决 N+1 问题)
$orders = Order::with('user')->where('amount', '>', 100)->get();
// 2 条 SQL:SELECT * FROM orders WHERE amount > 100;
// SELECT * FROM users WHERE id IN (1, 2, 3);
// 嵌套预加载
$users = User::with(['orders' => function ($query) {
$query->where('amount', '>', 100)->orderBy('created_at', 'desc');
}])->get();
// 关联条件查询
$users = User::whereHas('orders', function ($query) {
$query->where('amount', '>', 500);
})->get();
// 查询有订单金额 > 500 的用户
// ────────── 聚合 ──────────
$count = User::where('status', 'active')->count();
$maxAge = User::max('age');
$sum = Order::where('user_id', 1)->sum('amount');
// ────────── 事务 ──────────
DB::transaction(function () use ($fromId, $toId, $amount) {
$from = Account::find($fromId);
$from->balance -= $amount;
$from->save();
$to = Account::find($toId);
$to->balance += $amount;
$to->save();
});
// ────────── 原生 SQL 兜底(复杂情况) ──────────
$results = DB::select(
"SELECT u.name, SUM(o.amount) as total_amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.created_at >= ?
GROUP BY u.id
HAVING total_amount > ?
ORDER BY total_amount DESC",
['2024-01-01', 1000]
);
八、Go 和 PHP 的 SQL 使用对比总结
| 维度 | Go(GORM) | PHP(Eloquent / PDO) |
|---|---|---|
| 连接管理 | 连接池,需手动配置 | PDO 持久连接 / Laravel 内置连接池(Swoole 下) |
| 并发模型 | goroutine 天然并发 | PHP-FPM 每请求一个进程 |
| 查询构建 | GORM 链式调用 | Eloquent 链式调用 / Query Builder |
| 错误处理 | if err != nil 模式 |
try-catch 异常 |
| 类型安全 | 编译时 struct 校验 | 运行时(PHP 8.1+ 支持枚举/类型) |
| 事务写法 | 闭包回调 | 闭包回调 / 手动 begin-commit |
| 性能 | 编译语言,快 | 解释执行,慢(但数据库 IO 才是瓶颈) |
| 学习曲线 | 中等,但需理解并发 | 低,PHP 天然适合 Web CRUD |
九、SQL 注入:前端转后端的第一个安全课
9.1 什么是 SQL 注入
php
// ❌ 错误示范:拼接用户输入到 SQL
$id = $_GET['id']; // 用户输入:1 OR 1=1
$sql = "SELECT * FROM users WHERE id = $id";
// 实际执行:SELECT * FROM users WHERE id = 1 OR 1=1
// 结果:返回所有用户!(1=1 永远为真)
// ❌ 更危险:删库
$id = "1; DROP TABLE users; --";
// 执行:SELECT * FROM users WHERE id = 1; DROP TABLE users; --'
// 结果:用户表被删了
9.2 防注入的正确姿势:参数化查询
go
// ✅ Go:用 ? 占位符,驱动会自动转义
db.Query("SELECT * FROM users WHERE id = ?", userInput)
// userInput = "1 OR 1=1" 时,实际查询变成:
// SELECT * FROM users WHERE id = '1 OR 1=1' ← 变成字符串,不会执行 OR
// ✅ PHP PDO:同样用占位符
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
// ✅ GORM:自动使用参数绑定
db.Where("id = ?", userInput).Find(&users);
// ✅ Eloquent:自动参数绑定
User::where('id', $id)->get();
核心原则:任何用户输入都绝对不能直接拼接到 SQL 字符串中。用 ? 占位符,让驱动做转义。 这是前端转后端最容易踩的坑------习惯了 JS 的模板字符串拼接,直接 "SELECT * FROM users WHERE id = ${id}",然后就出事了。
十、前端转后端 SQL 学习路径
sql
第一阶段:理解关系模型
├── 表、行、列、主键、外键的概念
├── 一对一、一对多、多对多关系
└── 找个 MySQL/PostgreSQL 装起来,用图形化工具(DBeaver/TablePlus)看着操作
第二阶段:掌握基础 SQL
├── SELECT / INSERT / UPDATE / DELETE
├── WHERE / ORDER BY / LIMIT / GROUP BY / HAVING
├── INNER JOIN / LEFT JOIN
└── 索引的概念和创建(CREATE INDEX)
第三阶段:在代码中使用
├── Go: database/sql → GORM
├── PHP: PDO → Laravel Eloquent
└── 理解 ORM 生成的 SQL 是什么样的
第四阶段:进阶
├── EXPLAIN 分析查询计划
├── 慢查询优化
├── 事务隔离级别
├── 数据库连接池
├── 读写分离 / 分库分表
└── Redis 缓存层(给数据库减压)
十一、常见误区与踩坑
| 误区 | 真相 |
|---|---|
| "用 ORM 就不需要懂 SQL" | ORM 生成的 SQL 可能很糟糕(N+1 查询),看不懂日志里的慢 SQL 就没法优化 |
| "SELECT * 没问题" | 数据量大 + 字段多时,传输开销很大,而且返回到代码里有不用的字段污染 |
| "PHP 不需要连接池" | PHP-FPM 模式下每次请求重新连接很浪费,建议用持久连接或 Swoole |
| "写了索引就一定会走" | MySQL 优化器可能判断全表扫描更快(如 80% 数据都匹配时不走索引) |
| "一个复杂 SQL 比多个简单 SQL 好" | 不是绝对的,有时候拆开更好维护,且可以分别缓存 |
| "所有查询都走数据库" | 热点数据放 Redis,SQL 只处理 Redis 没有的或需要持久化的 |