Inserting a billion rows in SQLite under a minute
avi.im
avi.im
Another crazy idea is to learn about SQLite file format and just write the pages to disk.
> I am also interested in writing the SQLite or PostgreSQL file format straight to disk as a faster way to do ETL.
what exactly you are trying to do here?
https://github.com/sqlitebrowser/sqlitedatagen
There's some initial work to parallelise it with goroutines here:
https://github.com/sqlitebrowser/sqlitedatagen/blob/multi_ro...
Didn't go very far down that track though, as the mostly single threaded nature of writes in SQLite seemed to prevent that from really achieving much. Well, I _think_ that's why it didn't really help. ;)
I maintain a similar go tool for work, which I use to stuff around 1TB into MariaDB a time:
https://www.flamingspork.com/projects/libeatmydata/
libeatmydata shouldn't be used in production environments, generally speaking, as it biases for speed over safety (lib-eat-my-data), by disabling fsync, and associated commands for the running process under it. Disabling those commands results in less I/O pressure, but comes with the risk that the program thinks it has written safely and durably, and that may not be true. It essentially stops programs that are written to be crash proof, from being actually crash proof. Which under the circumstances you're operating on you almost certainly don't care.
I've used this when reloading database replicas from a dump from master before, as it drastically speeds up operations there.
Go is one of the programming languages that makes syscalls directly, thus libeatmydata has no effect on it (being an LD_PRELOAD to the libc).
pragma synchronous = off
https://www.sqlite.org/pragma.html#pragma_synchronousCertainly if I was trying to do the same thing in Pg my first thought would be "batched COPY commands".
However, I think your Rust threaded trial might be a little bit off in subtle ways. I would truly expect it to perform about several times better than single threaded Rust and async Rust (async is generally slower on these workloads, but still faster than Python)
Edit : After reading your rust code, you might have room for some improvements :
- don’t rely on random, simply cycle through your values
- pre-allocate your vecs with ˋwith_capacityˋ
- dont use channels, prefer deques..
- ..or even better, don’t use synchronization primitives and open one connection per thread (not sure if it will work with sqlite?)
> The machine I am using is MacBook Pro, 2019 (2.4 GHz Quad Core i5, 8GB, 256GB SSD, Big Sur 11.1)
Given the target schema, 100M rows with only 8GB RAM risks hitting swap hard.
https://github.com/siara-cc/sqlite_micro_logger_arduino
https://github.com/siara-cc/sqlite_micro_logger_arduino/blob...
This is a heavily subsetted implementation of SQLite3 that can read/write databases (presumably on SD cards) from very small microcontrollers.
It presumably doesn't have the same ACID compliance properties, but with a single <1.5k source file, may represent a particularly efficient way to rapidly learn the intrinsics if you happened to want to directly manipulate files on disk.
Now I'm thinking it could actually be interesting to see what drh thinks of this implementation (and any gotchas in it) because of its small size and accessibility.
We do SQL evaluation for a lot of business logic throughout our product, and we have found that starting from a template database (i.e. one with the schema predefined and canonical values populated) can save a lot of time when working in tight loops.
### PROLOGUE ### Sample row ### EPILOGUE
You copy & write prologue, write 1B sample raws (can optimize this at will, large writes, etc)
Copy & write epilogue and fsync the data. You probably need to modify some metadata, but that should be a few writes at most.
That should be as good as it gets, providing your IO is optimal.
For most SQL systems, the fastest way to do inserts is always just going to batched inserts. There's maybe some extra tricks to reduce network costs/optimize batches [0], but at it's core you are still essentially inserting into the table through the normal insert path. You can basically then only try and reduce the amount of work done on the DB side per insert, or optimize your OS for your workload.
Some other DB systems (more common in NoSQL) let you actually do real bulk loads [1] where you are writing direct(ish) database files and actually bypassing much of the normal write path.
[0] https://dev.mysql.com/doc/refman/5.7/en/insert-optimization.... [1] https://blog.cloudera.com/how-to-use-hbase-bulk-loading-and-...
For large deletes it is often better to move the rows that won't be deleted to a new table and rename the table when done.
With large updates it is important to look at the query plan and optimize it with good indexes. Batching also works well in this scenario.
Also I got another feedback that title should indicate that it is a test database and emphasise that it is not durable.
I am wondering the right way to convey all of this in the title yet also keep it short.
Add "trying":
"Trying to insert 1 billion rows in SQL in under a minute".
If anything it's more interesting because it implies the chance of failure.
> 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.
thousand = 1000
million = 1000 * thousand (or 1000^2)
billion = 1000 * million (or 1000^3)
trillion = 1000 * billion (or 1000^4)
(not to discount regional differences)For English-speaking countries, 1B = 1M * 1k
For Spanish-speaking countries, 1B = 1M * 1M
For other languages it's a big "it varies", though the second definition seems to be the most common. The term "billion" is honestly, as ambiguous as using "06-03" for a date.
Also note that, historically, English also followed the second definition, so for old literature it's also confusing.
Faster by 10% than fastest author implementation on my machine - 19 seconds against 21 for 'threaded_batched'.
And I think INSERT INTO ... SELECT is the fastest way to bulk insert data into sqlite.
Also, I have tried to use carray sqlite feature that allow to share memory with sqlite and use recursive CTE to query it, but it is slower. Though, you can pass values you've generated from Rust instead of using random().
Since it is only 100M rows, it takes 1.8 GB on the disk, so I've used tmpfs for this which essentially is a ramdisk. But I have a gen4 pcie nvme SSD - it can reliably write at 4GB/s sequentially, so writing takes a ~500ms for 100M rows, it is not a bottleneck here. random() takes ~half of the insert time. Generating those values with Rust, for example, is faster, but sharing this data with sqlite takes more time than generating it with random().
Maybe implementing custom virtual table in C or Rust like build-in generate_series, but the one that will produce user table fields will be faster, but that is significantly more effort than my query.
This query with random() and generate_series executed in sqlite CLI takes whooping 8MB of the RAM, so you don't even have to close all Electron-based applications to run it on a computer with 8GB of RAM.
Invocation:
command time sqlite3 ':memory:' '
create table IF NOT EXISTS user
(
id INTEGER not null primary key,
area CHAR(6),
age INTEGER not null,
active INTEGER not null
);
INSERT INTO user (area, age, active) SELECT 0, 1, 2 FROM generate_series(1, 100000000, 1);
'
Result: 16.34user 0.43system 0:16.89elapsed 99%CPU (0avgtext+0avgdata 1477320maxresident)k
11inputs+0outputs (0major+369851minor)pagefaults 0swaps
Invocation with pragmas: command time sqlite3 ':memory:' '
PRAGMA journal_mode = OFF;
PRAGMA synchronous = 0;
PRAGMA cache_size = 1000000;
PRAGMA locking_mode = EXCLUSIVE;
PRAGMA temp_store = MEMORY;
create table IF NOT EXISTS user
(
id INTEGER not null primary key,
area CHAR(6),
age INTEGER not null,
active INTEGER not null
);
INSERT INTO user (area, age, active) SELECT 0, 1, 2 FROM generate_series(1, 100000000, 1);
'
Result with pragmas: 17.31user 0.41system 0:17.85elapsed 99%CPU (0avgtext+0avgdata 1477288maxresident)k
11inputs+0outputs (0major+369850minor)pagefaults 0swaps
As expected, the pragmas make no difference when using `:memory:` -- 17 seconds, 1.4 GB RAM each on my laptop.Insertion performance on a single table are very very hard to optimize.
A single process looping it is your best bet.
I would just increase the batch size, which is the most influent factor.
Then another point... When you do batches, you do
BEGIN TRANSACTION;
for i in range(1, 50):
execute_stmt
COMMIT;
You do not create a long list of parameters.https://github.com/avinassh/fast-sqlite3-inserts/blob/master...
;)
> You do not create a long list of parameters.
I have done much worse by trying to insert a really long string of 100K params
I can import 1GB CSV file in 10 seconds on my MacBook. This queries from a virtual table and puts it in actual table
I just shat out a CSV file and then imported it. It was much quicker! (like 1000x faster)
This article has some useful notes for me now to try the next time I play with my pet projects. :)
I was actually just working on SQLite speed for Objective-S (http://objective.st), partly as a driver for getting some of the more glaring inefficiencies out.
Using a "to do list" schema, I currently get the 100M rows out in 56 seconds, which is around half the speed of the Rust example given here, but 3 times the speed of PyPy and almost 10x faster than Python.
This is from an interpreted script that not only does the inserts and creates the objects to insert in the first place, but also defines the actual class.
The lower-level code is written in Objective-C, like the rest of Objective-S.
Class definition:
class Task {
var <int> id.
var <bool> done.
var <NSString> title.
-description { "<Task: title: {this:title} done: {this:done}>". }
+sqlForCreate {
'( [id] INTEGER PRIMARY KEY, [title] NVARCHAR(220) NOT NULL, [done] INTEGER );'.
}
}.
Code to insert a computed array of tasks 10 times: 1 to:10 do: {
this:tasksTable insert:taskList.
}.Although, frankly, SQLite would not be my choice.
Whatever queries that might be a hashmap/tree/skiplist, etc. would be a lot better.
Actually, I am beyond certain. When it's all about the memory no database comes even remotely close to a properly picked datastructures + structure/objects layout.
If I need transaction log + persistence, databases have a decent application.
In more than 20y, I have never had a case: Yay, I can use relation structures in memory b/c I don't know what I am going to do with the data.
But a pretty good use case (IMO) is testing. If you want to do an integration test with an SQL database, and you want to test large numbers, this might be a good fully functional stub to run locally.
Redis is in pretty much the same category. Testing is sort of a valid case, if you are committed to write pure SQL with minimal use of any dialect specifics (but even 'create table' syntax might be different). Running on the real thing is close to no replacement when it comes to databases.
Many databases have docker images nowadays, so it aint hard to run them locally. Likely at a point you'd want to optimize the SQL, itself, rendering the tests incompatible.
Of course, if you know you get one single query which you know in advance, carefully build a data structure to cater just to that, and you know your data structures beyond the just the basics - then, yes, a DBMS would be overkill. But it still won't be a walk in the park.
Another is they support this closures.c extension which is very nice for rapid queries on tree structured data locally in memory. The JSON1 extension is also nice for rapidly querying/reshaping deeply nested json data. Theres also spellfix1 that can provide fuzzing capabilities. If you need any of these with low latency and in memory its a great choice.
Sqlite is great for rapidly building low latency static data serving services for frontend experiences. Something I’m exploring now is combining sqlite with streamlit to rapidly build data exploration UIs.
Like how many times have you wanted to quickly add fuzzy matching or full text search to some program? You use fuzzywuzzy but its pretty slow, sqlite provides performant implementations of this stuff thats super simple to set up.
Most attempts to query using raw data structures just means you end up rebuilding a (very poor) relational database with none of the features.
If you just want to browse what other people are using, and not go by the recommendations of a random commenter, try:
https://db-engines.com/en/ranking
but note that's a joint ranking both for transaction-focused and analytics-focused DBMSes.
If it's time series data there are some more specialized offerings and I'm (even) less of an expert there.
At billion rows, that is 18GB of data. With some overhead for storing page info, let’s call it 20GB flat.
A modern SSD can deliver ~500MB/s write speed. That means writing 20GB of data can be done in 40 seconds.
Therefore a billion rows in a minute is quite plausible. At-least not bottlenecked by disk speed (if we can saturate disk).
The SSD in the authors machine can do 1300MB/s and the latest M1 model can do 2100MB/s.
For current gen SSDs with PCIe 4.0, that number increases to 6600MB/s for the Sabrent Rocket 4 Plus.
That would mean just around ~5 seconds for writing to disk.
More like ~5GB/s.
Do that cartesian join three more times, and you have over 4 billion results.
Then you simply need to use the random function in conjunction with division, rounding and case statements to get the desired random numbers.
Prepared statements should be considered the norm for SQLite - they have pretty major performance benefits, and any decent ORM or query engine can probably do it for you implicitly or with a single flag, so it's practically free for many applications. I'm always surprised how often I see applications or benchmarks or "how to be fast with SQLite" not using them, so I'm definitely glad to see them covered here :)
- language has very little to do since the bottleneck will most likely be the way you insert data
- indeed prepared statements are useful but the performance didn't change much when I did long transactions and commit every certain amount of thousand of rows
- having lots of rows in your table is good but certain queries, like aggregation over many rows, are not what SQLite is great about.
- ClickHouse can easily ingest that and more in a laptop without even any scripting language.
/fast-sqlite3-inserts (master)> time make busy-rust
Sun Jul 18 17:04:59 UTC 2021 [RUST] busy.rs (100_000_000) iterations
real 0m9.816s
user 0m9.380s
sys 0m0.433s
________________________________________________________
Executed in 9.92 secs fish external
usr time 9.43 secs 0.20 millis 9.43 secs
sys time 0.47 secs 1.07 millis 0.47 secs
fast-sqlite3-inserts (master)> time make busy-rust-thread
Sun Jul 18 17:04:48 UTC 2021 [RUST] threaded_busy.rs (100_000_000) iterations
real 0m2.104s
user 0m13.640s
sys 0m0.724s
________________________________________________________
Executed in 2.33 secs fish external
usr time 13.68 secs 0.20 millis 13.68 secs
sys time 0.78 secs 1.18 millis 0.78 secs
I'm probably doing something wrong. Or I'm getting the pace needed for the billion?This is on a M1 MacBook Air.
pragma temp_store = memory;
only affects temporary tables and indices, not the main database itself[0]# createSQL.tcl
set increment 100000
set loop [expr {100000000 / $increment}]
puts {PRAGMA journal_mode = OFF; PRAGMA synchronous = 0; PRAGMA cache_size = 1000000; PRAGMA locking_mode = EXCLUSIVE; PRAGMA temp_store = MEMORY;
CREATE TABLE user( pk INTEGER PRIMARY KEY, area INTEGER, age INTEGER, active INTEGER );}
for {set i 0} {$i < $loop} {incr i} { puts " WITH RECURSIVE tmp(pk,area,age,active) AS ( SELECT [expr {$i * $increment}], 500001, 5, 0 UNION ALL SELECT pk+1, 10000*(abs(random())%9+1)+abs(random())%100000, 5*(abs(random()%3)+1), abs(random()%2) FROM tmp WHERE pk < [expr {($i+1)*$increment-1}] ) INSERT INTO user SELECT * FROM tmp;" }
#EOF
Core 2 Duo 2.53 GHz, 4GB RAM, SSD:
$ time -p tclsh createsql.tcl | sqlite3 test.db
off
exclusive
real 213.37
user 208.10
sys 4.58
1: is it useful / feasible to set a sort of "lower bound" to these optimizations by profiling the raw write time on a set of a certain size?
2. assuming that writes are ultimately the limiting factor, could you gain performance by calculating the minimum batch size as it's write time intersects in-memory calculation, and as soon as you hit that size, pop a thread to write that batch to disk - then return some async signal when the write is done to trigger the next batch of writes? is it possible to "stitch together" these batches post-writes?
edit: mobile formatting is le suck
INSERT INTO t (...) SELECT ... from virtual_table
would be any faster.
I wonder if you could insert into 10 different tables from 10 threads or processes then
insert into single_table (…) select (…) union.
No idea if insert select does the trick or not but you’re almost at the point of partitioning here. If the application called for it you could do some sort of poor mans partition in order to write all the data and then push some complexity into the read.
I also wondered what if any impact the ID primary key had on inserting at the level of frequency.
/armchair
The source code can be found here: https://github.com/wuxb45/remixdb/blob/master/i100m.c
$ make i100m.out libremixdb.so
$ numactl -N 0 ./i100m.out
insert time without sync: 24.865s, with sync: 25.801sThat is about 6 million combinations. This is not such a big space that it would be impractical to precompute all possible combinations. I wonder if hashing in to such a precomputed table might help. Might not if the overhead of maintaining the lookup table is too high.
I really like SQLite, it's well supported in Python, backup-restore is very simple. What is the real difference (probably application depended) between SQLite and a "real" DB engine?
I wonder at what scales this starts to count (in human readable units ;))
(It would likely be faster for other DB engines, because there is network overhead in the communication between the program and the DB; no such things for sqlite).
Lastly, how long does it take to make a copy of an already prepared 1b row SQLite file?that seems easier than generating a new one.
I wonder how much I could speed up my test suites (that don't rely on transaction rollbacks) by disabling journalling. Those milliseconds per query add up to seconds per tests and minutes per PR.
This is actually a little bit mind blowing for me. I'm gonna go and play with this. But what a cool read!
But the results given are all for inserting 0.1B rows, not 1B rows. What are the full results?
Also interesting whether batched numpy version would compare better to Rust.
PRAGMA locking_mode = EXCLUSIVE;
improve performance much?
Worth it.
>I could just run s the script again.
Found a typo.
The author must have a rather low opinion of programmers... generating data with a script is good for 1,000 rows. Maybe 10,000 rows. 100,000 is probably stretching it. Beyond that the slowness will be meaningful.
Anyway, if the "test database" data is given in advance, you want an in-memory DBMS which can load data from files. If it isn't - use an in-memory DBMS which supports generated columns, or apply your random-number-generating function on a trivial column.
(MonetDB standalone/embedded would be a decent option; maybe the columnar storage mechanism in MariaDB? And of course there are commercial offerings.)
> Unfortunately, it was slow. Really slow. So I did what any programmer would do: went down the rabbit hole of learning more about SQLite...
That's the wrong approach. SQLite isn't called "lite" for nothing. It's not what you would use for bulk operations on large tables.
Read the SQLite FAQ: https://www.sqlite.org/whentouse.html
it oversells itself a bit, but still says its (only) advantages for data analysis are: "easier to install and use and the resulting database is a single file that can be written to a USB memory stick or emailed."
- Assuming you do have a test database in advance that's represented in a file, but for some reason not a SQLite file or other database, how long does it take to load 100M rows in a database of your choice, and how does that compare to the author's results?
- The author does not in fact have a test database in advance, and is generating random data, which I think is what you're saying by "apply your random-number-generating function on a trivial column" - how long would your approach take to generate 100M rows?
- If you generated a database of about the same size in a database of your choice, how do bulk operations on large tables perform in comparison to SQLite? (My understanding is that it's "lite" because it's not client/server, not because its performance is poor.)
I don't think any of these experiments would be terribly hard to run.