Django ORM 中的 select_related 和 prefetch_related 有什么区别?
- 2025-01-17 09:22:00
- admin 原创
- 13
问题描述:
在 Django 文档中:
select_related()
“遵循”外键关系,在执行查询时选择额外的相关对象数据。
prefetch_related()
对每个关系进行单独查找,并在 Python 中执行“连接”。
“在 Python 中进行连接”是什么意思?有人可以举例说明吗?
我的理解是,对于外键关系,使用select_related
;对于 M2M 关系,使用prefetch_related
。这样对吗?
解决方案 1:
你的理解基本正确:
select_related
:当你要选择的对象是单个对象时,OneToOneField
或者ForeignKey
prefetch_related
:当你要得到一“组”东西时,所以ManyToManyField
如你所述或反向ForeignKey
。
为了澄清我所说的反向的意思ForeignKey
,这里有一个例子:
class ModelA(models.Model):
pass
class ModelB(models.Model):
a = ForeignKey(ModelA)
# Forward ForeignKey relationship
ModelB.objects.select_related('a').all()
# Reverse ForeignKey relationship
ModelA.objects.prefetch_related('modelb_set').all()
不同之处在于:
select_related
执行 SQL 连接,然后从 SQL 服务器获取表的一部分作为结果prefetch_related
另一方面执行另一个查询,从而减少原始对象中的冗余列(ModelA
在上面的例子中)
你可以用它prefetch_related
来做任何你能做select_related
的事情。
权衡是prefetch_related
必须创建并发送 ID 列表以选择回服务器,这可能需要一段时间。我不确定是否有一个好的方法可以在事务中完成此操作,但我的理解是 Django 总是只发送一个列表并说 SELECT ... WHERE pk IN (...,...,...) 基本上。在这种情况下,如果预取的数据是稀疏的(比如说链接到人们地址的美国州对象),这可能非常好,但是如果它更接近一对一,这可能会浪费大量通信。如果有疑问,请尝试两者并查看哪个性能更好。
上面讨论的所有内容基本上都是关于与数据库的通信。然而,在 Python 方面,prefetch_related
还有一个额外的好处,那就是使用单个对象来表示数据库中的每个对象。select_related
对于每个“父”对象,Python 中将创建重复的对象。由于 Python 中的对象有相当大的内存开销,因此这也可以作为一个考虑因素。
解决方案 2:
浏览了已经发布的答案。只是觉得如果我添加一个带有实际示例的答案会更好。
假设您有 3 个相关的 Django 模型。
class M1(models.Model):
name = models.CharField(max_length=10)
class M2(models.Model):
name = models.CharField(max_length=10)
select_relation = models.ForeignKey(M1, on_delete=models.CASCADE)
prefetch_relation = models.ManyToManyField(to='M3')
class M3(models.Model):
name = models.CharField(max_length=10)
在这里您可以使用字段查询M2
模型及其相关M1
对象,也可以使用字段查询对象。select_relation
`M3`prefetch_relation
但是,正如我们提到M1
的那样M2
,ForeignKey
它只为任何对象返回1 条M2
记录。同样的事情OneToOneField
也适用于。
但是M3
的关系M2
是ManyToManyField
可能返回任意数量的M1
对象。
假设您有 2 个M2
对象m21
,m22
它们有5 个M3
相同的ID 关联对象。当您为每个对象1,2,3,4,5
获取关联对象时,如果您使用 select related,它的工作方式如下。M3
`M2`
步骤:
尋找
m21
對象。查询与ID 为 的对象
M3
相关的所有对象。m21
`1,2,3,4,5`m22
对对象和所有其他对象重复相同的操作M2
。
1,2,3,4,5
由于两个对象具有相同的 ID m21
,如果m22
我们使用 select_related 选项,它将两次查询数据库以获取已经获取的相同 ID。
相反,如果您使用 prefetch_related,当您尝试获取M2
对象时,它将在查询表时记录您的对象返回的所有 ID(注意:仅 ID)M2
,作为最后一步,Django 将使用M3
您的对象返回的所有 ID 的集合对表进行查询M2
。并M2
使用 Python 而不是数据库将它们连接到对象。
这样,您只需查询所有M3
对象一次,从而提高性能,因为 python 连接比数据库连接更便宜。
解决方案 3:
两种方法都达到了相同的目的,即放弃不必要的数据库查询。但它们使用不同的方法来提高效率。
使用这两种方法的唯一原因是,单个大型查询比多个小型查询更可取。Django 使用大型查询预先在内存中创建模型,而不是对数据库执行按需查询。
select_related
每次查找时执行连接,但扩展选择以包含所有连接表的列。但是这种方法有一个警告。
连接可能会使查询中的行数增加几倍。当您通过外键或一对一字段执行连接时,行数不会增加。但是,多对多连接没有这种保证。因此,Django 限制了不会select_related
意外导致大量连接的关系。
“python 中的 join”比它prefetch_related
应该的要令人担忧一些。它为每个要连接的表创建一个单独的查询。它使用 WHERE IN 子句过滤每个表,例如:
SELECT "credential"."id",
"credential"."uuid",
"credential"."identity_id"
FROM "credential"
WHERE "credential"."identity_id" IN
(84706, 48746, 871441, 84713, 76492, 84621, 51472);
不是执行可能包含过多行的单个连接,而是将每个表拆分为单独的查询。
解决方案 4:
选择相关():
可以将一对一关系中正向外键和反向外键的多个
SELECT
查询减少为仅 1 个查询,也可以将一对多和多对多关系中正向外键的SELECT
查询减少为仅 1 个查询。不能在一对多和多对多关系中与反向外键一起使用。
prefetch_related():
在一对一、一对多和多对多关系中,可以使用前向外键和反向外键
SELECT
将多个查询减少到SELECT
至少 2 个查询。
*您可以看到我的回答,其中解释了前向外键和反向外键的含义。
下面展示了我在一对一、一对多和多对多关系中使用正向外键select_related()
和反向外键的实验。prefetch_related()
<一对一关系>
例如,有Person
和PersonDetail
具有一对一关系的模型,如下所示:
# "app/models.py
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=20)
def __str__(self):
return self.name
class PersonDetail(models.Model):
person = models.OneToOneField(Person, on_delete=models.CASCADE)
age = models.IntegerField()
gender = models.CharField(max_length=20)
def __str__(self):
return str(self.age) + " " + self.gender
并且,有Person
和PersonDetail
管理员,如下所示:
# "app/admin.py
from django.contrib import admin
from .models import Person, PersonDetail
class PersonDetailInline(admin.TabularInline):
model = PersonDetail
@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
inlines = (PersonDetailInline,)
list_display = ('id', 'name')
ordering = ('id',)
@admin.register(PersonDetail)
class PersonDetailAdmin(admin.ModelAdmin):
list_display = ('id', 'age', 'gender', 'person')
ordering = ('id',)
那么Person
admin就有5个对象,如下所示:
并且,PersonDetail
admin 有 5 个对象,如下所示:
<前向外键>
然后,我Person
从模型中迭代模型PersonDetail
,如下所示:
for obj in PersonDetail.objects.all():
print(obj.person)
然后控制台上会输出以下内容:
John
David
Lisa
Kai
Anna
然后,SELECT
运行 6 个查询,如下所示。 *我使用 PostgreSQL,下面是 PostgreSQL 的查询日志,您可以看到我的回答,其中解释了如何在 PostgreSQL 上启用和禁用查询日志:
接下来,我用从模型迭代Person
模型,如下所示。 *和的顺序无关紧要:PersonDetail
`select_related("person")select_related()
all()`
for obj in PersonDetail.objects.select_related("person").all():
print(obj.person)
然后控制台上会输出以下内容:
John
David
Lisa
Kai
Anna
然后,SELECT
运行 1 个查询,如下所示:
接下来,我用从模型迭代Person
模型,如下所示。 *和的顺序无关紧要:PersonDetail
`prefetch_related("person")prefetch_related()
all()`
for obj in PersonDetail.objects.prefetch_related("person").all():
print(obj.person)
然后控制台上会输出以下内容:
John
David
Lisa
Kai
Anna
然后SELECT
运行 2 个查询,如下所示:
<反向外键>
接下来,我PersonDetail
从模型中迭代模型Person
,如下所示:
for obj in Person.objects.all():
print(obj.persondetail)
然后控制台上会输出以下内容:
32 Male
18 Male
26 Female
36 Male
21 Female
然后SELECT
运行 6 个查询,如下所示:
接下来,我使用模型迭代PersonDetail
模型,如下所示:Person
`select_related("persondetail")`
for obj in Person.objects.select_related("persondetail").all():
print(obj.persondetail)
然后控制台上会输出以下内容:
32 Male
18 Male
26 Female
36 Male
21 Female
然后,SELECT
运行 1 个查询,如下所示:
接下来,我使用模型迭代PersonDetail
模型,如下所示:Person
`prefetch_related("persondetail")`
for obj in Person.objects.prefetch_related("persondetail").all():
print(obj.persondetail)
然后控制台上会输出以下内容:
32 Male
18 Male
26 Female
36 Male
21 Female
然后SELECT
运行 2 个查询,如下所示:
<一对多关系>
例如,有Category
和Product
具有一对多关系的模型,如下所示:
# "app/models.py"
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=20)
def __str__(self):
return self.name
class Product(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE)
name = models.CharField(max_length=50)
price = models.DecimalField(decimal_places=2, max_digits=5)
def __str__(self):
return str(self.category) + " " + self.name + " " + str(self.price)
并且,有Category
和Product
管理员,如下所示:
# "app/admin.py"
from django.contrib import admin
from .models import Category, Product
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('id', 'name')
ordering = ('id',)
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'price', 'category')
ordering = ('id',)
那么Category
admin就有4个对象,如下所示:
并且,Product
admin 有 6 个对象,如下所示:
<前向外键>
然后,我Category
从模型中迭代模型Product
,如下所示:
for obj in Product.objects.all():
print(obj.category)
然后控制台上会输出以下内容:
Fruits
Fruits
Vegetable
Meat
Meat
Fish
然后SELECT
运行 7 个查询,如下所示:
接下来,我使用模型迭代Category
模型,如下所示:Product
`select_related("category")`
for obj in Product.objects.select_related("category").all():
print(obj.category)
然后控制台上会输出以下内容:
Fruits
Fruits
Vegetable
Meat
Meat
Fish
然后,SELECT
运行 1 个查询,如下所示:
接下来,我使用模型迭代Category
模型,如下所示:Product
`prefetch_related("category")`
for obj in Product.objects.prefetch_related("category").all():
print(obj.category)
然后控制台上会输出以下内容:
Fruits
Fruits
Vegetable
Meat
Meat
Fish
然后SELECT
运行 2 个查询,如下所示:
<反向外键>
接下来,我Product
从模型中迭代模型Category
,如下所示:
for obj in Category.objects.all():
print(obj.product_set.all())
然后控制台上会输出以下内容:
<QuerySet [<Product: Fruits Apple 10.00>, <Product: Fruits Orange 20.00>]>
<QuerySet [<Product: Vegetable Carrot 30.00>]>
<QuerySet [<Product: Meat Chicken 40.00>, <Product: Meat Beef 50.00>]>
<QuerySet [<Product: Fish Salmon 60.00>]>
然后SELECT
运行 5 个查询,如下所示:
接下来,我尝试从模型中迭代Product
模型,如下所示:Category
`select_related("product_set")`
for obj in Category.objects.select_related("product_set").all():
print(obj.product_set.all())
然后,由于不能与反向外键select_related("product_set")
一起使用,会出现以下错误:
django.core.exceptions.FieldError: select_related 中给出的字段名称无效:'product_set'。选项有:(无)
实际上,如果我不使用参数select_related()
则不会出现错误,如下所示:
# ↓ No argument
for obj in Category.objects.select_related().all():
print(obj.product_set.all())
然后控制台上会输出以下内容:
<QuerySet [<Product: Fruits Apple 10.00>, <Product: Fruits Orange 20.00>]>
<QuerySet [<Product: Vegetable Carrot 30.00>]>
<QuerySet [<Product: Meat Chicken 40.00>, <Product: Meat Beef 50.00>]>
<QuerySet [<Product: Fish Salmon 60.00>]>
但是,SELECT
仍然运行 5 个查询而不是 1 个SELECT
查询,如下所示:
接下来,我使用模型迭代Product
模型,如下所示:Category
`prefetch_related("product_set")`
for obj in Category.objects.prefetch_related("product_set").all():
print(obj.product_set.all())
然后控制台上会输出以下内容:
<QuerySet [<Product: Fruits Apple 10.00>, <Product: Fruits Orange 20.00>]>
<QuerySet [<Product: Vegetable Carrot 30.00>]>
<QuerySet [<Product: Meat Chicken 40.00>, <Product: Meat Beef 50.00>]>
<QuerySet [<Product: Fish Salmon 60.00>]>
然后SELECT
运行 2 个查询,如下所示:
<多对多关系>
例如,Category
和Product
模型具有多对多关系,如下所示:
# "app/models.py"
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=20)
def __str__(self):
return self.name
class Product(models.Model):
categories = models.ManyToManyField(Category)
name = models.CharField(max_length=50)
price = models.DecimalField(decimal_places=2, max_digits=5)
def __str__(self):
return self.name + " " + str(self.price)
并且,有Category
和Product
管理员,如下所示:
# "app/admin.py
from django.contrib import admin
from .models import Category, Product
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'get_products')
ordering = ('id',)
@admin.display(description='products')
def get_products(self, obj):
return [product.name for product in obj.product_set.all()]
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'price', 'get_categories')
ordering = ('id',)
@admin.display(description='categories')
def get_categories(self, obj):
return [category.name for category in obj.categories.all()]
那么Category
admin就有5个对象,如下所示:
并且,Product
admin 有 6 个对象,如下所示:
<前向外键>
然后,我Category
从模型中迭代模型Product
,如下所示:
for obj in Product.objects.all():
print(obj.categories.all())
然后控制台上会输出以下内容:
<QuerySet [<Category: Fruits>, <Category: 20% OFF>]>
<QuerySet [<Category: Fruits>]>
<QuerySet [<Category: Vegetable>]>
<QuerySet [<Category: Meat>, <Category: 20% OFF>]>
<QuerySet [<Category: Meat>]>
<QuerySet [<Category: Fish>, <Category: 20% OFF>]>
然后SELECT
运行 7 个查询,如下所示:
接下来,我使用模型迭代Category
模型,如下所示:Product
`select_related("categories")`
for obj in Product.objects.select_related("categories").all():
print(obj.categories.all())
然后,由于不能与反向外键select_related("categories")
一起使用,会出现以下错误:
django.core.exceptions.FieldError: select_related 中给出的字段名称无效:'categories'。选项有:(无)
实际上,如果我不使用参数select_related()
则不会出现错误,如下所示:
# ↓ No argument
for obj in Product.objects.select_related().all():
print(obj.categories.all())
然后控制台上会输出以下内容:
<QuerySet [<Category: Fruits>, <Category: 20% OFF>]>
<QuerySet [<Category: Fruits>]>
<QuerySet [<Category: Vegetable>]>
<QuerySet [<Category: Meat>, <Category: 20% OFF>]>
<QuerySet [<Category: Meat>]>
<QuerySet [<Category: Fish>, <Category: 20% OFF>]>
但是,SELECT
仍然运行了 7 个查询,而不是 1 个SELECT
查询,如下所示:
接下来,我使用模型迭代Category
模型,如下所示:Product
`prefetch_related("categories")`
for obj in Product.objects.prefetch_related("categories").all():
print(obj.categories.all())
然后控制台上会输出以下内容:
<QuerySet [<Category: Fruits>, <Category: 20% OFF>]>
<QuerySet [<Category: Fruits>]>
<QuerySet [<Category: Vegetable>]>
<QuerySet [<Category: Meat>, <Category: 20% OFF>]>
<QuerySet [<Category: Meat>]>
<QuerySet [<Category: Fish>, <Category: 20% OFF>]>
然后SELECT
运行 2 个查询,如下所示:
<反向外键>
接下来,我Product
从模型中迭代模型Category
,如下所示:
for obj in Category.objects.all():
print(obj.product_set.all())
然后控制台上会输出以下内容:
<QuerySet [<Product: Apple 10.00>, <Product: Orange 20.00>]>
<QuerySet [<Product: Carrot 30.00>]>
<QuerySet [<Product: Chicken 40.00>, <Product: Beef 50.00>]>
<QuerySet [<Product: Salmon 60.00>]>
<QuerySet [<Product: Apple 10.00>, <Product: Chicken 40.00>, <Product: Salmon 60.00>]>
然后SELECT
运行 6 个查询,如下所示:
接下来,我使用模型迭代Product
模型,如下所示:Category
`select_related("product_set")`
for obj in Category.objects.select_related("product_set").all():
print(obj.product_set.all())
然后,由于不能与反向外键select_related("categories")
一起使用,会出现以下错误:
django.core.exceptions.FieldError: select_related 中给出的字段名称无效:'product_set'。选项有:(无)
实际上,如果我不使用参数select_related()
则不会出现错误,如下所示:
# ↓ No argument
for obj in Category.objects.select_related().all():
print(obj.product_set.all())
然后控制台上会输出以下内容:
<QuerySet [<Product: Apple 10.00>, <Product: Orange 20.00>]>
<QuerySet [<Product: Carrot 30.00>]>
<QuerySet [<Product: Chicken 40.00>, <Product: Beef 50.00>]>
<QuerySet [<Product: Salmon 60.00>]>
<QuerySet [<Product: Apple 10.00>, <Product: Chicken 40.00>, <Product: Salmon 60.00>]>
但是,SELECT
仍然运行了 6 个查询,而不是 1 个SELECT
查询,如下所示:
接下来,我使用模型迭代Product
模型,如下所示:Category
`prefetch_related("product_set")`
for obj in Category.objects.prefetch_related("product_set").all():
print(obj.product_set.all())
然后控制台上会输出以下内容:
<QuerySet [<Product: Apple 10.00>, <Product: Orange 20.00>]>
<QuerySet [<Product: Carrot 30.00>]>
<QuerySet [<Product: Chicken 40.00>, <Product: Beef 50.00>]>
<QuerySet [<Product: Salmon 60.00>]>
<QuerySet [<Product: Apple 10.00>, <Product: Chicken 40.00>, <Product: Salmon 60.00>]>
然后SELECT
运行 2 个查询,如下所示:
<附加实验>
例如,有Country
、State
和City
模型具有一对多关系,如下所示:
# "app/models.py"
from django.db import models
class Country(models.Model):
name = models.CharField(max_length=20)
def __str__(self):
return self.name
class State(models.Model):
country = models.ForeignKey(Country, on_delete=models.CASCADE)
name = models.CharField(max_length=20)
def __str__(self):
return self.name
class City(models.Model):
state = models.ForeignKey(State, on_delete=models.CASCADE)
name = models.CharField(max_length=20)
def __str__(self):
return self.name
其中,有Country
、State
和City
管理员,如下所示:
# "app/admin.py
from django.contrib import admin
from .models import Country, State, City
@admin.register(Country)
class CountryAdmin(admin.ModelAdmin):
list_display = ('id', 'name')
ordering = ('id',)
@admin.register(State)
class StateAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'get_country')
ordering = ('id',)
@admin.display(description='country')
def get_country(self, obj):
return obj.country
@admin.register(City)
class CityAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'get_state', 'get_country')
ordering = ('id',)
@admin.display(description='state')
def get_state(self, obj):
print(obj)
return obj.state
@admin.display(description='country')
def get_country(self, obj):
return obj.state.country
那么Country
admin有2个对象,如下所示:
并且,State
admin 有 3 个对象,如下所示:
那么City
admin就有6个对象,如下所示:
<前向外键>
然后,我使用模型迭代Country
模型,如下所示:City
`select_related("state__country")`
for obj in City.objects.all().select_related("state__country"):
print(obj.state.country)
然后控制台上会输出以下内容:
USA
USA
USA
USA
Japan
Japan
然后,SELECT
运行 1 个查询,如下所示:
接下来,我使用模型迭代Country
模型,如下所示:City
`prefetch_related("state__country")`
for obj in City.objects.all().prefetch_related("state__country"):
print(obj.state.country)
然后控制台上会输出以下内容:
USA
USA
USA
USA
Japan
Japan
然后SELECT
运行 3 个查询,如下所示:
<反向外键>
接下来,我使用模型迭代City
模型,如下所示:Country
`prefetch_related("state_set__city_set")`
for country_obj in Country.objects.all().prefetch_related("state_set__city_set"):
for state_obj in country_obj.state_set.all():
for city_obj in state_obj.city_set.all():
print(city_obj)
然后控制台上会输出以下内容:
San Francisco
Los Angeles
San Diego
Kansas City
Ginza
Akihabara
然后SELECT
运行 3 个查询,如下所示:
接下来,我使用filter()而不是all()从模型中迭代City
模型,如下所示。* with不起作用:Country
`prefetch_related("state_set__city_set")prefetch_related()
filter()`
# Here
for country_obj in Country.objects.filter().prefetch_related("state_set__city_set"):
for state_obj in country_obj.state_set.filter(): # Here
for city_obj in state_obj.city_set.filter(): # Here
print(city_obj)
然后控制台上会输出以下内容:
San Francisco
Los Angeles
San Diego
Kansas City
Ginza
Akihabara
然后SELECT
运行 8 个查询,如下所示:
因此,为了减少 8 个SELECT
查询,我需要使用Prefetch(),filter()
但我只知道如何使用和从模型迭代State
模型,如下所示。 *我在 Stack Overflow 上询问了如何使用和从模型迭代模型的问题:Country
`Prefetch()filter()
CityCountry
Prefetch()`filter()
for country_obj in Country.objects.filter().prefetch_related(
Prefetch('state_set', # Here
queryset=State.objects.filter(),
to_attr='state_obj'
)
):
print(country_obj.state_obj)
然后控制台上会输出以下内容:
[<State: California>, <State: Missouri>]
[<State: Tokyo>]
然后SELECT
运行 2 个查询,如下所示:
解决方案 5:
不要混淆
select_related:用于ForeignKey关系,
prefetch_related:用于ManyToManyField关系或反向ForeignKey。
它们做同样的事情,减少查询的数量
例如:
class ExampleClassA(models.Model):
title = models.CharField(max_length=50)
class ExampleClassB(models.Model):
example_class_a = models.ForeignKey(ExampleClassA,
on_delete=models.CASCADE)
objects = ExampleClassB.objects.all()
for obj in objects:
print(obj.example_class_a.title)
查询次数(访问相关字段):N+1(# n 是 ExampleClassA 的对象数量)
如果我们使用此查询:
objects = ExampleClassB.objects.select_related('example_class_a').all()
查询数量只有一个。
解决方案 6:
让我试着向你展示 Django 如何在 select_related 和 prefetch_related 中进行数据库调用
class a(models.Model):
name = models.CharField(max_length=100)
class b(models.Model):
name = models.CharField(max_length=100)
a = models.ForeignKey(A, on_delete=models.CASCADE)
select_related 查询->
b.objects.select_related('a').first()
为此执行的 SQL 查询将是
SELECT * FROM "b" LEFT OUTER JOIN "a" ON ("b"."a_id" = "a"."id") LIMIT 1
这里 Django 将使用 JOIN 获取“a”模型的详细信息
prefetch_related 查询->
B.objects.prefetch_related('a').first()
为此执行的 SQL 查询将是
SELECT * FROM "b" LIMIT 1
SELECT * FROM "a" WHERE "a"."id" IN (ids collected from above query)
这里Django将执行两个SQL查询并通过python合并它们
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 项目管理必备:盘点2024年13款好用的项目管理软件
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)