July 17, 2005
Db copying, merging, defragmenting, and linearizing
Sometimes it is percieved as desireable to linearize or 'defragment' an InfinityDB B-Tree database to increase speed or restore the almost perfect space utilization of a newly created db. (Note all db's use the UTF-8 and ZLib data compression at all times.) While this is not necessary in general, and has not been done in the past by most clients, it can be done with a trivial amount of code, as shown below. All internal free space - blocks on the free list - will be reclaimed, and the variable-length block storage format will allow extremely dense packing of data inside blocks. Linearizing the blocks should increase speed for sequential operations as well.Note that there is almost no 'internal' free space, i.e. per-block overhead, because blocks are stored variable length; the average waste can be as low as a few percent. So, when we refer to free space, we always mean the 'external' free space; we mean the free space external to the block, which constitutes a set of empty blocks on a 'free list' inside the single db file. Free space is always used during any modification before the file is extended.
Significant amounts of free space necessarily occur when a large number of blocks are deleted or when there are a very large number of random updates before a commit. Sequential updates - those affecting Items close together - will only dirty one or a few blocks, so each batch can be considered equivalent to one random update operation. If commits are done reasonably often, free space can be kept well below ten percent, with 5% not uncommon. Committing after every few random updates will keep free space almost non-existent, at the expense of performance, of course, since a commit writes several blocks and forces them out with a FileDescriptor.sync(). The FileDescriptor.sync() is the slowest part, and is the only way any DBMS can be sure data has been written to disk, so that a transaction becomes durable. The commit overhead can be spread out over a resonable number of modifications. The worst-case utilization due to random updates is 50% if every page in the database is dirtied before a commit(). The utilization in this case will go back up to nearly 100% as usual as data is inserted in the future. Note that this free space should be looked at as a proportion of the original uncompressed data, so it may be effectively much smaller.
These characteristics can be understood by considering that all blocks currently containing data in the db are left intact until the moment of commit, when the obsoleted blocks move atomically to the free list. Dirty blocks migrating out of the cache are kept in the free space until they become valid data atomically at commit().
Here is the simple code for copying an entire db:
void copyDb(String source, String dest) {
InfinityDB sourceDb = InfinityDB.open(source, false); // false=readonly
InfinityDB destDb = InfinityDB.create(dest, true); // true=overWrite
Cu cu = Cu.alloc(); // get a cursor from the free list of cursors
try {
while (sourceDb.next(cu)) { // get the next Item from sourceDb into the cursor
destDb.insert(cu); // put the Item into destDb
}
} catch (IOException e) {
e.printStackTrace();
}
cu.dispose(); // optional, for speed
sourceDb.close(); // release FD to be nice, free the cache
destDb.close(); // this also commits
}
This code will also merge sourceDb into destDb if destDb is not empty (use open(), not create()).
Because the InfinityDB B-Tree is fully Thread-safe and concurrent, it would also be easy to experiment with a multi-Thread version for use on SMP or RAID systems. The file might be partitioned by starting Each Thread's Cu at a different place, for example. Tell us if you do this - we'll report the results here.
July 10, 2005
RDBMS Tables vs Entity-Attribute-Value Modeling
Question: Being a relational db guy, I'm still trying to understand the B-Tree Model in practical use. For example, say I have several factories, each with several machines. I want to write an app to periodically collect quality metrics from each machine at each factory and store it with a timestamp. Here's what an RDBMS table might look like:
create table metrics {
when timestamp,
factoryId varchar(8),
machineId varchar(8),
metric integer
}
I'd probably create indexes on when and a compound index on factoryId And machineId to support my lookup SQL: select metric, when from metrics where factoryId='Atlanta' and machineId='lathe2' where when between Our Answer:
Please read ItemSpace Data Structures.html or ItemSpace DataStructures.pdf for the basics on the Entity-Attribute-Value model we will use here. You could also structure this as a relational system, but EAV is more fun. You would define an EntityClass, which is just the EAV name for a table:
static final EntityClass METRICS = new EntityClass(0); // the 'table'
and an Attribute, which is just the EAV name for a column:
static final Attribute METRIC = new Attribute(0); // the 'column'
As you know, to insert the data for a metric report or get results, you need considerable
JDBC code to bind the parameters and get the results and so on. The InfinityDB code
below is all there is. Here's the InfinityDB 'INSERT' statement:
static void insertMetricReport(String factoryId,
String machineId, Date date, long metric) throws IOException {
Cu cu = Cu.alloc(); // get a work cursor temporarily
// create a new METRIC report entry if necessary, identified by
// factoryId, machine id, and date and attach the metric to it.
cu.append(METRICS) // the EntityClass component
.append(factoryId) // next three components are the composite key
.append(machineId)
.append(date)
.append(METRIC) // the Attibute component is like a column name
.append(metric); // the Value component
db.insert(cu); // put all of them into db in one atomic action
Cu.dispose(cu); // Give back cursor for speed. no strictly necessary
}
Then the 'SELECT' iterates over the report dates. This is almost entirely
cookie-cutter code and follows the design patterns given in
ItemSpace Data Structures.html.
The cursor (a 'Cu') is initialized with the known part of the key, which are
the METRICS EntityClass, the factoryId and the MachineId. While iterating,
these components are protected, and only the components later in the Cu
change, including the date and other information.
void selectReports(String factoryId, String machineId,
Date startDate) throws IOException {
Cu cu = Cu.alloc()
.append(METRICS)
.append(factoryId)
.append(machineId);
int protectedPrefixLength = cu.length(); // The current contents of the Cu is protected
cu.append(startDate); // start the scan here. This part is not protected, so it changes
while (db.next(cu, protectedPrefixLength)) { // iterate over report dates
printMetric(factoryId, machineId, cu.dateAt(protectedPrefixLength));
// This cookie-cutter code allows iteration over a part of an Entity.
// More often, this is unnecessary because we iterate over multi-value Attributes.
cu.setLength(cu.skipComponent(protectedPrefixLength));
cu.incrementSuffix(protectedPrefixLength);
}
cu.dispose();
}
And now we print each report:
void printMetric(String factoryId, String machineID, Date reportDate) throws IOException {
Cu cu = Cu.alloc()
.append(METRICS)
.append(factoryId)
.append(machineID)
.append(reportDate)
.append(METRIC);
int protectedPrefix = cu.length();
if (db.next(cu, protectedPrefix)) { // got a report
System.out.print("date=" + reportDate + " metric=" + cu.longAt(protectedPrefix));
}
cu.dispose();
}
This can actually be done faster and with less code by inlining printMetric() but this
is hopefully clearer.