Consultas SELECT en Python

Una vez establecida la conexión con MySQL, la tarea más común es recuperar datos mediante consultas SELECT. El conector mysql-connector-python ofrece varias formas de ejecutar consultas y obtener resultados, desde tuplas simples hasta diccionarios que facilitan el acceso a los datos por nombre de columna. En este artículo aprenderás a ejecutar consultas parametrizadas de forma segura y a procesar los resultados de manera eficiente.

Requisitos previos

Necesitas tener el paquete mysql-connector-python instalado y una conexión configurada. Crea la tabla de prueba con el siguiente SQL:

CREATE DATABASE IF NOT EXISTS tienda;
USE tienda;
 
CREATE TABLE productos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    categoria VARCHAR(50) NOT NULL,
    precio DECIMAL(10, 2) NOT NULL,
    stock INT DEFAULT 0,
    activo TINYINT(1) DEFAULT 1,
    fecha_creacion DATETIME DEFAULT CURRENT_TIMESTAMP
);
 
INSERT INTO productos (nombre, categoria, precio, stock) VALUES
('Laptop HP Pavilion', 'Computadoras', 12999.99, 25),
('Mouse Logitech MX Master', 'Periféricos', 1599.00, 150),
('Teclado Mecánico Corsair K70', 'Periféricos', 2299.50, 80),
('Monitor Samsung 27"', 'Monitores', 6499.00, 40),
('Auriculares Sony WH-1000XM5', 'Audio', 5999.99, 60),
('Webcam Logitech C920', 'Periféricos', 1299.00, 200),
('SSD Samsung 1TB', 'Almacenamiento', 1899.00, 120),
('RAM Corsair 16GB DDR5', 'Componentes', 1499.00, 90);

Código completo

Este ejemplo ejecuta una consulta parametrizada y muestra los resultados:

import mysql.connector
 
def consultar_productos():
    conexion = mysql.connector.connect(
        host='localhost',
        user='root',
        password='tu_contraseña',
        database='tienda'
    )
 
    cursor = conexion.cursor(dictionary=True)
    categoria = 'Periféricos'
 
    cursor.execute(
        'SELECT id, nombre, precio, stock FROM productos '
        'WHERE categoria = %s ORDER BY precio DESC',
        (categoria,)
    )
 
    productos = cursor.fetchall()
 
    print(f'Productos en categoría "{categoria}":')
    for p in productos:
        print(f"  [{p['id']}] {p['nombre']} - ${p['precio']} ({p['stock']} en stock)")
 
    print(f'\nColumnas: {cursor.column_names}')
 
    cursor.close()
    conexion.close()
 
consultar_productos()

Salida esperada:

Productos en categoría "Periféricos":
  [3] Teclado Mecánico Corsair K70 - $2299.50 (80 en stock)
  [2] Mouse Logitech MX Master - $1599.00 (150 en stock)
  [6] Webcam Logitech C920 - $1299.00 (200 en stock)

Columnas: ('id', 'nombre', 'precio', 'stock')

Explicación paso a paso

El objeto cursor es el intermediario entre tu código Python y el servidor MySQL. Se obtiene llamando a conexion.cursor() y es responsable de ejecutar consultas y recuperar resultados. Al pasar dictionary=True, cada fila se devuelve como un diccionario donde las claves son los nombres de las columnas, lo que hace el código más legible.

Los parámetros se pasan usando el placeholder %s, independientemente del tipo de dato. El conector se encarga de convertir y escapar cada valor según su tipo Python. Nunca uses f-strings o concatenación de cadenas para incluir valores en la consulta, ya que esto expone tu aplicación a inyecciones SQL.

Es importante notar que el segundo argumento de execute() debe ser una tupla. Cuando pasas un solo parámetro, debes incluir la coma al final (valor,) para que Python lo reconozca como tupla y no como una expresión entre paréntesis.

Métodos de fetch

El cursor ofrece tres métodos para recuperar resultados, y cada uno es apropiado para diferentes situaciones:

cursor = conexion.cursor(dictionary=True)
 
# fetchall() - Obtener todas las filas de una vez
cursor.execute('SELECT * FROM productos WHERE activo = 1')
todas = cursor.fetchall()
print(f'Total: {len(todas)} productos')
 
# fetchone() - Obtener una sola fila
cursor.execute('SELECT * FROM productos WHERE id = %s', (1,))
producto = cursor.fetchone()
if producto:
    print(f"Producto: {producto['nombre']}")
else:
    print('No encontrado')
 
# fetchmany(size) - Obtener N filas a la vez
cursor.execute('SELECT * FROM productos ORDER BY id')
lote = cursor.fetchmany(3)
while lote:
    for p in lote:
        print(f"  {p['nombre']}")
    lote = cursor.fetchmany(3)

El método fetchmany() es especialmente útil cuando trabajas con conjuntos de datos grandes, ya que evita cargar todas las filas en memoria a la vez. Puedes procesar los datos en lotes del tamaño que especifiques.

Iterar sobre el cursor

También puedes iterar directamente sobre el cursor, lo que es equivalente a llamar fetchone() repetidamente:

cursor = conexion.cursor(dictionary=True)
cursor.execute('SELECT nombre, precio FROM productos WHERE activo = 1')
 
for fila in cursor:
    print(f"{fila['nombre']}: ${fila['precio']}")

Cursor como tupla vs diccionario

Sin el parámetro dictionary=True, cada fila se devuelve como una tupla donde accedes a los valores por índice:

# Cursor normal (tuplas)
cursor = conexion.cursor()
cursor.execute('SELECT nombre, precio FROM productos WHERE id = %s', (1,))
fila = cursor.fetchone()
print(f'Nombre: {fila[0]}, Precio: {fila[1]}')
 
# Cursor con named_tuple
cursor = conexion.cursor(named_tuple=True)
cursor.execute('SELECT nombre, precio FROM productos WHERE id = %s', (1,))
fila = cursor.fetchone()
print(f'Nombre: {fila.nombre}, Precio: {fila.precio}')

El cursor named_tuple ofrece un punto medio entre la tupla simple y el diccionario, permitiendo acceder a los valores por nombre con notación de punto.

Caso práctico

Veamos cómo construir una función de búsqueda de productos con paginación y filtros:

import mysql.connector
 
def buscar_productos(busqueda=None, categoria=None, precio_min=None,
                     precio_max=None, pagina=1, por_pagina=10):
    conexion = mysql.connector.connect(
        host='localhost',
        user='root',
        password='tu_contraseña',
        database='tienda'
    )
 
    cursor = conexion.cursor(dictionary=True)
 
    sql = 'SELECT id, nombre, categoria, precio, stock FROM productos WHERE activo = 1'
    params = []
 
    if busqueda:
        sql += ' AND nombre LIKE %s'
        params.append(f'%{busqueda}%')
 
    if categoria:
        sql += ' AND categoria = %s'
        params.append(categoria)
 
    if precio_min is not None:
        sql += ' AND precio >= %s'
        params.append(precio_min)
 
    if precio_max is not None:
        sql += ' AND precio <= %s'
        params.append(precio_max)
 
    # Contar total
    sql_count = sql.replace(
        'SELECT id, nombre, categoria, precio, stock',
        'SELECT COUNT(*) AS total'
    )
    cursor.execute(sql_count, tuple(params))
    total = cursor.fetchone()['total']
 
    # Agregar paginación
    sql += ' ORDER BY nombre LIMIT %s OFFSET %s'
    params.extend([por_pagina, (pagina - 1) * por_pagina])
 
    cursor.execute(sql, tuple(params))
    productos = cursor.fetchall()
 
    cursor.close()
    conexion.close()
 
    return {
        'datos': productos,
        'total': total,
        'pagina': pagina,
        'total_paginas': -(-total // por_pagina)  # División entera redondeando arriba
    }
 
# Uso
resultado = buscar_productos(categoria='Periféricos', precio_min=1000, precio_max=3000)
print(f"Mostrando {len(resultado['datos'])} de {resultado['total']} resultados")
print(f"Página {resultado['pagina']} de {resultado['total_paginas']}")
 
for p in resultado['datos']:
    print(f"  {p['nombre']} ({p['categoria']}) - ${p['precio']}")

Salida esperada:

Mostrando 3 de 3 resultados
Página 1 de 1
  Mouse Logitech MX Master (Periféricos) - $1599.00
  Teclado Mecánico Corsair K70 (Periféricos) - $2299.50
  Webcam Logitech C920 (Periféricos) - $1299.00

Manejo de errores

Las consultas pueden fallar por errores de sintaxis, tablas inexistentes o problemas de conexión:

from mysql.connector import Error, errorcode
 
def consulta_segura(conexion, sql, params=None):
    try:
        cursor = conexion.cursor(dictionary=True)
        cursor.execute(sql, params)
        resultados = cursor.fetchall()
        cursor.close()
        return resultados
 
    except Error as error:
        if error.errno == errorcode.ER_NO_SUCH_TABLE:
            print('La tabla especificada no existe')
        elif error.errno == errorcode.ER_PARSE_ERROR:
            print('Error de sintaxis en la consulta SQL')
        elif error.errno == errorcode.ER_BAD_FIELD_ERROR:
            print('Una de las columnas especificadas no existe')
        else:
            print(f'Error en la consulta: {error}')
        return []
 
# Manejo de resultados vacíos
productos = consulta_segura(
    conexion,
    'SELECT * FROM productos WHERE categoria = %s',
    ('Categoría Inexistente',)
)
 
if not productos:
    print('No se encontraron productos con los criterios especificados')

Ahora que dominas las consultas SELECT en Python, en el siguiente artículo aprenderás a insertar nuevos registros en MySQL.

Escrito por Eduardo Lázaro