大数跨境

真实聊天GPT对话:数据获取和查询优化

真实聊天GPT对话:数据获取和查询优化 索引目录
2025-09-15
2
导读:在本系列中,我分享了我在日常编程工作中使用 ChatGPT 的真实对话。

在本系列中,我分享了我在日常编程工作中使用 ChatGPT 的真实对话。我的目标是记录将 AI 融入我的工作流程所带来的好处,反思我从这些对话中学到的东西,并希望能够帮助其他开发者更有效地使用这些工具——让我们一起学习!

本文将主要采用逐一介绍与 ChatGPT 对话的形式,同时我会添加我发送特定提示背后的思考过程、我对输出的看法以及我在对话本身之外考虑的任何其他内容。

今天的主题:处理数据库查询的输出并优化该查询。

对话链接: https://chatgpt.com/share/68c6e283-c8a4-800e-bdef-986678a4267b

背景

我目前正在开发一个 API 端点,用于在ParaProse中触发 LLM 文本补全。该功能会根据特定章节触发,并从中收集大量上下文信息,包括故事标题、描述、标签以及生成点之前的几段内容。

在我最初尝试收集这些上下文信息时,我使用我的 ORM(Drizzle)编写了一个查询,但它返回的数据格式对我来说并不是特别有用。对话从我要求 ChatGPT 编写一些格式化代码开始,最终优化了查询并修复了一个由于我对连接工作原理了解有限而导致的类型错误。

1号弯

迅速的

我目前正在从我的 drizzle orm 中获取此结果:
const result: { chapter: { id: string; storyId: string; title: string; description: string; position: number; createdAt: Date; updatedAt: Date; }; story: { id: string; userId: string; title: string; description: string; createdAt: Date; updatedAt: Date; } | null; tag: { id: string; userId: string; value: string; } | null; }[]
您能否编写一个 TypeScript 映射或 Reduce 函数来将此数组处理为章节数组:
const chapters: { id: string; storyId: string; title: string; description: string; position: number; createdAt: Date; updatedAt: Date; story: {...story fields} tags: [...tag objects] }[]

回复

明白了👍 — 你想将 Drizzle 的平面结果折叠成一个章节数组,每个章节包含一个story和一个 数组tags
以下是使用 的简洁方法reduce
type Chapter = { id: string storyId: string title: string description: string position: number createdAt: Date updatedAt: Date story: { id: string userId: string title: string description: string createdAt: Date updatedAt: Date } | null tags: { id: string userId: string value: string }[] } const chapters: Chapter[] = result.reduce<Chapter[]>((acc, { chapter, story, tag }) => { // check if chapter already exists in accumulator let existing = acc.find(c => c.id === chapter.id) if (!existing) { existing = { ...chapter, story, tags: [] } acc.push(existing) } if (tag) { // avoid duplicates if the same tag shows up multiple times if (!existing.tags.some(t => t.id === tag.id)) { existing.tags.push(tag) } } return acc }, [])
✅ 每章都是独一无二的。✅
标签被分组到一个数组中。✅
每章只包含一个故事。
您是否希望我展示一个使用 而不是 的版本Map,以便find在大型数组上获得更好的性能?

思考与分析

此时,我主要寻找的是看起来足够接近我目标的函数式代码。我已经写过很多 map/reduce 函数了,不太想再写一遍,尤其是 AI 写这类代码的能力已经相当不错了。

最初,我打算忽略 Chapter 类型,但最终还是保留了它,以便chapters 正确输入变量。我并不想让我(目前)单文件路由的顶部堆满一堆类型,弄得乱七八糟;我们稍后再讨论这个问题。我继续将代码粘贴到正确的位置,并确保所有类型都对齐,这时我意识到我遗漏了一些上下文。

第 2 弯

迅速的

您能否更新它以包含段落?
const result: { chapter: { id: string; storyId: string; title: string; description: string; position: number; createdAt: Date; updatedAt: Date; }; story: { id: string; userId: string; title: string; description: string; createdAt: Date; updatedAt: Date; } | null; tag: { id: string; userId: string; value: string; } | null; paragraphs: { id: string; chapterId: string; content: string; position: number; source: "user" | "llm"; createdAt: Date; updatedAt: Date; } | null; }[]

回复

好了——现在paragraphs也一样。我切换到MapO(n) 分组,并添加了简单的重复数据删除。
type Story = { id: string userId: string title: string description: string createdAt: Date updatedAt: Date } type Tag = { id: string userId: string value: string } type Paragraph = { id: string chapterId: string content: string position: number source: "user" | "llm" createdAt: Date updatedAt: Date } type ChapterWithRelations = { id: string storyId: string title: string description: string position: number createdAt: Date updatedAt: Date story: Story | null tags: Tag[] paragraphs: Paragraph[] } const chapters: ChapterWithRelations[] = (() => { const map = new Map<string, ChapterWithRelations>() for (const row of result) { const { chapter, story, tag, paragraphs: paragraph } = row let entry = map.get(chapter.id) if (!entry) { entry = { ...chapter, story, tags: [], paragraphs: [], } map.set(chapter.id, entry) } if (tag && !entry.tags.some(t => t.id === tag.id)) { entry.tags.push(tag) } if (paragraph && !entry.paragraphs.some(p => p.id === paragraph.id)) { entry.paragraphs.push(paragraph) } } // Optional: keep things tidy for (const ch of map.values()) { ch.tags.sort((a, b) => a.value.localeCompare(b.value)) ch.paragraphs.sort((a, b) => a.position - b.position) } return Array.from(map.values()) })()
如果您想要保留原始标签顺序而不是排序,只需删除该ch.tags.sort(...)行。

思考与分析

这是一个非常省力的提示;只需快速询问并粘贴我在查询中添加另一个连接后得到的更新类型即可。考虑到它与我们正在进行的工作密切相关,我认为 ChatGPT 可以处理它;而且我基本上是对的。

说到响应,我立刻就不喜欢那个自调用函数,因为它不太符合我的风格。我没有费心去研究代码的质量;我的直觉告诉我,这太复杂了,也许有更简单的方法。Drizzle 对我来说相对较新(最近从 TypeORM 和 Supabase 学习过来的),所以我怀疑我获取数据的方式可能出了问题。

第 3 回合

迅速的

考虑到期望的最终状态,有没有更好的方法来编写这个查询?
typescript const result = await db .select({ chapter: chaptersTable, story: storiesTable, tag: tagsTable, paragraphs: paragraphsTable, }) .from(chaptersTable) .leftJoin(storiesTable, eq(chaptersTable.storyId, storiesTable.id)) .leftJoin(storyTagsTable, eq(storiesTable.id, storyTagsTable.storyId)) .leftJoin(tagsTable, eq(storyTagsTable.tagId, tagsTable.id)) .leftJoin( paragraphsTable, eq(paragraphsTable.chapterId, chaptersTable.id) ) .where( and( eq(storiesTable.userId, session.user.id), eq(chaptersTable.id, chapterId) ) )

回复

是的——你当前的连接会造成“行爆炸”(章节 × 标签 × 段落),然后你必须在 TS 中进行重复数据删除。以下是两种更好的模式:
1)单章:进行3次紧密查询(最简单+高效)
由于您按单个进行过滤chapterId,因此请将其拆分以完全避免重复。
// 1) Chapter + story const [chapterRow] = await db .select({ chapter: chaptersTable, story: storiesTable, }) .from(chaptersTable) .leftJoin(storiesTable, eq(chaptersTable.storyId, storiesTable.id)) .where(and(eq(storiesTable.userId, session.user.id), eq(chaptersTable.id, chapterId))) if (!chapterRow) throw new Error('Not found') const { chapter, story } = chapterRow // 2) Tags for the story const tags = story ? await db .select({ id: tagsTable.id, userId: tagsTable.userId, value: tagsTable.value }) .from(storyTagsTable) .innerJoin(tagsTable, eq(storyTagsTable.tagId, tagsTable.id)) .where(eq(storyTagsTable.storyId, story.id)) : [] // 3) Paragraphs for the chapter const paragraphs = await db .select({ id: paragraphsTable.id, chapterId: paragraphsTable.chapterId, content: paragraphsTable.content, position: paragraphsTable.position, source: paragraphsTable.source, createdAt: paragraphsTable.createdAt, updatedAt: paragraphsTable.updatedAt, }) .from(paragraphsTable) .where(eq(paragraphsTable.chapterId, chapter.id)) .orderBy(paragraphsTable.position) const result = [{ ...chapter, story, tags, paragraphs }]
优点:没有行爆炸,代码最简单,单个章节的性能可预测。

2)使用聚合子查询进行一次往返(如果您要批量处理多个章节,这很有用)
每个关系使用 JSON 聚合,并连接一次。这样每章就保留一行。
import { sql } from 'drizzle-orm' // paragraphs aggregated per chapter const paragraphsAgg = db .select({ chapterId: paragraphsTable.chapterId, paragraphs: sql<{ id: string; chapterId: string; content: string; position: number; source: 'user'|'llm'; createdAt: Date; updatedAt: Date; }[]>` COALESCE( JSON_AGG( JSON_BUILD_OBJECT( 'id', ${paragraphsTable.id}, 'chapterId', ${paragraphsTable.chapterId}, 'content', ${paragraphsTable.content}, 'position', ${paragraphsTable.position}, 'source', ${paragraphsTable.source}, 'createdAt', ${paragraphsTable.createdAt}, 'updatedAt', ${paragraphsTable.updatedAt} ) ORDER BY ${paragraphsTable.position} ) FILTER (WHERE ${paragraphsTable.id} IS NOT NULL), '[]'::json ) `.as('paragraphs'), }) .from(paragraphsTable) .groupBy(paragraphsTable.chapterId) .as('p_agg') // tags aggregated per story const tagsAgg = db .select({ storyId: storyTagsTable.storyId, tags: sql<{ id: string; userId: string; value: string }[]>` COALESCE( JSON_AGG( DISTINCT JSONB_BUILD_OBJECT( 'id', ${tagsTable.id}, 'userId', ${tagsTable.userId}, 'value', ${tagsTable.value} ) ) FILTER (WHERE ${tagsTable.id} IS NOT NULL), '[]'::jsonb ) `.as('tags'), }) .from(storyTagsTable) .leftJoin(tagsTable, eq(storyTagsTable.tagId, tagsTable.id)) .groupBy(storyTagsTable.storyId) .as('t_agg') // final select: 1 row per chapter with arrays embedded const rows = await db .select({ chapter: chaptersTable, story: storiesTable, tags: tagsAgg.tags, paragraphs: paragraphsAgg.paragraphs, }) .from(chaptersTable) .leftJoin(storiesTable, eq(chaptersTable.storyId, storiesTable.id)) .leftJoin(tagsAgg, eq(tagsAgg.storyId, storiesTable.id)) .leftJoin(paragraphsAgg, eq(paragraphsAgg.chapterId, chaptersTable.id)) .where(and(eq(storiesTable.userId, session.user.id), eq(chaptersTable.id, chapterId))) const chapters = rows.map(r => ({ ...r.chapter, story: r.story, tags: r.tags ?? [], paragraphs: r.paragraphs ?? [], }))
优点:一次数据库往返,JS 中无需重复数据删除,按段落稳定排序position
注意事项:通过 Postgres JSON 函数sql\\;Drizzle 类型在聚合上进行了注释。


选择哪一个?

  • 获取单个章节
    (您的示例):选项 1(3 个查询)最简单且非常有效。
  • 一次获取多个章节
    选项 2 的扩展性更好 - 一个查询,每章一行,聚合关系。


思考与分析

这个回复有点长,但看了一眼这两个选项的标题,我决定完全专注于第一个选项。我不知道这是否是实现我所做事情的最佳方式,但浏览了一下代码后,我确信它通过了“目前足够好”的检查。

考虑到这个查询是我在这个处理程序中编写的第一批代码之一,我甚至还没有任何东西可以运行。现阶段的目标是尽快让程序运行起来,然后我可以在此基础上进行迭代——如果没有基准测试可以作为参考,就无法进行性能测试。很容易就把这个代码粘贴到我已有的代码上。

我无法运行它来验证它是否真的有效(但),但检查所有返回类型发现了一个问题。

第四回合

迅速的

我选择选项 1;但是,我的story类型是:
const story: { id: string; userId: string; title: string; description: string; createdAt: Date; updatedAt: Date; } | null
这难道不可能吗?chaptersTable.storyId 不可为空,因此它必须始终附加到故事中;story变量为空表示数据库状态已损坏,那么为什么它会被输入为| null查询返回的结果呢?

回复

好问题。| null来自连接类型,而不是来自你的架构约束。在 Drizzle 中,leftJoin总是将右侧类型设置为 可能 ,null因为 SQL可能会为该表生成 NULL——即使你的外键不可为空。Drizzle 不会在类型级别“证明”外键完整性。
修复:使用innerJoin
由于章节必须有一个故事,因此使用内部连接以便 Drizzle 可以输入story非空值。
const [row] = await db .select({ chapter: chaptersTable, story: storiesTable, }) .from(chaptersTable) .innerJoin(storiesTable, eq(chaptersTable.storyId, storiesTable.id)) // ← innerJoin .where( and( eq(storiesTable.userId, session.user.id), eq(chaptersTable.id, chapterId) ) ) if (!row) throw new Error('Not found') const { chapter, story } = row // story: Story (non-null)
为什么不保留leftJoin
  • 使用 时leftJoin,Drizzle必须输入story为,Story | null因为 SQL 允许不匹配的行(即使您的 DB 在实践中从未生成它们)。
  • 您可以使用断言非空as NonNullable<typeof story>,但这是一种类型断言,而不是安全性。

需要注意的边缘情况
如果您在 上使用RLSstoriesinnerJoin当故事对用户不可见时(这通常是您想要的), 会将章节完全过滤掉。仍然可以使用原始 SQL返回包含故事leftJoin的章节行,但您无论如何都会将其删除。 因此, 既简单又正确。NULLWHERE stories.userId = ...innerJoin
替代 API(可选)
如果您定义relations(...),Drizzle 的关系 API 可以返回具有正确可空性的嵌套对象:
const chapter = await db.query.chaptersTable.findFirst({ where: eq(chaptersTable.id, chapterId), with: { story: true, // non-null due to inner relation + FK paragraphs: true, }, })
但是对于您选择的选项 1(单独查询),切换到innerJoin是最干净的修复方法。

思考与分析

这次轮到我了,说明我对不同类型的 SQL 连接语句不够熟悉。响应中的解释似乎很合理,并且将leftJoinfor 和innerJoindid 替换为正确的类型后,story变量的类型也正确了。然而,我不想盲目地相信这个模型,因为我没有足够的知识来对响应的判断充满信心。而且,这是一个很好的机会来弥补我所知道的一些弱点!

我们去 Google 上进行了一些研究:

  • https://en.wikipedia.org/wiki/Join_(SQL)
  • W3Schools.com
  • https://www.geeksforgeeks.org/sql/sql-join-set-1-inner-left-right-and-full-joins/

在将故事表与章节表连接起来的情况下,left joininner join的操作实际上是一样的,但后者的类型更优。Aleft join会包含所有章节,并且只提取有匹配章节的故事,而 aninner join只会包含有匹配故事的章节。但是,由于章节总是有关联的故事,因此无论我们使用哪种连接,结果都会是相同的集合。

我确实简要地查看了该回复末尾提供的替代 API 选项;但我目前尚未relations在数据库模式中进行设置,并且我认为交换连接比尝试这样做更容易。

结论

总的来说,这次对话让我节省了不少手动搜索和尝试获取功能查询的时间。我怀疑,如果我手动操作的话,我最终还是会得到第 3 轮的拆分查询;但这会比与 ChatGPT 对话时不到 5 分钟得出这个结论的时间更长。

它还凸显了我在一些本该早已掌握的知识(SQL 连接)上的差距,并促使我去学习。我觉得这是一种学习新技能的绝佳方式——当你面前有一个非常具体的问题时,你可以立即将所学知识付诸实践。然而,这种方法确实需要你对 LLM 聊天机器人的说法保持怀疑态度,因为如果你盲目接受眼前的答案,可能会对你的学习进度产生不利影响。

当我发现一些特别有帮助的对话时,我会尝试继续发布类似的对话——无论是对我的工作发展有帮助,还是我从中学到新东西。写下这些对话不仅能让我最大限度地从对话中获益,也希望它能激励其他人将人工智能融入他们的工作流程,并希望你们中的一些人也能对我如何改进工作流程提出建议!


【声明】内容源于网络
0
0
索引目录
索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
内容 0
粉丝 0
索引目录 索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
总阅读0
粉丝0
内容0