{"id":1881,"date":"2019-03-06T00:00:00","date_gmt":"2019-03-05T23:00:00","guid":{"rendered":"https:\/\/wwwneu.strehle.de\/tim\/weblog\/archives\/2019\/03\/06\/1640-2\/"},"modified":"2025-07-31T21:36:49","modified_gmt":"2025-07-31T19:36:49","slug":"1640-2","status":"publish","type":"post","link":"https:\/\/www.strehle.de\/tim\/weblog\/archives\/2019\/03\/06\/1640-2\/","title":{"rendered":"Using XML to copy data from PostgreSQL to MySQL"},"content":{"rendered":"\n<p>Today, I needed to copy a few tables from a PostgreSQL database into a MySQL database. I wanted to use XML files to exchange the data so I could inspect and validate it easily.<\/p>\n\n\n\n<p>I expected this to be a lot of work (including XSLT magic), but it was surprisingly easy: It turns out that PostgreSQL\u2019s default XML output format is supported by MySQL out of the box! (And has been for a while; I was doing this on PostgreSQL 9.2 and MariaDB 5.5.)<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Export XML from PostgreSQL<\/h3>\n\n\n\n<p>Create a SQL file <code>postgresql-xml-export.sql<\/code> that invokes <a href=\"https:\/\/www.postgresql.org\/docs\/current\/functions-xml.html#FUNCTIONS-XML-MAPPING\">query_to_xml()<\/a> for each table (there\u2019s table_to_xml() as well but query is more flexible) and writes the output to a file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\\pset format unaligned\n\\t\n\\o song.xml\nSELECT query_to_xml('select * from song', false, false, '');\n\\o songbook.xml\nSELECT query_to_xml('select * from songbook', false, false, '');\n<\/code><\/pre>\n\n\n\n<p>Run it via <code>psql<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ psql postgresdb -f postgresql-xml-export.sql\n<\/code><\/pre>\n\n\n\n<p>You should now see the XML files:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ ls *.xml\nsong.xml&nbsp; songbook.xml\n<\/code><\/pre>\n\n\n\n<p>Make sure they contain the right data. PostgreSQL uses a <code>\/table\/row\/COLUMNNAME<\/code> format by default. Example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;table xmlns:xsi=\"http:\/\/www.w3.org\/2001\/XMLSchema-instance\"&gt;\n&lt;row&gt;\n&nbsp; &lt;id&gt;song-119&lt;\/id&gt;\n&nbsp; &lt;fulltitle&gt;Amazing grace&lt;\/fulltitle&gt;\n&nbsp; &lt;language&gt;en&lt;\/language&gt;\n&lt;\/row&gt;\n&lt;\/table&gt;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: In MySQL, create the database schema and import XML<\/h3>\n\n\n\n<p>Make sure you have a MySQL database to import into. Use the <code>utf8mb4<\/code> character set unless you know exactly what you\u2019re doing. I created a test database:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>CREATE DATABASE importtest CHARACTER SET utf8mb4;\n<\/code><\/pre>\n\n\n\n<p>Wanting to delete all existing data on import, I wrote a single SQL script that drops and recreates the tables (I wrote the DDL manually), then imports the data via <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.0\/en\/load-xml.html\">LOAD XML<\/a>. (That\u2019s not what you might want to do.)<\/p>\n\n\n\n<p>Here\u2019s my <code>mysql-import-xml.sql<\/code> file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>DROP TABLE IF EXISTS song;\n\nCREATE TABLE song (\n&nbsp;&nbsp;&nbsp; id VARCHAR(128) NOT NULL,\n&nbsp;&nbsp;&nbsp; fulltitle VARCHAR(255) NOT NULL,\n&nbsp;&nbsp;&nbsp; language VARCHAR(10)\n);\n\nDROP TABLE IF EXISTS songbook;\n\nCREATE TABLE songbook (\n&nbsp;&nbsp;&nbsp; id VARCHAR(128) NOT NULL,\n&nbsp;&nbsp;&nbsp; name VARCHAR(255) NOT NULL\n);\n\nLOAD XML LOCAL INFILE 'song.xml' INTO TABLE song ROWS IDENTIFIED BY '&lt;row&gt;';\nLOAD XML LOCAL INFILE 'songbook.xml' INTO TABLE songbook ROWS IDENTIFIED BY '&lt;row&gt;';\n<\/code><\/pre>\n\n\n\n<p>Run that through <code>mysql<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ mysql -uUSER -pPASSWORD importtest &lt; mysql-xml-import.sql\n<\/code><\/pre>\n\n\n\n<p>\u2026 and all the data is in the MySQL database. Pretty nice, isn\u2019t it?<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Today, I needed to copy a few tables from a PostgreSQL database into a MySQL database. I wanted to use XML files to exchange the data so I could inspect and validate it easily. I expected this to be a lot of work (including XSLT magic), but it was surprisingly easy: It turns out that [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","_share_on_mastodon":"0"},"categories":[1],"tags":[],"class_list":["post-1881","post","type-post","status-publish","format-standard","hentry","category-weblog"],"share_on_mastodon":{"url":"","error":""},"_links":{"self":[{"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/posts\/1881","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/comments?post=1881"}],"version-history":[{"count":1,"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/posts\/1881\/revisions"}],"predecessor-version":[{"id":1892,"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/posts\/1881\/revisions\/1892"}],"wp:attachment":[{"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/media?parent=1881"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/categories?post=1881"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.strehle.de\/tim\/wp-json\/wp\/v2\/tags?post=1881"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}