嵌套查询 (Django + React) 的 Graphql 性能瓶颈使前端应用程序无法使用。请帮忙 :'(
Graphql bottleneck performance with nested query (Django + React ) makes frontend app unusable. Please help :'(
对于这个项目,我在后端使用 Python+Django 和 GraphQL(石墨烯),MySQL 作为数据库,React.js 作为前端。
在前端,用户登录后,我要执行以下查询:
const GET_ORGANIZATION = gql`
query getOrganization($orgId : Int!) {
organization(id:$orgId){
id
name
user{
id
username
firstName
lastName
email
dateJoined
lastLogin
isActive
trainings {
id
name
sessions {
id
name
category
createdAt
totalSteps
completedAt
user {
id
}
eventSet {
id
category
description
object
errorSeverity
performedAt
}
}
}
}
courses{
id
name
description
trainings{
id
name
user{
id
username
isSuperuser
isStaff
isActive
email
}
sessions{
id
name
category
createdAt
completedAt
user{
id
username
}
eventSet {
id
category
description
object
errorSeverity
performedAt
}
}
}
}
}
}`;
如您所见,它嵌套了多个级别。当我进入 sessions 和 events 时,问题就来了。我不是 graphQL 的超级专家,但我一直认为 GraphQL 的卖点是你可以在一个查询中使用所有这些嵌套字段。好吧,事实并非如此。这里有几张图片,原因是:
响应需要 30 多秒。深入研究我的数据库的 slow_log,我发现:
使用相同的参数重复多次相同的查询:
这重复了 5000 多次。
通读 SO 和其他来源,这似乎是 GraphQL
的经典 N+1 问题
所以现在我面临着一条(希望如此)解决方案的两条道路:
第一个,我找到了一种方法,使这个查询可以按原样使用,有大量的数据,这就是我需要建议和帮助的地方。有办法吗?我想是这样,因为如果几层嵌套和其中一层中的几千行就足以使它无法使用,那么我想没有人会使用它。
第二种方式,这就是我今天开始做的方式,但后来停下来,哭着来创造这个post:我限制我的查询只进行到培训级别,我没有一个大查询,而是几个较小的查询。这种方法的问题是造成我痛苦的原因是我意识到我基本上必须在前端重做我的整个反应组件,因为他们中的许多人都期望数据那么大 object,更不用说当您将 object 或其中的一部分传递给其他组件时
你会推荐哪一个?或者还有其他方法吗?
请记住,我只有非常非常有限的时间来进行重大更改,因为在接下来的几天里我将要进行此直播。
添加一些上下文:
我正在独自开发一种支持 VR 应用程序的度量应用程序。没有团队(我一加入就离开了前端和 devOps 人员),所以我必须做所有的 devOps,前端和后端工作,这是很多工作。由于这种压力,我犯了一个新手错误,即从不花时间获取包含 VR 收集的所有数据的数据库副本,这些数据实际上是用户将在前端可视化的数据。所以我一直在用虚拟的较小数据进行测试,到目前为止一切都“完美”。这可能是我自己和其他阅读这篇文章的人的一个教训 question/cry 寻求帮助:确保您的环境在开发时尽可能接近生产环境,特别是有足够的数据
这不是 GraphQL 本身的问题,而是石墨烯实现的问题。正如您所发现的,没有 SQL 查询优化,并且为每个深度级别创建查询,这对于深度 GraphQL 查询来说非常糟糕。
您有一些选择:
通过为多个深度级别创建某种查询优化器来修复石墨烯的实现——根据您的分析,您会发现进行一些改进很容易,如果您对社区最欣赏的开源项目(但此选项需要数月的工作)
创建您自己的解析器,劫持查询中特别慢的部分,并替换为您自己的优化查询,其中包括所有必要的连接和 returns 结构化 JSON 输出
创建不属于您的 Django 架构的自定义字段 and/or 对象类型,并如上所述编写优化的字段解析器。
(更新)寻找查询优化器(有关详细信息,请参阅@Rafael 的回答)
感谢马克的回答,我环顾四周,找到了这个包裹
https://github.com/tfoxy/graphene-django-optimizer 这完全符合他的建议。
使用起来非常简单。例如,为了优化我的 organization_resolver,我所要做的就是:
def resolve_organization(root, info, id):
return gql_optimizer.query(Organization.objects.filter(pk=id),info).get()
查询从 50 多岁变为 11 多岁。巨大的进步!
仍然,没有我能做到的那么快,但我仍然可以做其他改进。
在他们的页面中,高级用法下解释了如何对更复杂的解析器进行一些修改
希望这对同样面临同样问题的其他人有所帮助
尝试DataLoader。与其每次 graphql 解析嵌套 graphql query
时尝试访问数据库,我们可以批处理 database query
然后访问数据库一次。
示例(在js中因为我看不懂python)
没有数据加载器
// the schema
type Query {
user: User
users: [User]
}
type User {
id: String!
email: String!
name: String
profile: Profile
}
type Profile {
bio: String
age: Int
user: User
userId: String
}
// Resolver
export const resolvers = {
Query: {
user: (): null => null,
users: async (_, __, context) => {
return await context.db.user.findMany({})
},
},
User: {
profile: async (parent, _, context) => {
return await context.db.profile.findFirst({
where: {
userId: parent.id,
},
})
},
},
Profile: {
user: async (parent, _, context) => {
return await context.db.user.findFirst({
where: {
id: parent.userId
}
})
},
},
}
以及我尝试此 graphql 查询时的结果
query {
users {
email
name
profile {
bio
user {
email
}
}
}
}
是this(大约一百个数据库查询或更多)
带数据加载器
// Loader.js
async function profileBatchFunction(keys: readonly string[]) {
const result = await db.profile.findMany({
where: {
userId: {
in: keys as string[],
},
},
})
return keys.map(k => result.find(res => res.userId === k) || null)
}
export const profileLoader = new DataLoader(profileBatchFunction)
async function userBatchFunction(keys: readonly string[]) {
const result = await db.user.findMany({
where: {
id: {
in: keys as string[],
},
},
})
return keys.map(k => result.find(res => res.id === k) || null)
}
export const userLoader = new DataLoader(userBatchFunction)
// Resolver
export const resolvers = {
Query: {
user: (): null => null,
users: async (_, __, context) => {
return await context.db.user.findMany({})
},
},
Profile: {
user: async parent => {
return await userLoader.load(parent.userId)
},
},
User: {
profile: async parent => {
return await profileLoader.load(parent.id)
},
},
}
与graphql query
相同的结果是this one。好多了
对于这个项目,我在后端使用 Python+Django 和 GraphQL(石墨烯),MySQL 作为数据库,React.js 作为前端。
在前端,用户登录后,我要执行以下查询:
const GET_ORGANIZATION = gql`
query getOrganization($orgId : Int!) {
organization(id:$orgId){
id
name
user{
id
username
firstName
lastName
email
dateJoined
lastLogin
isActive
trainings {
id
name
sessions {
id
name
category
createdAt
totalSteps
completedAt
user {
id
}
eventSet {
id
category
description
object
errorSeverity
performedAt
}
}
}
}
courses{
id
name
description
trainings{
id
name
user{
id
username
isSuperuser
isStaff
isActive
email
}
sessions{
id
name
category
createdAt
completedAt
user{
id
username
}
eventSet {
id
category
description
object
errorSeverity
performedAt
}
}
}
}
}
}`;
如您所见,它嵌套了多个级别。当我进入 sessions 和 events 时,问题就来了。我不是 graphQL 的超级专家,但我一直认为 GraphQL 的卖点是你可以在一个查询中使用所有这些嵌套字段。好吧,事实并非如此。这里有几张图片,原因是:
响应需要 30 多秒。深入研究我的数据库的 slow_log,我发现:
使用相同的参数重复多次相同的查询:
这重复了 5000 多次。 通读 SO 和其他来源,这似乎是 GraphQL
的经典 N+1 问题所以现在我面临着一条(希望如此)解决方案的两条道路:
第一个,我找到了一种方法,使这个查询可以按原样使用,有大量的数据,这就是我需要建议和帮助的地方。有办法吗?我想是这样,因为如果几层嵌套和其中一层中的几千行就足以使它无法使用,那么我想没有人会使用它。
第二种方式,这就是我今天开始做的方式,但后来停下来,哭着来创造这个post:我限制我的查询只进行到培训级别,我没有一个大查询,而是几个较小的查询。这种方法的问题是造成我痛苦的原因是我意识到我基本上必须在前端重做我的整个反应组件,因为他们中的许多人都期望数据那么大 object,更不用说当您将 object 或其中的一部分传递给其他组件时
你会推荐哪一个?或者还有其他方法吗? 请记住,我只有非常非常有限的时间来进行重大更改,因为在接下来的几天里我将要进行此直播。
添加一些上下文: 我正在独自开发一种支持 VR 应用程序的度量应用程序。没有团队(我一加入就离开了前端和 devOps 人员),所以我必须做所有的 devOps,前端和后端工作,这是很多工作。由于这种压力,我犯了一个新手错误,即从不花时间获取包含 VR 收集的所有数据的数据库副本,这些数据实际上是用户将在前端可视化的数据。所以我一直在用虚拟的较小数据进行测试,到目前为止一切都“完美”。这可能是我自己和其他阅读这篇文章的人的一个教训 question/cry 寻求帮助:确保您的环境在开发时尽可能接近生产环境,特别是有足够的数据
这不是 GraphQL 本身的问题,而是石墨烯实现的问题。正如您所发现的,没有 SQL 查询优化,并且为每个深度级别创建查询,这对于深度 GraphQL 查询来说非常糟糕。
您有一些选择:
通过为多个深度级别创建某种查询优化器来修复石墨烯的实现——根据您的分析,您会发现进行一些改进很容易,如果您对社区最欣赏的开源项目(但此选项需要数月的工作)
创建您自己的解析器,劫持查询中特别慢的部分,并替换为您自己的优化查询,其中包括所有必要的连接和 returns 结构化 JSON 输出
创建不属于您的 Django 架构的自定义字段 and/or 对象类型,并如上所述编写优化的字段解析器。
(更新)寻找查询优化器(有关详细信息,请参阅@Rafael 的回答)
感谢马克的回答,我环顾四周,找到了这个包裹 https://github.com/tfoxy/graphene-django-optimizer 这完全符合他的建议。
使用起来非常简单。例如,为了优化我的 organization_resolver,我所要做的就是:
def resolve_organization(root, info, id):
return gql_optimizer.query(Organization.objects.filter(pk=id),info).get()
查询从 50 多岁变为 11 多岁。巨大的进步!
仍然,没有我能做到的那么快,但我仍然可以做其他改进。
在他们的页面中,高级用法下解释了如何对更复杂的解析器进行一些修改
希望这对同样面临同样问题的其他人有所帮助
尝试DataLoader。与其每次 graphql 解析嵌套 graphql query
时尝试访问数据库,我们可以批处理 database query
然后访问数据库一次。
示例(在js中因为我看不懂python)
没有数据加载器
// the schema
type Query {
user: User
users: [User]
}
type User {
id: String!
email: String!
name: String
profile: Profile
}
type Profile {
bio: String
age: Int
user: User
userId: String
}
// Resolver
export const resolvers = {
Query: {
user: (): null => null,
users: async (_, __, context) => {
return await context.db.user.findMany({})
},
},
User: {
profile: async (parent, _, context) => {
return await context.db.profile.findFirst({
where: {
userId: parent.id,
},
})
},
},
Profile: {
user: async (parent, _, context) => {
return await context.db.user.findFirst({
where: {
id: parent.userId
}
})
},
},
}
以及我尝试此 graphql 查询时的结果
query {
users {
email
name
profile {
bio
user {
email
}
}
}
}
是this(大约一百个数据库查询或更多)
带数据加载器
// Loader.js
async function profileBatchFunction(keys: readonly string[]) {
const result = await db.profile.findMany({
where: {
userId: {
in: keys as string[],
},
},
})
return keys.map(k => result.find(res => res.userId === k) || null)
}
export const profileLoader = new DataLoader(profileBatchFunction)
async function userBatchFunction(keys: readonly string[]) {
const result = await db.user.findMany({
where: {
id: {
in: keys as string[],
},
},
})
return keys.map(k => result.find(res => res.id === k) || null)
}
export const userLoader = new DataLoader(userBatchFunction)
// Resolver
export const resolvers = {
Query: {
user: (): null => null,
users: async (_, __, context) => {
return await context.db.user.findMany({})
},
},
Profile: {
user: async parent => {
return await userLoader.load(parent.userId)
},
},
User: {
profile: async parent => {
return await profileLoader.load(parent.id)
},
},
}
与graphql query
相同的结果是this one。好多了