StackPractices
beginner Por Mathias Paulenko

Conectar a MySQL

Cómo conectar a MySQL en Python, JavaScript y Java — con pool de conexiones, SSL y consultas preparadas que aguantan en producción.

Visión General

Conectar a MySQL traba a la gente siempre en los mismos tres puntos: elegir el driver correcto, configurar bien SSL y autenticación (MySQL 8 cambió el plugin de autenticación por defecto) y usar un pool de conexiones para que el servidor no llegue al max_connections. Aquí tienes una configuración que funciona en Python, JavaScript y Java, lista para copiar y adaptar.

Cuándo Usar

Usa este recurso cuando:

  • Construyes una app o API cuyo almacén principal es MySQL 8.x (o MariaDB 10.x, que comparte el protocolo y la mayoría de drivers)
  • Escribes scripts que importan o exportan datos a través de una conexión MySQL
  • Migras un código a otro driver o añades un pool de conexiones a código que abre una conexión por consulta
  • Apuntas un servicio a una instancia gestionada como AWS RDS, Aurora o Cloud SQL

Si necesitas almacenamiento de documentos, escritura multi-región o consultas JSON más profundas, MySQL no es la opción por defecto — consulta Conectar a PostgreSQL como la alternativa más cercana.

Solución

Python

import os
import mysql.connector

conn = mysql.connector.connect(
    host=os.getenv("DB_HOST", "localhost"),
    database=os.getenv("DB_NAME", "mydb"),
    user=os.getenv("DB_USER", "user"),
    password=os.environ["DB_PASSWORD"],
    ssl_ca="ca.pem",            # obligatorio en producción
    ssl_verify_cert=True,
    connection_timeout=5,
    charset="utf8mb4"
)

cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT * FROM users WHERE id = %s", (1,))
row = cursor.fetchone()
cursor.close()
conn.close()

JavaScript

const mysql = require('mysql2/promise');

const pool = mysql.createPool({
    host: process.env.DB_HOST || 'localhost',
    database: process.env.DB_NAME || 'mydb',
    user: process.env.DB_USER || 'user',
    password: process.env.DB_PASSWORD,
    ssl: { ca: require('fs').readFileSync('ca.pem') },
    connectionLimit: 10,
    waitForConnections: true,
    queueLimit: 0,
    connectTimeout: 10000
});

async function getUser(id) {
    const [rows] = await pool.execute('SELECT * FROM users WHERE id = ?', [id]);
    return rows[0];
}

Java

import java.sql.*;

public class MySQLConnect {
    public Connection connect() throws SQLException {
        String url = "jdbc:mysql://localhost:3306/mydb"
            + "?sslMode=VERIFY_CA&serverTimezone=UTC"
            + "&connectTimeout=5000&socketTimeout=30000";
        return DriverManager.getConnection(url, "user", "pass");
    }

    public void queryUser(int id) throws SQLException {
        try (Connection conn = connect();
             PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
            stmt.setInt(1, id);
            ResultSet rs = stmt.executeQuery();
            while (rs.next()) {
                System.out.println(rs.getString("email"));
            }
        }
    }
}

Explicación

Los tres ejemplos pasan la entrada del usuario por consultas preparadas (%s en Python, ? en mysql2 y JDBC), así que el driver escapa los valores y la inyección SQL queda descartada. Cada llamada a mysql.connector.connect() abre un socket TCP nuevo y autentica — vale para un script, pero es demasiado lento por petición; los ejemplos con pool de más abajo lo resuelven. El pool de mysql2 mantiene vivo un conjunto fijo de conexiones y encola el trabajo cuando todas están ocupadas. La versión Java usa el com.mysql.cj.jdbc.Driver moderno (Connector/J 8.x), que lee serverTimezone y sslMode directamente de la URL JDBC.

flowchart diagram: App[Aplicación

Dos detalles que queman a la gente más tarde. El utf8 de MySQL es una codificación de 3 bytes, no UTF-8 real — usa utf8mb4 (como en el ejemplo de Python, charset: 'utf8mb4' en mysql2 o characterEncoding=UTF-8 en JDBC) o los emojis y los caracteres de 4 bytes se corrompen sin avisar. Y wait_timeout vale 28800 segundos por defecto en el servidor: las conexiones inactivas del pool se mueren debajo de ti, así que dale al pool un timeout de inactividad más corto o keepalive (ver Consejos de Producción).

Una vez conectado, puedes ejecutar SQL en crudo o pasar a un ORM para operaciones CRUD.

Lo que funciona

  1. Dimensiona el pool para que connectionLimit × instancias de la app quede por debajo del max_connections del servidor (151 por defecto), dejando margen para sesiones de administración
  2. Verifica el certificado del servidor en producción (ssl_verify_cert=True, sslMode=VERIFY_CA); sslmode=require cifra pero no comprueba la identidad
  3. Dale al pool un timeout de inactividad más corto que el wait_timeout de MySQL, o las conexiones inactivas vuelven muertas en la siguiente consulta
  4. Usa execute() / consultas preparadas para cualquier dato del usuario; concatenar en query() es como ocurren las inyecciones
  5. Guarda las credenciales en variables de entorno o un gestor de secretos — en AWS RDS puedes omitir la contraseña con autenticación IAM de base de datos

Errores Comunes

  1. Instalar el paquete mysql deprecado en Node.js — lleva años sin mantenimiento; usa mysql2
  2. Olvidar serverTimezone=UTC en la URL JDBC, que desplaza los valores TIMESTAMP según la zona horaria local de la JVM
  3. Abrir una conexión nueva dentro de un bucle de peticiones en vez de pedirla al pool — llegarás a “Too many connections” (error 1040)
  4. Dejar que el pool sobreviva al wait_timeout y luego servir errores PROTOCOL_CONNECTION_LOST a los usuarios
  5. Asumir que utf8 es suficiente — guarda como máximo 3 bytes por carácter; necesitas utf8mb4

Ejemplos Avanzados

Python con pool de conexiones y gestor de contexto

import mysql.connector
from mysql.connector import pooling
from contextlib import contextmanager

mysql_pool = pooling.MySQLConnectionPool(
    pool_name="mypool",
    pool_size=10,
    host="localhost",
    database="mydb",
    user="user",
    password="pass",
    ssl_ca="ca.pem",
    ssl_verify_cert=True
)

@contextmanager
def get_db_cursor():
    conn = mysql_pool.get_connection()
    try:
        cursor = conn.cursor(dictionary=True)
        yield cursor
        conn.commit()
        cursor.close()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()  # devuelve la conexión al pool

# Uso
with get_db_cursor() as cur:
    cur.execute("SELECT * FROM users WHERE active = %s", (True,))
    rows = cur.fetchall()
    for row in rows:
        print(row)

JavaScript con manejo de transacciones

const mysql = require('mysql2/promise');

const pool = mysql.createPool({
    host: 'localhost',
    database: 'mydb',
    user: 'user',
    password: 'pass',
    ssl: { ca: require('fs').readFileSync('ca.pem') },
    waitForConnections: true,
    connectionLimit: 10,
    queueLimit: 0,
    enableKeepAlive: true,
    keepAliveInitialDelay: 10000
});

async function transferBalance(fromId, toId, amount) {
    const conn = await pool.getConnection();
    try {
        await conn.beginTransaction();

        await conn.execute(
            'UPDATE accounts SET balance = balance - ? WHERE id = ?',
            [amount, fromId]
        );

        await conn.execute(
            'UPDATE accounts SET balance = balance + ? WHERE id = ?',
            [amount, toId]
        );

        await conn.commit();
    } catch (err) {
        await conn.rollback();
        throw err;
    } finally {
        conn.release();
    }
}

Java con pool HikariCP

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.*;

public class MySQLPool {
    private static final HikariDataSource ds;

    static {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb?sslMode=VERIFY_CA&serverTimezone=UTC&rewriteBatchedStatements=true");
        config.setUsername("user");
        config.setPassword("pass");
        config.setMaximumPoolSize(20);
        config.setMinimumIdle(5);
        config.setIdleTimeout(30000);
        config.setConnectionTimeout(5000);
        config.setKeepaliveTime(300000); // refresca conexiones antes de que wait_timeout las mate
        ds = new HikariDataSource(config);
    }

    public static Connection getConnection() throws SQLException {
        return ds.getConnection();
    }

    public static void batchInsert(List<String> emails) throws SQLException {
        try (Connection conn = getConnection();
             PreparedStatement stmt = conn.prepareStatement(
                 "INSERT INTO users (email) VALUES (?)")) {
            for (String email : emails) {
                stmt.setString(1, email);
                stmt.addBatch();
            }
            stmt.executeBatch(); // rápido gracias a rewriteBatchedStatements=true
        }
    }
}

Python con PyMySQL (alternativa en Python puro)

import pymysql
from contextlib import contextmanager

@contextmanager
def get_db():
    conn = pymysql.connect(
        host="localhost",
        database="mydb",
        user="user",
        password="pass",
        ssl={"ca": "ca.pem"},
        charset="utf8mb4",
        cursorclass=pymysql.cursors.DictCursor
    )
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()

# Uso
with get_db() as conn:
    with conn.cursor() as cur:
        cur.execute("SELECT * FROM users WHERE id = %s", (1,))
        row = cur.fetchone()
        print(row)

Variantes

TecnologíaDriverAsyncPoolingNotas
Pythonmysql-connector-pythonNoMySQLConnectionPoolDriver oficial de Oracle
PythonPyMySQLNoManualPython puro, se instala sin extensiones C
PythonaiomysqlSíIntegradoMySQL asíncrono para asyncio
JavaScriptmysql2Sí (Promise)createPoolRecomendado para Node.js
JavaScriptmysqlNo (callback)createPoolDriver legacy con callbacks — evítalo en código nuevo
JavaJDBC + HikariCPNoHikariCPEstándar de la industria — opción de pooling para Spring Boot
Gogo-sql-driver/mysqlSídatabase/sqlDriver estándar de Go

Consejos de Producción

  1. Activa enableKeepAlive en mysql2. Las conexiones inactivas las descarta el servidor (wait_timeout, 28800s por defecto) o los equipos de red intermedios; las sondas keepalive evitan que el pool te entregue un socket muerto:
const pool = mysql.createPool({
    // ... otras opciones
    enableKeepAlive: true,
    keepAliveInitialDelay: 10000
});
  1. Usa LOAD DATA INFILE para cargas masivas. Es órdenes de magnitud más rápido que INSERT fila a fila para datasets grandes; tanto el servidor como el driver cliente necesitan local_infile activado:
LOAD DATA LOCAL INFILE '/tmp/users.csv'
INTO TABLE users
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n'
IGNORE 1 ROWS
(id, email, name);
  1. Ajusta wait_timeout e interactive_timeout en el servidor. Bájalos para reciclar antes las conexiones inactivas, o súbelos a propósito si el pool mantiene conexiones abiertas — en cualquier caso, mantén el timeout de inactividad del pool por debajo del del servidor:
SET GLOBAL wait_timeout = 300;
SET GLOBAL interactive_timeout = 300;
  1. Usa autenticación IAM en AWS RDS. Sustituye contraseñas guardadas por tokens de autenticación de 15 minutos, generados bajo demanda (hace falta SSL para usarlos):
aws rds generate-db-auth-token \
  --hostname mydb.abc123.us-east-1.rds.amazonaws.com \
  --port 3306 \
  --username myuser
  1. Vigila SHOW PROCESSLIST para detectar consultas fugadas. Muestra lo que hace cada conexión en este momento, para localizar y matar la consulta que bloquea al resto:
SHOW PROCESSLIST;
KILL <thread_id>;

Solución de Problemas

ErrorQué significaCómo arreglarlo
ECONNREFUSEDNo hay nada escuchando en host:puerto, o un firewall/grupo de seguridad lo bloqueaComprueba que mysqld está arriba, el puerto 3306 abierto y el host cliente permitido
ETIMEDOUT / PROTOCOL_CONNECTION_LOSTCaída de red, o el servidor mató una conexión inactiva en wait_timeoutActiva keepalive en el pool y pon su timeout de inactividad por debajo de wait_timeout
ER_ACCESS_DENIED_ERROR (1045)Contraseña incorrecta, o no hay permiso para ese par usuario@hostLa autenticación MySQL es por usuario+host — recrea el permiso para la IP/host real del cliente
ER_BAD_DB_ERROR (1049)El nombre de la base de datos no existe (o el usuario no puede verla)Crea el esquema o corrige el nombre en la configuración de conexión
Too many connections (1040)max_connections (151 por defecto) agotadoReduce los pools entre instancias de la app, o sube max_connections
Authentication plugin 'caching_sha2_password' cannot be loadedUn cliente/driver antiguo contra MySQL 8+Actualiza el driver; como último recurso, ALTER USER ... IDENTIFIED WITH mysql_native_password (deprecado, desactivado por defecto en 8.4)
Errores de certificado SSLBundle CA ausente o desactualizadoApunta ssl_ca / sslMode=VERIFY_CA a un bundle CA actual

Ver también

Preguntas frecuentes

¿Debo usar mysql-connector-python o PyMySQL?

mysql-connector-python es el driver oficial de Oracle y la opción por defecto más segura. Recurre a PyMySQL cuando no puedas instalar extensiones C o cuando topes con un caso límite del driver de Oracle — ambos aceptan los mismos placeholders %s, así que el cambio es pequeño.

¿Cómo manejo los timeouts de conexión en mysql2?

Configura connectTimeout en las opciones del pool — mysql2 no implementa acquireTimeout ni timeout, que eran del paquete legacy mysql. Para pools de larga duración, combina connectTimeout con enableKeepAlive y keepAliveInitialDelay para que los sockets muertos se refresquen en lugar de servirse a las consultas.

¿Cuál es la diferencia entre query() y execute() en mysql2?

query() envía el SQL como texto plano. execute() emite una consulta preparada con parámetros vinculados — segura contra inyección SQL y algo más rápida en llamadas repetidas porque el servidor reutiliza el plan de ejecución.

¿Cómo manejo los fallos de conexión con elegancia?

Envuelve las llamadas en lógica de reintento con backoff exponencial y un tope de intentos — tenacity en Python, p-retry en Node.js:

from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10))
def query_with_retry(sql, params):
    with get_db_cursor() as cur:
        cur.execute(sql, params)
        return cur.fetchall()
¿Cómo activo el log de consultas para depurar?

Activa el slow query log para capturar todo lo que supere un umbral:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.1;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

Después inspecciona las consultas marcadas con EXPLAIN; si el plan muestra escaneos completos en tablas grandes, consulta la guía de estrategias de indexación.

¿Qué es el multiplexing de conexiones en MySQL?

ProxySQL y MySQL Router se sitúan entre la app y la base de datos, manteniendo muchas sesiones de cliente sobre un conjunto menor de conexiones reales al servidor. Importa cuando tienes cientos de instancias de app contra un max_connections de 151. En ProxySQL, transaction_persistent=0 es el ajuste que la mayoría de apps quieren.

¿Cómo uso SSL con MySQL en AWS RDS?

Descarga el bundle CA actual de RDS (bundles por región como rds-ca-rsa2048-g1, o global-bundle.pem para todas las regiones — el viejo bundle 2019 ya está rotado) y apúntalo en el driver:

conn = mysql.connector.connect(
    host="mydb.abc123.us-east-1.rds.amazonaws.com",
    database="mydb",
    user="user",
    password="pass",
    ssl_ca="rds-ca-rsa2048-g1.pem",
    ssl_verify_cert=True
)