Truy vấn dữ liệu lớn với MySQL trong python

Khi chúng ta truy xuất nhiều bản ghi trong cơ sở dữ liệu cùng 1 lúc sẽ sảy ra một số vấn đề sau.

** I. Hết bộ nhớ**

1. Vấn đề

Một câu truy vấn có thể trả về một lượng dữ liệu khổng lồ. Ví dụ câu truy vấn SELECT * FROM users có thể trả về hơn chục triệu bản ghi ở các ứng dụng lớn. Trong điều kiện bình thường, có lẽ lượng bản ghi lớn như vậy sẽ khiến cho chương trình của chúng ta sử dụng hết bộ nhớ và bị buộc phải chấm dứt giữa chừng, cho dù chúng ta có sử dụng fetchone, fetchmany hay fetchall để lấy dữ liệu.

Lý do là phần giao tiếp giữa Python và MySQL mặc định sẽ lấy tất cả các bản ghi của câu truy vấn về trước, chứa chúng trong bộ nhớ, rồi sau đó trả về cho Python một bản ghi, nhiều bản ghi, hay tất cả các bản ghi đó tùy thuộc vào hàm nào được gọi.

Điều này cũng dễ hiểu vì giao thức mạng của MySQL là mô hình yêu cầu/đáp trả (request/response). Một truy vấn là một yêu cầu. Và các bản ghi của truy vấn đó là một đáp trả. Cho nên trình điều khiển (driver) cần phải đọc hết toàn bộ đáp trả để kết thúc chu trình yêu cầu/đáp trả trước khi trả lời các lời gọi hàm fetchone, fetchmany hay fetchall. Nói một cách khác, các hàm fetchone hay fetchmany trả về kết quả đã có trong bộ nhớ.

Đó cũng chính là lý do vì sao chúng ta có thể gọi fetchone hay fetchmany nhiều lần. Các hàm này không tạo một chu trình yêu cầu/đáp trả mới mà chỉ đơn giản là tiếp tục trả về các bản ghi đã chứa trong bộ nhớ.

2. Cách khắc phục

Cách khắc phục là sử dụng SSCursor khi tạo con trỏ từ kết nối MySQL. Lớp SSCursor nằm trong mô-đun MySQLdb.

conn = MySQLdb.connect(...)
cursor = MySQLdb.SSCursor(conn)

Sau đó khi gọi fetchone hoặc fetchmany thì trình điều khiển sẽ không đọc toàn bộ đáp trả vào bộ nhớ nữa mà sẽ chỉ đọc vừa đủ để trả về bấy nhiêu bản ghi cho ta.

3. Lưu ý

  • Khi sử dụng SSCursor, ta nhất định phải đảm bảo chu trình yêu cầu/đáp trả được hoàn tất. Ví dụ câu truy vấn trả về 10 bản ghi, thì ta phải đảm bảo đọc hết 10 bản ghi này. Nếu ta chỉ gọi fetchone 5 lần, thì sẽ còn 5 bản ghi vẫn chưa được đọc hết, và do đó ta sẽ không thể gửi truy vấn khác trong cùng kết nối hiện tại.

SSCursor không giữ kết quả trong bộ nhớ nên ta sẽ không thể di chuyển con trỏ tới, hoặc lùi để truy xuất bản ghi ta cần. Điều duy nhất chúng ta có thể làm với SSCursor là đọc tuần tự tất cả các bản ghi.

II. Hết giờ (timeout)

1. Vấn đề

Với SSCursor ta có thể sẽ viết mã như sau để đọc lần lượt các bản ghi trong bộ nhớ đệm:

row = cursor.fetchone()
while row:
    # xử lý row
    row = cursor.fetchone()

Đoạn mã này đôi khi sẽ gây ra lỗi đệm Lost connection to MySQL server during query.

Lý do là việc xử lý từng bản ghi sẽ rất tốn thời gian, và ta sẽ không thể đọc đáp trả đủ nhanh, khiến cho máy chủ MySQL phải hoãn việc gửi tiếp các bản ghi về cho ta. Máy chủ MySQL chỉ có thể hoãn việc gửi thông tin trong một thời gian ngắn. Quá thời gian này, máy chủ sẽ tự ngắt kết nối.

2. Cách khắc phục

Tốt nhất là chúng ta sử dụng SSCursor để đọc tất cả các bản ghi từ máy chủ ở xa và ghi chúng vào một tập tin trên máy hiện tại. Sau đó ta đọc lại từ tập tin này và xử lý từng bản ghi đã lưu. Khi làm như vậy, chúng ta tránh được lỗi hết bộ nhớ đã đề cập ở trên, và hy vọng rằng việc ghi bản tin ra dĩa xảy ra đủ nhanh để ta có thể đọc bản ghi khác gần như ngay lập tức, tránh được lỗi hết giờ.

_Note _: Hiện nay một số Frameworkày sử dụng ORM như Django hỗ trợ rất tốt vấn đề truy vấn này. Nên gần như lập trình viên ko còn phải qúa bận tâm. Nhưng nếu bạn thực sự muốn cấu hình Project của mình theo 1 cách riêng biệt thì hãy bắt tay vào học từ những điều cơ bản nhất.