Too many databases and collections in your MongoDB instance can degrade database performance and cause other issues.
The terms database and table, as used in traditional databases, correspond to MongoDB's database and collection, respectively.
In MongoDB, the WiredTiger storage engine creates a corresponding disk file for each collection. Each index also creates a new disk file. For every open resource, such as a file system object, the WiredTiger engine maintains a dhandle data structure. This structure stores information such as checkpoint details, session reference counts, pointers to in-memory B+ tree structures, and statistical data.
Therefore, the more databases and collections a MongoDB instance has, the more file system objects the WiredTiger engine must keep open. This increases the number of in-memory dhandle data structures. When a large number of these dhandle structures are held in memory, lock contention can occur, which degrades the instance's performance.
Potential issues
-
Slow queries and increased request latency due to
handleLockorschemaLock.Too many databases and collections can cause slow queries, which generate logs similar to the following example:
2024-03-07T15:59:16.856+0800 I COMMAND [conn4175155] command db.collections command: count { count: "xxxxxx", query: { A: 1, B: 1 }, $readPreference: { mode: "secondaryPreferred" }, $db: "db" } planSummary: COLLSCAN keysExamined:0 keysExaminedBySizeInBytes:0 docsExamined:1 docsExaminedBySizeInBytes:208 numYields:1 queryHash:916BD9E3 planCacheKey:916BD9E3 reslen:185 locks:{ ReplicationStateTransition: { acquireCount: { w: 2 } }, Global: { acquireCount: { r: 2 } }, Database: { acquireCount: { r: 2 } }, Collection: { acquireCount: { r: 2 } }, Mutex: { acquireCount: { r: 1 } } } storage:{ data: { bytesRead: 304, timeReadingMicros: 4 }, timeWaitingMicros: { handleLock: 40, schemaLock: 134101710 } } protocol:op_query 134268msThe preceding slow query log shows that a simple count operation on a collection with only one document had a long execution time. The
timeWaitingMicros: { handleLock: 40, schemaLock: 134101710 } } protocol:op_query 134268mspart of the log indicates that the read request spent too much time waiting to acquire the underlying storage engine'shandleLockandschemaLockdue to the large number of collections. -
Out-of-memory (OOM) errors during the initial synchronization phase when adding a new node.
-
Increased instance startup time.
-
Increased data synchronization time.
-
Longer time to back up and restore data.
-
Increased failure rate for physical backups.
-
Longer time for failure recovery.
A large number of databases and collections does not necessarily cause issues. The actual impact depends on factors such as the application's data model and workload. For example, consider two scenarios where databases of the same specification each have 10,000 collections and 100,000 total files. The challenges they face are completely different:
-
Accounting software system: Access patterns are highly concentrated. Most collections are used for cold data storage, and only a small, recent subset of collections is frequently accessed.
-
Multi-tenant management system: Tenants are isolated by using separate collections, and almost all collections are actively accessed or used.
Optimization methods
Remove unnecessary collections
Identify collections in the database that can be deleted, such as those that are expired or are no longer in use. Use the dropCollection command to remove them. For more information, see dropCollection().
Before performing any delete operations, ensure that you have an available full backup.
Use the following commands to view database and collection information:
-
Run the following command to view the number of collections in a database.
db.getSiblingDB(<dbName>).getCollectionNames().length -
Run the following command to view detailed information about a database, including the number of collections, indexes, and documents, as well as the total data size.
// View statistics for a specific database. db.getSiblingDB(<dbName>).stats() -
Run the following command to view detailed information about a specific collection.
// View statistics for a specific collection. db.getSiblingDB(<dbName>).<collectionName>.stats()
Remove unnecessary indexes
Reducing the number of indexes also decreases the number of disk files and corresponding dhandle structures maintained by the WiredTiger storage engine, which helps mitigate this issue.
Follow these basic principles for index optimization:
-
Avoid unused indexes
If a query never accesses a specific field, any index on that field will not be used. Such indexes are considered unused and can be removed.
-
Follow index prefix rules
For example, if you have both
{a:1}and{a:1,b:1}indexes, the first index is a redundant prefix of the second and can be removed. -
Consider index field order for equality queries
For equality matches, the order of fields in a composite index does not matter. For example, for queries on fields a and b, the indexes
{a:1,b:1}and{b:1,a:1}are functionally equivalent. You can remove the one that is used less frequently. -
Use the ESR rule for range queries
To build an optimal composite index for your queries, arrange the fields in the order of
Equality, Sort, Range. For more information, see The ESR (Equality, Sort, Range) Rule. -
Review indexes with low hit counts
An index with a low hit count often overlaps with a more efficient index. Analyze all relevant query patterns to determine if you can safely remove it.
You can use MongoDB's $indexStats aggregation stage to view statistics for all indexes in a collection. Ensure that you have the necessary permissions before running the following command.
// View index statistics for a specific collection.
db.getSiblingDB(<dbName>).<collectionName>.aggregate({"$indexStats":{}})
The command returns output similar to the following example.
{
"name" : "item_1_quantity_1",
"key" : { "item" : 1, "quantity" : 1 },
"host" : "examplehost.local:27018",
"accesses" : {
"ops" : NumberLong(1),
"since" : ISODate("2020-02-10T21:11:23.059Z")
}
}
The following table describes the parameters in the returned information.
|
Parameter |
Description |
|
name |
The name of the index. |
|
key |
Details of the index key. |
|
accesses.ops |
The number of operations that have used this index, which is equivalent to the index hit count. |
|
accesses.since |
The time from which statistics collection began. This field and the |
If you observe that an index has a very low hit count, for example, if accesses.ops is 1, it is likely a redundant or unused index that you can consider deleting. If your MongoDB instance is version 4.4 or later, you can use the hideIndex command to hide the index before dropping it. This allows you to confirm that there is no negative impact on your application for a period of time, reducing the risk of index deletion.
Example
Assume you have a collection of game players with a rule: "Whenever a player collects 20 coins, they are converted into 1 star." A document in the collection looks like this:
// players collection
{
"_id": "ObjectId(123)",
"first_name": "John",
"last_name": "Doe",
"coins": 11,
"stars": 2
}
The collection currently has the following five indexes, covering all fields:
-
_id(default index) -
{ last_name: 1 } -
{ last_name: 1, first_name: 1 } -
{ coins: -1 } -
{ stars: -1 }
The index optimization logic is as follows:
-
Application queries do not access the
coinsfield, so{ coins: -1 }is an unused index. -
According to the index prefix rule mentioned earlier, the
{ last_name: 1, first_name: 1 }index covers the{ last_name: 1 }index. Therefore, you can remove the{ last_name: 1 }index. -
Using the
$indexStatscommand, you observe that the hit count for{ stars: -1 }is low. However, at the end of a game round, the application requires sorting players by the number ofstarsin descending order for a leaderboard. Therefore, although it is not frequently used, the{ stars: -1 }index must be retained to avoid a full collection scan.
After optimization, three indexes remain in the collection:
-
_id -
{ last_name: 1, first_name: 1 } -
{ stars: -1 }
The benefits of this optimization include:
-
Reduced storage space.
-
Improved write performance.
If you have more questions about index optimization, please submit a ticket to contact Alibaba Cloud technical support for assistance.
Consolidate data from multiple collections
Consolidate data from multiple collections into a single collection to reduce the total collection count.
For example, a database named temperatures stores temperature data from sensors. A sensor operates from 10:00 AM to 10:00 PM, reading and storing temperature data every half hour. It stores the temperature data for each day in a separate collection named after the date.
The following snippets show partial data from two collections, temperatures.march-09-2020 and temperatures.march-10-2020.
-
Collection
temperatures.march-09-2020{ "_id": 1, "timestamp": "2020-03-09T010:00:00Z", "temperature": 29 } { "_id": 2, "timestamp": "2020-03-09T010:30:00Z", "temperature": 30 } ... { "_id": 25, "timestamp": "2020-03-09T022:00:00Z", "temperature": 26 } -
Collection
temperatures.march-10-2020{ "_id": 1, "timestamp": "2020-03-10T010:00:00Z", "temperature": 30 } { "_id": 2, "timestamp": "2020-03-10T010:30:00Z", "temperature": 32 } ... { "_id": 25, "timestamp": "2020-03-10T022:00:00Z", "temperature": 28 }
Over time, the number of collections in the database increases. Because MongoDB does not enforce a hard limit on the number of collections and this model lacks a clear data lifecycle policy, the number of collections and their corresponding indexes grows indefinitely.
Besides the problem of an ever-growing number of collections, this data model makes it difficult to perform queries that span multiple days. To query data across several days to analyze long-term temperature trends, you would need to use $lookup queries, which are less performant than querying within a single collection.
A better data model is to store all temperature readings in a single collection, with each day's data stored in a single document. This approach is an example of the Bucket Pattern. The following example shows the optimized model.
// temperatures.readings
{
"_id": ISODate("2020-03-09"),
"readings": [
{
"timestamp": "2020-03-09T010:00:00Z",
"temperature": 29
},
{
"timestamp": "2020-03-09T010:30:00Z",
"temperature": 30
},
...
{
"timestamp": "2020-03-09T022:00:00Z",
"temperature": 26
}
]
}
{
"_id": ISODate("2020-03-10"),
"readings": [
{
"timestamp": "2020-03-10T010:00:00Z",
"temperature": 30
},
{
"timestamp": "2020-03-10T010:30:00Z",
"temperature": 32
},
...
{
"timestamp": "2020-03-10T022:00:00Z",
"temperature": 28
}
]
}
The optimized model consumes far fewer resources than the original model. You no longer need to create indexes based on the time of day, and the default _id index on the collection facilitates queries by date. This also resolves the issue of an unbounded number of collections.
For time-series data, you can also consider using time series collections to address this problem.
The time series collection feature is supported only in MongoDB 5.0 and later versions.
Split the instance
If you cannot reduce the total number of databases and collections within a single MongoDB instance, consider a logical split of the database instance along with corresponding changes to your application.
You can approach this in two scenarios:
|
Scenario |
Splitting solution |
Considerations |
|
Collections are distributed across multiple databases |
If the business logic across databases is not closely related (for example, multiple applications or services share a single database instance), use DTS (Data Transmission Service) to migrate some databases to a new ApsaraDB for MongoDB replica set or sharded cluster instance. Before the migration is complete, you must also split the application logic and access methods accordingly. If the business logic across databases is closely related, refer to the splitting solution for the single-database scenario. |
|
|
Collections are concentrated in a single database |
Your business team must first determine if all collections can be split based on a specific dimension, such as region, city, priority, or any other meaningful business attribute. Then, use DTS to migrate some collections to one or more new MongoDB instances, effectively splitting one instance into N instances. Before the migration is complete, you must split the application logic and access methods accordingly. |
|
Example
A multi-tenant management platform uses a MongoDB database. In the initial data model, each tenant had a separate collection. As the business grew, the number of tenants exceeded one hundred thousand, and the total database size reached the terabyte level. The instance frequently experienced slow database access and high latency.
The business team decided to split the tenants by geographical region, dividing domestic tenants into North, Northeast, East, Central, South, Southwest, and Northwest China. They created new MongoDB instances in the corresponding availability zones for each region and performed multiple rounds of migration by using DTS. To meet the business's need for aggregate analysis, they also set up synchronization from the MongoDB instances to a data warehouse.
The split significantly reduced the number of databases and collections in each MongoDB instance, and the team scaled down the instance specifications accordingly. By adopting a nearest-region access principle, the application reduced request latency to the millisecond level, greatly improving the product experience. Subsequent instance maintenance also became much simpler.
Migrate to a sharded cluster with shard tags
If all your collections are in a single database and you want to manage them as a single logical instance, consider migrating your data to a sharded cluster architecture and using shard tags for management. The shard tag management method is slightly more complex and requires extra operational steps (using sh.addShardTag and sh.addTagRange). However, a single MongoDB instance still manages all collections, requiring minimal changes to your application. You only need to replace the connection string with that of the new sharded cluster instance.
For example, if your instance has 100,000 active collections, you can purchase a new 10-shard sharded cluster instance. Follow the process below to configure the instance and migrate data, which will result in 10,000 active collections per shard. The procedure is as follows:
-
Purchase a new sharded cluster instance. This example uses a 2-shard instance. For instructions on how to create a sharded cluster, see Create a sharded cluster instance.
-
Connect to the mongos node of the sharded cluster instance. For more information, see Connect to an ApsaraDB for MongoDB sharded cluster instance by using the mongo shell.
-
Run the following commands to add a shard tag to each shard.
sh.addShardTag("d-xxxxxxxxx1", "shard_tag1") sh.addShardTag("d-xxxxxxxxx2", "shard_tag2")Note-
Before you run these commands, ensure the account you are using has the required permissions.
-
DMS (Data Management) does not currently support the
sh.addShardTagcommand. We recommend connecting to the instance with the mongo shell or mongosh to run these commands.
-
-
Pre-configure the tag-based range distribution rules for all sharded collections.
use <dbName> sh.enableSharding("<dbName>") sh.addTagRange("<dbName>.test", {"_id":MinKey}, {"_id":MaxKey}, "shard_tag1") sh.addTagRange("<dbName>.test1", {"_id":MinKey}, {"_id":MaxKey}, "shard_tag2")This example uses
_idas the shard key. Choose a shard key that suits your workload, and ensure that all query operations include the shard key field. The shard key must be consistent with the field used in the next step. You must also use the[MinKey,MaxKey]boundaries to ensure that all data for a single collection resides on a single shard. -
Run the shardCollection operation for all collections to be migrated.
sh.shardCollection("<dbName>.test", {"_id":1}) sh.shardCollection("<dbName>.test1", {"_id":1}) -
Run the
sh.status()command to confirm that the rules are in effect.zhongli.test shard key: { "_id" : 1 } unique: false balancing: true chunks: d-xxx 1 { "_id" : { "$minKey" : 1 } } -->> { "_id" : { "$maxKey" : 1 } } on : d-xxx Timestamp(1, 0) tag: shard_tag1 { "_id" : { "$minKey" : 1 } } -->> { "_id" : { "$maxKey" : 1 } } zhongli.test1 shard key: { "_id" : 1 } unique: false balancing: true chunks: d-xxx 1 { "_id" : { "$minKey" : 1 } } -->> { "_id" : { "$maxKey" : 1 } } on : d-xxx Timestamp(1, 0) tag: shard_tag2 { "_id" : { "$minKey" : 1 } } -->> { "_id" : { "$maxKey" : 1 } } -
Migrate data from a replica set instance to a sharded cluster instance.
NoteBecause you have already pre-sharded the collections on the destination instance, all database and collection information already exists. Therefore, you must set the Processing Mode of Conflicting Tables to Ignore Errors and Proceed in your DTS task.
-
After verifying data consistency, switch your applications to access the new sharded cluster instance.
-
If you need to add more shards to the instance, you must repeat Step 3 to add tags to all new shards.
-
If new collections will be continuously added to the database, you must repeat Steps 4 and 5 for them. If you do not perform these steps, new collections will be created only on the primary shard, causing the number of collections on that shard to increase and potentially leading to performance instability again.
Migrate to a sharded cluster with zones
This method is similar to using shard tags, but it uses MongoDB's Zones feature instead. It requires extra operational steps, specifically sh.addShardToZone() and sh.updateZoneKeyRange().
The procedure is as follows:
-
Purchase a new sharded cluster instance. This example uses a 2-shard instance. For instructions on how to create a sharded cluster, see Create a sharded cluster instance.
-
Connect to the mongos node of the sharded cluster instance. For more information, see Connect to an ApsaraDB for MongoDB sharded cluster instance by using the mongo shell.
-
Run the following commands to assign each shard to a zone.
sh.addShardToZone("d-xxxxxxxxx1", "ZoneA") sh.addShardToZone("d-xxxxxxxxx2", "ZoneB")Note-
Before you run these commands, ensure the account you are using has the required permissions.
-
DMS does not currently support the
sh.addShardToZonecommand. We recommend connecting to the instance with the mongo shell or mongosh to run these commands.
-
-
Pre-configure the zone-based range distribution rules for all collections.
use <dbName> sh.enableSharding("<dbName>") sh.updateZoneKeyRange("<dbName>.test", { "_id": MinKey }, { "_id": MaxKey }, "ZoneA") sh.updateZoneKeyRange("<dbName>.test1", { "_id": MinKey }, { "_id": MaxKey }, "ZoneB")This example uses
_idas the shard key. Choose a shard key that suits your workload, and ensure that all query operations include the shard key field. The shard key must be consistent with the field used in the next step. You must also use the[MinKey,MaxKey]boundaries to ensure that all data for a single collection resides on a single shard. -
Run the shardCollection operation for all collections to be migrated.
sh.shardCollection("<dbName>.test", { "_id": 1 }) sh.shardCollection("<dbName>.test1", { "_id": 1 }) -
Run the
sh.status()command to view the sharding distribution and confirm that the zone configuration is in effect. The sample output is as follows:{ "_id" : "shardDistributionDB", "primary" : "d-xxx24", "partitioned" : false, "version" : { "uuid" : UUID("1dc635xxx"), "lastMod" : 1 } } shardDistributionDB.test shard key: { "_id" : "hashed" } unique: false balancing: true chunks: d-2ze9797089ef3704 1 { "_id" : { "$minKey" : 1 } } -->> { "_id" : { "$maxKey" : 1 } } on : d-xxx04 Timestamp(1, 0) tag: ZoneA { "_id" : { "$minKey" : 1 } } -->> { "_id" : { "$maxKey" : 1 } } shardDistributionDB.test1 shard key: { "_id" : "hashed" } unique: false balancing: true chunks: d-2zed2e752c35af24 1 { "_id" : { "$minKey" : 1 } } -->> { "_id" : { "$maxKey" : 1 } } on : d-xxx24 Timestamp(1, 0) tag: ZoneB { "_id" : { "$minKey" : 1 } } -->> { "_id" : { "$maxKey" : 1 } } -
Migrate data from a replica set instance to a sharded cluster instance.
NoteBecause you have already pre-sharded the collections on the destination instance, all database and collection information already exists. Therefore, you must set the Processing Mode of Conflicting Tables to Ignore Errors and Proceed in your DTS task.
-
After verifying data consistency, switch your applications to access the new sharded cluster instance.
-
If you need to add more shards to the instance, you must repeat Step 3 to assign zones to all new shards.
-
If new collections will be continuously added to the database, you must repeat Steps 4 and 5 for them. If you do not perform these steps, new collections will be created only on the primary shard, causing the number of collections on that shard to increase and potentially leading to performance instability again.
Risk advisory
We strongly advise against using the dropDatabase command to directly delete a database that contains a large number of collections.
After you run the dropDatabase command, the WiredTiger engine performs an asynchronous cleanup operation, sequentially removing the metadata and physical files for all collections marked for deletion. This operation can interfere with replication on secondary nodes, causing replication latency to increase. This, in turn, may trigger the flow control mechanism or affect all write operations that use {writeConcern:majority}.
Consider the following approaches to mitigate this risk:
-
Delete collections in batches with a reasonable interval between batches. After all collections are deleted, run the final
dropDatabasecommand. -
Use DTS or other migration tools to move the databases and collections you want to keep to a new instance. After the migration and cutover are complete, delete the old instance.
In all cases, you should configure appropriate replication latency alerts for your instance. If your instance encounters this issue, submit a ticket for technical assistance.
Summary
-
As a general rule, try to keep the total number of collections within a single replica set under 10,000. This number should be lower if individual collections have a large number of indexes (for example, more than 15).
-
If your application requirements necessitate a large number of collections, such as in a multi-tenant system that uses collection-based isolation, consider splitting your application logic and using sharded cluster instances.
-
If your database is already affected by too many collections and you are unsure how to modify your application's data model, you can submit a ticket to contact technical support for assistance.