{"id":107444,"date":"2025-08-15T11:00:00","date_gmt":"2025-08-15T11:00:00","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=107444"},"modified":"2025-08-06T14:55:21","modified_gmt":"2025-08-06T14:55:21","slug":"mysql-index-overviews-primary-key-indexes","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/mysql-index-overviews-primary-key-indexes\/","title":{"rendered":"MySQL Index Overviews: PRIMARY KEY Indexes"},"content":{"rendered":"\n<p>If you\u2019re a developer who had interactions with indexes in the past, you certainly know that indexes in <a href=\"https:\/\/www.mysql.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">MySQL<\/a> come in a variety of shapes and sizes. Simple Talk readers will certainly not be alien to <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/the-nuances-of-mysql-indexes\/\" target=\"_blank\" rel=\"noreferrer noopener\">the nuances of indexes within MySQL<\/a> \u2013 <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/mysql-index-overviews-b-tree-indexes\/\" target=\"_blank\" rel=\"noreferrer noopener\">as we&#8217;ve already covered B-tree indexes in MySQL<\/a>, and some readers may even be aware of the <a href=\"https:\/\/www.postgresql.org\/docs\/16\/indexes-types.html\" target=\"_blank\" rel=\"noreferrer noopener\">powers granted to them by database management systems such as PostgreSQL<\/a>, as it\u2019s known to be one of the most prolific database management systems for indexes and data types alike.<\/p>\n\n\n\n<p>While most of you may be aware of the power that B-tree indexes help you unleash, you may not be aware of specific types of indexes that exist but often remain behind the spotlight \u2013 these are <code>PRIMARY KEY<\/code> indexes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-what-are-primary-key-indexes\">What are PRIMARY KEY Indexes?<\/h2>\n\n\n\n<p>Beneath the hood, <code>PRIMARY KEY<\/code> indexes are still B-tree indexes in <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.4\/en\/innodb-introduction.html\" target=\"_blank\" rel=\"noreferrer noopener\">InnoDB<\/a>, but they are subject to stricter constraints (<code>NOT NULL<\/code>, <code>UNIQUE<\/code>). They are essential to the table structure and are used constantly in foreign key relationships, clustering, and lookups.<\/p>\n\n\n\n<p>The purpose of a primary key index is to uniquely identify rows in a table thus, they serve a special purpose. In InnoDB, it physically orders the table data by the primary key (clustered index).<\/p>\n\n\n\n<p>In MySQL, one can add primary key indexes to a column when creating a table like so:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE TABLE `demo_table` (\n`id` INT PRIMARY KEY,\n`message` VARCHAR(120) NOT NULL DEFAULT \u2018\u2019,\n`sender` VARCHAR(120) NOT NULL DEFAULT \u2018\u2019,\n`receiver` VARCHAR(120) NOT NULL DEFAULT \u2018\u2019,\n`timestamp` TIMESTAMP NOT NULL,\n\u2026\n);\n<\/pre><\/div>\n\n\n\n<p>Or when modifying a table like so:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">ALTER TABLE `demo_table` ADD PRIMARY KEY(`id`);<\/pre><\/div>\n\n\n\n<p>DBAs\/developers don\u2019t usually refer to primary keys as \u201cprimary key indexes\u201d \u2013 a primary key is always an index, but most of the industry folks refer to primary keys as primary keys \u2013 not as primary index keys or primary indexes. Just primary keys.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-rules-of-primary-key-indexes\">Rules of PRIMARY KEY Indexes<\/h2>\n\n\n\n<p>A <code>PRIMARY KEY<\/code> in MySQL is a type of B-tree index that uniquely identifies each row in a table. However, unlike regular indexes, a primary key comes with special constraints and behaviors:<\/p>\n\n\n<div class=\"block-core-list\">\n<ol class=\"wp-block-list\">\n<li><strong>A <code>PRIMARY KEY<\/code> column cannot contain <code>NULL<\/code> values<\/strong>. <br>By definition, a primary key must be <code>NOT NULL<\/code> and <code>UNIQUE<\/code>. MySQL enforces this automatically, even if you don&#8217;t explicitly declare the column as <code>NOT NULL<\/code>. Any attempt to insert a <code>NULL<\/code> value into a primary key column will result in an error like so:<br><br><pre class=\"lang:tsql decode:true \">ERROR 1048 (23000): Column \u2018colname\u2019 cannot be null<\/pre><br><\/li>\n\n\n\n<li><strong><code>AUTO_INCREMENT<\/code> allows you to omit the value for the column by specifying <code>NULL<\/code>\u2014but does not store <code>NULL<\/code> values.<\/strong><br>When a column is defined with <code>AUTO_INCREMENT<\/code>, you can insert a row without providing a value for that column. If you explicitly insert <code>NULL<\/code>, MySQL treats it as a signal to generate the next sequence value. However, the stored value will never be <code>NULL<\/code>\u2014it will be a generated number. This piece of SQL query would insert a <code>NULL<\/code> value into the id column of a table named demo:<br><br><pre class=\"lang:tsql decode:true \">INSERT INTO `demo` (`id`) VALUES (NULL);<\/pre><br><\/li>\n\n\n\n<li><strong><code>PRIMARY KEYS<\/code> are often, but not always, integers. <\/strong><br>While it&#8217;s common to use an integer column with <code>AUTO_INCREMENT<\/code> as a primary key, MySQL supports other data types too. For example, you can define a <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/data-types\/char-and-varchar-transact-sql?view=sql-server-ver16\" target=\"_blank\" rel=\"noreferrer noopener\">VARCHAR<\/a> or even a <code>CHAR<\/code> column as a primary key. The key requirement is that the values be unique and not <code>NULL<\/code>.<br><\/li>\n\n\n\n<li><strong>A table can have only one <code>PRIMARY KEY<\/code>. <\/strong><br>This is a rule of the relational model. A primary key represents the unique identifier for each row, and allowing more than one would violate that principle. However, other uniqueness constraints can be defined using <code>UNIQUE<\/code> indexes.<br><\/li>\n\n\n\n<li><strong>A <\/strong><span><code style=\"\"><b>PRIMARY KEY<\/b><\/code><b> can span multiple columns.<\/b><\/span> <br>MySQL supports composite primary keys, where two or more columns together form a unique identifier. In such cases, no individual column is necessarily unique on its own, but the combination must be.<br><\/li>\n\n\n\n<li><strong>MySQL will create a primary key automatically in some cases.<br><\/strong>Starting with MySQL 8.0.30, if you don\u2019t define any primary or unique key, MySQL may generate a <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.4\/en\/create-table-gipks.html\" target=\"_blank\" rel=\"noreferrer noopener\">Generated Invisible Primary Key (GIPK)<\/a> behind the scenes. These internal keys aren&#8217;t visible in the table definition, but they serve the purpose of uniquely identifying each row and can be referenced programmatically.<\/li>\n<\/ol>\n<\/div>\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"941\" height=\"382\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-29.png\" alt=\"\" class=\"wp-image-107445\" style=\"width:832px;height:auto\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-29.png 941w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-29-300x122.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-29-768x312.png 768w\" sizes=\"auto, (max-width: 941px) 100vw, 941px\" \/><figcaption class=\"wp-element-caption\"><em>Image 1 &#8211; VARCHAR values as PRIMARY KEYs<\/em><\/figcaption><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"941\" height=\"134\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-30.png\" alt=\"\" class=\"wp-image-107446\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-30.png 941w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-30-300x43.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-30-768x109.png 768w\" sizes=\"auto, (max-width: 941px) 100vw, 941px\" \/><figcaption class=\"wp-element-caption\"><em>Image 2 &#8211; AUTO_INCREMENT for VARCHAR values in MySQL<\/em><\/figcaption><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-working-with-primary-keys\">Working with PRIMARY KEYs<\/h2>\n\n\n\n<p>If there are no natural combinations of column values in the table that are unique, <code>PRIMARY KEYs<\/code> can be assigned that attributes that make them increment automatically and PKs having such attributes will be defined like so:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">`column_title` INT PRIMARY KEY AUTO_INCREMENT<\/pre><\/div>\n\n\n\n<p>Working with data inside of such columns isn\u2019t hard \u2013 if we insert data, we need to insert <code>NULL<\/code> values into the column with a PK, or omit the PK column in the insert statement. This causes MySQL to insert the next available value from the auto-increment sequence.<\/p>\n\n\n\n<p>Although inserting <code>NULL<\/code> will trigger <code>AUTO_INCREMENT<\/code>, it&#8217;s usually safer and clearer to omit the column entirely from your <code>INSERT<\/code> statement. Use <code>NULL<\/code> only if you&#8217;re relying on a feature like <code>LAST_INSERT_ID()<\/code>. Otherwise, you risk hitting errors like Duplicate entry &#8216;0&#8217; for key<code> 'PRIMARY'<\/code>, especially if a row with id = 0 was inserted before <code>AUTO_INCREMENT<\/code> was added. In such cases, MySQL may start the sequence at 1, but still encounter a conflict with the existing value. This is particularly common if the sql_mode setting treats 0 as a special value.<a id=\"_msocom_1\"><\/a><\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"941\" height=\"45\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-31.png\" alt=\"\" class=\"wp-image-107447\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-31.png 941w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-31-300x14.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-31-768x37.png 768w\" sizes=\"auto, (max-width: 941px) 100vw, 941px\" \/><figcaption class=\"wp-element-caption\"><em>Image 3 &#8211; Duplicate entry &#8216;0&#8217; for key &#8216;PRIMARY&#8217;<\/em><\/figcaption><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>To solve this problem, we\u2019ll have to define an <code>auto_increment<\/code> value and try again. This can be done through phpMyAdmin:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"941\" height=\"193\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-32.png\" alt=\"\" class=\"wp-image-107448\" style=\"width:832px;height:auto\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-32.png 941w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-32-300x62.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-32-768x158.png 768w\" sizes=\"auto, (max-width: 941px) 100vw, 941px\" \/><figcaption class=\"wp-element-caption\"><em>Image 4 &#8211; AUTO_INCREMENT in phpMyAdmin<\/em><\/figcaption><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>Or running a query like so:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">ALTER TABLE `demo_table` CHANGE `id` `id` INT(11) NOT NULL AUTO_INCREMENT;<\/pre><\/div>\n\n\n\n<p>Without moving far from here, I should also state that errors like the one related to duplicate entries in the third image can be avoided altogether if we specify an IGNORE clause in our SQL statement. The same statement written like so would ignore errors related to primary keys and insert the data regardless, which can be very useful if you need to insert the data for parsing or related operations:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \" >LOAD DATA INFILE \u2018C:\/wamp64\/tmp\/data1\/MOCK_DATA_1.csv\u2019 IGNORE INTO TABLE\n`demo_table` FIELDS TERMINATED BY \u2018,\u2019 (message,sender,receiver,timestamp);<\/pre><\/div>\n\n\n\n<p>However, IGNORE is not the cure to all of your problems. When running ALTER queries with auto_increment, you may also face errors such as:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \" >ERROR 1062 (23000): ALTER TABLE causes auto_increment resequencing,\nresulting in duplicate entry '1' for key 'PRIMARY'<\/pre><\/div>\n\n\n\n<p>This essentially says that one row already contains a value of &#8220;0&#8221;. This error typically arises because a value such as 0 or 1 was manually inserted before the <code>AUTO_INCREMENT<\/code> attribute was added. When MySQL attempts to re-sequence the column, it tries to begin from the lowest unused value \u2014 often 1 \u2014 and clashes with an existing row. <\/p>\n\n\n\n<p>The fix is usually straightforward: delete or update the conflicting row, or explicitly set the starting point for auto-increment with a statement like <code>ALTER TABLE demo_table AUTO_INCREMENT = 100<\/code>. If the conflict involves 0, and it\u2019s being treated as a request for an auto-generated value, setting sql_mode =<code> 'NO_AUTO_VALUE_ON_ZERO'<\/code> will restore clarity by ensuring that 0 is treated as a literal value.<\/p>\n\n\n\n<p>To avoid this sort of error, run a query like <code>SET SESSION sql_mode=' NO_AUTO_VALUE_ON_ZERO'<\/code>:<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"941\" height=\"149\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-33.png\" alt=\"\" class=\"wp-image-107449\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-33.png 941w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-33-300x48.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-33-768x122.png 768w\" sizes=\"auto, (max-width: 941px) 100vw, 941px\" \/><figcaption class=\"wp-element-caption\"><em>Image 5 &#8211; Making AUTO_INCREMENT errors go away<\/em><\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-using-primary-keys\">Using Primary Keys<\/h2>\n\n\n\n<p>Let\u2019s now take a look into cases necessitating the usage of our primary keys:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"941\" height=\"574\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-34.png\" alt=\"\" class=\"wp-image-107450\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-34.png 941w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-34-300x183.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-34-768x468.png 768w\" sizes=\"auto, (max-width: 941px) 100vw, 941px\" \/><figcaption class=\"wp-element-caption\"><em>Image 6 &#8211; Using PKs in SELECT queries<\/em><\/figcaption><\/figure>\n\n\n\n<p><\/p>\n\n\n<div class=\"block-core-list\">\n<ol class=\"wp-block-list\">\n<li>The first query makes use of a primary key because we only select the <code><em>id<\/em><\/code> column after the <code>WHERE<\/code> clause \u2013 quite self-explanatory.<br><br><\/li>\n\n\n\n<li>The second query doesn\u2019t even consider our primary key because we don\u2019t use it after a <code>WHERE<\/code> clause.<br><br><\/li>\n\n\n\n<li>The third query uses our primary key because we know which <code>id<\/code> we\u2019re searching for.<br><br><\/li>\n\n\n\n<li>The fourth query behaves the same way &#8211; it uses the primary key to locate a specific row.<br><br><\/li>\n\n\n\n<li>The last query says <code>\u201cImpossible WHERE noticed after reading const tables\u201d<\/code> which isn\u2019t an error (running such a query is possible and it would return an empty result set), but it denotes that the <code>WHERE<\/code> clause doesn\u2019t quite make sense (in this case, most likely because the <code>id<\/code> of 9,997 doesn\u2019t even exist \u2013 there are around 5,000 rows in the table).<\/li>\n<\/ol>\n<\/div>\n\n\n<p>To explain the thinking of our database in operations involving primary keys, we run an ordinary SQL query and add <code>EXPLAIN<\/code> in front of it. The way<code> EXPLAIN<\/code> works in different relational database management systems differs from DBMS to DBMS, but in a nutshell, it tells us how and why our query is executed in the way it is. <code>EXPLAIN<\/code> lets us in on multiple things:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>The key column refers to the index actually chosen by MySQL.<br><\/li>\n\n\n\n<li>The type of our SQL query (whether our query is using any <code>JOIN\/UNION<\/code> operations or runs as-is): that\u2019s the <code>select_type<\/code> column.<br><\/li>\n\n\n\n<li>The table our SQL query runs on \u2013 that\u2019s the <code>table<\/code> column.<br><\/li>\n\n\n\n<li>The <code>type<\/code> column shows the type of the query that was executed: a value of <code>const<\/code> means that MySQL finds at most one row in the table.<br><\/li>\n\n\n\n<li>The <code>possible_keys<\/code> column refers to the indexes considered to use by MySQL.<br><\/li>\n\n\n\n<li>The<code> key<\/code> column refers to the index actually chosen by MySQL.<br><\/li>\n\n\n\n<li>The value of the <code>key_len<\/code> column refers to the length of the index that was chosen by MySQL.<br><\/li>\n\n\n\n<li>The <code>ref<\/code> column refers to the column or value that MySQL uses to find rows in the table.<br><\/li>\n\n\n\n<li>The value in the <code>rows<\/code> column refers to the number of rows examined by MySQL.<br><\/li>\n\n\n\n<li><code>Extra<\/code> refers to anything \u201cextra\u201d MySQL feels like telling us.<\/li>\n<\/ul>\n<\/div>\n\n\n<p><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/optimizing-queries-in-mysql-optimizing-reads\/\" target=\"_blank\" rel=\"noreferrer noopener\">After we understand the internals of our queries<\/a>, we can see that operations with PK indexes seem to be similar to operations with ordinary B-tree indexes. At the same time,, we should keep in mind that in InnoDB, the primary key <em>is<\/em> the clustered index \u2014 it defines the <strong>physical order<\/strong> of the rows. This makes its use significantly different from a secondary index, which contains a copy of the key and a pointer to the clustered index.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-strings-as-primary-keys\">Strings as Primary Keys<\/h2>\n\n\n\n<p>While the use of string values may have a slight performance cost, natural keys, which are often strings, are perfectly valid, and more maintainable than surrogate keys. Their great advantage is that rows that are referenced by their primary keys are retrieved extremely efficiently. However, it isn\u2019t a good practice to store GUIDs as string values for primary keys. How is that value generated and how do you ensure that it will always be unique? And, if the need arises, how would you compare and search for data? <\/p>\n\n\n\n<p>Given the fact that many universally unique identifiers look like <span style=\"font-family: monospace, monospace; font-size: 1em; text-wrap-mode: nowrap;\">550e8400-e29b-41d4-a716-446655440000<\/span>, why do you need a GUID rather than an integer to act as a primary key in the first place? This comes at a cost: larger keys mean more storage, slower joins, and heavier indexes.<a id=\"_msocom_1\"><\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-optimizing-operations-with-primary-keys\">Optimizing Operations with Primary Keys<\/h2>\n\n\n\n<p>Primary keys aren\u2019t just about enforcing uniqueness \u2014 they\u2019re a performance lever. In InnoDB, the table is physically ordered by the primary key (a clustered index), which means smart primary key design can make lookups, joins and range queries lightning fast. But it cuts both ways: a poorly chosen key can drag everything down.<\/p>\n\n\n\n<p>A few golden rules:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Keep it short<\/strong> \u2013 Smaller keys mean faster indexes and leaner foreign keys.<\/li>\n\n\n\n<li><strong>Keep it stable<\/strong> \u2013 Changing primary keys is like moving the foundations of a house.<\/li>\n\n\n\n<li><strong>Avoid bulky composites<\/strong> \u2013 Use them only when truly necessary.<\/li>\n\n\n\n<li><strong>Think downstream<\/strong> \u2013 Every foreign key referencing your PK inherits its baggage.<\/li>\n<\/ul>\n<\/div>\n\n\n<p>Treat your primary key like the backbone of your table \u2014 strong, lightweight, and unchanging.<\/p>\n\n\n\n<p>Since primary keys in InnoDB are B-tree indexes, most of the same performance advice applies, so I\u2019d advise you to also apply <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/mysql-index-overviews-b-tree-indexes\/\" target=\"_blank\" rel=\"noreferrer noopener\">advice relevant to B-tree indexes<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-generated-invisible-primary-keys-gipks\">Generated Invisible Primary Keys (GIPKs)<\/h2>\n\n\n\n<p>In the <code>PRIMARY KEY<\/code> world, some of you may come across terms such as GIPK \u2013 as briefly mentioned earlier, such a term stands for a <em>Generated Invisible Primary Key<\/em>. If a table doesn\u2019t have any defined primary keys and it has no unique indexes with column values defined as <code>NOT NULL<\/code>, it has a GIPK.<\/p>\n\n\n\n<p>Generated Invisible Primary Keys have a couple of limitations unique to themselves:<\/p>\n\n\n<div class=\"block-core-list\">\n<ol class=\"wp-block-list\">\n<li>Generated Invisible Primary Keys are only available for tables running the <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.4\/en\/innodb-storage-engine.html\" target=\"_blank\" rel=\"noreferrer noopener\">InnoDB storage engine<\/a>.<br><br><\/li>\n\n\n\n<li>The creation of GIPK can be controlled by turning system variables such as <code>sql_generate_invisible_primary_key<\/code> to <code>ON<\/code>. If this value is set to <code>ON<\/code> (the default value is <code>OFF<\/code>), MySQL will build primary key values automatically (this variable is not available in<a href=\"https:\/\/mariadb.org\/\" target=\"_blank\" rel=\"noreferrer noopener\"> MariaDB<\/a>).<br><br><\/li>\n\n\n\n<li>A generated primary key cannot be altered, but it can be set to be visible or invisible. To make a generated primary key visible or invisible, make use of the <code>ALTER TABLE<\/code> statement after confirming the column name by inspecting the table structure:<br><code>ALTER TABLE 'demo' ALTER COLUMN 'my_row_id' SET VISIBLE\/INVISIBLE;<\/code><br><code><br><\/code><\/li>\n\n\n\n<li>Generated Invisible Primary Keys can be skipped during backup operations. This can be done by specifying the <code>mysqldump<\/code> option of <code>--skip-generated-invisible-primary-key.<\/code><br><\/li>\n<\/ol>\n<\/div>\n\n\n<p>By default, the InnoDB storage engine will always create a hidden Primary Key if one isn\u2019t provided (see explanations above), but by providing this feature, MySQL aims to make primary keys <em>visible and usable<\/em> to the end user. The downside of GIPK is that they\u2019re only available in newer versions of MySQL \u2013 they\u2019ve been deployed in MySQL 8.0.30 and act as special kinds of invisible columns. Users of MySQL can get them to work by setting the <code>sql_generate_invisible_primary_key<\/code> variable to <code>ON<\/code>, but MariaDB won\u2019t find this value at the helm:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"928\" height=\"179\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-35.png\" alt=\"\" class=\"wp-image-107451\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-35.png 928w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-35-300x58.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/07\/image-35-768x148.png 768w\" sizes=\"auto, (max-width: 928px) 100vw, 928px\" \/><figcaption class=\"wp-element-caption\"><em>Image 7 &#8211; No Generated Invisible Primary Keys in MariaDB<\/em><\/figcaption><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>Regardless, users of MySQL can make use of Generated Invisible Primary Keys like so:<\/p>\n\n\n<div class=\"block-core-list\">\n<ol class=\"wp-block-list\">\n<li>Set the variable of <code>sql_generate_invisible_primary_key<\/code> to <code>ON<\/code> by running <code>SET sql_generate_invisible_primary_key=ON<\/code><br><br><\/li>\n\n\n\n<li>Create a table without an explicit Primary Key:<br><code>CREATE TABLE `demo_table`(`column` VARCHAR(50));<\/code><br><br><\/li>\n\n\n\n<li>Check the table schema by running a <code>SHOW CREATE TABLE<\/code> query:<br><code>SHOW CREATE TABLE demo_table\\G<\/code><\/li>\n<\/ol>\n<\/div>\n\n\n<p>You will see something like so:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">mysql&gt; SHOW CREATE TABLE demo_tableG\n\n*************************** 1. row ***************************\n\nTable: demo_table\n\nCreate Table: CREATE TABLE `demo_table` (\n\n  `my_row_id` bigint unsigned NOT NULL AUTO_INCREMENT \/*!80023 INVISIBLE *\/,\n  `column` varchar(50) DEFAULT NULL,\n\n  PRIMARY KEY (`my_row_id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 \nCOLLATE=utf8mb4_general_ci<\/pre><\/div>\n\n\n\n<p>The <code>my_row_id<\/code> column has an invisible auto-incrementing primary key! That also means three things for you as a developer\/DBA:<\/p>\n\n\n<div class=\"block-core-list\">\n<ol class=\"wp-block-list\">\n<li>If you\u2019re using MySQL 8 or newer with the variable <code>sql_generate_invisible_primary_key<\/code> set to <code>ON<\/code>, none of your tables can have a column with the name of <code>my_row_id<\/code>.<br><br><\/li>\n\n\n\n<li>The <code>my_row_id<\/code> GIPK will always have a data type and it will always be <code>BIGINT UNSIGNED<\/code>. <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.4\/en\/create-table-gipks.html\" target=\"_blank\" rel=\"noreferrer noopener\">This is formally documented<\/a> and stable as of 8.0.30.<br><br><\/li>\n\n\n\n<li>The column will always have an <code>AUTO_INCREMENT<\/code> option set up.<\/li>\n<\/ol>\n<\/div>\n\n\n<p>All this also means that if you insert rows into your table, you will finally be able to select the <code>My_row_id<\/code> column and see all of the applicable values:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">mysql&gt; SELECT `my_row_id`, `name` FROM `demo_table`;\n+-----------+------+\n| my_row_id | name |\n+-----------+------+\n|         1 | Demo |\n|         2 | Jack |\n|         3 | Josh |\n|         4 | ...  |\n|         5 | ...  |\n+-----------+------+\n5 rows in set (0.00 sec)<\/pre><\/div>\n\n\n\n<p>Keep in mind that as the column is invisible, you are unlikely to see the column in a result set if you use <code>SELECT * FROM `your_table`<\/code> or similar queries.<\/p>\n\n\n\n<p>More information on Generated Invisible Primary Keys can be found <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/create-table-gipks.html\" target=\"_blank\" rel=\"noreferrer noopener\">in the MySQL documentation.<\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-summary\">Summary<\/h2>\n\n\n\n<p>Primary keys are at the heart of relational database design. In MySQL, especially with the InnoDB engine, they&#8217;re not just identifiers &#8211; they define how your data is stored, retrieved, and related. A well-chosen primary key improves performance, ensures data integrity, and forms the basis for efficient joins and lookups.<\/p>\n\n\n\n<p>While <code>AUTO_INCREMENT<\/code> integers are popular, MySQL supports a wide range of data types for primary keys, including strings &#8211; provided they&#8217;re unique, non-null, and thoughtfully chosen. One table, one primary key &#8211; but that key can span multiple columns if needed.<\/p>\n\n\n\n<p>And if you forget to define a primary key? MySQL 8.0.30 or later may create one automatically behind the scenes, using a GIPK &#8211; helpful, but worth understanding.<\/p>\n\n\n\n<p>Choose wisely. Your future queries will thank you.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-primary-key-indexes-frequently-asked-questions\">PRIMARY KEY Indexes \u2013 Frequently Asked Questions<\/h2>\n\n\n\n<p><strong>Q: What is a primary key?<\/strong><em><br><\/em>A: A primary key is a column or set of columns that uniquely identify each row in a table. It must be unique and not null, and it&#8217;s backed by an index.<\/p>\n\n\n\n<p><strong>Q: Can a table have more than one primary key?<\/strong><em><br><\/em>A: No. A table can only have one primary key constraint &#8211; but it can involve multiple columns. A primary key that involves multiple columns is called a composite primary key.<\/p>\n\n\n\n<p><strong>Q: Can primary keys contain <code>NULL<\/code> values?<\/strong><em><br><\/em>A: No. By definition, primary keys are always <code>NOT NULL<\/code>. MySQL enforces this even if you forget to declare it.<\/p>\n\n\n\n<p><strong>Q: Do primary keys have to be integers?<\/strong><em><br><\/em>A: Not at all. MySQL allows <code>CHAR<\/code>, <code>VARCHAR<\/code>, and other data types. Use what best fits your data &#8211; just keep it unique and stable. <\/p>\n\n\n\n<p><strong>Q: What&#8217;s the deal with Generated Invisible Primary Keys (GIPKs)?<\/strong><br>A: In MySQL 8.030+, if no primary or unique key is defined, MySQL may silently create an invisible <code>BIGINT<\/code> primary key (<code>my_row_id<\/code>) to ensure every row has a unique identifier. This helps replication and storage, but it&#8217;s best not to rely on it unless you understand the trade-offs.<\/p>\n\n\n\n<p><strong>Q: How do primary key indexes help performance?<\/strong><br>A: In InnoDB, the primary key determines the physical row order. That means faster lookups, efficient joins, and good use of caching &#8211; especially when your queries filter or join on the PK.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-appendix-table-structure-amp-data\">Appendix \u2013 Table Structure &amp; Data<\/h2>\n\n\n\n<p>The example data show in this article was generated using Mockaroo. The table structure used for most queries looked like this:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \" >CREATE TABLE demo_table (\n  id INT PRIMARY KEY AUTO_INCREMENT,\n  message VARCHAR(120) NOT NULL DEFAULT '',\n  sender VARCHAR(120) NOT NULL DEFAULT '',\n  receiver VARCHAR(120) NOT NULL DEFAULT '',\n  timestamp TIMESTAMP NOT NULL\n);<\/pre><\/div>\n\n\n\n<p><em>Note: While the column is called <\/em><code><em>timestamp<\/em><\/code><em>, the sample values were simply time-like strings rather than precise SQL timestamps. Always check and validate generated data types when mocking tables.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to use PRIMARY KEY indexes in MySQL &#8211; a type of B-tree index designed to help a database organize data.&hellip;<\/p>\n","protected":false},"author":339547,"featured_media":107456,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143523,145792],"tags":[4168,4170,5854,159360,4150,4151,4252],"coauthors":[146040],"class_list":["post-107444","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-databases","category-mysql","tag-database","tag-database-administration","tag-mysql","tag-primary-key-indexes","tag-sql","tag-sql-server","tag-t-sql-programming"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/107444","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=107444"}],"version-history":[{"count":8,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/107444\/revisions"}],"predecessor-version":[{"id":107550,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/107444\/revisions\/107550"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media\/107456"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=107444"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=107444"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=107444"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=107444"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}