SQLite
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> SQLite

Cạm bẫy và bẫy SQLite

SQLite là một cơ sở dữ liệu quan hệ, phổ biến mà bạn nhúng vào ứng dụng của mình. Tuy nhiên, có rất nhiều cạm bẫy và cạm bẫy bạn nên tránh. Bài viết này thảo luận về một số cạm bẫy (và cách tránh chúng), chẳng hạn như việc sử dụng ORM, cách lấy lại dung lượng đĩa, lưu ý đến số lượng biến truy vấn tối đa, kiểu dữ liệu cột và cách xử lý số nguyên lớn.

Giới thiệu

SQLite là hệ thống cơ sở dữ liệu quan hệ (DB) phổ biến . Nó có bộ tính năng rất giống với những người anh em lớn hơn của nó, chẳng hạn như MySQL , là các hệ thống dựa trên máy khách / máy chủ. Tuy nhiên, SQLite là một nhúng cơ sở dữ liệu . Nó có thể được đưa vào chương trình của bạn dưới dạng thư viện tĩnh (hoặc động). Điều này đơn giản hóa việc triển khai , bởi vì không có quy trình máy chủ riêng biệt nào là cần thiết. Thư viện ràng buộc và trình bao bọc cho phép bạn truy cập SQLite bằng hầu hết các ngôn ngữ lập trình .

Tôi đã làm việc rộng rãi với SQLite trong khi phát triển BSync như một phần của luận án Tiến sĩ của tôi. Bài viết này là danh sách (ngẫu nhiên) các cạm bẫy và cạm bẫy tôi đã vấp phải trong quá trình phát triển . Tôi hy vọng rằng bạn sẽ thấy chúng hữu ích và tránh mắc phải những sai lầm như tôi đã từng làm.

Cạm bẫy và cạm bẫy

Sử dụng thư viện ORM một cách thận trọng

Các thư viện Ánh xạ quan hệ đối tượng (ORM) tóm tắt các chi tiết từ các công cụ cơ sở dữ liệu cụ thể và cú pháp của chúng (chẳng hạn như các câu lệnh SQL cụ thể) thành một API hướng đối tượng cấp cao. Có rất nhiều thư viện của bên thứ ba trên mạng (xem Wikipedia). Thư viện ORM có một số ưu điểm:

  • Chúng tiết kiệm thời gian trong quá trình phát triển , vì chúng nhanh chóng ánh xạ mã / lớp của bạn với cấu trúc DB,
  • Chúng thường đa nền tảng , tức là cho phép thay thế công nghệ DB cụ thể (ví dụ:SQLite với MySQL),
  • Họ cung cấp mã trình trợ giúp để di chuyển giản đồ .

Tuy nhiên, chúng cũng có một số nhược điểm nghiêm trọng bạn nên biết về:

  • Họ làm việc với cơ sở dữ liệu xuất hiện dễ dàng . Tuy nhiên, trên thực tế, động cơ DB có những chi tiết phức tạp mà bạn chỉ cần biết . Một khi xảy ra sự cố, ví dụ:khi thư viện ORM đưa ra các ngoại lệ mà bạn không hiểu hoặc khi hiệu suất thời gian chạy suy giảm, thời gian phát triển bạn đã tiết kiệm được bằng cách sử dụng ORM sẽ nhanh chóng bị tiêu hao bởi những nỗ lực cần thiết để gỡ lỗi sự cố . Ví dụ:nếu bạn không biết chỉ số nào là, bạn sẽ gặp khó khăn khi khắc phục sự cố tắc nghẽn hiệu suất do ORM gây ra, khi ORM không tự động tạo tất cả các chỉ số bắt buộc. Về bản chất:không có bữa trưa nào miễn phí.
  • Do sự trừu tượng của nhà cung cấp DB cụ thể, chức năng dành riêng cho nhà cung cấp rất khó truy cập, hoàn toàn không thể truy cập được .
  • một số chi phí tính toán so với việc viết và thực thi các truy vấn SQL trực tiếp. Tuy nhiên, tôi cho rằng vấn đề này là đáng bàn trong thực tế, vì thông thường bạn sẽ giảm hiệu suất khi chuyển sang cấp độ trừu tượng cao hơn.

Cuối cùng, việc sử dụng thư viện ORM là vấn đề của sở thích cá nhân. Nếu bạn làm vậy, chỉ cần chuẩn bị rằng bạn sẽ phải tìm hiểu về các vấn đề của cơ sở dữ liệu quan hệ (và các cảnh báo dành riêng cho nhà cung cấp), khi xảy ra tắc nghẽn hiệu suất hoặc hành vi không mong muốn.

Bao gồm bảng di chuyển từ đầu

Nếu bạn không không bằng cách sử dụng thư viện ORM, bạn sẽ phải thực hiện quá trình di chuyển giản đồ của DB . Điều này liên quan đến việc viết mã di chuyển để thay đổi lược đồ bảng của bạn và chuyển đổi dữ liệu được lưu trữ theo một cách nào đó. Tôi khuyên bạn nên tạo một bảng có tên là “di chuyển” hoặc “phiên bản”, với một hàng và cột duy nhất, chỉ lưu trữ phiên bản giản đồ, ví dụ:bằng cách sử dụng một số nguyên tăng đơn điệu. Điều này cho phép chức năng di chuyển của bạn phát hiện những di chuyển nào vẫn cần được áp dụng. Bất cứ khi nào bước di chuyển được hoàn tất thành công, mã công cụ di chuyển của bạn sẽ tăng bộ đếm này qua UPDATE Câu lệnh SQL.

Cột rowid được tạo tự động

Bất cứ khi nào bạn tạo bảng, SQLite sẽ tự động tạo INTEGER cột có tên rowid dành cho bạn - trừ khi bạn cung cấp WITHOUT ROWID điều khoản (nhưng rất có thể bạn không biết về điều khoản này). rowid hàng là một cột khóa chính. Nếu bạn cũng tự chỉ định cột khóa chính như vậy (ví dụ:sử dụng cú pháp some_column INTEGER PRIMARY KEY ) cột này sẽ chỉ là một bí danh cho rowid . Xem ở đây để biết thêm thông tin, mô tả điều tương tự bằng những từ khá khó hiểu. Lưu ý rằng bảng SELECT * FROM table tuyên bố sẽ không bao gồm rowid theo mặc định - bạn cần yêu cầu rowid cột một cách rõ ràng.

Xác minh rằng PRAGMA s thực sự hoạt động

Trong số những thứ khác, PRAGMA các câu lệnh được sử dụng để định cấu hình cài đặt cơ sở dữ liệu hoặc để gọi các chức năng khác nhau (tài liệu chính thức). Tuy nhiên, có những tác dụng phụ không có tài liệu trong đó đôi khi việc đặt một biến thực sự không có tác dụng . Nói cách khác, nó không hoạt động và không hoạt động một cách âm thầm.

Ví dụ:nếu bạn đưa ra các câu lệnh sau theo thứ tự đã cho, thì cuối cùng tuyên bố sẽ không có bất kỳ ảnh hưởng nào. Biến auto_vacuum vẫn có giá trị 0 (NONE ), không có lý do chính đáng.

PRAGMA journal_mode = WAL
PRAGMA synchronous = NORMAL
PRAGMA auto_vacuum = INCREMENTAL
Code language: SQL (Structured Query Language) (sql)

Bạn có thể đọc giá trị của một biến bằng cách thực thi PRAGMA variableName và bỏ qua dấu bằng và giá trị.

Để sửa ví dụ trên, hãy sử dụng một thứ tự khác. Sử dụng thứ tự hàng 3, 1, 2 sẽ hoạt động như mong đợi.

Bạn thậm chí có thể muốn đưa các séc như vậy vào quá trình sản xuất của mình mã, vì những tác dụng phụ này có thể phụ thuộc vào phiên bản SQLite cụ thể và cách nó được xây dựng. Thư viện được sử dụng trong quá trình sản xuất có thể khác với thư viện bạn đã sử dụng trong quá trình phát triển.

Yêu cầu dung lượng đĩa cho cơ sở dữ liệu lớn

Theo mặc định, kích thước của tệp cơ sở dữ liệu SQLite là tăng trưởng đơn điệu . Việc xóa các hàng chỉ đánh dấu các trang cụ thể là miễn phí , để chúng có thể được sử dụng để INSERT dữ liệu trong tương lai. Để thực sự lấy lại dung lượng đĩa và để tăng tốc hiệu suất, có hai tùy chọn:

  1. Thực thi VACUUM tuyên bố . Tuy nhiên, điều này có một số tác dụng phụ:
    • Nó khóa toàn bộ DB. Không có hoạt động đồng thời nào có thể diễn ra trong VACUUM hoạt động.
    • Mất nhiều thời gian (đối với cơ sở dữ liệu lớn hơn), vì nó tái tạo trong nội bộ DB trong một tệp tạm thời, riêng biệt và cuối cùng xóa cơ sở dữ liệu gốc, thay thế nó bằng tệp tạm thời đó.
    • Tệp tạm thời sử dụng bổ sung dung lượng đĩa trong khi hoạt động đang chạy. Do đó, không phải là ý kiến ​​hay khi chạy VACUUM trong trường hợp bạn sắp hết dung lượng ổ đĩa. Bạn vẫn có thể làm điều đó, nhưng sẽ phải thường xuyên kiểm tra (freeDiskSpace - currentDbFileSize) > 0 .
  2. Sử dụng PRAGMA auto_vacuum = INCREMENTAL khi tạo DB. Đặt thành PRAGMA này đầu tiên sau khi tạo tệp! Điều này cho phép một số tính năng quản lý nội bộ, giúp cơ sở dữ liệu lấy lại không gian bất cứ khi nào bạn gọi PRAGMA incremental_vacuum(N) . Cuộc gọi này xác nhận quyền sở hữu lên đến N các trang. Tài liệu chính thức cung cấp thêm thông tin chi tiết và các giá trị có thể có khác cho auto_vacuum .
    • Lưu ý:bạn có thể xác định dung lượng đĩa trống (tính bằng byte) sẽ đạt được khi gọi PRAGMA incremental_vacuum(N) :nhân giá trị trả về với PRAGMA freelist_count với PRAGMA page_size .

Tùy chọn tốt hơn tùy thuộc vào ngữ cảnh của bạn. Đối với các tệp cơ sở dữ liệu rất lớn, tôi đề xuất tùy chọn 2 , bởi vì tùy chọn 1 sẽ làm phiền người dùng của bạn với vài phút hoặc hàng giờ chờ đợi cơ sở dữ liệu được dọn dẹp. Tùy chọn 1 phù hợp với cơ sở dữ liệu nhỏ hơn . Ưu điểm bổ sung của nó là hiệu suất của DB sẽ cải thiện (điều này không đúng với tùy chọn 2), bởi vì việc giải trí loại bỏ các tác dụng phụ của việc phân mảnh dữ liệu.

Lưu ý đến số lượng biến tối đa trong các truy vấn

Theo mặc định, số lượng biến (“tham số máy chủ”) tối đa mà bạn có thể sử dụng trong truy vấn được mã hóa cứng thành 999 (xem tại đây, phần Số lượng tham số máy chủ lưu trữ tối đa trong một câu lệnh SQL đơn ). Giới hạn này có thể thay đổi, vì đó là thời gian biên dịch tham số, giá trị mặc định mà bạn (hoặc bất kỳ ai khác đã biên dịch SQLite) có thể đã thay đổi.

Đây là một vấn đề trong thực tế, bởi vì không có gì lạ khi ứng dụng của bạn cung cấp một danh sách (lớn tùy ý) cho công cụ DB. Ví dụ:nếu bạn muốn hàng loạt- DELETE (hoặc SELECT ) các hàng dựa trên, chẳng hạn, một danh sách các ID. Một tuyên bố chẳng hạn như

DELETE FROM some_table WHERE rowid IN (?, ?, ?, ?, <999 times "?, ">, ?)Code language: SQL (Structured Query Language) (sql)

sẽ xuất hiện một lỗi và sẽ không hoàn thành.

Để khắc phục sự cố này, hãy xem xét các bước sau:

  • Phân tích danh sách của bạn và chia chúng thành các danh sách nhỏ hơn,
  • Nếu cần phân tách, hãy đảm bảo sử dụng BEGIN TRANSACTIONCOMMIT để mô phỏng tính nguyên tử mà một tuyên bố duy nhất sẽ có .
  • Đảm bảo bạn cũng xem xét các ? các biến bạn có thể sử dụng trong truy vấn của mình không liên quan đến danh sách đến (ví dụ:? các biến được sử dụng trong ORDER BY điều kiện), sao cho tổng số số lượng biến không vượt quá giới hạn.

Một giải pháp thay thế là sử dụng các bảng tạm thời. Ý tưởng là tạo một bảng tạm thời, chèn các biến truy vấn dưới dạng hàng, sau đó sử dụng bảng tạm thời đó trong một truy vấn con, ví dụ:

DROP TABLE IF EXISTS temp.input_data
CREATE TABLE temp.input_data (some_column TEXT UNIQUE)
# Insert input data, running the next query multiple times
INSERT INTO temp.input_data (some_column) VALUES (...)
# The above DELETE statement now changes to this one:
DELETE FROM some_table WHERE rowid IN (SELECT some_column from temp.input_data)Code language: SQL (Structured Query Language) (sql)

Hãy coi chừng mối quan hệ kiểu của SQLite

Các cột SQLite không được nhập đúng và chuyển đổi không nhất thiết xảy ra như bạn mong đợi. Các loại bạn cung cấp chỉ là gợi ý . SQLite thường sẽ lưu trữ dữ liệu của bất kỳ nhập vào bản gốc của nó và chỉ chuyển đổi dữ liệu sang loại cột trong trường hợp chuyển đổi không bị mất dữ liệu. Ví dụ:bạn có thể chỉ cần chèn "hello" chuỗi thành một INTEGER cột. SQLite sẽ không phàn nàn hoặc cảnh báo bạn về kiểu không khớp. Ngược lại, bạn có thể không mong đợi rằng dữ liệu được trả về bởi SELECT tuyên bố của một INTEGER cột luôn là một INTEGER . Các gợi ý kiểu này được gọi là "mối quan hệ kiểu" trong SQLite-speak, xem tại đây. Đảm bảo nghiên cứu kỹ phần này của sổ tay SQLite để hiểu rõ hơn ý nghĩa của các loại cột bạn chỉ định khi tạo bảng mới.

Cẩn thận với các số nguyên lớn

SQLite hỗ trợ đã ký Số nguyên 64 bit , mà nó có thể lưu trữ hoặc thực hiện các phép tính. Nói cách khác, chỉ các số từ -2^63 thành (2^63) - 1 được hỗ trợ, vì cần một bit để biểu diễn dấu hiệu!

Điều đó có nghĩa là nếu bạn muốn làm việc với số lượng lớn hơn, ví dụ:Số nguyên 128 bit (có dấu) hoặc số nguyên 64 bit không dấu, bạn phải chuyển đổi dữ liệu thành văn bản trước khi chèn nó .

Sự kinh hoàng bắt đầu khi bạn bỏ qua điều này và chỉ cần chèn các số lớn hơn (dưới dạng số nguyên). SQLite sẽ không phàn nàn và lưu trữ một làm tròn thay vào đó là số! Ví dụ:nếu bạn chèn 2 ^ 63 (đã nằm ngoài phạm vi được hỗ trợ), thì SELECT giá trị ed sẽ là 9223372036854776000, chứ không phải 2 ^ 63 =9223372036854775808. Tuy nhiên, tùy thuộc vào ngôn ngữ lập trình và thư viện ràng buộc mà bạn sử dụng, hành vi có thể khác nhau! Ví dụ:liên kết sqlite3 của Python kiểm tra các lỗi tràn số nguyên như vậy!

Không sử dụng REPLACE() cho đường dẫn tệp

Hãy tưởng tượng bạn lưu trữ các đường dẫn tệp tương đối hoặc tuyệt đối trong một TEXT cột trong SQLite, ví dụ:để theo dõi các tệp trên hệ thống tệp thực tế. Đây là một ví dụ về ba hàng:

foo/test.txt
foo/bar/
foo/bar/x.y

Giả sử bạn muốn đổi tên thư mục “foo” thành “xyz”. Bạn sẽ sử dụng lệnh SQL nào? Cái này?

REPLACE(path_column, old_path, new_path) Code language: SQL (Structured Query Language) (sql)

Đây là những gì tôi đã làm, cho đến khi những điều kỳ lạ bắt đầu xảy ra. Sự cố với REPLACE() là nó sẽ thay thế tất cả những lần xuất hiện. Nếu có một hàng có đường dẫn “foo / bar / foo /”, thì REPLACE(column_name, 'foo/', 'xyz/') sẽ tàn phá, vì kết quả sẽ không phải là “xyz / bar / foo /”, mà là “xyz / bar / xyz /”.

Một giải pháp tốt hơn là một cái gì đó giống như

UPDATE mytable SET path_column = 'xyz/' || substr(path_column, 4) WHERE path_column GLOB 'foo/*'"Code language: SQL (Structured Query Language) (sql)

4 phản ánh độ dài của đường dẫn cũ (‘foo /’ trong trường hợp này). Lưu ý rằng tôi đã sử dụng GLOB thay vì LIKE chỉ cập nhật những hàng bắt đầu với ‘foo /’.

Kết luận

SQLite là một công cụ cơ sở dữ liệu tuyệt vời, nơi hầu hết các lệnh hoạt động như mong đợi. Tuy nhiên, những nội dung phức tạp cụ thể, như những gì tôi vừa trình bày, vẫn cần sự chú ý của nhà phát triển. Ngoài bài viết này, hãy đảm bảo bạn cũng đọc tài liệu cảnh báo SQLite chính thức.

Bạn có gặp phải những cảnh báo khác trong quá khứ không? Nếu vậy, hãy cho tôi biết trong phần bình luận.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Đăng nhanh về SQLite UPSERT và điều khoản RETURNING mới.

  2. Tại sao chúng ta cần mệnh đề GLOB trong SQLite?

  3. Cách hoạt động của SQLite Max ()

  4. Cách cắt một chuỗi trong SQLite

  5. Tạo câu lệnh INSERT từ kết quả truy vấn SQLite