2 điểm bởi GN⁺ 2023-09-19 | 1 bình luận | Chia sẻ qua WhatsApp
  • Nếu gói OpenDocument Presentation (ODP) trong container SQLite thay vì kho lưu trữ ZIP, có thể thiết kế cách lưu, khởi động và khôi phục tài liệu an toàn hơn và nhanh hơn
  • Hiện nay ODP có cấu trúc gói kho lưu trữ ZIP chứa các tệp XML và hình ảnh, và tệp trình chiếu ví dụ gồm 49 slide được tạo thành từ tổng cộng 78 mục như content.xml, styles.xml, meta.xml, settings.xml cùng các hình ảnh
  • Với cấu trúc dựa trên ZIP, ngay cả thay đổi nhỏ cũng dễ dẫn đến phải ghi lại toàn bộ kho lưu trữ nên khó thực hiện cập nhật gia tăng, kéo theo độ trễ File/Save và tăng lượng ghi lên SSD
  • Khi chuyển sang SQLite, có thể lưu tệp dưới dạng các hàng trong bảng, và xa hơn nữa là tách nội dung cùng phiên bản theo từng slide để chỉ đọc slide đầu tiên hoặc chỉ lưu các slide đã thay đổi
  • Đây không phải là đề xuất chỉ trích hay thay đổi chính OpenDocument, mà là một ví dụ cho thấy SQLite có thể giúp tạo ra lưu nguyên tử, khả năng truy cập, quản lý phiên bản và chức năng khôi phục trong định dạng tệp ứng dụng một cách dễ dàng

Phạm vi và đối tượng của thí nghiệm tư duy

  • Đối tượng là ODP (OpenDocument Presentation), tức định dạng tài liệu trình chiếu trong OpenDocument
  • Mục đích không phải là thay đổi OpenDocument trong thực tế, mà là xem xét cách dùng SQLite làm container trong thiết kế định dạng tệp về sau
  • Hiệu quả kỳ vọng là tài liệu nhỏ hơn, File/Save nhanh hơn, khởi động nhanh hơn, dùng ít bộ nhớ hơn, quản lý phiên bản tài liệu và trải nghiệm người dùng tốt hơn

Cấu trúc tệp ODP hiện nay

  • Tệp ODP là một kho lưu trữ ZIP chứa các tệp XML và tài nguyên hình ảnh
  • Ví dụ, tệp trình chiếu 49 slide liên quan đến SQLite tại SouthEast LinuxFest 2014 có tổng cộng 78 mục trong đầu ra zip -l
    • Bốn tệp XML content.xml, styles.xml, meta.xml, settings.xml định nghĩa bố cục slide, nội dung văn bản và kiểu dáng
    • Tệp trình chiếu lưu 62 hình ảnh thành các tệp riêng, từ ảnh toàn màn hình đến các biểu tượng nhỏ
    • Tệp mimetype chứa một dòng application/vnd.oasis.opendocument.presentation
  • Tệp xử lý văn bản và bảng tính OpenDocument cũng có cấu trúc tương tự, nhưng đối tượng được phân tích ở đây là ODP

Hạn chế của ODP dựa trên ZIP

  • Kho lưu trữ ZIP gần với một cơ sở dữ liệu key/value được tối ưu cho ghi một lần, đọc nhiều lần, và phù hợp với cấu trúc có ít key nhưng giá trị BLOB lớn
  • Vì khó cập nhật từng mục riêng lẻ, khi người dùng chọn File/Save thường toàn bộ kho ZIP sẽ được ghi lại
    • Dù vẫn có thể cập nhật từng mục để tránh làm hỏng toàn bộ tài liệu trong lúc mất điện hay sự cố, việc này đủ khó để trên thực tế hầu như không được dùng
    • Với tệp trình chiếu 50MB, chỉ thay một ký tự cũng có thể dẫn đến phải ghi lại toàn bộ 50MB
  • Thời gian khởi động có thể chậm
    • ODP lưu toàn bộ nội dung slide trong một tệp XML lớn là content.xml
    • LibreOffice phải đọc và phân tích toàn bộ tệp này để hiển thị slide đầu tiên
    • Có vẻ mọi hình ảnh cũng được đọc hết vào bộ nhớ, nên khi nhấp đúp vào tệp, người dùng thấy thanh tiến trình thay vì slide đầu tiên
  • Mức dùng bộ nhớ tăng cao
    • Cấu trúc ZIP dễ dẫn đến cách triển khai là đọc toàn bộ tài liệu vào bộ nhớ khi khởi động, chỉnh sửa trong bộ nhớ rồi ghi lại toàn bộ xuống đĩa khi lưu
    • Một tệp trình chiếu 50MB có thể dùng hơn 200MB RAM
    • Nếu mở nhiều tệp trình chiếu cùng lúc, lại còn dùng trình duyệt và ứng dụng desktop, hiện tượng swapping có thể xảy ra
  • Khôi phục sau sự cố trở nên phiền phức
    • Dòng OpenOffice định kỳ sao lưu tài liệu đang nằm trong bộ nhớ để đề phòng crash
    • Trong lúc sao lưu, ứng dụng có thể đứng vài giây, và sau khi khởi động lại còn phải qua một hộp thoại khôi phục riêng
  • Khả năng truy cập nội dung thấp
    • Có thể trích xuất hình ảnh bằng công cụ ZIP, nhưng rất khó trích xuất hoặc chỉnh sửa văn bản slide bằng các công cụ thông thường
    • Trong tệp ví dụ, content.xml có dòng đầu là khai báo XML, còn dòng thứ hai chứa 211.792 ký tự XML trên đúng một dòng

Cải tiến đầu tiên: thay ZIP bằng SQLite

  • Bước đầu tiên là một cấu trúc đơn giản, thay các mục ZIP bằng các hàng trong bảng SQLite
CREATE TABLE OpenDocTree(
  filename TEXT PRIMARY KEY,
  filesize BIGINT,
  content BLOB
);
  • Ở giai đoạn này, phần còn lại của cấu trúc định dạng tệp không thay đổi
    • Vẫn là cấu trúc “đống tệp”, nhưng mỗi tệp trở thành một hàng trong cơ sở dữ liệu SQLite thay vì một entry ZIP
  • So sánh kích thước tệp SQLite được đóng gói lại bằng tiện ích SQLAR với nội dung giống self2014.odp do NeoOffice tạo ra như sau
    • self2014.odp: 10.514.994 byte
    • self2014.sqlar: 10.464.256 byte
    • zip.odp được nén lại bằng lệnh zip: 10.416.644 byte
  • Tệp SQLite nhỏ hơn khoảng 0,5% so với ODP do NeoOffice tạo ra
    • Tệp ZIP được nén tốt bằng lệnh zip lại nhỏ hơn SQLite thêm khoảng 0,5%
    • Cơ sở dữ liệu SQLite có thể cạnh tranh với kho ZIP về mặt kích thước
  • SQLite cung cấp ghi nguyên tử, nên có thể lưu các thay đổi gia tăng mà không có nguy cơ làm hỏng tài liệu trong lúc crash hay mất điện
    • Hạn chế là vẫn phải ghi lại toàn bộ content.xml
    • Dù vậy, 77 tệp còn lại có thể giữ nguyên, giúp File/Save nhanh hơn và giảm lượng ghi lên SSD

Cải tiến thứ hai: chia nội dung thành các mảnh nhỏ

  • SQLite có thể lưu hiệu quả không chỉ các khối lớn mà cả nhiều mảnh nhỏ, nên có thể tạo bảng nội dung theo từng slide
CREATE TABLE slide(
  pageNumber INTEGER,
  slideContent TEXT
);
CREATE INDEX slide_pgnum ON slide(pageNumber);
  • Khi hiển thị màn hình đầu tiên, ứng dụng chỉ cần đọc slide đầu tiên
SELECT slideContent FROM slide WHERE pageNumber=1;
  • Với cấu trúc này, chỉ nội dung của slide đầu tiên mới cần được lấy ra, phân tích và hiển thị nhanh, nên không cần đọc toàn bộ content.xml khi khởi động
  • Cũng có thêm nhiều lựa chọn triển khai
    • Sau khi hiển thị slide đầu tiên, có thể dùng luồng nền để đọc các trang còn lại
    • Có thể chỉ giữ một slide hiện tại trong bộ nhớ
    • Hoặc chỉ giữ slide hiện tại và slide tiếp theo trong bộ nhớ để chuyển trang nhanh
  • Việc lưu cũng nhanh hơn vì chỉ cần ghi lại các trang đã thay đổi
  • Với các mảnh văn bản ngắn, hiệu quả nén sẽ kém hơn nên kích thước tài liệu có thể tăng
    • Tuy vậy, phần lớn dung lượng tài liệu là hình ảnh, nên suy giảm hiệu quả nén văn bản có thể xem là cái giá nhỏ so với cải thiện trải nghiệm người dùng

Cải tiến thứ ba: quản lý phiên bản

  • Khi lưu slide như các đối tượng riêng lẻ, có thể chứa lịch sử phiên bản ngay trong cùng một tài liệu
CREATE TABLE slide(
  slideId INTEGER PRIMARY KEY,
  derivedFrom INTEGER REFERENCES slide,
  content TEXT
);
CREATE TABLE version(
  versionId INTEGER PRIMARY KEY,
  priorVersion INTEGER REFERENCES version,
  checkinTime DATETIME,
  comment TEXT,
  manifest TEXT
);
  • Mỗi slide có slideId riêng thay vì số trang, còn thứ tự được xác định bằng danh sách slideId lưu trong manifest của bảng version
  • Khi khởi động, ứng dụng trước tiên chọn phiên bản cần hiển thị, thường là lấy phiên bản mới nhất
SELECT manifest, versionId FROM version ORDER BY versionId DESC LIMIT 1;
  • Cũng có thể dùng truy vấn lấy phiên bản mới nhất theo checkinTime
SELECT manifest, versionId, max(checkinTime) FROM version;
  • Trong SQLite, truy vấn max(checkinTime) ở trên trả về kết quả được xác định, nhưng ở nhiều cơ sở dữ liệu SQL khác có thể trả về kết quả không xác định hoặc báo lỗi
  • Khi người dùng thực hiện File/Save, chỉ các slide đã sửa mới cần được thêm thành hàng mới trong bảng slide, rồi tạo một hàng version mới với manifest đã cập nhật
  • Bảng version lưu thời điểm check-in, bình luận của người dùng và phiên bản cha để giữ lại lịch sử thay đổi
  • Cũng có thể lưu nhiều bộ slide trình chiếu trong cùng một tài liệu
  • Thay vì tệp sao lưu riêng, có thể dùng một phiên bản pending đặc biệt để ghi lại thường xuyên và âm thầm các thay đổi chưa lưu
    • Vì chỉ ghi phần thay đổi chứ không phải toàn bộ tài liệu, lượng ghi có thể là vài KB thay vì vài MB
    • Thời gian lưu có thể là mili giây thay vì vài giây
    • Ngay cả sau khi crash và khởi động lại, phần lớn hoặc gần như toàn bộ công việc của người dùng vẫn có thể được giữ lại
    • Nếu muốn bỏ các thay đổi chưa lưu, người dùng chỉ cần quay lại phiên bản trước đó

Những tính năng còn có thể làm thêm với định dạng tệp SQLite

  • Chỉ với ba bảng, container SQLite đã có thể bổ sung các tính năng quan trọng cho định dạng tệp ứng dụng
  • Ngoài ra còn có thể tận dụng schema, index, trigger, view và constraint để tăng hiệu năng, tính tiện dụng và tính nhất quán
  • Một số ý tưởng mở rộng gồm
    • Lưu ngăn xếp undo/redo tự động trong bảng cơ sở dữ liệu để có thể hoàn tác cả các phiên chỉnh sửa trước
    • Thêm tìm kiếm toàn văn cho một bộ slide hoặc nhiều bộ slide
    • Tách settings.xml thành các bảng SQL để ứng dụng khác có thể xem và chỉnh sửa dễ hơn
    • Tách ghi chú người thuyết trình của từng slide sang bảng riêng để ứng dụng bên thứ ba hoặc script truy cập dễ dàng
    • Hỗ trợ cấu trúc trình bày vượt ra ngoài thứ tự slide tuyến tính đơn giản, với nhiều nhánh hoặc đường vòng tùy phản ứng của khán giả

Sự e dè thường gặp với SQLite và phản biện

  • Vì kinh nghiệm với cơ sở dữ liệu SQL cấp doanh nghiệp, nhiều người có thể cảm thấy e dè khi dùng SQLite làm định dạng tệp ứng dụng
  • Nhiều cơ sở dữ liệu doanh nghiệp khuyên không nên đưa chuỗi lớn hay BLOB vào cơ sở dữ liệu mà nên lưu thành tệp riêng, nhưng SQLite thì khác
    • Bất kỳ cột nào của SQLite cũng có thể lưu chuỗi hoặc BLOB tới khoảng 1GB
    • Với chuỗi và BLOB dưới 100KB, hiệu năng I/O còn tốt hơn so với tệp riêng
  • Quan niệm rằng mọi schema SQL đều phải theo chuẩn hóa mức ba (3NF) và chỉ lưu kiểu nguyên thủy nhỏ cũng có thể trở thành rào cản
    • Lý thuyết quan hệ rất quan trọng, nhưng trong định dạng tệp thực tế, lưu thông tin phức tạp như XML hay JSON trong trường văn bản cũng là một lựa chọn chấp nhận được

SQLite như một định dạng tệp ứng dụng

  • Tệp cơ sở dữ liệu SQLite có kích thước gần như tương đương kho ZIP chứa cùng thông tin, và trong một số trường hợp còn có thể nhỏ hơn
  • Nhờ cập nhật nguyên tử, các thay đổi nhỏ có thể được ghi an toàn vào tài liệu để giảm I/O đĩa và cải thiện hiệu năng File/Save
  • Ứng dụng có thể chỉ đọc phần nội dung cần cho màn hình đầu tiên để rút ngắn thời gian khởi động
  • Chỉ cần giữ nội dung liên quan đến phần đang hiển thị trong bộ nhớ, còn lại để trên đĩa, nên mức dùng bộ nhớ có thể giảm mạnh
  • Schema SQL có thể biểu diễn thông tin trực tiếp và cô đọng hơn so với cấu trúc key/value kiểu ZIP
    • Khả năng truy cập từ ứng dụng bên thứ ba và script sẽ tốt hơn
    • Việc triển khai các tính năng cao cấp như quản lý phiên bản tài liệu tích hợp và khôi phục công việc sau crash sẽ dễ hơn
  • OpenDocument vốn đã là một định dạng được thiết lập tốt và thiết kế tốt; vì SQLite xuất hiện sau OpenDocument nên đây không phải là lời chỉ trích lựa chọn trước đó
  • Tài liệu Application File Format đưa ra thêm nhiều ý tưởng về việc dùng SQLite làm định dạng tệp ứng dụng

1 bình luận

 
GN⁺ 2023-09-19
Ý kiến trên Hacker News
  • Tôi đang làm một ứng dụng dùng SQLite làm định dạng tệp
    Vì muốn giữ luồng thông thường là tệp chỉ thay đổi khi người dùng chỉnh sửa tài liệu rồi lưu, nên khi mở tệp tôi sao chép nó vào cơ sở dữ liệu :memory:: https://www.sqlite.org/inmemorydb.html
    Người dùng thao tác tùy ý, còn ứng dụng phản ánh trực tiếp vào định dạng cơ sở dữ liệu mà không cần mô hình tài liệu riêng. Khi lưu thì xử lý bằng cách ghi lại ra tệp cơ sở dữ liệu bằng VACUUM: https://www.sqlite.org/lang_vacuum.html
    Cách này hoạt động tốt với các tệp có kích thước vừa phải, và trong ứng dụng của tôi thì luôn nằm trong phạm vi đó

    • Tôi không hiểu vì sao lại dùng thêm một cơ sở dữ liệu tạm thời/volatile phụ trợ. Nếu người dùng đang chỉnh sửa tệp thì có lẽ cũng chưa tới 1 lần ghi mỗi giây, nên lợi ích hiệu năng không lớn
      Thà tự động lưu trực tiếp vào cơ sở dữ liệu và bỏ nút lưu còn hơn. Cách này chịu va chạm tốt, chỉ có một cơ sở dữ liệu nên code và bug ít hơn, còn thao tác ghi của SQLite thì hoặc thành công hoặc thất bại chứ không có trạng thái lưng chừng. Ngược lại, như tài liệu được trích dẫn, VACUUM INTO có thể để lại cơ sở dữ liệu đầu ra không hoàn chỉnh hoặc bị hỏng nếu thoát bất ngờ hay mất điện
      Nếu dùng SQLite đúng theo cách vốn được thiết kế, bạn sẽ không phải lo phần này trong suốt vòng đời của SQLite
    • Việc hoạt động như ứng dụng thông thường có nghĩa là nếu ứng dụng chết hoặc mất điện thì sẽ mất dữ liệu chưa lưu
      Tốt hơn nhiều là sau mỗi thao tác, lưu vào một vị trí tạm thời, chẳng hạn theo thư mục XDG là ~/.local/share/application/yourapp, rồi khi người dùng bấm lưu thì sao chép tệp tới vị trí họ muốn. Sau khi mất điện, mở lại ứng dụng vẫn có thể khôi phục gần như đúng chỗ cũ, nhiều nhất chỉ mất vài giây cuối
    • Đơn giản hơn nữa, có thể chỉ cần chuyển sang chế độ WAL khi mở cơ sở dữ liệu và tắt checkpoint tự động: https://www.sqlite.org/pragma.html#pragma_wal_autocheckpoint
      Khi người dùng lưu, thực hiện checkpoint để gộp nội dung WAL vào cơ sở dữ liệu chính là được
    • Theo tài liệu, VACUUM sao chép nội dung sang một tệp cơ sở dữ liệu tạm thời rồi ghi đè lên bản gốc; khi ghi đè, nó dùng rollback journal hoặc WAL như một transaction thông thường. Vì vậy cần dung lượng trống tối đa khoảng gấp đôi kích thước bản gốc
      VACUUM INTO dùng tệp được chỉ định trong INTO thay cho cơ sở dữ liệu tạm thời, và bỏ qua bước sao chép ngược lên bản gốc. Điều quan trọng là thứ thực sự đang dùng là VACUUM chịu được mất điện, hay VACUUM INTO có vẻ dễ bị lỗi nếu mất điện trong lúc ghi và có thể làm hỏng nếu đó là tên tệp hiện có
    • Tôi từng dùng cách tương tự: chạy cơ sở dữ liệu trong bộ nhớ như cache, lưu định kỳ xuống đĩa và dùng backup API: https://www.sqlite.org/backup.html
  • Vấn đề của SQLite là nó không phải định dạng tệp được chuẩn hóa
    Nó được tài liệu hóa tốt và được hiểu rộng rãi, nhưng ISO không định nghĩa chi tiết cách diễn giải tệp SQLite. Các triển khai thay thế cũng vậy
    Zip và XML có bề mặt API nhỏ hơn SQLite rất nhiều. API của SQLite không chỉ là vài hàm C mà là cả ngôn ngữ SQL; việc triển khai parser SQL, bộ tối ưu truy vấn, trình biên dịch, máy ảo bytecode, công cụ tìm kiếm toàn văn, v.v. mà không làm hỏng dữ liệu là việc lớn hơn parser XML rất nhiều
    Nếu là một ứng dụng đóng, chuyên biệt theo miền, nơi khả năng tương tác hoặc chuẩn hóa ISO không quan trọng, thì SQLite là một định dạng tệp tốt, nhưng tôi hiểu rằng với OpenOffice thì những lo ngại đó thực sự đã tồn tại

    • Không rõ vấn đề này đang nói tới điều gì. Định dạng tệp SQLite thuộc public domain, được tài liệu hóa tốt và có parser cho nhiều ngôn ngữ
      Thư viện C của SQLite cũng thuộc public domain nên mã nguồn hoàn toàn mở, xử lý định dạng tệp, và mức độ tài liệu hóa còn cao hơn phần lớn tiêu chuẩn ISO. Hầu như ngôn ngữ phổ biến nào cũng có binding
      Nếu vấn đề là một định dạng OpenDocument nào đó sẽ được lưu bên trong tệp SQLite còn chưa được tạo ra và tài liệu hóa, thì đó là chuyện khác. Tiêu chuẩn ISO thì tốt, nhưng nếu phải chờ ISO định nghĩa định dạng tệp thì sẽ có quá ít thứ có thể dùng được
    • Để trở thành định dạng tệp tiêu chuẩn, không cần phải triển khai toàn bộ parser SQL, bộ tối ưu truy vấn, trình biên dịch, máy ảo bytecode và công cụ tìm kiếm toàn văn
      Cũng giống như không cần triển khai mọi tính năng bảng tính để đọc bảng tính LibreOffice. Thứ cần có là khả năng tái dựng các bảng; sau đó, thông tin mong muốn có thể được lấy bằng cách duyệt qua bằng mã mệnh lệnh viết bằng ngôn ngữ bạn chọn
    • Điều này cũng không thành vấn đề với Thư viện Quốc hội Hoa Kỳ. Thư viện Quốc hội đã xác định SQLite là định dạng lưu trữ được khuyến nghị cho dataset, cùng với CSV, XML và JSON
    • Có vẻ đang trộn lẫn định dạng tệp với cách sử dụng nó. Ứng dụng dùng định dạng tệp SQLite chỉ cần dùng thư viện SQLite như một phần của ứng dụng
      Việc triển khai lại thư viện đó sẽ là chuyện lớn, nhưng cũng cùng loại với việc triển khai lại code dùng định dạng tệp OpenDocument. Bản thân định dạng tệp thì khá đơn giản
    • Không nhất thiết phải có tiêu chuẩn. Mọi tương tác giữa ứng dụng và tài liệu đều diễn ra qua SQL, và SQL ít nhất đã được chuẩn hóa ở các phần quan trọng
      Nếu lo về tính tương thích, có thể làm cho tài liệu cũng truy cập được bằng cơ sở dữ liệu khác như MySQL
  • Tôi từng nghĩ nếu Audacity áp dụng SQLite thì chức năng lưu tệp sẽ cải thiện đáng kể, nhưng thực tế lại có nhiều cạm bẫy
    Trên Linux, nếu lưu thành tệp mới vào một phân vùng NTFS do root sở hữu nhưng cho phép mọi người ghi, được mount bằng /etc/fstab, thì thao tác thất bại vì các lý do kiểu lỗi quyền; còn lưu vào tệp đã có thì hoạt động bình thường
    Ngay khi chỉnh sửa dự án, tệp trên đĩa đã bị sửa đổi, nên nếu đưa dự án Audacity vào Git như một khối nhị phân thì sẽ sinh ra các khác biệt Git không cần thiết. Ngay cả khi lưu, dữ liệu cũ hoặc đã xóa vẫn còn trong tệp SQLite cho đến khi đóng cửa sổ dự án, nên nếu không đóng cửa sổ trước khi commit thì chúng có thể lọt vào kho. Tôi nhớ trước đây phải tự VACUUM tệp .aup3, còn bây giờ chỉ cần đóng cửa sổ là đủ. Có cảm giác giống Fast Save của Word 2003

    • Nếu Audacity bị crash hoặc thoát bất thường thì hoàn toàn không có bước dọn dẹp nào, rất phiền. Trước đây, trong quá trình khôi phục, nó sẽ báo có các khối mồ côi và cho chọn giữ lại hay xóa đi
      Khi một dự án lẽ ra chỉ vài trăm MB phình lên thành vài GB và cần tiết kiệm dung lượng đĩa, với các tác vụ một track đơn giản thì Mix and Render là giải pháp. Nó không thay đổi âm thanh, nhưng có thể dọn rác khi lưu và thoát
      Đây rõ ràng không phải vấn đề của bản thân SQLite mà là vấn đề ở tầng ứng dụng. Tôi nhớ Audacity 2 dường như có khái niệm không gian làm việc tạm thời, còn Audacity 3 có vẻ dùng chính tệp .aup3 làm không gian làm việc
      Tôi đã xem qua định dạng Audacity 3 và thấy rất khó hiểu: dữ liệu dự án, vốn tương ứng với tệp .aup cũ, được lưu dưới dạng XML trong một bảng chỉ có một hàng, nhưng lại không ghi nguyên văn văn bản mà mã hóa bằng một bộ mã hóa từ điển đơn giản. Điều này làm khả năng tương tác và kiểm tra khó hơn nhiều, cũng gây hại dù rất nhỏ cho hiệu năng, còn dung lượng tiết kiệm được chắc chỉ là sai số làm tròn vài KB trong các tệp âm thanh hàng trăm MB
    • Cần tái hiện đúng chức năng mà người dùng kỳ vọng. Mọi thứ nên được lưu vào tệp tạm, và chỉ ghi đè tệp gốc khi có thao tác lưu rõ ràng
      Xét từ góc nhìn Git, dùng định dạng văn bản dễ diff và merge sẽ có lợi hơn. Tôi không rõ SQLite dump dễ đến mức nào ở khía cạnh này
    • Vợ tôi dùng Audacity cả ngày, và cứ vài ngày lại xuất hiện tệp SQLite bị hỏng. Nó báo lỗi khóa trùng lặp, và tôi không biết cách sửa hay nhập lại trong Audacity
      Nếu quan trọng thì có thể sửa thủ công, nhưng thường chỉ cần bỏ tệp đó đi là lại hoạt động
  • Bài viết hay. Tuy vậy, tôi thích việc OpenDocument là một bó các tệp XML bên trong một kho lưu trữ Zip
    Ngay cả khi không có thư viện nặng hiểu định dạng tài liệu, vẫn có thể tạo các tài liệu kiểu bảng tính khá dễ dàng
    Đôi khi người dùng dịch vụ web muốn dùng dữ liệu được xuất ra dưới dạng các hàng bảng trong nhiều công cụ khác nhau. UTF-8 CSV thì mở, theo thông lệ và dùng được, nhưng ai từng cung cấp CSV cho người dùng cuối đều biết nỗi đau khi các ứng dụng bảng tính gặp trục trặc với nó
    Tôi đã lưu một bảng tính mẫu thành ODS của OpenDocument và XLSX của OOXML, con quái vật XML của Microsoft, rồi chỉ nắm các điểm cơ bản của định dạng XML. Tôi rút gọn kho Zip chỉ còn các thành phần thiết yếu, đánh dấu vị trí sẽ đặt nội dung, và tạo tệp bảng tính mới khi có yêu cầu. Giờ có thể xuất cùng dữ liệu đó dưới dạng CSV, ODS, XLSX, JSON
    SQLite cũng có thể làm được, nhưng sẽ phức tạp hơn một chút và tốc độ phát triển chậm hơn. Việc có thể tạo tài liệu mẫu bằng bộ ứng dụng văn phòng rồi đào vào XML của tệp lưu là một tính năng tuy ngách nhưng tốt
    Đặc biệt Excel ở các locale như nl_NL là vấn đề, vì nó hành xử như thể dấu phân cách cột trong tệp CSV được hard-code là dấu chấm phẩy. Lý do là Microsoft đã khét tiếng quyết định rằng người Hà Lan không dùng dấu phẩy trong các tệp comma separated values

    • Hành vi đó không hoàn toàn được hard-code, mà phụ thuộc vào giá trị localeconv()->decimal_point. Nếu giá trị là ,, Excel sẽ dùng dấu chấm phẩy trong cả tệp CSV lẫn ngôn ngữ biểu diễn công thức
      Trước đây có thể cấu hình khi mở CSV/TXT trong Excel, và LibreOffice hiện vẫn làm được, nhưng trong quá trình đơn giản hóa UI nói chung, tùy chọn đó đã bị chuyển đâu đó vào menu/thẻ ribbon Data. Bạn phải mở một workbook mới rồi tìm đúng tùy chọn; nếu muốn tiết kiệm thời gian thì nên dùng LibreOffice
  • Phần này thật sự khiến tôi ngạc nhiên. Khó tin là không cần truy vấn lồng nhau
    SELECT manifest, versionId, max(checkinTime) FROM version;
    Người ta nói trong SQLite, truy vấn thứ hai dùng max(checkinTime) này thực sự hoạt động tốt và trả về một câu trả lời đã được định nghĩa. Các engine cơ sở dữ liệu SQL khác sẽ trả về câu trả lời không xác định hoặc báo lỗi, nhưng trong SQLite, nó trả về manifestversionId của mục có checkinTime lớn nhất

    • Có thể đây là một tính năng hữu ích, nhưng thành thật mà nói tôi sẽ không kỳ vọng một truy vấn như vậy trả về như thế
      Trong trường hợp này không cần truy vấn lồng nhau; chỉ cần sắp xếp theo checkinTime rồi giới hạn một hàng: select manifest, versionId, checkinTime from version order by checkinTime desc limit 1
      Ít nhất thì nó sẽ hoạt động trong SQLite và PostgreSQL. Tôi nhớ trong Oracle phải dùng where rownum=1, nên khi đó cần truy vấn lồng nhau
    • Có thể xem nó đại khái như dạng viết tắt của GROUP BY manifest, versionId ORDER BY 3 DESC LIMIT 1, hoặc của một CTE tìm checkinTime lớn nhất rồi join lại
      Tuy nhiên nếu có nhiều hàng có cùng checkinTime lớn nhất thì sẽ có tính ngẫu nhiên, trở thành khẩu súng chĩa vào chân, nên tôi không mấy khi dùng tính năng riêng của SQLite3 này. Để chọn hàng tốt nhất một cách xác định, cần cách viết tường minh tương tự như trên
    • Đây không phải hành vi thường được kỳ vọng trong SQL, nhưng SQLite thường đi chệch kỳ vọng. Trường hợp này thì tiện, nhưng không chuẩn
    • Có vẻ một tác dụng phụ tiện lợi của phần triển khai về sau đã trở thành hành vi chính thức. Giống thứ tự khóa trong dictionary của Python
      Trong Postgres có thể làm điều tương tự bằng truy vấn DISTINCT ON. Trong SQL, đây là một trong những việc trông có vẻ đơn giản nhưng tôi thấy khó nhất
    • Việc có thể khẳng định đây là hành vi đã được định nghĩa khá đáng ngạc nhiên. Vì manifestversionId không phụ thuộc hàm vào max(checkinTime)
      Ví dụ, có thể có hai hàng cùng giá trị checkinTime, và giá trị đó là giá trị lớn nhất
  • Từng có lúc phát hành một sản phẩm dùng cả SQLite lẫn tệp XML
    Một trong những phần được cải thiện là chuyển một số bảng có ít dữ liệu sang tệp XML. Vì tệp nhỏ và hầu như không được dùng, lớp truy cập dữ liệu và việc chẩn đoán trở nên đơn giản hơn, và XML được tạo với thụt lề tab nhiều dòng
    Việc yêu cầu nhân sự kỹ thuật cần chẩn đoán sản phẩm mở cơ sở dữ liệu SQLite ra xem là một gánh nặng lớn. Nhưng ở các phần chính của sản phẩm, SQLite vượt trội hơn hẳn so với tệp XML. Phiên bản trước dùng tệp XML, nhưng XML không có cách cập nhật tăng dần tốt nên gặp vấn đề về khả năng mở rộng
    Ưu điểm của XML là định dạng dễ đọc với con người chỉ thật sự phát huy khi tệp nhỏ và thiết kế schema được điều chỉnh cho XML dễ đọc. Việc phải ghi lại toàn bộ tệp XML mỗi lần, cùng độ phức tạp phát sinh khi có thêm nhiều tính năng, nhanh chóng bào mòn ưu điểm lớn nhất của XML
    Việc người dùng thông thường phải trực tiếp chỉnh sửa bên trong tài liệu office là đủ hiếm, nên mức rào cản như học cách dùng trình đọc SQLite là chấp nhận được. Giới hạn của XML+Zip đối với ghi ngẫu nhiên ở giữa tệp là thứ ngay cả định luật Moore cũng không vượt qua được

    • Tôi không rõ định dạng gốc của SQLite đạt được kích thước tương tự XML+Zip khi không có Zip như thế nào. Không biết các trường TEXT hay BLOB của SQLite có được nén không, hay giả định rằng bên gọi sẽ nén BLOB trước khi ghi
  • ODT được thiết kế với tiêu chuẩn hóa trong tâm trí. Định dạng trước đó cũng rất giống, nhưng phụ thuộc nhiều vào các tiêu chuẩn hiện có như XHTML, SVG, CSS
    Nếu không thể tham chiếu các tiêu chuẩn hiện có, bản đặc tả ODT tự nó sẽ đột nhiên trở nên khổng lồ. Nỗ lực cập nhật tiêu chuẩn cũng có vẻ đáng kể, và vài năm gần đây không có nhiều tiến triển
    Thực tế thì định dạng SQLite có thể được cung cấp như một lựa chọn, nhưng con thuyền về định dạng tài liệu office có lẽ đã rời bến. Tuy vậy, đây là một căn cứ tốt cho lập luận nên biên soạn đặc tả SQLite thành tiêu chuẩn chính thức

    • Đặc tả được viết rất súc tích và chủ yếu chỉ quy định cú pháp hơn là hiệu ứng và hành vi, vậy mà vẫn đồ sộ tới 840 trang
      Ngoại trừ một vài khiếm khuyết, chẳng hạn vấn đề thuộc tính ooo:rsid làm bùng nổ số lượng style cục bộ và phạm vi văn bản, bảng tính không thưa, các cơ chế kỳ lạ trong styling bảng, đây là một markup được thiết kế rất tốt cho loại dữ liệu tài liệu này. Nó cân bằng tốt giữa markup ngữ nghĩa và cách biểu đạt mà người dùng thực sự muốn
      Ngược lại, Office OpenXML có các thẻ rỗng dùng cho định dạng mang trạng thái, và trong DOCX chúng bật/tắt việc văn bản theo sau có được hiển thị đậm hay không
  • Việc gắn một định dạng tệp với SQLite có vẻ hơi sai sai
    SQLite thì tốt, nhưng trong lĩnh vực này nó khá đặc thù. Vì nó làm rất nhiều việc nên khó sao chép y nguyên
    Nhưng trong trường hợp này có cần nhiều tính năng đến vậy không thì không. Chỉ cần ngữ nghĩa giao dịch cơ bản, an toàn và khả năng lưu cấu trúc bảng đơn giản; không cần đến toàn bộ chuẩn SQL hay cả bộ tối ưu hóa truy vấn
    Có thể có định dạng tệp tốt hơn, nhưng sẽ tốt hơn nếu đó là một định dạng tách biệt với SQLite

    • Không rõ vì sao lại không được: https://www.sqlite.org/appfileformat.html
      Kích thước dưới 1MB, và https://sqlite.org/footprint.html, ngay cả khi bật mọi tính năng cũng chỉ 750KB: https://www.sqlite.org/about.html
      Khi biên dịch có thể bỏ khá nhiều tính năng, và dường như cũng có tùy chọn điều chỉnh hoặc thu gọn bộ lập kế hoạch truy vấn: https://www.sqlite.org/compile.html
      Hơn nữa còn có câu “SQLite không cạnh tranh với cơ sở dữ liệu client/server. SQLite cạnh tranh với fopen()”: https://www.sqlite.org/whentouse.html
      Rốt cuộc thứ cần thiết không phải là bản thân cơ sở dữ liệu, mà là một thư viện cung cấp API và hành vi của cơ sở dữ liệu
    • Khía cạnh giao dịch, đặc biệt khi truy cập tệp đồng thời, khó hơn tưởng. Khi đó việc xử lý SQLITE_BUSY khá vất vả
      Tôi biết rằng trong xử lý giao dịch, lỗi tuần tự hóa là điều có thể dự kiến, nhưng trong SQLite rất khó phân biệt giữa lỗi kéo dài kiểu tự deadlock và vấn đề cập nhật đồng thời tạm thời. Nếu là lỗi tạm thời thì chỉ cần chạy lại closure định nghĩa công việc của giao dịch, nhưng nếu là lỗi kéo dài thì vô nghĩa
      Một phần vấn đề là sqlite3_stmt gộp cả tính chất của câu lệnh đã chuẩn bị và tập kết quả. Vì muốn cache bytecode đã biên dịch nên thường giữ nó lâu, nhưng nếu dừng giữa chừng khi đang lặp thì tại thời điểm đó nó có thể đang giữ khóa. Điều này có thể dẫn đến lỗi nâng cấp khóa ngoài dự kiến
      Cuối cùng tôi đã loại bỏ vấn đề bằng cách tạo báo cáo lỗi chi tiết với sqlite3_next_stmt, sqlite3_stmt_busy, sqlite3_sql. Dù chỉ dùng cho cá nhân, mã retry giao dịch vẫn đầy logging tùy chọn và chú thích. Logic retry giao dịch cho PostgreSQL dễ hơn nhiều
      Một điểm nữa khiến tôi ngạc nhiên là tài liệu nói rằng ở chế độ WAL với synchronous=NORMAL, một giao dịch đã commit có thể bị rollback sau khi mất điện hoặc hệ thống crash: https://sqlite.org/pragma.html#pragma_synchronous
      Điều này không liên quan đến ứng dụng của tôi
    • SQLite đã được dùng đúng cho mục đích kiểu này rồi. Nó được dùng làm OGC GeoPackage, và các bộ dữ liệu Mapbox/Maptiler cũng dùng nó
    • Có những định dạng được thiết kế trước hết để trao đổi. Lập luận từ phía SQLite là chủ ứng dụng nên áp đặt định dạng SQLite lên người dùng để biến nó thành chuẩn trên thực tế, còn công việc biến nó thành chuẩn pháp lý thì bị bỏ qua
      Nếu Richard Hipp và công ty có thể đưa ra một chuẩn SQLite ISO/IEC/ANSI/ETSI mà họ tuyệt đối không đi chệch, một rà soát pháp lý xác nhận không có bằng sáng chế ảnh hưởng, và nhiều hiện thực SQLite tương thích vẫn giữ nguyên mọi ưu điểm, thì khi đó mới có thể bàn chuyện khuyến nghị nó làm định dạng tệp. Nếu không, đó chỉ là yêu cầu đặt sự phụ thuộc mạnh vào một hiện thực từ một nguồn duy nhất và đẩy cả sự phụ thuộc đó sang người dùng
      XML, ASN.1, JFIF là các chuẩn chính thức, và ZIP cũng là chuẩn chính thức được chấp nhận thành ISO/IEC 21320-1:2015 trong quá trình chuẩn hóa OpenDocument
      Điều quan trọng nhất với tài liệu là mọi người khác phải đọc được. Giảm thời gian cập nhật đĩa chỉ là chuyện thứ yếu. Không thể không rút ra bài học từ việc Microsoft bóp méo các cơ quan tiêu chuẩn hóa để duy trì sự phụ thuộc: https://arstechnica.com/uncategorized/2008/10/norwegian-standards-body-implodes-over-ooxml-controversy/
    • Nhìn các ứng dụng của Apple thì đa số dùng SQLite làm định dạng lưu trữ. iMovie, iPhoto, Voice recording đều như vậy, Docker cũng thế
      Đó không thể là một lựa chọn quá sai
  • Một ví dụ khác là tile bản đồ raster. Thực chất là các ảnh vuông nhỏ có thể lên tới hàng triệu
    Tôi đã thử Zip, tar, hệ thống tệp và SQLite, và SQLite là nhanh nhất, nhỏ nhất, thậm chí còn tốt hơn cả archive thông thường không có overhead

    • Nhiều hệ thống tệp gặp vấn đề khi có hàng chục nghìn tệp trở lên trong một thư mục duy nhất, và với tile bản đồ thì đúng là tình huống đó xảy ra. SQLite nhanh hơn cũng không có gì đáng ngạc nhiên
    • Nếu SQLite nhanh hơn thì vấn đề nằm ở thư viện Zip đang dùng
      SQLite có một nhược điểm lớn. BLOB lấy từ cơ sở dữ liệu không thể mmap, nên phải sao chép sang nơi khác. Tệp Zip, nếu không được nén hoặc được nén bằng kiểu mã hóa đặc biệt như PVRTC, thì có thể mmap trực tiếp
  • OpenDocument là ảnh đã nén và XML. Rốt cuộc điều đó có nghĩa là phải phân tích cú pháp toàn bộ định dạng rồi đưa lên bộ nhớ
    Tôi không rõ SQLite cải thiện việc này như thế nào. XML không lý tưởng, nhưng vì được nén bằng Zip nên mức phạt về kích thước cũng không lớn
    Tất cả các ưu điểm được liệt kê trong bài về SQLite đều có thể triển khai nếu dùng SQLite làm mô hình runtime của tài liệu. Có thể làm cả trên đĩa lẫn trong bộ nhớ, nhưng SQLite không nhất thiết phải là định dạng truyền tải
    Ngược lại, SQLite có thể lớn hơn định dạng hiện tại. Sau các thay đổi, có thể xuất hiện khoảng trống không dùng đến, bị phân mảnh và trở nên thưa. Nếu lần nào cũng phải tối ưu hóa thì những ưu điểm như lưu nhanh cũng biến mất
    Những định dạng cần cập nhật delta và tra cứu chỉ mục nhanh, đồng thời không nên nạp toàn bộ tệp vào bộ nhớ, thực tế thường dùng SQLite làm định dạng tệp. Tuy nhiên, tôi cảm thấy OpenDocument là một ví dụ không hay để chọn làm đối tượng áp dụng SQLite trong kịch bản giả định này

    • XML và Zip không làm tốt cập nhật gia tăng. Khi lưu, phải ghi toàn bộ tệp ứng dụng, và nếu có vấn đề trong lúc ghi thì có thể xảy ra hỏng dữ liệu
      Nếu dùng SQLite làm định dạng trên đĩa và triển khai ứng dụng đúng cách, có thể tránh kết thúc ở trạng thái bị hỏng
      XML/Zip cũng có thể đạt được điều tương tự bằng mẹo đổi tên tệp, nhưng SQLite cung cấp việc đó trong một tệp đĩa duy nhất. Nếu đã dùng SQLite làm mô hình bộ nhớ thì không có lý do gì để không dùng nó làm định dạng trên đĩa/truyền tải. Đến thời điểm đó thì gần như là miễn phí
      Vấn đề kích thước tệp có lẽ có thể xử lý bằng VACUUM