[{"data":1,"prerenderedAt":1320},["ShallowReactive",2],{"blog-post:\u002F2025-09-11-mysql-to-pg":3},{"id":4,"title":5,"body":6,"description":1312,"extension":1313,"meta":1314,"navigation":245,"path":1316,"seo":1317,"stem":1318,"__hash__":1319},"content\u002F2025-09-11-mysql-to-pg.md","Migrating a Large MySQL Database to PostgreSQL on AWS RDS",{"type":7,"value":8,"toc":1296},"minimark",[9,14,18,65,69,72,79,99,103,109,112,135,145,159,162,166,173,176,179,349,360,364,371,377,388,417,420,424,430,436,438,509,513,524,563,570,581,613,617,620,627,638,645,648,893,897,907,910,940,947,969,986,991,1058,1061,1069,1072,1104,1124,1210,1213,1218,1232,1236,1239,1244,1251,1255,1261,1265,1272,1275,1292],[10,11,13],"h2",{"id":12},"preconditions","Preconditions",[15,16,17],"p",{},"Before starting the migration, here was the situation:",[19,20,21,34,41,48,55],"ul",{},[22,23,24,25,29,30,33],"li",{},"The new PostgreSQL database had to run on ",[26,27,28],"strong",{},"AWS RDS"," (so ",[26,31,32],{},"no access to the database host filesystem",").",[22,35,36,37,40],{},"Some tables contained ",[26,38,39],{},"millions of rows"," (~1.5 GB per table).",[22,42,43,44,47],{},"The schema included ",[26,45,46],{},"JSON columns with virtual columns derived from JSON"," (used for indexing).",[22,49,50,51,54],{},"The database used ",[26,52,53],{},"ENUM types",".",[22,56,57,58,61,62,54],{},"The backend is written in ",[26,59,60],{},"Laravel",", with heavy use of ",[26,63,64],{},"database migrations",[10,66,68],{"id":67},"where-to-start","Where to start?",[15,70,71],{},"The first step was researching standard migration tools.",[15,73,74,75,78],{},"The most popular option is ",[26,76,77],{},"pgloader",". I also found a couple of Node.js-based tools. I ended up discarding both approaches because:",[19,80,81,92],{},[22,82,83,84,88,89,33],{},"Many column types were not mapped correctly (for example, ",[85,86,87],"code",{},"json"," not becoming ",[85,90,91],{},"jsonb",[22,93,94,95,98],{},"The migration process itself was ",[26,96,97],{},"too slow",", which is unacceptable when dealing with production data.",[10,100,102],{"id":101},"schema-first-approach","Schema-first approach",[15,104,105,106,54],{},"I eventually concluded that the ",[26,107,108],{},"PostgreSQL schema should be created manually before migrating the data",[15,110,111],{},"This decision was driven by several factors:",[19,113,114,117,123,129],{},[22,115,116],{},"Type mismatches mentioned earlier",[22,118,119,120],{},"The need to ",[26,121,122],{},"modify the schema during migration",[22,124,125,126],{},"Adding ",[26,127,128],{},"table partitioning",[22,130,131,132],{},"Removing ",[26,133,134],{},"deprecated tables",[15,136,137,138],{},"To achieve this, I generated a Laravel-compatible migration for the entire existing database using ",[139,140,144],"a",{"href":141,"rel":142},"https:\u002F\u002Fgithub.com\u002Fkitloong\u002Flaravel-migrations-generator",[143],"nofollow","laravel-migrations-generator",[15,146,147,148,151,152,155,156,54],{},"I used the ",[85,149,150],{},"--squash"," flag to produce ",[26,153,154],{},"one large migration file"," and removed all previous migrations. This allowed new environments to start with the ",[26,157,158],{},"current schema from scratch",[15,160,161],{},"Naturally, the generated migration required some adjustments.",[10,163,165],{"id":164},"handling-enum-types","Handling ENUM types",[15,167,168,169,172],{},"Laravel handles enums in PostgreSQL as ",[26,170,171],{},"check constraints",", not as real database types. I wanted proper PostgreSQL enum types.",[15,174,175],{},"To make that work, I extended Laravel’s schema grammar so migrations could reference custom types.",[15,177,178],{},"Example:",[180,181,186],"pre",{"className":182,"code":183,"language":184,"meta":185,"style":185},"language-php shiki shiki-themes github-light github-dark","Grammar::macro('typeCountry', function () {\n    return 'country';\n});\n\nDB::statement(\"CREATE TYPE country AS ENUM('russia', 'china', 'germany');\");\n\nSchema::create('users', function (Blueprint $table) {\n    ...\n    $table->addColumn('country', 'country');\n});\n","php","",[85,187,188,222,234,240,247,281,286,315,321,344],{"__ignoreMap":185},[189,190,193,197,201,205,209,213,216,219],"span",{"class":191,"line":192},"line",1,[189,194,196],{"class":195},"sj4cs","Grammar",[189,198,200],{"class":199},"szBVR","::",[189,202,204],{"class":203},"sScJk","macro",[189,206,208],{"class":207},"sVt8B","(",[189,210,212],{"class":211},"sZZnC","'typeCountry'",[189,214,215],{"class":207},", ",[189,217,218],{"class":199},"function",[189,220,221],{"class":207}," () {\n",[189,223,225,228,231],{"class":191,"line":224},2,[189,226,227],{"class":199},"    return",[189,229,230],{"class":211}," 'country'",[189,232,233],{"class":207},";\n",[189,235,237],{"class":191,"line":236},3,[189,238,239],{"class":207},"});\n",[189,241,243],{"class":191,"line":242},4,[189,244,246],{"emptyLinePlaceholder":245},true,"\n",[189,248,250,253,255,258,260,263,266,269,272,275,278],{"class":191,"line":249},5,[189,251,252],{"class":195},"DB",[189,254,200],{"class":199},[189,256,257],{"class":203},"statement",[189,259,208],{"class":207},[189,261,262],{"class":211},"\"",[189,264,265],{"class":199},"CREATE",[189,267,268],{"class":199}," TYPE",[189,270,271],{"class":211}," country ",[189,273,274],{"class":199},"AS",[189,276,277],{"class":211}," ENUM('russia', 'china', 'germany');\"",[189,279,280],{"class":207},");\n",[189,282,284],{"class":191,"line":283},6,[189,285,246],{"emptyLinePlaceholder":245},[189,287,289,292,294,297,299,302,304,306,309,312],{"class":191,"line":288},7,[189,290,291],{"class":195},"Schema",[189,293,200],{"class":199},[189,295,296],{"class":203},"create",[189,298,208],{"class":207},[189,300,301],{"class":211},"'users'",[189,303,215],{"class":207},[189,305,218],{"class":199},[189,307,308],{"class":207}," (",[189,310,311],{"class":195},"Blueprint",[189,313,314],{"class":207}," $table) {\n",[189,316,318],{"class":191,"line":317},8,[189,319,320],{"class":199},"    ...\n",[189,322,324,327,330,333,335,338,340,342],{"class":191,"line":323},9,[189,325,326],{"class":207},"    $table",[189,328,329],{"class":199},"->",[189,331,332],{"class":203},"addColumn",[189,334,208],{"class":207},[189,336,337],{"class":211},"'country'",[189,339,215],{"class":207},[189,341,337],{"class":211},[189,343,280],{"class":207},[189,345,347],{"class":191,"line":346},10,[189,348,239],{"class":207},[15,350,351,352,355,356,359],{},"Without the ",[85,353,354],{},"Grammar::macro",", Laravel would not recognize the ",[85,357,358],{},"country"," column type.",[10,361,363],{"id":362},"replacing-virtual-json-columns","Replacing virtual JSON columns",[15,365,366,367,370],{},"MySQL supported ",[26,368,369],{},"virtual columns derived from JSON fields",". PostgreSQL does not support virtual columns in the same way.",[15,372,373,374,54],{},"ChatGPT actually suggested a simple alternative: ",[26,375,376],{},"use an index on the JSON expression instead",[15,378,379,380,383,384,387],{},"For example, if ",[85,381,382],{},"settings"," is a JSON column in the ",[85,385,386],{},"users"," table:",[180,389,393],{"className":390,"code":391,"language":392,"meta":185,"style":185},"language-sql shiki shiki-themes github-light github-dark","CREATE INDEX ON users((settings->>'setting1'));\n","sql",[85,394,395],{"__ignoreMap":185},[189,396,397,399,402,405,408,411,414],{"class":191,"line":192},[189,398,265],{"class":199},[189,400,401],{"class":199}," INDEX",[189,403,404],{"class":203}," ON",[189,406,407],{"class":207}," users((settings",[189,409,410],{"class":199},"->>",[189,412,413],{"class":211},"'setting1'",[189,415,416],{"class":207},"));\n",[15,418,419],{},"The only thing to remember is to update the application code to access the value directly from the JSON field rather than the former virtual column.",[10,421,423],{"id":422},"preparing-tables-for-partitioning","Preparing tables for partitioning",[15,425,426,427,54],{},"One of the tables needed to be ",[26,428,429],{},"partitioned by date",[15,431,432,433,54],{},"In PostgreSQL, tables must be created ",[26,434,435],{},"with partitioning defined from the start",[15,437,178],{},[180,439,441],{"className":390,"code":440,"language":392,"meta":185,"style":185},"CREATE TABLE actions (\n    id SERIAL,\n    ...\n    created_at TIMESTAMP NOT NULL,\n    PRIMARY KEY (id, created_at)\n) PARTITION BY RANGE (created_at);\n",[85,442,443,456,467,471,484,492],{"__ignoreMap":185},[189,444,445,447,450,453],{"class":191,"line":192},[189,446,265],{"class":199},[189,448,449],{"class":199}," TABLE",[189,451,452],{"class":203}," actions",[189,454,455],{"class":207}," (\n",[189,457,458,461,464],{"class":191,"line":224},[189,459,460],{"class":207},"    id ",[189,462,463],{"class":199},"SERIAL",[189,465,466],{"class":207},",\n",[189,468,469],{"class":191,"line":236},[189,470,320],{"class":207},[189,472,473,476,479,482],{"class":191,"line":242},[189,474,475],{"class":207},"    created_at ",[189,477,478],{"class":199},"TIMESTAMP",[189,480,481],{"class":199}," NOT NULL",[189,483,466],{"class":207},[189,485,486,489],{"class":191,"line":249},[189,487,488],{"class":199},"    PRIMARY KEY",[189,490,491],{"class":207}," (id, created_at)\n",[189,493,494,497,500,503,506],{"class":191,"line":283},[189,495,496],{"class":207},") ",[189,498,499],{"class":199},"PARTITION",[189,501,502],{"class":199}," BY",[189,504,505],{"class":199}," RANGE",[189,507,508],{"class":207}," (created_at);\n",[10,510,512],{"id":511},"enabling-required-extensions","Enabling required extensions",[15,514,515,516,519,520,523],{},"One of the main motivations for moving to PostgreSQL was the ability to use ",[26,517,518],{},"vector data types",", so I added the ",[85,521,522],{},"vector"," extension during migration:",[180,525,527],{"className":182,"code":526,"language":184,"meta":185,"style":185},"DB::statement(\"CREATE EXTENSION IF NOT EXISTS vector;\");\n",[85,528,529],{"__ignoreMap":185},[189,530,531,533,535,537,539,541,543,546,549,552,555,558,561],{"class":191,"line":192},[189,532,252],{"class":195},[189,534,200],{"class":199},[189,536,257],{"class":203},[189,538,208],{"class":207},[189,540,262],{"class":211},[189,542,265],{"class":199},[189,544,545],{"class":211}," EXTENSION ",[189,547,548],{"class":199},"IF",[189,550,551],{"class":199}," NOT",[189,553,554],{"class":199}," EXISTS",[189,556,557],{"class":199}," vector",[189,559,560],{"class":211},";\"",[189,562,280],{"class":207},[15,564,565,566,569],{},"This command must be executed ",[26,567,568],{},"for every new database"," where the extension is required.",[15,571,572,573,576,577,580],{},"We also relied on ",[26,574,575],{},"geospatial queries",", so we enabled ",[26,578,579],{},"PostGIS"," as well:",[180,582,584],{"className":182,"code":583,"language":184,"meta":185,"style":185},"DB::statement(\"CREATE EXTENSION IF NOT EXISTS postgis;\");\n",[85,585,586],{"__ignoreMap":185},[189,587,588,590,592,594,596,598,600,602,604,606,608,611],{"class":191,"line":192},[189,589,252],{"class":195},[189,591,200],{"class":199},[189,593,257],{"class":203},[189,595,208],{"class":207},[189,597,262],{"class":211},[189,599,265],{"class":199},[189,601,545],{"class":211},[189,603,548],{"class":199},[189,605,551],{"class":199},[189,607,554],{"class":199},[189,609,610],{"class":211}," postgis;\"",[189,612,280],{"class":207},[10,614,616],{"id":615},"local-development-environment","Local development environment",[15,618,619],{},"Speaking of extensions, they must first be installed in the database environment.",[15,621,622,623,626],{},"AWS RDS comes with many extensions pre-installed. However, for ",[26,624,625],{},"local development"," we usually rely on Docker images.",[15,628,629,630,633,634,637],{},"Finding images with ",[26,631,632],{},"Postgres + pgvector"," or ",[26,635,636],{},"Postgres + PostGIS"," separately is easy.",[15,639,640,641,644],{},"Finding an image that contains ",[26,642,643],{},"both extensions and works on ARM64"," is not.",[15,646,647],{},"So I had to build one myself:",[180,649,653],{"className":650,"code":651,"language":652,"meta":185,"style":185},"language-dockerfile shiki shiki-themes github-light github-dark","FROM pgvector\u002Fpgvector:0.8.0-pg16\n\nENV POSTGIS_VERSION=3.5.0\n\nRUN apt-get update && apt-get install -y \\\n    build-essential \\\n    cmake \\\n    git \\\n    wget \\\n    pkg-config \\\n    postgresql-server-dev-16 \\\n    libxml2-dev \\\n    libgeos-dev \\\n    libproj-dev \\\n    libgdal-dev \\\n    libjson-c-dev \\\n    libprotobuf-c-dev \\\n    protobuf-c-compiler \\\n    libssl-dev \\\n    libcurl4-openssl-dev \\\n    libtiff-dev \\\n    libsqlite3-dev \\\n    sqlite3 \\\n    && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n\nRUN cd \u002Ftmp && \\\n    wget https:\u002F\u002Fdownload.osgeo.org\u002Fpostgis\u002Fsource\u002Fpostgis-${POSTGIS_VERSION}.tar.gz && \\\n    tar -xzf postgis-${POSTGIS_VERSION}.tar.gz && \\\n    cd postgis-${POSTGIS_VERSION} && \\\n    .\u002Fconfigure \\\n    --with-pgconfig=\u002Fusr\u002Fbin\u002Fpg_config \\\n    --with-geosconfig=\u002Fusr\u002Fbin\u002Fgeos-config \\\n    --with-projdir=\u002Fusr \\\n    --with-gdalconfig=\u002Fusr\u002Fbin\u002Fgdal-config \\\n    --with-jsondir=\u002Fusr \\\n    --with-protobufdir=\u002Fusr && \\\n    make && \\\n    make install && \\\n    cd \u002F && \\\n    rm -rf \u002Ftmp\u002Fpostgis-*\n","dockerfile",[85,654,655,663,667,675,679,687,692,697,702,707,712,718,724,730,736,742,748,754,760,766,772,778,784,790,796,801,809,815,821,827,833,839,845,851,857,863,869,875,881,887],{"__ignoreMap":185},[189,656,657,660],{"class":191,"line":192},[189,658,659],{"class":199},"FROM",[189,661,662],{"class":207}," pgvector\u002Fpgvector:0.8.0-pg16\n",[189,664,665],{"class":191,"line":224},[189,666,246],{"emptyLinePlaceholder":245},[189,668,669,672],{"class":191,"line":236},[189,670,671],{"class":199},"ENV",[189,673,674],{"class":207}," POSTGIS_VERSION=3.5.0\n",[189,676,677],{"class":191,"line":242},[189,678,246],{"emptyLinePlaceholder":245},[189,680,681,684],{"class":191,"line":249},[189,682,683],{"class":199},"RUN",[189,685,686],{"class":207}," apt-get update && apt-get install -y \\\n",[189,688,689],{"class":191,"line":283},[189,690,691],{"class":207},"    build-essential \\\n",[189,693,694],{"class":191,"line":288},[189,695,696],{"class":207},"    cmake \\\n",[189,698,699],{"class":191,"line":317},[189,700,701],{"class":207},"    git \\\n",[189,703,704],{"class":191,"line":323},[189,705,706],{"class":207},"    wget \\\n",[189,708,709],{"class":191,"line":346},[189,710,711],{"class":207},"    pkg-config \\\n",[189,713,715],{"class":191,"line":714},11,[189,716,717],{"class":207},"    postgresql-server-dev-16 \\\n",[189,719,721],{"class":191,"line":720},12,[189,722,723],{"class":207},"    libxml2-dev \\\n",[189,725,727],{"class":191,"line":726},13,[189,728,729],{"class":207},"    libgeos-dev \\\n",[189,731,733],{"class":191,"line":732},14,[189,734,735],{"class":207},"    libproj-dev \\\n",[189,737,739],{"class":191,"line":738},15,[189,740,741],{"class":207},"    libgdal-dev \\\n",[189,743,745],{"class":191,"line":744},16,[189,746,747],{"class":207},"    libjson-c-dev \\\n",[189,749,751],{"class":191,"line":750},17,[189,752,753],{"class":207},"    libprotobuf-c-dev \\\n",[189,755,757],{"class":191,"line":756},18,[189,758,759],{"class":207},"    protobuf-c-compiler \\\n",[189,761,763],{"class":191,"line":762},19,[189,764,765],{"class":207},"    libssl-dev \\\n",[189,767,769],{"class":191,"line":768},20,[189,770,771],{"class":207},"    libcurl4-openssl-dev \\\n",[189,773,775],{"class":191,"line":774},21,[189,776,777],{"class":207},"    libtiff-dev \\\n",[189,779,781],{"class":191,"line":780},22,[189,782,783],{"class":207},"    libsqlite3-dev \\\n",[189,785,787],{"class":191,"line":786},23,[189,788,789],{"class":207},"    sqlite3 \\\n",[189,791,793],{"class":191,"line":792},24,[189,794,795],{"class":207},"    && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n",[189,797,799],{"class":191,"line":798},25,[189,800,246],{"emptyLinePlaceholder":245},[189,802,804,806],{"class":191,"line":803},26,[189,805,683],{"class":199},[189,807,808],{"class":207}," cd \u002Ftmp && \\\n",[189,810,812],{"class":191,"line":811},27,[189,813,814],{"class":207},"    wget https:\u002F\u002Fdownload.osgeo.org\u002Fpostgis\u002Fsource\u002Fpostgis-${POSTGIS_VERSION}.tar.gz && \\\n",[189,816,818],{"class":191,"line":817},28,[189,819,820],{"class":207},"    tar -xzf postgis-${POSTGIS_VERSION}.tar.gz && \\\n",[189,822,824],{"class":191,"line":823},29,[189,825,826],{"class":207},"    cd postgis-${POSTGIS_VERSION} && \\\n",[189,828,830],{"class":191,"line":829},30,[189,831,832],{"class":207},"    .\u002Fconfigure \\\n",[189,834,836],{"class":191,"line":835},31,[189,837,838],{"class":207},"    --with-pgconfig=\u002Fusr\u002Fbin\u002Fpg_config \\\n",[189,840,842],{"class":191,"line":841},32,[189,843,844],{"class":207},"    --with-geosconfig=\u002Fusr\u002Fbin\u002Fgeos-config \\\n",[189,846,848],{"class":191,"line":847},33,[189,849,850],{"class":207},"    --with-projdir=\u002Fusr \\\n",[189,852,854],{"class":191,"line":853},34,[189,855,856],{"class":207},"    --with-gdalconfig=\u002Fusr\u002Fbin\u002Fgdal-config \\\n",[189,858,860],{"class":191,"line":859},35,[189,861,862],{"class":207},"    --with-jsondir=\u002Fusr \\\n",[189,864,866],{"class":191,"line":865},36,[189,867,868],{"class":207},"    --with-protobufdir=\u002Fusr && \\\n",[189,870,872],{"class":191,"line":871},37,[189,873,874],{"class":207},"    make && \\\n",[189,876,878],{"class":191,"line":877},38,[189,879,880],{"class":207},"    make install && \\\n",[189,882,884],{"class":191,"line":883},39,[189,885,886],{"class":207},"    cd \u002F && \\\n",[189,888,890],{"class":191,"line":889},40,[189,891,892],{"class":207},"    rm -rf \u002Ftmp\u002Fpostgis-*\n",[10,894,896],{"id":895},"migration-process","Migration process",[15,898,899,900,903,904,54],{},"After testing the process locally, I ran it in ",[26,901,902],{},"staging"," and ",[26,905,906],{},"production",[15,908,909],{},"The migration flow looked like this:",[911,912,913,919,925],"ol",{},[22,914,915,916],{},"Create a new ",[26,917,918],{},"AWS RDS instance",[22,920,921,922],{},"Connect using ",[85,923,924],{},"psql",[22,926,927,928],{},"Enable the required extensions:",[19,929,930,935],{},[22,931,932],{},[85,933,934],{},"aws_commons",[22,936,937],{},[85,938,939],{},"aws_s3",[15,941,942,943,946],{},"These allow importing CSV files from ",[26,944,945],{},"S3",", since RDS does not provide filesystem access.",[911,948,949,956,963,966],{"start":242},[22,950,951,952,955],{},"Create a ",[26,953,954],{},"VPC Endpoint"," so the database can access S3.",[22,957,958,959,962],{},"Update the Laravel database configuration (",[85,960,961],{},"config\u002Fdatabase.php",")",[22,964,965],{},"Deploy the code with the new migration and apply the schema.",[22,967,968],{},"Stop the application to prevent writes during migration:",[180,970,974],{"className":971,"code":972,"language":973,"meta":185,"style":185},"language-bash shiki shiki-themes github-light github-dark","php artisan down\n","bash",[85,975,976],{"__ignoreMap":185},[189,977,978,980,983],{"class":191,"line":192},[189,979,184],{"class":203},[189,981,982],{"class":211}," artisan",[189,984,985],{"class":211}," down\n",[911,987,988],{"start":317},[22,989,990],{},"Export data from MySQL to CSV:",[180,992,994],{"className":971,"code":993,"language":973,"meta":185,"style":185},"mysqldump --port=3308 -u root -psecret db-name \\\n  --no-create-info \\\n  --fields-terminated-by=, \\\n  --tab=\u002Fvar\u002Flib\u002Fmysql-files \\\n  --fields-enclosed-by='\\\"' \\\n  --lines-terminated-by='\\n'\n",[85,995,996,1019,1026,1033,1040,1050],{"__ignoreMap":185},[189,997,998,1001,1004,1007,1010,1013,1016],{"class":191,"line":192},[189,999,1000],{"class":203},"mysqldump",[189,1002,1003],{"class":195}," --port=3308",[189,1005,1006],{"class":195}," -u",[189,1008,1009],{"class":211}," root",[189,1011,1012],{"class":195}," -psecret",[189,1014,1015],{"class":211}," db-name",[189,1017,1018],{"class":195}," \\\n",[189,1020,1021,1024],{"class":191,"line":224},[189,1022,1023],{"class":195},"  --no-create-info",[189,1025,1018],{"class":195},[189,1027,1028,1031],{"class":191,"line":236},[189,1029,1030],{"class":195},"  --fields-terminated-by=,",[189,1032,1018],{"class":195},[189,1034,1035,1038],{"class":191,"line":242},[189,1036,1037],{"class":195},"  --tab=\u002Fvar\u002Flib\u002Fmysql-files",[189,1039,1018],{"class":195},[189,1041,1042,1045,1048],{"class":191,"line":249},[189,1043,1044],{"class":195},"  --fields-enclosed-by=",[189,1046,1047],{"class":211},"'\\\"'",[189,1049,1018],{"class":195},[189,1051,1052,1055],{"class":191,"line":283},[189,1053,1054],{"class":195},"  --lines-terminated-by=",[189,1056,1057],{"class":211},"'\\n'\n",[15,1059,1060],{},"This generates files for each table:",[180,1062,1067],{"className":1063,"code":1065,"language":1066},[1064],"language-text","actions.sql  actions.txt\nusers.sql    users.txt\n","text",[85,1068,1065],{"__ignoreMap":185},[15,1070,1071],{},"Key flags:",[19,1073,1074,1080,1086,1092,1098],{},[22,1075,1076,1079],{},[85,1077,1078],{},"--no-create-info"," → skip schema generation",[22,1081,1082,1085],{},[85,1083,1084],{},"--fields-terminated-by=,"," → comma separator",[22,1087,1088,1091],{},[85,1089,1090],{},"--tab=\u002Fvar\u002Flib\u002Fmysql-files"," → required export directory",[22,1093,1094,1097],{},[85,1095,1096],{},"--fields-enclosed-by='\"'"," → format expected by PostgreSQL COPY",[22,1099,1100,1103],{},[85,1101,1102],{},"--lines-terminated-by='\\n'"," → newline rows",[911,1105,1106,1121],{"start":323},[22,1107,1108,1109,1112,1113,1116,1117,1120],{},"Upload the ",[85,1110,1111],{},".txt"," files to an ",[26,1114,1115],{},"S3 bucket",".\nIf you generated the files inside a container, ",[85,1118,1119],{},"docker cp"," helps extract them.",[22,1122,1123],{},"Import each table using:",[180,1125,1127],{"className":390,"code":1126,"language":392,"meta":185,"style":185},"SELECT aws_s3.table_import_from_s3(\n  'users',\n  '',\n  '(format csv, header false, null ''\\N'', escape ''\\'')',\n  'db-name',\n  '\u002Fdumps\u002Fusers.txt',\n  'your-aws-region',\n  'AWS_ACCESS_KEY',\n  'AWS_SECRET_KEY',\n  ''\n);\n",[85,1128,1129,1145,1152,1159,1166,1173,1180,1187,1194,1201,1206],{"__ignoreMap":185},[189,1130,1131,1134,1137,1139,1142],{"class":191,"line":192},[189,1132,1133],{"class":199},"SELECT",[189,1135,1136],{"class":195}," aws_s3",[189,1138,54],{"class":207},[189,1140,1141],{"class":195},"table_import_from_s3",[189,1143,1144],{"class":207},"(\n",[189,1146,1147,1150],{"class":191,"line":224},[189,1148,1149],{"class":211},"  'users'",[189,1151,466],{"class":207},[189,1153,1154,1157],{"class":191,"line":236},[189,1155,1156],{"class":211},"  ''",[189,1158,466],{"class":207},[189,1160,1161,1164],{"class":191,"line":242},[189,1162,1163],{"class":211},"  '(format csv, header false, null ''\\N'', escape ''\\'')'",[189,1165,466],{"class":207},[189,1167,1168,1171],{"class":191,"line":249},[189,1169,1170],{"class":211},"  'db-name'",[189,1172,466],{"class":207},[189,1174,1175,1178],{"class":191,"line":283},[189,1176,1177],{"class":211},"  '\u002Fdumps\u002Fusers.txt'",[189,1179,466],{"class":207},[189,1181,1182,1185],{"class":191,"line":288},[189,1183,1184],{"class":211},"  'your-aws-region'",[189,1186,466],{"class":207},[189,1188,1189,1192],{"class":191,"line":317},[189,1190,1191],{"class":211},"  'AWS_ACCESS_KEY'",[189,1193,466],{"class":207},[189,1195,1196,1199],{"class":191,"line":323},[189,1197,1198],{"class":211},"  'AWS_SECRET_KEY'",[189,1200,466],{"class":207},[189,1202,1203],{"class":191,"line":346},[189,1204,1205],{"class":211},"  ''\n",[189,1207,1208],{"class":191,"line":714},[189,1209,280],{"class":207},[15,1211,1212],{},"Repeat this command for each table.",[911,1214,1215],{"start":714},[22,1216,1217],{},"Restart the application:",[180,1219,1221],{"className":971,"code":1220,"language":973,"meta":185,"style":185},"php artisan up\n",[85,1222,1223],{"__ignoreMap":185},[189,1224,1225,1227,1229],{"class":191,"line":192},[189,1226,184],{"class":203},[189,1228,982],{"class":211},[189,1230,1231],{"class":211}," up\n",[10,1233,1235],{"id":1234},"one-more-important-detail","One more important detail",[15,1237,1238],{},"When creating the PostgreSQL schema, there are two approaches:",[1240,1241,1243],"h3",{"id":1242},"option-1-no-foreign-keys-initially","Option 1 — No foreign keys initially",[15,1245,1246,1247,1250],{},"Create tables ",[26,1248,1249],{},"without foreign keys"," so the import order doesn't matter.\nThen apply a second migration to add constraints afterward.",[1240,1252,1254],{"id":1253},"option-2-foreign-keys-from-the-start","Option 2 — Foreign keys from the start",[15,1256,1257,1258,54],{},"Create tables with foreign keys immediately.\nIn this case, the ",[26,1259,1260],{},"table import order becomes important",[10,1262,1264],{"id":1263},"final-thoughts","Final thoughts",[15,1266,1267,1268,1271],{},"For large production databases, a ",[26,1269,1270],{},"schema-first approach with CSV bulk import"," turned out to be much more reliable and faster than using automated migration tools.",[15,1273,1274],{},"It also gave us full control over:",[19,1276,1277,1280,1283,1286,1289],{},[22,1278,1279],{},"data types",[22,1281,1282],{},"indexing strategy",[22,1284,1285],{},"partitioning",[22,1287,1288],{},"extensions",[22,1290,1291],{},"schema cleanup",[1293,1294,1295],"style",{},"html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":185,"searchDepth":224,"depth":224,"links":1297},[1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1311],{"id":12,"depth":224,"text":13},{"id":67,"depth":224,"text":68},{"id":101,"depth":224,"text":102},{"id":164,"depth":224,"text":165},{"id":362,"depth":224,"text":363},{"id":422,"depth":224,"text":423},{"id":511,"depth":224,"text":512},{"id":615,"depth":224,"text":616},{"id":895,"depth":224,"text":896},{"id":1234,"depth":224,"text":1235,"children":1308},[1309,1310],{"id":1242,"depth":236,"text":1243},{"id":1253,"depth":236,"text":1254},{"id":1263,"depth":224,"text":1264},"Experience I gained while migrating a large MySQL database to PostgreSQL on AWS RDS.","md",{"date":1315},"2025-09-11","\u002F2025-09-11-mysql-to-pg",{"title":5,"description":1312},"2025-09-11-mysql-to-pg","yTvoSQwqTmkRd3ZQ9uaAucVdx6LLjmQ4zROjMDVBVdc",1788961315295]