{"id":8226,"date":"2023-05-01T09:49:20","date_gmt":"2023-05-01T07:49:20","guid":{"rendered":"https:\/\/code.openark.org\/blog\/?p=8226"},"modified":"2023-05-02T06:01:54","modified_gmt":"2023-05-02T04:01:54","slug":"things-that-dont-work-well-with-mysqls-foreign-key-implementation","status":"publish","type":"post","link":"https:\/\/code.openark.org\/blog\/mysql\/things-that-dont-work-well-with-mysqls-foreign-key-implementation","title":{"rendered":"Things that don&#8217;t work well with MySQL&#8217;s FOREIGN KEY implementation"},"content":{"rendered":"<p>Foreign keys are an important database construct, that let you keep an aspect of data integrity within your relational model. Data integrity has many faces, most of which are application-specific. Foreign keys let you maintain an association integrity between rows in two tables, one being the <em>parent<\/em>, the other being the\u00a0<em>child<\/em>. I&#8217;ve personally mostly avoided using foreign keys in MySQL for many years in my professional work, for several reasons. I&#8217;d like to now highlight things I find to be wrong\/broken with the MySQL implementation.<\/p>\n<p>But, first, as in the past this caused some confusion: when I say I&#8217;m not using foreign keys, that does not mean I don&#8217;t <code>JOIN<\/code> tables. I still <code>JOIN<\/code> tables. I still have some <code>id<\/code> column in one table and some <code>parent_id<\/code> in another. I still use the benefit of the relational model. In a sense, I do use foreign keys. What I don&#8217;t normally is the foreign key <code>CONSTRAINT<\/code>, i.e. the declaration of a <code>CONSTRAINT some_fk FOREIGN KEY ...<\/code> in a table&#8217;s definition.<\/p>\n<p>So here are things I consider to be broken, either specific to the MySQL implementation, or in the general concept. Some are outright deal breakers for environments I&#8217;ve worked with. Others are things to work around. In no particular order:<\/p>\n<h2>No binary log entries for cascaded writes<\/h2>\n<p>I think there are many people unaware of this. In a way, MySQL doesn&#8217;t really support foreign keys. The InnoDB engine does. This is old history, from before InnoDB was even officially a MySQL technology, and was developed independently as a 3rd party product. There was a time when MySQL sought alternative engines. There was a time when there was a plan to implement foreign keys in MySQL, above the storage engine level. But as history goes, MySQL and InnoDB both became one with Oracle acquiring both, and I&#8217;m only guessing implementing foreign keys in MySQL became lower priority, to be eventually abandoned.<\/p>\n<p>Alas, the fact foreign keys are implemented in the storage engine level has dire consequences. The engine does not have direct access to the binary log. If you create a foreign key constraint with <code>ON DELETE|UPDATE<\/code> of <code>SET NULL<\/code> or <code>CASCADE<\/code>, you should be aware that cascaded operations are never written to the binary log. Consider these two tables:<!--more--><\/p>\n<pre>CREATE TABLE `parent_table` (\n  `id` int NOT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB;\n\nCREATE TABLE `child_table` (\n  `id` int NOT NULL,\n  `parent_id` int DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `parent_id_idx` (`parent_id`),\n  CONSTRAINT `child_parent_fk` FOREIGN KEY (`parent_id`) REFERENCES `parent_table` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB;\n\ninsert into parent_table values (1);\ninsert into child_table values (1, 1);\ninsert into child_table values (2, 1);\n<\/pre>\n<p>If you were to <code>DELETE FROM parent_table WHERE id=1<\/code>, then the two rows in <code>child_table<\/code> are also deleted, due to the <code>CASCADE<\/code> rule. However, only the <code>parent_table<\/code> deleted row is written in the binary log. The two <code>child_table<\/code> rows are deleted internally by the InnoDB engine. The assumption is that when a replica applies the <code>DELETE<\/code> on <code>parent_table<\/code>\u00a0the replica&#8217;s own InnoDB engine will likewise delete the two relevant <code>child_table<\/code> rows.<\/p>\n<p>Fair assumption. But we lose information along the way. As Change Data Captures are becoming more and more common, and as we stream changes from MySQL to other data stores, the <code>DELETE<\/code>s on <code>child_table<\/code> are never reflected and cannot be captured.<\/p>\n<h2>Online DDL, aka online schema changes<\/h2>\n<p>I&#8217;ve <a href=\"http:\/\/code.openark.org\/blog\/mysql\/the-problem-with-mysql-foreign-key-constraints-in-online-schema-changes\">written about this at length<\/a> in the past. But even that write up is incomplete!<\/p>\n<p>MySQL is pushing towards <code>INSTANT<\/code> DDL, which is a wonderful thing. With <code>8.0.29<\/code>, even more schema change operations are supported by <code>ALGORITHM=INSTANT<\/code>. But, there&#8217;s still quite a lot of operations unsupported yet, and until such time that <code>INSTANT<\/code> DDL supports all (or at least all common) schema changes, Online Schema Change tools like <code>gh-ost<\/code>, <code>pt-online-schema-change<\/code>, and <code>Vitess<\/code> (disclaimer: I&#8217;m a <code>Vitess<\/code> maintainer and actively developing <code>Vitess'<\/code>s Online DDL), are essential when it comes to production changes.<\/p>\n<p>Both <code>Vitess<\/code> and <code>gh-ost<\/code> tail the binary logs to capture changes to the table. In light of the previous section, it is impossible to run such an Online Schema Change operation on a foreign key child table that has either <code>SET NULL<\/code> or <code>CASCADE<\/code> rule. The changes to the table are never reflected in the binary log. <code>pt-online-schema-change<\/code> is also unable to detect those changes as there&#8217;s nothing to invoke the triggers.<\/p>\n<p>Then, please do go ahead and read <a href=\"http:\/\/code.openark.org\/blog\/mysql\/the-problem-with-mysql-foreign-key-constraints-in-online-schema-changes\" rel=\"bookmark\">The problem with MySQL foreign key constraints in Online Schema Changes<\/a>, as it goes deep into what it otherwise means to deal with FK constraints in Online DDL, as it cannot fit in this post.<\/p>\n<h2>Locked data types<\/h2>\n<p>In the above table definitions, <code>id<\/code> and <code>parent_id<\/code> are <code>int<\/code>. As data grows, I might realize the choice of data type was wrong. I really should have used <code>bigint unsigned<\/code>.<\/p>\n<p>Alas, it is <em>impossible<\/em> to change the data type in either <code>parent_table<\/code> or <code>child_table<\/code>:<\/p>\n<pre>&gt; alter table parent_table modify column id bigint unsigned;\nERROR 3780 (HY000): Referencing column 'parent_id' and referenced column 'id' in foreign key constraint 'child_parent_fk' are incompatible.\n\n> alter table child_table modify column parent_id bigint unsigned;\nERROR 3780 (HY000): Referencing column 'parent_id' and referenced column 'id' in foreign key constraint 'child_parent_fk' are incompatible.\n<\/pre>\n<p>It&#8217;s impossible to do that with straight-DDL (never mind <code>INSTANT<\/code>), and it&#8217;s impossible to do that with Online DDL. InnoDB (not MySQL) flatly refuses to accept any change in the related columns&#8217;s data type. Well, it&#8217;s not really about\u00a0<i>changing<\/i>\u00a0them as it is about having an\u00a0<i>incompatibility<\/i>. But then, we can&#8217;t change either. The column type changes are only made possible if we modify the child table to remove the foreign key constraint, then alter both parent and child to modify the respective columns types, then re-add the foreign key constraint. There are four different <code>ALTER TABLE<\/code> statements. Neither removing nor adding a foreign key constraint is supported in <code>INSTANT<\/code> algorithm, so you can expect a long time in which the foreign key relationship simply does not exist!<\/p>\n<h2>CREATE TABLE &#8230; LIKE<\/h2>\n<p>One of those quirks that come with InnoDB owning the foreign key definition, is that <code>CREATE TABLE ... LIKE<\/code> does not generate foreign keys. I think this is mostly an oversight. A <code>SHOW CREATE TABLE<\/code> statement does produce foreign key output, so I&#8217;m not sure why <code>CREATE TABLE ... LIKE<\/code> doesn&#8217;t. Continuing our above <code>child_table<\/code> example:<\/p>\n<pre>&gt; create table child_table_copy like child_table;\nQuery OK, 0 rows affected (0.06 sec)\n\n> show create table child_table_copy \\G\n*************************** 1. row ***************************\n       Table: child_table_copy\nCreate Table: CREATE TABLE `child_table_copy` (\n  `id` int NOT NULL,\n  `parent_id` int DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `parent_id_idx` (`parent_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci\n<\/pre>\n<h2>Unique constraint names<\/h2>\n<p>I know this is ANSI SQL, and so I won&#8217;t fault MySQL for this. I do think this is one of those scenarios where deviating from ANSI SQL would be beneficial. A foreign key constraint has a name (if you don&#8217;t provide one, one is auto-generated for you). And, that name, according to ANSI SQL, has to be unique across your schema. It means the following table conflicts with our original <code>child_table<\/code>:<\/p>\n<pre>CREATE TABLE `another_child_table` (\n  `id` int NOT NULL,\n  `parent_id` int DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `parent_id_idx` (`parent_id`),\n  CONSTRAINT `child_parent_fk` FOREIGN KEY (`parent_id`) REFERENCES `parent_table` (`id`) ON DELETE CASCADE\n) ENGINE=InnoDB;\n<\/pre>\n<p>You can&#8217;t have two foreign key constraints both named <code>child_parent_fk<\/code>.<\/p>\n<p>I never understood that limitation. See, it&#8217;s just fine the tables both have a key named <code>parent_id_idx<\/code>. No conflict about that. Why do foreign keys have to have unique names?<\/p>\n<p>Maybe, in ANSI SQL, foreign keys can be independent constructs, living outside the table scope. Meh, even so this could be technically solved using some sort of namespace. But, in MySQL this isn&#8217;t the case in the first place. Foreign keys are part of the table definition.<\/p>\n<p>This is again just painful for Online DDL, or for any automation that tries to duplicate tables on the fly.<\/p>\n<h2>Lack of declarative-only definitions<\/h2>\n<p>This is more of a &#8220;I wish this existed&#8221; rather than &#8220;this is wrong&#8221;. One of the greatest benefits of foreign keys is the <em>graph<\/em>. Given a schema with foreign keys, you can formally analyze the relationships between tables. You can draw the dependency graph. It&#8217;s really educating.<\/p>\n<p>What I wish for is to have a declarative-only foreign key definition. One that does not actually\u00a0<em>enforce<\/em> anything. Merely indicates an association. Something like so:<\/p>\n<pre>CREATE TABLE `child_table` (\n  `id` int NOT NULL,\n  `parent_id` int DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  KEY `parent_id_idx` (`parent_id`),\n  DECLARATIVE FOREIGN KEY (`parent_id`) REFERENCES `parent_table` (`id`)\n)\n<\/pre>\n<p>The declarative foreign key could still enforce the existence of the parent table and referenced column, definition-wise, but do nothing at all to enforce relationship of data.<\/p>\n<p>Anyway, just a wish.<\/p>\n<h2>SET FOREIGN_KEY_CHECKS<\/h2>\n<p>I love that we have <code>set foreign_key_checks<\/code>. But it&#8217;s a bit inconsistent. Basically, <code>set foreign_key_checks=0<\/code> lets you override foreign key constraints. You can do any of the following:<\/p>\n<ul>\n<li><code>INSERT<\/code> data to a child table even if the parent table does not have matching values.<\/li>\n<li>With <code>NO ACTION\/RESTRICT<\/code> rule, <code>DELETE<\/code> data from a parent table even if children tables have matching rows.<\/li>\n<li>With <code>SET NULL\/CASCADE<\/code> rule, <code>DELETE<\/code> data from a parent table without even attempting to cascade the change to children tables.<\/li>\n<li><code>CREATE TABLE child_table<\/code> that references <code>parent_table<\/code> even if <code>parent_table<\/code> does not exist.<\/li>\n<li><code>DROP TABLE parent_table<\/code> even if <code>child_table<\/code> exists and is populated.<\/li>\n<\/ul>\n<p>But, why oh why, will <code>set foreign_key_checks=0<\/code> not let me:<\/p>\n<ul>\n<li><code>alter table parent_table modify column id bigint unsigned;<\/code>(column type relationship are still enforced)<\/li>\n<li>Swap a new parent table using a <code>RENAME TABLE<\/code> statement (wishful feature, would really help Online DDL)<\/li>\n<\/ul>\n<h2>Limited to server scope<\/h2>\n<p>This one becomes obvious as your data grows. If you use foreign keys and you <em>rely<\/em> on their behavior (e.g. your app relies on a <code>DELETE<\/code> to fail if there&#8217;s dependent rows in children tables), and your data set grows such that a single server does not have the write capacity, you&#8217;re in trouble.<\/p>\n<p>You may attempt to do functional sharding. You will hopefully find two subsets of your schema&#8217;s tables, that are not connected in the foreign key graph. If so, you win the day. But if it&#8217;s all connected, then you have to break some relationships. You&#8217;d have to audit your app. It previously assume the database would take care of data integrity, and now, for some relationships, it wouldn&#8217;t.<\/p>\n<p>Or you may want to have horizontal sharding. If you mean to keep foreign key constraints, that means you need to find a way to co-locate data across the entire dependency graph. Unless this was pre-designed, you will probably find this to be impossible without a major refactor.<\/p>\n<p><code>Vitess<\/code> is looking into <code>FOREIGN KEY<\/code> implementation. It will attempt to address some of the above limitations. See <a href=\"https:\/\/github.com\/vitessio\/vitess\/issues\/11975\">https:\/\/github.com\/vitessio\/vitess\/issues\/11975<\/a> and <a href=\"https:\/\/github.com\/vitessio\/vitess\/issues\/12967\">https:\/\/github.com\/vitessio\/vitess\/issues\/12967<\/a> for some preliminary write ups and tracking.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Foreign keys are an important database construct, that let you keep an aspect of data integrity within your relational model. Data integrity has many faces, most of which are application-specific. Foreign keys let you maintain an association integrity between rows in two tables, one being the parent, the other being the\u00a0child. I&#8217;ve personally mostly avoided [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"enabled":false},"version":2}},"categories":[5],"tags":[],"class_list":["post-8226","post","type-post","status-publish","format-standard","hentry","category-mysql"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/p2bZZp-28G","_links":{"self":[{"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/posts\/8226","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/comments?post=8226"}],"version-history":[{"count":15,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/posts\/8226\/revisions"}],"predecessor-version":[{"id":8241,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/posts\/8226\/revisions\/8241"}],"wp:attachment":[{"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/media?parent=8226"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/categories?post=8226"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/tags?post=8226"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}