[no sq] Split blockpos into 3 different columns in sqlite3 map database - #15768
Conversation
Note that this index will only allow SQLite to speed up the following (parts of) a query:
That is, a range query can essentially only be done along a single axis, and all preceding axes must be equality comparisons (or SQLite will effectively have to loop over all possible values for each), in the order the keys appear in the index. |
|
We can add more indexing if needed tho. Edit: we can however influence the order of the default index. (X, Z, Y) sounds most useful so I'll change that. |
|
While you are at it, we could also split out a block's timestamp into a separate column. |
If all our databases support z-order we can do approximate spatial indexing that way. (It's not hard to add it via plpgsql to Postgres for example, and then index on a function.) On the whole I would expect this change to slow things, but that's just a gut feeling. |
|
Overall I'm in favour of this PR, as it will significantly improve performance of my map analysis scripts, in particularly RocketLib Toolkit which includes many functions for iterating mapblocks in certain areas. https://bitbucket.org/sorcerykid/rocketlib/src/master/ Currently, I have to resort to generating a "Cache" database that just consists of a cross-reference table for indexing Block Position (X, Y, Z) to Block ID. This process can be extremely time-consuming, particularly on a map.sqlite that is several gigabytes. It would be so nice to finally eliminate that step entirely. My map viewer application also makes extensive use of position queries for rendering and navigating a density map along the X, Y, or Z axis. Currently, I have to scan the entire database and manually total up the selected mapblocks. But with separate columns for Block Position, this could be streamlined. |
Would make sense to do at once, but extracting out the timestamp has a whole other bunch of different implications.
I couldn't find much info on native support in databases for this but also I don't really see any use case. |
Can you point to where this happens? I'm genuinely curious, because I was under the impression mapblocks were only written back to the database if a flag was set for one or more changes (aside from just a new timestamp). After all, if a mapblock is already loaded in memory, then I would think the timestamp is also in memory. So It seems the solution is simply not to write the mapblock to the database if only the timestamp has changed. |
luanti/src/serverenvironment.cpp Lines 1434 to 1448 in dd0070a Note that this only marks the block to be written when unloaded, not immediately. Someone could go and test how many byte of disk writes it would save if we could update the timestamp independently, for a typical SP or MP session. |
|
Created two worlds with same seed et al. Seen no measurable degradation in load speed. Good! |
Just rememberd f1349be :), before we marked every loaded block dirty. Now it's just unloaded active blocks. |
|
Now that I am looking a bit more, check this one: https://www.sqlite.org/lang_createtable.html#rowid, especially:
It seems all this time we had declared the sqlite table such that it had two INT keys. (Because we used INT instead of INTEGER to declared the PK). And when I change that INTEGER and create a new world (with same seed) I see that pure DB load time of a block goes from 6-7us to about 5-6us. In the scheme of things it won't make a visible difference, though. |
Wow. I can't say I'm a fan of all the subtle behaviors SQLite has (often justified with backwards compatibility). |
91eb4f2 to
1cc07d7
Compare
|
More of the sqlite quirkiness... According to this https://www.sqlite.org/rowidtable.html
So: Is the same as: And if so, we can replace the implicit rowid column with our pos column and maintain as before: This way we get the benefit of the x, y, z columns and backwards compatibility. Need to be tested of course! And according to this: https://www.sqlite.org/lang_createtable.html it's identical to: This also makes it clear that by adding the x, y, and z columns we essentially have just added an index. |
|
I'm sure that could be considered an elegant solution (by some measure), but I really don't want to make the "is this a new format or old format map" decision on a row-basis. It just complicates things for everyone and also literally breaks range queries. |
|
Looked at some DB sizes. Sames seed, and camera position and direction. viewing_range = 1000, mostly an open scene with some ocean. Loads about 8000 blocks.
sqlite seems to do well with this change. So while I'd prefer the old format (with the extra useless index removed for 20% space saving), I guess I'll be the only one. At least this one doesn't increase the size, and performance is on par as far as I can tell. |
|
@appgurueu @SmallJoker @grorp @Desour any more comments? |
Sorry for being late. Backwards compatibility by preserving the In the future it might be helpful to have a separate table to store the serialization version. It's not nice to run into incompatible mapblocks during startup (in case of a ser ver bump) or a "hard-crash" upon join. At least that could be wrapped into a nice error message. |
|
When trying to optimize a database, consider the queries you need to perform. Any more complicated encoding (e.g., z curves or similar spatial curves) only ever pay off if you frequently select ranges, and may even harm certain type of selections. Currently, luanti only ever accesses by ID, one block at a time. In fact, I'd argue that the (x,y,z) scheme comes with next to no advantage (except for ease of use for some tools that need range queries a lot, mappers might be such a case). The main disadvantage is that the old "magic" was a simple unique key, while in x,y,z only the combination is unique (implied by PRIMARY KEY), and will require slightly more effort for the DBMS to optimize and index. In the "cursed" scheme, by https://www.sqlite.org/rowidtable.html
With (x,y,z), sqlite supposedly creates a lookup table (x,y,z) -> rowid, then retrieves the rowid from the b-tree. Don't try to draw conclusions from 8k blocks. It's too small to make a difference. In many cases, indexes in DBMS will be organized in blocks, the default page size of SQLITE3 is now 4096. |
|
Note that the old tables made bad use of SQLITE, unfortunately. supposedly because it is an Note that this now is a direct Hence I suggest to rather use For xyz, we get The file has 136889 blocks in the old format, on average 228 bytes of data + 8 bytes of integer, but the overall file size is 360 bytes per block, so there is a substantial overhead. VACUUM reduced this to 283. |
|
To convert an existing sqlite to use This one currently has 271 bytes per block, as it does not have that unnecessary extra index. |
|
As the |
|
I don't think that we need to prematurely optimize the block table. I think this change is/was probably worth it for the code quality improvement alone, both in Luanti and for external tools such as luantimapper. Because And furthermore (see #15768 (comment)) some queries (which may be outside of luanti, e.g. luantimapper) may actually benefit from such an index. So even the overall performance tradeoffs aren't all that obvious. In conclusion, I think this PR is/was fine as-is (what do you want to happen?). If there is a significant performance improvement to be made from having a |
|
@sorcerykid I doubt that your current caching approach is that beneficial. It's very costly to maintain such a table. As shown in #15836, the translation of block numbers to x,y,z can be simplified quite a bit (although that branch still needs more tests). Here is an example query: SELECT
pos,
((pos + 0x800800800) & 0xFFF) - 0x800 as x,
(((pos + 0x800800800) >> 12) & 0xFFF) - 0x800 as y,
(((pos + 0x800800800) >> 24) & 0xFFF) - 0x800 as z
FROM blocks where
((pos + 0x800800800) & 0xFFF) - 0x800 >= -3 AND -- minx
((pos + 0x800800800) & 0xFFF) - 0x800 <= 3 AND -- maxx
(((pos + 0x800800800) >> 12) & 0xFFF) - 0x800 >= -5 AND -- miny
(((pos + 0x800800800) >> 12) & 0xFFF) - 0x800 <= 1 AND -- maxy
pos >= (-12 << 24) - 0x800800 AND -- minz
pos <= (-4 << 24) + 0x7FF7FF; -- maxzThis supposedly uses the z coordinate on the index (with INTEGER PRIMARY KEY): respectively (with INT PRIMARY KEY) If your only want the block numbers, the second may even be faster, as the autoindex is more compact, while in the first case it likely will already read the blobs from disk when scanning. It returns matching blocks as far as they exist in the database: (feel welcome to very that the math is correct) |
With the old "cursed" code that emulated python modulo operations I understand the motivation to clean this up very well. But with https://github.com/luanti-org/luanti/pull/15836/files it is not too bad actually. The (x,y,z) may be easier to play around with, but then you still need to understand the binary format if you want to do anything. You can put the SQL range query into the documentation.
As benchmarked by @lhofhansl in #15768 (comment):
That is about the magnitude to expect for INT vs. INTEGER, I guess. Using a rowid directly simply avoids around 3-4 page accesses (that is the depth of the b-tree we'll be seeing, with a fanout of ~250) for the index; but these pages will likely be in the LRU cache anyway. x,y,z may need more comparisons per key, and have a smaller fanout, but it will not make a "huge" difference. Tuning the database block size may very well have more impact. But as you can see from the query I showcased, you can do index-accelerated range queries on the old format, and most likely these will run faster than with the new format. So maybe just keep this in mind in case you update the format again that maybe the old coding wasn't too bad (nor was it as "cursed"); although a scheme that avoided negative numbers would have been more elegant (not needing the |
|
Most of your points have been brought up in some way or another, just wanted to note one thing: Luanti currently does not do anything that could benefit from range queries, it always just goes "give me block (x,y,z) if it exists".
FWIW minetestmapper uses z-descending, x-descending, y-descending order. |
This comment was marked as duplicate.
This comment was marked as duplicate.
|
I highly appreciate this change. Cheers! |

This changes the map database schema for sqlite to be (x, y, z, data) instead of (pos, data).
Advantages:
Disadvantages:
this might actually be minimally slowerTo do
This PR is a Work in Progress
How to test