2013-01-24 30 views
7

Tôi hiện đang làm việc trên một máy chủ web trong cơn lốc xoáy, nhưng đang gặp vấn đề với các bit mã khác nhau đang cố truy cập cơ sở dữ liệu cùng một lúc.Hiệu quả của việc mở lại cơ sở dữ liệu sqlite sau mỗi truy vấn

tôi đã đơn giản hóa điều này bằng cách đơn giản là có một chức năng truy vấn mà về cơ bản thực hiện điều này (nhưng hơi nâng cao hơn):

def query(command, arguments = []): 
    db = sqlite3.open("models/data.db") 
    cursor = db.cursor() 
    cursor.execute(command, arguments) 
    result = cursor.findall() 
    db.close() 
    return result 

Tôi chỉ tự hỏi như thế nào hiệu quả đó là để mở lại cơ sở dữ liệu sau mỗi lần truy vấn (I sẽ đoán nó là một hoạt động liên tục thời gian rất lớn, hoặc nó sẽ cache những thứ hay cái gì?), và cho dù có một cách tốt hơn để làm điều này.

+1

Vui lòng đăng mã thực, với các chức năng phù hợp, như 'connect' và' fetchall' thay vì 'open' và' findall'. – abarnert

+0

Bạn gặp phải vấn đề gì với "các mã khác nhau"? Nếu bạn đang cố gắng viết nhiều đồng thời, hãy sử dụng một RDBMS thực sự http://www.sqlite.org/faq.html#q5 – msw

Trả lời

12

Tôi đang thêm câu trả lời của riêng mình vì tôi không đồng ý với câu trả lời hiện được chấp nhận. Nó nói rằng hoạt động không an toàn với luồng, nhưng điều này là không đúng - SQLite uses file locking thích hợp với nền tảng hiện tại của nó để đảm bảo rằng tất cả các truy cập tuân thủ ACID.

Trên hệ thống Unix, khóa này sẽ là khóa fcntl() hoặc flock(), đây là khóa cho mỗi tệp. Kết quả là, mã được đăng mà tạo ra một kết nối mới mỗi lần sẽ luôn luôn phân bổ một tập tin mới và do đó khóa riêng của SQLite sẽ ngăn chặn tham nhũng cơ sở dữ liệu. Một hệ quả của việc này là thường là một ý tưởng tồi khi sử dụng SQLite trên một chia sẻ NFS hoặc tương tự, thường thì chúng không cung cấp khóa đặc biệt đáng tin cậy (tuy nhiên nó phụ thuộc vào việc thực hiện NFS của bạn).

Như @abernert đã chỉ ra trong nhận xét, SQLite has had issues with threads, nhưng điều này liên quan đến việc chia sẻ một kết nối duy nhất giữa các chuỗi. Như ông cũng đề cập, điều này có nghĩa nếu bạn sử dụng một hồ bơi ứng dụng rộng, bạn sẽ nhận được lỗi thời gian chạy nếu luồng thứ hai kéo ra một kết nối tái chế từ hồ bơi. Đây cũng là loại lỗi kích thích mà bạn có thể không nhận thấy trong thử nghiệm (tải nhẹ, có lẽ chỉ là một sợi đơn được sử dụng), nhưng có thể dễ dàng gây đau đầu sau này. Đề nghị sau này của Martijn Pieters về một hồ bơi địa phương có thể hoạt động tốt.

Như đã nêu trong SQLite FAQ như các phiên bản 3.3.1 nó thực sự an toàn để vượt qua các kết nối giữa các chủ đề miễn là họ không nắm giữ bất kỳ ổ khóa - đây là một sự nhượng bộ mà tác giả của SQLite thêm mặc dù là chỉ trích việc sử dụng các chủ đề nói chung.Bất kỳ việc thực hiện gộp kết nối hợp lý nào sẽ luôn đảm bảo rằng mọi thứ đã được cam kết hoặc cuộn lại trước khi thay thế kết nối trong nhóm, do đó, thực sự một hồ bơi toàn cầu ứng dụng sẽ có thể an toàn nếu nó không được kiểm tra Python. , mà tôi tin rằng vẫn còn tại chỗ ngay cả khi một phiên bản mới hơn của SQLite được sử dụng. Chắc chắn hệ thống Python 2.7.3 của tôi có mô-đun sqlite3 với sqlite_version_info báo cáo 3.7.9, nhưng nó vẫn ném một số RuntimeError nếu bạn truy cập nó từ nhiều luồng.

Trong mọi trường hợp, trong khi kiểm tra tồn tại thì các kết nối không thể được chia sẻ hiệu quả ngay cả khi thư viện SQLite bên dưới hỗ trợ nó.

Đối với câu hỏi ban đầu của bạn, chắc chắn tạo kết nối mới mỗi lần kém hiệu quả hơn là giữ một nhóm kết nối, nhưng đã được đề cập đến điều này sẽ cần phải là một hồ bơi địa phương. . Chi phí của việc tạo một kết nối mới đến cơ sở dữ liệu về cơ bản là mở tệp và đọc tiêu đề để đảm bảo đó là tệp SQLite hợp lệ. Chi phí thực thi câu lệnh cao hơn vì nó cần đưa ra vẻ ngoài và thực hiện khá nhiều tập tin I/O, do đó phần lớn công việc thực sự bị trì hoãn cho đến khi thực thi câu lệnh và/hoặc commit.

Thật thú vị, tuy nhiên, ít nhất là trên các hệ thống Linux, tôi đã xem mã để thực thi câu lệnh lặp lại các bước đọc tiêu đề tệp - kết quả là việc mở kết nối mới sẽ không tệ kể từ khi đọc ban đầu khi mở kết nối sẽ kéo tiêu đề vào bộ đệm hệ thống tập tin của hệ thống. Vì vậy, nó nắm xuống phần trên của việc mở một filehandle duy nhất.

Tôi cũng nên thêm rằng nếu bạn đang mong đợi mã của bạn để mở rộng đến đồng thời cao thì SQLite có thể là một lựa chọn không tốt. Như their own website points out nó không thực sự thích hợp cho đồng thời cao như các hit hiệu suất của việc phải ép tất cả truy cập thông qua một khóa toàn cầu duy nhất bắt đầu cắn như số lượng các chủ đề đồng thời tăng lên. Sẽ ổn nếu bạn đang sử dụng các chủ đề để thuận tiện, nhưng nếu bạn thực sự mong đợi một mức độ đồng thời cao thì tôi sẽ tránh SQLite.

Tóm lại, tôi không nghĩ rằng cách tiếp cận của bạn mở mỗi lần thực sự là tất cả những điều xấu. Có thể một hồ bơi thread-local cải thiện hiệu suất? Chắc là đúng. Liệu hiệu suất này có thể nhận thấy được không? Theo tôi, không trừ khi bạn nhìn thấy tỷ lệ kết nối khá cao, và tại thời điểm đó bạn sẽ có rất nhiều chủ đề vì vậy bạn có thể muốn di chuyển ra khỏi SQL anyway anyway bởi vì nó không xử lý đồng thời terribly tốt. Nếu bạn quyết định sử dụng, hãy đảm bảo rằng nó xóa kết nối trước khi trả lại hồ bơi - SQLAlchemy có một số chức năng connection pooling mà bạn có thể thấy hữu ích ngay cả khi bạn không muốn tất cả các lớp ORM ở trên cùng.

EDIT

Như khá hợp lý chỉ ra tôi nên đính kèm timings thực. Đây là từ một VPS được hỗ trợ khá thấp:

>>> timeit.timeit("cur = conn.cursor(); cur.execute('UPDATE foo SET name=\"x\" 
    WHERE id=3'); conn.commit()", setup="import sqlite3; 
    conn = sqlite3.connect('./testdb')", number=100000) 
5.733098030090332 
>>> timeit.timeit("conn = sqlite3.connect('./testdb'); cur = conn.cursor(); 
    cur.execute('UPDATE foo SET name=\"x\" WHERE id=3'); conn.commit()", 
    setup="import sqlite3", number=100000) 
16.518677949905396 

Bạn có thể thấy hệ số chênh lệch khoảng 3x, không đáng kể. Tuy nhiên, thời gian tuyệt đối vẫn còn dưới một phần nghìn giây, do đó, trừ khi bạn thực hiện nhiều truy vấn cho mỗi yêu cầu thì có thể có những nơi khác để tối ưu hóa trước tiên. Nếu bạn thực hiện nhiều truy vấn, thỏa hiệp hợp lý có thể là kết nối mới theo yêu cầu (nhưng không có sự phức tạp của một nhóm, chỉ cần kết nối lại mỗi lần).

Để đọc (tức là SELECT) thì chi phí liên quan của việc kết nối mỗi lần sẽ cao hơn, nhưng chi phí tuyệt đối trong đồng hồ treo tường phải nhất quán.

Như đã được thảo luận ở đâu đó về câu hỏi này, bạn nên thử nghiệm với các truy vấn thực, tôi chỉ muốn ghi lại những gì tôi đã làm để đi đến kết luận của mình.

+0

Tôi đồng ý với hầu hết điều này, nhưng ... thay vì đoán về hiệu suất, bạn thực sự có thể kiểm tra nó. Và, như bạn có thể thấy từ câu trả lời của tôi, việc mở một kết nối mất khoảng 4-8x miễn là một truy vấn đơn giản đối với tôi và hơn 10 lần cho OP, điều này có nghĩa là bạn đoán rằng việc mở một kết nối mới sẽ không để được tất cả những gì xấu "là sai. Nó có thể dễ dàng là trường hợp các truy vấn thực tế của OP chậm hơn rất nhiều so với các truy vấn đơn giản của tôi mà nó hóa ra không thành vấn đề, nhưng bạn không thể cho rằng một ưu tiên. – abarnert

+0

Tôi nghĩ rằng nếu bạn vượt qua 'check_same_thread = False', nó cho phép bạn chia sẻ một chuỗi kết nối chéo.Câu hỏi của tôi là nếu bạn làm điều đó bạn vẫn cần phải cung cấp đồng bộ hóa của riêng bạn hoặc sẽ sqllite xử lý bằng cách sử dụng một sợi chéo kết nối cho bạn? –

+0

Các phiên bản gần đây (3.3.1 và mới hơn) của SQLite3 cho phép các kết nối được sử dụng trong nhiều luồng và tham số 'check_same_thread' tồn tại để hỗ trợ điều này theo cách không phá vỡ bất kỳ mã kế thừa nào dựa vào hành vi cũ (hoặc là trên một nền tảng có phiên bản SQLite cũ hơn được liên kết). Tuy nhiên, bạn cần đảm bảo rằng không có bất kỳ khóa tệp SQLite nổi bật nào - nghĩa là tất cả các câu lệnh đã được hoàn tất. Xem [tại đây] (http://www.sqlite.org/faq.html#q6) để biết chi tiết. Đây là một giới hạn SQLite, không phải là mô-đun Python. – Cartroo

0

Nó không hiệu quả và không an toàn để khởi động.

Thay vào đó hãy sử dụng thư viện kết nối phù hợp. sqlalchemy cung cấp tính năng tổng hợp và nhiều thứ khác, hoặc tìm cho mình một hồ bơi có trọng lượng nhẹ hơn cho sqlite.

+0

Chính xác thì ý của bạn là gì không phải là an toàn luồng? Điều này không có nghĩa là chỉ có một thứ duy nhất có thể truy cập cơ sở dữ liệu cùng một lúc? – matts

+0

@matts: Ah, khi nó quay ra, cơn lốc xoáy không sử dụng đề tài (một giả định bị loại ra). Nếu bạn đang sử dụng một ứng dụng đa luồng, việc mở cơ sở dữ liệu sqlite từ nhiều luồng như vậy sẽ không hoạt động. –

+0

Phải. Vì vậy, bằng thread-an toàn bạn có nghĩa là các lỗi chucks như "cơ sở dữ liệu bị khóa"? – matts

1

Nếu bạn muốn biết làm thế nào không hiệu quả một cái gì đó, viết một bài kiểm tra và xem cho chính mình.

Khi tôi sửa các lỗi để làm ví dụ của bạn làm việc ngay từ đầu, và viết mã để tạo ra một trường hợp thử nghiệm để chạy nó, tìm ra cách để thời gian với timeit là tầm thường như thường lệ.

Xem http://pastebin.com/rd39vkVa

Vì vậy, điều gì sẽ xảy ra khi bạn chạy?

$ python2.7 sqlite_test.py 10000 
reopen: 2.02089715004 
reuse: 0.278793811798 
$ python3.3 sqlite_test.py 10000 
reopen: 1.8329595914110541 
reuse: 0.2124928394332528 
$ pypy sqlite_test.py 10000 
reopen: 3.87628388405 
reuse: 0.760829925537 

Vì vậy, mở cơ sở dữ liệu mất khoảng 4 đến 8 lần miễn là chạy truy vấn chết đơn giản đối với bảng trống gần không trả lại gì. Có trường hợp xấu nhất của bạn.

+0

'$ python3.2 test.py 10000' ' reopen: 0.8462200164794922' 'tái sử dụng: 0.07594895362854004' Dường như, vì hầu hết các truy vấn của tôi sẽ lớn hơn rất nhiều, nó sẽ không tạo nên sự khác biệt đáng chú ý , và cho rằng tôi đang tự mình thực hiện mọi thứ, nó sẽ là một nỗi đau cho việc đạt được rất ít. Ngoài ra, xin lỗi, nhưng tôi không biết làm thế nào để đặt dòng mới trong ý kiến ​​ – matts

+0

@matts: Đáng buồn thay, theo như tôi biết, bạn không thể đặt dòng mới trong ý kiến. (Bạn có thể dán chúng vào, nhưng chúng được biến thành không gian.) Dù sao, tôi muốn đề nghị bạn thử nghiệm với các mẫu truy vấn thực tế cho trường hợp sử dụng của bạn trước khi quyết định. Nếu đó là 11x đối với truy vấn tối thiểu, nó vẫn có thể là 2x đối với truy vấn đáng kể, điều này chắc chắn vẫn đáng chú ý ... hoặc có thể là 1.001x, trong trường hợp đó bạn có thể bỏ qua nó. Nhưng rất khó đoán trước. – abarnert

0

Tại sao không chỉ kết nối lại sau mỗi giây. Trong dịch vụ lookahead/cơ sở dữ liệu ajax của tôi mà là 30-40 dòng chai tôi kết nối mỗi giờ để có được những thông tin cập nhật, có cơ sở dữ liệu tốt hơn phù hợp nếu bạn cần phải làm việc trên dữ liệu trực tiếp:

t0 = time.time() 
con = None 
connect_interval_in_sec = 3600 

def myconnect(dbfile=<path to dbfile>): 
    try: 
     mycon = sqlite3.connect(dbfile) 
     cur = mycon.cursor() 
     cur.execute('SELECT SQLITE_VERSION()') 
     data = cur.fetchone() 
    except sqlite3.Error as e: 
     print("Error:{}".format(e.args[0])) 
     sys.exit(1) 
    return mycon 

Và trong vòng lặp chính :

if con is None or time.time()-t0 > connect_interval_in_sec: 
    con = myconnect() 
    t0 = time.time() 
<do your query stuff on con> 
Các vấn đề liên quan