Storing JSON documents in a SQL database does not make it a document database. The strength of any database lies in its indexing capabilities, and SQL databases, even with JSON datatypes, lack the flexibility of document databases, particularly when dealing with arrays in an embedded One-to-Many relationships.
In SQL databases, normalized tables are first-class citizens, with single-key indexes optimized for handling equality and range filters (WHERE), sorting (ORDER BY), and projection coverage (SELECT) for individual tables. To avoid joining before filtering, a denormalized model is preferred when those operations involve multiple tables.
While SQL databases can store documents, including arrays for One-to-Many relationships, working with arrays in JSON necessitates the use of inverted indexes like PostgreSQL's GIN, which do not cover range filters, sorting, and projection like regular indexes.
In contrast, MongoDB treats documents as the core of its data model. Its indexing mechanisms naturally extend to handle documents and their arrays, retaining functionality for filtering, sorting, and projection coverage, as regular indexes can hangle multiple keys per documents.
In previous posts, we examined how a multi-key index supports sort operations. Now, let's explore the conditions under which a query projection is covered by the index, eliminating the need to fetch the document from the collection.
Here is the table of content of the tests:
- With default projection: IXSCAN ➤ FETCH
- Partial projection: IXSCAN ➤ FETCH ➤ PROJECTION_SIMPLE
- Full projection: IXSCAN ➤ PROJECTION_COVERED
- Query on a single-key entry: IXSCAN ➤ PROJECTION_COVERED
- Query on a multi-key entry: IXSCAN ➤ FETCH ➤ PROJECTION_SIMPLE
- Projection of "_id" : IXSCAN ➤ FETCH ➤ PROJECTION_SIMPLE
-
Projection of "$recordId" : IXSCAN ➤ PROJECTION_DEFAULT
<!-- TOC end -->
Here is a collection of friends, with their first name, last name, and phone number.
db.friends.insertMany([
{ firstName: "Rachel", lastName: "Green", phoneNumber: "555-1234" },
{ firstName: "Ross", lastName: "Geller", phoneNumber: "555-2345" },
{ firstName: "Monica", lastName: "Geller", phoneNumber: "555-3456" },
{ firstName: "Chandler", lastName: "Bing", phoneNumber: "555-4567" },
{ firstName: "Joey", lastName: "Tribbiani", phoneNumber: "555-6789" },
{ firstName: "Janice", lastName: "Hosenstein", phoneNumber: "555-7890" },
{ firstName: "Gunther", lastName: "Centralperk", phoneNumber: "555-8901" },
{ firstName: "Carol", lastName: "Willick", phoneNumber: "555-9012" },
{ firstName: "Susan", lastName: "Bunch", phoneNumber: "555-0123" },
{ firstName: "Mike", lastName: "Hannigan", phoneNumber: "555-1123" },
{ firstName: "Emily", lastName: "Waltham", phoneNumber: "555-2234" }
])
In any database, relational or document, implementing new use cases often requires an index for effective access patterns. For instance, for a reverse phone directory, I create an index where the key starts with the phone number. I add the names to the key in order to allow for index-only scans to benefit from the O(log n) scalability of B-Tree indexes:
db.friends.createIndex(
{ phoneNumber:1, firstName:1, lastName:1 }
)
To confirm that an index-only scan is occurring, I examine the execution plan specifically for PROJECTION_COVERED rather than FETCH
With default projection: IXSCAN ➤ FETCH
Due to its flexible schema, MongoDB cannot assume that all fields in every document within a collection are covered by the index. As a result, it must fetch the entire document:
db.friends.find(
{ phoneNumber:"555-6789" }
).explain('executionStats').executionStats
{
executionSuccess: true,
nReturned: 1,
executionTimeMillis: 0,
totalKeysExamined: 1,
totalDocsExamined: 1,
executionStages: {
isCached: false,
stage: 'FETCH',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
docsExamined: 1,
alreadyHasObj: 0,
inputStage: {
stage: 'IXSCAN',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
keyPattern: { phoneNumber: 1, firstName: 1, lastName: 1 },
indexName: 'phoneNumber_1_firstName_1_lastName_1',
isMultiKey: false,
multiKeyPaths: { phoneNumber: [], firstName: [], lastName: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
phoneNumber: [ '["555-6789", "555-6789"]' ],
firstName: [ '[MinKey, MaxKey]' ],
lastName: [ '[MinKey, MaxKey]' ]
},
keysExamined: 1,
seeks: 1,
dupsTested: 0,
dupsDropped: 0
}
}
}
Looking at the result, I can see the "_id" which is stored in the document:
db.friends.find(
{ phoneNumber:"555-6789" }
)
[
{
_id: ObjectId('680d46a1672e2e146dd4b0c6'),
firstName: 'Joey',
lastName: 'Tribbiani',
phoneNumber: '555-6789'
}
]
I can remove it from the projection as I don't need it.
Partial projection: IXSCAN ➤ FETCH ➤ PROJECTION_SIMPLE
I add a projection to exclude the "_id" from the result, but it doesn't remove the FETCH that gets the document with all fields:
db.friends.find(
{ phoneNumber:"555-6789" }
, { "_id":0 }
).explain('executionStats').executionStats
{
executionSuccess: true,
nReturned: 1,
executionTimeMillis: 0,
totalKeysExamined: 1,
totalDocsExamined: 1,
executionStages: {
isCached: false,
stage: 'PROJECTION_SIMPLE',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
transformBy: { _id: 0 },
inputStage: {
stage: 'FETCH',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
docsExamined: 1,
alreadyHasObj: 0,
inputStage: {
stage: 'IXSCAN',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
keyPattern: { phoneNumber: 1, firstName: 1, lastName: 1 },
indexName: 'phoneNumber_1_firstName_1_lastName_1',
isMultiKey: false,
multiKeyPaths: { phoneNumber: [], firstName: [], lastName: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
phoneNumber: [ '["555-6789", "555-6789"]' ],
firstName: [ '[MinKey, MaxKey]' ],
lastName: [ '[MinKey, MaxKey]' ]
},
keysExamined: 1,
seeks: 1,
dupsTested: 0,
dupsDropped: 0
}
}
}
}
Even if I know that my documents have no other fields, the query planner doesn't know it and must plan to get the document.
Full projection: IXSCAN ➤ PROJECTION_COVERED
When the projection declares all fields, and they are in the index key, there's no need to fetch the document as the projection is covered:
db.friends.find(
{ phoneNumber:"555-6789" }
, { "_id":0 , firstName:1, lastName:1, phoneNumber:1 }
).explain('executionStats').executionStats
{
executionSuccess: true,
nReturned: 1,
executionTimeMillis: 0,
totalKeysExamined: 1,
totalDocsExamined: 0,
executionStages: {
isCached: false,
stage: 'PROJECTION_COVERED',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
transformBy: { _id: 0, firstName: 1, lastName: 1, phoneNumber: 1 },
inputStage: {
stage: 'IXSCAN',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
keyPattern: { phoneNumber: 1, firstName: 1, lastName: 1 },
indexName: 'phoneNumber_1_firstName_1_lastName_1',
isMultiKey: false,
multiKeyPaths: { phoneNumber: [], firstName: [], lastName: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
phoneNumber: [ '["555-6789", "555-6789"]' ],
firstName: [ '[MinKey, MaxKey]' ],
lastName: [ '[MinKey, MaxKey]' ]
},
keysExamined: 1,
seeks: 1,
dupsTested: 0,
dupsDropped: 0
}
}
}
Such plan is an index only scan, optimal as it doesn't need to read documents.
Adding an array instead of a scalar value
Now that we have examined how a query is covered by a single-key index, where each document has a unique index entry, let's explore the implications of a multi-key index. In MongoDB, a field can contain a single value in one document and an array of values in another. I add such a document, where one of the friends has three phone numbers:
db.friends.insertOne({
firstName: "Phoebe",
lastName: "Buffay",
phoneNumber: ["555-3344", "555-4455", "555-5566"]
})
We refer to the index as a multi-key index, but in reality, it remains the same index in MongoDB. The distinction lies in its capacity to hold multiple entries per document, rather than solely single-key entries.
Query on a single-key entry: IXSCAN ➤ PROJECTION_COVERED
When I query the same single-key document as before, nothing changes and the projection is covered:
db.friends.find(
{ phoneNumber:"555-6789" }
, { "_id":0 , firstName:1, lastName:1, phoneNumber:1 }
).explain('executionStats').executionStats
{
executionSuccess: true,
nReturned: 1,
executionTimeMillis: 0,
totalKeysExamined: 1,
totalDocsExamined: 1,
executionStages: {
isCached: false,
stage: 'PROJECTION_SIMPLE',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
transformBy: { _id: 0, firstName: 1, lastName: 1, phoneNumber: 1 },
inputStage: {
stage: 'FETCH',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
docsExamined: 1,
alreadyHasObj: 0,
inputStage: {
stage: 'IXSCAN',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
keyPattern: { phoneNumber: 1, firstName: 1, lastName: 1 },
indexName: 'phoneNumber_1_firstName_1_lastName_1',
isMultiKey: true,
multiKeyPaths: { phoneNumber: [ 'phoneNumber' ], firstName: [], lastName: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
phoneNumber: [ '["555-6789", "555-6789"]' ],
firstName: [ '[MinKey, MaxKey]' ],
lastName: [ '[MinKey, MaxKey]' ]
},
keysExamined: 1,
seeks: 1,
dupsTested: 1,
dupsDropped: 0
}
}
}
}
A key advantage of MongoDB's flexible document model is that changes in structure, as the business evolves, do not impact existing documents. This is more agile than SQL databases where changing a One-to-One relationship to a One-to-Many requires complete refactoring of the model and extensive non-regression testing.
Query on a multi-key entry: IXSCAN ➤ FETCH ➤ PROJECTION_SIMPLE
There's a difference when I query the document with an array of values, visible with isMultiKey: true in the IXSCAN, and a FETCH stage:
db.friends.find(
{ phoneNumber:"555-4455" }
, { "_id":0 , firstName:1, lastName:1, phoneNumber:1 }
).explain('executionStats').executionStats
{
executionSuccess: true,
nReturned: 1,
executionTimeMillis: 0,
totalKeysExamined: 1,
totalDocsExamined: 1,
executionStages: {
isCached: false,
stage: 'PROJECTION_SIMPLE',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
transformBy: { _id: 0, firstName: 1, lastName: 1, phoneNumber: 1 },
inputStage: {
stage: 'FETCH',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
docsExamined: 1,
alreadyHasObj: 0,
inputStage: {
stage: 'IXSCAN',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
keyPattern: { phoneNumber: 1, firstName: 1, lastName: 1 },
indexName: 'phoneNumber_1_firstName_1_lastName_1',
isMultiKey: true,
multiKeyPaths: { phoneNumber: [ 'phoneNumber' ], firstName: [], lastName: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
phoneNumber: [ '["555-4455", "555-4455"]' ],
firstName: [ '[MinKey, MaxKey]' ],
lastName: [ '[MinKey, MaxKey]' ]
},
keysExamined: 1,
seeks: 1,
dupsTested: 1,
dupsDropped: 0
}
}
}
}
Understanding the behavior is simplified by recognizing that there is one index entry for each key, with only one entry being read (keysExamined: 1). However, the projection requires access to all associated values. Even if a single value is used to locate the document, the result must display all relevant values:
db.friends.find(
{ phoneNumber:"555-4455" }
, { "_id":0 , firstName:1, lastName:1, phoneNumber:1 }
)
[
{
firstName: 'Phoebe',
lastName: 'Buffay',
phoneNumber: [ '555-3344', '555-4455', '
by Franck Pachot
Franck Pachot
Storing JSON documents in a SQL database does not make it a document database. The strength of any database lies in its indexing capabilities, and SQL databases, even with JSON datatypes, lack the flexibility of document databases, particularly when dealing with arrays in an embedded One-to-Many relationships.
In SQL databases, normalized tables are first-class citizens, with single-key indexes optimized for handling equality and range filters (WHERE), sorting (ORDER BY), and projection coverage (SELECT) for individual tables. To avoid joining before filtering, a denormalized model is preferred when those operations involve multiple tables.
While SQL databases can store documents, including arrays for One-to-Many relationships, working with arrays in JSON necessitates the use of inverted indexes like PostgreSQL's GIN, which do not cover range filters, sorting, and projection like regular indexes.
In contrast, MongoDB treats documents as the core of its data model. Its indexing mechanisms naturally extend to handle documents and their arrays, retaining functionality for filtering, sorting, and projection coverage, as regular indexes can hangle multiple keys per documents.
In previous posts, we examined how a multi-key index supports sort operations. Now, let's explore the conditions under which a query projection is covered by the index, eliminating the need to fetch the document from the collection.
Here are the execution plans tested:
- With default projection: IXSCAN ➤ FETCH
- Partial projection: IXSCAN ➤ FETCH ➤ PROJECTION_SIMPLE
- Full projection: IXSCAN ➤ PROJECTION_COVERED
- Query on a single-key entry: IXSCAN ➤ PROJECTION_COVERED
- Query on a multi-key entry: IXSCAN ➤ FETCH ➤ PROJECTION_SIMPLE
- Projection of "_id" : IXSCAN ➤ FETCH ➤ PROJECTION_SIMPLE
- Projection of "$recordId" : IXSCAN ➤ PROJECTION_DEFAULT
Here is a collection of friends, with their first name, last name, and phone number.
db.friends.insertMany([
{ firstName: "Rachel", lastName: "Green", phoneNumber: "555-1234" },
{ firstName: "Ross", lastName: "Geller", phoneNumber: "555-2345" },
{ firstName: "Monica", lastName: "Geller", phoneNumber: "555-3456" },
{ firstName: "Chandler", lastName: "Bing", phoneNumber: "555-4567" },
{ firstName: "Joey", lastName: "Tribbiani", phoneNumber: "555-6789" },
{ firstName: "Janice", lastName: "Hosenstein", phoneNumber: "555-7890" },
{ firstName: "Gunther", lastName: "Centralperk", phoneNumber: "555-8901" },
{ firstName: "Carol", lastName: "Willick", phoneNumber: "555-9012" },
{ firstName: "Susan", lastName: "Bunch", phoneNumber: "555-0123" },
{ firstName: "Mike", lastName: "Hannigan", phoneNumber: "555-1123" },
{ firstName: "Emily", lastName: "Waltham", phoneNumber: "555-2234" }
])
In any database, relational or document, implementing new use cases often requires an index for effective access patterns. For instance, for a reverse phone directory, I create an index where the key starts with the phone number. I add the names to the key in order to allow for index-only scans to benefit from the O(log n) scalability of B-Tree indexes:
db.friends.createIndex(
{ phoneNumber:1, firstName:1, lastName:1 }
)
To confirm that an index-only scan is occurring, I examine the execution plan specifically for PROJECTION_COVERED rather than FETCH
With default projection: IXSCAN ➤ FETCH
Due to its flexible schema, MongoDB cannot assume that all fields in every document within a collection are covered by the index. As a result, it must fetch the entire document:
db.friends.find(
{ phoneNumber:"555-6789" }
).explain('executionStats').executionStats
{
executionSuccess: true,
nReturned: 1,
executionTimeMillis: 0,
totalKeysExamined: 1,
totalDocsExamined: 1,
executionStages: {
isCached: false,
stage: 'FETCH',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
docsExamined: 1,
alreadyHasObj: 0,
inputStage: {
stage: 'IXSCAN',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
keyPattern: { phoneNumber: 1, firstName: 1, lastName: 1 },
indexName: 'phoneNumber_1_firstName_1_lastName_1',
isMultiKey: false,
multiKeyPaths: { phoneNumber: [], firstName: [], lastName: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
phoneNumber: [ '["555-6789", "555-6789"]' ],
firstName: [ '[MinKey, MaxKey]' ],
lastName: [ '[MinKey, MaxKey]' ]
},
keysExamined: 1,
seeks: 1,
dupsTested: 0,
dupsDropped: 0
}
}
}
Looking at the result, I can see the "_id" which is stored in the document:
db.friends.find(
{ phoneNumber:"555-6789" }
)
[
{
_id: ObjectId('680d46a1672e2e146dd4b0c6'),
firstName: 'Joey',
lastName: 'Tribbiani',
phoneNumber: '555-6789'
}
]
I can remove it from the projection as I don't need it.
Partial projection: IXSCAN ➤ FETCH ➤ PROJECTION_SIMPLE
I add a projection to exclude the "_id" from the result, but it doesn't remove the FETCH that gets the document with all fields:
db.friends.find(
{ phoneNumber:"555-6789" }
, { "_id":0 }
).explain('executionStats').executionStats
{
executionSuccess: true,
nReturned: 1,
executionTimeMillis: 0,
totalKeysExamined: 1,
totalDocsExamined: 1,
executionStages: {
isCached: false,
stage: 'PROJECTION_SIMPLE',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
transformBy: { _id: 0 },
inputStage: {
stage: 'FETCH',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
docsExamined: 1,
alreadyHasObj: 0,
inputStage: {
stage: 'IXSCAN',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
keyPattern: { phoneNumber: 1, firstName: 1, lastName: 1 },
indexName: 'phoneNumber_1_firstName_1_lastName_1',
isMultiKey: false,
multiKeyPaths: { phoneNumber: [], firstName: [], lastName: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
phoneNumber: [ '["555-6789", "555-6789"]' ],
firstName: [ '[MinKey, MaxKey]' ],
lastName: [ '[MinKey, MaxKey]' ]
},
keysExamined: 1,
seeks: 1,
dupsTested: 0,
dupsDropped: 0
}
}
}
}
Even if I know that my documents have no other fields, the query planner doesn't know it and must plan to get the document.
Full projection: IXSCAN ➤ PROJECTION_COVERED
When the projection declares all fields, and they are in the index key, there's no need to fetch the document as the projection is covered:
db.friends.find(
{ phoneNumber:"555-6789" }
, { "_id":0 , firstName:1, lastName:1, phoneNumber:1 }
).explain('executionStats').executionStats
{
executionSuccess: true,
nReturned: 1,
executionTimeMillis: 0,
totalKeysExamined: 1,
totalDocsExamined: 0,
executionStages: {
isCached: false,
stage: 'PROJECTION_COVERED',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
transformBy: { _id: 0, firstName: 1, lastName: 1, phoneNumber: 1 },
inputStage: {
stage: 'IXSCAN',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
keyPattern: { phoneNumber: 1, firstName: 1, lastName: 1 },
indexName: 'phoneNumber_1_firstName_1_lastName_1',
isMultiKey: false,
multiKeyPaths: { phoneNumber: [], firstName: [], lastName: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
phoneNumber: [ '["555-6789", "555-6789"]' ],
firstName: [ '[MinKey, MaxKey]' ],
lastName: [ '[MinKey, MaxKey]' ]
},
keysExamined: 1,
seeks: 1,
dupsTested: 0,
dupsDropped: 0
}
}
}
Such plan is an index only scan, optimal as it doesn't need to read documents.
Adding an array instead of a scalar value
Now that we have examined how a query is covered by a single-key index, where each document has a unique index entry, let's explore the implications of a multi-key index. In MongoDB, a field can contain a single value in one document and an array of values in another. I add such a document, where one of the friends has three phone numbers:
db.friends.insertOne({
firstName: "Phoebe",
lastName: "Buffay",
phoneNumber: ["555-3344", "555-4455", "555-5566"]
})
We refer to the index as a multi-key index, but in reality, it remains the same index in MongoDB. The distinction lies in its capacity to hold multiple entries per document, rather than solely single-key entries.
Query on a single-key entry: IXSCAN ➤ PROJECTION_COVERED
When I query the same single-key document as before, nothing changes and the projection is covered:
db.friends.find(
{ phoneNumber:"555-6789" }
, { "_id":0 , firstName:1, lastName:1, phoneNumber:1 }
).explain('executionStats').executionStats
{
executionSuccess: true,
nReturned: 1,
executionTimeMillis: 0,
totalKeysExamined: 1,
totalDocsExamined: 1,
executionStages: {
isCached: false,
stage: 'PROJECTION_SIMPLE',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
transformBy: { _id: 0, firstName: 1, lastName: 1, phoneNumber: 1 },
inputStage: {
stage: 'FETCH',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
docsExamined: 1,
alreadyHasObj: 0,
inputStage: {
stage: 'IXSCAN',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
keyPattern: { phoneNumber: 1, firstName: 1, lastName: 1 },
indexName: 'phoneNumber_1_firstName_1_lastName_1',
isMultiKey: true,
multiKeyPaths: { phoneNumber: [ 'phoneNumber' ], firstName: [], lastName: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
phoneNumber: [ '["555-6789", "555-6789"]' ],
firstName: [ '[MinKey, MaxKey]' ],
lastName: [ '[MinKey, MaxKey]' ]
},
keysExamined: 1,
seeks: 1,
dupsTested: 1,
dupsDropped: 0
}
}
}
}
A key advantage of MongoDB's flexible document model is that changes in structure, as the business evolves, do not impact existing documents. This is more agile than SQL databases where changing a One-to-One relationship to a One-to-Many requires complete refactoring of the model and extensive non-regression testing.
Query on a multi-key entry: IXSCAN ➤ FETCH ➤ PROJECTION_SIMPLE
There's a difference when I query the document with an array of values, visible with isMultiKey: true in the IXSCAN, and a FETCH stage:
db.friends.find(
{ phoneNumber:"555-4455" }
, { "_id":0 , firstName:1, lastName:1, phoneNumber:1 }
).explain('executionStats').executionStats
{
executionSuccess: true,
nReturned: 1,
executionTimeMillis: 0,
totalKeysExamined: 1,
totalDocsExamined: 1,
executionStages: {
isCached: false,
stage: 'PROJECTION_SIMPLE',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
transformBy: { _id: 0, firstName: 1, lastName: 1, phoneNumber: 1 },
inputStage: {
stage: 'FETCH',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
docsExamined: 1,
alreadyHasObj: 0,
inputStage: {
stage: 'IXSCAN',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
keyPattern: { phoneNumber: 1, firstName: 1, lastName: 1 },
indexName: 'phoneNumber_1_firstName_1_lastName_1',
isMultiKey: true,
multiKeyPaths: { phoneNumber: [ 'phoneNumber' ], firstName: [], lastName: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
phoneNumber: [ '["555-4455", "555-4455"]' ],
firstName: [ '[MinKey, MaxKey]' ],
lastName: [ '[MinKey, MaxKey]' ]
},
keysExamined: 1,
seeks: 1,
dupsTested: 1,
dupsDropped: 0
}
}
}
}
Understanding the behavior is simplified by recognizing that there is one index entry for each key, with only one entry being read (keysExamined: 1). However, the projection requires access to all associated values. Even if a single value is used to locate the document, the result must display all relevant values:
db.friends.find(
{ phoneNumber:"555-4455" }
, { "_id":0 , firstName:1, lastName:1, phoneNumber:1 }
)
[
{
firstName: 'Phoebe',
lastName: 'Buffay',
phoneNumber: [ '555-3344', '555-4455', '555-5566
by Franck Pachot
April 25, 2025
Percona Database Performance Blog
You’re likely racing to enhance your applications with more intelligent, data-driven capabilities, whether through AI-powered models (which have moved into “must implement now!” territory), advanced search functions, real-time fraud detection, or geospatial analysis. As these demands grow, you face a significant challenge: efficiently storing, managing, and querying high-dimensional vector data within your existing database infrastructure. […]
by David Quilty
April 24, 2025
CedarDB Blog
What It Takes to Be PostgreSQL Compatible
We, and many other database enthusiasts, are big fans of PostgreSQL. Even though we built a database
system from scratch, we believe there are many good reasons for using
PostgreSQL. In fact, we like PostgreSQL so much that we made sure to build CedarDB to be compatible
with PostgreSQL.
Because of PostgreSQL’s popularity, we were not the first to develop a PostgreSQL compatible
database system. CMU’s “Database of Databases” lists
over 40 database systems that claim to be PostgreSQL compatible. Among them you can find database
systems from large cloud vendors such as AlloyDB from
Google or Aurora from AWS.
April 23, 2025
Stack - Convex Blog
Learn how to integrate Bluefox—a modern developer-first email API built on AWS SES—with Convex, the reactive backend database. This guide covers everything from sending transactional and triggered emails to setting up real-time webhooks and preparing for production. You’ll walk away knowing how to configure your environment, build reusable email clients, handle errors safely, and manage multiple deployment environments with best practices for SES authentication, bounce handling, and email tracking.
Percona Database Performance Blog
It is generally known and established that different database versions provide different sets of features and performance rates. Along with the binary versions, MongoDB has a property called Feature Compatibility Value, which allows administrators to run a binary version without enabling any features that are incompatible with the previous one. This property is generally helpful […]
by Pablo Claudino