{"id":112395,"date":"2026-10-26T12:00:00","date_gmt":"2026-10-26T12:00:00","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=112395"},"modified":"2026-09-29T15:32:53","modified_gmt":"2026-09-29T15:32:53","slug":"the-key-syntax-differences-between-mysql-and-postgresql-everything-you-should-know","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/the-key-syntax-differences-between-mysql-and-postgresql-everything-you-should-know\/","title":{"rendered":"The key syntax differences between MySQL and PostgreSQL &#8211; everything you should know"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><strong>Discover the most common SQL syntax differences developers encounter when moving from MySQL to PostgreSQL, including identifier quoting, identifier rules, reserved keywords, limit vs. fetch first, inserting and updating data, and truncating tables.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Throughout this article &#8211; part of Aisha Bukar&#8217;s <a href=\"https:\/\/www.red-gate.com\/simple-talk\/collections\/aisha-bukar-series-comparing-mysql-and-postgresql\/\" target=\"_blank\" rel=\"noreferrer noopener\">complete MySQL vs PostgreSQL comparison guide<\/a> &#8211; you&#8217;ll also learn <em>why<\/em> these differences exist, and how to adapt your queries to work effectively across both databases.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Moving an application from <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/getting-started-mysql\/\" target=\"_blank\" rel=\"noreferrer noopener\">MySQL<\/a> to <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/\" target=\"_blank\" rel=\"noreferrer noopener\">PostgreSQL<\/a> involves more than exporting data and changing a few SQL statements. While both are <a href=\"https:\/\/db-engines.com\/en\/article\/Relational+DBMS?ref=RDBMS\" target=\"_blank\" rel=\"noreferrer noopener\">relational database management systems<\/a>, they interpret and extend SQL differently &#8211; and that gap has widened over the years as each system introduced its own syntax conventions, default behaviors, and features.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That&#8217;s exactly why some SQL statements look nearly identical across both systems, but others require syntax changes or a different approach altogether. Some of these easy to spot &#8211; for example, a query that executes successfully in MySQL may produce an error in PostgreSQL, because the latter enforces SQL standards more strictly.  It may also return different results because of the differences in default behavior.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Similarly, features such as <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/statements\/set-quoted-identifier-transact-sql?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">identifier quoting<\/a>, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/the-secret-life-of-database-keys\/#:~:text=auto%2Dgenerated%20keys%2C\" target=\"_blank\" rel=\"noreferrer noopener\">auto-generated keys<\/a>, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/pagination-and-ordering-by-large-values\/\" target=\"_blank\" rel=\"noreferrer noopener\">pagination<\/a>, and data manipulation are implemented differently &#8211; making <a href=\"https:\/\/www.red-gate.com\/simple-talk\/collections\/migrating-from-on-prem-to-the-cloud-dba-stories\/\" target=\"_blank\" rel=\"noreferrer noopener\">database migration<\/a> more than just a simple copy-and-paste exercise.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-identifier-quoting-in-mysql-and-postgresql-the-key-differences-explained\">Identifier quoting in MySQL and PostgreSQL &#8211; the key differences explained<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One of the first syntax differences developers notice when moving from MySQL to PostgreSQL is how <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/theory-and-design\/uniqueness-keys-and-identity\/#:~:text=and%20compound%20keys.-,Simple%20Keys,-A%20simple%20key\" target=\"_blank\" rel=\"noreferrer noopener\">identifiers<\/a> are quoted. Identifiers include database objects such as table names, column names, and schema names.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Firstly, MySQL uses backticks (`) to quote identifiers:<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>SELECT `first_name`, `last_name`FROM `employees`;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>PostgreSQL, on the other hand, follows the SQL standard by using double quotes (&#8220;):<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>SELECT \"first_name\", \"last_name\"FROM \"employees\";<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Interestingly, MySQL can also use double quotes for identifiers, but only when the <code><a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.4\/en\/sql-mode.html#sqlmode_ansi_quotes\" target=\"_blank\" rel=\"noreferrer noopener\">ANSI_QUOTES SQL<\/a><\/code> mode is enabled. Without this mode, double quotes are interpreted as <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/find-and-replace-text-in-strings-in-t-sql\/\" target=\"_blank\" rel=\"noreferrer noopener\">string delimiters<\/a> rather than identifier quotes. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Since <code>ANSI_QUOTES<\/code> is <em>not<\/em> enabled by default in most MySQL installations, many applications and codebases continue to use backticks.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This difference often becomes apparent during migration. <strong>If MySQL queries are copied into PostgreSQL without modification, any identifiers enclosed in backticks will cause <a href=\"https:\/\/www.geeksforgeeks.org\/c\/what-is-a-syntax-error-and-how-to-solve-it\/\" target=\"_blank\" rel=\"noreferrer noopener\">syntax errors<\/a>, because PostgreSQL does <em>not<\/em> recognize backticks as identifier delimiters.&nbsp;<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Replacing backticks with double quotes is usually straightforward, but developers should be aware that quoted identifiers in PostgreSQL become case-sensitive. An identifier created as <em>&#8220;Employees&#8221;<\/em>, for example, is different from <em>employees<\/em>, which can introduce unexpected errors if <a href=\"https:\/\/www.red-gate.com\/simple-talk\/other\/sql-naming-conventions\/\" target=\"_blank\" rel=\"noreferrer noopener\">naming conventions<\/a> are inconsistent.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is why many PostgreSQL developers avoid quoting identifiers unless necessary &#8211; preferring lowercase, unquoted names that are easier to reference across applications.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">While replacing backticks with double quotes is usually one of the simpler syntax changes during migration, it&#8217;s only one part of the story. Identifiers often need to be quoted because they conflict with SQL keywords or violate a database&#8217;s naming rules. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Since MySQL and PostgreSQL don&#8217;t reserve exactly the same keywords or enforce identifier rules in the same way, an acceptable object name in one database may require quoting or renaming in the other.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Understanding these differences is essential for avoiding syntax errors and ensuring that schemas and queries remain portable across both systems.<\/p>\n\n\n\n<section id=\"my-first-block-block_f2cd2984ef6770d28208cde7864294ed\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Get started with PostgreSQL &#8211; free book download<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            &#8216;Introduction to PostgreSQL for the data professional&#8217;, written by Grant Fritchey and Ryan Booz, covers all the basics of how to get started with PostgreSQL.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/hub\/books\/introduction-to-postgresql-for-the-data-professional\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Download your free copy: Get started with PostgreSQL - free book download\">Download your free copy<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-what-are-the-key-differences-in-reserved-keywords-and-identifier-rules-in-mysql-compared-to-postgresql\">What are the key differences in reserved keywords and identifier rules in MySQL compared to PostgreSQL?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>When writing SQL, identifiers are the names assigned to database objects such as tables, columns, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/mastering-sql-views\/\" target=\"_blank\" rel=\"noreferrer noopener\">views<\/a>, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/learn\/sql-server-index-basics\/\" target=\"_blank\" rel=\"noreferrer noopener\">indexes<\/a>, and schemas. Although developers are free to choose nearly <em>any<\/em> descriptive name, those names can&#8217;t conflict with the database&#8217;s reserved keywords.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/language-elements\/reserved-keywords-transact-sql?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">Reserved keywords<\/a> are words that have a predefined meaning in the SQL language. <\/strong>Statements such as <code><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/the-basic-t-sql-select-statement\/\" target=\"_blank\" rel=\"noreferrer noopener\">SELECT<\/a><\/code>, <code>FROM<\/code>, <code>WHERE<\/code>, and <code>ORDER<\/code> are reserved because they form part of SQL syntax. Attempting to use these words as identifiers without proper quoting can cause parsing errors or unexpected behavior.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>MySQL and PostgreSQL maintain different sets of reserved keywords.<\/strong> PostgreSQL generally reserves more keywords because it more closely adheres to the SQL standard <em>and<\/em> includes additional language features. Consequently, a valid identifier in MySQL may be rejected by PostgreSQL.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For example, consider the following table definition:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>CREATE TABLE user (id INT PRIMARY KEY,name VARCHAR(100));<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Depending on the database configuration and version, this statement may execute successfully in MySQL, but PostgreSQL treats user as a reserved keyword because it refers to the current <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/postgresql-basics-roles-and-privileges\/\" target=\"_blank\" rel=\"noreferrer noopener\">database role<\/a>. To use it as a table name, the identifier must be quoted:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>CREATE TABLE \"user\" (id INTEGER PRIMARY KEY,name VARCHAR(100));<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Although quoting resolves the syntax error, it introduces another consideration. In PostgreSQL, quoted identifiers preserve their original letter casing and become case-sensitive. For example, a table created as <em>&#8220;User&#8221;<\/em> must always be referenced using exactly the same capitalization and quotation marks.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">By contrast, unquoted identifiers are automatically folded to lowercase, making them easier to reference consistently. A common example is <em>user<\/em>. It&#8217;s not a reserved keyword in MySQL, but has special significance in PostgreSQL because it&#8217;s associated with the current database role. <\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-why-you-should-rename-conflicting-identifiers-instead-of-relying-on-quoted-names\">Why you should rename conflicting identifiers instead of relying on quoted names<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Although quoting such identifiers can resolve syntax errors, relying heavily on quoted names often makes SQL harder to read and maintain, particularly in large applications with hundreds of queries.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For this reason, many migration projects prefer to rename conflicting identifiers instead of relying on quoted names. Using descriptive, lowercase identifiers that avoid reserved keywords improves readability, reduces portability issues, and minimizes the likelihood of syntax errors when moving between database systems.<\/p>\n\n\n\n<div id=\"callout-block_489fe47bcfa20a175e79b73ac61b070f\" class=\"callout alignnone\">\n    <div class=\"child-last:mb-0 child-first:mt-0 bg-gray-50 dark:bg-gray-950 p-4xl my-3xl\">\n\n<p class=\"wp-block-paragraph\"><strong>Want to advance your MySQL or PostgreSQL learning?<\/strong> <strong>You may be interested in:<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/\" target=\"_blank\" rel=\"noreferrer noopener\">Simple Talk&#8217;s full archive of MySQL content (articles, guides, and more)<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/\" target=\"_blank\" rel=\"noreferrer noopener\">Simple Talk&#8217;s full archive of PostgreSQL content (articles, guides, and more)<\/a><\/p>\n\n<\/div>\n<\/div> \n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-to-avoid-naming-conflicts-with-reserved-keywords-in-mysql-and-postgresql\">How to avoid naming conflicts with reserved keywords in MySQL and PostgreSQL<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Since the list of reserved keywords evolves as new SQL features are introduced, it&#8217;s good practice to always verify whether an identifier is reserved <em>before<\/em> creating or migrating a schema. Detecting and correcting any naming conflicts early is considerably easier than discovering syntax errors <em>after<\/em> the application has been deployed.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To help with this, MySQL provides a <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.4\/en\/keywords.html#keywords-8-4-detailed-U\" target=\"_blank\" rel=\"noreferrer noopener\">comprehensive list of keywords in its documentation<\/a>, where entries marked with (R) indicate reserved words that cannot be used as identifiers without quoting. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">PostgreSQL likewise maintains an <a href=\"https:\/\/www.postgresql.org\/docs\/current\/sql-keywords-appendix.html\" target=\"_blank\" rel=\"noreferrer noopener\">appendix listing all SQL keywords<\/a>, indicating whether each word is reserved, non-reserved, or has other parser-specific classifications throughout. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Consulting these references during a migration can help identify naming conflicts before they become syntax errors.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-comparing-mysql-auto-increment-with-postgresql-serial-and-identity-the-key-differences-explained\">Comparing MySQL <code>AUTO_INCREMENT<\/code> with PostgreSQL <code>SERIAL<\/code> and <code>IDENTITY<\/code> (the key differences explained)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Automatically-generated <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/learn\/primary-key-primer-for-sql-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">primary keys<\/a> are a common feature of relational databases, but MySQL and PostgreSQL have different ways of implementing them. <\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>This is often one of the first schema changes developers encounter during migration because the <code><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/mysql-index-overviews-primary-key-indexes\/#:~:text=cannot%20be%20null-,AUTO_INCREMENT,-allows%20you%20to\" target=\"_blank\" rel=\"noreferrer noopener\">AUTO_INCREMENT<\/a><\/code> attribute used in MySQL is <em>not<\/em> recognized by PostgreSQL.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In MySQL, a column is made self-incrementing by applying the <code>AUTO_INCREMENT<\/code> attribute. Whenever a new row is inserted without an explicit value for that column, MySQL automatically generates the next available integer. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This approach is simple, and tightly integrated into the table definition:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>CREATE TABLE customers (&nbsp;&nbsp;&nbsp;&nbsp;customer_id INT AUTO_INCREMENT PRIMARY KEY,&nbsp;&nbsp;&nbsp;&nbsp;name VARCHAR(100) NOT NULL);<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Attempting to use the same definition in PostgreSQL results in a syntax error, because PostgreSQL does not support the <code>AUTO_INCREMENT<\/code> keyword<\/strong>:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>..imAGE<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Historically, PostgreSQL solved this problem using the <code>SERIAL<\/code> pseudo-type:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>CREATE TABLE customers (&nbsp;&nbsp;&nbsp;&nbsp;customer_id SERIAL PRIMARY KEY,&nbsp;&nbsp;&nbsp;&nbsp;name VARCHAR(100) NOT NULL);<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Although <code><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql-vs-postgresql-numeric-datatype-comparison\/#:~:text=Financial%20data-,SERIAL,-It%20is%20an\" target=\"_blank\" rel=\"noreferrer noopener\">SERIAL<\/a><\/code> <em>looks<\/em> like a data type, it&#8217;s actually a convenient shorthand. Behind the scenes, PostgreSQL creates an integer column, creates a separate sequence object, sets the column&#8217;s default value on the sequence to <code>nextval()<\/code>, and associates the sequence with the column. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In other words, <code>SERIAL<\/code> is a shortcut for creating a sequence-backed auto-generating column rather than a true data type.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Modern PostgreSQL also supports SQL-standard identity columns using the <code>GENERATED ... AS IDENTITY<\/code> clause:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>CREATE TABLE customers (&nbsp;&nbsp;&nbsp;&nbsp;customer_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,&nbsp;&nbsp;&nbsp;&nbsp;name VARCHAR(100) NOT NULL);<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Additionally, PostgreSQL supports <code>GENERATED BY DEFAULT AS IDENTITY<\/code>, which generates values automatically but allows applications to provide explicit values when needed. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">By contrast, <code>GENERATED ALWAYS AS IDENTITY<\/code> strictly enforces automatic value generation by rejecting user-supplied values <em>unless<\/em> the <code><a href=\"https:\/\/www.postgresql.org\/docs\/current\/sql-insert.html\" target=\"_blank\" rel=\"noreferrer noopener\">INSERT<\/a><\/code> statement explicitly includes the <code>OVERRIDING SYSTEM VALUE<\/code> clause. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This makes <code>GENERATED ALWAYS<\/code> a better choice when you want to prevent accidental modification of identity values while still retaining the option to override them in exceptional cases.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>For new PostgreSQL applications, <code><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/identity-vs-sequence-in-sql-server-which-should-you-use\/\" target=\"_blank\" rel=\"noreferrer noopener\">IDENTITY<\/a><\/code> is generally preferred over <code>SERIAL<\/code> because it conforms to the SQL standard, clearly expressing that the column&#8217;s values are generated automatically. <\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The PostgreSQL documentation does still support <code>SERIAL<\/code> &#8211; and it remains common in existing schemas &#8211; but <code>IDENTITY<\/code> is the recommended choice for new database designs.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-why-you-shouldn-t-replace-auto-increment-with-serial-in-postgresql\">Why you <em>shouldn&#8217;t<\/em> replace <code>AUTO_INCREMENT<\/code> with <code>SERIAL<\/code> in PostgreSQL<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>A common mistake during migration is attempting to replace MySQL&#8217;s <code>AUTO_INCREMENT<\/code> with PostgreSQL&#8217;s <code>SERIAL<\/code> using a simple text substitution. While this resolves the immediate syntax error, it overlooks the fact that PostgreSQL uses sequences to generate values. <\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As a result, operations involving sequence management, resetting counters, or retrieving generated values may need to be updated.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For example, many MySQL applications retrieve the last generated identifier using <code>LAST_INSERT_ID()<\/code>. In PostgreSQL, the preferred approach is to use the <code>RETURNING<\/code> clause as part of the <code>INSERT<\/code> statement, which returns the generated value directly &#8211; <em>without<\/em> requiring a separate query.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Another important distinction is that, although a <code>SERIAL<\/code> column is created with a <code>NOT NULL<\/code> constraint, neither <code>SERIAL<\/code> or <code>IDENTITY<\/code> create a <code>PRIMARY KEY<\/code> or <code>UNIQUE<\/code> constraint automatically. These mechanisms define how values are generated &#8211; <em>not<\/em> how uniqueness is enforced. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If the column is intended to uniquely identify each row (as is typically the case for <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/the-secret-life-of-database-keys\/#:~:text=from%20all%20others.-,Why%20use%20a%20surrogate%20database%20key%3F,-Some%20tables%20lack\" target=\"_blank\" rel=\"noreferrer noopener\">surrogate keys<\/a>), you must explicitly declare a <code>PRIMARY KEY<\/code> or <code>UNIQUE<\/code> constraint.&nbsp;&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It&#8217;s worth noting that MySQL also provides a <code>SERIAL<\/code> alias, although &#8211; unlike PostgreSQL &#8211; it&#8217;s not a pseudo-type that creates and manages a <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/learn\/sql-server-sequence-basics\/\" target=\"_blank\" rel=\"noreferrer noopener\">sequence<\/a>. Instead, <code>SERIAL<\/code> is simply an alias for the following column definition:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Consequently, a MySQL <code>SERIAL<\/code> column is always an unsigned 64-bit integer with the <code>AUTO_INCREMENT<\/code> attribute <em>and<\/em> an implicit <code>UNIQUE<\/code> constraint. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In PostgreSQL, <code>SERIAL<\/code> behaves very differently: it expands into an integer column backed by a sequence object, but doesn&#8217;t automatically create a <code>PRIMARY KEY<\/code> or <code>UNIQUE<\/code> constraint.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>So, although both databases provide a <code>SERIAL<\/code> alias, they serve different purposes and should <em>not<\/em> be considered equivalent during migration.<\/strong><\/p>\n\n\n\n<div id=\"callout-block_489fe47bcfa20a175e79b73ac61b070f\" class=\"callout alignnone\">\n    <div class=\"child-last:mb-0 child-first:mt-0 bg-gray-50 dark:bg-gray-950 p-4xl my-3xl\">\n\n<p class=\"wp-block-paragraph\"><strong>Learn more about primary keys on Simple Talk<\/strong>:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/learn\/primary-key-primer-for-sql-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">Primary Key Primer for SQL Server<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/mysql-index-overviews-primary-key-indexes\/\" target=\"_blank\" rel=\"noreferrer noopener\">MySQL Index Overviews: PRIMARY KEY Indexes<\/a><\/p>\n\n<\/div>\n<\/div> \n\n\n<h3 class=\"wp-block-heading\" id=\"h-differing-implementation-philosophies\">Differing implementation philosophies <\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Finally, while <code>AUTO_INCREMENT<\/code>, <code>SERIAL<\/code>, and <code>IDENTITY<\/code> all address the same requirement of generating numeric identifiers, they reflect different implementation philosophies. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">MySQL exposes auto-incrementing behavior as a column attribute, whereas PostgreSQL historically relied on independent sequence objects before adopting the SQL-standard identity column syntax. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Understanding these implementation differences makes schema migration considerably smoother &#8211; helping to avoid issues that extend beyond simply replacing one keyword with another.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-comparing-mysql-limit-and-postgresql-fetch-first-the-key-differences-explained\">Comparing MySQL <code>LIMIT<\/code> and PostgreSQL <code>FETCH FIRST<\/code> (the key differences explained)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>In most cases, applications rarely need to retrieve <em>every<\/em> row from a table. Whether displaying search results or paginating records, it&#8217;s common to limit the number of rows returned by a query. <\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>While MySQL and PostgreSQL both support this capability, they offer different syntax options, with PostgreSQL also implementing the SQL-standard approach.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In MySQL, the <code>LIMIT<\/code> clause restricts the number of rows returned by a query:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>SELECT *FROM employeesLIMIT 10;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">PostgreSQL also supports the same syntax:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>SELECT *FROM employeesLIMIT 10;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>For developers migrating from MySQL to PostgreSQL, this is good news, because most queries using <code>LIMIT<\/code> work without modification.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When skipping rows, both databases support the <code>OFFSET<\/code> clause:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>SELECT *FROM employeesLIMIT 10 OFFSET 20;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The query skips the first 20 rows and returns the next 10. It&#8217;s commonly used for implementing pagination.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">MySQL also provides a shorthand form that combines the offset and row count into a single <code>LIMIT<\/code> clause:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>SELECT *FROM employeesLIMIT 20, 10;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">In this example, the first value specifies the number of rows to skip, while the second specifies the maximum number of rows to return. Although this syntax is widely used in MySQL applications, PostgreSQL does <em>not<\/em> support it. During migration, it should be rewritten using the explicit <code>LIMIT ... OFFSET<\/code> form.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In addition to <code>LIMIT<\/code>, PostgreSQL supports the SQL-standard <code>FETCH FIRST<\/code> clause.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>SELECT *FROM employeesFETCH FIRST 10 ROWS ONLY;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">To skip rows before fetching results, <code>FETCH FIRST<\/code> is typically combined with <code>OFFSET<\/code>.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>SELECT *FROM employeesOFFSET 20FETCH FIRST 10 ROWS ONLY;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Unlike <code>LIMIT<\/code>, <code>FETCH FIRST<\/code> is part of the SQL standard, making it portable across several relational database systems. Although PostgreSQL supports both approaches, <code>LIMIT<\/code> remains the syntax most commonly used in PostgreSQL applications because of its brevity and long-standing familiarity.<\/p>\n\n\n\n<section id=\"my-first-block-block_96953ae6951b165228dfa49af30d535f\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Move fast. Govern at scale.<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            Redgate Flyway Enterprise embeds guardrails in the database layer, so every change is policy-checked, deterministic, and traceable.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/products\/flyway\/enterprise\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Try for free: Move fast. Govern at scale.\">Try for free<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<h3 class=\"wp-block-heading\" id=\"h-why-these-small-syntax-differences-still-need-your-attention\">Why these small syntax differences still need your attention<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The syntax differences between MySQL and PostgreSQL are relatively small in this area, but they can still cause migration issues. Queries written using MySQL&#8217;s <code>LIMIT<\/code> offset, count syntax must be rewritten using <code>LIMIT<\/code> count <code>OFFSET<\/code> offset, or alternatively <code>OFFSET ... FETCH FIRST ... ROWS ONLY<\/code> if adopting the SQL-standard syntax.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Another important consideration is that neither <code>LIMIT<\/code> or <code>FETCH FIRST<\/code> guarantee <em>which<\/em> rows are returned unless the query includes an <code>ORDER BY<\/code> clause. Without an explicit ordering, the database is free to return rows in any order &#8211; and that order may differ between the two platforms. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is because both MySQL and PostgreSQL use their own query planner and <a href=\"https:\/\/www.red-gate.com\/simple-talk\/resources\/books\/simple-talk-published-books\/sql-server-execution-plans\/\" target=\"_blank\" rel=\"noreferrer noopener\">execution strategy<\/a>. To ensure consistent and repeatable results, always combine row-limiting clauses with an appropriate <code>ORDER BY<\/code> clause &#8211;<em>especially<\/em> when implementing pagination.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Although PostgreSQL supports both <code>LIMIT<\/code> and <code>FETCH FIRST<\/code>, choosing between them largely comes down to coding standards and portability requirements. <\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>LIMIT<\/code> is concise and familiar to developers coming from MySQL, while <code>FETCH FIRST<\/code> aligns closer to the SQL standard and may be preferable in environments where cross-database compatibility is a priority.<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-what-are-the-key-differences-in-how-the-insert-statement-works-in-mysql-compared-to-postgresql\">What are the key differences in how the <code>INSERT<\/code> statement works in MySQL compared to PostgreSQL?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The basic <code>INSERT<\/code> syntax is remarkably similar in MySQL and PostgreSQL. In both databases, you can specify the target columns and provide one or more rows of values:<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>INSERT INTO customers (name, email)VALUES ('Aisha', 'aisha@example.com');<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The same statement works in both databases. This means that simple <code>INSERT<\/code> statements are generally among the easier parts of a MySQL-to-PostgreSQL migration.<\/strong><\/p>\n\n\n\n<div id=\"callout-block_489fe47bcfa20a175e79b73ac61b070f\" class=\"callout alignnone\">\n    <div class=\"child-last:mb-0 child-first:mt-0 bg-gray-50 dark:bg-gray-950 p-4xl my-3xl\">\n\n<p class=\"wp-block-paragraph\">Want to learn more about the <code>INSERT<\/code> statement in MySQL? <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/introducing-the-mysql-insert-statement\/\" target=\"_blank\" rel=\"noreferrer noopener\">Read Robert Sheldon&#8217;s guide<\/a>.<\/p>\n\n<\/div>\n<\/div> \n\n\n<p class=\"wp-block-paragraph\">The differences become more noticeable when an application relies on database-specific behavior, particularly when inserting rows with generated values, handling <code>duplicate<\/code> records, or retrieving the values generated by the database.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One of the more significant differences appears when an application needs the value generated by the database after an <code>INSERT<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In MySQL, an application commonly retrieves the automatically generated ID using <code>LAST_INSERT_ID()<\/code>:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>INSERT INTO customers (name)VALUES ('Aisha');<br>SELECT LAST_INSERT_ID();<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">PostgreSQL provides the <code>RETURNING<\/code> clause, allowing the generated value to be returned as part of the <code>INSERT<\/code> statement itself:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>INSERT INTO customers (name)VALUES ('Aisha')RETURNING customer_id;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The PostgreSQL approach can also return multiple columns or the entire newly inserted row:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>INSERT INTO customers (name)VALUES ('Aisha')RETURNING *;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">This difference matters during application migration, as code expecting a separate call to retrieve the generated identifier may need rewriting to use PostgreSQL&#8217;s <code>RETURNING<\/code> clause instead.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-insert-ignore-and-on-duplicate-key-update\"><code>INSERT IGNORE<\/code> and <code>ON DUPLICATE KEY UPDATE<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Another important difference appears when an application requires a row insert <em>only<\/em> if it doesn&#8217;t already exist, or updates an existing row when a conflict occurs. MySQL uses <code>INSERT IGNORE<\/code> and <code>ON DUPLICATE KEY UPDATE<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>INSERT IGNORE<\/code> command tells the database to continue processing the statement when certain errors or warnings occur. For example:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>INSERT IGNORE INTO customers (customer_id, name)VALUES (1, 'Aisha');<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">If the value of <code>customer_id<\/code> conflicts with an existing unique or primary key value, MySQL can ignore the conflicting row instead of terminating the statement with an error.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">MySQL also uses the <code>ON DUPLICATE KEY UPDATE<\/code>. It basically tells the database to <em>not<\/em> fail if the insert violates a <code>PRIMARY KEY<\/code> or <code>UNIQUE<\/code> constraint. Instead, it should update the existing row.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>INSERT INTO customers (customer_id, name)VALUES (1, 'Aisha')ON DUPLICATE KEY UPDATE&nbsp;&nbsp;&nbsp;&nbsp;name = 'Aisha';<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-on-conflict\"><code>ON CONFLICT<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">PostgreSQL uses the <code>ON CONFLICT<\/code> command for this:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>INSERT INTO customers (customer_id, name)VALUES (1, 'Aisha')ON CONFLICT (customer_id)DO UPDATESET name = EXCLUDED.name;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">PostgreSQL can also ignore the conflicting row:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>INSERT INTO customers (customer_id, name)VALUES (1, 'Aisha')ON CONFLICT (customer_id)DO NOTHING;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The syntax is different but, more importantly, the two databases express the conflict-handling logic differently. MySQL&#8217;s syntax is centered around duplicate-key detection, while PostgreSQL allows the developer to specify a conflict target and then choose what happens when that conflict occurs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There&#8217;s no direct equivalent to <code>INSERT IGNORE<\/code> in PostgreSQL. When the intention is specifically to ignore a conflict involving a <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/theory-and-design\/uniqueness-keys-and-identity\/\" target=\"_blank\" rel=\"noreferrer noopener\">unique<\/a> or primary key constraint, PostgreSQL uses <code>ON CONFLICT DO NOTHING<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-what-are-the-key-differences-in-how-truncating-tables-works-in-mysql-compared-to-postgresql\">What are the key differences in how <code>TRUNCATING TABLES<\/code> works in MySQL compared to PostgreSQL?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>TRUNCATE<\/code> is used to remove all rows from a table <em>without<\/em> removing the table itself. At first, MySQL and PostgreSQL appear to handle it in much the same way. The table remains available for future inserts, but its existing rows are removed:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>TRUNCATE TABLE customers;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">However, the similarities become less obvious when you look at <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/learn\/sql-server-transaction-log-architecture\/\" target=\"_blank\" rel=\"noreferrer noopener\">transactions<\/a>, generated identifiers, and relationships between tables.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One of the important differences is how <code>TRUNCATE<\/code> interacts with transactions. PostgreSQL treats <code>TRUNCATE<\/code> as a transactional operation. It can be included in a transaction and rolled back if the transaction is subsequently aborted:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>BEGIN;<br>TRUNCATE TABLE customers;<br>ROLLBACK;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">After the rollback, any rows removed by <code>TRUNCATE<\/code> are restored.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">MySQL&#8217;s behavior depends on the storage engine and transaction context. With <a href=\"https:\/\/en.wikipedia.org\/wiki\/InnoDB\" target=\"_blank\" rel=\"noreferrer noopener\">InnoDB<\/a>, for example, <code>TRUNCATE TABLE<\/code> causes an implicit commit before execution and cannot be rolled back like a regular transactional <code>DELETE<\/code>. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This difference can become significant when migrating application code that performs table cleanup as part of a larger transaction.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-mysql-and-postgresql-handle-generated-identifiers-the-key-differences-explained\">How MySQL and PostgreSQL handle generated identifiers &#8211; the key differences explained<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Another difference concerns automatically generated identifiers. Consider a PostgreSQL table using an identity column:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>CREATE TABLE customers (&nbsp;&nbsp;&nbsp;&nbsp;customer_id INTEGER GENERATED ALWAYS AS IDENTITY,&nbsp;&nbsp;&nbsp;&nbsp;name TEXT);<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">PostgreSQL allows the identity sequence to be reset when truncating the table:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>TRUNCATE TABLE customersRESTART IDENTITY;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The next inserted row will start again from the identity sequence&#8217;s configured starting value. PostgreSQL also provides <code>CONTINUE IDENTITY<\/code> when the existing sequence values should be preserved:&nbsp;&nbsp;<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>TRUNCATE TABLE customersCONTINUE IDENTITY;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">A MySQL <code>TRUNCATE TABLE<\/code> also resets the <code>AUTO_INCREMENT<\/code> counter for an InnoDB table, so a straightforward PostgreSQL <code>TRUNCATE<\/code> will generally have similar behavior with respect to generated identifiers. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">However, PostgreSQL gives you <em>explicit control<\/em> over this behavior through <code>RESTART IDENTITY<\/code> and <code>CONTINUE IDENTITY<\/code>.&nbsp;&nbsp;<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-what-are-the-key-differences-in-how-foreign-key-relationships-work-in-mysql-compared-to-postgresql\">What are the key differences in how foreign key relationships work in MySQL compared to PostgreSQL?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/database-administration-sql-server\/foreign-keys-and-their-states\/\" target=\"_blank\" rel=\"noreferrer noopener\">Foreign key relationships<\/a> are handled differently in MySQL and PostgreSQL.<\/strong> If, for example, orders reference customers through a foreign key, PostgreSQL won&#8217;t allow a table to be truncated because it would violate a foreign key relationship. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The only exception would be if the related tables are included in the operation, or if cascading is explicitly requested. For this, PostgreSQL provides the <code><a href=\"https:\/\/www.postgresql.org\/docs\/current\/ddl-constraints.html#:~:text=CASCADE%20specifies%20that%20when%20a%20referenced%20row%20is%20deleted%2C%20row(s)%20referencing%20it%20should%20be%20automatically%20deleted%20as%20well.\" target=\"_blank\" rel=\"noreferrer noopener\">CASCADE<\/a><\/code> option:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>TRUNCATE TABLE customers CASCADE;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">This can also truncate tables that depend on customers through foreign key relationships.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because <code>CASCADE<\/code> can affect more tables than the one named in the statement, it should be used carefully, particularly in production environments.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">PostgreSQL also supports truncating several related tables explicitly:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>TRUNCATE TABLE customers, orders;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Over in MySQL, the way you truncate tables with foreign key relationships differs from PostgreSQL. In particular, <code>TRUNCATE TABLE<\/code> is treated as a <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/theory-and-design\/data-control-language-aka-security\/\" target=\"_blank\" rel=\"noreferrer noopener\">DDL<\/a> operation in MySQL, and foreign key restrictions can prevent the operation &#8211; <em>even<\/em> when the referencing table is empty.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>You should therefore review foreign key dependencies, rather than just <em>assuming<\/em> a MySQL <code>TRUNCATE<\/code> statement can simply be copied to PostgreSQL.<\/strong> <strong>And remember: despite them both removing rows, <code>TRUNCATE<\/code> is <em>not<\/em> the same as <code>DELETE<\/code>, so should not be treated as such.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A statement such as:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><code>DELETE FROM customers;<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">removes rows through the normal row-level deletion mechanism and can be combined with a <code>WHERE<\/code> clause. <code>TRUNCATE<\/code>, by contrast, removes <em>all<\/em> rows from the table, and does <em>not<\/em> support a <code>WHERE<\/code> clause.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-why-this-matters\">Why this matters<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The distinction matters during migration. Replacing one with the other can change transaction behavior, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/database-administration-sql-server\/sql-server-triggers-good-scary\/\" target=\"_blank\" rel=\"noreferrer noopener\">trigger<\/a> behavior, identity handling, and the way foreign key relationships are handled.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The basic <code>TRUNCATE TABLE<\/code> syntax is portable between MySQL and PostgreSQL, but the behavior surrounding it is not. In particular, migration scripts should be reviewed for assumptions about transaction <a href=\"https:\/\/www.red-gate.com\/simple-talk\/podcasts\/rollback-vs-roll-forward-in-databases\/\" target=\"_blank\" rel=\"noreferrer noopener\">rollback<\/a>, automatically- generated identifiers, foreign key dependencies, and cascading behavior.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If the purpose of the operation is simply to remove selected rows, <code>DELETE<\/code> is usually the appropriate choice. If the intention is to empty an entire table and the database-specific consequences are understood, however, <code>TRUNCATE<\/code> can be more efficient. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-in-summary\">In summary<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The important point during migration is not to treat the two commands as interchangeable &#8211; and <em>never<\/em> assume that a <code>TRUNCATE<\/code> statement copied from MySQL will have exactly the same effect in PostgreSQL.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">These examples highlight an important pattern throughout a MySQL-to-PostgreSQL migration: the closer you look, the more the differences extend beyond syntax. A statement may look familiar while relying on database-specific behavior underneath. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The same becomes even more apparent when logic moves beyond individual SQL statements and into stored procedures, functions, and triggers.<\/p>\n\n\n\n<section id=\"my-first-block-block_4e2f48e47d3665f086fc2dbc3817e6ca\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Simple Talk is brought to you by Redgate Software<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            Take control of your databases with the trusted Database DevOps solutions provider. Automate with confidence, scale securely, and unlock growth through AI.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/solutions\/overview\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Discover how Redgate can help you: Simple Talk is brought to you by Redgate Software\">Discover how Redgate can help you<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. Why does a MySQL query with backticks fail in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p dir=\"ltr\">MySQL uses backticks to quote identifiers like table and column names, while PostgreSQL follows the SQL standard and uses double quotes instead. Backticks aren&#8217;t recognized as identifier delimiters in PostgreSQL, so any query copied over without modification will throw a syntax error.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. Does PostgreSQL support MySQL&#039;s AUTO_INCREMENT?<\/h3>\n            <div class=\"faq-answer\">\n                <p>No. PostgreSQL doesn&#8217;t recognize the <code>AUTO_INCREMENT<\/code> attribute. Instead, it offers the <code>SERIAL<\/code> pseudo-type (a shorthand for a sequence-backed column) or the SQL-standard <code>GENERATED ... AS IDENTITY<\/code> syntax, which is now the recommended approach for new schemas.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. Can I just replace AUTO_INCREMENT with SERIAL and call it done?<\/h3>\n            <div class=\"faq-answer\">\n                <p dir=\"ltr\">Not quite. While <code>SERIAL<\/code> fixes the syntax error, it doesn&#8217;t behave identically to MySQL&#8217;s version; PostgreSQL&#8217;s <code>SERIAL<\/code> relies on a separate sequence object and doesn&#8217;t automatically create a <code>PRIMARY KEY<\/code> or <code>UNIQUE<\/code> constraint, unlike MySQL&#8217;s <code>SERIAL<\/code> alias.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">4. Does MySQL&#039;s LIMIT clause work the same way in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p dir=\"ltr\">Mostly, yes. Basic <code>LIMIT<\/code> and <code>OFFSET<\/code> syntax is supported in both. However, MySQL&#8217;s shorthand <code>LIMIT offset, count<\/code> isn&#8217;t supported in PostgreSQL and must be rewritten as <code>LIMIT count OFFSET offset<\/code>, or using the SQL-standard <code>FETCH FIRST<\/code> clause.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">5. What&#039;s the PostgreSQL equivalent of MySQL&#039;s ON DUPLICATE KEY UPDATE?<\/h3>\n            <div class=\"faq-answer\">\n                <p dir=\"ltr\">PostgreSQL uses <code>ON CONFLICT<\/code> instead. You specify a conflict target (like a column with a unique constraint) and choose whether to update the existing row (<code>DO UPDATE<\/code>), or skip it entirely (<code>DO NOTHING<\/code>) &#8211; which also replaces MySQL&#8217;s <code>INSERT IGNORE<\/code>.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">6. Is TRUNCATE TABLE safe to use inside a transaction in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p dir=\"ltr\">Yes. PostgreSQL treats <code>TRUNCATE<\/code> as fully transactional, so it can be rolled back. This differs from MySQL&#8217;s InnoDB engine, where <code>TRUNCATE<\/code> causes an implicit commit and can&#8217;t be undone the same way.<\/p>\n            <\/div>\n            <\/section>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Migrating from MySQL to PostgreSQL? Learn the key SQL syntax differences &#8211; identifier quoting, auto-increment, LIMIT vs FETCH FIRST, and more.&hellip;<\/p>\n","protected":false},"author":341597,"featured_media":107513,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143523,53,145792,143534,143531],"tags":[159268,4168,4170,5854,158978,4150,4252],"coauthors":[158988],"class_list":["post-112395","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-databases","category-featured","category-mysql","category-postgresql","category-t-sql-programming-sql-server","tag-aishabukar_mysql_postgresql","tag-database","tag-database-administration","tag-mysql","tag-postgresql","tag-sql","tag-t-sql-programming"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/112395","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\/341597"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=112395"}],"version-history":[{"count":20,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/112395\/revisions"}],"predecessor-version":[{"id":112890,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/112395\/revisions\/112890"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media\/107513"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=112395"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=112395"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=112395"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=112395"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}