SQL 实战与 Go 客户端

本章聚焦 MySQL 的实用 SQL 技巧,并给出 Go 访问 MySQL 的两种方式:标准库 database/sql 与 ORM 框架 GORM。

常用函数

类别函数说明
字符串CONCAT(a, b)拼接
字符串SUBSTRING(s, pos, len)截取
字符串GROUP_CONCAT(col)分组内拼接
日期NOW() / CURDATE()当前时间 / 日期
日期DATE_FORMAT(t, '%Y-%m')格式化
日期DATE_ADD(t, INTERVAL 7 DAY)日期加减
数值ROUND(x, 2) / FLOOR / CEIL取整
条件IF(cond, a, b)三元
条件CASE WHEN ... THEN ... END多分支
空值IFNULL(a, 0) / COALESCE(...)空值兜底
JSONJSON_EXTRACT(j, '$.k') / ->>提取字段
-- 按月统计 GMV
SELECT DATE_FORMAT(created_at, '%Y-%m') AS month,
       SUM(amount)                       AS gmv
FROM orders
WHERE status = 'paid'
GROUP BY month
ORDER BY month;

-- 用 CASE 做分桶统计
SELECT CASE
         WHEN amount < 100   THEN '小额'
         WHEN amount < 1000  THEN '中额'
         ELSE '大额'
       END          AS bucket,
       COUNT(*)     AS cnt
FROM orders
GROUP BY bucket;

-- 提取 JSON 字段(->> 返回文本)
SELECT attrs->>'$.color' AS color
FROM products
WHERE attrs->>'$.color' = 'red';

实用模式

-- 幂等插入:冲突时忽略 / 更新
INSERT INTO inventory (sku, qty) VALUES ('A001', 10)
ON DUPLICATE KEY UPDATE qty = qty + VALUES(qty);

-- 原子扣减库存,避免超卖(影响行数为 0 说明库存不足)
UPDATE inventory SET qty = qty - 1
WHERE sku = 'A001' AND qty >= 1;

-- 存在则更新、不存在则插入的另一种写法
REPLACE INTO inventory (sku, qty) VALUES ('A001', 10);

-- 统计各城市订单数并排序,仅显示有订单的城市
SELECT u.city, COUNT(*) AS cnt
FROM orders o JOIN users u ON u.id = o.user_id
GROUP BY u.city
ORDER BY cnt DESC;
Warning

REPLACE INTO先删除再插入,导致自增主键变化、触发器与级联外键被触发,并可能丢失未指定列的值。除非明确需要,优先使用 INSERT ... ON DUPLICATE KEY UPDATE

用 database/sql 访问 MySQL

go get github.com/go-sql-driver/mysql
package main

import (
    "context"
    "database/sql"
    "fmt"
    "log"
    "time"

    _ "github.com/go-sql-driver/mysql"
)

type Order struct {
    ID        int64
    UserID    int64
    Amount    float64
    CreatedAt time.Time
}

func main() {
    // parseTime=true 让驱动把 DATETIME 解析为 time.Time
    dsn := "app:password@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=true&loc=Local"
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // 连接池配置
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(25)
    db.SetConnMaxLifetime(5 * time.Minute)

    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()
    if err := db.PingContext(ctx); err != nil {
        log.Fatal("连接失败:", err)
    }

    var o Order
    err = db.QueryRowContext(ctx,
        "SELECT id, user_id, amount, created_at FROM orders WHERE id = ?", 1,
    ).Scan(&o.ID, &o.UserID, &o.Amount, &o.CreatedAt)
    if err == sql.ErrNoRows {
        fmt.Println("订单不存在")
        return
    }
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%+v\n", o)
}
Tip

sql.Open 不会真正建立连接,务必用 PingContext 验证。SetConnMaxLifetime 很关键:它能避免长时间空闲连接被数据库或中间件单方面断开而导致的错误。

用 GORM 访问 MySQL

go get gorm.io/gorm
go get gorm.io/driver/mysql
package main

import (
    "time"

    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

type Order struct {
    ID        int64 `gorm:"primaryKey"`
    UserID    int64 `gorm:"index:idx_user_created"`
    Amount    float64
    Status    string `gorm:"size:16;index"`
    CreatedAt time.Time
}

func open() (*gorm.DB, error) {
    dsn := "app:password@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=true&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        return nil, err
    }
    sqlDB, _ := db.DB()
    sqlDB.SetMaxOpenConns(25)
    sqlDB.SetMaxIdleConns(25)
    sqlDB.SetConnMaxLifetime(5 * time.Minute)
    return db, nil
}

func crud(db *gorm.DB) error {
    // 增
    o := Order{UserID: 1, Amount: 99.5, Status: "paid"}
    if err := db.Create(&o).Error; err != nil {
        return err
    }

    // 查
    var list []Order
    db.Where("user_id = ? AND status = ?", 1, "paid").
        Order("created_at DESC").
        Limit(20).
        Find(&list)

    // 改(只更新非零字段)
    db.Model(&o).Update("status", "refunded")

    // 事务
    return db.Transaction(func(tx *gorm.DB) error {
        return tx.Create(&Order{UserID: 2, Amount: 10, Status: "paid"}).Error
    })
}
Warning

GORM 的 Updates(Struct) 会跳过零值字段(如 Amount: 0),更新零值请用 map[string]anySelect 指定列。另外 First 查不到会返回 gorm.ErrRecordNotFound,业务需显式处理。

小结

  • 常用函数与 ON DUPLICATE KEY UPDATE、原子扣减等模式是日常主力。
  • database/sql 提供统一接口:注意 DSN 的 parseTime、连接池参数与 ErrNoRows
  • GORM 提升开发效率,但要清楚零值更新与软删除的边界。
  • SQL 一律参数化(MySQL 用 ?),杜绝注入。