{"id":95637,"date":"2023-01-31T19:48:04","date_gmt":"2023-01-31T19:48:04","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=95637"},"modified":"2023-01-31T19:48:04","modified_gmt":"2023-01-31T19:48:04","slug":"optimizing-queries-in-mysql-optimizing-reads","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/optimizing-queries-in-mysql-optimizing-reads\/","title":{"rendered":"Optimizing Queries in MySQL: Optimizing Reads"},"content":{"rendered":"<p>In this blog, we\u2019re going to walk you through how to solve the issues posed by that concern. At the end of the article, we\u2019ll also walk you through some DDL operations to load test data.<\/p>\n<h2>SELECT Queries in MySQL<\/h2>\n<p>In my <a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/optimizing-mysql-the-basics-of-query-optimization\/\">introduction blog<\/a>, I noted: <em>Queries are processes composed of tasks \u2013 their performance depends directly on the performance of those tasks<\/em>.<\/p>\n<p>To address problems posed by <code>SELECT<\/code> queries, the first thing we need to understand is what happens when a <code>SELECT<\/code> queries are executed. Here\u2019s what MySQL does in the background when we execute a <code>SELECT<\/code> query:<\/p>\n<ol>\n<li>Our SQL client sends the query to the server to be executed.<\/li>\n<li>A specific query execution plan is followed.<\/li>\n<li>The result is returned.<\/li>\n<\/ol>\n<p>Understanding these steps is no less vital than diving deeper into the query execution plan, and the output provided by an <code>EXPLAIN<\/code> statement execution &#8211; a query execution plan will provide us information in regards to what time MySQL takes to go through each step necessary to complete a query. The <code>EXPLAIN<\/code> statement will provide us more details in how MySQL executes the query itself.<\/p>\n<p>For the purposes of this blog, we\u2019re going to use the following table with three columns (it\u2019s not necessary to specify the storage engine \u2013 MySQL will create the table based on the <code>InnoDB<\/code> storage engine by default if it\u2019s not specified. In this specific scenario, the indexing part is not necessary, but if it\u2019s specified, MySQL will create an index named \u201cemail\u201d (indexes can have any names) on a column named \u201cemail.\u201d) Also note the sizes of the email and username columns \u2013 in this case, emails shouldn\u2019t be longer than 35 characters in length, and usernames shouldn\u2019t be longer than 20.<\/p>\n<p><a id=\"post-95637-_heading=h.gjdgxs\"><\/a> It\u2019s also worth noting that for the columns with the type of an <code>integer<\/code> the length of the column is deprecated starting from MySQL 8. So you might as well want to consider leaving the columns with an integer type intact without specifying a length attribute:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">CREATE TABLE demo_table (\r\n  id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\r\n  email VARCHAR(35) NOT NULL,\r\n  username VARCHAR(20) NOT NULL DEFAULT '',\r\n  INDEX email(email)\r\n) ENGINE = InnoDB;<\/pre>\n<p>To insert data into the table, use <code>INSERT<\/code> or <code>LOAD DATA INFILE<\/code> <code>\u2013 INSERT<\/code> is an excellent choice if you\u2019re dealing with smaller data sets (less than 100 million rows), while <code>LOAD DATA INFILE<\/code> can be very useful to work with big data sets. For INSERT, the syntax looks like so:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">--A few rows for demos. At the end of the article is \r\n--a method to load more rows.\r\nINSERT INTO demo_table (email,username) \r\nVALUES ('demo@demo.com', 'Demo'), \r\n('demo2@demo.com', 'Demo'), \r\n('Another@Email.com'), \r\n('AnotherDemo');<\/pre>\n<p>For <code>LOAD DATA INFI<\/code>LE, the syntax looks like this \u2013 the <code>FIELDS TERMINATED BY<\/code>. Part of the query can be used to specify the character that terminates fields from one another (one can also specify what columns to load the data into by specifying them at the end of the query if we wish):<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">LOAD DATA INFILE '\/path\/to\/file.txt' \r\n  INTO TABLE demo_table [FIELDS TERMINATED BY '|'];<\/pre>\n<h2>The Query Execution Plan<\/h2>\n<p>To dive deeper into the query execution plan, we can use the MySQL query profiler or <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/performance-schema-query-profiling.html\">dig into the richer performance schema by following the steps outlined in the MySQL documentation<\/a>. For many, though, using the query schema is more difficult than going through the simpler query, so I will use the profiler:<\/p>\n<ol>\n<li>Select a database, then issue a <code>SET profiling = 1<\/code> query to enable profiling.<\/li>\n<li>Run your query, then issue a <code>SHOW PROFILES<\/code> query to investigate all of the profiles of all queries.<\/li>\n<li>Select the ID of the query you just ran (identify the query from the list provided by the profiler), then execute a <code>SHOW PROFILE FOR QUERY [id]<\/code> query.<\/li>\n<\/ol>\n<p><em>Note: Query profiling with <\/em><code>SET profiling = 1<\/code><em> has been deprecated since MySQL 5.6.7 and people are advised to follow the performance schema instead \u2013 <\/em><a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/performance-schema-query-profiling.html\"><em>the documentation will walk you through all of the necessary steps<\/em><\/a><em>, just note that getting started with it is a little more complex than using the optimizer. <\/em><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"850\" height=\"505\" class=\"wp-image-95638\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/d-redgate-blogs-october-2022-optimiziation-series-2.png\" alt=\"D:\\Redgate\\Blogs\\October 2022\\Optimiziation Series\\The Basics of Query Optimization\\Images\\Query Profiling - 1.png\" \/><\/p>\n<p><em>Image 1 &#8211; Profiling a Query. Initializing<\/em><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"848\" height=\"765\" class=\"wp-image-95639\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/d-redgate-blogs-october-2022-optimiziation-series-3.png\" alt=\"D:\\Redgate\\Blogs\\October 2022\\Optimiziation Series\\The Basics of Query Optimization\\Images\\Query Profiling - 2.png\" \/><\/p>\n<p><em>Image 2 &#8211; Profiling a Query. Results<\/em><\/p>\n<p>The query execution plan is displayed above. Now we need to properly understand what\u2019s it all about, We will start from the top and move toward the bottom (for newer versions of MySQL, the results will look a little different \u2013 there may be more items such as \u201cExecuting hook on transaction\u201d and \u201cwaiting for handler commit\u201d, but the core premise is the same):<\/p>\n<ol>\n<li><code>Starting<\/code> &#8211; The process of initializing the query.<\/li>\n<li><code>Checking permissions<\/code> &#8211; Refers to the process of MySQL checking whether the user has sufficient permissions to run the query. If permissions are not present or they are not sufficient, the process terminates here.<\/li>\n<li><code>Opening tables<\/code> &#8211; The process of MySQL opening all tables for operations. Once the tables are opened or if the tables are already open, MySQL proceeds to step #4.<\/li>\n<li><code>Init<\/code> &#8211; MySQL performs initialization processes \u2013 for example, the InnoDB log and the binary log are being flushed.<\/li>\n<li><code>System lock<\/code> &#8211; Once MySQL reaches this phase, it\u2019s waiting for a system lock on the table to be released if it\u2019s in place. In this phase, the query is making use of a function called <code>mysql_lock_tables()<\/code> and is waiting for its completion \u2013 more information can be found <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/5.7\/en\/general-thread-states.html\">in the documentation<\/a>, in the \u201cSystem lock\u201d paragraph.<\/li>\n<li><code>Optimizing<\/code> &#8211; Internal processes performed by MySQL to determine how to execute the query in the quickest way. Think of math equations \u2013 if you multiply 2 by 0, you can realize the answer is 0 in a couple of ways: by solving the equation, or by realizing that multiplying anything by 0 yields 0. That\u2019s what MySQL does here.<\/li>\n<li><code>Statistics<\/code> &#8211; Calculating statistics-related data to further develop the query execution plan.<\/li>\n<li><code>Preparing<\/code> is pretty much self-explanatory: MySQL is preparing to run the query.<\/li>\n<li><code>Executing<\/code> &#8211; Once the server reaches the execution phase, the query is being executed to start producing rows for output.<\/li>\n<li><code>Sending data<\/code> &#8211; MySQL is doing internal work to execute the query and return the results of that query.<\/li>\n<li><code>End<\/code> &#8211; The end of the query before the cleanup process.<\/li>\n<li><code>Query end<\/code> &#8211; refers to the end of query processing.<\/li>\n<li><code>Closing tables<\/code> &#8211; The tables that were opened and impacted by the query are being closed.<\/li>\n<li><code>Freeing items<\/code> &#8211; Means that MySQL is freeing items from the thread used to execute a query.<\/li>\n<li><code>Cleaning up<\/code> &#8211; Finally, this stage refers to the cleaning up of items in the memory and resetting certain necessary internal variables.<\/li>\n<\/ol>\n<p>For those who use the profiler, the results will be a little different, but the result will be pretty much the same. To employ the profiler, employ the following:<\/p>\n<ol>\n<li>Tell MySQL to enable monitoring for the user that runs the query by running two queries &#8211; the first one will ensure that monitoring is off for everyone, and the second one will enable monitoring for your specific account (replace \u201croot\u201d with your account name):<\/li>\n<\/ol>\n<p><code>UPDATE performance_schema.setup_actors SET ENABLED = 'NO', HISTORY = 'NO' WHERE HOST = '%' AND USER = '%';<br \/>\nINSERT INTO performance_schema.setup_actors (HOST,USER,ROLE,ENABLED,HISTORY) VALUES('localhost','root','%','YES','YES')<\/code>; <\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1424\" height=\"220\" class=\"wp-image-95640\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-3.png\" \/><\/p>\n<ol start=\"2\">\n<li>Enable two things &#8211; logs for statements and statement stages by running these queries:<\/li>\n<\/ol>\n<p><code>UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE NAME LIKE '%events_statements_%';<br \/>\n<\/code> <br \/>\n<code>UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE NAME LIKE '%events_stages_%'<\/code>; <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"1222\" height=\"245\" class=\"wp-image-95641\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-4.png\" \/><\/p>\n<ol start=\"3\">\n<li>Update the setup_<code>instruments<\/code> table with these queries (there are no matches for me because the values are already updated):<br \/>\n<code>UPDATE performance_schema.setup_instruments SET ENABLED = 'YES', TIMED = 'YES' WHERE NAME LIKE '%statement\/%';<\/code><br \/>\n<code>UPDATE performance_schema.setup_instruments SET ENABLED = 'YES', TIMED = 'YES' WHERE NAME LIKE '%stage\/%';<\/code><\/li>\n<\/ol>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1294\" height=\"193\" class=\"wp-image-95642\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-5.png\" \/><\/p>\n<ol start=\"4\">\n<li>Finally, you can execute your query: <br \/>\n<code>SELECT * FROM demo WHERE email = 'demo@demo.com';<br \/>\n<\/code> <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"646\" height=\"218\" class=\"wp-image-95643\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-6.png\" \/><\/li>\n<li>Now identify the query by querying the query history &#8211; run this query (replace YOUR QUERY HERE with your actual query): <br \/>\n<code>SELECT EVENT_ID, TRUNCATE(TIMER_WAIT\/1000000000000,6) as Duration, SQL_TEXT FROM performance_schema.events_statements_history_long WHERE SQL_TEXT like '%YOUR QUERY HERE%';<\/code><\/li>\n<li>Then to see the query duration and stage information, query the query history table like so (replace <code>Query_ID<\/code> with the actual query <code>ID<\/code>): <br \/>\n<code>SELECT event_name AS Stage, <br \/>\n       TRUNCATE(TIMER_WAIT\/1000000000000,6) AS Duration <br \/>\nFROM performance_schema.events_stages_history_long <br \/>\nWHERE NESTING_EVENT_ID = QUERY_ID;<\/code><\/li>\n<\/ol>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1008\" height=\"592\" class=\"wp-image-95644\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-7.png\" \/><\/p>\n<p>As you can see, the results don\u2019t differ very much, and once we are aware of what the tasks in the query execution plan mean in the context of MySQL, we can start optimizing them.<\/p>\n<h2>Optimizing Tasks &#8211; the EXPLAIN Query<\/h2>\n<p>To optimize the tasks that comprise a <code>SELECT<\/code> query and make the query faster as a result, we need to take a look at the output of an <code>EXPLAIN<\/code> query that explains what the query is making use of internally. Profiling will tell us how long each query phase takes while explaining queries will tell us how the query is actually executing. Issue a <code>SELECT<\/code> query like you normally would, just add <code>EXPLAIN<\/code> at the start. We will use an example query specified below:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\"><code>EXPLAIN SELECT * \r\nFROM demo_table \r\nWHERE email = 'jfriesen@example.net';<\/code><\/pre>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1373\" height=\"252\" class=\"wp-image-95645\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-8.png\" \/><\/p>\n<p>The output of the <code>EXPLAIN<\/code> query will tell us a number of things:<\/p>\n<ul>\n<li>The <code>type<\/code> column depicts how the row was accessed (for more information on the various methods of query access, <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/explain-output.html#explain_type\">refer to the docs<\/a>. <code>SIMPLE<\/code> in this case refers to the fact it doesn\u2019t use a <code>UNION<\/code> or subquery)<\/li>\n<li>The <code>table<\/code> column will tell you what table the <code>SELECT<\/code> query is running on. For some, it may be self-explanatory, but if you run a lot of <code>SELECT<\/code> statements, such a feature is useful. <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/explain-output.html#explain_table\">The documentation<\/a> has more information on its functionality.<\/li>\n<li><a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/explain-output.html#explain_partitions\">The <code>partitions <\/code>column<\/a> will tell you whether partitions are used, and if so, what their names are.<\/li>\n<li>The <code>possible_keys<\/code> column describes the indexes MySQL could have chosen. The key column provides us with the chosen index, and the <code>key_len <\/code>column depicts the length of the index on that column. Do note that the key length is the index length \u2013 not the length of data (it will not be the same as defined in the data type.) In this case, the length of the index (the key length) is also calculated internally by MySQL, thus the value of \u201c142.\u201d More information on possible indexes can be found <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/explain-output.html#explain_possible_keys\">here<\/a>, and indexes are covered <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/explain-output.html#explain_key\">here<\/a>.<\/li>\n<li>The <code>ref<\/code> column tells us what columns were working with the index in question to select data. In other words, MySQL is searching for columns that are working with the index <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/explain-output.html#explain_ref\">as described in the docs<\/a>.<\/li>\n<li>The <code>rows<\/code> column depicts the number of rows in the table. Bear in mind that for <code>InnoDB<\/code>, this number is likely to be an estimate rather than the actual value because <code>InnoDB<\/code> doesn\u2019t store the row count internally as <code>MyISAM<\/code> does \u2013 more information can be found in the documentation over <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/explain-output.html#explain_rows\">here<\/a>.<\/li>\n<li>The <code>filtered<\/code> column shows an estimate of how many rows are going to be filtered out (i.e. not included in the result set.) The percentage given in this column is only an estimate, so don\u2019t be surprised if you see \u201c100.00\u201d like in the example above. More information on this value can be found <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/explain-output.html#explain_filtered\">here<\/a>.<\/li>\n<li>There are also other possible values, like the <code>select_type<\/code> value: <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/explain-output.html#explain_select_type\">the <code>select_type<\/code> value outlines the type of <code>SELECT <\/code>that was used<\/a> (if the <code>SELECT<\/code> used a <code>UNION<\/code> clause, etc.)<\/li>\n<\/ul>\n<p>To better understand the results of profiling and the <code>EXPLAIN<\/code> query, keep in mind the following:<\/p>\n<ol>\n<li>A <code>SELECT<\/code> query will be blazing fast if it scans through as few rows as possible. Hence, we should avoid selecting all columns for a result set \u2013 only selecting necessary columns will be enough.<\/li>\n<li>The main thing that optimizes the performance of a <code>SELECT<\/code> query is indexes (also called keys.) Look closely at the output of the <code>EXPLAIN<\/code> query &#8211; the \u201c<code>possible_keys<\/code>\u201d column depicts the indexes that MySQL can use, and the \u201c<code>key<\/code>\u201d column depicts what index was actually chosen. <em>Indexes make <\/em><code>SELECT<\/code><em> queries faster because when an index is used, MySQL doesn\u2019t read through all data to find a column value (refer to point #1.)<\/em>\n<p>Indexes have multiple types (we will tell you what they are in a second), and indexes do really deserve a book in and of themselves, so for those who are interested in how they work on a deeper level, we suggest you <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/mysql-indexes.html\">read about everything surrounding them in the documentation of MySQL<\/a> or read <a href=\"https:\/\/onlinelibrary.wiley.com\/doi\/book\/10.1002\/0471721379\">Relational Database Index Design and the Optimizers<\/a> by Tapio Lahdenmaki and Mike Leach.<\/li>\n<li><code>SELECT<\/code> query performance can be significantly improved by using partitions too. Partitions essentially split tables into multiple different tables internally. Since those tables are still treated as one table by the storage engine layer, it\u2019s usually used to store only data that begins with, say, only a specific character or number. A query then uses the data in the partition and makes the <code>SELECT<\/code> query faster because we adhere to the point #1.<\/li>\n<\/ol>\n<p>Indexes and partitions are two things that are used the most frequently when optimizing <code>SELECT<\/code> queries &#8211; each of them has multiple types unique to themselves.<\/p>\n<p>Both indexes and partitions help optimize <code>SELECT<\/code> query performance because indexes let MySQL find rows with specific column values in a quick fashion, and partitions act as tables within tables &#8211; MySQL can turn to them when a search query begins with a specific character to only read through the table of rows beginning with that character. As a result, <code>SELECT <\/code>statements are faster in both cases.<\/p>\n<h2>Types of Indexes<\/h2>\n<p>Indexes can be split into a couple of types \u2013 we can have B-Tree, R-Tree (spatial), and hash indexes. A<em>ll indexes except spatial indexes and hash indexes are B-Tree indexes<\/em> whereas spatial indexes are R-Tree indexes. There are also hash indexes, which are only supported by the <code>MEMORY<\/code> storage engine:<\/p>\n<ol>\n<li>B-Tree: such indexes are the most frequently used type of index and it\u2019s easy to understand why &#8211; they improve the performance of queries that search for exact matches of data, use the less than (\u201c&lt;\u201d), more than (\u201c&gt;\u201d) operators, or operators with an equal sign (\u201c&gt;=\u201d, etc.): they are also used when wildcards are in use (not in all cases, though &#8211; we\u2019ll get into that in a moment.) <br \/>\nB-Tree indexes are sorted tree structures that are suited for databases that deal with a lot of data due to its ability to traverse a tree from top to bottom in a recursive fashion. Due to the fact that B-Tree indexes are sorted, exact searches (searches with the \u201c=\u201d operator) are made blazing fast.<\/li>\n<li>R-Tree, or spatial, indexes are used when DBAs are running operations on geographical data.<\/li>\n<li>Hash indexes are used for exact matches of data (i.e. only queries that use either the \u201c=\u201d or the \u201c&lt;=&gt;\u201d operators.) but only within the <code>MEMORY<\/code> storage engine<\/li>\n<\/ol>\n<h2>Index Properties<\/h2>\n<p>Indexes also have multiple properties that need to be discussed:<\/p>\n<ol>\n<li><strong>Covering indexes<\/strong> <em>cover all of the columns required for a query to execute<\/em> and when they are in use, MySQL can read them instead of reading the data. Since reading the index is significantly faster than reading the data, queries also finish quickly. <br \/>\nSuch indexes are pretty self-explanatory: include all of the columns used by the query in the index, and you have a covering index. Woohoo!<\/li>\n<li><strong>Composite indexes <\/strong>are sometimes confused with covering indexes, but they\u2019re not the same; <em>composite indexes cover multiple columns, but these columns may not necessarily be the columns that a specific query might be using<\/em>.<\/li>\n<li><strong>Prefix indexes<\/strong> index a prefix (a part) of a column. Such types of indexes are frequently used when developers need to save storage space, but need the upsides of an index too. For example, should we want to index the first 5 characters existing in an email column, we can do that like so: <br \/>\n<code>CREATE INDEX prefix_idx ON demo_table(email(5));<br \/>\n<\/code> Once such an index is created, we should observe the table structure within phpMyAdmin &#8211; we will see a row saying \u201c<code>column (characters)<\/code>\u201d in it indicating that only a specific amount of characters are indexed \u2013 according to MySQL, for more characters than defined the index is used to exclude rows that don\u2019t match the query, and the remaining rows are scanned through for possible use cases with the index (see example below.) (In the following image, you can see that we have a <code>PRIMARY KEY<\/code> on the <code>id<\/code> column two other indexes.): <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"1144\" height=\"222\" class=\"wp-image-95646\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-9.png\" \/><\/li>\n<li><strong>Clustered indexes<\/strong> \u2013 these indexes are unique in a couple of aspects firstly because only one clustered index can exist on a table at a time. A clustered index is also an index with the exact order of rows in the index as in the table. In MySQL, such indexes are often used at the <code>PRIMARY KEY<\/code> constraints index structure, though that\u2019s not always the case \u2013 <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/5.7\/en\/innodb-index-types.html\">refer to the documentation<\/a> for more information.<\/li>\n<\/ol>\n<h2>Indexes Examples<\/h2>\n<p>Now for the examples. Not that when observing the EXPLAIN statement output, the cardinality column doesn\u2019t define the number of rows in a column, but rather the number of unique values in it.<\/p>\n<ol>\n<li>B-Tree indexes are usually defined like so (<code>email_idx<\/code> is the name of the index, and <code>email<\/code> is the name of the column): <br \/>\n<code>ALTER TABLE `demo_table` ADD INDEX email_idx(email);<br \/>\n<\/code> If we don\u2019t want to use <code>ALTER<\/code>, we can also create indexes like so (<code>email_idx<\/code> is the name of the key \u2013 keys are different names for indexes): <br \/>\n<code>CREATE INDEX email_idx ON demo_table (email, another_column_if_you_like, ...);<\/code> <\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1099\" height=\"170\" class=\"wp-image-95647\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-10.png\" \/> <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"1391\" height=\"265\" class=\"wp-image-95648\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-11.png\" \/><\/li>\n<li>R-Tree, or spatial, indexes can only be defined on geographical data &#8211; that is, columns having the <code>GEOMETRY<\/code> data type. R-Tree indexes are suited for indexing geographical data due to its nature: the data structure is suited for geographical coordinates, but bear in mind that LIKE queries may not always use indexes as you can see below: <br \/>\n<code>ALTER TABLE 'demo_table' ADD <\/code><strong>SPATIAL<\/strong><code> INDEX(column);<br \/>\n<\/code> For more detail on R-Tree structures, see this <a href=\"https:\/\/en.wikipedia.org\/wiki\/R-tree\">Wikipedia article<\/a>. <img loading=\"lazy\" decoding=\"async\" width=\"1158\" height=\"235\" class=\"wp-image-95649\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-12.png\" \/> <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"1400\" height=\"188\" class=\"wp-image-95650\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-13.png\" \/><\/li>\n<li>For an index to be a covering index for a query, it must include all of the columns in a table that a specific query is using. So, for example, if our query looks like <code>SELECT * FROM demo_table WHERE c1 = 'A' AND c2 = 'B';<\/code>, our covering index would look like so (phpMyAdmin will also show the cardinality of both index key columns towards the right): <br \/>\n<code>ALTER TABLE `demo_table` ADD INDEX covering_idx (email,username);<\/code> <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"1133\" height=\"67\" class=\"wp-image-95651\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-14.png\" \/> <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"1558\" height=\"190\" class=\"wp-image-95652\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-15.png\" \/> <br \/>\nIn this case, MySQL has elected to use the covering index &#8211; the index is of 224 characters in size. <br \/>\nCovering indexes cover all of the fields required for a query to execute (see example below.) A covering index covers all of the columns required for a query to execute, but not necessarily in a specific manner, meaning that if we would switch the order of columns, the index would still be covering. If our query looks like this, we would most likely benefit from an index on the columns <code>c1<\/code>, <code>c2<\/code>, and <code>c3<\/code>: <br \/>\n<code>SELECT * FROM demo_table WHERE c1 = \u2018Demo\u2019 AND c2 = 'Demo' AND c3 = 'demo@demo.com';<br \/>\n<\/code><\/li>\n<li>Composite indexes include <em>multiple columns, but will not necessarily cover all of the columns for any given query<\/em>. If we have three columns &#8211; <code>c1<\/code>, <code>c2<\/code>, and <code>c3<\/code>, a composite index would be any index that covers multiple columns. Such an index may cover columns <code>c1<\/code> and <code>c2<\/code>, it may index the columns <code>c3<\/code> and <code>c1<\/code>, it may have other combinations &#8211; but <em>if the index does not cover all of the columns required for a query to execute, it\u2019s not a covering index<\/em>. Composite indexes are simply indexes that include multiple columns inside of them and they don\u2019t have anything unique to themselves, other than the fact that key columns need to be ordered first, so the most important columns are scanned first. <br \/>\nIn this case, we cover the <code>username<\/code> and <code>id<\/code> columns with a query like so: <br \/>\n<code>ALTER TABLE 'demo_table' ADD INDEX covering_idx(username,id);<br \/>\n<\/code> <img loading=\"lazy\" decoding=\"async\" width=\"1155\" height=\"232\" class=\"wp-image-95653\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-16.png\" \/> <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"1438\" height=\"200\" class=\"wp-image-95654\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-17.png\" \/><\/li>\n<li>Indexing a prefix of a column might be a necessity to save storage space &#8211; the golden rule here is that the fewer characters that are in the prefix, the better for storage, but worse for performance because of the additional scan of rows that partially match that is necessitated. If you don\u2019t have much storage space but still want increased performance, prefix indexes are for you. Define prefix indexes like so (in this example, we assume that your column is called c1 and we index the first 5 characters of the column): <br \/>\n<code>ALTER TABLE 'demo_table' ADD INDEX idx_name(c1(5));<br \/>\n<\/code> When such indexes are in use, we could still search for exact matches as we do below (we only indexed the first 5 characters, but the query was still using index): <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"1490\" height=\"186\" class=\"wp-image-95655\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-18.png\" \/><\/li>\n<li><code>PRIMARY KEY<\/code> indexes are usually defined when creating a table, and they must be set to increment automatically: <br \/>\n<code>CREATE TABLE demo_table (<br \/>\n'id' INT(255) <\/code><strong>PRIMARY KEY AUTO_INCREMENT<\/strong><code>,<br \/>\n`column_1` VARCHAR(255),<br \/>\n...<br \/>\n);<\/code> <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"852\" height=\"189\" class=\"wp-image-95656\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-19.png\" \/> <br \/>\nAs you can see from the query below, the <code>id<\/code> column has incremented by 1 since one row was inserted into the table: <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"891\" height=\"361\" class=\"wp-image-95657\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-20.png\" \/><\/li>\n<\/ol>\n<p>As far as <code>PRIMARY KEY<\/code> constraint indexes are concerned, keep in mind that the type of the column that has a primary key doesn\u2019t have to be an integer. For MySQL, <code>PRIMARY KEY <\/code>constraints have the following requirements:<\/p>\n<ul>\n<li>The column that has a primary key constraint on it cannot be null, nor should it be empty.<\/li>\n<li>The column that has a primary key constraint on it must only contain unique values.<\/li>\n<li>A table can only contain one primary key constraint.<\/li>\n<li>The length of the column that has a primary key constraint cannot be longer than 767 bytes.<\/li>\n<\/ul>\n<p>Also, keep in mind that primary key constraints can consist of multiple columns \u2013 for many, such an approach looks like the following (notice the constraint on line 6):<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"679\" height=\"305\" class=\"wp-image-95658\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/d-redgate-blogs-november-2022-optimiziation-serie-1.png\" alt=\"D:\\Redgate\\Blogs\\November 2022\\Optimiziation Series\\Optimizing SELECT Queries\\Images\\Primary Key.png\" \/><\/p>\n<p>Indexes affect queries in various ways: the aim of B-Tree indexes is to help the database find rows matching a specific <code>WHERE<\/code> clause quickly enough. How much the performance is improved can depends on how our instances are configured. Some indexes are typically added when creating a table or with an <code>ALTER TABLE<\/code> query defined above.<\/p>\n<p>There are also fulltext type indexes for those who would like to run more queries that behave more like search engine, for example when searching through long text columns, Fulltext indexes are defined by adding the <code>FULLTEXT<\/code> keyword in the <code>ALTER TABLE<\/code> or <code>CREATE TABLE<\/code> queries, or when creating tables, they can be defined like so:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">CREATE TABLE demo_table (\r\n  id INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,\r\n  email VARCHAR(35) NOT NULL,\r\n  username VARCHAR(20) NOT NULL DEFAULT '',\r\n<strong>  FULLTEXT KEY(email)<\/strong>\r\n) ENGINE = InnoDB;<\/pre>\n<p>There is also a type of index that\u2019s a fit for the MEMORY storage engine \u2013 hash indexes can be defined like so (in this case, <code>demo_table_memory<\/code> is based upon the <code>MEMORY<\/code> storage engine instead of InnoDB. Make sure your table is based upon the <code>MEMORY<\/code> storage engine before running this query): <br \/>\n<code>ALTER TABLE `demo_table_memory` ADD INDEX idx_name(column); <\/code><strong>USING HASH<\/strong><code>;<\/code> <br \/>\nHash indexes are only supported when the <code>MEMORY<\/code> storage engine is in use, and they only support exact matches of data (only queries with the \u201c=\u201d operator.) Also, note that you should bear in mind the amount of memory available on the server to not face the \u201cThe table is full\u201d error (the name of the table will be weird because it\u2019s in memory): <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"1434\" height=\"186\" class=\"wp-image-95659\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-22.png\" \/> <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"676\" height=\"354\" class=\"wp-image-95660\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-23.png\" \/><\/p>\n<h2>Partitioning MySQL<\/h2>\n<p>Now for partitions. They also have multiple types &#8211; aside from the fact that tables can be either partitioned horizontally or vertically, there are six partitioning types that are available. Also, mind the fact that since some storage engines don\u2019t provide support for partitioning, partitions are recommended to use on <code>InnoDB<\/code> or <code>XtraDB<\/code> storage engines.<\/p>\n<p>MySQL has six partitioning types (as of the time of writing, MySQL supports horizontal partitioning, but not vertical):<\/p>\n<ol>\n<li>Partitioning by <code>RANGE<\/code> &#8211; such a partitioning type is used to partition data that falls within a given range. For example, if we have a large data set and we search for values beginning with numbers, we might benefit from partitions beginning with 0, 1, 2, and other numbers.<\/li>\n<li>Partitioning by <code>LIST<\/code> &#8211; such a partitioning type is used to partition data that falls within one category in one partition, another category in another, etc. Think about geographical data falling in East, West, North, or East &#8211; such a partitioning type would be useful when 4 partitions are in use and values of rows could fall in one or more of defined lists.<\/li>\n<li>Partitioning by <code>COLUMNS<\/code> can let people partition data by columns \u2013 i.e. such a partitioning type lets you use multiple columns as partitioning keys.<\/li>\n<li>Partitioning by <code>HASH<\/code> is frequently used when partitioning id columns. Partitioning by <code>HASH<\/code> lets people split the data into a specified number of partitions with an equal amount of rows in them.<\/li>\n<li>Partitioning by <code>KEY<\/code> is similar to partitioning by <code>HASH<\/code>, except that MySQL chooses the way of partitioning data. If users don\u2019t want to, columns don\u2019t have to be defined &#8211; simply specifying the number of partitions is enough.<\/li>\n<\/ol>\n<p>The list below will tell you when certain partitioning types should be used:<\/p>\n<ul>\n<li>Partitioning by <code>RANGE<\/code> is frequently used by search engines that deal with a lot of data and need quick SELECT response times.<\/li>\n<li>Partitioning by <code>LIST<\/code> is used when we need a couple of lists falling within a category.<\/li>\n<li>Partitioning by <code>COLUMNS<\/code> is used by MySQL to determine which partitions are to be checked for matching rows or for the purpose of placing rows into partitions.<\/li>\n<li>Partitioning by <code>HASH<\/code> ensures even distribution of data across partitions.<\/li>\n<li>Partitioning by <code>KEY<\/code> is similar to partitioning by <code>HASH<\/code>, just that it takes a hashing function provided to it by MySQL.<\/li>\n<\/ul>\n<p>There is also a concept known as subpartitioning which is relatively self-explanatory: it refers to partitions within partitions.<\/p>\n<p>Partitions are usually defined when creating tables, and they can be defined like so (in this example, we use partitioning by <code>KEY<\/code>, but other partitioning types can be defined in a similar fashion &#8211; <a href=\"https:\/\/dev.mysql.com\/doc\/mysql-partitioning-excerpt\/5.7\/en\/partitioning-types.html\">refer to the documentation for explanation<\/a>, and before deciding on any one specific partitioning type, make sure to experiment):<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">CREATE TABLE demo_table (\r\nid INT NOT NULL PRIMARY KEY,\r\nemail VARCHAR(35) NOT NULL,\r\nusername VARCHAR(20) NOT NULL DEFAULT \u2018\u2019\r\n) PARTITION BY KEY() PARTITIONS 5;<\/pre>\n<p>In newer versions of MySQL (MySQL 8.0 and above \u2013 at the time of writing, MySQL 8.0.31 was the most recent stable release), partitioning is only supported in <code>InnoDB<\/code> and <code>NDB<\/code> storage engines &#8211; if older versions of MySQL are in use, users can also elect to partition data in the MyISAM storage engine. For more details of how partitioning impacts <code>SELECT<\/code> operations, <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/partitioning-overview.html\">refer to the MySQL documentation<\/a>.<\/p>\n<p>Tips &amp; Tricks for Optimizing Read Operations<\/p>\n<p>The optimization of <code>SELECT<\/code> operations using indexes is not a completely worry-free operation where you add a bunch of indexes and it all just works. Problems are an inevitable part of optimization operations, so we\u2019ve also prepared a quick cheat sheet of tips &amp; tricks that you can glance at and solve if not all, at least the majority of the problems you\u2019re facing:<\/p>\n<ul>\n<li>Use normalization wherever possible &#8211; a properly normalized database is typically great for <code>SELECT<\/code> queries. This is because the lack of redundant data means less to be read. This has its limits based on instance capacity, number of rows, and how many tables are needed to answer queries, but it is almost always better to have a well normalized database.<\/li>\n<li>After creating an index, make sure that your queries use the indexes you have created for the queries that read data. Use <code>EXPLAIN<\/code> to allow MySQL to tell you what indexes are used and why (see examples we gave earlier on.) and adjust where necessary.<\/li>\n<li>Make use of the <code>LIMIT<\/code> clause where necessary and avoid using the \u201c<code>SELECT *<\/code>\u201d operator if possible \u2013 the less data you select, the faster your queries will become (the default limit of rows that are returned by MySQL is 200.)<\/li>\n<li>If possible, avoid overcomplicating your <code>SELECT<\/code> statements. Sometimes using too many <code>OR<\/code> expressions can mean that MySQL needs to use an index more than one time. It can be valuable to consider splitting a query into two queries, returning less results (use <code>LIMIT<\/code>), or selecting fewer columns to return (instead of using <code>SELECT *<\/code> use <code>SELECT columnname, columnname2, <\/code>etc.)<\/li>\n<li>If you find yourself using wildcards with the <code>LIKE<\/code> expression (a wildcard is the <code>'%'<\/code> operator), make sure to employ them only at the end of the search string. Using <code>'%'<\/code> at the start of your expression will return all data to be scanned, which typically makes the search operation slower.<\/li>\n<\/ul>\n<p>SELECT Queries: DDL and the Data<\/p>\n<p>As we\u2019ve mentioned in the beginning of the article, now we\u2019ll provide you with a couple of DDL operations to help you get started.<\/p>\n<p>First off, we have to create the tables (the names of the tables and their contents can be any &#8211; also note that the tables should use the <code>InnoDB<\/code> or <code>XtraDB<\/code> storage engines for the best results in the optimization realm):<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">CREATE TABLE demo_table (\r\nid INT NOT NULL PRIMARY KEY AUTO_INCREMENT PRIMARY KEY,\r\nemail VARCHAR(35) NOT NULL,\r\nusername VARCHAR(20) NOT NULL DEFAULT \u2018\u2019\r\n)  ENGINE = InnoDB;<\/pre>\n<ol>\n<li>Now, we have to fill the table up with data \u2013 One method is to use <a href=\"https:\/\/filldb.info\/dummy\">the FillDB dummy data generator<\/a> for that task &#8211; paste your database schema (you might need to rename some of your tables &#8211; the data generator saves all of the previously used table names), then generate data for columns: <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"1700\" height=\"723\" class=\"wp-image-95661\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-24.png\" \/><\/li>\n<li>Finally, specify the number of rows to be generated (we recommend to generate no more than 100,000 rows at once to not overwhelm your browser), then move on: <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"403\" height=\"217\" class=\"wp-image-95662\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-25.png\" \/><\/li>\n<li>Your data should be now ready to go &#8211; you will be able to view the data that the tool has generated: <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"1712\" height=\"561\" class=\"wp-image-95663\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-26.png\" \/><\/li>\n<li>The only thing left for you to do is to import the data into your own database instance. Export the file from the dummy data generator by clicking the cyan button towards the right (it should say \u201cExport tablename.\u201d) <br \/>\n<img loading=\"lazy\" decoding=\"async\" width=\"1714\" height=\"80\" class=\"wp-image-95664\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-27.png\" \/><\/li>\n<\/ol>\n<p>Now, import the data into MySQL via the CLI or the Import functionality within phpMyAdmin, and have fun optimizing your queries!<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"826\" height=\"531\" class=\"wp-image-95665\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/01\/word-image-95637-28.png\" \/><\/p>\n<h2>The Downsides of Optimizing SELECT statements<\/h2>\n<p>At the end of the day, almost everything we need to do to optimize a <code>SELECT<\/code> query comes down to the fact that we need to make MySQL able to access as little data as possible. If that doesn\u2019t help, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/optimizing-my-cnf-for-mysql-performance\/\">we need to look into the configuration of our MySQL instances<\/a>.<\/p>\n<p>However, one of the most significant nuances in this space is that when <code>SELECT<\/code> queries are being optimized, we need to compromise on storage and the performance of other queries.<\/p>\n<p>That\u2019s because indexes and partitions speed up read operations, but slow down the speed of <code>INSERT<\/code>, <code>UPDATE<\/code>, and <code>DELETE<\/code> queries. This is because when data is modified data also needs to be inserted, updated, or deleted together with it, and data may need to be shuffled around internally as well. It is rare that you have so many indexes that it noticeably harms performance, but it can happen, especially for high-throughput database applications.<\/p>\n<p>Also keep in mind that the usefulness of indexes depends a lot on how frequently our queries are being executed. If a query is executed daily and saves 10 seconds on the runtime, is it worth it for all the maintenance it requires? Before making use of any advice mentioned in this article in a production environment, make sure to experiment and play around with the advice given in this article, and you should be good to go!<\/p>\n<h2>Summary<\/h2>\n<p>In this blog, we\u2019ve walked you through numerous ways that help both novices and experienced DBAs optimize their <code>SELECT<\/code> query performance in MySQL. We\u2019ve told you what query profiling has to do with their performance, walked you through the query execution plan provided by <code>EXPLAIN<\/code> queries, types of indexes, and partitioning.<\/p>\n<p>We hope that some of the information provided in this blog has helped you optimize your <code>SELECT<\/code> query performance \u2013 tell us if the advice given in this blog has helped empower your applications in the comment section down below, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/\">come back to the blog to read up on how to optimize the performance of other types of queries<\/a>, and until next time!<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Optimizing read operations is one of the most prominent problems concerning any database administrator. No matter what kind of database management system is in use \u2013 MySQL, its flavors like Percona Server or MariaDB, MongoDB, TimescaleDB, SQL Server, or others, read queries are a concern. The primary read query example is SELECT queries, but a lot of this pertains to UPDATE and DELETE as well since they too have to fetch rows to be operated on.&hellip;<\/p>\n","protected":false},"author":339547,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[53,145792],"tags":[],"coauthors":[146040],"class_list":["post-95637","post","type-post","status-publish","format-standard","hentry","category-featured","category-mysql"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/95637","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=95637"}],"version-history":[{"count":3,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/95637\/revisions"}],"predecessor-version":[{"id":95669,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/95637\/revisions\/95669"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=95637"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=95637"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=95637"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=95637"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}