当前位置: 首页 > news >正文

Graphql中的N+1问题

开篇

原文出处

Graphql 是一种 API 查询语言和运行时环境,可以帮助开发人员快速构建可伸缩的 API。然而,尽管 Graphql 可以提供一些优秀的查询性能和数据获取的能力,但是在使用 Graphql 的过程中,开发人员也会遇到一些常见问题,其中最常见的一个问题是 N+1 问题。

什么是 GraphQL 中的 N+1 问题

在 GraphQL 中,N+1 问题指的是在一个查询语句中,某个字段需要通过 N 次额外查询来获取其关联的数据,导致查询效率低下的情况。这个问题的本质是由于 GraphQL 的数据模型本身的特性引起的。

在 GraphQL 中,查询语句可以包含多个字段,每个字段可能需要访问一个不同的数据源。当查询涉及到关联数据时,如果不做特殊处理,GraphQL 会逐个获取每个字段的数据,这可能会导致大量的额外查询,进而影响查询效率。

假设我们有一个电影网站,它有电影和演员两个实体,每部电影都有多个演员。我们可以用 GraphQL 定义如下的 schema:

type Movie {id: ID!title: String!actors: [Actor!]!
}type Actor {id: ID!name: String!age: Int!
}type Query {movies: [Movie!]!
}

现在,我们想要查询所有电影及其演员。我们可以像这样编写 GraphQL 查询:

query {movies {titleactors {name}}
}

在这个查询中,我们获取了所有电影的标题,以及每部电影的所有演员的名称。然而,如果我们没有采取任何措施来解决 N+1 问题,每个电影的演员都将需要单独查询。因此,如果我们有 100 部电影,就会产生 101 次查询(1 次获取电影,100 次获取演员),这会严重影响性能。

解决方案

Data loader

Data loader 是一个常用的解决 N+1 问题的工具,它可以将多个查询合并成一个查询,以减少查询次数。它的工作原理是在执行查询时,将多个相同类型的查询合并成一个批量查询,并将结果缓存起来,以便在需要时快速获取。Data loader 可以轻松地与 GraphQL 集成,并提供了许多可配置的选项,以便根据应用程序的需要进行优化。

下面是一个使用 data loader 的示例代码:

const DataLoader = require('dataloader')
const { actorsByMovieId } = require('./db')const actorsLoader = new DataLoader(async (movieIds) => {const actors = await actorsByMovieId(movieIds)const actorsMap = actors.reduce((acc, actor) => {acc[actor.movieId] = acc[actor.movieId] || []acc[actor.movieId].push(actor)return acc}, {})return movieIds.map((movieId) => actorsMap[movieId] || [])
})const resolvers = {Query: {movies: () => getMovies(),},Movie: {actors: (movie, args, context, info) => actorsLoader.load(movie.id),},
}

在上面的代码中,我们使用 data loader 来批量获取每个电影的演员。当 GraphQL 执行查询时,它将调用 load 函数,并将所有需要获取的电影 ID 传递给它。load 函数将所有电影 ID 作为参数,并从数据库中获取所有与这些电影相关的演员。然后,它将演员按电影 ID 分组,并将结果返回到 GraphQL 查询中。由于使用了 data loader,我们现在只需要进行一次查询来获取所有电影及其演员。

Join Monster

Join Monster 是一个解决 GraphQL N+1 问题的工具,它使用了 SQL 批量操作的思想。Join Monster 的主要思想是将多个 GraphQL 解析器的数据请求合并成一个 SQL 查询。这个 SQL 查询是经过优化的,只会查询数据库中需要的数据。同时,Join Monster 还使用了多级缓存来减少数据库的访问次数。

在代码层面,使用 Join Monster 时,我们需要先定义一个解析器,然后在 GraphQL 的 schema 中使用该解析器来查询数据。以下是一个使用 Join Monster 的示例代码:

const joinMonster = require('join-monster').default
const { GraphQLObjectType, GraphQLList } = require('graphql')
const db = require('./db')
const { UserType } = require('./userType')const CommentType = new GraphQLObjectType({name: 'Comment',fields: {id: { type: GraphQLInt },content: { type: GraphQLString },user: {type: UserType,resolve: (parent, args, context, resolveInfo) => {return joinMonster(resolveInfo, {}, (sql) => {return db.query(sql)})},},},
})const Query = new GraphQLObjectType({name: 'Query',fields: {comments: {type: new GraphQLList(CommentType),resolve: (parent, args, context, resolveInfo) => {return joinMonster(resolveInfo, {}, (sql) => {return db.query(sql)})},},},
})module.exports = new GraphQLSchema({ query: Query })

在上述代码中,我们定义了一个 CommentType,它包含了一个 user 字段,该字段使用 Join Monster 进行了解析。同时,我们还定义了一个 Query,该 Query 包含了 comments 字段,使用了 joinMonster 进行解析。在 resolve 函数中,我们将 Join Monster 的解析器传入,并在其中使用了 db.query 函数执行了查询。

假设我们有如下 GraphQL 查询:

{comments {idcontentuser {idname}}
}

在使用 Join Monster 之前,该查询需要进行 N+1 次 SQL 查询,每个 comment 对应一次查询,每个 user 对应一次查询。

在使用 Join Monster 之后,我们的查询只需要进行一次 SQL 查询。Join Monster 会根据 GraphQL 查询中的字段生成相应的 SQL 查询语句,并在数据库中执行该语句。以下是 Join Monster 生成的 SQL 语句的示例:

SELECT`Comment`.`id`,`Comment`.`content`,`User`.`id` AS `user.id`,`User`.`name` AS `user.name`
FROM`Comment`
LEFT JOIN`User`
ON`Comment`.`userId` = `User`.`id`

这个 SQL 查询语句会同时返回 comments 和它们对应的 users 的信息。由于只进行了一次 SQL 查询,Join Monster 大大减少了数据库访问的次数,从而提升了性能。

方案对比

方案优点缺点适用场景
dataloader可以自动处理 N+1 查询问题;可以使用缓存机制提高性能;比较成熟稳定,社区支持度高不能自动处理多层嵌套,对复杂查询支持不够好,需要手动编写基于 dataloader 嵌套查询适用于中小规模的项目,需要快速上手,提高开发效率的场景
join-monster可以自动生成高效的 SQL 查询,性能优秀; 可以自动处理多层嵌套的 N+1 查询问题依赖于 SQL 数据库,不适用于非 SQL 数据库场景(需要将 Graphql 当作 ORM)适用于需要高性能的场景,需要处理复杂查询场景

Data loader 的实现

考虑到 dataloader 比较好实现,且使用广泛,我们选取它进行简单的实现,以此更加深入的理解它是如何解决 N+1 问题的。

根据DataLoader的使用例子来看,DataLoader除了构造器以外,只有一个 load 方法,所以一个简单的 DataLoader 的声明如下:

type BatchFn = <Key, Entity>(keys: Key[]): Promise<Entity[]>;class DataLoader<Key, Entity> {constructor(batchFn: BatchFn<Key, Entity>) {// todo}load(key: Key): Promise<Entity> {// todo}
}

load 方法只是加入到 batch 的队列中,并不会立刻执行,执行条件是“没有地方调用 load 后“,才会执行整个 batch 队列的请求。于是有了一个小实现:

class DataLoader<Key, Entity> {readonly batchFn: BatchFn<Key, Entity>;readonly keys: Key[] = [];constructor(batchFn: BatchFn<Key, Entity>) {this.batchFn = batchFn;}async load(key: Key): Promise<void> {this.keys.push(key);if (this.keys.length === 1) {this.doBatch();//I hope it executes later}}doBatch(): Promise<Entity[]> {return this.batchFn(this.keys);}
}

代码很简单,只是遗留了一个问题,也是最重要的问题,如何让this.doBatch能够延迟行,延迟到所有的 load 同步方法调用完后。

此时就需要利用事件循环来改变它的执行顺序:

setImmediate(() => this.doBatch())

因为setImmediate会在回调阶段执行,因此会等到所有同步方法完成在执行。

一个DataLoader的最小实现就产生了:

class DataLoader<Key, Entity> {readonly batchFn: BatchFn<Key, Entity>;readonly keys: Key[] = [];constructor(batchFn: BatchFn<Key, Entity>) {this.batchFn = batchFn;}async load(key: Key): Promise<void> {this.keys.push(key);if (this.keys.length === 1) {setImmediate(() => this.doBatch());}}doBatch(): Promise<Entity[]> {return this.batchFn(this.keys);}
}

可是它的功能很局限,load 方法不能返回任何的值,Graphql 的 resolve 也就解析不了了。

因此,修改如下:

export default class DataLoader<Key, Entity> {readonly batchFn: BatchFn<Key, Entity>;readonly storage: {key: Key;promise: Promise<Entity>;resolve: ((entity: Entity) => void) | null;}[] = [];constructor(batchFn: BatchFn<Key, Entity>) {this.batchFn = batchFn;}async load(key: Key): Promise<Entity> {let resolve = null;const promise = new Promise<Entity>((res) => (resolve = res));this.storage.push({key,promise,resolve,});if (this.storage.length === 1) {setImmediate(() => this.doBatch());}return promise;}doBatch(): Promise<void> {const keys = this.storage.map(({ key }) => key);return this.batchFn(keys).then((entities) =>entities.forEach((entity, index) => {const { resolve } = this.storage[index];resolve && resolve(entity);}));}
}

doBatch将结果依次给到 load 当时挂载的 promise 上,这样以来 resolver 中的 promise 状态就会由 pending 转化为 fulfilled。

当然,为了考虑性能和健壮性,我们还可以继续扩展:

  • 增加缓存
  • 捕获异常
  • 支持手动执行 batch

最终完善如下(github repo):

type BatchFn<K, E> = (keys: K[]) => Promise<E[]>;interface PromiseMeta<E> {resolve: ((entity: E) => void) | null;promise: Promise<E>;
}interface Options {immediate: boolean;
}export default class DataLoader<K, E> {readonly batchFn: BatchFn<K, E>;readonly cache = new Map<K, PromiseMeta<E>>();readonly options: Options = {immediate: true,};constructor(batchFn: BatchFn<K, E>, options?: Options) {this.batchFn = batchFn;this.options = {...this.options,...options,};}async load(key: K): Promise<E> {if (this.options.immediate) {if (this.cache.size === 0) {setImmediate(() => this.doBatch());}}let resolve = null;const promise = new Promise<E>((res) => (resolve = res));this.cache.set(key, {promise,resolve,});return promise;}doBatch(): Promise<void> {const keys = [...this.cache.keys()];return this.batchFn(keys).then((entities) =>entities.forEach((entity, index) => {const promiseMeta = this.cache.get(keys[index]);if (promiseMeta) {const { resolve } = promiseMeta;resolve && resolve(entity);}})).catch(() => this.cache.clear());}dispatch(): Promise<void> {if (!this.options.immediate) {return this.doBatch();}throw new Error("Doesn't allow to dispatch given immediate is true!");}
}

最后

在本文中,我们深入探讨了 GraphQL 中的 N+1 问题。首先,我们介绍了 GraphQL 中常见的一些问题,例如查询过度嵌套和查询重复等。然后,我们详细介绍了 N+1 问题的定义及其出现的原因。接着,我们给出了具体的例子,并讨论了 N+1 问题对性能的影响。在解决 N+1 问题方面,我们列举了几种工具,包括 Batch loading、Data loader 和 Join Monster,并展示了它们在代码层面上的使用。我们还对这些工具的优缺点进行了比较和分析,并给出了最佳实践。

最后,我们介绍了一些避免 N+1 问题的最佳实践,例如避免嵌套查询、使用 GraphQL 片段和优化查询。这些实践可以帮助开发人员避免 N+1 问题并提高查询性能。

总的来说,N+1 问题是 GraphQL 中常见的性能问题之一,但是通过合适的工具和最佳实践,我们可以有效地解决它,提高查询性能,为用户提供更好的体验。

相关文章:

Graphql中的N+1问题

开篇 原文出处 Graphql 是一种 API 查询语言和运行时环境&#xff0c;可以帮助开发人员快速构建可伸缩的 API。然而&#xff0c;尽管 Graphql 可以提供一些优秀的查询性能和数据获取的能力&#xff0c;但是在使用 Graphql 的过程中&#xff0c;开发人员也会遇到一些常见问题&…...

mysql、oracle、sqlserver常见方法区分

整理了包括字符串与日期互转、字符串与数字互转、多行合并为一行、拼接字段等一些常用的函数&#xff0c;当然有些功能实现的方法不止一种&#xff0c;这里列举了部分常用的&#xff0c;后续会持续补充。 MySQLOracleSQL Server字符串转数字 CAST(123 as SIGNED) 或 CONVERT(12…...

AcWing 4382. 快速打字

原题链接&#xff1a;AcWing 4382. 快速打字 关键词&#xff1a;双指针、判断子序列 芭芭拉是一个速度打字员。 为了检查她的打字速度&#xff0c;她进行了一个速度测试。 测试内容是给定她一个字符串 I&#xff0c;她需要将字符串正确打出。 但是&#xff0c;芭芭拉作为一…...

DataFrame.query()--Pandas

1. 函数功能 Pandas 中的一个函数&#xff0c;用于在 DataFrame 中执行查询操作。这个方法会返回一个新的 DataFrame&#xff0c;其中包含符合查询条件的数据行。请注意&#xff0c;query 方法只能用于筛选行&#xff0c;而不能用于筛选列。 2. 函数语法 DataFrame.query(ex…...

【C语言】美元名字和面额对应问题

题目 美元硬币从小到大分为1美分&#xff08;penny&#xff09;5美分&#xff08;nickel&#xff09;10美分&#xff08;dime&#xff09;25美分&#xff08;quarter&#xff09;和50美分&#xff08;half-dollar&#xff09;&#xff0c;写一个程序实现当给出一个数字面额可以…...

uniapp隐藏底部导航栏(非自定义底部导航栏)

uniapp隐藏底部导航栏 看什么看&#xff0c;要多看uni官方文档&#xff0c;里面啥都有 看什么看&#xff0c;要多看uni官方文档&#xff0c;里面啥都有 uniapp官方网址&#xff1a;uni设置TabBar // 展示 uni.showTabBar({animation:true,success() {console.debug(隐藏成功)…...

CSS background 背景

background属性为元素添加背景效果。 它是以下属性的简写&#xff0c;按顺序为&#xff1a; background-colorbackground-imagebackground-repeatbackground-attachmentbackground-position 以下所有示例中的花花.jpg图片的大小是4848。 1 background-color background-col…...

安防监控视频平台EasyCVR视频汇聚平台和税务可视化综合管理应用方案

一、方案概述 为了确保税务执法的规范性和高效性&#xff0c;国家税务总局要求全面推行税务系统的行政执法公示制度、执法全过程记录制度和重大执法决定法制审核制度。为此&#xff0c;需要全面推行执法全过程记录制度&#xff0c;并推进信息化建设&#xff0c;实现执法全过程的…...

深度学习实战50-构建ChatOCR项目:基于大语言模型的OCR识别问答系统实战

大家好,我是微学AI,今天给大家介绍一下深度学习实战50-构建ChatOCR项目:基于大语言模型的OCR识别问答系统实战,该项目是一个基于深度学习和大语言模型的OCR识别问答系统的实战项目。该项目旨在利用深度学习技术和先进的大语言模型,构建一个能够识别图像中文本,并能够回答与…...

计算机安全学习笔记(I):访问控制安全原理

访问控制原理 从广义上来讲&#xff0c;所有的计算机安全都与访问控制有关。 RFC 4949: Internet Security Glossary, Version 2 (rfc-editor.org) RFC 4949 定义的计算机安全&#xff1a;用来实现和保证计算机系统的安全服务的措施&#xff0c;特别是保证访问控制服务的措施…...

Linux 虚拟机安装 hadoop

目录 1 hadoop下载 2 解压hadoop 3 为 hadoop 文件夹改名 4 给 hadoop 文件夹赋权 5 修改环境变量 6 刷新环境变量 7 在hadoop313目录下创建文件夹data 8 检查文件 9 编辑 ./core-site.xml文件 10 编辑./hadoop-env.sh文件 11 编辑./hdfs-site.xml文件 12 编辑./mapr…...

FxFactory 8 Pro Mac 苹果电脑版 fcpx/ae/motion视觉特效软件包

FxFactory pro for mac是应用在Mac上的fcpx/ae/pr视觉特效插件包&#xff0c;包含了成百上千的视觉效果&#xff0c;打包了很多插件&#xff0c;如调色插件&#xff0c;转场插件&#xff0c;视觉插件&#xff0c;特效插件&#xff0c;文字插件&#xff0c;音频插件&#xff0c;…...

解决问题:如何在 Git 中查看提交历史

可以使用以下命令查看 Git 中的提交历史&#xff1a; git log这将显示当前分支上的所有提交历史。每个提交的输出包括提交哈希&#xff08;SHA-1 值&#xff09;、作者、日期和提交注释。 您也可以添加一些选项&#xff0c;以获取更详细的提交历史&#xff1a; --oneline 显示…...

不同规模的测试团队分别适合哪些测试用例管理工具?测试用例管理工具选型指南

随着软件系统规模的持续增大&#xff0c;业务复杂度的持续增加&#xff0c;软件测试的复杂度也随之越来越大。软件测试工作的复杂性主要体现在测试用例的编写、维护、执行和管理方面。而创建易于阅读、维护和管理的测试用例能够显著减轻测试工作的复杂性。 本篇文章将较为系统的…...

服务器遭受攻击,CPU升高,流量升高,你一般如何处理

服务器遭受攻击&#xff0c;CPU升高&#xff0c;流量升高&#xff0c;你一般如何处理&#xff1f; 在什么情况下服务器遭受攻击&#xff0c;会导致CPU升高&#xff0c;流量升高 1.DDoS&#xff08;分布式拒绝服务攻击&#xff09;&#xff1a;这是一种常见的网络攻击方式&…...

GPT生产实践之定制化翻译

GPT生产实践之定制化翻译 GPT除了能用来聊天以外&#xff0c;其实功能非常强大&#xff0c;但是我们如何把它运用到生产实践中去&#xff0c;为公司带来价值呢&#xff1f;下面一个使用案例–使用gpt做专业领域定制化翻译 思路&#xff1a; 定制化&#xff1a;有些公司词条的…...

SpringMVC入门笔记

一、SpringMVC简介 1. 什么是MVC MVC是一种软件架构的思想&#xff0c;将软件按照模型、视图、控制器来划分 M&#xff1a;Model&#xff0c;模型层&#xff0c;指工程中的JavaBean&#xff0c;作用是处理数据 JavaBean分为两类&#xff1a; 一类称为实体类Bean&#xff1…...

如何构建多域名HTTPS代理服务器转发

在当今互联网时代&#xff0c;安全可靠的网络访问是至关重要的。本文将介绍如何使用SNI Routing技术来构建多域名HTTPS代理服务器转发&#xff0c;轻松实现多域名的安全访问和数据传输。 SNI代表"Server Name Indication"&#xff0c;是TLS协议的扩展&#xff0c;用于…...

【Java 高阶】一文精通 Spring MVC - 数据验证(七)

&#x1f449;博主介绍&#xff1a; 博主从事应用安全和大数据领域&#xff0c;有8年研发经验&#xff0c;5年面试官经验&#xff0c;Java技术专家&#xff0c;WEB架构师&#xff0c;阿里云专家博主&#xff0c;华为云云享专家&#xff0c;51CTO 专家博主 ⛪️ 个人社区&#x…...

木叶飞舞之【机器人ROS2】篇章_第一节、ROS2 humble及cartorgrapher安装

ROS2的humble安装 1、系统配置ubuntu 22.04 假如长期使用ros2&#xff0c;建议是ubuntu系统或者双系统下安装操作&#xff0c;不要在虚拟机中进行。ubuntu系统能用最新的大系统就用最新的&#xff0c;比如22.04。等明年24.04出来可以用24.04 2、humble安装 ros版本选择humb…...

挑战杯推荐项目

“人工智能”创意赛 - 智能艺术创作助手&#xff1a;借助大模型技术&#xff0c;开发能根据用户输入的主题、风格等要求&#xff0c;生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用&#xff0c;帮助艺术家和创意爱好者激发创意、提高创作效率。 ​ - 个性化梦境…...

HTML 语义化

目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案&#xff1a; 语义化标签&#xff1a; <header>&#xff1a;页头<nav>&#xff1a;导航<main>&#xff1a;主要内容<article>&#x…...

论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(二)

HoST框架核心实现方法详解 - 论文深度解读(第二部分) 《Learning Humanoid Standing-up Control across Diverse Postures》 系列文章: 论文深度解读 + 算法与代码分析(二) 作者机构: 上海AI Lab, 上海交通大学, 香港大学, 浙江大学, 香港中文大学 论文主题: 人形机器人…...

无法与IP建立连接,未能下载VSCode服务器

如题&#xff0c;在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈&#xff0c;发现是VSCode版本自动更新惹的祸&#xff01;&#xff01;&#xff01; 在VSCode的帮助->关于这里发现前几天VSCode自动更新了&#xff0c;我的版本号变成了1.100.3 才导致了远程连接出…...

蓝桥杯 2024 15届国赛 A组 儿童节快乐

P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡&#xff0c;轻快的音乐在耳边持续回荡&#xff0c;小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下&#xff0c;六一来了。 今天是六一儿童节&#xff0c;小蓝老师为了让大家在节…...

【单片机期末】单片机系统设计

主要内容&#xff1a;系统状态机&#xff0c;系统时基&#xff0c;系统需求分析&#xff0c;系统构建&#xff0c;系统状态流图 一、题目要求 二、绘制系统状态流图 题目&#xff1a;根据上述描述绘制系统状态流图&#xff0c;注明状态转移条件及方向。 三、利用定时器产生时…...

Module Federation 和 Native Federation 的比较

前言 Module Federation 是 Webpack 5 引入的微前端架构方案&#xff0c;允许不同独立构建的应用在运行时动态共享模块。 Native Federation 是 Angular 官方基于 Module Federation 理念实现的专为 Angular 优化的微前端方案。 概念解析 Module Federation (模块联邦) Modul…...

dify打造数据可视化图表

一、概述 在日常工作和学习中&#xff0c;我们经常需要和数据打交道。无论是分析报告、项目展示&#xff0c;还是简单的数据洞察&#xff0c;一个清晰直观的图表&#xff0c;往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server&#xff0c;由蚂蚁集团 AntV 团队…...

均衡后的SNRSINR

本文主要摘自参考文献中的前两篇&#xff0c;相关文献中经常会出现MIMO检测后的SINR不过一直没有找到相关数学推到过程&#xff0c;其中文献[1]中给出了相关原理在此仅做记录。 1. 系统模型 复信道模型 n t n_t nt​ 根发送天线&#xff0c; n r n_r nr​ 根接收天线的 MIMO 系…...

RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)

RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发&#xff0c;后来由Pivotal Software Inc.&#xff08;现为VMware子公司&#xff09;接管。RabbitMQ 是一个开源的消息代理和队列服务器&#xff0c;用 Erlang 语言编写。广泛应用于各种分布…...