Aprendepython - Es-Sqlite - Aprende Python
Aprendepython - Es-Sqlite - Aprende Python
aprendepython.es/stdlib/data_access/sqlite
sqlite
El módulo sqlite3 permite trabajar con bases de datos de tipo SQLite [1].
¿Qué es SQLite?
SQLite es un sistema gestor de bases de datos relacional contenido en una pequeña
librería escrita en C (~275kB).
Índices multi-columna.
Subconsultas.
1/19
«Joins» de tipo «left», «right» y «full outer».
Funciones de agregación.
Funciones de ventana.
Cláusula UPSERT.
>>> con
<sqlite3.Connection at 0x106ea8210>
Advertencia
2/19
>>> cur = con.cursor()
>>> cur
<sqlite3.Cursor at 0x106a63960>
Creación de tablas
Para poder crear una tabla primero debemos manejar los tipos de datos SQLite
disponibles. Aunque hay alguno más, con los siguientes nos será suficiente para la
inmensa mayoría de diseños de bases de datos que podamos necesitar:
Prudencia
Durante toda esta sección vamos a trabajar con una tabla de ejemplo que represente las
distintas versiones de Python que han sido liberadas.
>>> cur.execute(sql)
<sqlite3.Cursor at 0x106a63960>
Consejo
Las cadenas multilínea son grandes aliadas a la hora de escribir sentencias SQL.
Truco
3/19
No es necesario añadir punto y coma ; al final de la sentencia SQL cuando usamos el
módulo sqlite3 salvo que se trate de scripts.
Si comprobamos ahora el contenido del fichero python.db podemos observar que nos
indica la versión de SQLite y la última escritura:
Añadiendo datos
Para tener contenido sobre el que trabajar, vamos primeramente a añadir ciertos datos a
la tabla. Como básicamente seguimos ejecutando sentencias SQL (en este caso de
inserción) podemos volver a hacer uso de la función execute():
>>> sql = 'INSERT INTO pyversions VALUES ("2.6", 2008, 10, "Barry Warsaw")'
>>> cur.execute(sql)
<sqlite3.Cursor at 0x106a63960>
Resulta que no obtenemos ningún registro. ¿Por qué ocurre esto? Se debe a que la
transacción está aún pendiente de confirmar. Para consolidarla tendremos que hacer uso
de la función commit():
>>> con.commit()
Ver también
Cada vez que usamos la función execute() comienza una nueva transacción a la base
de datos que debe confirmarse con commit() o bien deshacerse con rollback().
Nota
Autocommit
4/19
Cuando creamos la conexión a la base de datos podemos pasar como argumento
autocommit=True de tal forma que no sea necesario invocar explícitamente a commit():
Cada vez que ejecutamos operaciones de modificación sobre la base de datos se lanza
automáticamente el método commit() confirmando los cambios indicados.
Inserciones parametrizadas
Supongamos que no sabemos, a priori, los datos que vamos a insertar en la tabla puesto
que provienen del usuario o de otra fuente externa. En este caso cabría plantearse cuál
es la mejor opción para parametrizar la consulta.
Usando f-strings
Una primera aproximación podrían ser los f-strings a través de una simple interpolación
de variables. Veamos un ejemplo de ello:
>>> cur.execute(sql)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OperationalError: near "Langa": syntax error
Pero existe otra aproximación y es usar los «placeholders» que ofrece SQLite al
ejecutar sentencias. Estos «placeholders» se representan por el símbolo de
interrogación ? y se sustituyen por el valor correspondiente en una tupla (o iterable)
que pasamos como parámetro a posteriori.
5/19
Ahora sí que todo ha ido bien y no nos hemos tenido que preocupar del tipo de los
campos. Ya sólo por esto valdría la pena utilizar esta aproximación pero también ayuda
a evitar ataques por inyección SQL [4].
Prudencia
Cuando sólo haya un «placeholder» hay que recordar que las tuplas de un único
elemento necesitan una coma al final: cur.execute('INSERT INTO table (column)
VALUES (?)', (value,))
En estos casos quizás sea incluso más sencillo pasar una lista de un elemento: [value]
>>> sql = 'INSERT INTO pyversions VALUES (:branch, :year, :month, :manager)'
Truco
Nótese que no es necesario usar el mismo orden de los parámetros cuando utilizamos
esta aproximación nominal ya que el diccionario incluye las claves.
Inserciones en lote
Vamos a pensar en un escenario algo más real, en el que necesitamos insertar en la
tabla más de un registro. Obviamente la solución programática no puede ser ir de uno
en uno.
1branch,year,month,manager
22.6,2008,10,Barry Warsaw
32.7,2010,7,Benjamin Peterson
43.0,2008,12,Barry Warsaw
53.1,2009,6,Benjamin Peterson
63.2,2011,2,Georg Brandl
73.3,2012,9,Georg Brandl
83.4,2014,3,Larry Hastings
93.5,2015,9,Larry Hastings
103.6,2016,12,Ned Deily
113.7,2018,6,Ned Deily
123.8,2019,10,Łukasz Langa
133.9,2020,10,Łukasz Langa
143.10,2021,10,Pablo Galindo Salgado
153.11,2022,10,Pablo Galindo Salgado
163.12,2023,10,Thomas Wouters
6/19
Queremos procesar cada línea e insertarla en la tabla como un nuevo registro. Veamos
una primera aproximación:
Pero este módulo permite atacar el problema desde otro enfoque utilizando la función
executemany(). Esta función admite un iterable de iterables (con el mismo número de
campos que la tabla) desde donde recupera los datos:
>>> f = open('pyversions.csv')
>>> data = [line.strip().split(',') for line in f.readlines()[1:]]
>>> data
[['2.6', '2008', '10', 'Barry Warsaw'],
['2.7', '2010', '7', 'Benjamin Peterson'],
['3.0', '2008', '12', 'Barry Warsaw'],
['3.1', '2009', '6', 'Benjamin Peterson'],
['3.2', '2011', '2', 'Georg Brandl'],
['3.3', '2012', '9', 'Georg Brandl'],
['3.4', '2014', '3', 'Larry Hastings'],
['3.5', '2015', '9', 'Larry Hastings'],
['3.6', '2016', '12', 'Ned Deily'],
['3.7', '2018', '6', 'Ned Deily'],
['3.8', '2019', '10', 'Łukasz Langa'],
['3.9', '2020', '10', 'Łukasz Langa'],
['3.10', '2021', '10', 'Pablo Galindo Salgado'],
['3.11', '2022', '10', 'Pablo Galindo Salgado'],
['3.12', '2023', '10', 'Thomas Wouters']]
>>> con.commit()
7/19
>>> f = open('pyversions.csv')
>>> fields = f.readline().strip().split(',')
>>> data = [{f: v for f, v in zip(fields, line.strip().split(','))} for line in f]
>>> data
[{'branch': '2.6', 'year': '2008', 'month': '10', 'manager': 'Barry Warsaw'},
{'branch': '2.7', 'year': '2010', 'month': '7', 'manager': 'Benjamin Peterson'},
{'branch': '3.0', 'year': '2008', 'month': '12', 'manager': 'Barry Warsaw'},
{'branch': '3.1', 'year': '2009', 'month': '6', 'manager': 'Benjamin Peterson'},
{'branch': '3.2', 'year': '2011', 'month': '2', 'manager': 'Georg Brandl'},
{'branch': '3.3', 'year': '2012', 'month': '9', 'manager': 'Georg Brandl'},
{'branch': '3.4', 'year': '2014', 'month': '3', 'manager': 'Larry Hastings'},
{'branch': '3.5', 'year': '2015', 'month': '9', 'manager': 'Larry Hastings'},
{'branch': '3.6', 'year': '2016', 'month': '12', 'manager': 'Ned Deily'},
{'branch': '3.7', 'year': '2018', 'month': '6', 'manager': 'Ned Deily'},
{'branch': '3.8', 'year': '2019', 'month': '10', 'manager': 'Łukasz Langa'},
{'branch': '3.9', 'year': '2020', 'month': '10', 'manager': 'Łukasz Langa'},
{'branch': '3.10', 'year': '2021', 'month': '10', 'manager': 'Pablo Galindo
Salgado'},
{'branch': '3.11', 'year': '2022', 'month': '10', 'manager': 'Pablo Galindo
Salgado'},
{'branch': '3.12', 'year': '2023', 'month': '10', 'manager': 'Thomas Wouters'}]
>>> sql = 'INSERT INTO pyversions VALUES (:branch, :year, :month, :manager)'
>>> cur.executemany(sql, data)
<sqlite3.Cursor at 0x106e96030>
>>> con.commit()
Identificador de fila
En el comportamiento por defecto de una base de datos SQLite todas las tablas
disponen de una columna «oculta» denominada rowid o identificador de fila.
8/19
Esta columna se va rellenando de forma automática con valores enteros únicos y
puede utilizarse como clave primaria de los registros.
Cerrando la conexión
Al igual que ocurre con un fichero de texto, es necesario cerrar la conexión abierta para
que se liberen los recursos asociados y se debloquee la base de datos.
>>> con.close()
Atención
Si hay alguna transacción pendiente, esta no será guardada al cerrar la conexión con la
base de datos, si previamente no se consolidan los cambios.
Gestor de contexto
9/19
1>>> with con:
2... cur.execute('INSERT INTO pyversions VALUES ("3.13", 2024, 10, "Thomas
Wouters")')
3... cur.execute('INSERT INTO pyversions VALUES ("3.12", 2023, 10, "Thomas
Wouters")')
4...
5Traceback (most recent call last):
6 Cell In, line 3
7 cur.execute('INSERT INTO pyversions VALUES ("3.12", 2023, 10, "Thomas
Wouters")')
8IntegrityError: UNIQUE constraint failed: pyversions.branch
Línea 1:
Creamos el gestor de contexto.
Línea 2:
Insertamos una nueva fila en la tabla que no tiene ningún problema aparente.
Línea 3:
Insertamos una nueva fila que viola la restricción de clave única para la columna
«branch».
Es interesante conocer las distintas excepciones que pueden producirse al trabajar con
este módulo a la hora del control de errores y de plantear posibles escenarios de mejora.
Consultas
La manera más sencilla de hacer una consulta es utilizar un cursor. Existen dos
aproximaciones en el tratamiento de los resultados de la consulta:
10/19
>>> for row in cur.execute('SELECT * FROM pyversions'):
... print(row)
...
('2.6', 2008, 10, 'Barry Warsaw')
('2.7', 2010, 7, 'Benjamin Peterson')
('3.0', 2008, 12, 'Barry Warsaw')
('3.1', 2009, 6, 'Benjamin Peterson')
('3.2', 2011, 2, 'Georg Brandl')
('3.3', 2012, 9, 'Georg Brandl')
('3.4', 2014, 3, 'Larry Hastings')
('3.5', 2015, 9, 'Larry Hastings')
('3.6', 2016, 12, 'Ned Deily')
('3.7', 2018, 6, 'Ned Deily')
('3.8', 2019, 10, 'Łukasz Langa')
('3.9', 2020, 10, 'Łukasz Langa')
('3.10', 2021, 10, 'Pablo Galindo Salgado')
('3.11', 2022, 10, 'Pablo Galindo Salgado')
('3.12', 2023, 10, 'Thomas Wouters')
('3.13', 2024, 10, 'Thomas Wouters')
También tenemos la opción de utilizar las funciones fetchone() y fetchall() para obtener
una o todas las filas de la consulta:
>>> res.fetchone()
('2.6', 2008, 10, 'Barry Warsaw')
>>> res.fetchall()
[('2.7', 2010, 7, 'Benjamin Peterson'),
('3.0', 2008, 12, 'Barry Warsaw'),
('3.1', 2009, 6, 'Benjamin Peterson'),
('3.2', 2011, 2, 'Georg Brandl'),
('3.3', 2012, 9, 'Georg Brandl'),
('3.4', 2014, 3, 'Larry Hastings'),
('3.5', 2015, 9, 'Larry Hastings'),
('3.6', 2016, 12, 'Ned Deily'),
('3.7', 2018, 6, 'Ned Deily'),
('3.8', 2019, 10, 'Łukasz Langa'),
('3.9', 2020, 10, 'Łukasz Langa'),
('3.10', 2021, 10, 'Pablo Galindo Salgado'),
('3.11', 2022, 10, 'Pablo Galindo Salgado'),
('3.12', 2023, 10, 'Thomas Wouters'),
('3.13', 2024, 10, 'Thomas Wouters')]
Prudencia
Nótese que la llamada a fetchone() hace que quede «una fila menos» que recorrer. Es
un comportamiento totalmente análogo a la lectura de una línea en un fichero.
11/19
Este módulo también nos permite obtener los resultados de una consulta como objetos
de tipo Row lo que facilita acceder a los valores de cada registro tanto por el índice
como por el nombre de la columna.
Para «activar» este modo tendremos que fijar el valor de la factoría de filas en la
conexión:
Importante
Para que las consultas usen esta factoría hay que fijar el atributo row_factory antes de
crear el cursor correspondiente.
>>> row.keys()
['branch', 'released_at_year', 'released_at_month', 'release_manager']
>>> row['branch']
'2.6'
>>> row['released_at_year']
2008
>>> row['released_at_month']
10
>>> row['release_manager']
'Barry Warsaw'
Pero también es posible seguir accediendo a cada columna a través del índice:
>>> row[0]
'2.6'
>>> row[1]
2008
>>> row[2]
10
>>> row[3]
'Barry Warsaw'
Desempaquetando filas
Cuando disponemos de una fila como resultado de una consulta (ya sea en formato tupla
o en formato sqlite3.Row) podemos realizar un desempaquetado para separar sus
campos en variables únicas:
12/19
>>> sql = 'SELECT * FROM pyversions'
>>> result = cur.execute(sql)
>>> row = result.fetchone()
>>> row
<sqlite3.Row at 0x102e71ab0>
>>> branch
'2.6'
>>> released_at_year
2008
>>> released_at_month
10
>>> release_manager
'Barry Warsaw'
Número de filas
Hay ocasiones en las que lo que necesitamos obtener no es el dato en sí mismo, sino el
número de filas vinculadas a una determinada consulta. En este sentido hay varias
alternativas.
>>> len(rows)
15
13/19
Veamos una posible implementación:
Otras funcionalidades
Tablas en memoria
Existe la posibilidad de trabajar con tablas en memoria sin necesidad de tener un fichero
en disco.
>>> sql = 'CREATE TABLE temp (id INTEGER PRIMARY KEY, value TEXT)'
>>> cur.execute(sql)
<sqlite3.Cursor at 0x107884ea0>
Prudencia
14/19
Claves autoincrementales
Es muy habitual encontrar en la definición de una tabla un campo identificador
numérico entero que actúe como clave primaria y se le asignen valores
automáticamente.
Veamos un ejemplo de aplicación con una tabla en memoria que almacena ciudades y
sus geolocalizaciones:
Importante
Si la clave primaria de una tabla es una columna de tipo INTEGER ésta se convierte en un
alias para rowid.
Copias de seguridad
Es posible realizar copias de seguridad de manera programática [3]:
15/19
>>> def progress(status, remaining, total):
... print(f'Copied {total-remaining} of {total} pages...')
...
>>> dst.close()
>>> src.close()
Ver también
El parámetro pages del método backup() indica el número de páginas a copiar a la vez.
Si este valor es menor o igual que 0, la base de datos se copia en un único paso. El valor
por defecto es -1.
1. Funciona incluso si la base de datos está siendo accedida por otros clientes o
concurrentemente por la misma conexión.
Ver también
Hacer directamente una copia del fichero file.db (desde el propio sistema operativo)
también es una opción rápida para disponer de copias de seguridad.
Información de filas
16/19
Cuando ejecutamos una sentencia de modificación sobre la base de datos podemos
obtener el número de filas modificadas.
Este dato lo sacamos del atributo rowcount del cursor. Veamos un ejemplo:
>>> cur.rowcount
16 # filas modificadas
>>> cur.execute('INSERT INTO pyversions VALUES ("3.14", 2025, 10, "Guido Van
Rossum")')
<sqlite3.Cursor at 0x105593dc0>
>>> cur.lastrowid
17
Truco
Esto último también funciona si utilizamos una clave primaria entera personalizada e
insertamos un valor «manualmente» en dicha columna.
Ejecución de scripts
¿Qué pasaría si intentamos ejecutar varias sentencias SQL a la vez con las
herramientas que hemos visto hasta ahora?
17/19
Supongamos una tabla de ejemplo que mantiene estadísticas de los mejores jugadores
históricos de la NBA. Queremos crear la tabla e insertar 3 registros en una misma
ejecución:
>>> cur.execute(sql)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ProgrammingError: You can only execute one statement at a time.
Obtenemos un error indicando que sólo se puede ejecutar una sentencia cada vez.
>>> cur.executescript(sql)
<sqlite3.Cursor at 0x1028ce840>
Aparentemente ahora sí que ha ido todo bien. Podemos comprobar que la tabla está
creada y los registros insertados:
>>> res.fetchall()
[('LeBron James', 40474),
('Kareem Abdul-Jabbar', 38387),
('Karl Malone', 36928)]
Ejercicios
1. Escriba una clase ToDo y una clase Task que permita implementar una aplicación
de gestión de tareas.
Plantilla: todo.py
Comprobación: pytest -xq test_todo.py
18/19
2. Escriba una clase Twitter junto a dos clases User y Tweet que permita
implementar una aplicación de tipo «Twitter».
Plantilla: twitter.py
Comprobación: pytest -xq test_twitter.py
[1]
Foto original de portada por Jandira Sonnendeck en Unsplash.
[2]
Herramienta cliente de sqlite para terminal.
[3]
Ejemplo tomado de la documentación oficial de Python.
[4]
Inyección SQL es un método de infiltración de código intruso que se vale de una
vulnerabilidad informática presente en una aplicación en el nivel de validación de las
entradas para realizar operaciones sobre una base de datos.
[5]
En tecnologías de base de datos, un «rollback» o reversión es una operación que
devuelve a la base de datos a algún estado previo.
19/19