内部培训
02 第2周D11 PostgresSQL

D11 Postgres / SQL

D11 Postgres / SQL

标准(必讲)。公司的"钱账本"全在数据库里(用户余额、流水、定价表)。计费系统的正确性=SQL 的正确性。

教学目标(学完能做什么)

  • 会写基础 SQL:增删改查、WHERE、JOIN、GROUP BY、ORDER BY
  • 理解表设计三要素:主键、外键、索引
  • 知道事务(transaction)与 ACID,能说清"扣费为什么必须用事务"
  • 能用 psql / 驱动连数据库做只读查询,能给 D10 的 API 接上数据库

前置要求

  • D10(FastAPI)、D9(Python)

本模块在业务中的位置

  • 公司每个业务都有 Postgres(见 CLAUDE.md DB index:core_api、newapi、push_gateway、account_vault 等)。计费 = 数据库里扣余额 + 记流水,这一步错一分钱都是事故。D15 计费、第 4 周项目 B(计费引擎)全靠本模块。

内容分段

1. 为什么需要数据库

  • 内存 dict(D10 练习用)进程一重启就丢;数据库持久化 + 并发安全 + 查询能力
  • 公司统一用 PostgreSQL

2. SQL 增删改查(CRUD)

-- 建表
CREATE TABLE users (
  id      BIGSERIAL PRIMARY KEY,      -- 自增主键
  name    TEXT NOT NULL,
  balance NUMERIC(20, 8) NOT NULL DEFAULT 0   -- 金额/余额,用 NUMERIC 别用 float!
);

-- 插入
INSERT INTO users (name, balance) VALUES ('alice', 100.0);

-- 查询
SELECT id, name, balance FROM users WHERE balance > 0 ORDER BY balance DESC;

-- 更新
UPDATE users SET balance = balance - 10 WHERE id = 1;

-- 删除
DELETE FROM users WHERE id = 1;
  • 金额永远用 NUMERIC,绝不用 float(二进制浮点会有 0.1+0.2≠0.3 问题,钱账必须精确)。

3. 联表与聚合(业务常用)

-- JOIN:订单关联用户
SELECT o.id, u.name, o.amount
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.status = 'paid';

-- GROUP BY:按模型汇总消耗
SELECT model, SUM(tokens) AS total_tokens, COUNT(*) AS calls
FROM usage_log
GROUP BY model
ORDER BY total_tokens DESC;
  • 记忆:JOIN 是把两张表"接"起来,GROUP BY 是"分堆"后统计

4. 索引

  • 没有索引 = 全表扫(数据多了巨慢);有索引 = 走目录。
  • 高频 WHERE/JOIN 的列要加索引:
CREATE INDEX idx_usage_user ON usage_log(user_id);
CREATE INDEX idx_usage_model ON usage_log(model);
  • 不是越多越好:索引占空间、拖慢写;只给高频查询列加

5. 事务与 ACID(计费的生命线)

BEGIN;
UPDATE users SET balance = balance - 10 WHERE id = 1;   -- 扣费
INSERT INTO bills (user_id, amount, type) VALUES (1, 10, 'chat');  -- 记流水
COMMIT;   -- 要么都成功
-- 任何一步失败就 ROLLBACK,余额不会扣了流水没记
  • ACID:原子性(Atomicity)/一致性(Consistency)/隔离性(Isolation)/持久性(Durability)。理解第一个最重要:一坨操作要么全成、要么全不成
  • 为什么扣费必须用事务:否则"余额扣了但流水没记"= 对不上账。

6. 从 FastAPI 连数据库(给 D10 接库)

pip install asyncpg            # 或 sqlalchemy 等 ORM
import asyncpg

async def get_balance(uid: int):
    conn = await asyncpg.connect("postgresql://user:pass@localhost:5432/db")
    try:
        row = await conn.fetchrow("SELECT balance FROM users WHERE id=$1", uid)
        return row["balance"] if row else None
    finally:
        await conn.close()
  • 生产不把密码写代码里,走环境变量/Secret(D8 提过)。

7. psql 只读连接(公司练习口径)

psql "postgresql://用户名:密码@localhost:5432/库名"   # 本地练习
# 生产库密码在 Secret 里,新人只读需负责人授权;只跑 SELECT

讲解节奏建议(约 90 分钟)

时段内容
09:00-09:10引入:公司的钱账本全在 Postgres,计费=SQ L 正确
09:10-09:40CRUD + NUMERIC(强调金额不用 float)
09:40-10:05JOIN / GROUP BY
10:05-10:25索引
10:25-10:50事务与 ACID(计费生命线,多花时间)
10:50-11:30学员实操:本地库建表/查询 + 给 API 接库(对应作业)
11:30-11:50常见误区 + 小结
11:50-12:00布置作业

常见误区汇总

误区正确理解
金额用 float用 NUMERIC;float 精度问题会算错钱
全表扫没什么数据量大就慢到不可用,加索引
索引越多越好占空间拖写,只加高频查询列
扣费不用事务可能扣了钱流水没记,对不上账=事故
DELETE 了数据还能找回默认不可逆;删前先 SELECT 确认(Class A)
密码写代码里走环境变量/Secret,绝不硬编码

On this page