Fix MongoServerError: Exceeded memory limit for $group stage — allowDiskUse:true và Các Giải Pháp Tối Ưu

intermediate🍃 MongoDB2026-03-21| MongoDB 4.x / 5.x / 6.x / 7.x — mọi hệ điều hành (Linux, macOS, Windows), mọi driver (Node.js, Python, Go, Java)

Error Message

MongoServerError: Exceeded memory limit for $group stage, but didn't allow external sort. Pass allowDiskUse:true to opt in.
#mongodb#aggregation#memory#pipeline#allowDiskUse

TL;DR

MongoDB giới hạn 100MB RAM cho mỗi giai đoạn pipeline. Thêm allowDiskUse: true để mở khóa query ngay lập tức — dữ liệu trung gian sẽ được ghi ra đĩa thay vì bị crash. Với môi trường production, hãy kết hợp thêm $match sớm trong pipeline để giảm lượng dữ liệu đi vào các giai đoạn nặng.

// Sửa nhanh — Node.js driver
db.collection('orders').aggregate(
  [
    { $group: { _id: '$customerId', total: { $sum: '$amount' } } },
    { $sort: { total: -1 } }
  ],
  { allowDiskUse: true }  // ← thêm dòng này
);

Nguyên nhân xảy ra lỗi

Các giai đoạn như $group, $sort$bucket là dạng blocking — chúng phải tích lũy toàn bộ dữ liệu đầu vào trước khi cho ra kết quả. MongoDB giới hạn mức tích lũy này ở 100MB RAM mỗi giai đoạn. Vượt quá giới hạn, MongoDB sẽ dừng query thay vì để nó tiêu thụ hết bộ nhớ khả dụng.

Một số tình huống thường xuyên gây ra lỗi này:

  • Group trên collection lớn chưa được lọc (hàng chục triệu document)
  • Dùng $sort trước $limit — MongoDB phải sắp xếp toàn bộ trước khi cắt bớt
  • Xây dựng mảng bằng $push hoặc $addToSet trên nhiều document
  • Cluster dùng chung với wiredTigerCacheSizeGB thấp (ví dụ: 0.5GB trên instance 1GB)

Cách sửa 1: allowDiskUse (giải pháp thoát hiểm)

Ghi tràn ra đĩa cho phép MongoDB lưu trạng thái trung gian của một giai đoạn vào file tạm thay vì giữ tất cả trong RAM. Query sẽ hoàn thành — nhưng chậm hơn. Trên SSD NVMe nhanh, thời gian phản hồi có thể tăng 5–20 lần so với chạy trong bộ nhớ. Trên đĩa cơ hoặc lưu trữ qua mạng, còn chậm hơn nữa.

mongosh / mongo shell

db.orders.aggregate(
  [
    { $group: { _id: '$customerId', total: { $sum: '$amount' } } }
  ],
  { allowDiskUse: true }
);

Node.js (mongodb driver)

const cursor = collection.aggregate(pipeline, { allowDiskUse: true });
const results = await cursor.toArray();

Python (pymongo)

results = list(collection.aggregate(pipeline, allowDiskUse=True))

Mongoose

const results = await Order.aggregate(pipeline).allowDiskUse(true);

Phù hợp với: báo cáo một lần, xuất dữ liệu admin, migration dữ liệu. Không phù hợp với: API phục vụ request người dùng hoặc dashboard làm mới mỗi vài giây. Nếu pipeline này chạy thường xuyên, các cách sửa tiếp theo quan trọng hơn.

Cách sửa 2: Lọc sớm để thu nhỏ tập dữ liệu làm việc

Đặt $match lên đầu. Thay đổi đó thường cải thiện bộ nhớ hơn bất kỳ cách nào khác — MongoDB xử lý ít document hơn qua mọi giai đoạn tốn kém phía sau. Một collection 50 triệu đơn hàng sau khi lọc theo đơn hoàn thành của tháng trước có thể chỉ còn 200K document trước khi vào $group.

db.orders.aggregate([
  // ✅ Lọc TRƯỚC khi group — dùng index, thu nhỏ tập dữ liệu
  { $match: {
    createdAt: { $gte: new Date('2024-01-01') },
    status: 'completed'
  }},
  { $group: {
    _id: '$customerId',
    total: { $sum: '$amount' },
    count: { $sum: 1 }
  }}
]);

Nếu không có index trên các trường $match, MongoDB vẫn phải quét toàn bộ collection. Hãy tạo index:

db.orders.createIndex({ createdAt: 1, status: 1 });

Cách sửa 3: Chỉ project những trường cần thiết

Loại bỏ các trường ngay sau $match. Document chứa mảng nhúng hoặc trường văn bản lớn có thể nặng vài kilobyte mỗi cái. Loại bỏ tất cả những gì $group không dùng đến có thể giảm kích thước tập dữ liệu làm việc tới 50–80%.

db.orders.aggregate([
  { $match: { status: 'completed' } },
  // Loại bỏ các trường nặng (ví dụ: mảng line items nhúng) sớm
  { $project: { customerId: 1, amount: 1, _id: 0 } },
  { $group: {
    _id: '$customerId',
    total: { $sum: '$amount' }
  }}
]);

Cách sửa 4: Tránh dùng $push / $addToSet không giới hạn

$push bên trong $group thêm một phần tử cho mỗi document trong nhóm đó. Với một triệu đơn hàng chia cho 10.000 khách hàng, trung bình mỗi mảng có 100 phần tử — con số này tăng nhanh khi mỗi phần tử mang theo dữ liệu. Hãy đếm thay thế, hoặc giới hạn kích thước mảng sau khi group.

// ❌ Có thể tạo ra mảng rất lớn
{ $group: { _id: '$userId', items: { $push: '$productId' } } }

// ✅ Đếm thay thế, nếu đó là tất cả những gì bạn cần
{ $group: { _id: '$userId', itemCount: { $sum: 1 } } }

// ✅ Hoặc giới hạn kích thước mảng sau khi group
{ $project: { items: { $slice: ['$items', 100] } } }

Cách sửa 5: Tăng giới hạn cấp server (MongoDB 6.0+)

Từ MongoDB 6.0, internalQueryMaxBlockingSortMemoryUsageBytes kiểm soát ngưỡng tối đa cho mỗi giai đoạn. Giá trị mặc định là 104.857.600 byte (đúng 100MB). Tăng giá trị này cho thêm dung lượng — nhưng một query chạy bất thường giờ có thể tiêu thụ nhiều RAM hơn trước khi thất bại, vì vậy hãy cân nhắc sự đánh đổi này.

// Tăng giới hạn lên 500MB (mặc định là 100MB = 104857600 bytes)
db.adminCommand({
  setParameter: 1,
  internalQueryMaxBlockingSortMemoryUsageBytes: 524288000
});

MongoDB Atlas không cho phép thay đổi tham số này. Ở đó, bạn chỉ có thể tối ưu hóa pipeline hoặc nâng cấp tier cluster.

Kiểm tra: xác nhận đã sửa thành công

Chạy pipeline với explain để xem điều gì đã xảy ra ở mỗi giai đoạn:

const explain = await db.orders.aggregate(
  pipeline,
  { allowDiskUse: true, explain: true }
).toArray();

console.log(JSON.stringify(explain, null, 2));

Tìm usedDisk: true trong output của giai đoạn — điều đó xác nhận giai đoạn đã ghi tràn ra đĩa. Không có key usedDisk (hoặc giá trị false) nghĩa là đã chạy trong bộ nhớ. Trong production, so sánh operationTime trước và sau khi thay đổi để đo lường mức cải thiện thực tế.

Tóm tắt nhanh

  • Sửa ngay lập tức: thêm allowDiskUse: true vào tùy chọn aggregate
  • Thực hành tốt nhất: $match trước, $project thứ hai, group cuối cùng
  • Kiểm tra index: đảm bảo các trường trong $match đã được đánh index
  • Tránh: dùng $push/$addToSet không giới hạn trên collection lớn
  • Người dùng Atlas: nâng cấp tier hoặc tối ưu pipeline — không có quyền truy cập tham số server

Related Error Notes