trình tạo / tạo trình lặp SqlAlchemy tích hợp hiệu quả bộ nhớ?


90

Tôi có một bảng MySQL bản ghi ~ 10 triệu mà tôi giao diện bằng SqlAlchemy. Tôi nhận thấy rằng các truy vấn trên các tập hợp con lớn của bảng này sẽ tiêu tốn quá nhiều bộ nhớ mặc dù tôi nghĩ rằng tôi đang sử dụng một trình tạo tích hợp có thể tìm nạp các phần nhỏ cỡ nhỏ của tập dữ liệu một cách thông minh:

for thing in session.query(Things):
    analyze(thing)

Để tránh điều này, tôi thấy mình phải xây dựng trình vòng lặp của riêng mình.

lastThingID = None
while True:
    things = query.filter(Thing.id < lastThingID).limit(querySize).all()
    if not rows or len(rows) == 0: 
        break
    for thing in things:
        lastThingID = row.id
        analyze(thing)

Điều này là bình thường hay có điều gì đó tôi đang thiếu liên quan đến máy phát điện tích hợp SA?

Câu trả lời cho câu hỏi này dường như chỉ ra rằng mức tiêu thụ bộ nhớ không được mong đợi.


Tôi có một cái gì đó rất giống nhau, ngoại trừ việc nó tạo ra "thing". Hoạt động tốt hơn so với tất cả các giải pháp khác
iElectric

2
Phải không Thing.id> lastThingID? Và "hàng" là gì?
synergetic

Câu trả lời:


118

Hầu hết các triển khai DBAPI đều đệm đầy đủ các hàng khi chúng được tìm nạp - vì vậy thông thường, trước khi SQLAlchemy ORM nhận được một kết quả, toàn bộ tập kết quả sẽ nằm trong bộ nhớ.

Nhưng sau đó, cách Queryhoạt động là nó tải hoàn toàn kết quả đã cho được đặt theo mặc định trước khi trả về cho bạn các đối tượng của bạn. Sự hợp lý ở đây liên quan đến các truy vấn không chỉ là các câu lệnh SELECT đơn giản. Ví dụ: trong các phép nối đến các bảng khác có thể trả về cùng một đối tượng nhiều lần trong một tập kết quả (phổ biến với tải nhanh), tập hợp đầy đủ các hàng cần phải có trong bộ nhớ để có thể trả về kết quả chính xác, nếu không thì các tập hợp và tương tự có thể chỉ được điền một phần.

Vì vậy, Querycung cấp một tùy chọn để thay đổi hành vi này thông qua yield_per(). Lệnh gọi này sẽ tạo ra các Queryhàng theo lô, nơi bạn cung cấp cho nó kích thước lô. Như trạng thái tài liệu, điều này chỉ thích hợp nếu bạn không thực hiện bất kỳ loại tải bộ sưu tập háo hức nào, vì vậy về cơ bản nó là nếu bạn thực sự biết mình đang làm gì. Ngoài ra, nếu các hàng DBAPI bên dưới bộ đệm trước, sẽ vẫn có chi phí bộ nhớ đó nên cách tiếp cận chỉ có quy mô tốt hơn một chút so với việc không sử dụng nó.

Tôi hầu như không bao giờ sử dụng yield_per(); thay vào đó, tôi sử dụng phiên bản tốt hơn của phương pháp LIMIT mà bạn đề xuất ở trên bằng cách sử dụng các hàm cửa sổ. LIMIT và OFFSET có một vấn đề lớn là các giá trị OFFSET rất lớn khiến truy vấn ngày càng chậm hơn, vì OFFSET của N khiến nó chuyển trang qua N hàng - giống như thực hiện cùng một truy vấn năm mươi lần thay vì một, mỗi lần đọc một số lượng hàng lớn hơn và lớn hơn. Với cách tiếp cận hàm cửa sổ, tôi tìm nạp trước một tập hợp các giá trị "cửa sổ" tham chiếu đến các phần của bảng mà tôi muốn chọn. Sau đó, tôi phát ra các câu lệnh SELECT riêng lẻ mà mỗi câu lệnh kéo từ một trong các cửa sổ đó tại một thời điểm.

Phương pháp tiếp cận chức năng cửa sổ có trên wiki và tôi sử dụng nó rất thành công.

Cũng lưu ý: không phải tất cả các cơ sở dữ liệu đều hỗ trợ các chức năng cửa sổ; bạn cần Postgresql, Oracle hoặc SQL Server. IMHO sử dụng ít nhất Postgresql chắc chắn đáng giá - nếu bạn đang sử dụng cơ sở dữ liệu quan hệ, bạn cũng có thể sử dụng tốt nhất.


Bạn đề cập đến Truy vấn cài đặt mọi thứ để so sánh danh tính. Có thể tránh được điều này bằng cách sắp xếp theo khóa chính và chỉ so sánh các kết quả liên tiếp không?
Tobu

vấn đề là nếu bạn mang lại một cá thể có danh tính X, ứng dụng sẽ nắm giữ nó và sau đó đưa ra quyết định dựa trên thực thể này và thậm chí có thể thay đổi nó. Sau đó, có lẽ (thực sự thường là) ngay cả ở hàng tiếp theo, kết quả giống nhau sẽ xuất hiện trở lại, có lẽ để thêm nhiều nội dung hơn vào bộ sưu tập của nó. Do đó, ứng dụng đã nhận đối tượng ở trạng thái chưa hoàn chỉnh. sắp xếp không giúp ích gì ở đây vì vấn đề lớn nhất là hoạt động của quá trình tải nhanh - cả tải "đã tham gia" và "truy vấn con" đều có các vấn đề khác nhau.
zzzeek

Tôi đã hiểu điều "hàng tiếp theo cập nhật bộ sưu tập", trong trường hợp đó bạn chỉ cần nhìn trước một hàng db để biết khi nào bộ sưu tập hoàn tất. Việc triển khai tải nhanh sẽ phải hợp tác với sắp xếp, để cập nhật bộ sưu tập luôn được thực hiện trên các hàng liền kề.
Tobu

tùy chọn output_per () luôn ở đó khi bạn tin rằng truy vấn bạn đang gửi tương thích với việc phân phối từng phần kết quả. Tôi đã dành một phiên chạy marathon kéo dài vài ngày để cố gắng kích hoạt hành vi này trong mọi trường hợp, luôn có những điều tối nghĩa, nghĩa là, cho đến khi chương trình của bạn sử dụng một trong số chúng, các cạnh không thành công. Đặc biệt, không thể giả định dựa vào đặt hàng. Như mọi khi, tôi hoan nghênh các đóng góp mã thực tế.
zzzeek

1
Vì tôi đang sử dụng postgres nên có thể sử dụng giao dịch chỉ đọc Lặp lại Đọc và chạy tất cả các truy vấn cửa sổ trong giao dịch đó.
schatten

23

Tôi không phải là chuyên gia về cơ sở dữ liệu, nhưng khi sử dụng SQLAlchemy làm lớp trừu tượng Python đơn giản (tức là không sử dụng đối tượng Truy vấn ORM), tôi đã nghĩ ra một giải pháp thỏa mãn để truy vấn bảng 300M hàng mà không sử dụng bộ nhớ ...

Đây là một ví dụ giả:

from sqlalchemy import create_engine, select

conn = create_engine("DB URL...").connect()
q = select([huge_table])

proxy = conn.execution_options(stream_results=True).execute(q)

Sau đó, tôi sử dụng fetchmany()phương thức SQLAlchemy để lặp lại các kết quả trong một whilevòng lặp vô hạn :

while 'batch not empty':  # equivalent of 'while True', but clearer
    batch = proxy.fetchmany(100000)  # 100,000 rows at a time

    if not batch:
        break

    for row in batch:
        # Do your stuff here...

proxy.close()

Phương pháp này cho phép tôi thực hiện tất cả các loại tổng hợp dữ liệu mà không cần bất kỳ chi phí bộ nhớ nguy hiểm nào.

NOTE các stream_resultslàm việc với Postgres và pyscopg2bộ chuyển đổi, nhưng tôi đoán nó sẽ không làm việc với bất kỳ DBAPI, cũng không phải với bất kỳ trình điều khiển cơ sở dữ liệu ...

Có một usecase thú vị trong bài đăng blog này đã truyền cảm hứng cho phương pháp trên của tôi.


1
Nếu một người đang làm việc trên postgres hoặc mysql (with pymysql), thì đây phải là câu trả lời được IMHO chấp nhận.
Yuki Inoue

1
Đã cứu mạng tôi, khi thấy các truy vấn của tôi ngày càng chạy chậm hơn. Tôi đã công cụ ở trên trên pyodbc (từ máy chủ sql đến postgres) và nó đang chạy như một giấc mơ.
Ed Baker

Đây là cách tiếp cận tốt nhất đối với tôi. Vì tôi đang sử dụng ORM, tôi cần biên dịch SQL sang phương ngữ của mình (Postgres) và sau đó thực thi trực tiếp từ kết nối (không phải từ phiên) như được hiển thị ở trên. Biên dịch "làm thế nào để" tôi tìm thấy trong câu hỏi khác stackoverflow.com/questions/4617291 . Cải thiện vận tốc là rất lớn. Thay đổi từ JOINS thành SUBQUERIES cũng làm tăng hiệu suất đáng kể. Bạn cũng nên sử dụng sqlalchemy_mixins, việc sử dụng smart_query đã giúp rất nhiều để tạo truy vấn hiệu quả nhất. github.com/absent1706/sqlalchemy-mixins
Gustavo Gonçalves

14

Tôi đã xem xét việc duyệt / phân trang hiệu quả với SQLAlchemy và muốn cập nhật câu trả lời này.

Tôi nghĩ rằng bạn có thể sử dụng lệnh gọi lát cắt để giới hạn đúng phạm vi của một truy vấn và bạn có thể sử dụng lại nó một cách hiệu quả.

Thí dụ:

window_size = 10  # or whatever limit you like
window_idx = 0
while True:
    start,stop = window_size*window_idx, window_size*(window_idx+1)
    things = query.slice(start, stop).all()
    if things is None:
        break
    for thing in things:
        analyze(thing)
    if len(things) < window_size:
        break
    window_idx += 1

Điều này có vẻ rất đơn giản và nhanh chóng. Tôi không chắc .all()là cần thiết. Tôi nhận thấy tốc độ được cải thiện rất nhiều sau cuộc gọi đầu tiên.
hamx0r

@ hamx0r Tôi nhận thấy đây là một bình luận cũ nên chỉ để lại cho hậu thế. Nếu không có .all()những điều biến là một truy vấn mà không hỗ trợ len ()
David

9

Theo tinh thần của câu trả lời của Joel, tôi sử dụng như sau:

WINDOW_SIZE = 1000
def qgen(query):
    start = 0
    while True:
        stop = start + WINDOW_SIZE
        things = query.slice(start, stop).all()
        if len(things) == 0:
            break
        for thing in things:
            yield thing
        start += WINDOW_SIZE

things = query.slice (start, stop) .all () sẽ trả về [] ở cuối và vòng lặp while sẽ không bao giờ bị phá vỡ
Martin Reguly

4

Sử dụng LIMIT / OFFSET là không tốt, vì bạn cần tìm tất cả các cột {OFFSET} trước đó, vì vậy OFFSET càng lớn - bạn nhận được yêu cầu càng lâu. Đối với tôi, việc sử dụng truy vấn cửa sổ cũng cho kết quả không tốt trên bảng lớn với lượng dữ liệu lớn (bạn đợi kết quả đầu tiên quá lâu, điều đó không tốt trong trường hợp của tôi đối với phản hồi web phân khúc).

Cách tiếp cận tốt nhất được đưa ra tại đây https://stackoverflow.com/a/27169302/450103 . Trong trường hợp của tôi, tôi đã giải quyết vấn đề chỉ bằng cách sử dụng chỉ mục trên trường datetime và tìm nạp truy vấn tiếp theo với datetime> = before_datetime. Thật ngu ngốc, vì tôi đã sử dụng chỉ mục đó trong các trường hợp khác nhau trước đây, nhưng nghĩ rằng để tìm nạp tất cả truy vấn cửa sổ dữ liệu sẽ tốt hơn. Trong trường hợp của tôi, tôi đã sai.


3

AFAIK, biến thể đầu tiên vẫn lấy tất cả các bộ giá trị từ bảng (với một truy vấn SQL) nhưng xây dựng bản trình bày ORM cho từng thực thể khi lặp lại. Vì vậy, sẽ hiệu quả hơn việc xây dựng một danh sách tất cả các thực thể trước khi lặp lại nhưng bạn vẫn phải tìm nạp tất cả dữ liệu (thô) vào bộ nhớ.

Vì vậy, sử dụng LIMIT trên các bảng lớn nghe có vẻ là một ý kiến ​​hay đối với tôi.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.