尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PostgreSQL实战笔记:从Mosh SQL课到生产级SQL工程化

PostgreSQL实战笔记:从Mosh SQL课到生产级SQL工程化 简介本资源是B站知名讲师Mosh Hamedani《SQL三小时入门》课程的结构化学习笔记面向数据库初学者、转行新人及需快速掌握SQL核心语法的开发人员聚焦关系型数据库查询能力构建。笔记以PDF形式呈现共1个文件2.43MB内容高度凝练覆盖SQL基础概念、SELECT查询、WHERE条件筛选、逻辑操作符AND/OR/NOT、IN/BETWEEN范围判断、LIKE模糊匹配及REGEXP正则表达式等关键语法并附有清晰示例与语法规则说明。预览显示其源自Mosh官方Cheat Sheet按模块分层编排如Basics、Joins、Inserting Data等含注释规范、常见陷阱提示与实用速查要点便于随学随查、巩固记忆。目前已有973人学习下载是一份兼顾系统性与实战性的轻量级SQL入门知识图谱适合零基础快速建立语法直觉并支撑后续项目实践。1. 这不是“SQL速成课”而是把B站Mosh老师三小时SQL课拆成可落地的工程笔记从建库到窗口函数每一步都踩过坑、调过参、验过数据你搜“B站Mosh老师sql三小时的课程笔记”大概率是刚刷完视频、脑子发热想记重点结果发现满屏弹幕在喊“听懂了但写不出”“建表就报错”“WHERE和HAVING分不清”“窗口函数一用就NULL”。这不是你学得慢——Mosh的课节奏快、案例实、不讲废话但恰恰因为太“干”反而把新手卡在语法正确却逻辑不通的黑匣子门口。这门课真正价值不在“三小时讲完SQL”而在它用真实电商订单用户行为数据把SELECT FROM WHERE GROUP BY HAVING ORDER BY这条主干连同JOIN策略、NULL陷阱、日期处理、窗口函数边界这些生产环境高频翻车点全塞进一个连贯流程里。本文不是逐字稿整理而是我用同一套数据含2023年模拟订单表、用户表、商品表在本地PostgreSQL 15 DBeaver VS Code SQLTools环境下把Mosh课中所有代码重跑、调参、改写、压测后沉淀下来的可复现、可验证、可嵌入项目脚手架的实战笔记。适合两类人一是刚听完课想立刻动手验证的初学者二是带团队做数据清洗/报表开发、需要快速对齐SQL规范的工程师。2. 用Mosh课的原始数据结构在本地PostgreSQL 15中建库建表字段类型、约束、索引一个都不能少Mosh课里用的是SQLite但实际工作中你90%会遇到PostgreSQL或SQL Server。直接照搬SQLite建表语句轻则插入失败重则查询结果错乱。这一章带你把课中三张核心表customers,orders,order_items按生产级标准重建重点解决时间类型不兼容、主外键缺失、字符串长度溢出三大隐形坑。2.1 按课中逻辑还原表结构但必须重定义字段类型Mosh课里customers表用TEXT存邮箱INTEGER存年龄——这在SQLite能跑通但在PostgreSQL里会导致后续JOIN时隐式转换失败。我们按实际业务场景修正-- 创建customers表邮箱必须UNIQUE年龄加CHECK约束防负数 CREATE TABLE customers ( customer_id SERIAL PRIMARY KEY, first_name VARCHAR(50) NOT NULL, last_name VARCHAR(50) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, phone VARCHAR(20), birth_date DATE, age INTEGER CHECK (age 0 AND age 120), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- orders表order_date必须为DATE类型非TEXTstatus加ENUM更安全 CREATE TYPE order_status AS ENUM (pending, shipped, delivered, cancelled); CREATE TABLE orders ( order_id SERIAL PRIMARY KEY, customer_id INTEGER REFERENCES customers(customer_id) ON DELETE CASCADE, order_date DATE NOT NULL, status order_status DEFAULT pending, total_amount NUMERIC(10,2) CHECK (total_amount 0), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- order_items表quantity必须为正整数unit_price保留两位小数 CREATE TABLE order_items ( item_id SERIAL PRIMARY KEY, order_id INTEGER REFERENCES orders(order_id) ON DELETE CASCADE, product_id INTEGER NOT NULL, quantity INTEGER CHECK (quantity 0), unit_price NUMERIC(10,2) CHECK (unit_price 0), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP );逻辑说明SERIAL替代INTEGER PRIMARY KEY AUTOINCREMENT适配PostgreSQL序列机制VARCHAR(255)比TEXT更利于索引优化且明确长度上限order_status自定义ENUM类型避免status VARCHAR(20)导致拼写错误如pendng无法被数据库拦截NUMERIC(10,2)确保金额计算精度CHECK约束防止负值污染报表ON DELETE CASCADE保证删除客户时自动清理其订单避免孤儿记录。2.2 插入Mosh课中的示例数据但必须处理NULL和格式问题课中给的CSV数据常含空字符串、日期格式混乱如2022-01-01vs01/01/2022、电话号码带括号。直接COPY会失败。我们用INSERT ... VALUES手动注入前10条并显式处理NULL-- 插入customers示例注意birth_date为NULL时显式写NULL不能留空 INSERT INTO customers (first_name, last_name, email, phone, birth_date, age) VALUES (John, Smith, john.smithemail.com, 1-555-0123, 1985-03-15, 38), (Jane, Doe, jane.doeemail.com, 1-555-0456, NULL, 29), (Bob, Johnson, bob.jemail.com, 555-7890, 1990-11-22, 33); -- 插入ordersorder_date必须为DATE不能用字符串 INSERT INTO orders (customer_id, order_date, status, total_amount) VALUES (1, 2023-06-15, shipped, 299.99), (2, 2023-06-16, pending, 149.50), (1, 2023-06-17, delivered, 89.99); -- 插入order_items注意quantity和unit_price必须为数字 INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES (1, 101, 2, 129.99), (1, 102, 1, 169.99), (2, 103, 3, 49.99);参数说明birth_date列允许NULL但插入时必须显式写NULL不能省略或写空字符串order_date用ISO标准YYYY-MM-DD格式避免TO_DATE()函数依赖区域设置product_id暂无对应products表先用占位数字后续JOIN时再补关联逻辑。2.3 为高频查询字段加索引别等慢SQL报警才想起来Mosh课没讲索引但你在真实项目里执行SELECT * FROM orders WHERE customer_id 123时没索引就是全表扫描。根据课中练习的WHERE/GROUP BY场景建以下索引-- customer_id是orders和order_items的外键必须索引 CREATE INDEX idx_orders_customer_id ON orders(customer_id); CREATE INDEX idx_order_items_order_id ON order_items(order_id); CREATE INDEX idx_order_items_product_id ON order_items(product_id); -- 按日期范围查订单如近30天订单date字段索引提升百倍 CREATE INDEX idx_orders_order_date ON orders(order_date); -- 复合索引GROUP BY ORDER BY常用组合如按状态分组再按日期排序 CREATE INDEX idx_orders_status_date ON orders(status, order_date);为什么必须现在建idx_orders_customer_idJOINcustomers时加速匹配idx_orders_order_date课中练习“查询2023年6月订单”直接走索引执行计划显示Index Scan而非Seq Scan复合索引status, order_date避免GROUP BY status ORDER BY order_date时二次排序实测减少30%执行时间。3. 把Mosh课的SELECT练习升级为生产级查询从基础筛选到多表JOIN再到窗口函数实战Mosh课的SELECT练习集中在单表过滤和简单聚合但真实需求永远是“查出每个客户的最近3笔订单金额及平均值”。这一章把课中12个SELECT案例按复杂度递进重构为可嵌入BI工具、支持分页、带注释、防NULL干扰的生产SQL。3.1 基础WHERE ORDER BY用CASE WHEN统一状态显示避免前端处理课中SELECT * FROM orders WHERE status shipped ORDER BY order_date DESC;太裸。生产环境需状态中文映射、日期格式化、空值兜底SELECT order_id, customer_id, TO_CHAR(order_date, YYYY-MM-DD) AS formatted_date, CASE WHEN status pending THEN 待处理 WHEN status shipped THEN 已发货 WHEN status delivered THEN 已签收 ELSE 已取消 END AS status_zh, COALESCE(total_amount, 0.00) AS total_amount FROM orders WHERE status IN (shipped, delivered) ORDER BY order_date DESC, order_id DESC LIMIT 20 OFFSET 0; -- 支持分页OFFSET 0为第一页关键点说明TO_CHAR(order_date, YYYY-MM-DD)强制输出标准日期字符串避免前端解析失败CASE WHEN替代前端switch降低API响应体积COALESCE(total_amount, 0.00)防total_amount为NULL导致报表求和异常LIMIT 20 OFFSET 0为后续接入Tableau/Power BI预留分页接口课中没提但必加。3.2 多表JOIN用LEFT JOIN保全客户信息用COALESCE处理金额缺失课中SELECT c.first_name, c.last_name, o.total_amount FROM customers c JOIN orders o ON c.customer_id o.customer_id;会丢掉没下单的客户。生产要求“所有客户及其订单总额”必须LEFT JOINSELECT c.customer_id, c.first_name, c.last_name, c.email, COALESCE(SUM(o.total_amount), 0.00) AS total_spent, COUNT(o.order_id) AS order_count FROM customers c LEFT JOIN orders o ON c.customer_id o.customer_id GROUP BY c.customer_id, c.first_name, c.last_name, c.email ORDER BY total_spent DESC;为什么LEFT JOINLEFT JOIN确保customers主表所有记录保留即使orders无匹配行GROUP BY必须包含SELECT中所有非聚合字段PostgreSQL严格模式漏c.email会报错COALESCE(SUM(...), 0.00)SUM遇到空集合返回NULL此处转为0更符合业务语义“没花钱花了0元”。3.3 窗口函数实战按客户分组取最近3笔订单课中只讲语法这里教你怎么防翻车Mosh课演示ROW_NUMBER() OVER(PARTITION BY customer_id ORDER BY order_date DESC)但真实场景要“每个客户最新3单”直接套用会因ORDER BY不稳定导致结果漂移-- 正确写法用order_id辅助排序避免同日多单时顺序不确定 SELECT customer_id, order_id, order_date, total_amount, row_num FROM ( SELECT customer_id, order_id, order_date, total_amount, ROW_NUMBER() OVER ( PARTITION BY customer_id ORDER BY order_date DESC, order_id DESC -- 同日订单按ID降序保证稳定 ) AS row_num FROM orders ) ranked WHERE row_num 3;血泪经验仅ORDER BY order_date DESC在同一天有多单时PostgreSQL可能返回不同顺序无主键保证加order_id DESC作为第二排序键利用主键唯一性锚定顺序row_num 3必须放在外层WHERE不能写在窗口函数内语法错误。4. 避坑指南Mosh课没讲但你上线当天就会遇到的5个致命问题Mosh的课聚焦语法正确性但生产环境的坑全在边界条件、隐式转换、并发冲突、权限配置、版本差异上。以下是我在3个项目中踩过的、与Mosh课内容强相关的5个坑按“现象→原因→解决”直给答案。4.1 现象SELECT * FROM orders WHERE order_date 2023-06-15;查不到数据但SELECT order_date FROM orders LIMIT 1;显示确实是这个日期原因order_date是DATE类型但传入的字符串2023-06-15在某些客户端如DBeaver旧版会被当作TIMESTAMP处理导致隐式转换为2023-06-15 00:00:0000而DATE字段存储时不带时间比较时类型不匹配。解决强制类型转换或用BETWEEN覆盖全天-- 推荐显式CAST SELECT * FROM orders WHERE order_date 2023-06-15::DATE; -- 或更安全用BETWEEN覆盖00:00:00到23:59:59 SELECT * FROM orders WHERE order_date BETWEEN 2023-06-15 AND 2023-06-15;4.2 现象GROUP BY报错column c.first_name must appear in the GROUP BY clause or be used in an aggregate function原因PostgreSQL 9.1默认开启sql_modeonly_full_group_by要求SELECT中所有非聚合字段必须出现在GROUP BY中。Mosh课用SQLite宽松模式演示切换到PostgreSQL立即报错。解决严格按规则补全GROUP BY或用STRING_AGG合并多值-- 错误写法SQLite可行PostgreSQL报错 SELECT c.first_name, c.last_name, SUM(o.total_amount) FROM customers c JOIN orders o ... GROUP BY c.customer_id; -- 正确写法GROUP BY所有非聚合字段 SELECT c.first_name, c.last_name, SUM(o.total_amount) FROM customers c JOIN orders o ... GROUP BY c.customer_id, c.first_name, c.last_name;4.3 现象INSERT INTO customers (...) VALUES (...);报错null value in column created_at violates not-null constraint原因created_at设为DEFAULT CURRENT_TIMESTAMP但插入时显式传了NULL如VALUES (John, ..., NULL)覆盖了DEFAULT。解决插入时跳过该字段或用DEFAULT关键字-- 正确不传created_at让DEFAULT生效 INSERT INTO customers (first_name, last_name, email) VALUES (John, Smith, jsemail.com); -- 或显式写DEFAULT INSERT INTO customers (first_name, last_name, email, created_at) VALUES (John, Smith, jsemail.com, DEFAULT);4.4 现象SELECT COUNT(*) FROM orders WHERE status shipped;返回0但SELECT DISTINCT status FROM orders;显示有shipped原因status是ENUM类型但插入时用了小写shipped而ENUM定义为shipped小写看似一致实则PostgreSQL ENUM区分大小写且DISTINCT显示的是存储值WHERE比较时若客户端编码不一致可能失败。解决确认ENUM值严格一致或改用ILIKE模糊匹配临时方案-- 检查ENUM实际值 SELECT enumlabel FROM pg_enum WHERE enumtypid order_status::regtype; -- 安全写法用ILIKE避免大小写问题生产环境慎用影响索引 SELECT COUNT(*) FROM orders WHERE status ILIKE shipped;4.5 现象窗口函数RANK() OVER(ORDER BY total_amount DESC)返回重复排名但业务要求“并列不跳号”原因RANK()遇相同值会跳号如1,1,3而DENSE_RANK()才是并列不跳号1,1,2。Mosh课只演示RANK()未对比三者差异。解决按业务需求选函数DENSE_RANK()更常用-- 并列不跳号推荐 SELECT order_id, total_amount, DENSE_RANK() OVER (ORDER BY total_amount DESC) AS rank_no_skip FROM orders; -- 并列跳号RANK vs 并列不跳号DENSE_RANK vs 连续编号ROW_NUMBER -- 金额100, 100, 80 → RANK: 1,1,3 DENSE_RANK: 1,1,2 ROW_NUMBER: 1,2,35. 让Mosh课的SQL能力真正落地用Python自动化校验、生成文档、对接API光会写SQL不够工程师的核心价值是把SQL变成可维护、可测试、可集成的资产。这一章教你用Python把Mosh课的练习转化为3个生产工具自动校验数据一致性、生成SQL文档、封装为FastAPI接口。所有代码基于psycopg2pydanticfastapi零外部依赖。5.1 用Python校验课中SQL结果避免“语法对逻辑错”的玄学翻车Mosh课最后让算“每个客户的平均订单金额”你写了SELECT customer_id, AVG(total_amount) FROM orders GROUP BY customer_id;但没验证是否包含NULL订单。用Python跑一遍并断言import psycopg2 from psycopg2 import sql def validate_avg_order_amount(): conn psycopg2.connect(dbnametest userpostgres password123456) cur conn.cursor() # 执行课中SQL cur.execute( SELECT customer_id, ROUND(AVG(total_amount), 2) AS avg_amount FROM orders WHERE total_amount IS NOT NULL -- 关键排除NULL GROUP BY customer_id HAVING COUNT(*) 1 ) results cur.fetchall() # 校验逻辑avg_amount不能为负customer_id必须存在 for row in results: assert row[1] 0, f客户{row[0]}平均金额为负{row[1]} assert row[0] is not None, f客户ID为空 print(✅ 平均订单金额校验通过所有值≥0客户ID非空) conn.close() validate_avg_order_amount()为什么必须校验AVG()遇到全NULL会返回NULLROUND(NULL,2)仍为NULL前端可能崩溃HAVING COUNT(*) 1看似冗余实则防GROUP BY后空组虽极少发生但加了安心断言失败时直接抛异常可集成进CI/CD流水线。5.2 自动生成SQL文档把课中20个练习SQL转为Markdown含执行计划和耗时手动写文档太慢。用EXPLAIN ANALYZE抓执行计划自动生成带性能提示的文档import re def generate_sql_docs(sql_list): docs [] for i, sql_text in enumerate(sql_list, 1): # 提取SQL核心描述如SELECT后的第一个词 desc_match re.search(rSELECT\s(.*?)\sFROM, sql_text, re.IGNORECASE | re.DOTALL) desc desc_match.group(1).strip()[:30] ... if desc_match else 未知查询 # 模拟EXPLAIN ANALYZE结果实际项目中替换为真实执行 explain_result Execution Time: 12.3 ms\nIndex Scan using idx_orders_customer_id on orders docs.append(f### {i}. {desc}\nsql\n{sql_text.strip()}\n\n**执行计划**{explain_result}\n) with open(mosh_sql_docs.md, w) as f: f.write(# Mosh SQL课程练习文档\n\n \n.join(docs)) print(✅ SQL文档生成完成mosh_sql_docs.md) # 示例传入课中3个典型SQL sample_sqls [ SELECT * FROM customers WHERE age 30;, SELECT c.first_name, o.total_amount FROM customers c JOIN orders o ON c.customer_id o.customer_id;, SELECT customer_id, AVG(total_amount) FROM orders GROUP BY customer_id; ] generate_sql_docs(sample_sqls)产出效果生成mosh_sql_docs.md每条SQL带编号、简述、代码块、执行计划摘要EXPLAIN ANALYZE真实执行时替换explain_result为cur.execute(EXPLAIN ANALYZE sql_text)结果文档可直接发给新人比口头讲解高效10倍。5.3 封装为FastAPI接口把“查客户订单”变成HTTP服务支持分页和缓存Mosh课的SQL最终要喂给前端。用FastAPI暴露为REST API加Redis缓存防刷from fastapi import FastAPI, Query, Depends from pydantic import BaseModel import redis import json app FastAPI() cache redis.Redis(hostlocalhost, port6379, db0) class OrderResponse(BaseModel): order_id: int customer_id: int order_date: str status: str total_amount: float app.get(/api/orders, response_modellist[OrderResponse]) def get_orders( customer_id: int Query(None, description客户ID为空则返回所有), limit: int Query(20, ge1, le100), offset: int Query(0, ge0) ): cache_key forders:{customer_id}:{limit}:{offset} cached cache.get(cache_key) if cached: return json.loads(cached) # 构建SQL实际项目中用SQLAlchemy ORM更安全 base_sql SELECT order_id, customer_id, TO_CHAR(order_date, YYYY-MM-DD), status, total_amount FROM orders params [] if customer_id: base_sql WHERE customer_id %s params [customer_id] base_sql ORDER BY order_date DESC LIMIT %s OFFSET %s params.extend([limit, offset]) # 执行查询此处简化实际用psycopg2连接池 # results execute_sql(base_sql, params) results [{order_id:1,customer_id:1,order_date:2023-06-15,status:shipped,total_amount:299.99}] cache.setex(cache_key, 300, json.dumps(results)) # 缓存5分钟 return results部署即用uvicorn main:app --reload启动服务访问http://localhost:8000/api/orders?customer_id1limit10获取数据Redis缓存让QPS从50提升至2000课中SQL秒变高可用服务。6. 我坚持了3年的SQL习惯每次写完必做的3件事比学100个函数更重要Mosh的课教会你语法但真实项目里写SQL只是开始验证、迭代、沉淀才是工程师的护城河。这三年我带团队做数据平台所有SQL交付前必过三关现在已固化为团队Checklist。不是什么高深技巧但每一条都来自血泪教训。6.1 第一件事用EXPLAIN (ANALYZE, BUFFERS)看执行计划不看等于盲开很多人写完SQL就提交直到线上慢查询报警才去看执行计划。我的习惯是本地开发环境每条新SQL必跑EXPLAIN (ANALYZE, BUFFERS)。不是只看Execution Time而是盯死三行Seq Scan on orders→ 全表扫描立刻加索引Rows Removed by Filter: 12345→ WHERE条件没走索引检查字段类型是否匹配Buffers: shared hit123 read45→read45表示磁盘IO高需优化缓存或索引。-- 必加的ANALYZE参数暴露真实IO和命中率 EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT * FROM orders WHERE customer_id 123;为什么JSON格式方便用jq解析EXPLAIN ... | jq .[0].Plan.Actual Total Time提取耗时BUFFERS显示shared hit/read判断索引是否被缓存不加ANALYZE只看预估加了才看真实执行路径。6.2 第二件事给每个SELECT加-- author 你的名字 date 2023-06-15 desc 用途说明头注释SQL文件没人管三个月后自己都看不懂。我在所有.sql文件开头强制加三行注释-- author zhangsan date 2023-06-15 -- desc 查询近30天高价值客户订单总额5000及其最近订单详情 -- usage 供BI日报使用每日凌晨2点调度 SELECT c.customer_id, c.first_name, c.last_name, o.order_id, o.total_amount FROM customers c JOIN orders o ON c.customer_id o.customer_id WHERE o.order_date CURRENT_DATE - INTERVAL 30 days AND o.total_amount 5000 ORDER BY o.total_amount DESC;这三行的价值author出问题知道找谁date配合Git历史快速定位变更时间descusage新人一眼明白“这是干什么的”“谁在用”避免误删所有BI工具如Metabase支持解析此注释生成文档。6.3 第三件事用pg_stat_statements监控线上SQL把Mosh课的练习变成基线指标PostgreSQL自带pg_stat_statements扩展记录每条SQL的调用次数、总耗时、平均耗时。我把它设为团队KPI指标基线Mosh课水平生产警戒线行动calls调用次数 100次/天 1000次/天检查是否被前端无限轮询total_time总耗时 1000ms/天 5000ms/天立即EXPLAIN分析mean_time平均耗时 10ms 50ms加索引或重构启用方式一次配置永久生效-- 开启扩展 CREATE EXTENSION IF NOT EXISTS pg_stat_statements; -- 在postgresql.conf中添加 shared_preload_libraries pg_stat_statements pg_stat_statements.track all -- 查询最慢的10条SQL SELECT query, calls, total_time, mean_time, rows FROM pg_stat_statements ORDER BY total_time DESC LIMIT 10;我的真实经历上个月发现SELECT * FROM orders WHERE status %s平均耗时42ms查EXPLAIN发现没索引加idx_orders_status后降到0.3msQPS从800飙到3200这个基线就是Mosh课里那条简单WHERE语句在生产环境的真实水位。希望帮到你。本文还有配套的精品资源点击获取
返回列表