WEEK 01

入职第一周:老板的第一张表

公司刚成立半年,只有十来个人,你是第一个和数据沾边的人。老板把后台导出的订单表丢给你:「咱全部的生意都在里面。」你的任务:把它变成一个能查的库,然后开始接老板一个接一个的问题。
目标:零基础起步,周五闭卷完成 30 道单表查询题;任何单表需求不查文档能写对。
0 / 7 天
剧情 入职第一天,老板发来订单导出文件:「明天告诉我昨天卖了多少。」你环顾四周--公司连个数据库都没有。
学 · 40 min
  • 什么是数据库 / 表 / 行 / 列:和 Excel 的对应关系

    场景 老板丢来一个订单导出文件,你只会用 Excel 打开它。今天起,同样的数据要换一种存放方式。

    讲解 对应关系一句话:数据库 = 整个工作簿文件,表 = 一个 sheet,行 = 一条订单记录,列 = 一个字段(id、金额、时间……)。本质区别在约束和访问方式:Excel 的单元格里塞什么都可以,数据库的每一列有类型(金额必须是数字)和可空性(支付时间可以没有),访问统一走 SQL 而不是鼠标点选。

    试试
    -- 表在数据库里的样子:列有类型,行是一条条订单
    select id, user_id, status, total_amount, created_at
    from orders
    limit 3;

    易错 Excel 里「没填」和「填了 0」长得差不多;数据库里 NULL(没发生)和 0(发生了、值为零)是两个世界,D3 整天都在讲它。

  • Docker 起 PostgreSQL 16,psql 连进去

    场景 公司连个数据库都没有,你今天要自己搭一个--但不能往笔记本上裸装软件。

    讲解 Docker 一条命令起一个干净的 PostgreSQL 16 容器,删掉重来毫无负担。psql 是 PG 官方命令行客户端,连进去就是你的操作台。装完先做三件事:进容器、开 psql、建库。

    试试
    docker run -d --name pg16 -e POSTGRES_PASSWORD=dev -p 5432:5432 postgres:16
    docker exec -it pg16 psql -U postgres
    CREATE DATABASE shop;
    \c shop

    易错 容器删了数据就没了。要持久化就加 -v pgdata:/var/lib/postgresql/data 挂个卷;练习期无所谓,崩了重灌反而干净。

  • 6 个元命令:\l \dt \d \x \timing

    场景 进了 psql 就是一块黑屏--先学会「环顾四周」。

    讲解 元命令是 psql 客户端的功能,不是 SQL:\l 列出所有库、\dt 列出当前库的表、\d 表名 看表结构(字段、类型、可空、默认值)、\x 把宽结果竖排显示、\timing on 显示每条 SQL 的耗时(W6 优化周全靠它)。

    试试
    \dt          -- 库里有哪些表
    \d orders    -- orders 的完整结构
    \timing on   -- 之后每条 SQL 都带耗时

    易错 元命令以反斜杠开头、不带分号,只在 psql 里有效。换任何别的客户端(代码里连库)它们都不存在。

  • CREATE TABLE:orders 的字段类型你来定

    场景 建表没有标准答案,字段类型是业务判断题:这张表在你眼里是什么。

    讲解 每个字段回答两个问题:什么类型、能不能为空。id 用 uuid(导出文件里就是无序编号);金额用 numeric(10,2) 定点小数,不是 float;status 用 int 存状态码;created_at 必填,paid_at 允许 NULL--可空性是业务事实:没支付的单就是没有支付时间。

    试试
    CREATE TABLE orders (
      id           uuid PRIMARY KEY DEFAULT gen_random_uuid(),
      user_id      uuid NOT NULL,
      status       integer,
      total_amount numeric(10,2),
      created_at   timestamp NOT NULL,
      paid_at      timestamp
    );

    易错 金额用 float / double 会丢精度(二进制存不了 0.1),报表对不上账。钱永远 numeric / decimal。

  • 跑 seed.sql 的「D01」段:灌入 5 万行(里面埋了脏数据)

    场景 空表什么都查不了,把老板的订单灌进去。

    讲解 psql -f 文件 或进去之后 \i 文件 执行整个脚本;今天只跑 §A 段两条语句(先 INSERT 5 万行,再 UPDATE 埋脏数据)。灌完 select count(*) 核对行数,数据里埋着负金额、时间倒挂、空状态--都是本周的练习素材。

    试试
    -- 方式一:psql -f 整个文件,或编辑后只留 §A 段
    -- 方式二:psql 里逐条粘贴执行
    select count(*) from orders;   -- 50000

    易错 同一段 INSERT 跑两遍 = 数据翻倍(或主键冲突)。发现数字不对劲,先怀疑自己重复灌过。

练 · 65 min
  1. Docker 起 pg16,建库 shop 并连进去
  2. 建 orders 表:字段类型自己定,定完对照本页下方的结构检查
  3. 跑 seed.sql 的「D01」段灌入 5 万行
  4. \dt\d orders 看结构,输出贴进 notes.md
  5. \timing,跑 select count(*) from orders,记下行数与耗时
过关库里有且只有 orders 一张表、5 万行;能不看结构说出每个字段的业务含义。
专注视图 ->
剧情 老板在工位后面喊:「昨天有多少单?」--你人生第一条业务查询。
学 · 40 min
  • SELECT 的解剖:选哪些列、从哪张表、* 与具体列

    场景 老板问昨天卖了多少。第一条 SQL 之前,先把 SQL 的「主谓宾」拆清楚。

    讲解 select 列 决定输出什么,from 表 决定从哪拿。* 是全部列,探索数据时用它看长相;正式查询写具体列--少传无用数据,读 SQL 的人一眼看出结果长什么样。

    试试
    -- 探索:先看看数据长什么样
    select * from orders limit 10;
    
    -- 正式:要什么写什么
    select id, total_amount, created_at from orders limit 10;

    易错 生产代码里 select * 配上表结构变更会悄悄多传/错传数据,报错都懒得报。

  • WHERE 比较运算符与日期字面量

    场景 5 万行订单,老板只要昨天的。全表看一遍是人的活,不是 SQL 的。

    讲解 where 逐行判断表达式,结果为 TRUE 的行才保留。日期写法用「左闭右开」区间:>= 昨天 0 点< 今天 0 点,一秒不多不少。不硬编码日期用 current_date - 1

    试试
    select count(*)
    from orders
    where created_at >= current_date - 1   -- 昨天 0 点起
      and created_at <  current_date;      -- 今天 0 点止

    易错 时间戳别用等值判断:created_at = current_date - 1 只匹配「昨天 0 点整」那一瞬间。也别写 between(含两头,边界多算一天)。

  • ORDER BY 与 LIMIT:找「最大 / 最新」的标准姿势

    场景 老板要看金额最大的 10 单、最新的 10 单。

    讲解 标准姿势 = order by 排序键 desc 把想要的行顶到最上面,limit n 截取前 n 行。Top N 问题在 SQL 里永远这么解,没有别的语法。

    试试
    select id, total_amount
    from orders
    order by total_amount desc
    limit 10;

    易错 金额或时间相同的行,顺序是未定义的--补第二排序键(如 order by total_amount desc, id desc)才稳定,这是面试加分点。

  • 排序不稳定:LIMIT 10 不加 ORDER BY 结果每次都不一样

    场景 limit 10 连跑 5 次,5 看似随机的结果,以为数据库坏了。

    讲解 SQL 标准就不承诺无 ORDER BY 时的返回顺序:物理存储顺序、缓存、并行扫描都会影响先吐出哪 10 行。数据库没坏,是你的查询没定义「前 10」的依据。

    试试
    -- 连跑 5 次,对比输出
    select id, total_amount from orders limit 10;
    select id, total_amount from orders limit 10;
    select id, total_amount from orders limit 10;

    易错 不带 order by 的 limit = 「随便给我 10 行」,业务上几乎一定是 bug。

练 · 65 min
  1. select * from orders limit 10 先看看数据长什么样
  2. 查昨天的订单数(先手写日期字面量,再想怎么不硬编码)
  3. 查金额 > 1000 的订单数
  4. 按金额从大到小取前 10 单;再按 created_at 取最新 10 单
  5. LIMIT 10 不加 ORDER BY 连跑 5 次,观察差异并解释
过关不查文档写出「日期范围 + 排序 + 取前 N」的查询。
专注视图 ->
剧情 你汇报「非已支付状态的订单」时数字对不上--运营说还有一批 status 为空的脏数据,而 status <> 2 把它们全漏掉了。
学 · 40 min
  • 三值逻辑:TRUE / FALSE / UNKNOWN,以及 WHERE 只保留 TRUE

    场景 你用 status 不等于 2 统计「非已支付」,漏掉了一大批单。漏掉的那批 status 是 NULL--这不是 bug,是 SQL 的设计。

    讲解 NULL 参与任何比较,结果既不是 TRUE 也不是 FALSE,而是第三个值 UNKNOWN。而 WHERE 只放行 TRUE--UNKNOWN 和 FALSE 一样被丢弃。所以 status <> 2 查不到 status 为 NULL 的行:不是「不等于 2」不成立,是「结果未知」就不放行。

    试试
    select null = null,   -- NULL(UNKNOWN)
           null <> null,   -- NULL(UNKNOWN)
           not null;       -- NULL(UNKNOWN)
    -- TRUE AND NULL = NULL;TRUE OR NULL = TRUE;FALSE OR NULL = NULL

    易错 结论先背下来:NULL 和任何值比较(包括和自己)都得 UNKNOWN,永远不会是 TRUE。

  • IS NULL、IS NOT NULL、IS DISTINCT FROM

    场景 既然比较运算符对 NULL 失灵,判空就得有专门的语法。

    讲解 is null / is not null 是唯一能对 NULL 做出 TRUE/FALSE 判断的写法。进阶是 is distinct from:把 NULL 当成一个具体值参与比较,「不等于某值(连 NULL 也算)」的正确写法。

    试试
    select count(*) from orders where status is null;             -- 状态为空的脏数据
    select count(*) from orders where status <> 2;              -- 漏掉 NULL 行
    select count(*) from orders where status is distinct from 2; -- 正确的「非已支付」

    易错 「不等于某值」的需求永远先想到 IS DISTINCT FROM,<> 只对非 NULL 行负责。

  • NULL 参与比较、算术、排序时分别得到什么

    场景 脏数据的 NULL 会顺着各种运算悄悄传染,得知道传到哪一步会变成什么。

    讲解 算术:null + 1 = null,传染。聚合:sum / avg 把 NULL 当「这行不参与」。排序:PG 默认升序 NULL 排最后、降序排最前,可以用 nulls first / last 显式指定。

    试试
    select null + 1;                          -- null
    select 1 + 2 + null;                      -- null:一个环节空,整条链空
    
    select id, status from orders
    order by status desc nulls last;          -- 脏数据固定垫底

    易错 各数据库对 NULL 排序位置不统一(MySQL 恒排最前),写报表想稳就显式写 nulls first/last。

  • count(*) vs count(col) 初见:NULL 不被 count(col) 数到

    场景 同一张表,三个 count 数出三个数,老板以为你在做假账。

    讲解 count(*) 数行,一行算一个;count(列) 只数该列非空的行。两个数字相减 = 该列为 NULL 的行数--这本身就是个查脏数据的小技巧。

    试试
    select count(*),          -- 50000:总行数
           count(status),       -- ≈49478:有状态的
           count(paid_at)       -- ≈34968:付过钱的
    from orders;

    易错 avg / sum 跳过 NULL 意味着「平均值」的分母变小了--分母口径问题 D4 展开。

练 · 65 min
  1. select null = null, null <> null, not null 验证三值逻辑,做出 AND/OR 真值表
  2. 查 status 为空的订单数;对比 status <> 2 的行数,解释差额来自哪
  3. 对比 count(*) / count(status) / count(paid_at) 三个数字,解释差异
  4. paid_at 为 NULL 的订单在业务上是什么含义?写一句话进 notes.md
  5. coalesce(status, '未知') 把脏数据标出来,统计各状态订单数
过关能解释「col <> 'A' 为什么查不到 col 为 NULL 的行」,并写出正确写法。
专注视图 ->
剧情 老板不想一个数字一个数字地问:「订单按状态分个类,各多少单、多少钱,顺便告诉我每档金额的分布。」
学 · 40 min
  • 聚合函数 count / sum / avg / min / max 的输入输出;count 的三种语义

    场景 老板要的是「分布」,不是某一个数字--把 5 万行压成几个数的运算。

    讲解 聚合函数吃很多行、吐一个值:count 数个数、sum 求和、avg 均值、min / max 极值。count 有三种语义要分清:count(*) 数行、count(列) 数非空、count(distinct 列) 数去重后的非空值。

    试试
    select count(*),                      -- 总行数
           count(distinct user_id)        -- 多少个不同用户下过单
    from orders;

    易错 count(distinct a, b) 这种多列组合写法 PG 不直接支持,要写 count(distinct (a, b))。

  • GROUP BY:把很多行压成「每组一行」

    场景 「按状态分类」= 先按 status 把 5 万行分堆,再对每堆各算一个 count。

    讲解 group by status 之后,5 万行变成 4 行(每个状态一行)。理解它最好的方式:把表想象成按状态码堆好的几摞,select 里的聚合函数对每一摞各算一次。status 为 NULL 的行不会消失,自己占一组。

    试试
    select status, count(*) as 单数, sum(total_amount) as 总金额
    from orders
    group by status
    order by 单数 desc;

    易错 分组后每组只剩一行,select 里出现「既不在 group by、也没被聚合」的列会报错--W2 D11 专门拆这个约束。

  • WHERE(分组前过滤)vs HAVING(分组后过滤)

    场景 「找出订单数超过 100 的日期」--这个 100 是数出来的,数之前它还不存在。

    讲解 执行顺序说了算:WHERE 在分组作用于原始行,HAVING 在分组作用于组。判断条件用到聚合结果(每组数出来的那个数)的,只能进 HAVING;HAVING 里可以放心写 count(*),WHERE 里写它直接报错。

    试试
    select created_at::date as 日期, count(*) as 单数
    from orders
    group by 1
    having count(*) > 100          -- 过滤的是「组」
    order by 1;

    易错 报错 aggregate functions are not allowed in WHERE 就是把聚合条件写错了地方。

  • sum / avg 自动跳过 NULL 带来的分母问题

    场景 运营质疑你算的「平均金额」:和 Excel 里算的不一样。

    讲解 sum / avg 遇到 NULL 行直接跳过:sum 没影响(加零而已),但 avg 的分母变小了--Excel 的 AVERAGE 其实也一样,但没人意识到。想「NULL 按 0 参与平均」要自己 avg(coalesce(列, 0)),两个口径都对,错的是不说明口径。

    试试
    select avg(total_amount),                 -- 分母 = 非空行数
           avg(coalesce(total_amount, 0))    -- 分母 = 全部行数
    from orders;

    易错 报「平均值」之前先回答:分母是谁?这是口径问题,不是语法问题。

  • CASE 两种写法(简单式 / 搜索式):把值映射成标签、把金额分档

    场景 老板看不懂状态码,报表要输出「已支付 / 待支付」;还要把连续的金额切成 0-100 / 100-500 / 500+ 三档。

    讲解 简单式 case 列 when 值 then ... 做等值映射;搜索式 case when 条件 then ... 做任意判断(分档、多条件)。条件从上往下,第一个命中生效,所以分档的条件按从小到大排;else 兜住没列到的值和 NULL。

    试试
    -- 简单式:值 -> 标签
    select case status when 2 then '已支付'
                       when 1 then '待支付'
                       when 3 then '已取消'
                       else '未知' end as 状态
    from orders limit 5;
    
    -- 搜索式:金额分档(条件顺序就是判断顺序)
    select case when total_amount < 100 then '0-100'
                when total_amount < 500 then '100-500'
                else '500+' end as 金额档, count(*)
    from orders
    group by 1;

    易错 分档条件写反(先 < 500 后 < 100),所有小单都进了第一档--顺序就是逻辑。

练 · 65 min
  1. 按状态统计订单数和总金额
  2. 算每种状态的订单占比(总单数用子查询再除)
  3. 用 CASE 把状态映射成中文标签(如「已支付」「待支付」)再分组
  4. 按金额分档(0–100 / 100–500 / 500+)统计订单数与占比
  5. 用 HAVING 找出订单数 > 100 的日期,并解释为什么这题不能用 WHERE
过关一条 SQL 输出「状态 | 订单数 | 金额 | 占比%」完整报表。
专注视图 ->
剧情 导数据时埋的雷陆续爆了:金额有负数、有的订单 paid_at 比 created_at 还早。老板:「能不能清洗成能看的样子?」
学 · 35 min
  • 字符串函数:concat / substring / split_part / trim / lpad

    场景 老板要「ORD-000001」格式的业务单号--拼接、截取、补零,全是字符串函数的活。

    讲解 一句话各记一个用途:concat / || 拼接、substring 按位置截取、split_part(串, 分隔符, 第几段) 按分隔符拆、trim 去首尾空白、lpad(文本, 总长, 补什么) 左侧补字符到指定长度。

    试试
    select 'ORD-' || lpad('123', 6, '0');        -- ORD-000123
    select split_part('ORD-000123', '-', 2);     -- 000123
    select substring('ORD-000123' from 5 for 3); -- 000

    易错 lpad 的参数是文本,数字要先 ::text;数字直接塞进去会报类型错。

  • 类型转换 :: 与 cast(),转换失败怎么办

    场景 字符串 '123' 要参与计算、'000123' 要变回数字--类型不转,SQL 寸步难行。

    讲解 :: 是 PG 的简写(cast(x as 类型) 是标准写法),'123'::int123::text 随手转。要命的是转换失败直接报错:整条查询挂掉,一行脏数据毁一锅。清洗脏数据时先用 ~ 正则验格式,确认能转再转。

    试试
    select '123'::int + 1;                          -- 124
    select 'ORD-000123' ~ '^ORD-\d{6}$';          -- true:格式合法才转
    select split_part('ORD-000123', '-', 2)::int;  -- 123

    易错 PG 没有内置的「转换失败返回 NULL」函数(别的库叫 try_cast),要么先验格式,要么自定义函数。

  • COALESCE / NULLIF / GREATEST / LEAST

    场景 负金额要抬到 0、除法分母可能为 0、展示时 NULL 要换文字--四个小工具各治一病。

    讲解 coalesce(a, b, ...) 取第一个非空(NULL 兜底);nullif(a, b) 在 a=b 时返回 NULL(专治除零:x / nullif(y, 0) 分母为 0 得 NULL 而不是崩);greatest / least 取多值中的最大 / 最小(负金额抬零用 greatest(金额, 0))。

    试试
    select coalesce(paid_at, created_at) as 最后动作时间   -- NULL 兜底
    from orders limit 5;
    
    select greatest(total_amount, 0) as 修正展示     -- 负数抬 0
    from orders where total_amount < 0 limit 5;

    易错 除法永远配 nullif(分母, 0) 防御--报表崩在线上多半是没写它。

  • round / abs 修数值:整数除法的坑

    场景 算占比输出全是 0,数据明明没错。

    讲解 round(x, n) 四舍五入到 n 位,n 可以是负数(-1 舍到十位);abs 绝对值。占比输出为 0 的元凶几乎总是整数除法:1 / 2 在 SQL 里得 0,写 100.0 * a / b 先把一边变浮点。

    试试
    select round(1234.56, -1);         -- 1230:舍到十位
    select 1 / 2, 100.0 * 1 / 2;       -- 0 vs 50
    select abs(-42.5);                 -- 42.5

    易错 整数 / 整数 = 整数(向零截断),参与除法前先写 100.0 或 ::numeric。

  • 清洗的正确姿势:先「展示时修正」,别急着 UPDATE 原表

    场景 老板说「洗成能看的样子」--你想直接把负金额 update 成 0。

    讲解 原表是事实记录:负金额是「导出工具出了问题」这个事实的证据,UPDATE 掉它,问题就永远查不到了。正确姿势是在查询层修正展示(greatest、coalesce、case),原表保持原样;什么时候真的要改表、怎么改才安全,W5 专门讲。

    试试
    -- 展示时修正:原表不动
    select id,
           total_amount as 原始值,
           greatest(total_amount, 0) as 展示值
    from orders
    where total_amount < 0 limit 5;

    易错 动手 UPDATE 原表 = 销毁证据。清洗的第一原则:能不动原表就不动。

练 · 70 min
  1. 找出负金额订单,用 greatest(total_amount, 0) 修正展示
  2. 查出 paid_at 早于 created_at 的异常单(数据里埋了,找出几条)
  3. 金额四舍五入到十位(round(x, -1)),统计各档订单数
  4. row_number() 按时间顺序给订单编号,再拼出「ORD-000001」格式的业务单号(lpad
  5. split_part 把第 4 题的单号反解回数字,验证与编号一致
过关负金额、时间倒挂、空状态三类脏数据各有处理办法,能说出为什么不直接改原表。
专注视图 ->
剧情 老板周五要看「近 30 天每天卖多少」。你发现没有订单的日期在报表里整行消失;顺便,是时候搞清楚一条 SQL 到底按什么顺序执行了。
学 · 40 min
  • date / timestamp / interval;date_trunc 按天周月截断、extract 取分量

    场景 周报要按天、按周汇总--不搞清楚时间类型,分组都是错的。

    讲解 date 只有日期,timestamp 带时分秒,interval 是时间段(加减日期用)。两个主力函数:date_trunc('week', 列) 截断到所在周周一零点(按周分组就靠它);extract('year' from 列) 取出年 / 月 / 日等分量。

    试试
    select date_trunc('week', current_date);      -- 本周一 0 点
    select extract('dow' from current_date);      -- 星期几(0=周日)
    select current_date + interval '1 day';       -- 明天

    易错 date_trunc('week') 的周从周一开始;业务如果按周日切周,得自己算偏移。

  • generate_series 生成日期序列左连接补零

    场景 近 30 天日报交上去,老板发现没单的日期整行消失--不是没卖,是没卖的日子连行都没有。

    讲解 解法的骨架:用 generate_series(起, 止, interval '1 day') 造出完整的 30 行日期,把它当左表,left join orders。没订单的日期那行 o.* 全是 NULL,靠 count(o.id) 数出 0、coalesce 把 NULL 金额补 0。

    试试
    select d::date as 日期,
           count(o.id) as 订单数,
           coalesce(sum(o.total_amount), 0) as gmv
    from generate_series(current_date - 29, current_date, interval '1 day') d
    left join orders o
      on o.created_at >= d and o.created_at < d + interval '1 day'
    group by d
    order by d;

    易错 日期序列是驱动表(左表),写在 from 里、orders 去 join 它--方向反了补零就失效。

  • 查「上周一到上周日」不硬编码日期的写法

    场景 写死日期的报表下周就作废,老板每周都要--日期必须算出来。

    讲解 公式就一行:date_trunc('week', current_date) 是本周一零点,减 7 天得上周一零点,左闭右开到本周一。所有「上一个周期」的需求都是这个模板:先锚定本周期的起点,再平移。

    试试
    where created_at >= date_trunc('week', current_date) - interval '1 week'
      and created_at <  date_trunc('week', current_date)

    易错 时间戳别用 between:它含两头,上周日 23:59:59.999 之后、周一零点之前的毫秒归属说不清。

  • 九步逻辑执行顺序:FROM -> JOIN -> WHERE -> GROUP BY -> HAVING -> SELECT -> DISTINCT -> ORDER BY -> LIMIT

    场景 本周背不下来这个,下周的多表查询会处处撞墙。它是理解一切「为什么这样写不行」的钥匙。

    讲解 书写顺序和执行顺序是两回事:先有表(FROM/JOIN),再筛行(WHERE),再分组(GROUP BY)、筛组(HAVING),然后才算 SELECT 里写的东西,DISTINCT、ORDER BY、LIMIT 依次收尾。你在 WHERE 里用不了 SELECT 的别名、在 WHERE 里写不了聚合,全是这一个原因。

    试试
    select status,                -- 6. SELECT 决定输出哪些列
           count(*) as n           -- 6. 聚合在这里算出来
    from orders
    where total_amount > 100      -- 3. WHERE 过滤行
    group by status               -- 4. 分组,每组之后只剩一行
    having count(*) > 10          -- 5. HAVING 过滤组
    order by n desc               -- 8. 排序(此时才能用别名 n)
    limit 3;                      -- 9. 最后才取前 3

    易错 面试必背。默写不出来的话,把它当成「一句话说明书」:先拿数据,再筛数据,再算数据,最后摆盘。

  • 别名可见性:为什么 WHERE 用不了 SELECT 的别名,ORDER BY 却可以

    场景 你在 where 里写了 select 定义的别名,报错 column does not exist--明明拼写没错。

    讲解 还是执行顺序:WHERE 是第 3 步、SELECT 是第 6 步--WHERE 执行时别名还没出生;ORDER BY 在 SELECT 之后,别名已经存在。一句话:一个子句只能用它执行时已经存在的东西

    试试
    select total_amount as amt
    from orders
    where amt > 100;     -- ERROR: column "amt" does not exist
    -- order by amt 却没问题

    易错 PG 的 group by 其实允许用别名(group by amt 可以),但这是方言、可移植性差--按「别名只有 ORDER BY 能用」记最安全。

练 · 65 min
  1. 近 30 天每日订单数和 GMV,没有订单的日期要补 0(generate_series 左连接)
  2. 按周统计订单数与 GMV(date_trunc('week')
  3. 查「上周」的订单,不许硬编码日期
  4. 写一条包含全部九个子句的查询,在注释里标注每步之后大约剩多少行
  5. 故意在 WHERE 里用 SELECT 定义的别名,记下报错并解释原因
过关白纸默写出九步顺序;补零日报完整无缺行。
专注视图 ->
剧情 第一周结束。老板要一份「本周经营周报」,你就用它来验收自己这周学的一切。
复盘 · 10 min
  • 翻一遍本周 notes.md,标出还讲不利索的点
测评 + 周报 · 110 min
  1. 70 分钟闭卷做 30 道单表查询题(LeetCode 简单难度 + 自出题)
  2. 错题全部重写一遍,写进 mistakes.md
  3. 错题归因:分成「没懂概念 / 记不住语法 / 看错题意」三类统计
  4. 写出给老板的周报:总单量 / GMV / 状态分布 / 近 7 天趋势(每日一行,缺日补 0)
  5. 把「逻辑执行顺序」和「NULL 三值逻辑」各写成 5 句话的口述稿
过关30 题正确 ≥ 24 题;周报里每个数字都能对应到一条你写过的查询。
专注视图 ->