Graphing Time series in MySQL

We'll generate a series of sensor recordings from raw sensor readings that can come in at arbitrary times:

Begin by creating a table of recordings:

  CREATE TABLE sensor_readings (
    id int not null auto_increment,
    name varchar(255) not null,
    value int not null,
    `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY (`name`)
  );


  INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor1', 1, '2023-02-10 20:00:00');
  INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor2', 5, '2023-02-11 20:00:00');
  INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor2', 10, '2023-02-11 20:00:00');

  -- Wait some time for next recording

  INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor1', 3, '2023-02-16 20:00:00');
  INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor2', 7, '2023-02-17 20:00:00');


  -- Wait some time for next recording

  INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor1', 15, '2023-02-22 20:00:00');
  INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor2', 18, '2023-02-22 20:00:00');
mysql> select * from sensor_readings;
+----+---------+-------+---------------------+
| id | name    | value | created_at          |
+----+---------+-------+---------------------+
|  1 | sensor1 |     1 | 2023-02-10 20:00:00 |
|  2 | sensor2 |     5 | 2023-02-11 20:00:00 |
|  3 | sensor2 |    10 | 2023-02-11 20:00:00 |
|  4 | sensor1 |     3 | 2023-02-16 20:00:00 |
|  5 | sensor2 |     7 | 2023-02-17 20:00:00 |
|  6 | sensor1 |    15 | 2023-02-22 20:00:00 |
|  7 | sensor2 |    18 | 2023-02-22 20:00:00 |
+----+---------+-------+---------------------+
7 rows in set (0.00 sec)

Now let's graph the data over a specific range:

WITH RECURSIVE date_range as (
      SELECT '2023-01-30' AS start_date, '2023-01-31' as end_date 

        UNION ALL 

      SELECT DATE_ADD(start_date, INTERVAL 1 DAY), DATE_ADD(end_date, INTERVAL 1 DAY) FROM date_range 
      WHERE start_date < '2023-02-25'

    ), distinct_sensors as (

        SELECT date_range.start_date, date_range.end_date, name FROM date_range 
        CROSS JOIN (SELECT DISTINCT(name) as name FROM sensor_readings) T

    ), output as (

        SELECT start_date, end_date, distinct_sensors.name, value, 
               ROW_NUMBER() OVER(PARTITION BY start_date, name) as row_num FROM distinct_sensors
        LEFT JOIN sensor_readings ON 
             sensor_readings.created_at <= distinct_sensors.end_date
         AND sensor_readings.created_at > distinct_sensors.start_date
         AND sensor_readings.name = distinct_sensors.name

    )

    -- SELECT * FROM date_range
    -- SELECT * FROM distinct_sensors
    -- SELECT * FROM output

SELECT start_date, name, IFNULL(value, 0) as value 
FROM output WHERE row_num = 1

;

+------------+---------+-------+
| start_date | name    | value |
+------------+---------+-------+
| 2023-01-30 | sensor1 |     0 |
| 2023-01-30 | sensor2 |     0 |
| 2023-01-31 | sensor1 |     0 |
| 2023-01-31 | sensor2 |     0 |
| 2023-02-01 | sensor1 |     0 |
| 2023-02-01 | sensor2 |     0 |
| 2023-02-02 | sensor1 |     0 |
| 2023-02-02 | sensor2 |     0 |
| 2023-02-03 | sensor1 |     0 |
| 2023-02-03 | sensor2 |     0 |
| 2023-02-04 | sensor1 |     0 |
| 2023-02-04 | sensor2 |     0 |
| 2023-02-05 | sensor1 |     0 |
| 2023-02-05 | sensor2 |     0 |
| 2023-02-06 | sensor1 |     0 |
| 2023-02-06 | sensor2 |     0 |
| 2023-02-07 | sensor1 |     0 |
| 2023-02-07 | sensor2 |     0 |
| 2023-02-08 | sensor1 |     0 |
| 2023-02-08 | sensor2 |     0 |
| 2023-02-09 | sensor1 |     0 |
| 2023-02-09 | sensor2 |     0 |
| 2023-02-10 | sensor1 |     1 |
| 2023-02-10 | sensor2 |     0 |
| 2023-02-11 | sensor1 |     0 |
| 2023-02-11 | sensor2 |     5 |
| 2023-02-12 | sensor1 |     0 |
| 2023-02-12 | sensor2 |     0 |
| 2023-02-13 | sensor1 |     0 |
| 2023-02-13 | sensor2 |     0 |
| 2023-02-14 | sensor1 |     0 |
| 2023-02-14 | sensor2 |     0 |
| 2023-02-15 | sensor1 |     0 |
| 2023-02-15 | sensor2 |     0 |
| 2023-02-16 | sensor1 |     3 |
| 2023-02-16 | sensor2 |     0 |
| 2023-02-17 | sensor1 |     0 |
| 2023-02-17 | sensor2 |     7 |
| 2023-02-18 | sensor1 |     0 |
| 2023-02-18 | sensor2 |     0 |
| 2023-02-19 | sensor1 |     0 |
| 2023-02-19 | sensor2 |     0 |
| 2023-02-20 | sensor1 |     0 |
| 2023-02-20 | sensor2 |     0 |
| 2023-02-21 | sensor1 |     0 |
| 2023-02-21 | sensor2 |     0 |
| 2023-02-22 | sensor1 |    15 |
| 2023-02-22 | sensor2 |    18 |
| 2023-02-23 | sensor1 |     0 |
| 2023-02-23 | sensor2 |     0 |
| 2023-02-24 | sensor1 |     0 |
| 2023-02-24 | sensor2 |     0 |
| 2023-02-25 | sensor1 |     0 |
| 2023-02-25 | sensor2 |     0 |
+------------+---------+-------+
54 rows in set (0.00 sec)

This is problematic if you have tons of sensors and find that data doesn't change often. In these cases, you may find you store excessive amounts of data, in which case you'll have to query the existing data and then decide to store the result if the prior value is different than the new value.

SELECT created_at, name, value FROM sensor_readings WHERE name = 'sensor1' ORDER BY created_at DESC LIMIT 1;
INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor1', 1, '2023-02-10 20:00:00')

You could also create a secondary table that ledgers the time series with only updated rows:

  TRUNCATE TABLE sensor_readings;
  ALTER TABLE sensor_readings ADD UNIQUE KEY (`name`);

  CREATE TABLE sensor_readings_ledger (
    id int not null auto_increment,
    name varchar(255) not null,
    value int not null,
    `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY (`name`)
  );
DELIMITER $$
CREATE TRIGGER ledger_sensor_readings_inserts
AFTER INSERT ON `sensor_readings`
FOR EACH ROW
BEGIN
  INSERT INTO sensor_readings_ledger (name, value, created_at) VALUES (NEW.name, NEW.value, NEW.created_at);
END;$$
DELIMITER ;

DELIMITER $$
CREATE TRIGGER ledger_sensor_readings_updates
AFTER UPDATE ON `sensor_readings`
FOR EACH ROW
BEGIN
  IF (OLD.value != NEW.value) THEN
      INSERT INTO sensor_readings_ledger (name, value, created_at) VALUES (NEW.name, NEW.value, NEW.created_at);
  END IF; 
END;$$
DELIMITER ;


  INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor1', 1, '2023-02-10 20:00:00') ON DUPLICATE KEY UPDATE value = 1, created_at = '2023-02-10 20:00:00';
  INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor2', 5, '2023-02-11 20:00:00') ON DUPLICATE KEY UPDATE value = 5, created_at = '2023-02-11 20:00:00';
  INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor2', 10, '2023-02-11 20:00:00') ON DUPLICATE KEY UPDATE value = 10, created_at = '2023-02-11 20:00:00';

  -- Wait some time for next recording

  INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor1', 3, '2023-02-16 20:00:00') ON DUPLICATE KEY UPDATE value = 3, created_at = '2023-02-16 20:00:00';
  INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor2', 7, '2023-02-17 20:00:00') ON DUPLICATE KEY UPDATE value = 7, created_at = '2023-02-17 20:00:00';


  -- Wait some time for next recording

  INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor1', 15, '2023-02-22 20:00:00') ON DUPLICATE KEY UPDATE value = 15, created_at = '2023-02-22 20:00:00';
  INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor2', 18, '2023-02-22 20:00:00') ON DUPLICATE KEY UPDATE value = 18, created_at = '2023-02-22 20:00:00';

  INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor1', 15, '2023-02-25 20:00:00') ON DUPLICATE KEY UPDATE value = 15, created_at = '2023-02-25 20:00:00';
  INSERT INTO sensor_readings (name, value, created_at) VALUES ('sensor2', 18, '2023-02-25 20:00:00') ON DUPLICATE KEY UPDATE value = 18, created_at = '2023-02-25 20:00:00';


    mysql> select * from sensor_readings;
    +----+---------+-------+---------------------+
    | id | name    | value | created_at          |
    +----+---------+-------+---------------------+
    |  1 | sensor1 |    15 | 2023-02-22 20:00:00 |
    |  2 | sensor2 |    18 | 2023-02-22 20:00:00 |
    +----+---------+-------+---------------------+
    2 rows in set (0.01 sec)

    mysql> select * from sensor_readings_ledger;
    +----+---------+-------+---------------------+
    | id | name    | value | created_at          |
    +----+---------+-------+---------------------+
    |  1 | sensor1 |     1 | 2023-02-10 20:00:00 |
    |  2 | sensor2 |     5 | 2023-02-11 20:00:00 |
    |  3 | sensor2 |    10 | 2023-02-11 20:00:00 |
    |  4 | sensor1 |     3 | 2023-02-16 20:00:00 |
    |  5 | sensor2 |     7 | 2023-02-17 20:00:00 |
    |  6 | sensor1 |    15 | 2023-02-22 20:00:00 |
    |  7 | sensor2 |    18 | 2023-02-22 20:00:00 |
    +----+---------+-------+---------------------+
    7 rows in set (0.00 sec)