Multi-Table MariaDB Database Partitioning Solution

Viewed 40

Looking for some guidance on how to best tackle partitioning on some database tables for the purpose of archiving/deleting data over a certain age. The main reason for this is to resolve some issues in database size.

You can think of the data akin to telemetry data where is is growing over time, but once it enters the database it doesn't change outside of the first 10-15 minutes in the event there is any form of conflicting data that requires the application to update a recent record (max 15 mins).

Current database size is approaching 500GB and is sitting on NVMe storage across a 3x Node Galera cluster in three cities. Backups are becoming increasingly larger and if an SST is needed between nodes this can take a couple of hours to complete which is less than ideal.

The plan to deal with this is by way of archiving, where we plan to off-board historical data to another server (say once a year) with slower storage that can then be backed up once and won't change for 12 months. The historical data will be rarely accessed, and in the event it is our application will handle querying the archive server if older than a certain date instead of the production servers that are relied on heavily for "recent" data.

We have 3x tables per customer, and they reference each other in a sort of heirarchy. There are no foreign keys in the tables, but they do hold references to one another and are used in JOIN queries. Eg. summary table is the top of the hierarchy and holds one record per "event". Under this is the details table and there could be 1-10 detail records sitting under the summary event. Under details is the digits table that could include 0-10 records per detailed record.

CREATE TABLE data below;

CREATE TABLE `summary_X` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `start_utc` datetime DEFAULT NULL,
  `end_utc` datetime DEFAULT NULL,
  `total_duration` smallint(6) DEFAULT NULL,
  `legs` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `start_utc` (`start_utc`)
) ENGINE=InnoDB

CREATE TABLE `details_X` (
  `xid` bigint(20) NOT NULL AUTO_INCREMENT,
  `id` int(11) NOT NULL,
  `duration` smallint(6) DEFAULT NULL,
  `start_utc` timestamp NULL DEFAULT NULL,
  `end_utc` timestamp NULL DEFAULT NULL,
  `event` varchar(2) DEFAULT NULL,
  `event_time` smallint(6) DEFAULT NULL,
  `event_a` varchar(7) DEFAULT NULL,
  `event_b` varchar(7) DEFAULT NULL,
  `ani` varchar(20) DEFAULT NULL,
  `dnis` varchar(10) DEFAULT NULL,
  `first_time` varchar(30) DEFAULT NULL,
  `final_time` varchar(30) DEFAULT NULL,
  `digits_count` int(2) DEFAULT 0,
  `sys_a` varchar(3) DEFAULT NULL,
  `sys_b` varchar(3) DEFAULT NULL,
  `log_id_a` varchar(12) DEFAULT NULL,
  `seq_a` varchar(1) DEFAULT NULL,
  `log_id_b` varchar(12) DEFAULT NULL,
  `seq_b` varchar(1) DEFAULT NULL,
  `assoc_log_id_a` varchar(12) DEFAULT NULL,
  `assoc_log_id_b` varchar(12) DEFAULT NULL,
  PRIMARY KEY (`xid`),
  KEY `start_utc` (`start_utc`),
  KEY `end_utc` (`end_utc`),
  KEY `event_a` (`event_a`),
  KEY `event_b` (`event_b`),
  KEY `id` (`id`),
  KEY `final_digits` (`final_digits`),
  KEY `log_id_a` (`log_id_a`),
  KEY `log_id_b` (`log_id_b`)
) ENGINE=InnoDB

CREATE TABLE `digits_X` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `leg_id` bigint(20) DEFAULT NULL,
  `sequence` int(2) NOT NULL,
  `digits` varchar(30) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `digits` (`digits`),
  KEY `leg_id` (`leg_id`)
) ENGINE=InnoDB

My first thought was to partition on Year, sounds easy enough but we don't have a date column on the digits table, so records here could be orphaned away from their mapped details record and no longer match in a JOIN on the archive server. We then can also have a similar issue with summary and the timestamps on the "details" records could span multiple years. Eg. Summary event starts at 2021-12-31 23:55:00. First detail record is same timestamp, and then the next detail under the same event could be 2022-01-01 00:11:00. If 2021 partition was archived off to the other server, the 2022 detail would be orphaned and no longer JOIN to the 2021 summary event.

One alternative could be not to partition at all and do SELECT/INSERT/DELETE which isn't practical with the volume of data. Some tables have 30M-40M rows per year so this would be very resource taxing. There are also 400+ customers each with their own sets of tables.

Another I thought of was to add a column to the three tables as a "Year" column we can partition on but would include the Year of first event across all, so all related records can be on the same partitions/server, but this seems like a waste of space and there should be a better way.

Any thoughts or guidance would be appreciated.

1 Answers

To add PARTITIONing will require copying the entire table over. That will involve downtime and disk space. If you can live with that, then...

PARTITION BY RANGE(...) where the expression involves, say, TO_DAYS(...) or possibly TO_SECONDS(...). Then set up cron jobs to add a new partition periodically. (There is nothing automated for such.) And to detach the oldest partition. See Partition for a discussion of the details. (TO_DAYS avoids the need for a 'year' column.)

Note that Partitioning is implemented as several sub-tables under a table. With "transportable tablespaces", you can detach a partition from the big table, turning it into a table unto itself. At that point, you are free to move it to another server of something.

In a situation like yours, I might consider the following.

  • Write the raw data to a file (perhaps one per day) for archiving;
  • Insert into a table that will live only briefly; this will be purged by some means frequently;
  • Update "normalization" tables
  • "Summarize" the data into Summary Tables, where each set of rows covers one hour (or whatever makes sense).
  • Write "reports" from the summary table(s).

Be aware that each Partition takes an extra 5.5MB (average), so do not make many partitions. Or do you need only 2, each containing 15 minutes' data?

Meanwhile, I would look carefully at the schema. Can an INT (4 bytes) be turned into a SMALLINT (2 bytes). Can more things be Normalized.

digits_count int(2) -- that is a 4-byte INT; the (2) has no meaning and has been removed in MySQL 8. (MariaDB may follow suit someday.) It sounds like you need only a 1-byte TINYINT UNSIGNED (range: 0..255).

Since this is log info, be aware of Daylight Savings wrt DATETIME. (One hour per year is missing; another hour repeats.) This problem does not occur with TIMESTAMP. Each one takes 5 bytes (unless you include fractional seconds.)

(I can't advise on unnecessary indexes without seeing the queries.) SHOW TABLE STATUS will tell you how much space is being consumed by all the indexes.

Are the 3 tables of similar size?

Re "orphaning" -- You need at least 2 partitions -- one being filled (0-100% full) and an older partition (100% full)

"30M-40M rows per year" times 400 customers. Does that add up to 500 rows inserted per second? Are they INSERTed one row at a time? High speed ingestion

Are there more deletes and selects than inserts? And/or do they involve more than single rows? (I'm fishing for more info go help with some other issues you either have or are threatening to have.) Even with Deletes and no Partitioning, the disk growth will slow down as free space is generated, then reused. ("Rince and repeat.")

Without partitioning, see Huge Deletes . But... DELETEing data from a table does not shrink it disk footprint. However if each 'customer' has 1/400th of the data; and (of course) you do each customer separately, then there may not be any disk problem

I've given you a lot to think about. Answer some of my questions; I may have more advice.

Related