惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tenable Blog
K
Kaspersky official blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Security Latest
Security Latest
P
Privacy & Cybersecurity Law Blog
Google DeepMind News
Google DeepMind News
Simon Willison's Weblog
Simon Willison's Weblog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
人人都是产品经理
人人都是产品经理
O
OpenAI News
Help Net Security
Help Net Security
N
News and Events Feed by Topic
博客园 - 司徒正美
U
Unit 42
Security Archives - TechRepublic
Security Archives - TechRepublic
The Cloudflare Blog
D
DataBreaches.Net
Y
Y Combinator Blog
AI
AI
L
LINUX DO - 最新话题
C
CXSECURITY Database RSS Feed - CXSecurity.com
H
Heimdal Security Blog
宝玉的分享
宝玉的分享
C
CERT Recently Published Vulnerability Notes
博客园 - 聂微东
TaoSecurity Blog
TaoSecurity Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Project Zero
Project Zero
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Jina AI
Jina AI
M
MIT News - Artificial intelligence
Microsoft Azure Blog
Microsoft Azure Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Attack and Defense Labs
Attack and Defense Labs
V
V2EX
博客园 - 【当耐特】
S
SegmentFault 最新的问题
小众软件
小众软件
L
LangChain Blog
N
Netflix TechBlog - Medium
V
Vulnerabilities – Threatpost
T
Tor Project blog
AWS News Blog
AWS News Blog
博客园 - 三生石上(FineUI控件)
Recent Announcements
Recent Announcements
G
GRAHAM CLULEY
Know Your Adversary
Know Your Adversary
S
Securelist
T
Troy Hunt's Blog

blag

SQLite prefixes its temp files with `etilqs_` - blag Setsum - order agnostic, additive, subtractive checksum - blag Oldest recorded transaction - blag Replacing a cache service with a database - blag SQLite commits are not durable under default settings - blag PSA: SQLite WAL checksums fail silently and may lose data - blag Rickrolling Turso DB (SQLite rewrite in Rust) - blag Collection of insane and fun facts about SQLite - blag How bloom filters made SQLite 10x faster - blag In search of a faster SQLite - blag Galloping Search - blag Building a distributed log using S3 (under 150 lines of Go) - blag Zero Disk Architecture - blag PSA: Most databases do not do checksums by default - blag PSA: SQLite does not do checksums - blag Disaggregated Storage - a brief introduction - blag Why does SQLite (in production) have such a bad rep? - blag SQLite Slaps - blag Now - blag Learning C - blag Snapshot Testing - blag Win: contribution to libSQL (SQLite) codebase - blag Errata in Hekaton MVCC paper - blag Internet is wholesome: MVCC edition - blag It is becoming difficult for me to be productive in Python - blag MongoDB secondary only index - blag Introducing CaskDB – a project to teach you writing a key-value store - blag Recurse Center: Winter Break - blag Recurse Center Day 24: Hacking Go compiler to add a new keyword - blag Recurse Center Day 20: Django v4 upgrade (from v1) - blag Recurse Center Day 19 - blag Recurse Center Day 18 - blag Recurse Center Day 17 - blag Recurse Center Day 16: Open Source - blag Recurse Center Day 15: B Tree Algorithms - blag Recurse Center Day 14: NoSQL Transactions - blag Recurse Center Day 13: Why 'Raft'? - blag Recurse Center Day 12: Isolation Anomalies - blag Recurse Center Day 11: B Tree Insertions - blag Recurse Center Day 10: Learning Distributed Systems - blag Recurse Center Day 9: Papers We Love - blag Recurse Center Day 8: B Tree Fill Factor (Part 2) - blag Recurse Center Day 7: Basics of ncurses - blag Recurse Center Day 6: B Tree Root - blag Recurse Center First Week - blag Recurse Center Day 5: Garbage Collection Algorithms - blag Recurse Center Day 4: B Tree fill factor - blag Recurse Center Day 3: Hammock Driven Development - blag Recurse Center Day 2: BTree Node - blag Recurse Center Day 1: init - blag What I want to do at Recurse Center - blag Accepted to the Recurse Center! - blag Marshaling Struct with Special Fields to JSON in Golang - blag I ended up adding duplicate records on a unique index in MongoDB - blag Setting up Github Actions for Hugo - blag Moving to Hugo - blag Catching SIGTERM in Python - blag Git/Github fork-pull request-update cycle - blag Using uWSGI with Python 3 - blag When is my Cake Day? - blag Staying Ahead of Amazon, in Amazon Treasure Hunt Contest - blag How I Am Maintaining Multiple Emails For Git On A Same Machine - blag An exploit on Gaana.com gave me access to their entire User Database - blag Flashing Asus-WRT Merlin by XVortex on NetGear NightHawk R7000 - blag Install Windows 8 UEFI on Legacy BIOS with Clover (and Dual boot with Yosemite) - blag Scraping Javascript page using Python - blag Installing Transmission (remote and CLI) client on Raspberry Pi - blag About - blag Projects - blag
Towards Inserting One Billion Rows in SQLite Under A Minute - blag
2021-07-18 · via blag

Current Best: 100M rows inserts in 33 seconds. (you can check the source code on Github)


Recently, I ran into a situation where I needed a test database with lots of rows and needed it fast. So I did what any programmer would do: wrote a Python script to generate the DB. Unfortunately, it was slow. Really slow. So I did what any programmer would do: went down the rabbit hole of learning more about SQLite, Python, and eventually Rust… in my quest to get a 1B row database under a minute. This blog post is a summary of this fun and educational exercise.

Goal

The goal of this experiment is to generate an SQLite database with one billion rows under a minute, on my machine, with the table having the following schema:

create table IF NOT EXISTS user
(
    id INTEGER not null primary key,
    area CHAR(6),
    age INTEGER not null,
    active INTEGER not null
);

The generated data would be random with following constraints: The area column would hold six digits area code (any six digits would do, no validation). The age would be any of 5, 10, or 15. The active column is either 0 or 1.

The machine I am using is MacBook Pro, 2019 (2.4 GHz Quad Core i5, 8GB, 256GB SSD, Big Sur 11.1)

Aspects I was willing to compromise on were:

  • I don’t need the durability guarantee. That is, it is fine if the process crashes and all the data is lost. I could just run the script again.
  • It may use my machine resources to the fullest: 100% CPU, 8GB Memory and gigabytes of SSD space.
  • No need of using true random methods, pseudo-random methods from stdlib are just fine.

Python Prototype

Python is my go to language for any kind of scripting. The standard library provides a nice SQLite module, using which I wrote my first version. Here is the full code. In this script, I tried to insert 10M rows, one by one, in a for loop. This version took close to 15 minutes, sparked my curiosity and made me explore further to reduce the time.

In SQLite, each insertion is atomic and is a transaction. Each transaction guarantees that it is written to disk thus could be slow. I tried different sizes of batch inserts, found out 100,000 to be a sweet spot. With this simple change, the running time was reduced to 10 minutes. Here is the full code

SQLite Optimisations

The script I had written is very simple, so I assumed there isn’t much room for optimisation. Secondly, I wanted the code to be simple and near to the daily usage version. The next logical step was to look for database optimisations and I started diving into the amazing world of SQLite.

The internet is filled with many SQLite optimisation posts. Based on them, I made following changes:

PRAGMA journal_mode = OFF;
PRAGMA synchronous = 0;
PRAGMA cache_size = 1000000;
PRAGMA locking_mode = EXCLUSIVE;
PRAGMA temp_store = MEMORY;

What do these do?

  • Turning off journal_mode will result in no rollback journal, thus we cannot go back if any of the transactions fail. This disables the atomic commit and rollback capabilities of SQLite. Do not use this in production.
  • By turning off synchronous, SQLite does not care about writing to disk reliably and hands off that responsibility to the OS. A write to SQLite, may not mean it is flushed to the disk. Do not use this in production.
  • The cache_size specifies how many memory pages SQLite is allowed to hold in the memory. Do not set this to a high value in production.
  • In EXCLUSIVE locking mode, the lock held by the SQLite connection is never released.
  • Setting temp_store to MEMORY will make it behave like an in-memory database.

The SQLite docs have a full page dedicated on these parameters, they also list a bunch of other parameters. I haven’t tried all of them, the ones I selected provided a decent running time.

Here are some of the articles I read on the internet which helped me with these optimisation parameters: 1, 2, 3, 4, 5.

Python Revisited

I rewrote the Python script again, this time including the fine-tuned SQLite parameters which gave a huge boost and the running time was reduced drastically.

PyPy

I have never used PyPy and PyPy homepage highlight that it is 4x faster than CPython, I felt this is a good opportunity to try it and test out their claims. I was also wondering if I had to make changes to make it run, however, my existing code ran smoothly.

All I had to do was run my existing code, without any change, using PyPy. It worked and the speed bump was phenomenal. The batched version took only 2.5 minutes to insert 100M rows. I got close to 3.5x speed :)

(I am not affiliated with PyPy but I would request you consider donating to PyPy for their efforts.)

Busy Loop(?)

I wanted to get some idea of how much time Python is spending just in loops. So I removed the SQL instructions and ran the code:

  • The batched version took 5.5 minutes in CPython
  • The batched version took 1.5 minutes in PyPy (again a 3.5x speed bump)

I rewrote the same in Rust, the loop took only 17 seconds. I decided to move from Python and experiment further in Rust.

(Note: This is NOT a speed comparison post between Python and Rust. Both have very different goals and places in your toolkit.)

Rust

Just like Python, I wrote a naive Rust version where I inserted each row in a loop. However, I included all the SQLite optimisations. This version took about 3 minutes. Then I did further experiments:

  • The previous version had used rusqlite, I switched to sqlx which runs asynchronously. This version took about 14 mins. I was expecting this degraded performance. But it is worth noting that it performed worse than any of the Python iterations I had come up with so far.
  • I was executing raw SQL statements, switched to prepared statements and inserted the rows in a loop, but reusing the prepared statement. This version took about only a minute.
  • Also tried creating a long string with an insert statement, I don’t think this performed any better. The repository has few other versions as well.

The (Current) Best Version

  • I used prepared statements and inserted them in a batch of 50 rows. To insert 100M rows, took 34.3 seconds. source code
  • I created a threaded version, where I had one writer thread that received data from a channel and four other threads which pushed data to the channel. This is the current best version which took about 32.37 seconds. source code

IO Time

Good folks at the SQLite forum gave me an interesting idea, measure the time it takes for in-memory DB. I ran the code again giving the DB location as :memory:, the rust version took two seconds less to complete (29 seconds). I guess it is fair to assume that it takes 2 seconds to flush 100M rows to disk. This also shows that there might not be any more SQLite optimisations possible to write to disk in a faster way, since 99% of time is being spent in generating and adding rows.

Leaderboard

(*at the time of writing. The repo has the upto date numbers)

VariantTime
Rust33 seconds
PyPy150 seconds
CPython510 seconds

Key Takeaways

  • Make use of SQLite PRAGMA statements when possible
  • Use prepared statements
  • Do a batched insertions
  • PyPy is actually 4x faster than CPython
  • Threads / async may not be faster always

Further Ideas

Here are a few directions I plan to explore next to improve performance:

  1. I haven’t run the code through a profiler. It might give us hints about the slow parts and help us optimising the code further.
  2. The second fastest version runs single threaded, on a single process. Since I have a four-core machine, I could launch 4 processes, get up to 800M rows under a minute. Then I would have to merge these in few seconds, so that the overall time taken is still less than a minute.
  3. Write a go version with the garbage collector completely disabled.
  4. It is entirely possible that rust compiler might have optimised the busy loop code and removed the allocations, calls to random functions since it had no side effects. Analysis of the generated binary might shed more light.
  5. Here is a really crazy idea: learn about SQLite file format then generate the pages and write to disk directly.

Looking forward to discussions and/or collaborations with curious souls in my quest to generate a billion record SQLite DB quickly. If this sounds interesting to you, reach out to me on Twitter or submit a PR.

Thanks to Bhargav, Rishi, Saad, Sumesh, Archana, and Aarush for reading a draft of this.


1. Why? In a telegram bot, I wrote, one of the SQL queries required a partial index. I have used partial indexes in Postgres/Mongo, but I was pleasantly surprised to know that SQLite also supported them. I decided to write a blog post (spoiler: which I never did), with numbers showing the effectiveness of partial indexes. I wrote a quick script and generated a DB, but the data was too small to show the power of partial indexes and queries were fast without them. The larger DB required more than 30 minutes to generate. So I spent 30+ hours to reduce the 30 mins of running time :p
2. If you liked this post, then you may like my experiment with MongoDB where I inserted duplicate records in a collection with a unique index - link.
update (19, July): prefixed the title with ’towards’ to make the intention more clear