Skip to content

Commit e468292

Browse files
authored
perf(db-mongodb): improve performance of all operations, up to 50% faster (#9594)
This PR improves speed and memory efficiency across all operations with the Mongoose adapter. ### How? - Removes Mongoose layer from all database calls, instead uses MongoDB directly. (this doesn't remove building mongoose schema since it's still needed for indexes + users in theory can use it) - Replaces deep copying of read results using `JSON.parse(JSON.stringify(data))` with the `transform` `operation: 'read'` function which converts Date's, ObjectID's in relationships / joins to strings. As before, it also handles transformations for write operations. - Faster `hasNearConstraint` for potentially large `where`'s - `traverseFields` now can accept `flattenedFields` which we use in `transform`. Less recursive calls with tabs/rows/collapsible Additional fixes - Uses current transaction for querying nested relationships properties in `buildQuery`, previously it wasn't used which could've led to wrong results - Allows to clear not required point fields with passing `null` from the Local API. Previously it didn't work in both, MongoDB and Postgres Benchmarks using this file https://github.com/payloadcms/payload/blob/chore/db-benchmark/test/_community/int.spec.ts ### Small Dataset Performance | Metric | Before Optimization | After Optimization | Improvement (%) | |---------------------------|---------------------|--------------------|-----------------| | Average FULL (ms) | 1170 | 844 | 27.86% | | `payload.db.create` (ms) | 1413 | 691 | 51.12% | | `payload.db.find` (ms) | 2856 | 2204 | 22.83% | | `payload.db.deleteMany` (ms) | 15206 | 8439 | 44.53% | | `payload.db.updateOne` (ms) | 21444 | 12162 | 43.30% | | `payload.db.findOne` (ms) | 159 | 112 | 29.56% | | `payload.db.deleteOne` (ms) | 3729 | 2578 | 30.89% | | DB small FULL (ms) | 64473 | 46451 | 27.93% | --- ### Medium Dataset Performance | Metric | Before Optimization | After Optimization | Improvement (%) | |---------------------------|---------------------|--------------------|-----------------| | Average FULL (ms) | 9407 | 6210 | 33.99% | | `payload.db.create` (ms) | 10270 | 4321 | 57.93% | | `payload.db.find` (ms) | 20814 | 16036 | 22.93% | | `payload.db.deleteMany` (ms) | 126351 | 61789 | 51.11% | | `payload.db.updateOne` (ms) | 201782 | 99943 | 50.49% | | `payload.db.findOne` (ms) | 1081 | 817 | 24.43% | | `payload.db.deleteOne` (ms) | 28534 | 23363 | 18.12% | | DB medium FULL (ms) | 519518 | 342194 | 34.13% | --- ### Large Dataset Performance | Metric | Before Optimization | After Optimization | Improvement (%) | |---------------------------|---------------------|--------------------|-----------------| | Average FULL (ms) | 26575 | 17509 | 34.14% | | `payload.db.create` (ms) | 29085 | 12196 | 58.08% | | `payload.db.find` (ms) | 58497 | 43838 | 25.04% | | `payload.db.deleteMany` (ms) | 372195 | 173218 | 53.47% | | `payload.db.updateOne` (ms) | 544089 | 288350 | 47.00% | | `payload.db.findOne` (ms) | 3058 | 2197 | 28.14% | | `payload.db.deleteOne` (ms) | 82444 | 64730 | 21.49% | | DB large FULL (ms) | 1461097 | 969714 | 33.62% |
1 parent 034b442 commit e468292

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1339
-941
lines changed

packages/db-mongodb/src/count.ts

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,47 @@
11
import type { CountOptions } from 'mongodb'
22
import type { Count } from 'payload'
33

4-
import { flattenWhereToOperators } from 'payload'
5-
64
import type { MongooseAdapter } from './index.js'
75

6+
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
87
import { getSession } from './utilities/getSession.js'
98

109
export const count: Count = async function count(
1110
this: MongooseAdapter,
1211
{ collection, locale, req, where },
1312
) {
1413
const Model = this.collections[collection]
15-
const options: CountOptions = {
16-
session: await getSession(this, req),
17-
}
14+
const session = await getSession(this, req)
1815

19-
let hasNearConstraint = false
20-
21-
if (where) {
22-
const constraints = flattenWhereToOperators(where)
23-
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
24-
}
16+
const hasNearConstraint = getHasNearConstraint(where)
2517

2618
const query = await Model.buildQuery({
2719
locale,
2820
payload: this.payload,
21+
session,
2922
where,
3023
})
3124

3225
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
3326
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
3427

35-
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
36-
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
37-
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
38-
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
39-
// the correct indexed field
40-
options.hint = {
41-
_id: 1,
42-
}
43-
}
44-
4528
let result: number
4629
if (useEstimatedCount) {
47-
result = await Model.estimatedDocumentCount({ session: options.session })
30+
result = await Model.collection.estimatedDocumentCount()
4831
} else {
49-
result = await Model.countDocuments(query, options)
32+
const options: CountOptions = { session }
33+
34+
if (this.disableIndexHints !== true) {
35+
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
36+
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
37+
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
38+
// the correct indexed field
39+
options.hint = {
40+
_id: 1,
41+
}
42+
}
43+
44+
result = await Model.collection.countDocuments(query, options)
5045
}
5146

5247
return {

packages/db-mongodb/src/countGlobalVersions.ts

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,47 @@
11
import type { CountOptions } from 'mongodb'
22
import type { CountGlobalVersions } from 'payload'
33

4-
import { flattenWhereToOperators } from 'payload'
5-
64
import type { MongooseAdapter } from './index.js'
75

6+
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
87
import { getSession } from './utilities/getSession.js'
98

109
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(
1110
this: MongooseAdapter,
1211
{ global, locale, req, where },
1312
) {
1413
const Model = this.versions[global]
15-
const options: CountOptions = {
16-
session: await getSession(this, req),
17-
}
14+
const session = await getSession(this, req)
1815

19-
let hasNearConstraint = false
20-
21-
if (where) {
22-
const constraints = flattenWhereToOperators(where)
23-
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
24-
}
16+
const hasNearConstraint = getHasNearConstraint(where)
2517

2618
const query = await Model.buildQuery({
2719
locale,
2820
payload: this.payload,
21+
session,
2922
where,
3023
})
3124

3225
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
3326
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
3427

35-
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
36-
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
37-
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
38-
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
39-
// the correct indexed field
40-
options.hint = {
41-
_id: 1,
42-
}
43-
}
44-
4528
let result: number
4629
if (useEstimatedCount) {
47-
result = await Model.estimatedDocumentCount({ session: options.session })
30+
result = await Model.collection.estimatedDocumentCount()
4831
} else {
49-
result = await Model.countDocuments(query, options)
32+
const options: CountOptions = { session }
33+
34+
if (Object.keys(query).length === 0 && this.disableIndexHints !== true) {
35+
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
36+
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
37+
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
38+
// the correct indexed field
39+
options.hint = {
40+
_id: 1,
41+
}
42+
}
43+
44+
result = await Model.collection.countDocuments(query, options)
5045
}
5146

5247
return {

packages/db-mongodb/src/countVersions.ts

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,47 @@
11
import type { CountOptions } from 'mongodb'
22
import type { CountVersions } from 'payload'
33

4-
import { flattenWhereToOperators } from 'payload'
5-
64
import type { MongooseAdapter } from './index.js'
75

6+
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
87
import { getSession } from './utilities/getSession.js'
98

109
export const countVersions: CountVersions = async function countVersions(
1110
this: MongooseAdapter,
1211
{ collection, locale, req, where },
1312
) {
1413
const Model = this.versions[collection]
15-
const options: CountOptions = {
16-
session: await getSession(this, req),
17-
}
14+
const session = await getSession(this, req)
1815

19-
let hasNearConstraint = false
20-
21-
if (where) {
22-
const constraints = flattenWhereToOperators(where)
23-
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
24-
}
16+
const hasNearConstraint = getHasNearConstraint(where)
2517

2618
const query = await Model.buildQuery({
2719
locale,
2820
payload: this.payload,
21+
session,
2922
where,
3023
})
3124

3225
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
3326
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
3427

35-
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
36-
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
37-
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
38-
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
39-
// the correct indexed field
40-
options.hint = {
41-
_id: 1,
42-
}
43-
}
44-
4528
let result: number
4629
if (useEstimatedCount) {
47-
result = await Model.estimatedDocumentCount({ session: options.session })
30+
result = await Model.collection.estimatedDocumentCount()
4831
} else {
49-
result = await Model.countDocuments(query, options)
32+
const options: CountOptions = { session }
33+
34+
if (this.disableIndexHints !== true) {
35+
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
36+
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
37+
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
38+
// the correct indexed field
39+
options.hint = {
40+
_id: 1,
41+
}
42+
}
43+
44+
result = await Model.collection.countDocuments(query, options)
5045
}
5146

5247
return {

packages/db-mongodb/src/create.ts

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,44 @@
1-
import type { CreateOptions } from 'mongoose'
2-
import type { Create, Document } from 'payload'
1+
import type { Create } from 'payload'
32

43
import type { MongooseAdapter } from './index.js'
54

65
import { getSession } from './utilities/getSession.js'
76
import { handleError } from './utilities/handleError.js'
8-
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
7+
import { transform } from './utilities/transform.js'
98

109
export const create: Create = async function create(
1110
this: MongooseAdapter,
1211
{ collection, data, req },
1312
) {
1413
const Model = this.collections[collection]
15-
const options: CreateOptions = {
16-
session: await getSession(this, req),
17-
}
18-
19-
let doc
14+
const session = await getSession(this, req)
2015

21-
const sanitizedData = sanitizeRelationshipIDs({
22-
config: this.payload.config,
23-
data,
24-
fields: this.payload.collections[collection].config.fields,
25-
})
16+
const fields = this.payload.collections[collection].config.flattenedFields
2617

2718
if (this.payload.collections[collection].customIDType) {
28-
sanitizedData._id = sanitizedData.id
19+
data._id = data.id
2920
}
3021

22+
transform({
23+
adapter: this,
24+
data,
25+
fields,
26+
operation: 'create',
27+
})
28+
3129
try {
32-
;[doc] = await Model.create([sanitizedData], options)
33-
} catch (error) {
34-
handleError({ collection, error, req })
35-
}
30+
const { insertedId } = await Model.collection.insertOne(data, { session })
31+
data._id = insertedId
3632

37-
// doc.toJSON does not do stuff like converting ObjectIds to string, or date strings to date objects. That's why we use JSON.parse/stringify here
38-
const result: Document = JSON.parse(JSON.stringify(doc))
39-
const verificationToken = doc._verificationToken
33+
transform({
34+
adapter: this,
35+
data,
36+
fields,
37+
operation: 'read',
38+
})
4039

41-
// custom id type reset
42-
result.id = result._id
43-
if (verificationToken) {
44-
result._verificationToken = verificationToken
40+
return data
41+
} catch (error) {
42+
handleError({ collection, error, req })
4543
}
46-
47-
return result
4844
}
Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,39 @@
1-
import type { CreateOptions } from 'mongoose'
21
import type { CreateGlobal } from 'payload'
32

43
import type { MongooseAdapter } from './index.js'
54

65
import { getSession } from './utilities/getSession.js'
7-
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
8-
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
6+
import { transform } from './utilities/transform.js'
97

108
export const createGlobal: CreateGlobal = async function createGlobal(
119
this: MongooseAdapter,
1210
{ slug, data, req },
1311
) {
1412
const Model = this.globals
1513

16-
const global = sanitizeRelationshipIDs({
17-
config: this.payload.config,
18-
data: {
19-
globalType: slug,
20-
...data,
21-
},
22-
fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields,
23-
})
14+
const fields = this.payload.config.globals.find(
15+
(globalConfig) => globalConfig.slug === slug,
16+
).flattenedFields
2417

25-
const options: CreateOptions = {
26-
session: await getSession(this, req),
27-
}
18+
transform({
19+
adapter: this,
20+
data,
21+
fields,
22+
globalSlug: slug,
23+
operation: 'create',
24+
})
2825

29-
let [result] = (await Model.create([global], options)) as any
26+
const session = await getSession(this, req)
3027

31-
result = JSON.parse(JSON.stringify(result))
28+
const { insertedId } = await Model.collection.insertOne(data, { session })
29+
;(data as any)._id = insertedId
3230

33-
// custom id type reset
34-
result.id = result._id
35-
result = sanitizeInternalFields(result)
31+
transform({
32+
adapter: this,
33+
data,
34+
fields,
35+
operation: 'read',
36+
})
3637

37-
return result
38+
return data
3839
}

0 commit comments

Comments
 (0)