tencent cloud

TencentDB for MongoDB

Query and Write Specifications

Download
포커스 모드
폰트 크기
마지막 업데이트 시간: 2026-07-10 22:00:05

Scenarios

Querying and writing are the core interactions between applications and MongoDB. In production environments, improper query and write operations often lead to severe availability issues, such as:
Full table scans cause API avalanches: core queries miss indexes, response times increase from milliseconds to seconds, and during peak periods, this triggers API timeouts and service unavailability.
Batch operations impact the database: large-volume write or delete operations instantly saturate CPU and disk I/O, causing online business query response times to degrade to seconds.
Write loss during primary/secondary switchover: Core services use the default Write Concern=1, and writes that have been confirmed as successful are lost after a primary node failure.
This document provides operational practices and recommendations for database querying and writing, aiming to standardize development practices and reduce system performance risks and business security vulnerabilities.

Query Specifications

Specification 1: Verifying Execution Plans Using explain()

Core Action: All production query statements must be validated using explain("executionStats") to confirm index hits and reasonable scan efficiency.
In production environments, developers typically assume that queries "should" use indexes. However, the actual execution path may degrade to a full table scan (COLLSCAN) due to reasons such as missing indexes or mismatched field order. explain() is the only reliable method for validating execution plans, as it can reveal key information including scan efficiency, sorting methods, and index hits. By comparing and analyzing these key metrics, you can quickly locate query performance bottlenecks:
Metric
Ideal Value
Requires Optimization
`winningPlan.stage`
`IXSCAN`
`COLLSCAN` (full table scan, missing index)
`totalKeysExamined / nReturned`
≈ 1
>> 10 (low index efficiency)
Whether `SORT` Stage Exists
None.
Yes (in-memory sorting; create an index for the sorting field)
Execution Stage Type Description:
Stage
Description
Ideal or Not
`COLLSCAN`
Full Table Scan
Optimization recommended
`IXSCAN`
Index scanning
Ideal
`FETCH`
Fetching documents from the table
Optimization recommended
`SORT`
In-memory sorting
Create an index for the sorting field.
Business Application: After an e-commerce order query API was deployed, its execution plan was not validated. A query that should have hit the userId_createTime composite index actually triggered a full table scan.
Problem Manifestation: During the Double 11 period, the response time of this API surged from 10ms to 5s, the order list page displayed a blank screen, triggering a large number of user complaints.
Optimization Operation: Before deployment, use explain("executionStats") to validate the execution plan. Ensure that winningPlan.stage is IXSCAN and the scan efficiency ratio is close to 1 before proceeding with the release.
explain() Usage Example: The following example demonstrates how to verify whether a query hits an index by examining its execution plan.
// Obtain execution statistics (Recommended)
db.t_orders.find({
status: "paid",
createTime: { $gte: ISODate("2024-01-01") }
}).explain("executionStats")

Specification 2: Using Projection in Queries

Core Action: Use Projection to return only the required fields during queries, and avoid returning the entire document.
MongoDB returns all fields of a document by default. When a document contains large fields such as lengthy HTML descriptions or binary attachments, indiscriminately returning the entire document significantly increases network traffic and memory consumption, leading to bandwidth bottlenecks and response delays in high-concurrency scenarios.
Business Application: An e-commerce platform's product detail page contains dozens of fields (including lengthy HTML descriptions), but the mobile client only needs to display the price and inventory.
Problem Manifestation: Each query returns the entire document. During major promotional events, under millions of requests, bandwidth surges and response latency skyrockets from 5ms to 200ms.
Optimization Operation: By using projection to return only the necessary fields, the data volume per query is reduced by 80%, and the API throughput is increased by 5 times.
Projection Query Example: The following example demonstrates how to reduce the amount of returned data through projection.
// Error: Returning the entire document (including unnecessary large fields)
db.t_products.find({ productId: "P001" })
// Correct: Returning only the required fields
db.t_products.find(
{ productId: "P001" },
{ _id: 1, name: 1, price: 1, stock: 1 }
)
// Correct: Excluding large fields
db.t_products.find(
{ productId: "P001" },
{ description: 0, htmlContent: 0 }
)

Specification 3: Avoiding Deep Pagination

Core Action: Avoid using skip + limit for deep pagination, and instead use cursor-based pagination on sorted fields.
skip(N) works by first scanning and discarding the first N records, and then returning the subsequent results. When N reaches tens of thousands or even hundreds of thousands, the query time increases linearly with the page number, and the response time for deep pagination can reach several seconds or even time out. Cursor-based pagination records the sort field value of the last record on the previous page and uses an index to directly locate the starting point of the next page, making its performance independent of the page number.
Business Application: The order list in the operations backend uses skip(100000).limit(20) to navigate to page 5000.
Problem Manifestation: MongoDB must first scan and skip the first 100,000 records, resulting in a single query taking over 10 seconds.
Optimization Operation: Switch to cursor-based pagination using _id and sort fields. Regardless of which page is navigated to, the query time remains stable within 10ms.
Cursor Pagination Example: The following example demonstrates the performance issues of deep pagination and the correct usage of cursor pagination.
// Error: skip pagination, where performance degrades sharply during deep pagination.
db.t_orders.find().sort({ createTime: -1 }).skip(100000).limit(20)
// Correct: Cursor pagination, stable performance
// Page 1
db.t_orders.find().sort({ createTime: -1, _id: -1 }).limit(20)
// Subsequent pages (using the createTime and _id of the last record from the previous page)
db.t_orders.find({
$or: [
{ createTime: { $lt: lastTime } },
{ createTime: lastTime, _id: { $lt: lastId } }
]
}).sort({ createTime: -1, _id: -1 }).limit(20)

Specification 4: Controlling the List Size for $in / $or

Core Action: Keep the list size for $in and $or within 50 items. If it exceeds this limit, perform queries in batches.
When MongoDB executes a $in query, it performs a multi-interval index scan. In the Plan Stage, $or is split into multiple sub-plans, where each branch may use a different index, requiring deduplication and merging. When the list is too large, the number of index lookups and the overhead of result merging increase linearly. This can cause a single query delay of up to several hundred milliseconds and impose an instantaneous load on the database.
Business Application: In a single request, the recommendation system needs to query 100 products that a user might be interested in, using {productId: {$in: [100 IDs]}}.
Problem Manifestation: MongoDB breaks it down into 100 index lookups and then merges the results. This can cause a delay of up to several hundred milliseconds and saturate the database connection pool under high concurrency.
Optimization Operation: Change to querying in 5 batches of 20 IDs each. The overall latency is actually lower, and no instantaneous pressure is imposed on the database.
Batch Query Example: The following example demonstrates how to split a large list $in query into multiple batches for execution.
// Exercise caution in high-concurrency scenarios: large list $in
db.t_orders.find({ productId: { $in: [/* 100 IDs */] } })
// Correct: Batch Querying
async function batchQuery(productIds, batchSize = 50) {
const results = [];
for (let i = 0; i < productIds.length; i += batchSize) {
const batch = productIds.slice(i, i + batchSize);
const docs = await db.t_orders.find({
productId: { $in: batch }
}).toArray();
results.push(...docs);
}
return results;
}

Write Specifications

Specification 5: Performing Partial Updates Using $set

Core Action: Use update operators such as $set for update operations. For large documents, full document replacement is not recommended.
Full document replacement (replaceOne) overwrites the entire document, causing unspecified fields to be lost. Operators such as $set modify only the specified fields, resulting in smaller data writes and no risk of concurrent overwrites.
Business Application: When a system updates a user's nickname, it retrieves the entire user document, modifies the nickname field, and then writes the entire document back.
Problem Manifestation: The amount of data written equals the full document size (approximately 10KB).
Optimization Operation: Switch to partial updates using $set. This completely resolves concurrency issues and reduces the amount of data written from 10KB to 100B.
Partial Update Example: The following example demonstrates the risks of full document replacement and the correct usage of partial updates with $set.
// Incorrect: Full document replacement (unspecified fields are lost, and concurrency issues may arise).
const user = await db.t_users.findOne({ _id: userId });
user.nickName = "new nickname";
await db.t_users.replaceOne({ _id: userId }, user); // Dangerous!
// Correct: Partial update with $set
await db.t_users.updateOne(
{ _id: userId },
{ $set: { nickName: "new nickname" } }
);
// Correct: Combine multiple update operators
await db.t_users.updateOne(
{ _id: userId },
{
$set: { nickName: "new nickname" },
$inc: { loginCount: 1 },
$currentDate: { lastLoginTime: true }
}
);

Specification 6: Including Query Conditions in Update Operations

Core Action: Update statements must include explicit and non-empty query conditions (Query).
An empty condition update ({}) matches all documents in the collection, posing the risk of inadvertently updating non-target data.
Business Application: While performing an account balance adjustment, a financial system's Update statement query parameter degraded to an empty object {} because the variables in the query condition were not assigned correctly.
Problem Manifestation: During an update operation, a non-target user's account balance was erroneously overwritten to the test value 0, triggering a major capital loss incident.
Optimization Actions: Enforce mandatory code reviews, ensure all Update operations include non-empty query condition checks, and add empty-condition interception protection at the application layer.
Secure Update Example: The following example demonstrates the risks of empty condition updates and the application-layer protection methods.
// Danger: An empty condition matches all documents, making it highly prone to erroneously updating non-target data.
db.t_users.updateOne({}, { $set: { status: "inactive" } })

// Correct: With explicit query conditions, precisely targeting the intended documents.
db.t_users.updateOne(
{ userId: "U001" },
{ $set: { status: "inactive" } }
)

// Correct: Batch updates must include explicit conditions, and the impact scope should be assessed before updating.
db.t_users.updateMany(
{ status: "pending", createTime: { $lt: ISODate("2024-01-01") } },
{ $set: { status: "expired" } }
)
Application-layer protection example:
# Python Example: Preventing Empty Condition Updates
def safe_update(collection, filter_query, update_doc, **kwargs):
if not filter_query or filter_query == {}:
raise ValueError("Update filter cannot be empty!")
return collection.update_one(filter_query, update_doc, **kwargs)

Specification 7: Using Operators for Array Updates

Core Action: When array elements are updated, do not fetch the entire array, modify it, and write it back. You must use array update operators.
Replacing an entire array in full writes the whole array field to the oplog as a single write operation. When the array is large (for example, a user's following list with thousands of entries), each modification of a single element generates oplog records of several kilobytes, causing severe "write amplification," accelerating the filling of the oplog, and exacerbating master-slave latency.
Business Application: A social platform's user following list can easily contain thousands of entries. A feature needs to modify the remarks for a specific person within that list.
Problem Manifestation: Fetching, modifying, and writing back the entire array generates several kilobytes of write volume and oplog records. In high-activity scenarios, the oplog fills up rapidly, and master-slave latency worsens.
Optimization Operation: Use $set + a positional operator to precisely update a single element, reducing the write volume by 99%.
Array Update Operator Example: The following example demonstrates the write amplification issue caused by full array replacement and the usage of operator-based precise updates.
// Incorrect: Full array replacement.
const order = await db.t_orders.findOne({ orderId: "ORD001" });
order.items[0].quantity = 5;
await db.t_orders.updateOne(
{ orderId: "ORD001" },
{ $set: { items: order.items } } // Full write
);
// Correct: Update matched elements using the $ positional operator.
await db.t_orders.updateOne(
{ orderId: "ORD001", "items.productId": "P001" },
{ $set: { "items.$.quantity": 5 } } // Precise update
);
// Correct: Use arrayFilters to update multiple elements (MongoDB 3.6+)
await db.t_orders.updateOne(
{ orderId: "ORD001" },
{ $set: { "items.$[elem].quantity": 5 } },
{ arrayFilters: [{ "elem.productId": { $in: ["P001", "P002"] } }] }
);
// Correct: Append elements to an array.
await db.t_orders.updateOne(
{ orderId: "ORD001" },
{ $push: { items: { productId: "P003", quantity: 1 } } }
);
// Correct: Remove elements from an array.
await db.t_orders.updateOne(
{ orderId: "ORD001" },
{ $pull: { items: { productId: "P003" } } }
);

Specification 8: Controlling the Rate for Batch Writes

Core Action: For batch writes, control the number of records per batch (recommended range: 1000 to 5000 records/batch), add pauses between batches, and avoid instantaneous pressure impacting the database.
Large-volume writes can instantly saturate MongoDB's CPU and disk I/O resources, impacting query response times for online business. By controlling the number of records per batch and adding pauses between batches, the write pressure is smoothly distributed over a longer time window.
Business Application: One million records are imported in a single operation by the data migration script, with a batch size of 100,000 records.
Symptom: A sudden write surge causes MongoDB CPU usage to spike to 100%, and online business query response time deteriorates from 10ms to 2s.
Optimized Operation: Change to 1000 records per batch with a 100ms pause between batches. Although migration duration increases, the online business remains completely unaffected.
Batch Write Example: The following example demonstrates how to split a large volume of data into multiple batches for smooth writing.
# Python Example: Batch Insertion
import time
def batch_insert(collection, documents, batch_size=1000, interval=0.1):
for i in range(0, len(documents), batch_size):
batch = documents[i:i + batch_size]
collection.insert_many(batch, ordered=False)
# Pause between batches to reduce database pressure.
if i + batch_size < len(documents):
time.sleep(interval)

Deletion Specifications

Specification 9: Prohibiting Direct Batch Removal

Core Action: Do not directly perform remove operations on large volumes of data. Use TTL indexes, batch deletion, or drop the collection instead.
Directly performing a remove operation on a large volume of data generates a substantial amount of delete oplog logs in a short time, saturating CPU and disk I/O resources and causing a sharp spike in primary-secondary replication delay. For time-based expiration cleanup scenarios, TTL indexes are smoothly executed by MongoDB background tasks, resulting in minimal impact on business operations. Options for deletion strategies:
Solution
Use Cases
Performance Impact
TTL index
Automatically expires based on time
Runs smoothly in the background with no perceptible impact.
drop collection
Emptying the entire collection
Completes instantly.
Batch deletion
Deleting partial data
Controllable, requires rate control.
Rename + create new
Retaining table structure and clearing data
Fast.
Business Application: The log system periodically cleans up historical data older than 30 days, and a script executes db.logs.remove({createTime: {$lt: a_timestamp}}).
Symptom: Hundreds of millions of records are deleted over several hours. During this period, database CPU usage reaches 100%, primary-secondary replication delay spikes to minutes, and online business operations read stale data.
Optimized Operation: Switch to using TTL indexes for automatic cleanup. MongoDB background tasks smoothly evict expired data, resulting in minimal business impact.
Batch Deletion Example: The following example demonstrates how to split a large deletion operation into multiple batches for smooth execution.
// Batch deletion, 1000 records per batch, with a 100ms interval between batches.
async function batchDelete(collection, filter, batchSize = 1000) {
let totalDeleted = 0;
while (true) {
// First, query the document IDs to be deleted.
const docs = await collection.find(filter, { _id: 1 })
.limit(batchSize)
.toArray();
if (docs.length === 0) break;
const ids = docs.map(d => d._id);
const result = await collection.deleteMany({ _id: { $in: ids } });
totalDeleted += result.deletedCount;
console.log(`${totalDeleted} records deleted...`);
// Pause between intervals.
await new Promise(resolve => setTimeout(resolve, 100));
}
return totalDeleted;
}

Running Environment

Operating System: Ubuntu 24.04.3 LTS / x86_64

Runtime Version: Node.js v22.13.1

Specification 10: Using Time-Based Sharding Instead of Individual Deletions for High Write-Expiration Scenarios

Core Action: For services with high write volumes and fixed data retention periods, write data into separate collections based on time dimensions (such as monthly sharding). When data expires, directly drop the entire collection to avoid the performance impact caused by row-by-row deletion.
Business Application: A user behavior tracking system writes tens of millions of records daily and only needs to retain data from the most recent month. Initially, TTL indexes were used for automatic expiration. As data volume grows, the TTL background thread continuously deletes a large amount of data, causing business performance jitter. This process also generates a substantial volume of oplog, leading to a continuous increase in primary-secondary replication delay.
Symptom: Tens of millions of expired documents need to be cleaned up daily via TTL. The background deletion task occupies disk I/O for extended periods, causing prolonged business jitter. Concurrently, primary-secondary replication delay deteriorates from seconds to minutes, and reads from secondary nodes frequently time out.
Optimized Operation: Switch to monthly sharding. Write data for each month into separate collections (such as t_events_202601 and t_events_202602), and route business read/write operations based on time. When expiration cleanup is performed, directly drop the older collection. This operation completes instantly, generates no deletion oplog, and allows primary-secondary replication delay to return to normal.
Monthly Sharding Example: The following example demonstrates the write routing and expiration cleanup methods for monthly sharding.
// Route writes to the corresponding collection based on month.
const collName = "t_events_" + new Date().toISOString().slice(0, 7).replace("-", "")
db.getCollection(collName).insertOne({
userId: "U001",
action: "click_buy",
createTime: new Date()
})

// Clean up expired data: Directly drop collections older than two months. This operation completes instantly.
db.t_events_202601.drop()


Write Concern Configuration

Specification 10: Using w:majority for Core Business Operations

Core Action: For core services such as payment and order processing, you must configure writeConcern: { w: "majority", j: true } to ensure that success is returned only after data is written to a majority of nodes.
The default w:1 acknowledges a write as successful only after the data is written to the Primary node. If the Primary node fails before the data is synchronized to a Secondary node, writes that have been acknowledged as successful will be lost after a primary/secondary switchover. w:"majority" requires that data be written to a majority of nodes before acknowledgment, ensuring that data is not lost even if the Primary node fails.
Configuration
Data Security
Write Latency
Use Cases
`{ w: 1 }`
Medium
Lowest
Scenarios with low data security requirements, such as logging
`{ w: "majority" }`
High
Relatively high
Core business, transactions, and orders
`{ w: 1, j: false }`
Extremely low
Lowest
Do not use. Data loss may occur.
`{ w: "majority", j: true }`
Extremely high
Relatively high
Finance, payments, and other capital-related operations
Business Application: A payment system uses the default w:1 configuration, and success is returned immediately after data is written to the Primary node.
Symptom: The Primary node experiences a hardware failure after write acknowledgment but before data is synchronized to a Secondary node. After the primary/secondary switchover, payment records that were acknowledged as successful are lost, resulting in a situation where users have paid but the order status is not updated.
Optimized Operation: Switch to w:"majority". Success is returned only after data is synchronized to a majority of nodes. Write latency increases slightly, but data loss is zero during a primary/secondary switchover.
Write Concern Configuration Example: The following example demonstrates how to configure different Write Concern levels.
// Prohibited: j:false may cause data loss.
db.t_orders.insertOne(
{ orderId: "ORD001" },
{ writeConcern: { w: 1, j: false } } // Dangerous!
)
// Correct: Use majority for core services.
db.t_orders.insertOne(
{ orderId: "ORD001", amount: 199.00 },
{ writeConcern: { w: "majority", j: true, wtimeout: 5000 } }
)
// Connection String Configuration Method (Globally Effective)
// mongodb://user:pwd@host1,host2,host3/db?w=majority&j=true&wtimeout=5000

Aggregation Operation Specifications

Specification 11: Aggregation Pipeline Optimization Principles

Core Action: In an aggregation pipeline, place $match at the beginning and follow it immediately with $project to reduce fields. Avoid overusing $unwind.
The execution order of an aggregation pipeline directly determines the volume of intermediate data. If you first execute $unwind to expand arrays and then $match to filter, the intermediate data volume can be expanded by tens of times. Conversely, if you first execute $match to filter and then $unwind, the data volume can be significantly reduced at the source.
Business Application: In the data analysis system, the aggregation pipeline first uses $unwind to expand arrays (expanding 1 million documents to 100 million entries), then uses $match to filter (leaving 100,000 entries), and finally uses $group for aggregation.
Symptom: The volume of intermediate data expands by 100 times, memory usage exceeds the limit and triggers an error, and the execution time exceeds the timeout threshold.
Optimized Operation: Adjust the order to first filter with $match (reducing from 1 million to 10,000), then expand with $unwind (expanding to 1 million entries). This reduces memory usage to a manageable range and cuts execution time from a timeout to 5 seconds.
Aggregation Pipeline Optimization Example: The following example demonstrates the impact of pipeline order on performance.
// Error: Expanding before filtering causes intermediate data volume to expand.
db.t_orders.aggregate([
{ $unwind: "$items" }, // Unwinding first causes data volume to expand.
{ $match: { status: "paid" } }, // Then filter
{ $group: { _id: "$items.category", total: { $sum: "$items.amount" } } }
])
// Correct: Filtering before expanding reduces data volume at the source.
db.t_orders.aggregate([
{ $match: { status: "paid" } }, // Filter first to reduce data volume.
{ $project: { items: 1 } }, // Keep only the required fields.
{ $unwind: "$items" }, // Then unwind.
{ $group: { _id: "$items.category", total: { $sum: "$items.amount" } } }
])

Specification 12: Offloading Complex Computations to the Application Layer

Core Action: In high-concurrency scenarios, it is recommended to offload complex computations from the database to the application layer or an offline system.
MongoDB's aggregation pipeline is suitable for medium-complexity data processing. When operations involve heavy computations such as multi-layer $lookup joins or large-scale $group aggregations, a single aggregation can take tens of seconds. During peak hours, even a small number of such requests can saturate the database CPU, impacting core business operations.
Scenario
Not Recommended
Recommended
Complex reports
Real-time Aggregation
Offline computing + caching
Multi-table Join
Multiple `$lookup` operations
Application-layer stitching
Real-time statistics
MapReduce
Pre-aggregation + caching
Business Application: The real-time report uses Aggregation for complex statistics, involving multi-layer $lookup, $unwind, and $group operations. A single aggregation takes more than 10 seconds.
Symptom: During peak hours, a small number of report requests saturate the database CPU, causing the query response time for core transaction business to deteriorate.
Optimization Operation: Migrate complex computations to Spark for offline processing. Write the results to a cache for frontend queries. This reduces the database load by 70%.

Query and Write Checklist

Pre-Launch Review

Check Item
Verification Method
Passing Criteria
All queries hit indexes.
`explain("executionStats")`
The `stage` is `IXSCAN`, with no `COLLSCAN`.
No in-memory sorting
`explain("executionStats")`
No `SORT` Stage
Reasonable scan efficiency
`totalDocsExamined / nReturned`
A ratio close to 1
Using projection
Code review
Returning only the necessary fields
Conditional Update
Code review
No empty conditions
Using $set for updates
Code review
Full document replacement is not recommended for scenarios involving updates to only a few fields.
Controlled batch operations
Code review
Limit each batch to ≤ 1000 items, and appropriately control the concurrency.
Core business w:majority
Check Write Concern.
Using majority for payment/order transactions.

Periodic Inspection

Check Item
Verification Method
Recommended Action
Slow query analysis
Slow query log
Regularly analyze queries that take longer than 100 ms.
Write latency monitoring
Monitoring dashboard
Investigate the cause when the P99 latency is abnormal.
Oplog utilization
`rs.printReplicationInfo()`
Oplog window period > 24 hours

FAQs

Q1: How to Determine Whether a Query Needs Optimization

// 1. Use explain to view the execution plan.
var result = db.t_orders.find({ status: "paid" }).explain("executionStats")

// 2. Check whether indexes are used.
// A winningPlan.stage value of IXSCAN indicates that an index is hit.
// A winningPlan.stage value of COLLSCAN indicates a full collection scan, which requires immediate optimization.

// 3. Check scan efficiency.
var stats = result.executionStats
if (stats.nReturned === 0) {
print("The returned result is empty. Please verify that the query conditions are correct.")
} else if (stats.totalKeysExamined === 0) {
print("No index is used (full collection scan). Please refer to Step 2 to determine whether an index needs to be created.")
} else {
print("Index Scan Ratio:", (stats.totalKeysExamined / stats.nReturned).toFixed(2))
print("Document Scan Ratio:", (stats.totalDocsExamined / stats.nReturned).toFixed(2))
// A value close to 1 for both ratios is ideal.
// An index scan ratio significantly greater than 1 indicates insufficient index selectivity or redundant scanning.
// A document scan ratio significantly greater than 1 indicates that a large number of documents that do not meet the criteria still need to be filtered by returning to the table after index filtering.
}

// 4. Check whether in-memory sorting exists.
// The presence of a SORT stage in executionStages indicates that sorting is not performed using an index and has degraded to in-memory sorting.
// Common causes: The sorting field is not included in the index, or the sort order of the field in the index does not match the query.

Q2: How to Safely Perform Batch Data Updates

1. Assess the impact scope: The secondary node executes count() to confirm the number of matching documents.
2. Select an Off-Peak Period: Perform the operation during a service off-peak period.
3. Execute in Batches: Process 1000 to 5000 records per batch, with a pause between batches.
4. Monitor Resources: Observe CPU, memory, and oplog utilization.
5. Record Progress: Save the _id of the last record in each batch to support resumable execution.

도움말 및 지원

문제 해결에 도움이 되었나요?

피드백