{"id":102811,"date":"2024-08-30T00:12:48","date_gmt":"2024-08-30T00:12:48","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=102811"},"modified":"2024-08-30T01:24:36","modified_gmt":"2024-08-30T01:24:36","slug":"mysql-index-overviews-composite-b-tree-indexes","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/mysql-index-overviews-composite-b-tree-indexes\/","title":{"rendered":"MySQL Index Overviews: Composite B-Tree Indexes"},"content":{"rendered":"<p>Indexes in MySQL are one of the primary ways to enhance query performance and they are especially useful when the primary use case of our project refers to reading data stored in a database. <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/the-nuances-of-mysql-indexes\/\">We\u2018ve already told you about the nuanced world of indexes in MySQL<\/a> \u2013 and there we\u2018ve mentioned that MySQL has multiple types of SQL indexes available for you to choose from.<\/p>\n<p>The primary index type available within MySQL is the B-Tree index which we\u2018ve already covered in one of our previous articles. If you know your way around MySQL though, you will certainly be aware of other nuances of indexes, too \u2013 and one of those nuances has to do with the fact that B-Tree indexes can also consist of multiple columns (commonly referred to as composite indexes). In this example, we\u2019re using MariaDB as our DBMS of choice. Percona Server for MySQL and MySQL Server will act identically.<\/p>\n<p>Find the queries recreating the table structure and composite indexes in the appendix, and let\u2019s get started.<\/p>\n<h2>What are Composite Indexes?<\/h2>\n<p>In the database world, composite indexes refer to indexes on multiple columns. Composite indexes are sometimes referred to as multicolumn indexes because that\u2018s precisely what they are \u2013 such indexes are keys on multiple columns, while indexes on the same column are referred to as simple indexes.<\/p>\n<p>Multicolumn indexes have various specifics unique to themselves \u2013 depending on the database management system you find yourself using, you will find that composite indexes can be created on up to 16 or 32 columns, that these kinds of indexes only use B-Tree (MySQL) or GIN, BRIN, and GiST index types (PostgreSQL), and that a multicolumn index can be similar to a sorted array.<\/p>\n<h2>Composite Indexes in MySQL<\/h2>\n<p>Let\u2018s not get too far away from our primary DBMS of interest \u2013 MySQL. As noted before, the specifics of the components within your DBMS depend directly on the database management system you find yourself using \u2013 that\u2018s true for every component including composite indexes.<\/p>\n<p>Composite indexes in MySQL:<\/p>\n<ul>\n<li>May consist of up to 16 columns.<\/li>\n<li>Will be used if your query uses all columns in the composite index or the first X columns in the composite index allowing the query optimizer to use any leftmost prefix of the index to look up rows.<\/li>\n<li>Have a crucially important feature \u2013 the order of columns within the index.<\/li>\n<\/ul>\n<p>Here\u2018s a basic composite index definition:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">CREATE INDEX composite_idx ON demo_table(c1,c2,c3);<\/pre>\n<p>When composite indexes are in use, the order of the columns within the index is a crucial point. One cannot skip columns or read them out of order for the index to be used.<\/p>\n<p>Can you guess what we just did? We\u2018ve created a composite index called <code>composite_idx<\/code> on a table named <code>demo_table<\/code> and created the multicolumn index on three columns \u2013 <code>c1<\/code>, <code>c2<\/code>, and <code>c3<\/code>. Not exactly rocket science, is it?<\/p>\n<p>Such an index definition enables you to have indexed search capabilities on queries that act in three ways:<\/p>\n<ol>\n<li>Queries that make use of the <code>c1<\/code> column<\/li>\n<li>Queries that use both <code>c1<\/code> and <code>c2<\/code> columns<\/li>\n<li>Queries that utilize all three \u2013 <code>c1<\/code>, <code>c2<\/code>, and <code>c3<\/code> \u2013 columns.<\/li>\n<\/ol>\n<p>Interesting, right? Such an interesting behavior applicable to defined multicolumn indexes also means that certain limits are in place \u2013 aside from being able to define up to 16 columns and not more, users employing composite indexes should also be aware of the fact that MySQL can only access or use your composite indexes in order, and MySQL will also stop using the composite index after the first range condition. We\u2019ve provided an example of how everything looks like below.<\/p>\n<p>Let\u2018s try them out! We have a simple table depicting items purchased by customers that internally looks like so:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">DESCRIBE demo_table;<\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"860\" height=\"250\" class=\"wp-image-102812\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-3.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\DESCRIBE demo_table.png\" \/><\/p>\n<p><strong>Image 1 &#8211; DESCRIBE Query on a MariaDB Table without a Composite Index<\/strong><\/p>\n<p>Now, we define a composite index on the columns <code>item_purchased<\/code> and <code>quantity_purchased<\/code>, then inspect (<code>DESCRIBE<\/code>) the table again:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">CREATE INDEX composite_idx ON demo_table(item_purchased,quantity_purchased);<\/pre>\n<p>The output should be similar to:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1037\" height=\"345\" class=\"wp-image-102813\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-4.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\DESCRIBE with Composite Indexes.png\" \/><\/p>\n<p><strong>Image 2 &#8211; DESCRIBE Query on a Table with a Composite Index<\/strong><\/p>\n<p>Great! We now see that we have a key \u2013 an index \u2013 referred to as \u201cMUL\u201d \u2013 a multiple-column, or composite, index. Do note that various database management systems display such indexes differently and MySQL (and its counterparts) will only show \u201cMUL\u201d to the side of the first column that makes up a composite index, but inspecting the table structure in phpMyAdmin (or other SQL clients, for that matter) will show you that your column has multiple indexes more clearly once you inspect the table structure. See the example below:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1306\" height=\"273\" class=\"wp-image-102814\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-5.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\Indexes in phpMyAdmin.png\" \/><\/p>\n<p><strong>Image 3 &#8211; Indexes in phpMyAdmin<\/strong><\/p>\n<p>Now we will dig into the internals of composite indexes, then run a couple of SQL queries to better understand the internals of multicolumn indexes in our MySQL infrastructure.<\/p>\n<h2>Index Order in Composite Indexes and Internals<\/h2>\n<p>Here we come back to the main capability of composite indexes: they are B-Tree indexes, but with a special feature in that they contain multiple columns within one index. As such, the order of columns is crucially important: SQL queries will run the most efficiently when you have a composite index with a column order ranging from the most to least distinct counting from the left. In other words, try to order your columns in such an order that the column that returns the highest amount of distinct rows when queried with the<code> SELECT DISTINCT<\/code> query comes first.<\/p>\n<p>Suppose we have a demo_data table like so with a bunch of data inside of it (running <code>COUNT(*)<\/code> queries in InnoDB can take much longer than in this example if you run bigger data sets \u2013 InnoDB doesn\u2019t store an internal row count inside of itself unlike MyISAM):<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">DESCRIBE demo_data;\n\nSELECT COUNT(*) AS total_rows FROM demo_data;<\/pre>\n<p>The output should be similar to:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"698\" height=\"426\" class=\"wp-image-102815\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-6.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\DESCRIBE demo_data.png\" \/><\/p>\n<p><strong>Image 4 &#8211; The Structure of demo_data Table<\/strong><\/p>\n<p>Let\u2019s now add a composite index spanning all three columns with this query (the <code>USING BTREE<\/code> clause is optional and is the default):<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">ALTER TABLE `your_table` \nADD INDEX `index_name`(columns,separated,by,comma) \n [USING BTREE];<\/pre>\n<p>So for our table:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">ALTER TABLE demo_data \nADD INDEX composite_idx (email, username, ip) \n USING BTREE;<\/pre>\n<p>Executing this you will see the following:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1060\" height=\"95\" class=\"wp-image-102816\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-7.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\Adding Composite Index on demo_data.png\" \/><\/p>\n<p><strong>Image 5 &#8211; Adding a B-Tree Composite Index<\/strong><\/p>\n<p>Let\u2019s now confirm our index has been added on all three columns:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1240\" height=\"278\" class=\"wp-image-102817\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-8.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\Composite Indexes in phpmyAdmin.png\" \/><\/p>\n<p><strong>Image 6 &#8211; A B-Tree Composite Index on a Table<\/strong><\/p>\n<p>Great. Now, take note of the index order \u2013 it\u2019s email, then username, then IP. Not the other way around! We can prove the index order is at play by running a query like <code>SHOW INDEXES<\/code> and observing the <code>Seq_in_index<\/code> column:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SHOW INDEXES FROM demo_data;<\/pre>\n<p>The output will be:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1894\" height=\"402\" class=\"wp-image-102818\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-9.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\SHOW INDEXES FROM demo_data.png\" \/><\/p>\n<p><strong>Image 7 &#8211; SHOW INDEXES in MariaDB<\/strong><\/p>\n<p>This means that our index will be used if we use queries like these (note that we use all columns inside of the composite index):<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SELECT * \nFROM demo_data \nWHERE email = \u2018aero42@demo.com\u2019 \n  AND username = \u2018Demo 3\u2019 \n  AND ip = \u2018127.0.0.2\u2019;<\/pre>\n<p>Output:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1519\" height=\"190\" class=\"wp-image-102819\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-10.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\Using Indexes on demo_data.png\" \/><\/p>\n<p>Image 8 &#8211; Using Composite Indexes Searching for Multiple Columns<\/p>\n<p>Our composite index will also be used if we search solely for the first column from the left-hand side (in this case, email):<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">EXPLAIN SELECT * \n        FROM demo_data \n        WHERE email = \u2018aero42@demo.com\u2019;<\/pre>\n<p>You will see something like the following:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1388\" height=\"185\" class=\"wp-image-102820\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-11.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\EXPLAIN demo_data WHERE email.png\" \/><\/p>\n<p>Image 9 &#8211; Using Composite Indexes Searching for One Column<\/p>\n<p>You may also see a situation where possible_keys may be depicted as NULL, but the key (chosen index) wouldn\u2019t be NULL:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">EXPLAIN SELECT * \n        FROM demo_data \n        WHERE ip = \u2018127.0.0.2\u2019 \n          AND username = \u2018Demo 3\u2019;<\/pre>\n<p>This should return:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1415\" height=\"184\" class=\"wp-image-102821\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-12.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\possible_keys NULL key not null.png\" \/><\/p>\n<p><strong>Image 10 &#8211; NULL or NOT NULL?<\/strong><\/p>\n<p><em>Such situations can happen if MySQL (in this case, MariaDB) thinks that none of the indexes are suitable for a lookup, but all the columns selected by our query are columns of an index<\/em>, which is exactly what\u2019s happening here.<\/p>\n<p>If we had another, non-indexed column, our composite index would still be used if our query would use that column and a column in the composite index:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">EXPLAIN SELECT * \n        FROM demo_data \n        WHERE new_column = \u2018Value 1\u2019 \n          AND email = \u2018good_is_a_man@demo.net\u2019;<\/pre>\n<p>Executing this should return something like:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1501\" height=\"190\" class=\"wp-image-102822\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-13.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\new_column and using index.png\" \/><\/p>\n<p><strong>Image 11 &#8211; Using a Composite Index with a Non-Indexed Column<\/strong><\/p>\n<p>However, if our query wouldn\u2019t use the leftmost prefix in the composite index (this column is email), no index would be used:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">EXPLAIN SELECT * \n        FROM demo_data \n        WHERE new_column = \u2018Value 1\u2019 \n          AND username = \u2018Demo 9\u2019;<\/pre>\n<p>The output:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1154\" height=\"191\" class=\"wp-image-102823\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-14.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\No Composite Index Used on demo_data.png\" \/><\/p>\n<p><strong>Image 12 \u2013 No Usage of Composite Index<\/strong><\/p>\n<h2>Analyzing Composite Indexes in MySQL<\/h2>\n<p>Now that we know the basics of how composite indexes work internally, we can start analyzing them.<\/p>\n<p>The first SQL query we run is a simple SELECT with one column in question:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">EXPLAIN SELECT * \n        FROM demo_table \n        WHERE item_puchased = \u2018Xiaomi Redmi 12 128GB\u2019;<\/pre>\n<p>Executing this, you should see something similar to:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1370\" height=\"180\" class=\"wp-image-102824\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-15.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\EXPLAIN Simple.png\" \/><\/p>\n<p><strong>Image 14 &#8211; EXPLAINing a Simple SELECT Query<\/strong><\/p>\n<p>The output of the EXPLAIN query shows that MariaDB elects to use our composite index. Our index has a key length of 482 bytes and that allows us to determine how many parts of our composite index are used by MySQL. This time, it\u2019s a safe bet that MariaDB uses the item_purchased part of the composite_idx and we can confirm it by swapping the column order with a query like so:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">ALTER TABLE `demo_table2` \n    DROP INDEX `composite_idx`, \n    ADD INDEX `composite_idx` (`quantity_purchased`, \n                         `item_purchased`) USING BTREE;<\/pre>\n<p>Then trying the query again:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">EXPLAIN SELECT * \n        FROM demo_table2 \n        WHERE item_purchased = \u2018%Xiaomi Redmi 12 128GB\u2019;<\/pre>\n<p>Now you will see:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1153\" height=\"182\" class=\"wp-image-102825\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-16.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\EXPLAIN Simple on demo_table2.png\" \/><\/p>\n<p><strong>Image 15 &#8211; The Same SQL Query with a Composite Index in Switched Order<\/strong><\/p>\n<p>See \u2013 no possible keys here! Why? We didn\u2019t use the leftmost prefix of the column that was indexed, we\u2019ve used the rightmost prefix instead. In<\/p>\n<p>This also explains the key length of 482 bytes \u2013 <em>MariaDB stops using the multicolumn index at the first range condition and key_len specifies the number of bytes that are used from the index.<\/em><\/p>\n<p>Let\u2019s try again \u2013 this time we run a different query:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">EXPLAIN SELECT * \n        FROM demo_table \n        WHERE quantity_purchased &gt;= 5;<\/pre>\n<p>This should be the output:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1147\" height=\"185\" class=\"wp-image-102826\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-17.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\EXPLAIN without Using Indexes.png\" \/><\/p>\n<p><strong>Image 16 &#8211; A Query That Doesn&#8217;t Use Indexes<\/strong><\/p>\n<p>This time we see that MySQL didn\u2019t even consider any indexes to be used. To explain this, we need to take a step back and remember the specifics of multicolumn indexes \u2013 <em>such indexes will only be used for finding rows if we use a leftmost prefix of the index<\/em> \u2013 in our case, that\u2019s the item_purchased column. Let\u2019s include the column, then try again:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">EXPLAIN SELECT * \n        FROM demo_table \n        WHERE item_purchased LIKE \u2018iPhone%\u2019 \n          AND quantity_purchased &gt;= 5;<\/pre>\n<p>Now you will see the index has been used:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1363\" height=\"186\" class=\"wp-image-102827\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-18.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\Wildcard - Index Used.png\" \/><\/p>\n<p><strong>Image 17 &#8211; A Query That Uses a Composite Index<\/strong><\/p>\n<p>Woohoo! MariaDB uses our index again. MariaDB can traverse an index backward too and let\u2019s not forget that the usual rules for indexes apply here too \u2013 attentive readers will notice that we have a wildcard only at the end of our search statement because otherwise, no index will be used no matter the order of the columns:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">EXPLAIN SELECT * \n        FROM demo_table \n        WHERE item_purchased LIKE \u2018%iPhone%\u2019 \n          AND quantity_purchased &gt;= 5;<\/pre>\n<p>Now you see:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1273\" height=\"189\" class=\"wp-image-102828\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-19.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\Wildcard - No Index Used.png\" \/><\/p>\n<p><strong>Image 18 &#8211; a Query with a Wildcard<\/strong><\/p>\n<p>A query searching for exact matches, however, would use the composite index:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">EXPLAIN SELECT * \n        FROM demo_table \n        WHERE item_purchased = \u2018iPhone X\u2019 \n          AND quantity_purchased = 5;<\/pre>\n<p>You can see that in the output:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1373\" height=\"185\" class=\"wp-image-102829\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-20.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\Perfect Equality Operator.png\" \/><\/p>\n<p><strong>Image 19 &#8211; a Query Searching for Exact Matches<\/strong><\/p>\n<p>Do note that where we see that MySQL uses the composite index, the length of the key (the index) remains being 482 bytes no matter what we do \u2013 that means that MySQL stops using the multicolumn index at the first range condition and doesn\u2019t use the other part of the index (we defined a composite index on two columns \u2013 item_purchased and quantity_purchased.)<\/p>\n<h3>Composite Indexes, Sorting, and Search Operators<\/h3>\n<p>Everything gets even more interesting when sorting operations are involved \u2013 suppose we have a table like so:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">DESCRIBE people;<\/pre>\n<p>You can see the table information:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"671\" height=\"232\" class=\"wp-image-102830\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-21.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\DESCRIBE people.png\" \/><\/p>\n<p><strong>Image 20 &#8211; Another Example of a Table Using Composite Indexes<\/strong><\/p>\n<p>On this table, we\u2019ve defined a composite index like so:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">CREATE INDEX composite_idx ON people(first_name, last_name);<\/pre>\n<p>We will now check the plans of a couple of SELECT queries with the ORDER BY clause:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">EXPLAIN SELECT * \n        FROM people \n        WHERE first_name = \u2018John\u2019 \n        ORDER BY last_name;\n\nEXPLAIN SELECT * \n        FROM people \n        WHERE first_name = \u2018John\u2019 \n        ORDER BY birthday;\n\nEXPLAIN SELECT * \n        FROM people \n        WHERE birthday = \u2018 1999-01-01\u2019 \n        ORDER BY birthday;<\/pre>\n<p>This will return the following outputs:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1844\" height=\"569\" class=\"wp-image-102831\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/d-misc-redgate-blogs-may-2024-mysql-index-deep-di-22.png\" alt=\"D:\\Misc\\Redgate\\Blogs\\May 2024\\MySQL Index Deep Dive - Composite Indexes\\Images\\EXPLAINS - People.png\" \/><\/p>\n<p><strong>Image 21 \u2013 Exploring Sorting in Composite Indexes<\/strong><\/p>\n<p>Now we\u2018ve run three queries \u2013 attentive readers will notice that the Extra column in all of the result sets is different:<\/p>\n<ol>\n<li>The first result ordered by the <code>last_name<\/code> was <em>using the index condition and using the WHERE clause.<\/em> The first example is using the composite index (composite_idx) because we search for two columns (first_name and last_name) that are both part of a composite index.<\/li>\n<li>The second result ordered by the birthday was <em>using the index condition, using a WHERE clause, and using a filesort operation. Sorting occurs when no applicable index satisfies the ordering of rows.<\/em><\/li>\n<li>The third result searching for a birthday and ordering by birthday was <em>using a WHERE clause and using a filesort operation.<\/em><\/li>\n<\/ol>\n<p>Here\u2018s what that means:<\/p>\n<ul>\n<li><em>Using the index condition<\/em> means that MySQL filters rows by using the index and then uses the table to retrieve the matched rows.<\/li>\n<li><em>Using a WHERE clause<\/em> can have different meanings. Some argue that this piece of information is frequently inconsistent at best, but in this specific case, it most likely means that the expression within a WHERE clause performed and index seek operation.<\/li>\n<li><em>Using a filesort operation<\/em> means that rows in the table were read and sorted during an extra sorting phase during query execution.<\/li>\n<\/ul>\n<p>Aside from the explanation of the query internals, one would benefit from using all of the parts of the composite index. Doing so is remarkably simple and in our case, would look like so:<\/p>\n<pre class=\"lang:none theme:none\">SELECT * \nFROM people \nWHERE first_name = 'Josh' \nORDER BY last_name;<\/pre>\n<p>Pay special attention to the second part of the query straight after the <code>WHERE<\/code> clause \u2013 we search for a<code> first_name<\/code> and <code>ORDER BY last_name<\/code> because our index looks exactly like that:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">CREATE INDEX composite_idx \n     ON people(first_name, last_name);<\/pre>\n<p>To use a composite index with an <code>ORDER BY<\/code> clause, ensure that you search for the leftmost column, then order by the rest with an equality operator.<\/p>\n<p>Do keep in mind that the direction your <code>ORDER BY<\/code> is sorted in also has a say here:<\/p>\n<ul>\n<li>MySQL will avoid a filesort operation if all parts of your composite index are sorted in an ascending (ASC) or descending (DESC) order and it has all of the rows from the equality match. This may seem like a narrow use case, but it\u2019s worth keeping in mind. Remember that queries are tasks composed of smaller tasks \u2013 eliminating any unnecessary smaller task will make the performance of your query faster.<\/li>\n<li>You may not be able to avoid a filesort operation if one part of your composite index is sorted in a different manner than the other (e.g. if one part of your query is sorted in an ASCending order and the other in a DESCending order.) If you want your database to make use of your covering index, make sure that your query criteria specifies that \u2013 e.g. if you have a covering index on two columns, search for one column and the other or search for one column and order by the other, etc.<\/li>\n<li>If you search for something in a way that\u2018s backwards to the way you defined your composite indexes, you may be privy to a backwards index scan where MySQL decides to start searching for something starting from the end of the root node of the B-Tree index.<\/li>\n<\/ul>\n<h2>Key Takeaways and Summary<\/h2>\n<p><em>Composite, or multicolumn, indexes are indexes that are defined on multiple columns.<\/em> To effectively use such indexes, make sure to use the \u201cleftmost prefix\u201c principle: put the column you\u2018re accessing first in the first place, the second \u2013 in the second place, and so on.<\/p>\n<p>An understanding of indexes will take you very far in the database world and beyond. The understanding of multicolumn indexes in your specific database management system of choice \u2013 in our case, MySQL and its counterparts like MariaDB Server and Percona Server \u2013 will significantly improve the performance of SQL queries that use filtering or sorting operations on columns.<\/p>\n<p>Before embarking on any kind of index within your database management system, make sure to choose the proper index type <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/\">after educating yourself on the Simple Talk site<\/a>, educate yourself on their upsides and downsides, and most importantly \u2013 use indexes wisely. Use the index, Luke! Use indexes wisely and until next time.<\/p>\n<h2>Appendix \u2013 Table Structure and Demo Data<\/h2>\n<p>In case you want to create the tables and indexes in this blog, the table structure and all demo data is available <a href=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2024\/06\/LukasAppendixIndexP2SampleCreate.txt\">here<\/a>.<\/p>\n<p>To generate a bunch of random data, <a href=\"https:\/\/generatedata.com\/\">take any data set with &gt;100K rows<\/a>, or anonymize if you have live data (this SQL query will update all usernames to one of 10 random values):<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">UPDATE `demo_data` \nSET `username` = ELT(FLOOR(RAND()*10) +1, 'Demo', 'Demo 2', \n                 'Demo 3', 'Demo 4', 'Demo 5', \n                 'Another Demo', 'Demo 7', 'Demo 8', \n                 'Demo 9', 'Demo 10');<\/pre>\n<p>If you\u2019re lazy, you can also use RAND() like so to create quick sample data (this query will set all usernames to random numeric values):<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">UPDATE `demo_data` \nSET `username` = RAND();<\/pre>\n<p>Anonymize email addresses with these queries (replace email domains with different domains):<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">UPDATE `demo_data` SET `email` = REPLACE(`email`, 'gmail.com', 'demo.com');\nUPDATE `demo_data` SET `email` = REPLACE(`email`, 'hotmail.com', 'demo.org');\n--etc<\/pre>\n<p>And anonymize IP addresses like so (these queries will anonymize the first 200,000 IP addresses to 127.0.0.1, and the rest to 127.0.0.2):<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">UPDATE `demo_data` SET ip = \u2018127.0.0.1\u2019 LIMIT [0,]200000;\nUPDATE `demo_data` SET ip = \u2018127.0.0.2\u2019 WHERE ip != \u2018127.0.0.1\u2019;<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Indexes in MySQL are one of the primary ways to enhance query performance and they are especially useful when the primary use case of our project refers to reading data stored in a database. We\u2018ve already told you about the nuanced world of indexes in MySQL \u2013 and there we\u2018ve mentioned that MySQL has multiple&#8230;&hellip;<\/p>\n","protected":false},"author":339547,"featured_media":103671,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[53,145792],"tags":[5854],"coauthors":[146040],"class_list":["post-102811","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-featured","category-mysql","tag-mysql"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/102811","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/339547"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=102811"}],"version-history":[{"count":5,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/102811\/revisions"}],"predecessor-version":[{"id":103672,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/102811\/revisions\/103672"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media\/103671"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=102811"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=102811"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=102811"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=102811"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}