[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-/blog/tutorial/migrar-mysql-a-postgresql":3,"prev-/blog/tutorial/migrar-mysql-a-postgresql":1872,"next-/blog/tutorial/migrar-mysql-a-postgresql":1875,"related-/blog/tutorial/migrar-mysql-a-postgresql":1878},{"id":4,"title":5,"author":6,"authorUrl":7,"body":8,"category":1839,"cta":1840,"date":1843,"dateModified":1843,"description":1844,"draft":1845,"extension":1846,"faq":1847,"featured":1845,"image":1860,"imageAlt":1861,"meta":1862,"navigation":421,"path":1863,"readingTime":490,"seo":1864,"stem":1865,"tags":1866,"__hash__":1871},"blog/blog/tutorial/migrar-mysql-a-postgresql.md","Cómo migrar de MySQL a PostgreSQL sin perder datos","Syswork México","/nosotros",{"type":9,"value":10,"toc":1816},"minimark",[11,21,29,34,37,60,68,72,75,153,156,160,163,168,316,320,734,737,741,750,783,787,790,808,861,865,868,1020,1023,1035,1038,1040,1044,1048,1051,1293,1297,1300,1344,1348,1446,1450,1454,1457,1507,1554,1558,1561,1597,1599,1603,1606,1642,1646,1649,1742,1750,1754,1757,1803,1812],[12,13,14,15,20],"p",{},"MySQL ha servido bien a muchas empresas, pero llega un punto donde sus limitaciones empiezan a pesar — el manejo de JSON es más limitado que JSONB de PostgreSQL, las extensiones son escasas, la replicación tiene más restricciones y el cumplimiento del estándar SQL no es tan estricto. Si tu aplicación está creciendo y necesitas más potencia, flexibilidad y un camino claro de escalamiento, ",[16,17,19],"a",{"href":18},"/tecnologias/postgresql","PostgreSQL"," es el siguiente paso natural.",[12,22,23,24,28],{},"En esta guía vas a migrar una base de datos ",[16,25,27],{"href":26},"/tecnologias/mysql","MySQL"," completa a PostgreSQL usando pgloader — desde la planificación hasta la validación de que no se perdió ni un registro.",[30,31,33],"h2",{"id":32},"antes-de-empezar-realmente-necesitas-migrar","Antes de empezar: ¿realmente necesitas migrar?",[12,35,36],{},"No toda base de datos MySQL necesita migrar. Evalúa si te aplica alguno de estos escenarios:",[38,39,40,44,47,54,57],"ul",{},[41,42,43],"li",{},"Tu aplicación necesita JSONB para consultas complejas sobre datos semi-estructurados",[41,45,46],{},"Quieres extensiones como PostGIS (geoespacial), pg_trgm (búsqueda fuzzy) o pgvector (embeddings IA)",[41,48,49,50],{},"La replicación streaming de PostgreSQL se adapta mejor a tus necesidades de ",[16,51,53],{"href":52},"/blog/replicacion-postgresql-alta-disponibilidad","alta disponibilidad",[41,55,56],{},"Quieres eliminar costos de licencia (MySQL Enterprise) o restricciones del modelo Oracle",[41,58,59],{},"Tu equipo de desarrollo prefiere PostgreSQL y la aplicación no tiene dependencias duras con MySQL",[12,61,62,63,67],{},"Si tu aplicación funciona bien con MySQL y no necesitas nada de lo anterior, optimizar MySQL con nuestro ",[16,64,66],{"href":65},"/servicios/bases-de-datos","servicio de bases de datos"," puede ser más práctico que migrar.",[30,69,71],{"id":70},"plan-de-migración","Plan de migración",[12,73,74],{},"Una migración exitosa tiene 5 fases. No saltes ninguna:",[76,77,78,94],"table",{},[79,80,81],"thead",{},[82,83,84,88,91],"tr",{},[85,86,87],"th",{},"Fase",[85,89,90],{},"Descripción",[85,92,93],{},"Duración típica",[95,96,97,109,120,131,142],"tbody",{},[82,98,99,103,106],{},[100,101,102],"td",{},"1. Análisis",[100,104,105],{},"Inventario de tablas, tipos, funciones, triggers y queries específicas de MySQL",[100,107,108],{},"2-3 días",[82,110,111,114,117],{},[100,112,113],{},"2. Migración de esquema y datos",[100,115,116],{},"Conversión con pgloader + ajustes manuales",[100,118,119],{},"1-2 días",[82,121,122,125,128],{},[100,123,124],{},"3. Ajuste de aplicación",[100,126,127],{},"Reescribir queries incompatibles, ORM config, funciones",[100,129,130],{},"3-10 días",[82,132,133,136,139],{},[100,134,135],{},"4. Pruebas",[100,137,138],{},"Regresión completa, validación de datos, rendimiento",[100,140,141],{},"3-5 días",[82,143,144,147,150],{},[100,145,146],{},"5. Corte",[100,148,149],{},"Ventana de mantenimiento, migración final, switch",[100,151,152],{},"1 día",[154,155],"ad-banner",{},[30,157,159],{"id":158},"paso-1-análisis-de-compatibilidad","Paso 1: Análisis de compatibilidad",[12,161,162],{},"Antes de tocar pgloader, analiza qué va a necesitar ajustes.",[164,165,167],"h3",{"id":166},"diferencias-de-tipos-de-datos","Diferencias de tipos de datos",[76,169,170,181],{},[79,171,172],{},[82,173,174,176,178],{},[85,175,27],{},[85,177,19],{},[85,179,180],{},"Notas",[95,182,183,199,218,233,247,267,283,301],{},[82,184,185,191,196],{},[100,186,187],{},[188,189,190],"code",{},"TINYINT(1)",[100,192,193],{},[188,194,195],{},"BOOLEAN",[100,197,198],{},"pgloader lo convierte automáticamente",[82,200,201,206,215],{},[100,202,203],{},[188,204,205],{},"INT AUTO_INCREMENT",[100,207,208,211,212],{},[188,209,210],{},"SERIAL"," o ",[188,213,214],{},"GENERATED ALWAYS AS IDENTITY",[100,216,217],{},"Conversión automática",[82,219,220,225,230],{},[100,221,222],{},[188,223,224],{},"DATETIME",[100,226,227],{},[188,228,229],{},"TIMESTAMP",[100,231,232],{},"Compatible",[82,234,235,240,245],{},[100,236,237],{},[188,238,239],{},"DOUBLE",[100,241,242],{},[188,243,244],{},"DOUBLE PRECISION",[100,246,232],{},[82,248,249,254,264],{},[100,250,251],{},[188,252,253],{},"ENUM('a','b','c')",[100,255,256,259,260,263],{},[188,257,258],{},"TEXT"," + ",[188,261,262],{},"CHECK"," constraint",[100,265,266],{},"Requiere atención",[82,268,269,274,280],{},[100,270,271],{},[188,272,273],{},"SET('x','y')",[100,275,276,279],{},[188,277,278],{},"TEXT[]"," (array)",[100,281,282],{},"Requiere conversión manual",[82,284,285,294,299],{},[100,286,287,290,291],{},[188,288,289],{},"BLOB"," / ",[188,292,293],{},"LONGBLOB",[100,295,296],{},[188,297,298],{},"BYTEA",[100,300,217],{},[82,302,303,308,313],{},[100,304,305],{},[188,306,307],{},"JSON",[100,309,310],{},[188,311,312],{},"JSONB",[100,314,315],{},"Recomendado cambiar a JSONB",[164,317,319],{"id":318},"queries-que-necesitan-ajuste","Queries que necesitan ajuste",[321,322,327],"pre",{"className":323,"code":324,"language":325,"meta":326,"style":326},"language-sql shiki shiki-themes material-theme-lighter github-light github-dark","-- MySQL: comillas invertidas para identificadores\nSELECT `order`.`id` FROM `order`;\n-- PostgreSQL: comillas dobles (o sin comillas si no es palabra reservada)\nSELECT \"order\".\"id\" FROM \"order\";\n\n-- MySQL: LIMIT con OFFSET\nSELECT * FROM productos LIMIT 10, 20;\n-- PostgreSQL\nSELECT * FROM productos LIMIT 20 OFFSET 10;\n\n-- MySQL: GROUP_CONCAT\nSELECT GROUP_CONCAT(nombre SEPARATOR ', ') FROM tags;\n-- PostgreSQL\nSELECT STRING_AGG(nombre, ', ') FROM tags;\n\n-- MySQL: IF()\nSELECT IF(stock > 0, 'Disponible', 'Agotado') FROM productos;\n-- PostgreSQL\nSELECT CASE WHEN stock > 0 THEN 'Disponible' ELSE 'Agotado' END FROM productos;\n\n-- MySQL: IFNULL\nSELECT IFNULL(telefono, 'Sin teléfono') FROM clientes;\n-- PostgreSQL\nSELECT COALESCE(telefono, 'Sin teléfono') FROM clientes;\n\n-- MySQL: NOW() retorna DATETIME\n-- PostgreSQL: NOW() retorna TIMESTAMP WITH TIME ZONE (más correcto)\n","sql","",[188,328,329,338,379,385,416,423,429,458,464,488,493,499,523,528,552,557,563,605,610,654,659,665,690,695,717,722,728],{"__ignoreMap":326},[330,331,334],"span",{"class":332,"line":333},"line",1,[330,335,337],{"class":336},"sutJx","-- MySQL: comillas invertidas para identificadores\n",[330,339,341,345,349,353,356,360,362,365,367,370,372,374,376],{"class":332,"line":340},2,[330,342,344],{"class":343},"sw1J6","SELECT",[330,346,348],{"class":347},"sjJ54"," `",[330,350,352],{"class":351},"s_sjI","order",[330,354,355],{"class":347},"`",[330,357,359],{"class":358},"su5hD",".",[330,361,355],{"class":347},[330,363,364],{"class":351},"id",[330,366,355],{"class":347},[330,368,369],{"class":343}," FROM",[330,371,348],{"class":347},[330,373,352],{"class":351},[330,375,355],{"class":347},[330,377,378],{"class":358},";\n",[330,380,382],{"class":332,"line":381},3,[330,383,384],{"class":336},"-- PostgreSQL: comillas dobles (o sin comillas si no es palabra reservada)\n",[330,386,388,390,393,395,398,400,402,404,406,408,410,412,414],{"class":332,"line":387},4,[330,389,344],{"class":343},[330,391,392],{"class":347}," \"",[330,394,352],{"class":351},[330,396,397],{"class":347},"\"",[330,399,359],{"class":358},[330,401,397],{"class":347},[330,403,364],{"class":351},[330,405,397],{"class":347},[330,407,369],{"class":343},[330,409,392],{"class":347},[330,411,352],{"class":351},[330,413,397],{"class":347},[330,415,378],{"class":358},[330,417,419],{"class":332,"line":418},5,[330,420,422],{"emptyLinePlaceholder":421},true,"\n",[330,424,426],{"class":332,"line":425},6,[330,427,428],{"class":336},"-- MySQL: LIMIT con OFFSET\n",[330,430,432,434,438,440,443,446,450,453,456],{"class":332,"line":431},7,[330,433,344],{"class":343},[330,435,437],{"class":436},"smGrS"," *",[330,439,369],{"class":343},[330,441,442],{"class":358}," productos ",[330,444,445],{"class":343},"LIMIT",[330,447,449],{"class":448},"srdBf"," 10",[330,451,452],{"class":358},", ",[330,454,455],{"class":448},"20",[330,457,378],{"class":358},[330,459,461],{"class":332,"line":460},8,[330,462,463],{"class":336},"-- PostgreSQL\n",[330,465,467,469,471,473,475,477,480,483,486],{"class":332,"line":466},9,[330,468,344],{"class":343},[330,470,437],{"class":436},[330,472,369],{"class":343},[330,474,442],{"class":358},[330,476,445],{"class":343},[330,478,479],{"class":448}," 20",[330,481,482],{"class":358}," OFFSET ",[330,484,485],{"class":448},"10",[330,487,378],{"class":358},[330,489,491],{"class":332,"line":490},10,[330,492,422],{"emptyLinePlaceholder":421},[330,494,496],{"class":332,"line":495},11,[330,497,498],{"class":336},"-- MySQL: GROUP_CONCAT\n",[330,500,502,504,507,510,512,514,517,520],{"class":332,"line":501},12,[330,503,344],{"class":343},[330,505,506],{"class":358}," GROUP_CONCAT(nombre SEPARATOR ",[330,508,509],{"class":347},"'",[330,511,452],{"class":351},[330,513,509],{"class":347},[330,515,516],{"class":358},") ",[330,518,519],{"class":343},"FROM",[330,521,522],{"class":358}," tags;\n",[330,524,526],{"class":332,"line":525},13,[330,527,463],{"class":336},[330,529,531,533,537,540,542,544,546,548,550],{"class":332,"line":530},14,[330,532,344],{"class":343},[330,534,536],{"class":535},"sptTA"," STRING_AGG",[330,538,539],{"class":358},"(nombre, ",[330,541,509],{"class":347},[330,543,452],{"class":351},[330,545,509],{"class":347},[330,547,516],{"class":358},[330,549,519],{"class":343},[330,551,522],{"class":358},[330,553,555],{"class":332,"line":554},15,[330,556,422],{"emptyLinePlaceholder":421},[330,558,560],{"class":332,"line":559},16,[330,561,562],{"class":336},"-- MySQL: IF()\n",[330,564,566,568,571,574,577,580,582,584,587,589,591,593,596,598,600,602],{"class":332,"line":565},17,[330,567,344],{"class":343},[330,569,570],{"class":343}," IF",[330,572,573],{"class":358},"(stock ",[330,575,576],{"class":436},">",[330,578,579],{"class":448}," 0",[330,581,452],{"class":358},[330,583,509],{"class":347},[330,585,586],{"class":351},"Disponible",[330,588,509],{"class":347},[330,590,452],{"class":358},[330,592,509],{"class":347},[330,594,595],{"class":351},"Agotado",[330,597,509],{"class":347},[330,599,516],{"class":358},[330,601,519],{"class":343},[330,603,604],{"class":358}," productos;\n",[330,606,608],{"class":332,"line":607},18,[330,609,463],{"class":336},[330,611,613,615,618,621,624,626,628,631,634,636,638,641,643,645,647,650,652],{"class":332,"line":612},19,[330,614,344],{"class":343},[330,616,617],{"class":343}," CASE",[330,619,620],{"class":343}," WHEN",[330,622,623],{"class":358}," stock ",[330,625,576],{"class":436},[330,627,579],{"class":448},[330,629,630],{"class":343}," THEN",[330,632,633],{"class":347}," '",[330,635,586],{"class":351},[330,637,509],{"class":347},[330,639,640],{"class":343}," ELSE",[330,642,633],{"class":347},[330,644,595],{"class":351},[330,646,509],{"class":347},[330,648,649],{"class":343}," END",[330,651,369],{"class":343},[330,653,604],{"class":358},[330,655,657],{"class":332,"line":656},20,[330,658,422],{"emptyLinePlaceholder":421},[330,660,662],{"class":332,"line":661},21,[330,663,664],{"class":336},"-- MySQL: IFNULL\n",[330,666,668,670,673,676,678,681,683,685,687],{"class":332,"line":667},22,[330,669,344],{"class":343},[330,671,672],{"class":343}," IFNULL",[330,674,675],{"class":358},"(telefono, ",[330,677,509],{"class":347},[330,679,680],{"class":351},"Sin teléfono",[330,682,509],{"class":347},[330,684,516],{"class":358},[330,686,519],{"class":343},[330,688,689],{"class":358}," clientes;\n",[330,691,693],{"class":332,"line":692},23,[330,694,463],{"class":336},[330,696,698,700,703,705,707,709,711,713,715],{"class":332,"line":697},24,[330,699,344],{"class":343},[330,701,702],{"class":535}," COALESCE",[330,704,675],{"class":358},[330,706,509],{"class":347},[330,708,680],{"class":351},[330,710,509],{"class":347},[330,712,516],{"class":358},[330,714,519],{"class":343},[330,716,689],{"class":358},[330,718,720],{"class":332,"line":719},25,[330,721,422],{"emptyLinePlaceholder":421},[330,723,725],{"class":332,"line":724},26,[330,726,727],{"class":336},"-- MySQL: NOW() retorna DATETIME\n",[330,729,731],{"class":332,"line":730},27,[330,732,733],{"class":336},"-- PostgreSQL: NOW() retorna TIMESTAMP WITH TIME ZONE (más correcto)\n",[12,735,736],{},"Busca estos patrones en tu código antes de migrar. Si usas un ORM como SQLAlchemy, Prisma o TypeORM, la mayoría de estos ajustes se manejan cambiando el driver de conexión.",[30,738,740],{"id":739},"paso-2-instalar-pgloader","Paso 2: Instalar pgloader",[12,742,743,749],{},[16,744,748],{"href":745,"rel":746},"https://pgloader.io/",[747],"nofollow","pgloader"," es la herramienta estándar para migrar de MySQL a PostgreSQL. Lee directamente de MySQL, convierte tipos automáticamente y carga en PostgreSQL:",[321,751,755],{"className":752,"code":753,"language":754,"meta":326,"style":326},"language-bash shiki shiki-themes material-theme-lighter github-light github-dark","sudo apt install pgloader -y\npgloader --version\n","bash",[188,756,757,776],{"__ignoreMap":326},[330,758,759,763,766,769,772],{"class":332,"line":333},[330,760,762],{"class":761},"sbgvK","sudo",[330,764,765],{"class":351}," apt",[330,767,768],{"class":351}," install",[330,770,771],{"class":351}," pgloader",[330,773,775],{"class":774},"stzsN"," -y\n",[330,777,778,780],{"class":332,"line":340},[330,779,748],{"class":761},[330,781,782],{"class":774}," --version\n",[30,784,786],{"id":785},"paso-3-preparar-postgresql-destino","Paso 3: Preparar PostgreSQL destino",[12,788,789],{},"Crea la base de datos de destino vacía:",[321,791,793],{"className":752,"code":792,"language":754,"meta":326,"style":326},"sudo -u postgres psql\n",[188,794,795],{"__ignoreMap":326},[330,796,797,799,802,805],{"class":332,"line":333},[330,798,762],{"class":761},[330,800,801],{"class":774}," -u",[330,803,804],{"class":351}," postgres",[330,806,807],{"class":351}," psql\n",[321,809,811],{"className":323,"code":810,"language":325,"meta":326,"style":326},"CREATE USER app WITH PASSWORD 'contraseña_segura';\nCREATE DATABASE miapp_pg OWNER app;\n\\q\n",[188,812,813,840,856],{"__ignoreMap":326},[330,814,815,818,821,825,828,831,833,836,838],{"class":332,"line":333},[330,816,817],{"class":343},"CREATE",[330,819,820],{"class":343}," USER",[330,822,824],{"class":823},"sGLFI"," app",[330,826,827],{"class":343}," WITH",[330,829,830],{"class":343}," PASSWORD",[330,832,633],{"class":347},[330,834,835],{"class":351},"contraseña_segura",[330,837,509],{"class":347},[330,839,378],{"class":358},[330,841,842,844,847,850,853],{"class":332,"line":340},[330,843,817],{"class":343},[330,845,846],{"class":343}," DATABASE",[330,848,849],{"class":823}," miapp_pg",[330,851,852],{"class":343}," OWNER",[330,854,855],{"class":358}," app;\n",[330,857,858],{"class":332,"line":381},[330,859,860],{"class":358},"\\q\n",[30,862,864],{"id":863},"paso-4-migrar-con-pgloader","Paso 4: Migrar con pgloader",[12,866,867],{},"Crea el archivo de comandos de pgloader:",[321,869,871],{"className":752,"code":870,"language":754,"meta":326,"style":326},"cat > migracion.load \u003C\u003C'EOF'\nLOAD DATABASE\n  FROM mysql://root:mysql_password@localhost:3306/miapp_mysql\n  INTO postgresql://app:contraseña_segura@localhost:5432/miapp_pg\n\nWITH include drop,\n     create tables,\n     create indexes,\n     reset sequences,\n     downcase identifiers,\n     batch rows = 1000,\n     prefetch rows = 10000\n\nSET maintenance_work_mem TO '512MB',\n    work_mem TO '128MB'\n\n-- Conversiones personalizadas\nCAST type tinyint to boolean using tinyint-to-boolean,\n     type int with extra auto_increment to serial,\n     type bigint with extra auto_increment to bigserial\n\n-- Excluir tablas que no necesitas migrar (sesiones, cache, etc.)\nEXCLUDING TABLE NAMES MATCHING 'sessions', 'cache', 'migrations'\n\nBEFORE LOAD DO\n  $$ CREATE SCHEMA IF NOT EXISTS public; $$;\n\nEOF\n",[188,872,873,890,895,900,905,909,914,919,924,929,934,939,944,948,953,958,962,967,972,977,982,986,991,996,1000,1005,1010,1014],{"__ignoreMap":326},[330,874,875,878,881,884,887],{"class":332,"line":333},[330,876,877],{"class":761},"cat",[330,879,880],{"class":436}," >",[330,882,883],{"class":351}," migracion.load",[330,885,886],{"class":436}," \u003C\u003C",[330,888,889],{"class":347},"'EOF'\n",[330,891,892],{"class":332,"line":340},[330,893,894],{"class":351},"LOAD DATABASE\n",[330,896,897],{"class":332,"line":381},[330,898,899],{"class":351},"  FROM mysql://root:mysql_password@localhost:3306/miapp_mysql\n",[330,901,902],{"class":332,"line":387},[330,903,904],{"class":351},"  INTO postgresql://app:contraseña_segura@localhost:5432/miapp_pg\n",[330,906,907],{"class":332,"line":418},[330,908,422],{"emptyLinePlaceholder":421},[330,910,911],{"class":332,"line":425},[330,912,913],{"class":351},"WITH include drop,\n",[330,915,916],{"class":332,"line":431},[330,917,918],{"class":351},"     create tables,\n",[330,920,921],{"class":332,"line":460},[330,922,923],{"class":351},"     create indexes,\n",[330,925,926],{"class":332,"line":466},[330,927,928],{"class":351},"     reset sequences,\n",[330,930,931],{"class":332,"line":490},[330,932,933],{"class":351},"     downcase identifiers,\n",[330,935,936],{"class":332,"line":495},[330,937,938],{"class":351},"     batch rows = 1000,\n",[330,940,941],{"class":332,"line":501},[330,942,943],{"class":351},"     prefetch rows = 10000\n",[330,945,946],{"class":332,"line":525},[330,947,422],{"emptyLinePlaceholder":421},[330,949,950],{"class":332,"line":530},[330,951,952],{"class":351},"SET maintenance_work_mem TO '512MB',\n",[330,954,955],{"class":332,"line":554},[330,956,957],{"class":351},"    work_mem TO '128MB'\n",[330,959,960],{"class":332,"line":559},[330,961,422],{"emptyLinePlaceholder":421},[330,963,964],{"class":332,"line":565},[330,965,966],{"class":351},"-- Conversiones personalizadas\n",[330,968,969],{"class":332,"line":607},[330,970,971],{"class":351},"CAST type tinyint to boolean using tinyint-to-boolean,\n",[330,973,974],{"class":332,"line":612},[330,975,976],{"class":351},"     type int with extra auto_increment to serial,\n",[330,978,979],{"class":332,"line":656},[330,980,981],{"class":351},"     type bigint with extra auto_increment to bigserial\n",[330,983,984],{"class":332,"line":661},[330,985,422],{"emptyLinePlaceholder":421},[330,987,988],{"class":332,"line":667},[330,989,990],{"class":351},"-- Excluir tablas que no necesitas migrar (sesiones, cache, etc.)\n",[330,992,993],{"class":332,"line":692},[330,994,995],{"class":351},"EXCLUDING TABLE NAMES MATCHING 'sessions', 'cache', 'migrations'\n",[330,997,998],{"class":332,"line":697},[330,999,422],{"emptyLinePlaceholder":421},[330,1001,1002],{"class":332,"line":719},[330,1003,1004],{"class":351},"BEFORE LOAD DO\n",[330,1006,1007],{"class":332,"line":724},[330,1008,1009],{"class":351},"  $$ CREATE SCHEMA IF NOT EXISTS public; $$;\n",[330,1011,1012],{"class":332,"line":730},[330,1013,422],{"emptyLinePlaceholder":421},[330,1015,1017],{"class":332,"line":1016},28,[330,1018,1019],{"class":347},"EOF\n",[12,1021,1022],{},"Ejecuta la migración:",[321,1024,1026],{"className":752,"code":1025,"language":754,"meta":326,"style":326},"pgloader migracion.load\n",[188,1027,1028],{"__ignoreMap":326},[330,1029,1030,1032],{"class":332,"line":333},[330,1031,748],{"class":761},[330,1033,1034],{"class":351}," migracion.load\n",[12,1036,1037],{},"pgloader muestra el progreso tabla por tabla con conteo de filas, errores y tiempo. Una migración típica de 10 GB toma 5-15 minutos.",[154,1039],{},[30,1041,1043],{"id":1042},"paso-5-validar-la-migración","Paso 5: Validar la migración",[164,1045,1047],{"id":1046},"conteo-de-registros","Conteo de registros",[12,1049,1050],{},"Compara el número de registros en cada tabla entre MySQL y PostgreSQL:",[321,1052,1054],{"className":752,"code":1053,"language":754,"meta":326,"style":326},"# Script de validación\ncat > validar.py \u003C\u003C'PYTHON'\nimport mysql.connector\nimport psycopg2\n\n# Conexiones\nmy = mysql.connector.connect(host='localhost', user='root', password='mysql_password', database='miapp_mysql')\npg = psycopg2.connect(host='localhost', user='app', password='contraseña_segura', dbname='miapp_pg')\n\nmy_cur = my.cursor()\npg_cur = pg.cursor()\n\n# Obtener tablas de MySQL\nmy_cur.execute(\"SHOW TABLES\")\ntables = [t[0] for t in my_cur.fetchall()]\n\nprint(f\"{'Tabla':\u003C30} {'MySQL':>10} {'PostgreSQL':>12} {'OK':>5}\")\nprint(\"-\" * 60)\n\nerrors = 0\nfor table in tables:\n    try:\n        my_cur.execute(f\"SELECT COUNT(*) FROM `{table}`\")\n        my_count = my_cur.fetchone()[0]\n\n        pg_cur.execute(f\"SELECT COUNT(*) FROM {table.lower()}\")\n        pg_count = pg_cur.fetchone()[0]\n\n        ok = \"✓\" if my_count == pg_count else \"✗\"\n        if my_count != pg_count:\n            errors += 1\n\n        print(f\"{table:\u003C30} {my_count:>10} {pg_count:>12} {ok:>5}\")\n    except Exception as e:\n        print(f\"{table:\u003C30} {'ERROR':>10} {str(e)[:20]:>12}\")\n        errors += 1\n\nprint(f\"\\nResultado: {len(tables) - errors}/{len(tables)} tablas coinciden\")\n\nmy.close()\npg.close()\nPYTHON\n\npython3 validar.py\n",[188,1055,1056,1061,1075,1080,1085,1089,1094,1099,1104,1108,1113,1118,1122,1127,1132,1137,1141,1146,1151,1155,1160,1165,1170,1175,1180,1184,1189,1194,1198,1204,1210,1216,1221,1227,1233,1239,1245,1250,1256,1261,1267,1273,1279,1284],{"__ignoreMap":326},[330,1057,1058],{"class":332,"line":333},[330,1059,1060],{"class":336},"# Script de validación\n",[330,1062,1063,1065,1067,1070,1072],{"class":332,"line":340},[330,1064,877],{"class":761},[330,1066,880],{"class":436},[330,1068,1069],{"class":351}," validar.py",[330,1071,886],{"class":436},[330,1073,1074],{"class":347},"'PYTHON'\n",[330,1076,1077],{"class":332,"line":381},[330,1078,1079],{"class":351},"import mysql.connector\n",[330,1081,1082],{"class":332,"line":387},[330,1083,1084],{"class":351},"import psycopg2\n",[330,1086,1087],{"class":332,"line":418},[330,1088,422],{"emptyLinePlaceholder":421},[330,1090,1091],{"class":332,"line":425},[330,1092,1093],{"class":351},"# Conexiones\n",[330,1095,1096],{"class":332,"line":431},[330,1097,1098],{"class":351},"my = mysql.connector.connect(host='localhost', user='root', password='mysql_password', database='miapp_mysql')\n",[330,1100,1101],{"class":332,"line":460},[330,1102,1103],{"class":351},"pg = psycopg2.connect(host='localhost', user='app', password='contraseña_segura', dbname='miapp_pg')\n",[330,1105,1106],{"class":332,"line":466},[330,1107,422],{"emptyLinePlaceholder":421},[330,1109,1110],{"class":332,"line":490},[330,1111,1112],{"class":351},"my_cur = my.cursor()\n",[330,1114,1115],{"class":332,"line":495},[330,1116,1117],{"class":351},"pg_cur = pg.cursor()\n",[330,1119,1120],{"class":332,"line":501},[330,1121,422],{"emptyLinePlaceholder":421},[330,1123,1124],{"class":332,"line":525},[330,1125,1126],{"class":351},"# Obtener tablas de MySQL\n",[330,1128,1129],{"class":332,"line":530},[330,1130,1131],{"class":351},"my_cur.execute(\"SHOW TABLES\")\n",[330,1133,1134],{"class":332,"line":554},[330,1135,1136],{"class":351},"tables = [t[0] for t in my_cur.fetchall()]\n",[330,1138,1139],{"class":332,"line":559},[330,1140,422],{"emptyLinePlaceholder":421},[330,1142,1143],{"class":332,"line":565},[330,1144,1145],{"class":351},"print(f\"{'Tabla':\u003C30} {'MySQL':>10} {'PostgreSQL':>12} {'OK':>5}\")\n",[330,1147,1148],{"class":332,"line":607},[330,1149,1150],{"class":351},"print(\"-\" * 60)\n",[330,1152,1153],{"class":332,"line":612},[330,1154,422],{"emptyLinePlaceholder":421},[330,1156,1157],{"class":332,"line":656},[330,1158,1159],{"class":351},"errors = 0\n",[330,1161,1162],{"class":332,"line":661},[330,1163,1164],{"class":351},"for table in tables:\n",[330,1166,1167],{"class":332,"line":667},[330,1168,1169],{"class":351},"    try:\n",[330,1171,1172],{"class":332,"line":692},[330,1173,1174],{"class":351},"        my_cur.execute(f\"SELECT COUNT(*) FROM `{table}`\")\n",[330,1176,1177],{"class":332,"line":697},[330,1178,1179],{"class":351},"        my_count = my_cur.fetchone()[0]\n",[330,1181,1182],{"class":332,"line":719},[330,1183,422],{"emptyLinePlaceholder":421},[330,1185,1186],{"class":332,"line":724},[330,1187,1188],{"class":351},"        pg_cur.execute(f\"SELECT COUNT(*) FROM {table.lower()}\")\n",[330,1190,1191],{"class":332,"line":730},[330,1192,1193],{"class":351},"        pg_count = pg_cur.fetchone()[0]\n",[330,1195,1196],{"class":332,"line":1016},[330,1197,422],{"emptyLinePlaceholder":421},[330,1199,1201],{"class":332,"line":1200},29,[330,1202,1203],{"class":351},"        ok = \"✓\" if my_count == pg_count else \"✗\"\n",[330,1205,1207],{"class":332,"line":1206},30,[330,1208,1209],{"class":351},"        if my_count != pg_count:\n",[330,1211,1213],{"class":332,"line":1212},31,[330,1214,1215],{"class":351},"            errors += 1\n",[330,1217,1219],{"class":332,"line":1218},32,[330,1220,422],{"emptyLinePlaceholder":421},[330,1222,1224],{"class":332,"line":1223},33,[330,1225,1226],{"class":351},"        print(f\"{table:\u003C30} {my_count:>10} {pg_count:>12} {ok:>5}\")\n",[330,1228,1230],{"class":332,"line":1229},34,[330,1231,1232],{"class":351},"    except Exception as e:\n",[330,1234,1236],{"class":332,"line":1235},35,[330,1237,1238],{"class":351},"        print(f\"{table:\u003C30} {'ERROR':>10} {str(e)[:20]:>12}\")\n",[330,1240,1242],{"class":332,"line":1241},36,[330,1243,1244],{"class":351},"        errors += 1\n",[330,1246,1248],{"class":332,"line":1247},37,[330,1249,422],{"emptyLinePlaceholder":421},[330,1251,1253],{"class":332,"line":1252},38,[330,1254,1255],{"class":351},"print(f\"\\nResultado: {len(tables) - errors}/{len(tables)} tablas coinciden\")\n",[330,1257,1259],{"class":332,"line":1258},39,[330,1260,422],{"emptyLinePlaceholder":421},[330,1262,1264],{"class":332,"line":1263},40,[330,1265,1266],{"class":351},"my.close()\n",[330,1268,1270],{"class":332,"line":1269},41,[330,1271,1272],{"class":351},"pg.close()\n",[330,1274,1276],{"class":332,"line":1275},42,[330,1277,1278],{"class":347},"PYTHON\n",[330,1280,1282],{"class":332,"line":1281},43,[330,1283,422],{"emptyLinePlaceholder":421},[330,1285,1287,1290],{"class":332,"line":1286},44,[330,1288,1289],{"class":761},"python3",[330,1291,1292],{"class":351}," validar.py\n",[164,1294,1296],{"id":1295},"verificar-secuencias","Verificar secuencias",[12,1298,1299],{},"pgloader resetea las secuencias automáticamente, pero verifica:",[321,1301,1303],{"className":323,"code":1302,"language":325,"meta":326,"style":326},"-- En PostgreSQL: verificar que las secuencias están al valor correcto\nSELECT schemaname, sequencename, last_value\nFROM pg_sequences\nWHERE schemaname = 'public';\n",[188,1304,1305,1310,1317,1324],{"__ignoreMap":326},[330,1306,1307],{"class":332,"line":333},[330,1308,1309],{"class":336},"-- En PostgreSQL: verificar que las secuencias están al valor correcto\n",[330,1311,1312,1314],{"class":332,"line":340},[330,1313,344],{"class":343},[330,1315,1316],{"class":358}," schemaname, sequencename, last_value\n",[330,1318,1319,1321],{"class":332,"line":381},[330,1320,519],{"class":343},[330,1322,1323],{"class":358}," pg_sequences\n",[330,1325,1326,1329,1332,1335,1337,1340,1342],{"class":332,"line":387},[330,1327,1328],{"class":343},"WHERE",[330,1330,1331],{"class":358}," schemaname ",[330,1333,1334],{"class":436},"=",[330,1336,633],{"class":347},[330,1338,1339],{"class":351},"public",[330,1341,509],{"class":347},[330,1343,378],{"class":358},[164,1345,1347],{"id":1346},"verificar-constraints-e-índices","Verificar constraints e índices",[321,1349,1351],{"className":323,"code":1350,"language":325,"meta":326,"style":326},"-- Constraints\nSELECT table_name, constraint_name, constraint_type\nFROM information_schema.table_constraints\nWHERE table_schema = 'public'\nORDER BY table_name;\n\n-- Índices\nSELECT tablename, indexname, indexdef\nFROM pg_indexes\nWHERE schemaname = 'public'\nORDER BY tablename;\n",[188,1352,1353,1358,1365,1378,1394,1402,1406,1411,1418,1425,1439],{"__ignoreMap":326},[330,1354,1355],{"class":332,"line":333},[330,1356,1357],{"class":336},"-- Constraints\n",[330,1359,1360,1362],{"class":332,"line":340},[330,1361,344],{"class":343},[330,1363,1364],{"class":358}," table_name, constraint_name, constraint_type\n",[330,1366,1367,1369,1373,1375],{"class":332,"line":381},[330,1368,519],{"class":343},[330,1370,1372],{"class":1371},"s_hVV"," information_schema",[330,1374,359],{"class":358},[330,1376,1377],{"class":1371},"table_constraints\n",[330,1379,1380,1382,1385,1387,1389,1391],{"class":332,"line":387},[330,1381,1328],{"class":343},[330,1383,1384],{"class":358}," table_schema ",[330,1386,1334],{"class":436},[330,1388,633],{"class":347},[330,1390,1339],{"class":351},[330,1392,1393],{"class":347},"'\n",[330,1395,1396,1399],{"class":332,"line":418},[330,1397,1398],{"class":343},"ORDER BY",[330,1400,1401],{"class":358}," table_name;\n",[330,1403,1404],{"class":332,"line":425},[330,1405,422],{"emptyLinePlaceholder":421},[330,1407,1408],{"class":332,"line":431},[330,1409,1410],{"class":336},"-- Índices\n",[330,1412,1413,1415],{"class":332,"line":460},[330,1414,344],{"class":343},[330,1416,1417],{"class":358}," tablename, indexname, indexdef\n",[330,1419,1420,1422],{"class":332,"line":466},[330,1421,519],{"class":343},[330,1423,1424],{"class":358}," pg_indexes\n",[330,1426,1427,1429,1431,1433,1435,1437],{"class":332,"line":490},[330,1428,1328],{"class":343},[330,1430,1331],{"class":358},[330,1432,1334],{"class":436},[330,1434,633],{"class":347},[330,1436,1339],{"class":351},[330,1438,1393],{"class":347},[330,1440,1441,1443],{"class":332,"line":495},[330,1442,1398],{"class":343},[330,1444,1445],{"class":358}," tablename;\n",[30,1447,1449],{"id":1448},"paso-6-ajustar-la-aplicación","Paso 6: Ajustar la aplicación",[164,1451,1453],{"id":1452},"cambiar-el-driver-de-conexión","Cambiar el driver de conexión",[12,1455,1456],{},"Si tu aplicación usa un ORM, el cambio suele ser solo la cadena de conexión:",[321,1458,1462],{"className":1459,"code":1460,"language":1461,"meta":326,"style":326},"language-python shiki shiki-themes material-theme-lighter github-light github-dark","# SQLAlchemy — antes (MySQL)\nDATABASE_URL = \"mysql+pymysql://root:password@localhost/miapp\"\n\n# SQLAlchemy — después (PostgreSQL)\nDATABASE_URL = \"postgresql+psycopg2://app:password@localhost/miapp_pg\"\n","python",[188,1463,1464,1469,1485,1489,1494],{"__ignoreMap":326},[330,1465,1466],{"class":332,"line":333},[330,1467,1468],{"class":336},"# SQLAlchemy — antes (MySQL)\n",[330,1470,1471,1474,1477,1479,1482],{"class":332,"line":340},[330,1472,1473],{"class":1371},"DATABASE_URL",[330,1475,1476],{"class":436}," =",[330,1478,392],{"class":347},[330,1480,1481],{"class":351},"mysql+pymysql://root:password@localhost/miapp",[330,1483,1484],{"class":347},"\"\n",[330,1486,1487],{"class":332,"line":381},[330,1488,422],{"emptyLinePlaceholder":421},[330,1490,1491],{"class":332,"line":387},[330,1492,1493],{"class":336},"# SQLAlchemy — después (PostgreSQL)\n",[330,1495,1496,1498,1500,1502,1505],{"class":332,"line":418},[330,1497,1473],{"class":1371},[330,1499,1476],{"class":436},[330,1501,392],{"class":347},[330,1503,1504],{"class":351},"postgresql+psycopg2://app:password@localhost/miapp_pg",[330,1506,1484],{"class":347},[321,1508,1512],{"className":1509,"code":1510,"language":1511,"meta":326,"style":326},"language-javascript shiki shiki-themes material-theme-lighter github-light github-dark","// Prisma — schema.prisma\ndatasource db {\n  // Antes\n  // provider = \"mysql\"\n  // Después\n  provider = \"postgresql\"\n  url      = env(\"DATABASE_URL\")\n}\n","javascript",[188,1513,1514,1519,1524,1529,1534,1539,1544,1549],{"__ignoreMap":326},[330,1515,1516],{"class":332,"line":333},[330,1517,1518],{},"// Prisma — schema.prisma\n",[330,1520,1521],{"class":332,"line":340},[330,1522,1523],{},"datasource db {\n",[330,1525,1526],{"class":332,"line":381},[330,1527,1528],{},"  // Antes\n",[330,1530,1531],{"class":332,"line":387},[330,1532,1533],{},"  // provider = \"mysql\"\n",[330,1535,1536],{"class":332,"line":418},[330,1537,1538],{},"  // Después\n",[330,1540,1541],{"class":332,"line":425},[330,1542,1543],{},"  provider = \"postgresql\"\n",[330,1545,1546],{"class":332,"line":431},[330,1547,1548],{},"  url      = env(\"DATABASE_URL\")\n",[330,1550,1551],{"class":332,"line":460},[330,1552,1553],{},"}\n",[164,1555,1557],{"id":1556},"ajustar-queries-raw","Ajustar queries raw",[12,1559,1560],{},"Si tienes queries SQL escritas directamente (no via ORM), revisa los patrones que listamos en el Paso 1 y reescríbelos. Los más comunes:",[38,1562,1563,1566,1575,1583,1591],{},[41,1564,1565],{},"Comillas invertidas → comillas dobles o sin comillas",[41,1567,1568,1571,1572],{},[188,1569,1570],{},"GROUP_CONCAT"," → ",[188,1573,1574],{},"STRING_AGG",[41,1576,1577,1571,1580],{},[188,1578,1579],{},"IFNULL",[188,1581,1582],{},"COALESCE",[41,1584,1585,1571,1588],{},[188,1586,1587],{},"LIMIT offset, count",[188,1589,1590],{},"LIMIT count OFFSET offset",[41,1592,1593,1596],{},[188,1594,1595],{},"NOW()"," devuelve timestamp con timezone en PostgreSQL",[154,1598],{},[30,1600,1602],{"id":1601},"paso-7-pruebas-de-regresión","Paso 7: Pruebas de regresión",[12,1604,1605],{},"Antes de cortar a producción, ejecuta:",[1607,1608,1609,1616,1622,1631],"ol",{},[41,1610,1611,1615],{},[1612,1613,1614],"strong",{},"Suite de tests"," — si tienes tests automatizados, ejecútalos contra PostgreSQL",[41,1617,1618,1621],{},[1612,1619,1620],{},"Pruebas manuales"," — recorre los flujos principales de la aplicación (crear registro, editar, buscar, reportes)",[41,1623,1624,1627,1628],{},[1612,1625,1626],{},"Pruebas de rendimiento"," — compara tiempos de consultas críticas entre MySQL y PostgreSQL. PostgreSQL puede ser más rápido o más lento dependiendo de las queries — optimiza con ",[188,1629,1630],{},"EXPLAIN ANALYZE",[41,1632,1633,1636,1637,1641],{},[1612,1634,1635],{},"Prueba de backup"," — verifica que puedes ",[16,1638,1640],{"href":1639},"/blog/backup-automatizado-python-cron","respaldar y restaurar"," la base de datos PostgreSQL",[30,1643,1645],{"id":1644},"paso-8-corte-a-producción","Paso 8: Corte a producción",[12,1647,1648],{},"Cuando las pruebas pasan, programa la ventana de mantenimiento:",[321,1650,1652],{"className":752,"code":1651,"language":754,"meta":326,"style":326},"# 1. Poner la aplicación en mantenimiento\n# 2. Exportar datos frescos de MySQL de producción\nmysqldump --single-transaction -u root -p miapp > produccion_final.sql\n\n# 3. Migrar con pgloader (usando los datos de producción)\npgloader migracion.load\n\n# 4. Validar conteos\npython3 validar.py\n\n# 5. Cambiar la cadena de conexión en la aplicación\n# 6. Reiniciar la aplicación apuntando a PostgreSQL\n# 7. Verificar que todo funciona\n# 8. Quitar modo mantenimiento\n",[188,1653,1654,1659,1664,1688,1692,1697,1703,1707,1712,1718,1722,1727,1732,1737],{"__ignoreMap":326},[330,1655,1656],{"class":332,"line":333},[330,1657,1658],{"class":336},"# 1. Poner la aplicación en mantenimiento\n",[330,1660,1661],{"class":332,"line":340},[330,1662,1663],{"class":336},"# 2. Exportar datos frescos de MySQL de producción\n",[330,1665,1666,1669,1672,1674,1677,1680,1683,1685],{"class":332,"line":381},[330,1667,1668],{"class":761},"mysqldump",[330,1670,1671],{"class":774}," --single-transaction",[330,1673,801],{"class":774},[330,1675,1676],{"class":351}," root",[330,1678,1679],{"class":774}," -p",[330,1681,1682],{"class":351}," miapp",[330,1684,880],{"class":436},[330,1686,1687],{"class":351}," produccion_final.sql\n",[330,1689,1690],{"class":332,"line":387},[330,1691,422],{"emptyLinePlaceholder":421},[330,1693,1694],{"class":332,"line":418},[330,1695,1696],{"class":336},"# 3. Migrar con pgloader (usando los datos de producción)\n",[330,1698,1699,1701],{"class":332,"line":425},[330,1700,748],{"class":761},[330,1702,1034],{"class":351},[330,1704,1705],{"class":332,"line":431},[330,1706,422],{"emptyLinePlaceholder":421},[330,1708,1709],{"class":332,"line":460},[330,1710,1711],{"class":336},"# 4. Validar conteos\n",[330,1713,1714,1716],{"class":332,"line":466},[330,1715,1289],{"class":761},[330,1717,1292],{"class":351},[330,1719,1720],{"class":332,"line":490},[330,1721,422],{"emptyLinePlaceholder":421},[330,1723,1724],{"class":332,"line":495},[330,1725,1726],{"class":336},"# 5. Cambiar la cadena de conexión en la aplicación\n",[330,1728,1729],{"class":332,"line":501},[330,1730,1731],{"class":336},"# 6. Reiniciar la aplicación apuntando a PostgreSQL\n",[330,1733,1734],{"class":332,"line":525},[330,1735,1736],{"class":336},"# 7. Verificar que todo funciona\n",[330,1738,1739],{"class":332,"line":530},[330,1740,1741],{"class":336},"# 8. Quitar modo mantenimiento\n",[1743,1744,1747],"alert",{"title":1745,"type":1746},"Mantén MySQL disponible","warning",[12,1748,1749],{},"No apagues MySQL inmediatamente después del corte. Mantenlo en solo lectura durante 1-2 semanas como plan de rollback. Si algo falla en PostgreSQL que no detectaste en pruebas, puedes volver a MySQL rápidamente.",[30,1751,1753],{"id":1752},"siguientes-pasos","Siguientes pasos",[12,1755,1756],{},"Con la migración completada:",[38,1758,1759,1772,1780,1786,1795],{},[41,1760,1761,1764,1765,452,1768,1771],{},[1612,1762,1763],{},"Optimizar PostgreSQL"," — ajustar ",[188,1766,1767],{},"shared_buffers",[188,1769,1770],{},"work_mem"," y crear índices específicos para tus queries más frecuentes",[41,1773,1774,1779],{},[1612,1775,1776],{},[16,1777,1778],{"href":52},"Replicación"," — configurar alta disponibilidad con streaming replication",[41,1781,1782,1785],{},[1612,1783,1784],{},"Extensiones"," — aprovechar JSONB, pg_trgm para búsqueda fuzzy, PostGIS para datos geoespaciales",[41,1787,1788,1794],{},[1612,1789,1790],{},[16,1791,1793],{"href":1792},"/blog/monitoreo-grafana-prometheus","Monitoreo"," — dashboards de rendimiento de PostgreSQL con postgres_exporter",[41,1796,1797,1802],{},[1612,1798,1799],{},[16,1800,1801],{"href":65},"Bases de datos administradas"," — nuestro servicio DBA para que tu PostgreSQL siempre esté optimizado y protegido",[1804,1805],"call-to-action",{"description":1806,"eyebrow":1807,"icon":1808,"label":1809,"title":1810,"to":1811},"Planificamos y ejecutamos la migración completa con validación de datos, ajuste de queries y acompañamiento post-migración.","Migración profesional","i-lucide-database","Solicitar evaluación","¿Necesitas migrar de MySQL a PostgreSQL sin riesgos?","/contacto",[1813,1814,1815],"style",{},"html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .sw1J6, html code.shiki .sw1J6{--shiki-light:#F76D47;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sptTA, html code.shiki .sptTA{--shiki-light:#6182B8;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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);}html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .stzsN, html code.shiki .stzsN{--shiki-light:#91B859;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":326,"searchDepth":340,"depth":381,"links":1817},[1818,1819,1820,1824,1825,1826,1827,1832,1836,1837,1838],{"id":32,"depth":340,"text":33},{"id":70,"depth":340,"text":71},{"id":158,"depth":340,"text":159,"children":1821},[1822,1823],{"id":166,"depth":381,"text":167},{"id":318,"depth":381,"text":319},{"id":739,"depth":340,"text":740},{"id":785,"depth":340,"text":786},{"id":863,"depth":340,"text":864},{"id":1042,"depth":340,"text":1043,"children":1828},[1829,1830,1831],{"id":1046,"depth":381,"text":1047},{"id":1295,"depth":381,"text":1296},{"id":1346,"depth":381,"text":1347},{"id":1448,"depth":340,"text":1449,"children":1833},[1834,1835],{"id":1452,"depth":381,"text":1453},{"id":1556,"depth":381,"text":1557},{"id":1601,"depth":340,"text":1602},{"id":1644,"depth":340,"text":1645},{"id":1752,"depth":340,"text":1753},"tutorial",{"title":1841,"description":1842,"label":1809,"to":1811,"icon":1808},"¿Necesitas ayuda para migrar tu base de datos?","Planificamos y ejecutamos la migración de MySQL a PostgreSQL con validación completa, ajuste de queries y cero pérdida de datos.","2026-01-25","Guía completa para planificar y ejecutar la migración de una base de datos MySQL a PostgreSQL con pgloader, incluyendo conversión de tipos, ajuste de queries y validación de integridad.",false,"md",[1848,1851,1854,1857],{"question":1849,"answer":1850},"¿Por qué migrar de MySQL a PostgreSQL?","PostgreSQL ofrece tipos de datos más avanzados (JSONB, arrays, rangos), extensiones potentes (PostGIS, pg_trgm, pgvector), replicación streaming robusta, mejor soporte para transacciones complejas y cumplimiento más estricto del estándar SQL. Para aplicaciones empresariales que crecen, PostgreSQL escala mejor y tiene menos limitaciones que MySQL.",{"question":1852,"answer":1853},"¿Cuánto tiempo toma una migración?","La migración técnica de datos con pgloader puede tomar desde minutos (bases pequeñas) hasta horas (cientos de GB). Lo que más tiempo consume es el ajuste de queries y funciones específicas de MySQL, las pruebas de regresión y la validación. Una migración completa típica toma de 2 a 6 semanas incluyendo pruebas.",{"question":1855,"answer":1856},"¿Puedo migrar sin detener la aplicación?","Sí, con una estrategia de migración en paralelo: configuras PostgreSQL como réplica de MySQL usando herramientas como pg_chameleon, ejecutas ambas bases en paralelo durante un período de prueba, y cuando estás seguro, cortas el tráfico a PostgreSQL. En esta guía cubrimos la migración directa que requiere una ventana de mantenimiento.",{"question":1858,"answer":1859},"¿pgloader convierte los tipos de datos automáticamente?","Sí. pgloader mapea automáticamente la mayoría de tipos MySQL a sus equivalentes PostgreSQL — INT, VARCHAR, TEXT, DATETIME, DECIMAL, BLOB, etc. Para tipos específicos de MySQL (como ENUM, SET o TINYINT usado como boolean), puedes definir reglas de conversión personalizadas.","/images/blog/mysql-a-postgresql.jpg","Terminal mostrando el proceso de migración de MySQL a PostgreSQL con pgloader y barra de progreso",{},"/blog/tutorial/migrar-mysql-a-postgresql",{"title":5,"description":1844},"blog/tutorial/migrar-mysql-a-postgresql",[1867,1868,1869,1870,1839],"postgresql","mysql","migracion","bases-de-datos","jGZVLFmGfjo0fpVMv1TMumnUfTvPJGdB5lqN306GFtM",{"path":1873,"title":1874},"/blog/tutorial/instalar-proxmox-primera-vm","Instalar Proxmox VE y crear tu primera máquina virtual",{"path":1876,"title":1877},"/blog/tutorial/configurar-vpn-wireguard","Configurar VPN WireGuard en tu servidor en 10 minutos",[1879,1886,1893],{"path":1880,"title":1881,"description":1882,"date":1883,"category":1839,"image":1884,"imageAlt":1885,"readingTime":501},"/blog/tutorial/apis-rest-python-fastapi","Crear APIs REST con Python y FastAPI para integraciones empresariales","Guía paso a paso para construir una API REST profesional con Python y FastAPI que conecte tu ERP, CRM o cualquier sistema con validación, autenticación y documentación automática.","2026-03-04","/images/blog/fastapi-api-rest.jpg","Editor de código mostrando una API FastAPI con documentación Swagger generada automáticamente",{"path":1887,"title":1888,"description":1889,"date":1890,"category":1839,"image":1891,"imageAlt":1892,"readingTime":460},"/blog/tutorial/configurar-firewall-ufw-linux","Configurar firewall en Linux con UFW — reglas esenciales","Guía paso a paso para configurar UFW (Uncomplicated Firewall) en Ubuntu y Debian con las reglas esenciales para proteger servidores de producción.","2026-03-01","/images/blog/ufw-firewall-linux.jpg","Terminal de Linux mostrando reglas de firewall UFW activas protegiendo un servidor de producción",{"path":1894,"title":1895,"description":1896,"date":1897,"category":1839,"image":1898,"imageAlt":1899,"readingTime":490},"/blog/tutorial/traefik-reverse-proxy-docker","Configurar Traefik como reverse proxy para contenedores Docker","Guía paso a paso para instalar Traefik como reverse proxy con descubrimiento automático de contenedores Docker, SSL con Let's Encrypt y dashboard de monitoreo.","2026-02-28","/images/blog/traefik-docker.jpg","Dashboard de Traefik mostrando rutas automáticas hacia múltiples contenedores Docker con SSL activo"]