Thursday 27 March 2014

caches

I've looked through the code of MariaDB/MySQL and tried to understand what the program does and how it does this. And (quite naturally) I was sure that I could do better. So I thought of a better approach, a better algorithm, made some drawings on paper, wrote some lines of pseudo-code on paper and then implemented my own code.

And then: I tested my approach and compared it to the results of the original code. Well, what do I say, hmmmm, ..... (please look at the little drawing at the bottom of this page)

OK, I say it: the original code was faster than my code.

There could be only one reason for this: the server caches a lot of the data (and my code didn't).

So how is this caching done by MariaDB/MySQL?

what is caching

You can find a good description of caching on the Wikipedia.
In a few words: if you connect a fast device with a slow one a cache can help speedup some operations. And in our case the fast device is the CPU and the slow device is the harddisk.

OS

Caching is used quite often. The operating system does a lot of caching by itself. I don't want to describe this and I don't want to play with this, I only want to show the effect of caching done by the MySQL/MariaDB-server.

cash

If you look through the code of MySQL and MariaDB you will see the words cache and cash appear, they are used interchangeably. Don't let this confuse you, it's a programmers fun. Newer versions of the software replace the word cash with the word cache (the correct word).

environment

I did all my tests with the MyISAM-engine. In caching data each engine can do whatever it wants so the results may differ between the engines.

My machine here is equipped with 32 GB of RAM. If youre machine is equipped with a different amount of RAM then your results may differ a bit.

types

The MyISAM-engine supports 3 types of tabls: static (fixed-length records), dynamic (variable length records) and packed. I tested only static and dynamic.

standard

Let's start with the standard-approach:
MariaDB [TestOK]> create database TestCache;
Query OK, 1 row affected (0.00 sec)

MariaDB [TestOK]> use TestCache;
Database changed
MariaDB [TestCache]>  create table TestStat (                                                                                                                                                                             
    ->                 Id   char(8) not null,
    ->                 PZN  char(7) not null,
    ->                 EVP  char(8) not null,
    ->                 HAP  char(8) not null,
    ->                 ArtikelBez char(40) not null,
    ->                 ArtikelText char(26) not null,
    ->                 Hersteller  char(5) not null)
    ->                 engine = MyIsam
    -> ;
Query OK, 0 rows affected (0.05 sec)

MariaDB [TestCache]>  create table TestDyn (                                                                                                                                                                             
    ->                 Id   varchar(8) not null,
    ->                 PZN  varchar(7) not null,
    ->                 EVP  varchar(8) not null,
    ->                 HAP  varchar(8) not null,
    ->                 ArtikelBez varchar(40) not null,
    ->                 ArtikelText varchar(26) not null,
    ->                 Hersteller  varchar(5) not null)
    ->                 engine = MyIsam
    -> ;
Query OK, 0 rows affected (0.04 sec)

MariaDB [TestCache]> insert into TestStat select * from TestOK.ABDAOK
    -> ;
Query OK, 10000000 rows affected (34.84 sec)
Records: 10000000  Duplicates: 0  Warnings: 0

MariaDB [TestCache]> insert into TestDyn select * from TestOK.ABDAOK
    -> ;
Query OK, 10000000 rows affected (46.90 sec)
Records: 10000000  Duplicates: 0  Warnings: 0

MariaDB [TestCache]> 
As you can see I created a database and in this database I created 2 tables. The tables are identical, except that the table TestStat is of type static (=fixed length records) whereas the table TestDyn is of type dynamic (=variable length records). Both tables contain 10 Mio. records of identical data. The table TestDyn does not contain any updated data, and each record consist of only one block, so no data is spread around the datafile.

This is how the tables look on the OS-side:
august@AMD4:~/MariaDB/data/TestCache$ ls -l
insgesamt 1880292
-rw-rw---- 1 august august         65 Mär 18 15:47 db.opt
-rw-rw---- 1 august august        685 Mär 18 15:50 TestDyn.frm
-rw-rw---- 1 august august  895389376 Mär 18 15:53 TestDyn.MYD
-rw-rw---- 1 august august       1024 Mär 18 16:15 TestDyn.MYI
-rw-rw---- 1 august august        679 Mär 18 15:50 TestStat.frm
-rw-rw---- 1 august august 1030000000 Mär 18 15:51 TestStat.MYD
-rw-rw---- 1 august august       1024 Mär 18 16:15 TestStat.MYI
august@AMD4:~/MariaDB/data/TestCache$ 
If you add the numbers of the column-widths given in the CREATE-statement you will get 102 as the width of the record. Add to this one byte for a flag, multiply this value with 10 Mio. and ŷou will get exactly the size of the file TestStat.MYD. The file TestDyn.MYD is a bit smaller because the data is not filled up to the full width of each column (justification of using the VARCHAR column-type).

Let's do our first tests. Nothing is changed, these tests are all done with the server out of the box. And everything is a table-scan:
MariaDB [TestCache]> select count(*) from TestStat where Hersteller <> 'ABCDE';
+----------+
| count(*) |
+----------+
| 10000000 |
+----------+
1 row in set (10.63 sec)

MariaDB [TestCache]> select count(*) from TestStat where Hersteller <> 'ABCDF';
+----------+
| count(*) |
+----------+
| 10000000 |
+----------+
1 row in set (10.58 sec)

MariaDB [TestCache]> select count(*) from TestStat where Hersteller <> 'ABCDG';
+----------+
| count(*) |
+----------+
| 10000000 |
+----------+
1 row in set (10.58 sec)

MariaDB [TestCache]> select count(*) from TestDyn where Hersteller <> 'ABCDE';
+----------+
| count(*) |
+----------+
| 10000000 |
+----------+
1 row in set (14.16 sec)

MariaDB [TestCache]> select count(*) from TestDyn where Hersteller <> 'ABCDF';
+----------+
| count(*) |
+----------+
| 10000000 |
+----------+
1 row in set (14.14 sec)

MariaDB [TestCache]> select count(*) from TestDyn where Hersteller <> 'ABCDG';
+----------+
| count(*) |
+----------+
| 10000000 |
+----------+
1 row in set (14.13 sec)

MariaDB [TestCache]> 

All time-values are taken from the frontend mysql as you can see. All tests are done 3 times to compensate for optimizations done by the OS (=caching on the OS-level).

Accessing table TestStat is about 30% faster than accessing the table TestDyn. The reason for this is the more complex structure of the later table. You will find a description of the structure of the MyISAM-tables here. You may also look at this post.

In all future examples of this post I will do the tests 3 times, but I will only present the average value for each test.

into the code

Let's step into the code and look how the server does the read-operation.

All operations that I used in my tests are sequential reads (=table scan) because no other access is possible (no index was created). These operations are executed by the following functions:
  • xxxx::rnd_init() is called at the beginning of the process. it initializes everything needed
  • xxxx::rnd_next() returns one row of the table. this function is called repeatedly until it returns the constant HA_ERR_END_OF_FILE
  • xxxx::rnd_end() is called at the end of the of the read-operation. here you can clean things up
This is valid for all engines, every storage engine must implement these functions or the function of the base class will be executed. So replace xxxx with the class name and look into the corresponding file for the details. You will find more on this here

This is the function hierarchy using MyISAM in the case of fixed-sized records:
ha_myisam.cc  :  ha_myisam::rnd_next()
    mi_scan.c  : mi_scan()
        mi_stat_rec.c  : mi_read_rnd_static_record() 
            mf_iocache.c  : _my_b_read()
                mysql_file.h  : inline_mysql_file_read
                    my_read.c  :  my_read()     reads 128K from the datafile into the buffer

And in the case of variable length records:
ha_myisam.cc  :  ha_myisam::rnd_next()
    mi_scan.c  : mi_scan()
        mi_dynrec.c  :  _mi_read_rnd_dynamic_record()
            mi_cache.c  :  _mi_read_cache()
                mf_iocache.c  :  _my_b_read()
                    mysql_file.h  :  inline_mysql_file_read
                        my_read.c  :  my_read()     reads 128K from the datafile into the buffer
The last function fills the cache and this buffer is used internally by the MyISAM-engine in the next read-operations.

modifications

I want to know how this buffer is used and especially what happens when I change the size of the buffer. So let's modify the code a bit. I will present the places where I added or changed code and mark added/changed lines in bold.

At first: when the cache is unable to satisfy the request the data must be read from the datafile, so let's count how many times the function read() of the C-library is called:
modify the file storage/myisam/ha_mysiam.cc near the top of this file:
extern int counterReadStaticRecord;    
extern int counterReadDynamicReacord;
somewhere in the middle of this file:
int ha_myisam::rnd_init(bool scan)
{
  counterReadStaticRecord = counterReadDynamicReacord = 0;
  if (scan)
    return mi_scan_init(file);
  return mi_reset(file);                        // Free buffers
}

int ha_myisam::rnd_end()
{
  DBUG_ENTER("ha_myisam::rnd_end");
  fprintf(stderr, "no. of reads:\tstatic=%d\tdynamic=%d\n", counterReadStaticRecord, counterReadDynamicReacord);
  ds_mrr.dsmrr_close();
#if !defined(DBUG_OFF) && defined(SQL_SELECT_FIXED_FOR_UPDATE)
  file->update&= ~HA_STATE_AKTIV;               // Forget active row
#endif
  DBUG_RETURN(0);
}

modify the file storage/myisam/mi_statrec.c before the function _mi_read_rnd_static_record():
//##########################################################
// the original code:
//#define my_b_read(info,Buffer,Count) \
//  ((info)->read_pos + (Count) <= (info)->read_end ?\
//   (memcpy(Buffer,(info)->read_pos,(size_t) (Count)), \
//    ((info)->read_pos+=(Count)),0) :\
//   (*(info)->read_function)((info),Buffer,Count))

int counterReadStaticRecord;
static inline int AQ_my_b_read(IO_CACHE *info, uchar *Buffer, ulong Count)
{
     if ( (info)->read_pos + (Count) <= (info)->read_end )
     {
         memcpy(Buffer,(info)->read_pos,(size_t) (Count));
         ((info)->read_pos+=(Count));
         return 0;
     }
     counterReadStaticRecord++;
     return  (*(info)->read_function)((info),Buffer,Count);
}

in the function _mi_read_rnd_static_record():
//   error=my_b_read(&info->rec_cache,(uchar*) buf,share->base.reclength);
/////
    error=AQ_my_b_read(&info->rec_cache,(uchar*) buf,share->base.reclength);
/////

I have to explain a bit what I did here and why:
There is a makro my_b_read, you will find it in include/my_sys.h.
I included it as a comment in the file mi_statrec.c and also duplicated it as a function with a new name: AQ_my_b_read. I hope this function better shows what's going on in this code and it also allows for better debugging. And I added the counter-variable to this inline-function.
Then I replaced the call of the makro my_b_read with the call of my own function AQ_my_b_read.

Let's continue with our modifications, now for the dynamic table.

in storage/myisam/mi_cache.c somewhere near the start of the file add:
int counterReadDynamicReacord;

somewhere in the function _mi_read_cache():
    else
      info->read_pos=info->read_end;   /* All block used */

    counterReadDynamicReacord++;  // <----  add this line
    if (!(*info->read_function)(info,buff,length))
      DBUG_RETURN(0);

why these modifications

A table-scan is done by calling at first the function ha_myisam::rnd_init(). In this function I initialize the counters to 0.
Then the function ha_myisam::rnd_next() ist called until we reached the end of the datafile. Nothing new here.
And finally the function ha_myisam::rnd_end() is called which will report the values of our counter-variables.

In the case of a static table the read-operation is handled in the function AQ_my_b_read in the file mi_statrec.c so here I'm counting.
And for a dynamic table the read-operation is done in the function _mi_read_cache() so I'm counting here.

more modifications

Counting the number of disk-accesses is only a minor point. With thi information we can compare the effect of modifying the size of the buffer. But we didn't modify this size, so let's do it now: in the file sql/sysvars.cc:
static Sys_var_ulong Sys_read_buff_size(   
       "read_buffer_size",
       "Each thread that does a sequential scan allocates a buffer of "
       "this size for each table it scans. If you do many sequential scans, "
       "you may want to increase this value",
       SESSION_VAR(read_buff_size), CMD_LINE(REQUIRED_ARG),
       VALID_RANGE(IO_SIZE*2, INT_MAX32), DEFAULT(128*1024),
       BLOCK_SIZE(IO_SIZE));
as you can see the default value is 128*1024 = 128K (I marked the entry in bold). I can easily change this, recompile and do the tests, but there are restrictions built into the server-code which can lead to the following error-message appearing on start-up:
Sysvar 'read_buffer_size' failed 'min_val <= def_val'
I could try to find the places where these checks happen or look for something else. I decided to modify this entry: in include/my_global.h:
// #define IO_SIZE  4096
#define IO_SIZE   64

now recompile everything (the last modification leads to a recompilation of almost everything) and do the test. The error-message did not appear anymore.

results

Here are the results of my tests. I changed the DEFAULT-value from 128 bytes to 256 bytes to 512 bytes and so on up to 1 GB and did my tests, with both types of tables, and did every test 3times. Here are the results of the tests in graphical form:

If you are interested in the details of my tests please look at this text-file. It contains the raw data of my tests.

conclusion

The default value of these buffers is 128K bytes. If you look at the picture above you will see that this is a good compromise. If you make the buffer smaller everything will need more time. If you increase the size you will not get much improvement (and maybe the request will need more time). And keep in mind that RAM is a finite resource, and you're not alone on this server.
It's a good compromise.


some more information

I collected a lot more of information then the time elapsed for the SELECT. So let me give you some more data.

Let's first check the number of reads.
The values are:
number of reads with different sizes of buffer:
size of            number of reads
 buffer           static      dynamic
   128b          4023438      3497615
   256b          4023438      3497615
   512b          2011719      1748808
     1K          1005860       874404
     2K           502930       437202
     4K           251465       218601
     8K           125733       109301
    16K            62867        54651
    32K            31434        27326
    64K            15717        13663
   128K             7859         6832
   256K             3930         3416
   512K             1965         1708
     1M              983          854
     2M              492          427
     4M              246          214
     8M              123          107
    16M               62           54
    32M               31           27
    64M               16           14
   128M                8            7
   256M                4            4
   512M                2            2
    1G                 1            1

In the last line you see why my testing stopped here: with a buffer-ize of 1GB the whole file is accessed in one read()-call.

In the first lines you will alo see identical numbers for the cache-sizes of 128 and 256 bytes. The reason for this is that with these values a minimal buffer of 256 is used even when it is set to a smaller value.

If you multiply the number of reads with the size of the buffer you will always get a value that is bigger then the size of the corresponding file, so the whole file is read.

And you will also see that doubling the size of the cache halves the number of reads.

support

The support for caching data is already there. In your own storage engine you can use it (or write your own routine which are hopefully better/faster).

Please look at the code in mysys/mf_iocache.c. The buffers are created in the function init_io_cache(). This function is called after a call of ha_myisam::rnd_init(). Here is the hierarchy:
ha_myiam.cc  :  ha_myisam::extra_opt()
    mi_extra.c  : mi_extra()
        mf_iocache.c : init_io_cache()

and the buffers are freed after a call of ha_myisam::rnd_end() in the function end_io_cache(). This is the hierarchy:
ha_myisam.cc  :  ha_myisam::extra()
    mi_extra.c : mi_extra()
        mf_iocache.c : end_io_cache()

functions

Above I've written about functions of the storage-engine that are called in the execution of the SELECT-stmt. Here is a listing of these functions as they are called called by the server:
<TestMax> ha_maxtempl::extra
<TestMax> ha_maxtempl::column_bitmaps_signal
<TestMax> ha_maxtempl::extra
<TestMax> ha_maxtempl::extra
<TestMax> ha_maxtempl::store_lock
<TestMax> ha_maxtempl::external_lock
<TestMax> ha_maxtempl::table_cache_type
<TestMax> ha_maxtempl::register_query_cache_table
<TestMax> ha_maxtempl::table_cache_type
<TestMax> ha_maxtempl::info
<TestMax> ha_maxtempl::info
<TestMax> ha_maxtempl::scan_time
<TestMax> ha_maxtempl::scan_time
<TestMax> ha_maxtempl::rnd_init
<TestMax> ha_maxtempl::extra_opt
<TestMax> ha_maxtempl::rnd_next
<TestMax> ha_maxtempl::rnd_next
<TestMax> ha_maxtempl::rnd_next
<TestMax> ha_maxtempl::rnd_next
<TestMax> ha_maxtempl::rnd_next
<TestMax> ha_maxtempl::rnd_next
<TestMax> ha_maxtempl::rnd_next
<TestMax> ha_maxtempl::rnd_end
<TestMax> ha_maxtempl::extra
<TestMax> ha_maxtempl::external_lock
<TestMax> ha_maxtempl::extra
<TestMax> ha_maxtempl::extra
<TestMax> ha_maxtempl::reset
For this test I created a table TestMax in the database TestCache and inserted 6 records into this table. Then I started the >SELECT-stmt given above. You can see which functions are called, how often they are called and in which order they are called. An example: rnd_next() is called 7 times, once for each of the 6 records in the table and a 7th time, in which the EOF is detected and returned.

The table TestMax uses the storage engine MAXTEMPL.

Sunday 2 March 2014

od

I would like to take you with me for a diving-tour through the data-file of the MyISAM-engine, the MYD-file. You will find this engine in MySQL and MariaDB.

You will find a description of the internals here. I want to add some information to this description and give you some examples.

od is a GNU-tool to dump the contents of a file in octal form (od = octal dump), but I would like to present the data in hexadecimal form.

So let's start.
There are three possible formats for the MYD-file: fixed, dynamic, and packed. As I've never used the packed format I can't say anything about this so I omit it. Let me start with the fixed format.

fixed format

I created a table in the fixed format, inserted 2 records into the table and verified it:
MariaDB [Test]> use Test;
Database changed
MariaDB [Test]> create table TestOD (                                                                                                                                                                             
    ->                 Id   char(8) not null,
    ->                 PZN  char(7) not null,
    ->                 EVP  char(8) not null,
    ->                 HAP  char(8) not null,
    ->                 ArtikelBez char(40) not null,
    ->                 ArtikelText char(26) not null,
    ->                 Hersteller  char(5) not null)
    ->                 engine = MyISAM
    -> ;
Query OK, 0 rows affected (0.04 sec)

MariaDB [Test]> insert into TestOD values( 12345678, 1234567, 1234.67, 123456.78, '12345678901234567890', 
    ->     '123456789012345678901234567890123456', '12345')
    -> ;
Query OK, 1 row affected, 2 warnings (0.00 sec)

MariaDB [Test]> insert into TestOD values( 223456789, 2345678, 2345.78, 234567.89, '234567890123456789012345678901234567890', 
    ->     '234567890123456789012345678901234567', '23456')
    -> ;
Query OK, 1 row affected, 3 warnings (0.00 sec)

MariaDB [Test]> select * from TestOD;
+----------+---------+---------+----------+-----------------------------------------+----------------------------+------------+
| Id       | PZN     | EVP     | HAP      | ArtikelBez                              | ArtikelText                | Hersteller |
+----------+---------+---------+----------+-----------------------------------------+----------------------------+------------+
| 12345678 | 1234567 | 1234.67 | 123456.7 | 12345678901234567890                    | 12345678901234567890123456 | 12345      |
| 22345678 | 2345678 | 2345.78 | 234567.8 | 234567890123456789012345678901234567890 | 23456789012345678901234567 | 23456      |
+----------+---------+---------+----------+-----------------------------------------+----------------------------+------------+
2 rows in set (0.01 sec)

MariaDB [Test]> 
You can see that the table contains no columns of variable length. Now I want to look at the MYD-file:
august@AMD4:~/MariaDB/data/Test$ od -t x1 -Ax TestOD.MYD
000000  ff                                                  // flag
        31 32 33 34 35 36 37 38                             // column Id  
        31 32 33 34 35 36 37                                // column PZN
        31 32 33 34 2e 36 37                                // column EVP   
        20 31 32 33 34 35 36 2e 37                          // column HAP
        31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36     // column ArtikelBez
        37 38 39 30 20 20 20 20 20 20 20 20 20 20 20 20
        20 20 20 20 20 20 20 20 
        31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36     // column ArtikelText 
        37 38 39 30 31 32 33 34 35 36   
        31 32 33 34 35                                      // column Herssteller  
        
000067  ff                                                  // flag
        32 32 33 34 35 36 37 38                             // column Id
        32 33 34 35 36 37 38                                // column PZN
        32 33 34 35 2e 37 38                                // column EVP   
        20 32 33 34 35 36 37 2e 38                          // column HAP  
        32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37     // column ArtikelBez
        38 39 30 31 32 33 34 35 36 37 38 39 30 31 32 33
        34 35 36 37 38 39 30 20  
        32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37     // column ArtikelText  
        38 39 30 31 32 33 34 35 36 37                         
        32 33 34 35 36                                      // column Hersteller
I rearranged the output of the od-command and added some comments to show how to interpret the numbers. And all numbers are in hex.
The data begins with a flag which I would like to skip. After this byte the data is stored in consecutive form, one column after the other. As you can see the value of the column ArtikelBez is not filled up to it's full length witch characters in the INSERT-statement. In the first record it's only 20 bytes long and the server added 20 bytes of space (=0x20), in the second record one byte is missing and the server added one byte of space at the end of this column.

Next I delete the first record in this table:
MariaDB [Test]> delete from TestOD where Id = '12345678';
Query OK, 1 row affected (0.00 sec)

MariaDB [Test]> select * from TestOD;
+----------+---------+---------+----------+-----------------------------------------+----------------------------+------------+
| Id       | PZN     | EVP     | HAP      | ArtikelBez                              | ArtikelText                | Hersteller |
+----------+---------+---------+----------+-----------------------------------------+----------------------------+------------+
| 22345678 | 2345678 | 2345.78 | 234567.8 | 234567890123456789012345678901234567890 | 23456789012345678901234567 | 23456      |
+----------+---------+---------+----------+-----------------------------------------+----------------------------+------------+
1 row in set (0.00 sec)

MariaDB [Test]> 
Here is how this looks like in the MYD-file:
august@AMD4:~/MariaDB/data/Test$ od -t x1 -Ax TestOD.MYD
000000  00                                                  // record is deleted
        ff ff ff ff ff ff                                   // deleted-flags
        37 38 31 32 33 34 35 36 37 31 32 33 34 2e 36 37     // garbage
        20 31 32 33 34 35 36 2e 37 31 32 33 34 35 36 37 
        38 39 30 31 32 33 34 35 36 37 38 39 30 20 20 20 
        20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 
        20 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 
        36 37 38 39 30 31 32 33 34 35 36 31 32 33 34 35 
         
000067  ff                                                  // flag 
        32 32 33 34 35 36 37 38                             // column Id
        32 33 34 35 36 37 38                                // column PZN
        32 33 34 35 2e 37 38                                // column EVP   
        20 32 33 34 35 36 37 2e 38                          // column HAP  
        32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37     // column ArtikelBez
        38 39 30 31 32 33 34 35 36 37 38 39 30 31 32 33
        34 35 36 37 38 39 30 31   
        32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37     // column ArtikelText  
        38 39 30 31 32 33 34 35 36 37                         
        32 33 34 35 36                                      // column Hersteller
enough written about this type of format. I want to switch to the other format.

moving to dynamic

Next I want to change one of the columns to one of variable length:
MariaDB [Test]> drop table TestOD;
Query OK, 0 rows affected (0.00 sec)

MariaDB [Test]> create table TestOD (                                                                                                                                                                             
    ->                 Id   char(8) not null,
    ->                 PZN  char(7) not null,
    ->                 EVP  char(8) not null,
    ->                 HAP  char(8) not null,
    ->                 ArtikelBez varchar(40) not null,
    ->                 ArtikelText char(26) not null,
    ->                 Hersteller  char(5) not null)
    ->                 engine = MyISAM
    -> ;
Query OK, 0 rows affected (0.03 sec)

MariaDB [Test]> insert into TestOD values( 12345678, 123, 12.34, 123456.78, '123456789012345', 
    ->     '12345678901234567890', '12')
    -> ;
Query OK, 1 row affected, 1 warning (0.01 sec)

MariaDB [Test]> insert into TestOD values( 23456789, 234, 234.56, 234567.89, '2345678901234567890', 
    ->     '23456789012345678901', '23456')
    -> ;
Query OK, 1 row affected, 1 warning (0.00 sec)

MariaDB [Test]> select * from TestOD;
+----------+-----+--------+----------+---------------------+----------------------+------------+
| Id       | PZN | EVP    | HAP      | ArtikelBez          | ArtikelText          | Hersteller |
+----------+-----+--------+----------+---------------------+----------------------+------------+
| 12345678 | 123 | 12.34  | 123456.7 | 123456789012345     | 12345678901234567890 | 12         |
| 23456789 | 234 | 234.56 | 234567.8 | 2345678901234567890 | 23456789012345678901 | 23456      |
+----------+-----+--------+----------+---------------------+----------------------+------------+
2 rows in set (0.00 sec)

MariaDB [Test]> desc TestOD;
+-------------+-------------+------+-----+---------+-------+
| Field       | Type        | Null | Key | Default | Extra |
+-------------+-------------+------+-----+---------+-------+
| Id          | char(8)     | NO   |     | NULL    |       |
| PZN         | char(7)     | NO   |     | NULL    |       |
| EVP         | char(8)     | NO   |     | NULL    |       |
| HAP         | char(8)     | NO   |     | NULL    |       |
| ArtikelBez  | varchar(40) | NO   |     | NULL    |       |
| ArtikelText | char(26)    | NO   |     | NULL    |       |
| Hersteller  | char(5)     | NO   |     | NULL    |       |
+-------------+-------------+------+-----+---------+-------+
7 rows in set (0.01 sec)

MariaDB [Test]>
as you can see the table-definition is identical to the first example except that the column ArtikelBez iss now of type VARCHAR. So this must be a table of type dynamic. What does the data-file look like in hex:
august@AMD4:~/MariaDB/data/Test$ od -t x1 -a -Ax TestOD.MYD
// block #1:
0000000 03                                                  // block-type
        00 43                                               // actual record-length
        01                                                  // no. of filler-bytes at the end of this block
        36                                                  // flags
        31 32 33 34 35 36 37 38                             // column Id as CHAR
        03 31 32 33                                         // column PZN as VARCHAR
        05 31 32 2e 33 34                                   // column EVP as VARCHAR
        31 32 33 34 35 36 2e 37                             // column HAP as CHAR
        0f 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35     // column ArtikelBez as VARCHAR
        14 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36  // column ArtikelText as VARCHAR
           37 38 39 30  
        02 31 32                                            // column Hersteller as VARCHAR
        00                                                  // filler-bytes
// block #2:
000048 03                                                   // block-type
       00 4a                                                // actual record-length
       02                                                   // no. of filler-bytes at the end of this block
       16                                                   // flags
       32 33 34 35 36 37 38 39                              // column Id as CHAR
       03 32 33 34                                          // column PZN as VARCHAR
       06 32 33 34 2e 35 36                                 // column EVP as VARCHAR
       32 33 34 35 36 37 2e 38                              // column HAP as CHAR
       13 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36      // column ArtikelBez as VARCHAR
          37 38 39 30
       14 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36      // column ArtikelText as VARCHAR 
          37 38 39 30 
       31 32 33 34 35 36                                    // column Hersteller as CHAR
       00 00                                                // filler-bytes 
So you see something we didn't intend: some of the CHAR-columns are treated as VARCHAR, e.g. PZN or ArtikelText. In the case of at least 1 column of a type with varying length MySQL/MariaDB will treat everything as a type with varying length in the data-file. And the VARCHAR-data begins with a length-byte followed by the data.

So let's do an UPDATE on the first record:
MariaDB [Test]> update TestOD set ArtikelBez = 'ABCDEFGHIJKLMOPQRSTUVWXYZ' where Id = '12345678';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

MariaDB [Test]> select * from TestOD;
+----------+-----+--------+----------+---------------------------+----------------------+------------+
| Id       | PZN | EVP    | HAP      | ArtikelBez                | ArtikelText          | Hersteller |
+----------+-----+--------+----------+---------------------------+----------------------+------------+
| 12345678 | 123 | 12.34  | 123456.7 | ABCDEFGHIJKLMOPQRSTUVWXYZ | 12345678901234567890 | 12         |
| 23456789 | 234 | 234.56 | 234567.8 | 2345678901234567890       | 23456789012345678901 | 23456      |
+----------+-----+--------+----------+---------------------------+----------------------+------------+
2 rows in set (0.00 sec)

MariaDB [Test]>
Everything looks fine, o let's look into the data-file:
august@AMD4:~/MariaDB/data/Test$ od -t x1 -Ax TestOD.MYD
// block #1: the first record, but only partial
000000 05                                                   // block-type
       00 4d                                                // actual record-length
       00 3b                                                // length of this block 
       00 00 00 00 00 00 00 98                              // position of next block
       36                                                   // flags
       31 32 33 34 35 36 37 38                              // column Id as CHAR 
       03 31 32 33                                          // column PZN as VARCHAR
       05 31 32 2e 33 34                                    // column EVP as VARCHAR
       31 32 33 34 35 36 2e 37                              // column HAP as CHAR
       19 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4f 50      // column ArtikelBez as VARCHAR
          51 52 53 54 55 56 57 58 59 5a
       14 31 32 33 34 35                                    // column ArtikelText as VARCHAR (partial)
       
// block #2: the second record
000048 03                                                   // block-type
       00 4a                                                // actual record-length
       02                                                   // no. of filler-bytes at the end of this block
       16                                                   // flags
       32 33 34 35 36 37 38 39                              // column Id as CHAR
       03 32 33 34                                          // column PZN as VARCHAR
       06 32 33 34 2e 35 36                                 // column EVP as VARCHAR
       32 33 34 35 36 37 2e 38                              // column HAP as CHAR 
       13 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36      // column ArtikelBez as VARCHAR
          37 38 39 30 
       14 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36      // column ArtikelText as VARCHAR
          37 38 39 30 31 
       32 33 34 35 36                                       // column Hersteller as CHAR
       00 00                                                // filler-bytes
       
// block #3: second block of the first record
000098 09                                                   // block-type
       00 12                                                // len of data in this block
       02                                                   // no. of filler-bytes at the end of this block
       36 37 38 39 30 31 32 33 34 35 36 37 38 39 30         // the missing part of column ArtikelText
       02 31 32                                             // column Hersteller as VARCHAR
       00 00                                                // filler-bytes 
As you have already guessed it the first record has not enough room to hold the full record now: in the old record it contained 20 bytes in the column ArtikelBez, now it must hold 26 bytes in this column. So the server splits the data, writes the first part of the data in the space of the first record and searches for free space for the rest of the data. It finds free space only at the end of the file so it writes the rest of the data here.

blocks

When you look at the hex-values you see that the data-file consists of blocks and every block starts with a number that describes this block. As you can see in the last example this is not always 0x03. In the first example of a dynamic table we found 2 blocks of type 0x03, in the second example we had three blocks with types 0x005, ox03 and 0x09.
Following the block-type you see some values which I want to explain now. Here is a list of all possible blocks:
block-types:
 0             block contains deleted data (=free block)
     3 bytes   len of this block
     8 bytes   ptr. to next free block
     8 bytes   ptr. to previous free block

 1             block contains complete data
     2 bytes   len of this block (=len of data, =len of record)

 2             block contains complete data
     3 bytes   len of this block (=len of data, =len of record)

13             first block of this record
     4 bytes   len of record
     3 bytes   len of this block
     8 bytes   ptr. to next block of this record

 3             block contains complete data
     2 bytes   len of record
     1 byte    no. of filler bytes at end of record
               (len of block = len of record + this byte)

 4             block contains complete data
     3 bytes   len of record
     1 byte    no. of filler bytes at the end of record                
               (len of block = len of record + this byte)

 5             first block of this record
     2 bytes   len of record
     2 bytes   len of block
     8 bytes   ptr. to next block of this record
     
 6             first block of record
     3 bytes   len of record
     3 bytes   len of block
     8 bytes   ptr. to next block of this record
     
 7             last block of record
     2 bytes   len of this block (=len of data, =len of record)
     
 8             last block of this record
     3 bytes   len of this block (=len of data, =len of record)
     
 9             last block of this record
     2 bytes   len of data
     1 byte    no. of filler bytes at the end of record                
               (len of block = len of record + this byte)
               
10             last block of this record
     3 bytes   len of data
     1 byte    no. of filler bytes at the end of record                
               (len of block = len of record + this byte)
                    
11             another block of data
    2 bytes    len of data (=len of block)
    8 bytes    ptr. to next block of this record
    
12             another block of data
    3 bytes    len of data (=len of block)
    8 bytes    ptr. to next block of this record
These lines are taken from the file mi_dynrec.cc, look at the function _mi_get_block_info(). I simplified it a bit.

In the last example the data-file starts with a block of type 0x05 (=first block of this record). After this byte follows the length of this record (2 bytes), then the length of this block (2 bytes), and the ptr. to the next block of this record (ptr. = offset in file). And then the data of this record follows, as long as the block can hold.
Then comes the next block (= next record, unchanged from the previous example). And finally the next block of type 0x09 (=last block of a record) contains the rest of the data of the first record.

As you cans see every block of a data-file of a dynamic table begins with a type followed by some additional information. This header of a block can contain between 3 and 20 bytes.

modifying again

Now we have 2 records which created 3 blocks in the data-file. Let's add another record to this table:
MariaDB [Test]> insert into TestOD values( 34567890, 345, 345.67, 345678.90, '3456789012345678901234567890', 
    ->          '34567890123456789012', '3456')
    -> ;
Query OK, 1 row affected, 1 warning (0.00 sec)

MariaDB [Test]> select * from TestOD;
+----------+-----+--------+----------+------------------------------+----------------------+------------+
| Id       | PZN | EVP    | HAP      | ArtikelBez                   | ArtikelText          | Hersteller |
+----------+-----+--------+----------+------------------------------+----------------------+------------+
| 12345678 | 123 | 12.34  | 123456.7 | ABCDEFGHIJKLMOPQRSTUVWXYZ    | 12345678901234567890 | 12         |
| 23456789 | 234 | 234.56 | 234567.8 | 2345678901234567890          | 23456789012345678901 | 23456      |
| 34567890 | 345 | 345.67 | 345678.9 | 3456789012345678901234567890 | 34567890123456789012 | 3456       |
+----------+-----+--------+----------+------------------------------+----------------------+------------+
3 rows in set (0.00 sec)

MariaDB [Test]>
and here it is in hex:
august@AMD4:~/MariaDB/data/Test$ od -t x1 -Ax TestOD.MYD
// block #1: the firt record, but only partial
000000 05                                                   // block-type
       00 4d                                                // actual record-length
       00 3b                                                // len of blck
       00 00 00 00 00 00 00 98                              // ptr. to next block to record
       36                                                   // flags
       31 32 33 34 35 36 37 38                              // column Id as CHAR
       03 31 32 33                                          // column PZN as VARCHAR
       05 31 32 2e 33 34                                    // column EVP as VARCHAR
       31 32 33 34 35 36 2e 37                              // column HAP as CHAR
       19 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4f 50      // column ArtikelBez as VARCHAR
          51 52 53 54 55 56 57 58 59 5a 
       14 31 32 33 34 35                                    // column ArtikelText as VARCHAR (partial)
       
// block #2: the second record
000048 03                                                   // block-type
       00 4a                                                // actual record-length
       02                                                   // no. of filler-bytes at the end of this block
       16                                                   // flags
       32 33 34 35 36 37 38 39                              // column Id as CHAR
       03 32 33 34                                          // column PZN as VARCHAR
       06 32 33 34 2e 35 36                                 // column EVP as VARCHAR
       32 33 34 35 36 37 2e 38                              // column HAP as CHAR
       13 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36      // column ArtikelBez as VARCHAR
          37 38 39 30 
       14 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36      // column ArtikelText as VARCHAR
          37 38 39 30
       31 32 33 34 35 36                                    // column Hersteller as CHAR
       00 00 

// block #3: first record, second part
000098 09                                                   // block-type
       00 12                                                // actual record-length
       02                                                   // no. of filler-bytes at the end of this block
       36 37 38 39 30 31 32 33 34 35 36 37 38 39 30         // rest of coulmn ArtikelText
       02 31 32                                             // column Hersteller as VARCHAR
       00 00                                                // filler-bytes
       
// block #4: third record
0000b0 03                                                   // block-type
       00 53                                                // actual record-length
       01                                                   // no. of filler-bytes at the end of this block
       16                                                   // flags
       33 34 35 36 37 38 39 30                              // column Id as CHAR
       03 33 34 35                                          // column PZN as VARCHAR
       06 33 34 35 2e 36 37                                 // column EVP as VARCHAR
       33 34 35 36 37 38 2e 39                              // column HAP as CHAR
       1c 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37      // column ArtikelBez as VARCHAR
          38 39 30 31 32 33 34 35 36 37 38 39 30 
       14 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37      // column ArtikelText as VARCHAR
          38 39 30 31 32 
       33 34 35 36 20                                       // column Hersteller as CHAR
       00                                                   // filler-byte

I don't want to go into the details here because a lot of things repeat but I want to show you: our data-file now consists of 4 blocks:
  • block #1 (type = 0x05) contains the first part of the data of the first record
  • block #2 (type = 0x03) contains the data of the second record
  • block #3 (type = 0x09) contains the rest of the data of the first record
  • block #4 (type = 0x03) contains the data of the third record

complications

I want to complicate this situation again: let's do an UPDATE that will make the column ArtikelBez of the first record even longer:
MariaDB [Test]> update TestOD set ArtikelBez = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOOP' where Id = '12345678' ;
Query OK, 1 row affected, 1 warning (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 1

MariaDB [Test]> select * from TestOD;
+----------+-----+--------+----------+------------------------------------------+----------------------+------------+
| Id       | PZN | EVP    | HAP      | ArtikelBez                               | ArtikelText          | Hersteller |
+----------+-----+--------+----------+------------------------------------------+----------------------+------------+
| 12345678 | 123 | 12.34  | 123456.7 | abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN | 12345678901234567890 | 12         |
| 23456789 | 234 | 234.56 | 234567.8 | 2345678901234567890                      | 23456789012345678901 | 23456      |
| 34567890 | 345 | 345.67 | 345678.9 | 3456789012345678901234567890             | 34567890123456789012 | 3456       |
+----------+-----+--------+----------+------------------------------------------+----------------------+------------+
3 rows in set (0.00 sec)

MariaDB [Test]> 
Here is the result in the datafile:
august@AMD4:~/MariaDB/data/Test$ od -t x1 -Ax TestOD.MYD
// block #1: the first record, but only partial
000000 05                                                   // block-type
       00 5c                                                // actual record-length
       00 3b                                                // len of thi block
       00 00 00 00 00 00 00 98                              // ptr. to next block of this record
       36                                                   // flags
       31 32 33 34 35 36 37 38                              // column Id as CHAR
       03 31 32 33                                          // column PZN as VARCHAR
       05 31 32 2e 33 34                                    // column EVP as VARCHAR
       31 32 33 34 35 36 2e 37                              // column HAP as CHAR
       28 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f      // column ArtikelBez as VARCHAR (partial)
          70 71 72 73 74 75 76 77 78 79 7a 41 42 43 44 
          45

// block #2: the second record           
000048 03                                                   // block-type
       00 4a                                                // actual record-length
       02                                                   // no. of filler-bytes at the end of this block
       16                                                   // flags
       32 33 34 35 36 37 38 39                              // column Id as CHAR
       03 32 33 34                                          // column PZN as VARCHAR
       06 32 33 34 2e 35 36                                 // column EVP as VARCHAR
       32 33 34 35 36 37 2e 38                              // column HAP as CHAR
       13 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36      // column ArtikelBez as VARCHAR
          37 38 39 30 
       14 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36      // column ArtikelText as VARCHAR
          37 38 39 30 31 
       32 33 34 35 36                                       // column Hersteller as CHAR
       00 00                                                // filler-bytes
       
// block #3: first record, second part
000098 0b                                                   // block-type
       00 0d                                                // len of data in this block
       00 00 00 00 00 00 01 08                              // ptr. to next block of this record
       46 47 48 49 4a 4b 4c 4d 4e                           // rest of column ArtikelBez
       14 31 32 33                                          // column ArtikelText as VARCHAR (partial)
       
// block #4: third record
0000B0 03                                                   // block-type
       00 53                                                // actual record-length
       01                                                   // no. of filler-bytes at the end of this block
       16                                                   // flags
       33 34 35 36 37 38 39 30                              // column Id as CHAR
       03 33 34 35                                          // column PZN as VARCHAR
       06 33 34 35 2e 36 37                                 // column EVP as VARCHAR
       33 34 35 36 37 38 2e 39                              // column HAP as CHAR
       1c 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37      // column ArtikelBez as VARCHAR
          38 39 30 31 32 33 34 35 36 37 38 39 30 
       14 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37      // column ArtikelText as VARCHAR
          38 39 30 31 32 
       33 34 35 36 20                                       // column Hersteller as CHAR
       00                                                   // filler-byte
        
// block #5: first record, third part
000108 09                                                   // block-type
       00 14                                                // löen of data in this block
       00                                                   // no. of filler-bytes at the end
       34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 39      // rest of column ArtikelText
          30 
       02 31 32                                             // column Hersteller as VARCHAR
Now the data-file consists of 5 blocks:
  • block #1 (type = 0x05) contains the first part of the data of the first record
  • block #2 (type = 0x03) contains the data of the second record
  • block #3 (type = 0x0B) contains the second part of the data of the first record
  • block #4 (type = 0x03) contains the data of the third record
  • block #5 (type = 0x09) contains the last part of the data of the first record
As you can see the data of the first record is broken into the 3 parts and spread around the data-file.

There are some more cases still possible that I didn't show until now, so let's go to another case: DELETE the first record:
MariaDB [Test]> delete from TestOD where Id = '12345678';
Query OK, 1 row affected (0.00 sec)

MariaDB [Test]> select * from TestOD;
+----------+-----+--------+----------+------------------------------+----------------------+------------+
| Id       | PZN | EVP    | HAP      | ArtikelBez                   | ArtikelText          | Hersteller |
+----------+-----+--------+----------+------------------------------+----------------------+------------+
| 23456789 | 234 | 234.56 | 234567.8 | 2345678901234567890          | 23456789012345678901 | 23456      |
| 34567890 | 345 | 345.67 | 345678.9 | 3456789012345678901234567890 | 34567890123456789012 | 3456       |
+----------+-----+--------+----------+------------------------------+----------------------+------------+
2 rows in set (0.01 sec)

MariaDB [Test]>
and here is everything in hex:
august@AMD4:~/MariaDB/data/Test$ od -t x1 -Ax TestOD.MYD
// block #1: first record, deleted block
000000 00                                                   // block-type
       00 00 48                                             // len of this block
       ff ff ff ff ff ff ff ff                              // ptr. to previous free block (no prev. block here)
       00 00 00 00 00 00 00 98                              // ptr. to next free block
       37 38 03 31 32 33 05 31 32 2e 33 34 31 32 33 34      // garbage (=old contents of thi block)
       35 36 2e 37 28 61 62 63 64 65 66 67 68 69 6a 6b 
       6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a 41 
       42 43 44 45 

// block #2: second record 
000048 03                                                   // block-type
       00 4a                                                // actual record-length
       02                                                   // no. of filler-bytes at the end of this block
       16                                                   // flags
       32 33 34 35 36 37 38 39                              // column Id as CHAR 
       03 32 33 34                                          // column PZN as VARCHAR
       06 32 33 34 2e 35 36                                 // column EVP as VARCHAR
       32 33 34 35 36 37 2e 38                              // column HAP as CHAR
       13 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36      // column ArtikelBez as VARCHAR 
          37 38 39 30 
       14 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36      // column ArtikelText as VARCHAR
          37 38 39 30 31 
       32 33 34 35 36                                       // column Hersteller as CHAR 
       00 00                                                // filler-bytes
       
// block #3: next deleted block (= former second block of first record)
000098 00                                                   // block-type
       00 00 18                                             // len of this block
       00 00 00 00 00 00 00 00                              // ptr. to previou fre block 
       00 00 00 00 00 00 01 08                              // ptr. to next free block
       14 31 32 33                                          // garbage
       
// block #4: the third record (same as above)
0000b0 03                                                   // block-type
       00 53                                                // actual record-length
       01                                                   // no. of filler-bytes at the end of this block
       16                                                   // flags
       33 34 35 36 37 38 39 30                              // column Id as CHAR
       03 33 34 35                                          // column PZN as VARCHAR
       06 33 34 35 2e 36 37                                 // column EVP as VARCHAR
       33 34 35 36 37 38 2e 39                              // column HAP as CHAR
       1c 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37      // column ArtikelBez as VARCHAR
          38 39 30 31 32 33 34 35 36 37 38 39 30 
       14 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37      // column ArtikelText as VARCHAR
          38 39 30 31 32 
       33 34 35 36 20                                       // column Hersteller as CHAR 
       00                                                   // filler-byte
        
// block #5: next deleted block (= former third block of first record)
000108 00                                                   // block-type
       00 00 18                                             // len of this block
       00 00 00 00 00 00 00 98                              // ptr. to previou free block
       ff ff ff ff ff ff ff ff                              // ptr. to next free block (no next free block here)
       30 02 31 32                                          // garbage
We still have 5 blocks but the types and the contents of block #1, #3 and #5 changed, the blocks #2 and #4 are unchanged as they contained the second and the third record.
Let's look at the changed blocks: they start with a block-type of 0x00, which marks them as deleted blocks. And as we now have 3 deleted blocks these free block are linked together via pointers following the length-info of this block. These free blocks have forward and backward pointers so they build a double-linked list.

something different

There are a lot of topics I didn't mention here like the interpretation of the filler-bytes or packing adjacent blocks or handling of NULL-values or ....
but I want to switch to another theme: we only had columns of type CHAR or VARCHAR in our test-table. What about the other data-types?

more data-types

Let's create a different table that contains more types than CHAR and VARCHAR:
MariaDB [Test]> drop table TestOD;
Query OK, 0 rows affected (0.00 sec)

MariaDB [Test]> create table TestOD (                                                                                                                                                                             
    ->                 Id   numeric(8,0) not null,
    ->                 PZN  char(7) not null,
    ->                 EVP  decimal(8,2) not null,
    ->                 HAP  decimal(9,2) not null,
    ->                 ArtikelBez varchar(40) not null,
    ->                 Datum date not null,
    ->                 Uhrzeit time not null,
    ->                 DatumZeit  datetime not null,
    ->                 Tstamp timestamp not null,
    ->                 PreciseTime datetime(6) not null)
    ->                 engine=MyISAM
    -> ;
Query OK, 0 rows affected (0.04 sec)

MariaDB [Test]> insert into TestOD values( 12345678, 1234567, 123456.78, 1234567.89, '1234567890123456789012345678901234567890', 
    ->     '2014-02-04', '14:59', '2014-02-04 14:59:00', current_timestamp, current_timestamp)
    -> ;
Query OK, 1 row affected (0.00 sec)

MariaDB [Test]> insert into TestOD values( -12345678, -1234567, -123456.78, -1234567.89, '1234567890123456789012345678901234567890', 
    ->     '2014-02-05', '15:01', '2014-02-05 15:01:00', current_timestamp, '2014-02-05 19:51:17.123456')
    -> ;
Query OK, 1 row affected, 1 warning (0.02 sec)

MariaDB [Test]> select * from TestOD;
+-----------+---------+------------+-------------+------------------------------------------+------------+----------+---------------------+---------------------+----------------------------+
| Id        | PZN     | EVP        | HAP         | ArtikelBez                               | Datum      | Uhrzeit  | DatumZeit           | Tstamp              | PreciseTime                |
+-----------+---------+------------+-------------+------------------------------------------+------------+----------+---------------------+---------------------+----------------------------+
|  12345678 | 1234567 |  123456.78 |  1234567.89 | 1234567890123456789012345678901234567890 | 2014-02-04 | 14:59:00 | 2014-02-04 14:59:00 | 2014-02-26 17:23:10 | 2014-02-26 17:23:10.000000 |
| -12345678 | -123456 | -123456.78 | -1234567.89 | 1234567890123456789012345678901234567890 | 2014-02-05 | 15:01:00 | 2014-02-05 15:01:00 | 2014-02-26 17:23:20 | 2014-02-05 19:51:17.123456 |
+-----------+---------+------------+-------------+------------------------------------------+------------+----------+---------------------+---------------------+----------------------------+
2 rows in set (0.00 sec)

MariaDB [Test]> desc TestOD;
+-------------+--------------+------+-----+-------------------+-----------------------------+
| Field       | Type         | Null | Key | Default           | Extra                       |
+-------------+--------------+------+-----+-------------------+-----------------------------+
| Id          | decimal(8,0) | NO   |     | NULL              |                             |
| PZN         | char(7)      | NO   |     | NULL              |                             |
| EVP         | decimal(8,2) | NO   |     | NULL              |                             |
| HAP         | decimal(9,2) | NO   |     | NULL              |                             |
| ArtikelBez  | varchar(40)  | NO   |     | NULL              |                             |
| Datum       | date         | NO   |     | NULL              |                             |
| Uhrzeit     | time         | NO   |     | NULL              |                             |
| DatumZeit   | datetime     | NO   |     | NULL              |                             |
| Tstamp      | timestamp    | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
| PreciseTime | datetime(6)  | NO   |     | NULL              |                             |
+-------------+--------------+------+-----+-------------------+-----------------------------+
10 rows in set (0.00 sec)

MariaDB [Test]> 
As you can see this table consists of many more data-types. So let's see how this is organized in the data-file:
august@AMD4:~/MariaDB/data/Test$ od -t x1 -Ax TestOD.MYD
000000 03                                                   // block-type
       00 58                                                // actual record-length
       00                                                   // no. of filler-bytes at the end of this block 
       00                                                   // flags 
       80 bc 61 4e                                          // column ID: bit-flag + 0x00bc614e
       31 32 33 34 35 36 37                                 // column PZN as CHAR
       81 e2 40 4e                                          // column EVP: flag + 0x01e240 + 0x4e = '+' + 123456 + 78
       80 12 d6 87 59                                       // column HAP: flag + 0x0012d687 + 0x59 = '+' + 1234567 + 89
       28 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35      // column ArtikelBez as VARCHAR
          36 37 38 39 30 31 32 33 34 35 36 37 38 39 30 
          31 32 33 34 35 36 37 38 39 30 
       44 bc 0f                                             // column Datum: 0x0fbc44 = 1031236d = DD + 32*MM + 16*32*YYYY = 04 + 02*32 + 2014*16*32  
       ec 39 02                                             // column Uhrzeit: 0x0239ec = 145900d = HHMMSS
       ec bc b6 41 51 12 00 00                              // column DatumZeit: 0x125141b6bcec = 20140204145900d = YYYYMMDDHHMMSS
       ee 14 0e 53                                          // column Tstamp = 0x530E14EE = no. of seconds since 1970 (Unix-like)
       01 01 33 a0 34 e7 5b 80                              // column PreciseTime: see separate descriiption

00005C 03                                                   // block-type
       00 58                                                // actual record-length
       00                                                   // no. of filler-bytes at the end of the block
000060 00                                                   // flags
       7f 43 9e b1                                          // column ID: bit-flag + NOT 0x00bc614e
       2d 31 32 33 34 35 36                                 // column PZN as CHAR
       7e 1d bf b1                                          // column EVP: flag + NOT 0x7e1dbf + NOT b1 = '-' + 123456 + 78
000070 7f ed 29 78 a6                                       // column HAP: flag + NOT 0x7fed2978 + NOT a6 = '-' + 1234567 + 89 
       28 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35      // column ArtikelBez as VARCHAR
          36 37 38 39 30 31 32 33 34 35 36 37 38 39 30 
          31 32 33 34 35 36 37 38 39 30 
       45 bc 0f                                             // column Datum: 0x0fbc45 = 1031237d =  DD + 32*MM + 16*32*YYYY = 05 + 02*32 + 2014*16*32
       54 4a 02                                             // column Uhrzeit: 0x024a54 = 150100d = HHMMSS
       94 0f c6 41 51 12 00 00                              // column DatumZeit: 0x125141cd0f94 = 20140205150100d = YYYYMMDDHHMMSS 
       f8 14 0e 53                                          // column Tstamp = 0x530e14f8 = no. of seconds since 1970 (Unix-like)
0000b0 01 01 31 fb d3 f3 39 80                              // column PreciseTime: see separate descriiption
As you can see in the comments I added to every column that the internal representation is a bit tricky sometimes, but I hope the comments explain everything.
Sometimes the information is stored in the form Hi-Lo (e.g. in the Id-column), sometimes the information is stored in the form Lo-Hi (e.g. in the Tstamp-column); even trickier is the information stored in the HAP-column: numbers are grouped by values up to 1 billion (= 9 digits) and stored separately. The column Tstamp contains the value in the form of the Unix-function time(), which returns the number of seconds since 1970/01/01. And the trickiest part is the column PreciseTime. Instead of an explanation I took a famous program and added some code so that it look like this now:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

static void unpack( unsigned long packed)
{
   unsigned long second_part = packed % 1000000ULL;
   packed /= 1000000ULL;
   int second = packed % 60ULL;
   packed /= 60ULL;
   int minute = packed %60ULL;
   packed /= 60ULL;
   int hour = packed % 24UL;
   packed /= 24UL;
   int day = packed % 32UL;
   packed /= 32UL;
   int month = packed % 13UL;
   packed /= 13UL;
   int year = packed;
   printf( "%4.4d-%02.2d-%02.2d %02.2d:%02.2d:%02.2d.%06.6ld\n", year, month, day, hour, minute, second, second_part);
}
int main(void) {
    puts("!!!Hello World!!!"); /* prints !!!Hello World!!! */

//  outputs: 2014-02-26 17:23:10.000000
    unpack( 0x010133a034e75b80 );
//  outputs: 2014-02-05 19:51:17.123456
    unpack( 0x010131fbd3f33980 );;

    return EXIT_SUCCESS;
}
I hope these lines of code show how the information is grouped together in a column of this type.

finally

This is not complete, it's only an overview. But I hope I gave you a good entry for further tests.