What I cannot create, I do not understand以前我对费曼这句话的理解比较老派：代码你手敲不出来，你就是不懂但在今天这个大模型满天飞的环境下，似乎有了新的理解：如果你脑子里没有一张完整的业务闭环和工程蓝图，那你在 AI 面前其实就是在盲开产品层面的盲区：你连该让 AI 写什么都不知道很多人产生了一种错觉，觉得有了 AI，自己单枪匹马分分钟就能搓出一个商业级应用举个最直白的例子：你想做一个 AI SaaS 软件。但如果你自己都没有作为深度用户去体验过优秀的同类产品，你用 AI 生成出来的东西一定会极其可笑因为你对 SaaS 的业务流根本没有建立起 理解，所以你根本无法向 AI 下达正确的 创造 指令：你只知道让 AI 写个 登录页，却不知道让它写 新手指引，因为你不懂新用户注册后的第一步最容易流失你只知道让 AI 写个 调用大模型 API，却不知道让它设计 Token 计费池和并发限制，结果一上生产环境被人恶意一刷，账单当场爆炸你只知道让 AI 接入 Stripe 支付，却根本不懂怎么处理 Webhook 里的 订阅降级、退款和过期重试，因为你以为支付就是 付钱-开通 这两步，根本不理解 SaaS 订阅制的复杂状态机这时候费曼那句话就应验了：你自以为在 创造 一个产品，其实你只是用 AI 拼凑了一个根本跑不通商业逻辑的玩具工程层面的盲区：跑起来之后，灾难才真正开始退一步讲，就算你把产品逻辑理顺了，让 AI 去写代码。这事儿得看你怎么定义 搞定能 的部分大家都体会过了：AI 确实能把项目 从 0 到 0.6 的时间压缩得很短。你说一句搞个 FastAPI + React，它立马给你吐出一整套路由、组件、接口。一键跑起来，看着挺像那么回事但 不能 的部分，往往是从代码跑起来那一刻才真正开始知乎上有个老哥总结过一个特别典型的 链式翻车 惨案：用户连点两次提交，数据库里莫名其妙多了两条脏数据你让 AI 去修，AI 在前端加了个防抖你想着后端也得兜底啊，让它再加个唯一索引接着为了捕获异常给用户个友好提示，AI 改了返回结构——啪，顺手把其他接口的响应格式全搞崩了修了三轮之后，这项目就成屎山💩了，每次改动都在赌今天会不会踩雷。那位老哥有一句话说得极为精准：主流程只是 30%。AI 最擅长的就是让你产生 主流程跑通了就万事大吉 的错觉，而真正折磨人的，永远是剩下那 70% 的边缘分支和系统状态复杂项目难在约束与边界复杂项目难的从来不是代码量，而是 约束 与 权衡Nginx 团队的工程师(知乎老哥自称)做过一个很有意思的实验，让 AI 从零糊一个系统级的 C 项目（事件驱动、多线程那种）。结论非常贴近工程现实：AI 砌墙的速度贼快，但它手里根本没有图纸模块边界在哪？依赖方向怎么理？这些统统得靠人脑子去定这里面潜伏着一个极其致命的陷阱：看起来一切正常当你依赖同一个模型去生成实现代码和测试用例时，它们的盲区是完全重合的。测试用例照着错误的假设写，跑出来当然是一片绿灯，但这叫 虚假的成功，就是你 被 AI 骗了。 如果你没有跳出代码之外去 review 和推翻它的能力，你连自己是怎么掉坑里的都不知道再看个最近发生的例子。青龙面板最近的一个 Issue（#2933）就非常典型：因为鉴权逻辑对路径大小写处理不一致，配合 Express 默认的不敏感策略，像 /aPi/... 这种变体路径居然能直接绕过 JWT 校验，命中实际路由并打到高权限接口这种坑其实是个特别典型的工程问题。AI 在写接口时，通常只会考虑最理想的执行路径，只要正常请求能跑通就算大功告成。但真实业务里的路由策略、白名单限制、大小写处理，这些需要全局约束的地方，AI 根本照顾不到。它只覆盖 快乐路径（Happy Path），稍微变个形的异常请求，它根本想不到去防这句话放到今天到底指什么我现在觉得，它指的早就不是 你得手写每一行代码 了它的真正意思是：你必须具备对整个产品业务和系统工程的绝对掌控力你想靠 AI 搓出一个真正能上线的商业产品，你敢不敢在发布前回答这几个问题：产品与商业侧：你的核心业务闭环了吗？不管你做的是 AI SaaS 还是社区工具，新手指引、防刷策略和核心资源的消耗控制兜得住吗架构与工程侧：并发一旦上来，用户在核心链路（比如支付或调用大模型）断网了，你的重试、补偿、降级机制和事务边界做好了吗安全与权限侧：默认的接口暴露面有哪些？会不会被变体路径越权访问，甚至被人直接把你后台的高权限 API 拿去当肉鸡如果你答不上来，那你用 AI 堆出来的这个东西就算能跑，也只是 暂时没死 而已。AI 帮你写得越快，你欠的业务债和技术债就越多我现在怎么用 AIAI 该用还是得用，毕竟速度摆在那里。但我现在的心态彻底变了：我是这个产品的产品经理兼架构师，而 AI 只是个手速极快但完全不担责的赛博外包配合它干活，我现在基本只守着这几套底线：先理清业务流再动手。在连用户路径、核心状态机都没想清楚之前，绝对不盲目让 AI 去生成代码。产品的全景蓝图必须在我自己脑子里核心边界死死握着。鉴权、数据一致性、核心回调处理、资源消耗逻辑，这些决定产品命脉的模块，具体的代码块可以让它去填空，但架构怎么定、边界在哪，必须我自己拍板AI 套娃 Review。第一遍生成的代码我绝对不直接合进项目。我会开一个新的 Context，让 AI 去 review：这段逻辑有没有漏洞？高并发下会不会出大问题？用魔法打败魔法，强迫它提升代码质量逼它输出失败模式与对抗性测试。别光给我一坨能跑的代码，直接问它：这个核心接口报错了怎么回滚？主流程测一遍没报错就行了，剩下的时间全拿去测重复提交、越权访问和异常参数，专挑它想不到的死角攻击工具再强，代码写得再快，一旦产品在半夜挂了，或者业务被人恶意刷爆了，爬起来连服务器擦屁股的终究还是我们自己最后来看一个梗图参考链接：知乎：仅凭ai真的能做好复杂项目吗？知乎回答（NGINX 洪志道）：知乎链接微软安全建议：Running OpenClaw safely: identity, isolation, and runtime risk青龙面板鉴权绕过 Issue：#2933
最近看到一些关于 AI 的帖子，发现一个词开始频繁出现：SKILL一开始其实没太在意，直到我发现一件事：几乎所有提到 SKILL 的文章，都是默认你已经知道它是什么的但当我真的停下来想认真回答一句 「SKILL 到底是什么」 的时候，却发现很难给出一个干脆的答案。没有统一定义，也没有哪篇文章是从 为什么需要它 开始的于是我知道大家在用 SKILL，但我并不知道我 自己到底需不需要它为什么使用 SKILL如果只看能力层面，其实 Agent 和 MCP 已经很强了Agent 可以负责推理、规划步骤、决定做什么MCP 可以让模型以一种安全、结构化的方式调用外部能力目前来看，这已经是一个完整的流程了但问题出现在用了一段时间之后当我开始用 Agent 去处理一些长期且重复的事情时，会发现一些现象，比如：有些事情我已经让 Agent 做过很多次每次流程都差不多但 Agent 每次还是会从 0 开始理性上这当然没错，但实际用起来就会发现：有些事情并不值得每次都重新思考， 完全是一个流程接下来 SKILL 就登场了，它补的并不是像 agent mcp 的能力，而是使用方式SKILL 对比 MCP / Agent 的优势在我目前的理解里，SKILL 并不是在“能力”上压过 Agent 或 MCP，而是在职责上补了一层。AgentMcpSKILL负责判断要不要做、怎么拆步骤让模型拥有调用外部的能力负责把一件已经成熟的事情，封装成一个可以被反复调用的能力换句话说：Agent 解决的是「要不要做」MCP 解决的是「怎么调用」SKILL 解决的是「这件事有没有必要每次都重新想一遍」这里有一个很关键、但经常被忽略的点：SKILL 是按需加载、按需调用的，只有当某个场景真的需要它时，它才会被调用这带来的一个很现实的好处是：更省 token，也更干净你不需要把一整套固定流程、风格说明、约束条件反复塞进上下文里该用的时候用，不用的时候完全不存在快速上手这里我用一个现成的 SKILL 来做演示，选择的是这个用来快速生成博客草稿的 SKILL：https://skillsmp.com/zh/skills/nicknisi-dotfiles-home-claude-skills-blog-post-writer-skill-md下载 SKILL把文件夹或者单独的 SKILL.md 放入 .claude/skills 下面使用:我有一堆想法，准备写一篇博客，请用 Nick Nisi 那种风格帮我把它组织成一篇完整的文章： - 最近我在研究 AI 架构里的 SKILL - 看到很多文章默认大家都懂 - 其实我有点困惑 - 想写一篇总结性的博客在发送之后，你会看到 Claude 识别到当前上下文非常符合这个 SKILL 的使用场景，于是开始触发 SKILL 的调用流程。中间会有一段交互过程，大概会确认一些写作方向或细节，例如风格、结构之类的。当这些问题确认完成之后，Claude 会给出一篇完整、可读、已经帮你组织好结构的博客文章。甚至在这个基础上，你还可以继续让它调用其他的 MCP / Agent / SKILL，比如：做二次润色调整结构直接发布到博客系统下面是输出的完整内容 完整内容 Claude Code 中的 SKILL：我花了一周才搞懂的事"你只需要创建一个 SKILL 就行了。"我在文档里看到这句话的时候，内心是崩溃的。等等，我已经有 MCP 工具了，可以调用外部服务。我还有 agent，可以执行复杂的多步任务。现在你告诉我还有个 SKILL？为什么？## 我的困惑最近在深入研究 Claude Code 的架构。看了不少文章，发现大家都在说 SKILL 多好用，SKILL 多强大。但没人解释为什么我们需要它。我是说，我已经有了：MCP (Model Context Protocol): 连接外部工具和数据源Agent: 处理复杂的多步骤任务，可以自主决策那 SKILL 呢？它在这个生态里扮演什么角色？我翻了好几篇文章，都默认你已经懂了。我不懂。## 开始挖掘我做了大部分开发者都会做的事：打开文档，开始一个个翻。MCP 的定义很清楚：标准化的协议，让 Claude 能访问外部工具、数据库、API。想让 Claude 读取你的 Notion 数据？写个 MCP server。想让它查询你的 PostgreSQL？MCP。Agent 也说得过去：一个可以自主工作的实体，能分解任务、使用工具、做决策。需要 Claude 帮你做代码审查、提交 PR、运行测试？用 agent。但 SKILL 呢？文档说它是"可重用的工作流"、"封装的能力"、"用户可调用的命令"。好的，我还是不懂。## 那个瞬间直到我看到一个实际的例子。假设你每次让 Claude 总结对话时，都要说："请帮我总结一下我们刚才讨论的内容，提取关键要点，用简洁的语言列出来。"每次。这是个简单的过程，步骤固定，但你得每次都重复这段话。然后我意识到了：这就是 SKILL 存在的理由。SKILL 不是用来做 MCP 或 agent 做的事。它是用来封装那些重复的、简单的、固定模式的过程。你创建一个 /summarize SKILL，把那段提示词封装进去。以后只需要：/summarize就这样。## 三者的定位我终于搞明白了它们的关系：MCP 是能力层。它让 Claude 能做原本做不到的事——访问外部世界。Agent 是智能层。它让 Claude 能自主决策、规划、执行复杂任务。SKILL 是效率层。它让你能快速调用那些重复的、已经验证过的工作流。它们不是互相替代的关系，而是互补的。一个具体的场景：用 MCP 连接你的代码仓库Agent 分析代码、做出修改建议用 SKILL 一键执行"审查代码 → 生成报告 → 创建 issue"这个固定流程每一层都有自己的价值。## 那个简单的例子我给自己写了第一个 SKILL：上下文总结。没什么花哨的，就是把我常用的总结提示词封装起来：请总结我们的对话：主要讨论的问题得出的结论待办事项（如果有）用简洁的中文列出。保存为 /ctx-summary。现在每次对话结束，我只需要输入这个命令，而不是每次都重新描述一遍我想要什么。这就是 SKILL 的价值：把重复变成简单。## 我的理解回头看，SKILL 的设计其实很聪明。它不试图做所有事情。它只做一件事：让你的常用工作流变成肌肉记忆。你不需要每次都向 Claude 解释你要什么。你不需要维护一个长长的提示词列表。你只需要调用一个命令。而当这些 SKILL 组合起来，当你的工作流里有了 /review、/test、/summarize、/commit，你会发现你和 Claude 的协作变得流畅了很多。不是因为它更聪明了，而是因为你们建立了共同的语言。## 写在最后我花了一周时间才搞懂这个看似简单的概念。不是因为它复杂，而是因为没人解释它为什么存在。如果你也在困惑 SKILL 和 MCP、agent 的区别，希望这篇文章能帮到你。记住：MCP 扩展能力Agent 处理复杂性SKILL 简化重复它们一起，让你和 AI 的协作更高效。现在去创建你的第一个 SKILL 吧。从那个你每周都要重复十次的任务开始 感受说实话，一开始我对 SKILL 是有怀疑的，真的能像传言中那么神吗？它看起来很抽象，似乎又是一个难以理解的概念但在真正用了以后，我才意识到，它解决的是一个很现实的问题：减少重复思考，减少上下文负担不过也有一些很明确的不适合用 SKILL的场景一次性的需求强依赖即时上下文、还在频繁变化的流程连你自己都还没想清楚怎么做的事情SKILL 更像是一个 沉淀工具 ，而不是探索工具到现在为止，我依然不敢说我完全理解了 SKILL但至少有一点是确定的：SKILL 并不神秘，也不高级，它本质上是一种非常工程化、非常现实的抽象方式和 N8N 概念一样，并没有想象当中的复杂与难懂
n8n 新手上路：从一无所知到跑出第一个自动化流程
AI 自动化这几年真的越来越重要了不管是处理重复任务、整理数据，还是用 AI 做一些智能化的流程，现在这些都能被自动化接管这是我第一次接触 n8n ，也是一无所知的新手，但在简单的了解过后，我已经跑通了一个完整的自动化工作流：从 RSS 读取今日热点 → AI 自动总结 → AI 自动制图 → 生成 HTML 日报页什么是 n8n如果你没用过 n8n，可以简单把它想象成开源的 Coze不同的是它：能自托管拖几个节点就能把各种 API 接起来和 AI 配合的时候真的很爽以前做自动化要写 Python / NodeJS，配定时任务，自己处理数据结构、容错之类的。现在很多东西拖一拖就完了n8n 的强大之处不仅在于它能做传统自动化（如备份、同步、发通知）而是在 AI 时代，它可以成为「调度大脑」它自己不做什么复杂逻辑，但非常擅长“把事情串起来”例如：使用 AI 进行复杂抓取与筛选多源 RSS / API / Webhook 信息聚合使用 AI 自动判断价值度对新闻进行 分类、比对、摘要、聚合自动过滤重复内容按重要程度排序，生成清晰结构化数据我的第一个流程：RSS → AI 总结 → AI 制图 → HTML 日报这是我第一次在 n8n 里动手做出的完整流程：从 RSS 获取热点 → AI 自动总结 → AI 自动生成日报图 → 生成 HTML 页面这里推荐一个持续跟进官方的 汉化版本 ，方便学习部署docker volume create n8n_data docker run -it --name n8n -p 5678:5678 -e N8N_RELEASE_TYPE=stable -e N8N_DIAGNOSTICS_ENABLED=false -e N8N_VERSION_NOTIFICATIONS_ENABLED=false -e N8N_HIDE_USAGE_PAGE=true -e N8N_LICENSE_AUTO_RENEW_ENABLED=false -e N8N_RUNNERS_ENABLED=true -e NODE_ENV=development -e N8N_DEFAULT_LOCALE=zh-CN -e N8N_ENTERPRISE_MOCK=true -v n8n_data:/home/node/.n8n ghcr.io/deluxebear/n8n:chs随后直接访问 http://localhost:5678 即可流程展示Webhook → RSS Read → AI Summary → AI Image → Markdown → HTML → Webhook Response整个流程非常直观，基本看一眼图就能理解整个 workflow 是如何工作的通过 Webhook 触发 RSS Read, 交给 AI 进行总结内容，随后将输出结果使用 AI 制作出图片,我这里使用的是 Nano Banana Pro 返回的是 Markdown 格式的 Base64,需要转换成 HTML 才能响应 Webhook，展示在前端。预览效果如下各节点分析Webhook（流程入口）这是整个流程的触发器, 只要访问 http://localhost:5678/webhook/xxxxx 就能出发整个流程的运行RSS Read这个节点负责获取信息源作为简单的项目演示，采用了 知乎每日新闻的 RSS https://plink.anyfeeder.com/zhihu/daily 运行返回结果如下[ { "title":"为什么古代中国没有其它文明那么多大型石砌建筑？", "link":"https://daily.zhihu.com/story/9785710", "pubDate":"Invalid Date", "content":" \n \n\n \n\n\n\n \n\n \n\n\n\n\n \n \n\n \n\n \n \nBrian C，有问题请私信加微信\n查看知乎原文\n \n\n \n 为什么古代中国没有其它文明那么多大型石砌建筑？ \n 因为任何大型建筑本质上就是两门科学“物理和数学”的应用。 \n 物理学上讲， \n 中国古代人无法把向下的力转化为水平的力， ...这些内容随后被送入 AI 做内容提炼与聚合AI 总结这一节点是整个工作流的核心我这里用的不是官方 OpenAI 接口，而是一个第三方聚合平台，所以 n8n 里直接用 HTTP Request 节点 来调用 AI。看起来很高端，其实流程非常简单：就是发一个接口，把 prompt 丢进去，让 AI 返回你想要的文本或是一些其他的如图片等拿到结果后，再用 n8n 的 Code 或 Set 节点稍微处理一下，就能继续用在下一步流程（例如生成 Markdown、渲染 HTML、推送消息）流程其实不复杂：AI Summary：生成结构化日报AI Image：根据日报生成海报（Base64 返回）Code / Set 节点：轻度加工 AI 输出只要能发送 HTTP 请求，就能调用任何模型，n8n 在这里就像一个完美的调度层渲染处理我使用的生图模型是 gemini-3-pro-image-preview，它输出的是 Base64为了在浏览器中体现，我做了三步：把 Base64 拼成 markdown 图片语法用 markdownToHTML 转成 Webhook Response 输出 HTML 页面你可以在这里下载此次的 workflow: My first workflow.json一些开源的 N8N 项目n8n 官方示例库N8N‑Telegram‑News‑Agentn8n‑workflows我第一次用 n8n 的一些感受作为一个第一次接触 n8n 的新手，我的整体体验是非常惊喜的本以为会需要一些学习成本，实则不然上手速度非常快可视化编辑比写代码更直观每一步都能实时看到输出，非常适合调试和 AI 搭配后，真正实现自动化过去我们做自动化，往往要：写 Python / Node.js 脚本搭建定时任务平台, 设置定时任务自己处理 API 调用手动处理数据维护项目结构排查错误而 n8n 给我最大的感受是：你不再需要写多少代码了，你只是在 表达你的流程以前需要几百行脚本，现在可能只需要：一个 HTTP 节点: 信息获取一个 AI 处理: AI 信息加工各种中间件节点: 对内容进行定制化加工响应节点: 通知或是渲染这一切都是可视化的我觉得 n8n 的核心价值在于：让自动化从“写代码”变成“表达业务逻辑”让 AI 从“单次调用”变成“自动化协作”你拼装的不是流程，而是 一个能自主处理信息的 AI 工作代理人（AI Agent Workflow）这对我来说非常震撼期待接下来深入 n8n，继续折腾更多自动化项目！
为什么 includes 让内存暴涨，而 SQL 子查询却如此高效 —— Rails N+1 查询的真相本文基于 Akshay Khot 的文章 和本地实验结果，深入剖析 Rails 中 N+1 查询的本质、为什么 includes 会消耗更多内存，以及 SQL 子查询（Subquery）在时间与空间上的取舍。🧩 什么是 N+1 查询在 Rails 开发中，N+1 查询是常见的性能陷阱。以 User 和 Post 两个模型为例：class User < ApplicationRecord has_many :posts end class Post < ApplicationRecord belongs_to :user end控制器中简单地读取文章列表：def index @posts = Post.order(created_at: :desc) end在视图中渲染每篇文章及其作者：<% @posts.each do |post| %> <%= post.title %> User：<%= post.user.name %> <% end %>这段代码背后会执行：一条查询读取所有 posts，随后对每个 post 再单独查询对应的 user。形成了“一条主查询 + N 条子查询”的模式，被称作 N+1 查询。当数据量较大时，多次数据库往返会显著拉长响应时间。⚙️ 性能分析工具：Rack Mini Profiler为了更好地观察这些查询带来的开销，我们使用 Rack Mini Profiler。在开发环境中安装并配置后，在浏览器 URL 后加 ?pp=profile-memory 可以看到页面顶部的性能面板，显示 SQL 查询次数、执行时间和内存占用等信息。🧪 实验环境与数据生成使用 Rails 8 和 SQLite 创建简化的 users、posts 表。每个用户生成 5 000 篇文章，共两名用户，以便观察不同查询方式的差异。⚖️ 三种方案的性能对比性能对比一览模式查询数页面加载时间SQL 总耗时内存分配Ruby 对象数N+1 查询10 0015.8 s146.3 ms5.9 KB86includes2188 ms56.7 ms455.4 KB3675子查询10 0015.7 s128.8 ms7.2 KB91🚨 方案一：N+1 查询在控制器中简单地 @posts = Post.order(created_at: :desc)，然后在视图中访问 post.user.name。这种方式代码简洁，但会导致每次访问关联字段都单独执行一次查询。截图： 下图展示了 N+1 查询在 Rack Mini Profiler 中的总览。可以看到页面渲染时间超过 5.8 秒，SQL 概述中显示查询次数达到 10001 条。内存分配： 另一张图给出了 N+1 查询的内存分配情况小结：简单但最慢——因为往返次数爆炸⚙️ 方案二：includes 预加载通过 includes 让 Rails 预先加载所有关联用户，减少数据库往返：@posts = Post.includes(:user).order(created_at: :desc)Rails 会先取出所有 posts，再批量查询这些 post.user 对象：截图： Rack Mini Profiler 显示 includes 方式只发起了 2 次查询，页面响应时间从 8 秒级降至约 188 毫秒内存占用： includes 虽然快了很多，但在内存里实例化了用户对象，仍有一定开销。测试中分配的内存为 455.4 kb，生成了 3675 个对象小结：用内存换查询次数。当仅需要某个字段时，把整批关联对象都载入内存，往往不划算。🧮 方案三：SQL 子查询（Subquery）第三种方式是将关联逻辑交给数据库，通过子查询直接把 users.name 字段放在 posts 查询结果中：subquery = User .select(:name) .where("users.id = posts.user_id") .limit(1) .to_sql @posts = Post .select("posts.*, (#{subquery}) AS author_name") .order(created_at: :desc)生成的 SQL 如下：SELECT posts.*, (SELECT users.name FROM users WHERE users.id = posts.user_id LIMIT 1) AS author_name FROM posts ORDER BY created_at DESC;理论上，这能减少 Rails 与数据库的往返和对象生成。但在测试环境中，每一条 post 的行都执行了一次子查询：截图： Rack Mini Profiler 提示页面加载时间约 5.7 秒，SQL 概述中显示 10001 条查询内存占用： 使用子查询方式时，内存使用量为约 7.2 KB，对象数约 91小结：让数据库干擅长的事。对于「每行只需要一个派生字段」的场景，子查询通常**更快且更省内存。🔍 Rails N+1 查询的本质懒加载机制： ActiveRecord 天生按需查询，在视图里访问关联对象时才执行 SQL，这导致循环里触发多次查询。慢在往返，不在 SQL： N+1 查询慢在无谓的数据库往返，而不是 SQL 执行本身。即使单次查询只有几毫秒，累加起来仍然很慢。includes 的权衡： 通过提前加载关联对象来减少往返，但需要额外内存。Subquery 的局限： 如果每行数据都触发一次子查询，虽然减少了 Ruby 对象生成，但查询次数仍然是 N+1 级别，整体反而更慢。⚖️ 开发中的选择建议场景推荐方案理由数据量小（<20 条），需要多个关联字段.includes减少查询次数，代码直观数据量大，仅需单字段聚合子查询或聚合函数内存占用小，减少对象创建高频访问且字段变化不大缓存或反规范化字段减少实时查询，提升速度💡 总结N+1 查询本质问题在于数据库往返次数过多includes 通过预加载在内存中构建关联，适合小数据集子查询能减少 Ruby 对象生成，但如果每行都执行子查询，响应时间不一定快根据场景选择合适的策略，让数据库做它擅长的工作，Rails 应该更专注于渲染最后，想送给自己，也送给大家一句话：纸上得来终觉浅，绝知此事要躬行。仅仅是看一遍、懂了大概或是一知半解，并不能真正掌握。唯有亲自动手实践，才能体会其中的重点与要领。希望大家在遇到问题时，不妨多尝试自己解决，在行动中积累经验，才能真正看清自己的实力与成长。参考链接：Fix N+1 Queries Without Eager Loading Using SQL Subqueries – Akshay Khot
Vibe Coding 中，如何提高开发效率随着 AI 技术的发展，越来越多的开发者开始尝试 Vibe Coding ——也就是把 AI 当作“结对编程的伙伴”，通过对话的方式完成代码编写、调试和优化一、现阶段主流 AI 工具对比目前我常用到的 AI 模型有 GPT-5、Gemini 2.5（Pro/Flash）以及 Claude 4.x。不同工具各有优势，适合的场景也不一样。工具优势适用场景GPT-5语言理解和推理能力极强，支持复杂的多轮对话和跨领域问题；适合生成计划以及修复 BUG复杂项目、跨栈开发、需要较高逻辑推理的任务Gemini 2.5 Pro上下文记忆更持久，长对话能力强，适合规划大型项目需要详细讨论和规划的长期开发Claude 4.x编码能力极强，且 UI 方面也非常不错。Plan 模式有助于结构化开发，代码逻辑清晰需要详细规划、文档和代码整合的场景补充：作为全栈开发，GPT-5 往往能提供更强的逻辑和上下文理解，利用其强大的理解能力，在配合上 Claude 的 Plan 模式。做到 先规划在执行 的场景，这样的开发会很流畅二、Vibe Coding 中常见问题在实际项目中利用 AI 开发，新手往往会遇到包括但不限于以下问题：1. Prompt 不清晰案例：我在 xxx 项目中想加一个团队管理功能，直接对 GPT-5 说“帮我做一个团队系统”，它给我生成了一个全新的 Team 模型和用户关联，但忽略了代码或者模板本身提供的如 Jumpstart 内置的组织/用户模型结构的功能。导致结果冲突很多，花了不少时间重构。重复造轮子2. 功能过于宽泛案例：我让 AI “帮我实现订阅功能”，它直接写了 Stripe 集成的控制器和视图，AI 可能会直接去帮你实现，但是确实按照了它的逻辑，如果你没有主动告诉 AI 你已经有部分实现的代码，那么大概率最终代码重复且冲突。3. 缺乏计划阶段案例：让 Claude 帮我“加一个发票模块”，Claude 写的代码看似完整，但路由、前端样式和 TailwindCSS 组件完全没有和前端模板对齐，导致的后果就是，同一个项目，像是不同的人在施工，参杂了各种冗余且复杂的代码。如果在一开始有计划的先让 AI 输出“发票模块所需的字段、数据库迁移、UI 组件和权限控制”，再进入编码，效率会更高。4. 上下文管理不当案例：在一次对话中不断添加新需求，比如“加多租户支持”“再加 API Key 管理”，但是没有维护一份需求文档。导致 GPT-5 忘记了我项目已经使用 Apartment gem 做多租户，后来又建议我用另外一种方案，冲突严重。三、解决思路与效率提升方法针对上述问题，总结出以下几条经验：1. 提升 Prompt 质量在 Vibe Coding 中，Prompt 就是需求说明书。AI 能输出什么样的结果，往往取决于你给它的输入。模糊的 Prompt 会让 AI 胡乱发挥，而一个精准的 Prompt 则能让 AI 像资深开发一样给出贴合需求的代码。Prompt 的作用明确边界：告诉 AI 该做什么、不该做什么，避免生成多余或冲突的代码。统一风格：通过 Prompt 说明项目用的框架、依赖和代码风格（例如 Rails 8 + Hotwire + TailwindCSS + Flowbite），让 AI 始终保持一致。减少返工：当 Prompt 已经涵盖关键细节，AI 生成的结果往往能直接使用，而不是需要开发者大规模重写。为什么好的 Prompt 能事半功倍降低沟通成本：把复杂需求转化成清晰的文字，AI 能一次性理解，大幅减少多轮交互。提高输出质量：明确的上下文信息，让 AI 生成的代码更可维护，避免“屎山代码”。提升团队效率：在多人协作时，标准化的 Prompt 能确保每个开发者和 AI 的交互保持一致，不会因为表达差异而导致风格混乱。优秀 Prompt 的参考资源在 GIthub 里，已经有不少优秀的 Prompt 示例可以直接拿来学习和套用：awesome-chatgpt-prompts 经典的 Prompt 收集库，涵盖开发、调试、代码审查等场景。prompt-engineering-for-developers 面向开发者的 Prompt 工程指南，详细解释了如何写高质量 Prompt，以及不同场景下的策略。高质量 Prompt 不仅告诉 AI 做什么，还明确了 如何做、用什么技术做，结果往往能直接落地，极大提升效率。2. 分解任务，逐步引导在 Vibe Coding 中，最容易犯的错误就是“一股脑把需求抛给 AI”。这样做的结果往往是：AI 给出一大段貌似完整的代码，但和你的项目框架、依赖不兼容，甚至出现“屎山代码”。解决办法有两个核心原则：任务拆解：把大需求分成小模块，逐步实现；计划先行：让 AI 在动手写代码前，先输出一份开发计划。实践案例：文件下载并上传功能模型设计添加 FileRecord 模型，字段包括 user_id、file_name、status 等确认文件存储策略（本地/云存储）下载逻辑在拿到文件直链后直接在本地服务器上使用 xxx 类中的方法进行下载。上传逻辑使用 Active Storage 进行上传操作测试覆盖编写 RSpec 系统测试，验证下载链接、上传逻辑和文件状态更新的正确性。用 Plan 模式提升质量在 Claude 或 GPT-5 中，可以直接让它先生成“计划”而不是代码，这样做的好处是：减少返工：先验证方案对不对，再让 AI 编码；方便协作：团队可以先讨论 AI 给的计划，然后再落地；保持一致性：避免不同模块之间风格和实现不统一。总结一句话：把需求拆小，并让 AI 先写“计划书”，再写“代码”。这样 Vibe Coding 才能真正做到高效，而不是边改边救火。4. 维护项目记忆可以新建一个 project_context.md 文件，记录：项目架构，功能代码风格编码前后是否要运行某些检查脚本当前开发进度我在 project_context.md 中写清楚了这些，后续和 AI 对话时，只要贴上这份文件，它就不用再反复强调项目的事情了四、总结（感受）在实际项目的开发中，我最大的感受是：AI 是一个极好的生产力放大器，但前提是你必须先帮它把边界划清楚。（也就是把 AI 当作傻子看待，不要以为它能理解）比如在实现团队订阅功能时，如果我只是简单说“帮我加订阅”，AI 生成的代码大概率和 Jumpstart 内置的结构冲突；但当我明确说明“基于 Jumpstart 的 Pay gem，在团队维度做订阅，并使用 Flowbite 组件做 UI”，AI 给的代码几乎可以直接落地Vibe Coding 的关键不在于“AI 能写多少代码”，而在于“你能否把需求描述清楚、拆分合理，并维护上下文”。一旦这些做到位，AI 就能帮你完成大量的重复劳动，而你只需要把控架构和关键实现从我的经验来看，Vibe Coding 更像是带着一个随叫随到的天才傻子写代码
本篇文章只记录 索引优化 和 SQL优化索引优化有无索引对比准备 test 表CREATE TABLE `test` ( `id` bigint NOT NULL, `name` char(4) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;插入 100w 数据import pymysql import random import string import time # 数据库配置 (根据实际情况修改) DB_CONFIG = { 'host': 'localhost', 'user': 'your_username', 'password': 'your_password', 'database': 'your_database', 'charset': 'utf8mb4' } # 生成随机4字符名称 def random_name(): return ''.join(random.choices(string.ascii_letters + string.digits, k=4)) def insert_massive_data(): try: # 连接数据库 conn = pymysql.connect(**DB_CONFIG) cursor = conn.cursor() print("开始插入数据...") start_time = time.time() # 批量插入配置 batch_size = 5000 # 每批插入量 total_records = 1_000_000 batch_count = total_records // batch_size # 循环插入批次数据 for batch in range(batch_count): # 构建批量数据 data = [] for offset in range(batch_size): id_val = batch * batch_size + offset + 1 data.append((id_val, random_name())) # 执行批量插入 sql = "INSERT INTO test (id, name) VALUES (%s, %s)" cursor.executemany(sql, data) conn.commit() # 打印进度 if (batch + 1) % 10 == 0 or batch == 0: processed = (batch + 1) * batch_size print(f"已插入: {processed:,} 条, 进度: {processed/total_records:.1%}") # 插入剩余数据（如有） remaining = total_records % batch_size if remaining: data = [] start_id = total_records - remaining + 1 for i in range(remaining): data.append((start_id + i, random_name())) cursor.executemany(sql, data) conn.commit() print(f"最后一批插入完成: {remaining} 条") # 统计结果 duration = time.time() - start_time print(f"插入完成! 总共插入 {total_records:,} 条记录") print(f"耗时: {duration:.2f} 秒") print(f"平均速度: {total_records/duration:,.0f} 条/秒") except pymysql.Error as e: print(f"数据库错误: {e}") conn.rollback() finally: if conn: conn.close() if __name__ == "__main__": insert_massive_data()有索引情况无索引情况总结性能对比分析查询类型查询语句执行时间扫描行数​​有索引情况​​SELECT * FROM test WHERE id = 7894560.00s1​​无索引情况​​SELECT * FROM test WHERE name = 'abc'0.16s1,000,000关键发现​​索引的性能优势​​：​​数量级差异​​：索引查询仅扫描 1 行，而无索引查询扫描全部 100 万行​​时间差异​​：索引查询接近 0 秒(0.00s)，无索引查询耗时 0.16 秒​​全表扫描的代价​​：数据库必须遍历每一条记录进行匹配I/O 操作量呈线性增长对性能的影响随数据量增大呈指数级增加​​主键索引的特殊优势​​：B+ 树结构实现 O(log n)时间复杂度的查找不需要回表查询（索引即包含所有数据）索引选择建议索引类型适用场景优点缺点​​主键索引​​唯一标识的记录查询最佳性能，数据物理排序只能有一个​​普通索引​​WHERE/JOIN/ORDER BY 字段提高特定条件查询性能需要额外空间​​复合索引​​多条件组合查询覆盖索引避免回表顺序敏感​​前缀索引​​长文本字段（char/varchar）节省索引空间可能降低精度合理使用索引可以在百万级数据环境中实现查询响应时间的量级提升！最左前缀原则最左前缀原则（Leftmost Prefix Principle）​​ 是数据库索引优化中针对​​复合索引（Combined Index，也称组合索引或多列索引）​​ 的核心使用规则。该原则规定了复合索引只有在满足特定条件下才能被有效利用，确保索引发挥最大效能。简单来说 当 SQL 查询条件​​从左向右连续匹配​​复合索引的列顺序时，索引才能生效。如同"连续开锁"：必须从第一把锁开始按顺序打开，不能跳过或打乱顺序。准备 user 表CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, country_code CHAR(2) NOT NULL COMMENT '国家代码，如CN/US', -- 高频查询字段 province VARCHAR(20) NOT NULL COMMENT '省份/州', -- 常用查询字段 city VARCHAR(30) NOT NULL COMMENT '城市', -- 常用查询字段 district VARCHAR(30) COMMENT '区县', -- 较少查询 gender ENUM('M','F','O') DEFAULT 'O' COMMENT '性别', -- 低区分度字段 birth_date DATE NOT NULL COMMENT '出生日期', -- 范围查询字段 register_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', last_login_time DATETIME COMMENT '最后登录时间', phone VARCHAR(20) UNIQUE COMMENT '手机号', email VARCHAR(50) UNIQUE COMMENT '邮箱', hashed_password CHAR(128) NOT NULL COMMENT '密码哈希值', INDEX idx_loc (country_code, province, city), -- 复合索引1 INDEX idx_activity (register_time, last_login_time), -- 复合索引2 INDEX idx_profile (birth_date, gender) -- 复合索引3 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;插入 100w 数据import pymysql import random import string import time from datetime import datetime, timedelta # 数据库配置（根据实际情况修改） DB_CONFIG = { 'host': 'localhost', 'user': 'root', 'password': 'your_password', 'database': 'your_database', 'charset': 'utf8mb4' } # 国家省份城市数据（简化版） COUNTRIES = [ ('CN', '中国'), ('US', '美国'), ('JP', '日本'), ('KR', '韩国'), ('UK', '英国') ] # 中国省份和城市 CN_PROVINCES = [ ('Guangdong', ['Guangzhou', 'Shenzhen', 'Dongguan']), ('Jiangsu', ['Nanjing', 'Suzhou', 'Wuxi']), ('Zhejiang', ['Hangzhou', 'Ningbo', 'Wenzhou']), ('Beijing', ['Beijing']), ('Shanghai', ['Shanghai']) ] # 美国州和城市 US_STATES = [ ('California', ['Los Angeles', 'San Francisco', 'San Diego']), ('Texas', ['Houston', 'Dallas', 'Austin']), ('New York', ['New York City', 'Buffalo', 'Rochester']) ] # 其他国家主要城市 OTHER_CITIES = ['Tokyo', 'Seoul', 'London', 'Paris', 'Singapore'] # 生成唯一手机号（确保百万数据中唯一） def random_phone(user_id): # 前三位：固定为1 + 3X（30-39） prefix = "1" + str(random.randint(30, 39)) # 中间4位：基于用户ID和批次计算 mid = f"{(user_id // 1000) % 10000:04d}" # 后4位：基于用户ID确保唯一 suffix = f"{user_id % 10000:04d}" return prefix + mid + suffix # 生成唯一邮箱（确保百万数据中唯一） def random_email(user_id): domains = ["gmail.com", "yahoo.com", "hotmail.com", "example.com"] return f"user{user_id}@{random.choice(domains)}" # 生成随机地址 def random_location(): # 70%概率为中国 if random.random() < 0.7: country = 'CN' province_data = random.choice(CN_PROVINCES) province = province_data[0] city = random.choice(province_data[1]) else: country = random.choice(['US', 'JP', 'KR', 'UK']) if country == 'US': province_data = random.choice(US_STATES) province = province_data[0] city = random.choice(province_data[1]) else: province = country city = random.choice(OTHER_CITIES) district = f"District {random.randint(1, 20)}" return country, province, city, district # 生成随机生日（18-65岁） def random_birthdate(): end = datetime.now() - timedelta(days=365 * 18) start = end - timedelta(days=365 * 47) # 18-65岁 random_days = random.randint(0, (end - start).days) return (start + timedelta(days=random_days)).date() # 生成随机注册时间（2020-2023） def random_register_time(): start = datetime(2020, 1, 1) end = datetime(2023, 12, 31) random_seconds = random.randint(0, int((end - start).total_seconds())) return start + timedelta(seconds=random_seconds) # 生成用户密码哈希（简化版） def random_password_hash(): salt = ''.join(random.choices(string.ascii_letters + string.digits, k=8)) return f"sha256${salt}${''.join(random.choices(string.hexdigits, k=64))}" def insert_massive_users(): total_records = 1_000_000 batch_size = 5000 batch_count = total_records // batch_size start_time = time.time() conn = pymysql.connect(**DB_CONFIG) cursor = conn.cursor() print(f"开始向user表插入{total_records:,}条数据...") for batch in range(batch_count): data = [] base_id = batch * batch_size + 1 for i in range(batch_size): user_id = base_id + i country, province, city, district = random_location() gender = random.choice(['M', 'F', 'O']) birth_date = random_birthdate() register_time = random_register_time() # 10%的用户没有最后登录时间 last_login = register_time + timedelta(days=random.randint(1, 365)) if random.random() < 0.1: last_login = None phone = random_phone(user_id) email = random_email(user_id) password = random_password_hash() data.append(( user_id, country, province, city, district, gender, birth_date, register_time, last_login, phone, email, password )) # 执行批量插入 sql = """ INSERT IGNORE INTO user ( id, country_code, province, city, district, gender, birth_date, register_time, last_login_time, phone, email, hashed_password ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ try: cursor.executemany(sql, data) conn.commit() except pymysql.err.IntegrityError as e: print(f"检测到重复条目错误: {e}") print("正在处理重复项...") # 逐条处理失败的数据 success_count = 0 for record in data: try: cursor.execute(sql, record) conn.commit() success_count += 1 except pymysql.err.IntegrityError: # 为手机号添加后缀修复冲突 record_list = list(record) record_list[9] += str(random.randint(0, 9)) # 在手机号后加随机数字 cursor.execute(sql, tuple(record_list)) conn.commit() success_count += 1 print(f"处理完成: {success_count}/{len(data)} 条数据成功插入") # 打印进度 processed = (batch + 1) * batch_size if (batch + 1) % 10 == 0 or batch == 0: progress = processed / total_records * 100 print(f"已插入: {processed:,}条 ({progress:.1f}%)") # 插入剩余记录 remaining = total_records % batch_size if remaining: data = [] base_id = batch_count * batch_size + 1 for i in range(remaining): user_id = base_id + i country, province, city, district = random_location() gender = random.choice(['M', 'F', 'O']) birth_date = random_birthdate() register_time = random_register_time() last_login = register_time + timedelta(days=random.randint(1, 365)) if random.random() < 0.1: last_login = None phone = random_phone(user_id) email = random_email(user_id) password = random_password_hash() data.append(( user_id, country, province, city, district, gender, birth_date, register_time, last_login, phone, email, password )) try: cursor.executemany(sql, data) conn.commit() print(f"最后一批插入完成: {remaining}条") except pymysql.err.IntegrityError: for record in data: try: cursor.execute(sql, record) conn.commit() except pymysql.err.IntegrityError: record_list = list(record) record_list[9] += str(random.randint(0, 9)) cursor.execute(sql, tuple(record_list)) conn.commit() # 统计结果 duration = time.time() - start_time print(f"插入完成! 总共插入 {total_records:,} 条用户记录") print(f"耗时: {duration:.2f} 秒") print(f"平均速度: {total_records/duration:,.0f} 条/秒") cursor.close() conn.close() if __name__ == "__main__": insert_massive_users()最左前缀原则演示查询复合索引 1：idx_loc (country_code, province, city)-- ✅ 有效查询1: 最左列精确匹配 SELECT * FROM user WHERE country_code = 'CN'; -- ✅ 有效查询2: 连续两列精确匹配 SELECT * FROM user WHERE country_code = 'US' AND province = 'California'; -- ✅ 有效查询3: 全三列精确匹配 SELECT * FROM user WHERE country_code = 'JP' AND province = 'Tokyo' AND city = 'Shibuya'; -- ❌ 无效查询1: 缺少最左列 SELECT * FROM user WHERE province = 'Guangdong' AND city = 'Shenzhen'; -- ❌ 无效查询2: 跳过中间列 SELECT * FROM user WHERE country_code = 'KR' AND city = 'Seoul'; -- ⚠️ 部分有效查询: 最左列范围查询 SELECT * FROM user WHERE country_code IN ('CN', 'TW') AND province = 'Fujian'; -- 仅country_code使用索引复合索引 2：idx_activity (register_time, last_login_time)-- ✅ 有效查询: 最左列范围查询 + 后续列精确 SELECT * FROM user WHERE register_time >= '2023-01-01' AND last_login_time IS NOT NULL; -- ❌ 无效查询: 非最左列单独查询 SELECT * FROM user WHERE last_login_time < '2023-06-01'; -- ⚠️ 特殊情况: 最左列精确 + 第二列范围 SELECT * FROM user WHERE register_time = '2023-05-15' AND last_login_time BETWEEN '2023-06-01' AND '2023-06-30'; -- 两列均可使用索引复合索引 3：idx_profile (birth_date, gender)-- ✅ 有效查询: 低区分度字段结合其他条件 SELECT COUNT(*) FROM user WHERE birth_date BETWEEN '1990-01-01' AND '1999-12-31' AND gender = 'F'; -- 出生日期筛选后性别区分度变高 -- ❌ 无效查询: 单独低区分度字段 SELECT * FROM user WHERE gender = 'M'; -- 全表扫描更高效性能对比测试数据查询场景使用索引10 万行耗时未用索引耗时国家 + 省份精确查询idx_loc2ms120ms城市单独查询(未使用)105ms105ms注册时间范围 + 最后登录精确idx_activity5ms95ms单独性别查询(全表扫描)90ms90ms索引设计实践建议1. 复合索引顺序黄金法则-- 推荐顺序：高频精确查询 → 范围查询 → 低区分度字段 CREATE INDEX idx_optimal ON user ( country_code, -- 精确过滤 >90% register_time, -- 范围过滤剩余10% gender -- 低区分度，但前两列过滤后更有效 );2. 覆盖索引设计-- 创建包含查询列的复合索引 CREATE INDEX idx_covering ON user ( country_code, province, city ) INCLUDE (register_time, last_login_time); -- MySQL 8.0+ 支持 -- 查询完全通过索引完成 SELECT country_code, province, register_time FROM user WHERE country_code = 'CN' AND province = 'Jiangsu';3. 防止索引失效的优化写法-- 原始低效查询： SELECT * FROM user WHERE YEAR(birth_date) = 1995; -- 函数导致索引失效 -- 优化写法： SELECT * FROM user WHERE birth_date BETWEEN '1995-01-01' AND '1995-12-31'; -- 模糊查询优化： -- 原始：WHERE city LIKE '%York' -- 前模糊索引失效 -- 优化：WHERE city LIKE 'New%' -- 前缀匹配可用索引索引失效情况1. 索引列参加了运算失效原因​​：当在 WHERE 子句中对索引列进行运算或函数处理时，优化器无法直接定位索引值-- 索引失效 SELECT * FROM users WHERE YEAR(birth_date) = 1990; -- 优化方案（使用范围查询） SELECT * FROM users WHERE birth_date BETWEEN '1990-01-01' AND '1990-12-31';优化建议​​：避免对索引列使用函数（UPPER(), LOWER(), DATE_FORMAT() 等）将运算移至等号右侧对于日期类型，使用范围查询代替日期函数2. 索引列使用了模糊查询，以 % 开始失效原因​​：前通配符破坏了索引的有序性-- 索引失效（全表扫描） SELECT * FROM products WHERE name LIKE '%apple%'; -- 优化方案（使用全文索引） ALTER TABLE products ADD FULLTEXT(name); SELECT * FROM products WHERE MATCH(name) AGAINST('apple');优化建议​​：限制后缀模糊查询（LIKE 'apple%'）使用全文检索（FULLTEXT）替代 LIKE %% 查询对于短文本考虑前缀索引（INDEX(name(10))）3. 索引列是字符串类型char varchar，但查询时省略了 '' 单引号失效原因​​：数据库会进行隐式类型转换，导致无法使用索引-- 索引失效（phone为varchar类型） SELECT * FROM users WHERE phone = 13800138000; -- 优化方案（正确使用引号） SELECT * FROM users WHERE phone = '13800138000';优化建议​​：始终为字符串值添加引号使用 ORM 框架的严格类型绑定开启 SQL_MODE 的 STRICT_TRANS_TABLES 避免隐式转换4. 查询条件中有 or ，有未添加的索引字段失效原因​​：MySQL 处理 OR 条件时，需要合并多个结果集-- 假设email有索引但mobile无索引 SELECT * FROM users WHERE email = 'test@example.com' OR mobile = '13800138000'; -- 优化方案（使用UNION或索引覆盖） (SELECT * FROM users WHERE email = 'test@example.com') UNION ALL (SELECT * FROM users WHERE mobile = '13800138000')优化建议​​：为 OR 条件中的所有字段添加单独索引使用 UNION ALL 替代 OR调整查询逻辑避免混合条件5. 查询的符合条件的记录在表中占比过大失效原因​​：当查询返回超过表行数的 15-20% 时，全表扫描比索引更快-- 假设status=1占全表80% SELECT * FROM orders WHERE status = 1; -- 优化方案（分批处理） SELECT * FROM orders WHERE status = 1 AND id > 10000 LIMIT 5000; -- 使用游标分页优化建议​​：添加限制条件（时间范围、ID 区间等）强制索引（FORCE INDEX(index_name)）分批查询 + 游标处理大数据集6. is null 和 is not null 的相关问题失效原理​​：当 NULL 值比例很低时（<5%），IS NULL 可能使用索引当 NULL 值比例很高时（>30%），IS NOT NULL 可能使用索引多数情况下 IS NOT NULL 会全表扫描类似 第5 查询的符合条件的记录在表中占比过大-- 最坏情况（全表扫描） SELECT * FROM users WHERE last_login_time IS NOT NULL; -- 优化方案（使用范围查询） SELECT * FROM users WHERE last_login_time > '1970-01-01';​​优化建议​​：避免可空字段，设置默认值对高选择性的 IS NULL 添加筛选索引：使用 COALESCE() 函数转换 NULL 值：指定索引(强制索引)当 MySQL 优化器没有选择最优索引时，我们可以强制指定索引来实现更高效的查询使用场景：优化器错误选择了效率较低的索引表中有多个相似索引但特定场景需要特定索引测试比较不同索引的性能差异-- 强制使用特定索引 SELECT * FROM orders FORCE INDEX (idx_order_date) WHERE customer_id = 100 AND order_date > '2023-01-01'; -- 建议使用索引（优化器可能拒绝） SELECT * FROM orders USE INDEX (idx_customer) WHERE status = 'shipped' AND amount > 1000; -- 忽略特定索引 SELECT * FROM orders IGNORE INDEX (idx_status) WHERE create_time < '2023-06-01';覆盖索引当索引包含所有查询字段时，称为覆盖索引，此时查询无需回表-- 包含所有查询字段的复合索引 CREATE INDEX idx_covering ON orders ( customer_id, order_date, amount ); -- MySQL 8.0+ 使用INCLUDE子句 CREATE INDEX idx_include ON orders (order_date) INCLUDE (customer_id, amount);前缀索引针对长字符串字段（如 CHAR/VARCHAR/TEXT），仅索引字段开头部分字符适用场景：VARCHAR(255) 字段但实际平均长度短TEXT 类型字段需要索引减少复合索引大小-- 对email字段前20字符创建索引 CREATE INDEX idx_email_prefix ON users (email(20)); -- 联合索引中的前缀索引 CREATE INDEX idx_name_prefix ON users ( last_name(10), first_name(5) );评估最优前缀长度SELECT COUNT(DISTINCT SUBSTRING(字段,1, 长度)) /COUNT(*) FROM table; 查询结果越接近 1 则此长度越适合索引创建原则表数据量庞大，通常超过百万宗数据。经常出现在where，order by，group by后面的字段建议添加索引。创建索引的字段尽是具有很强的唯一性。如果字段存储文本，内容较大，一定要创建前缀索引。尽星使用复合索引，使用单列索引容易回表查询。如果一个字段中的数据不会为NULL，建议建表时添加not null约束，这样优化器就知道使用哪个索引列更加有效。不要创建太多索引，当对数据进行增删改的时候，索引需要重新重新排序。如果很少的查询，经常的增刚改不建议加索引。SQL优化order by 优化优化原理与策略当使用 ORDER BY 时，避免文件排序（filesort）是关键目标。通过合理利用索引可将时间复杂度从 O(n log n) 降低到 O(n)-- 原始查询（可能导致 filesort） SELECT * FROM orders WHERE customer_id = 100 ORDER BY order_date DESC; -- 优化方案（添加组合索引） CREATE INDEX idx_customer_date ON orders(customer_id, order_date DESC);优化技术方案：​​索引排序覆盖​​：-- 索引 (customer_id, order_date) SELECT order_id, order_date FROM orders WHERE customer_id = 100 ORDER BY order_date DESC; -- 直接使用索引排序多字段排序优化​​：-- 错误示例： SELECT * FROM products ORDER BY category, price DESC; -- 混合排序方向 -- 优化方案（统一方向）： SELECT * FROM products ORDER BY category ASC, price ASC; -- 使用索引 (category, price)限制结果集大小​​：-- 文件排序优化 SELECT*FROM logs ORDERBY create_time DESC LIMIT 100; -- 优先返回最新 100 条性能对比：方法百万数据耗时资源消耗索引排序10-50ms低内存文件排序500-2000ms高磁盘文件排序5-30s非常高group by 优化优化核心技术：通过创建合适的索引将分组操作从临时表处理转为松散索引扫描（Loose Index Scan）-- 原始低效查询 SELECT category, COUNT(*) FROM products GROUP BY category; -- 优化方案 ALTER TABLE products ADD INDEX (category);limit 优化传统分页痛点-- 深度分页性能差 SELECT * FROM orders ORDER BY id DESC LIMIT 100000, 20; -- 读取100020行后丢弃前10万优化方案索引 + 子查询 ：SELECT o.* FROM orders or JOIN ( SELECT id FROM orders LIMIT 100000, 20 ) o ON o.id = or.id主键优化设计原则​​自增 INT/BIGINT 主键​​：CREATE TABLE users ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, ... ) ENGINE=InnoDB;​​复合主键优化​​：-- 时间序列数据 CREATE TABLE metrics ( device_id INT, metric_time DATETIME(6), value FLOAT, PRIMARY KEY (device_id, metric_time) ) WITH CLUSTERING ORDER BY (metric_time DESC);​​主键性能影响对比​​：主键类型插入速度范围查询存储开销自增 INT100% (基准)优4 字节UUID35-60%差16 字节复合主键70-90%优不定​​分库分表全局 ID 方案​​：-- Snowflake ID 结构 -- 63位整型：1位符号+41位时间戳+10位节点ID+12位序列号 CREATE TABLE orders ( id BIGINT UNSIGNED PRIMARY KEY, ... );count 优化精确优化计数-- 原始（慢）： SELECT COUNT(*) FROM orders WHERE status = 'shipped'; -- 优化方案1（汇总表）： SELECT total_shipped FROM order_stats; -- 优化方案2（条件计数）： SELECT COUNT(id) FROM orders WHERE status = 'shipped'; -- 优于COUNT(*)insert 优化大文本推荐 LOAD DATA INFILE,需要先开启 set global local_infile=1;LOAD DATA INFILE '/tmp/orders.csv' INTO TABLE orders FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n';update 优化避免全表锁 ： 即尽量不要让索引失效，否则将会由行级锁变为全表锁
此文章内容记录学习 JUC 时的各种问题问题虚假唤醒​​虚假唤醒​​是多线程编程中的一个典型问题，它是指线程在未被明确通知（如 signal() 或 signalAll()）的情况下从等待状态中唤醒。这可能导致程序在条件未满足时继续执行，从而引发逻辑错误为什么会发生虚假唤醒？​​底层操作系统机制​​操作系统底层的条件变量实现（如 POSIX 的 pthread_cond_wait()）允许在无显式信号时唤醒线程（通常出于性能考虑）。Java 作为跨平台语言无法完全避免这一问题。​​中断和超时机制​​当等待的线程被外部中断（interrupt()）或达到超时时间时，也可能意外唤醒。class SharedResource { private int item = 0; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); // 生产者方法：非安全实现 public void produce() throws InterruptedException { lock.lock(); try { // 错误：使用if而非while检查条件 if (item != 0) { condition.await(); // 可能虚假唤醒后继续执行 } item++; System.out.println("生产: " + item); condition.signal(); } finally { lock.unlock(); } } // 消费者方法：非安全实现 public void consume() throws InterruptedException { lock.lock(); try { // 错误：使用if而非while检查条件 if (item == 0) { condition.await(); // 可能虚假唤醒后继续执行 } System.out.println("消费: " + item); item--; condition.signal(); } finally { lock.unlock(); } } public static void main(String[] args) { SharedResource share = new SharedResource(); for(int i=0; i<2; i++) { new Thread(() -> { for (int j = 0; j < 100; j++) { try { share.produce(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "Producer" + i).start(); } for(int i=0; i<2; i++) { new Thread(() -> { for (int j = 0; j < 100; j++) { try { share.consume(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "Consumer" + i).start(); } } }可以看到已经产生了虚假唤醒解决方案使用while代替ifclass SafeResource { private int value = 0; private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); // 安全的生产者 public void safeIncrement() throws InterruptedException { lock.lock(); try { // 修复：使用while循环防御虚假唤醒 while (value != 0) { condition.await(); } value++; System.out.println(Thread.currentThread().getName() + " 安全生产后: " + value); condition.signalAll(); } finally { lock.unlock(); } } // 安全的消费者 public void safeDecrement() throws InterruptedException { lock.lock(); try { // 修复：使用while循环防御虚假唤醒 while (value != 1) { condition.await(); } value--; System.out.println(Thread.currentThread().getName() + " 安全消费后: " + value); condition.signalAll(); } finally { lock.unlock(); } } }死锁死锁是多线程编程中的一种严重问题，指​​两个或多个线程永久阻塞​​，每个线程都在等待其他线程释放资源的状态。死锁产生的四个必要条件​​互斥条件​​：资源只能被一个线程占用​​持有等待​​：线程持有资源同时等待其他资源​​不可剥夺​​：资源不能被强制抢占​​循环等待​​：多个线程形成资源等待环示例public class DeadLockExample { public static void main(String[] args) { Object a = new Object(); Object b = new Object(); new Thread(() -> { synchronized (a) { System.out.println("获取到了锁 a, 尝试获取锁 b"); try { Thread.sleep(100L); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (b) { System.out.println("获取到了锁 b"); } } }, "A").start(); new Thread(() -> { synchronized (b) { System.out.println("获取到了锁 b, 尝试获取锁 a"); synchronized (a) { System.out.println("获取到了锁 a"); } } }, "B").start(); } }获取到了锁 a, 尝试获取锁 b 获取到了锁 b, 尝试获取锁 a解决方案从四个必要条件入手破坏请求条件破坏不可剥夺条件破坏互斥条件破坏循环等待条件练习创建四个线程，并按照 D -> B -> C -> A 的顺序执行class Control { private int flag = 0; private final int MAX_NUMBER = 4; private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); public void wait(int target) throws InterruptedException { lock.lock(); try { while(flag % MAX_NUMBER != target - 1) { condition.await(); } flag++; System.out.println(Thread.currentThread().getName()); condition.signalAll(); } finally { lock.unlock(); } } } public class Test01 { public static void main(String[] args) { Control control = new Control(); new Thread( () -> { try { control.wait(4); } catch (InterruptedException e) { throw new RuntimeException(e); } }, "A" ).start(); new Thread( () -> { try { control.wait(2); } catch (InterruptedException e) { throw new RuntimeException(e); } }, "B" ).start(); new Thread( () -> { try { control.wait(3); } catch (InterruptedException e) { throw new RuntimeException(e); } }, "C" ).start(); new Thread( () -> { try { control.wait(1); } catch (InterruptedException e) { throw new RuntimeException(e); } }, "D" ).start(); } }输出D B C A
最近看到某论坛上有关闲鱼获取详细的帖子，便有了写个脚本的想法闲鱼h5的研究可以看到咸鱼的这个这个接口便是对应的数据url: https://h5api.m.goofish.com/h5/mtop.taobao.idle.pc.detail/1.0/param: jsv=2.7.2&appKey=34839810&t=1749660271468&sign=d99215598a87fc030a45a608269f6906&v=1.0&type=originaljson&accountSite=xianyu&dataType=json&timeout=20000&api=mtop.taobao.idle.pc.detail&sessionOption=AutoLoginOnly&spm_cnt=a21ybx.item.0.0data: {"itemId":"859199211604"}cookie: ..._m_h5_tk=xxx; _m_h5_tk_enc=xxx经过我多次尝试，只有t,sign,_m_h5_tk,_m_h5_tk_enc是变化的，现在目标就很明确了编码实现t为13位时间戳，而经过查阅资料可以分析出，token: _m_h5_tk sign：token&t&appKey&data,然后取MD5_m_h5_tk会有过期时间，通过开发者工具，这个地址https://h5api.m.goofish.com/h5/mtop.gaia.nodejs.gaia.idle.data.gw.v2.index.get/1.0/在首次访问时候，在header里面的set-cookie能找到_m_h5_tk,_m_h5_tk_enc，而首次访问所携带的token必为空，也就是说，计算sign只需要t和data即可代码实现def _get_m_h5_tk(self) -> list: """获取API认证所需的_m_h5_tk令牌""" # 构建一个简单的请求数据和参数 data = '{"piUrl":"https://h5.m.goofish.com/wow/moyu/moyu-project/xy-site/pages/announcement"}' # 生成时间戳 t = str(round(time.time() * 1000)) token = "" appKey = "34839810" # token + & + t + & + appKey + & + data_str sign_string = f"{token}&{t}&{appKey}&{data}" sign = md5(sign_string.encode()).hexdigest() params = { 'jsv': "2.7.2", 'appKey': "34839810", "t": t, "sign": sign, 'api': 'mtop.gaia.nodejs.gaia.idle.data.gw.v2.index.get', 'v': '1.0', "type": "originaljson", "accountSite": "xianyu", "dataType": "json", "timeout": "20000", "sessionOption": "AutoLoginOnly", "spm_cnt": "a21ybx.item.0.0", } # API请求地址 api = 'https://h5api.m.goofish.com/h5/mtop.gaia.nodejs.gaia.idle.data.gw.v2.index.get/1.0/' response = requests.get(api, params=params) # 从响应cookies中提取_m_h5_tk cookies = response.cookies _m_h5_tk = cookies.get('_m_h5_tk', '') _m_h5_tk_enc = cookies.get('_m_h5_tk_enc', '') return [_m_h5_tk, _m_h5_tk_enc]['08c31977d09c9c8c610114029b8e17a5_1749669468863', 'b8d02be3c6b0ee2a0f5a21d8c7023b93']对应的获取商品详细也能写出来了def get_info(self, url: str): """ 获取闲鱼商品信息 """ itemId = self._get_itemId(url) if itemId is None: logging.error("获取商品ID失败") return None cookies = self._update_tokens() # 准备参数 appKey = "34839810" api = "mtop.taobao.idle.pc.detail" v = "1.0" data_str = f'{{"itemId":"{itemId}"}}' # 生成时间戳 t = str(round(time.time() * 1000)) # 从 _m_h5_tk 中提取 token token = cookies['_m_h5_tk'].split('_')[0] # token + & + t + & + appKey + & + data_str sign_string = f"{token}&{t}&{appKey}&{data_str}" sign = md5(sign_string.encode()).hexdigest() # URL 参数 params = { "jsv": "2.7.2", "appKey": appKey, "t": t, "sign": sign, "v": v, "type": "originaljson", "accountSite": "xianyu", "dataType": "json", "timeout": "20000", "api": api, "sessionOption": "AutoLoginOnly", "spm_cnt": "a21ybx.item.0.0", # "spm_pre": "a21ybx.item.itemCnxh.2.165c3da6uwAGTv", } query = '&'.join([f"{k}={quote(str(v))}" for k, v in params.items()]) url = f"https://h5api.m.goofish.com/h5/{api}/{v}/?{query}" headers = { "Host": "h5api.m.goofish.com", "Connection": "keep-alive", "sec-ch-ua-platform": '"Windows"', "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0", "Accept": "application/json", "sec-ch-ua": 'Chromium";v="136", "Microsoft Edge";v="136", "Not.A/Brand";v="99', "Content-type": "application/x-www-form-urlencoded", "sec-ch-ua-mobile": "?0", "Origin": "https://www.goofish.com", "Sec-Fetch-Site": "same-site", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Referer": "https://www.goofish.com/", "Cookie": self.xyCookie.strip() } data = f"data={quote(data_str)}" res = requests.post(url, headers=headers, data=data) res.encoding = "utf-8" if res.status_code != 200: logging.error("获取商品信息失败") return None data = self._format_item_info(res.json()) return data{'itemId': 859199211604, 'title': '出曲面屏笔记本电脑，新出厂的曲面屏[火][火]', 'desc': '出曲面屏笔记本电脑，新出厂的曲面屏[火][火]\n感兴趣的话点“我想要”和我私聊吧～', 'soldPrice': '1466', 'originalPrice': '0', 'transportFee': '0.00', 'image_urls': ['http://img.alicdn.com/bao/uploaded/i3/O1CN01uw42JI2Az5fSKioAd_!!4611686018427383553-0-fleamarket.jpg'], 'tags': [ '包邮'], 'browseCnt': 32499, 'wantCnt': 63, 'gmtCreate': 1733215983000}注意，此方法已经被我的其他方法封装过了，如self._get_itemId即获取商品id，self._format_item_info即格式化最后的json，因为数据过多，只保留我需要的即可
关于浏览器传参丢失精度的问题的解决办法
今天在写项目时候，需要前端传一个 number 类型的参数，可能由于类型过长，url接收时候直接丢失了精度，导致了一系列的错误前端源码const doEdit = () => { console.log(p.value.id) router.push({ path: '/add', query: { id: p.value.id }, }) }这里用控制台打印了 p.value.id 来确认是否是后端传参前端接收时丢失了精度，但是控制台打印是正常的结果 1927331787680264194, 但是看浏览器重重定向的地址为 http://localhost:8081/api/get/vo?id=1927331787680264200 明显是这里出现了问题问题出在 JavaScript 的数值精度限制上。JavaScript 使用 IEEE 754 双精度浮点数表示数字，最大可精确表示的整数是 2^53 - 1（约 16 位数字或 ±9007199254740991）所以出现了精度丢失的问题解决方案解决问题很简单 String(p.value.id) 只需把高精度的数字转换成 string 类型传入即可
vue使用 vue-waterfall-plugin-next 实现瀑布流Vue3瀑布流插件支持PC和移动端，包含Animate的各种动画效果以及图片懒加载功能。根据文档，可以先写出这样的一个例子 <Waterfall ref="waterfall" :list="list" :breakpoints="breakpoints"> <LazyImg class="card_img" :url="url" /> </Waterfall> 跨域问题文档提供的是本地图片，再用远程图片加载时候，不出意外地会出现跨域问题Access to image at 'https://emample.com/1.png' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. 仔细研究文档，会看到 Props参数 里面有个跨域的参数参数名类型默认值描述crossOriginBooleantrue图片加载是否开启跨域把默认值修改成 false 即可，即 <Waterfall :crossOrigin="false">