skip to Main Content

I have a table that contains a month and a year column.

I have a query which usually looks something like WHERE month=1 AND year=2022

Given how large this table is i would like to make it more efficient using partitions and sub partitions.

table 1

Querying the data i need took around 2 minutes and 30 seconds.

CREATE TABLE `table_1` (
  `id` int NOT NULL AUTO_INCREMENT,
  `entity_id` varchar(36) NOT NULL,
  `entity_type` varchar(36) NOT NULL,
  `score` decimal(4,3) NOT NULL,
  `month` int NOT NULL DEFAULT '0',
  `year` int NOT NULL DEFAULT '0',
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `deleted_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_month_year` (`month`,`year`, `entity_type`)
)

Partitioning by "month"

Querying the data i need took around 21 seconds (big improvement).

CREATE TABLE `table_1` (
  `id` int NOT NULL AUTO_INCREMENT,
  `entity_id` varchar(36) NOT NULL,
  `entity_type` varchar(36) NOT NULL,
  `score` decimal(4,3) NOT NULL,
  `month` int NOT NULL DEFAULT '0',
  `year` int NOT NULL DEFAULT '0',
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `deleted_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`,`month`),
  KEY `idx_month_year` (`month`,`year`, `entity_type`)
) ENGINE=InnoDB AUTO_INCREMENT=21000001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
/*!50100 PARTITION BY LIST (`month`)
(PARTITION p0 VALUES IN (0) ENGINE = InnoDB,
 PARTITION p1 VALUES IN (1) ENGINE = InnoDB,
 PARTITION p2 VALUES IN (2) ENGINE = InnoDB,
 PARTITION p3 VALUES IN (3) ENGINE = InnoDB,
 PARTITION p4 VALUES IN (4) ENGINE = InnoDB,
 PARTITION p5 VALUES IN (5) ENGINE = InnoDB,
 PARTITION p6 VALUES IN (6) ENGINE = InnoDB,
 PARTITION p7 VALUES IN (7) ENGINE = InnoDB,
 PARTITION p8 VALUES IN (8) ENGINE = InnoDB,
 PARTITION p9 VALUES IN (9) ENGINE = InnoDB,
 PARTITION p10 VALUES IN (10) ENGINE = InnoDB,
 PARTITION p11 VALUES IN (11) ENGINE = InnoDB,
 PARTITION p12 VALUES IN (12) ENGINE = InnoDB) */

I would like to see if i can improve the performance even further by partitioning by year and then subpartitioning by month. How can i do that?

I’m not sure the following question Partition by year and sub-partition by month mysql is relevant with no marked answers and that question looks to be particular to mysql 5* and php. Im asking about mysql 8, are there no changes since then regarding partioning/subpartioning/list columns/range columns etc? which could help me.

Broader query im making

SELECT
    table_1.entity_id AS entity_id,
    table_1.entity_type,
    table_1.score
FROM table_1
WHERE table_1.month = 12 AND table_1.year = 2022
AND table_1.score > 0
AND table_1.entity_type IN ('type1', 'type2', 'type3', 'type4') # only ever 4 types usually all 4 are present in the query

2

Answers


    • Splitting a date into columns is usually counterproductive. It is much easier to split during SELECT.

    • PARTITIONing is usually useless for performance of any SELECT.

    • When partitioning (or unpartitioning), the indexes usually need changing.

    For that query, I recommend a combined date column,

    WHERE date >= '2022-01-01'
      AND date  < '2022-01-01' + INTERVAL 1 MONTH
    

    and some INDEX starting with date.

    (You probably have other queries; let’s see some of them; they may need a different index.)

    Covering index — This is an index that contains all the columns found anywhere in the SELECT. It is may be better (faster) than having only the columns needed for WHERE or WHERE + GROUP BY + ORDER BY. It depends on a lot of variables.

    Order of columns in an index (or PK): The leftmost column(s) have priority. That is the order of the index rows on disk. PK(id, date) is useful if looking up by id (in the WHERE), but not if you are just searching by date.

    Sargablesargable — Hiding a column in a function disables the use of an index. That is MONTH(date) cannot use INDEX(date).

    BlogsIndex Cookbook and Partition

    Test plan

    I recommend you time all your queries against a variety of Create Tables.

    For the WHERE clause:

    • The order of ANDs does not matter.
    • When using IN, a single value os equivalent to = and optimizes better. Multiple values may optimize more poorly. As Bill hints at, when the IN list contains all the options, you should eliminate the clause since the Optimizer is not smart enough. So, be sure to test with 1 and/or many items, so as to be realistic to your app.

    For the table

    • Try Partition BY year + Subpartition by month.
    • Try Partition by a column that is the combination of year and month.
    • Try without partitioning.

    For indexes

    • Order of the columns (in a composite index) does matter, so try different orderings.
    • When partitioning, be sure to tack onto the end of the PK the partition key(s).
    • A partitioned table needs different indexes than a non-partitioned table. That is, what works well for one may work poorly for the other.

    Simply use something like this pattern to test various layouts:

    CREATE TABLE (( a new layout with or without partitioning and with indexes ))
    INSERT INTO test_table  SELECT ... FROM real_table;
        Change the "..." to adapt to any extra/missing columns in test_table
    SELECT ...
        Run various 'real' queries
        Run each query twice (caching sometimes messes with the timing)
    

    Report the results — If you provide sufficient info (CREATE TABLE and SELECT), I may have suggestions on further speeding up the test (whether it is partitioned or not).

    Login or Signup to reply.
  1. To answer your question directly, below is example syntax that accomplishes the subpartitioning. Notice the PRIMARY KEY must include all columns used for partitioning or subpartitioning. Read the manual on subpartitioning for more information: https://dev.mysql.com/doc/refman/8.0/en/partitioning-subpartitions.html

    Schema (MySQL v8.0)

    CREATE TABLE `table_1` (
      `id` int NOT NULL AUTO_INCREMENT,
      `entity_id` varchar(36) NOT NULL,
      `entity_type` varchar(36) NOT NULL,
      `score` decimal(4,3) NOT NULL,
      `month` int NOT NULL DEFAULT '0',
      `year` int NOT NULL DEFAULT '0',
      `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
      `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      `deleted_at` timestamp NULL DEFAULT NULL,
      PRIMARY KEY (`id`,`month`, `year`),
      KEY `idx_month_year` (`month`,`year`, `score`, `entity_type`)
    ) ENGINE=InnoDB AUTO_INCREMENT=21000001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
    PARTITION BY LIST (`month`)
    SUBPARTITION BY HASH(`year`)
    SUBPARTITIONS 10 (
      PARTITION p0 VALUES IN (0) ENGINE = InnoDB,
      PARTITION p1 VALUES IN (1) ENGINE = InnoDB,
      PARTITION p2 VALUES IN (2) ENGINE = InnoDB,
      PARTITION p3 VALUES IN (3) ENGINE = InnoDB,
      PARTITION p4 VALUES IN (4) ENGINE = InnoDB,
      PARTITION p5 VALUES IN (5) ENGINE = InnoDB,
      PARTITION p6 VALUES IN (6) ENGINE = InnoDB,
      PARTITION p7 VALUES IN (7) ENGINE = InnoDB,
      PARTITION p8 VALUES IN (8) ENGINE = InnoDB,
      PARTITION p9 VALUES IN (9) ENGINE = InnoDB,
      PARTITION p10 VALUES IN (10) ENGINE = InnoDB,
      PARTITION p11 VALUES IN (11) ENGINE = InnoDB,
      PARTITION p12 VALUES IN (12) ENGINE = InnoDB
    );
    

    Using EXPLAIN on your query reveals that the query references only one subpartition.

    Query #1

    EXPLAIN
    SELECT
      table_1.entity_id AS entity_id,
      table_1.entity_type,
      table_1.score
    FROM table_1
    WHERE table_1.month = 12
      AND table_1.year = 2022
      AND table_1.score > 0
      AND table_1.entity_type IN ('type1', 'type2', 'type3', 'type4');
    
    id select_type table partitions type possible_keys key key_len ref rows filtered Extra
    1 SIMPLE table_1 p12_p12sp2 range idx_month_year idx_month_year 11 1 100 Using index condition

    The partitions field of the EXPLAIN shows that it accesses only partition p12_p12sp2. The year the query references, 2022, modulus the number of subpartitions, 10, will read from the subpartition 2.

    In addition to the partitioning by month and year, it is also helpful to use an index. In this case, I added score to the index so it would filter out rows where score <= 0. The note in the EXPLAIN "Using index condition" shows that it is delegating further filtering on entity_type to the storage engine. Though in your example, you said there are only four values for entity type, and all four are selected, so that condition won’t filter out any rows anyway.


    View on DB Fiddle


    Re your questions in comments below:

    a little bit confused on SUBPARTITIONS 10 , why 10

    It’s just an example. You can choose a different number of subpartitions. Whatever you feel is required to reduce the search as much as you want.

    To be honest, I’ve never encountered a situation that required subpartitioning at all, if the search is also optimized with indexes. So I have no guidance on what is an appropriate number of subpartitions.

    It’s your responsibility to test performance until you are satisfied.

    also bit confusd on the partition name p12_p12sp2 how do i know it selected the partition with year 2022 from looking at that?

    The query has a condition year = 2022.

    There are 10 subpartitions in my example.

    Hash partitioning just uses the integer value to be partitioned, modulus the number of partitions.

    2022 modulus 10 is 2. Hence the partition ending in ...sp2 is the one used.

    I also came across this anothermysqldba.blogspot.com/2014/12/… do you know how yours differs from what it shown here ( bare in mind that blog is from 2014)

    They chose to name the subpartitions. There’s no need to do that.

    would there be any performance difference in having a single date e.g (2022-12-21) instead of sepreate columns month and year.

    That depends on the query, and I’ll leave it to you to test. Any predictions I make won’t be accurate with your data on your server.

    i can also see that you partition by month and subpartition by year, as oppose to partition by year and subpartition by month. can you explain the reasoning?

    Subpartitioning works only if the outer partitions are LIST or RANGE partitions, and the subpartitions are HASH or KEY partitions. This is in the manual page I linked to.

    There are a finite number of months (12). This makes it easy to partition by LIST as you did. You won’t ever need more partitions. If you had partitioned by YEAR as the outer partition, you would have needed to specify year values in the list, and this is a growing set, so you would periodically have to alter the table to extend the list or range to account for new years.

    Whereas when partitioning by HASH for the subpartitioning, the new year values are mapped into the finite set of subpartitions, so it’s okay that it’s not a finite list. You won’t have to alter table to repartition (unless you want to change the number of subpartitions).

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search