Este documento pretende ser una fuente rapida de informacion sobre herramientas y cosas relacionadas o utiles al momento de trabajar con una base de datos PostgreSQL.
- Listado de herramientas utiles.
- Administracion de postgreSQL con PgAdmin4.
- Introduccion estandar SQL.
- Implementacion PostgreSQL de SQL.
- Extraccion de datos con pandas.
- Extraccion de datos con SqlAlchemy.
- Insercion de registros.
Cuando trabajamos con una base de datos normalmente nos encontraremos realizando una de las siguientes tareas/actividades:
-
Exploracion de datos.
Esta tarea consiste en identificar el contenido de las tablas, y sus tipos de datos. Tambien analisis sencillos como cuentas y sumas agrupadas.
-
Lectura de datos para procesamiento.
Esta tarea consiste en leer grandes cantidades de datos (mas de un registro) y realizar alguna operacion con estos datos, por ejemplo analisis matematicos/estadicos, generacion de reportes entre otros.
-
Escritura de datos.
Esta tarea consiste bien sea un crear nuevos registros dentro de una tabla, o actualizar registros previamente existentes.
-
Administracion.
Esta tarea consiste relizar actividades no relacionadas directamente con los datos, sino con las estructuras y modelos donde estos datos son almacenados; por ejemplo; la creacion, eliminacion e identificacion de tablas y campos; identificacion y creacion de indices sobre una columna; entre otras actividades.
Para la realizacion de estas actividades, a dia de hoy, encuentro que las siguientes herramientas podran ser utiles:
-
PgAdmin https://www.pgadmin.org/
Este es el administrador oficial de PostgreSQL. Consiste en una aplicacion que podemos instalar facilmente en todos los OS y conectarnos a ella mediante un navegador web.
Algunas de sus caracteristicas de interes son:
- Muy intuitivo.
- Te permite realizar casi todas las operacion con cero (0) conocimiento de SQL.
- Permite hacer y restaurar backups.
- Permite conectarnos a diferentes servidores.
- Nos muestra, y permite editar, objetos que en muchos casos ni sabemos que existen, lo cual nos da una gran visibilidad de todo el sistema.
- Permite monitorear el servidor, cosas de nerds, como numero de sesiones, transacciones por segundo y otro monton de cosas.
-
Pandas https://pandas.pydata.org/
Pandas es una herramienta para manipulacion y analisis de datos, casi un estandar de facto cuando se trabaja con datos desde el lenguaje python.
Algunas de sus caraceristicas de interes son:
-
Conexion mediante una URI.
-
Permite leer datos desde una base de datos SQL sin escribir SQL.
-
Permite realizar complejas consultas a una base de datos SQl y obtener los resultados en un DataFrame.
-
Muy util para leer varias tablas (completas) y posteriormente hacer joins en memoria cuando es muy pesado para la base de datos.
-
Permite guardar un DataFrame en una base de datos SQL.
-
-
SqlAlchemy https://www.sqlalchemy.org/
SqlAlchemy es un ORM (object relational mapper), es decir convierte objetos sql(registros) en objetos python, muy utilizado por su ligereza, que en este caso es de interes por las abstracciones que implementa, las cuales nos permitiran conectarnos facilmente a la base de datos usando simplemente una URI.
Caracteristicas de interes:
- Conexion mediante una URI.
- Puede conectarse multiples bases de datos.
- Implementa cursores para lectura de datos, esto permite procesar grandes cantidades de datos sin cargarlos todos en memoria.
- Permite realizar complejas consultas de escritura sobre una o multiples tablas.
Teniendo en cuentas estas herramientas, y bajo la mirada de estas actividades/tareas, se presenta a continuacion una pequeña tabla, donde se relacionan cada herramienta y la facilidad que esta herramienta presta al momento de realizar cada tarea.
Herramienta | Exploracion de datos | Lectura de datos para procesamiento | Escritura de datos | Administracion |
---|---|---|---|---|
PgAdmin4 | 2 | 0 | 2 | 3 |
Pandas | 3 | 2 | 1 | 0 |
SQLAlchemy | 1 | 3 | 3 | 1 |
* Un valor mas alto indica mas facilidad, y un valor de 0 indica imposibilidad de realizar la tarea.
https://es.wikipedia.org/wiki/SQL
El lenguaje estandar SQL, es un lenguaje de programacion declarativo, inicialmente desarrollado por matematicos en los años 70 y en evolucion continua hasta el dia de hoy, cuando es ampliamente utilizado por diversos motores de bases de datos, entre los mas comunes: MySQL, PostgreSQL, SQL Server, Oracle y SQlite ademas de otras herramientas de ETL y procesamiento de datos.
Aunque es un estandar, cada implementacion es diferente, y pueden haber grandes cambios de un motor a otro.
Este documento solo pretende registrar la forma de leer, escribir, actualizar y borrar datos de una base de datos, por lo que se descartan todas las sentencias relacionadas a la administracion de objetos y estructuras del motor, tampoco se toca la configuracion del motor mediante la interfaz SQL.
para propositos de ejemplos se tendran las siguientes tablas:
-
Alumnos
id nombre telefono edad curso_id 1 Simon pedro 111111111 30 12 2 Juan pablo II 264264264 25 11 3 Benedicto 5656656556 30 11 4 Francisco 45656453 15 11 5 José chepe 87987210 18 12 -
Cursos
id nombre horas_semanales 11 latin 8 12 hebreoo 16
Una sentencia hace referencia a una instruccion o conjunto de ordenes que se le da al servidor de la base de datos para ser procesadas.
-
INSERT, Esta es la sentencia encargada de ingresar nuevos registros en la tabla de destino, su sintaxis mas tipica es de la siguiente forma:
-- Registrar a Juana e inscribirla en el curso de latin INSERT INTO "Alumnos"("id", "nombre", "telefono", "curso_id") VALUES (6, "Juana", "09898909", 11); -- Registrar varias personas al mismo tiempo INSERT INTO "Alumnos"("id", "nombre", "telefono", "curso_id") VALUES (6, "Juana", "09898909", 11), (7, "Willian", "0258963", 12);
-
UPDATE, Esta es la sentencia encargada de ingresar actualizar o cambiar registros previamente existentes, algunos algunos ejemplos:
-- Cambiar la duracion de todos curso a 10 horas UPDATE "Cursos" SET "horas_semanales" = 10; -- Aumentar la duracion del curso en dos horas UPDATE "Cursos" SET "horas_semanales" = "horas_semanales" + 2; -- Cambiar a todos los Alumnos que se llamen "Juana" de curso UPDATE "Alumnos" SET "curso_id" = 11 WHERE "nombre" = 'Juana'; -- Varias columnas al mismo tiempo UPDATE "Alumnos" SET "curso_id" = 11, "edad" = "edad" + 1 WHERE "nombre" ~ 'Juana'; -- el operador `~` permite usar regex POSIX
-
DELETE, esta sentencia nos permite borrar registros.
-- Eliminar a benedicto DELETE FROM "Alumnos" WHERE "nombre" ~* 'benedicto' LIMIT 1; -- el operador `~*` tambien permite usar regex POSIX, pero -- a diferencia de `~` es "case insensitive" -- Eliminar a todos los alumnos de latin, por que nadie les entiende DELETE FROM "Alumnos" INNER JOIN "Cursos" ON "Cursos"."id" = "Alumnos"."curso_id" WHERE "Cursos"."nombre" = 'latin';
-
SELECT, Esta es la sentencia mas usada, ya que es la encargada de obtener datos desde la tabla y retornarlos, tambien permite obtener resultados de calculos asi como hacer joins entre tablas.
La estructura "general" (realmente puede complicarse mas) de esta sentencia es la siguiente:
SELECT [{ALL|DISTINCT}] <nombre_campo>[, <nombre_campo>...] FROM {<nombre_tabla>} [WHERE <condición> [{AND|OR} <condición>...]] [GROUP BY <nombre_campo>[, <nombre_campo>...]] [HAVING <condición> [{AND|OR} <condición>...]] [ORDER BY {<nombre_campo>|<indice_campo>} [{ASC|DESC}][, {<nombre_campo>|<indice_campo>} [{ASC|DESC}]]]; [LIMIT limite_registros]
Algunos ejemplos mas concretos pueden ser:
-- Seleccionar toda la informacion de la tabla de alumnos SELECT * FROM "Alumnos"; -- Traer el nombre curso con mayor duracion SELECT "nombre" FROM "Cursos" ORDER BY "horas_semanales" DESC LIMIT 1 -- Traer la duracion del curso mas largo SELECT MAX("horas_semanales") AS "duracion maxima" FROM "Cursos" -- Traer el nombre de los alumnos, y las horas a la semana que estudian SELECT "Alumnos"."nombre", "Cursos"."horas_semanales" FROM "Alumnos" INNER JOIN "Cursos" ON "Cursos"."id" = "Alumnos"."curso_id" -- Distintos nombre de alumnos SELECT DISTINCT "nombre" FROM "Alumnos" -- Numero de alumnos por curso SELECT "Cursos."."nombre", COUNT("Alumnos"."id") FROM "Alumnos" INNER JOIN "Cursos" ON "Cursos"."id" = "Alumnos"."curso_id" GROUP BY "Cursos"."nombre" -- Los dos cursos con mas "alumno-hora" -- notese que "ORDER BY 2 DESC" quiere decir que ordene por la segunda(2) -- columna de resultados, es decir, "alumno-hora" SELECT "Cursos."."nombre", COUNT("Alumnos"."id")*"Cursos"."horas_semanales" AS "Alumno-hora" FROM "Alumnos" INNER JOIN "Cursos" ON "Cursos"."id" = "Alumnos"."curso_id" GROUP BY "Cursos"."nombre" ORDER BY 2 DESC LIMIT 2
En este punto ya debio notar que se usa comilla sencilla para indicarle al
motor que se hace referencia a una cadena de texto, y las comillas dobles
quedan reservadas para hacer refencia a objetos del motor; por ejemplo
nombres de las bases de datos, de los esquemas o las tablas; aunque no
siempre es necesario usar estas comillas dobles.
Estas comillas son obligatorias si al momento de crear el objeto se creo
con ellas.
Hay que tener en cuenta que la siguiente sintaxis es valida en implementaciones como SQlite y MySQL, pero son INVALIDAS/ILEGALES en otras como PostgreSQL y SQL Server, aunque son muy convenientes.
Por ejemplo en MySql podemos hacer lo siguiente:
-- si encontrar al alumno de mayor edad por cada curso.
--
-- Notese que en mysql las comillas (") no se usan de la misma forma.
SELECT nombre
FROM Alumnos
GROUP BY curso_id
ORDER BY edad desc
La anterior sentencia es INVALIDA en PostgreSQL ya que no se puede hacer un GROUP BY y poner en la lista de campos a seleccionar una columna que, o no esta en el GROUP BY o no esta dentro de una funcion de agregacion [MIN, MAX, COUNT...]
La forma adecuada en PostgreSQL seria:
SELECT DISTINCT ON("curso_id") nombre
FROM "Alumnos"
ORDER BY "curso_id", edad DESC
El operador DISTINCT ON(column1[, column2, ... columnN])
es muy similar
al operador GROUP BY
y puede entenderse de la siguiente forma:
Para cada grupo de columnas "column1[, column2, ... columnN]"
seleccione
el primer registro y retorne las columnas de las sentencia SELECT
,
en nuestro ejemplo, esto es la columna nombre
.
Notese que "el primer registro" podria ser cualquiera, y podria no ser
siempre lo mismo, por lo que PostgreSQL recomienda que las columnas
"column1[, column2, ... columnN]"
esten todas en el comienzo del
ORDER BY
Otra caracteristica que esta presente en postgreSQL y no esta disponible en otros motores son las funciones de ventanas, estas funciones nos permiten realizar una operacion, tipicamente una agregacion, sobre un conjunto de datos que es diferente a los datos que estan siendo retornados.
El siguente ejemplo muestra como calcular la edad de cada alumno comparada con la edad promedio del curso.
SELECT "nombre", "edad", "curso_id", avg("edad") OVER (PARTITION BY "curso_id") FROM "Alumnos";
-- para cada alumno se calcula el "avg" de edad sobre
-- las celdas que esten en la misma "partition"
-- la cual corresponde a "curso_id"
nombre | edad | curso_id | avg |
---|---|---|---|
Simon pedro | 30 | 12 | 24 |
José chepe | 18 | 12 | 24 |
Juan pablo II | 25 | 11 | 23,33 |
Benedicto | 30 | 11 | 23,33 |
Francisco | 15 | 11 | 23,33 |
Estas particiones tambien pueden tener un ORDER BY
por ejemplo,
OVER (PARTITION BY "curso_id" ORDER BY "edad" DESC
pero en este caso
la particion tiene un significado un poco diferente, ya que para cada fila se
realizara la agregacion con los datos "anteriores" o que estan primero sobre
la particion que el dato actual, por ejemplo:
SELECT
"nombre",
"edad",
"curso_id",
avg("edad") OVER (PARTITION BY "curso_id" ORDER BY "edad" DESC) AS "avg_desc",
avg("edad") OVER (PARTITION BY "curso_id" ORDER BY "edad" ASC) AS "avg_asc"
FROM "Alumnos";
nombre | edad | curso_id | avg_desc | avg_asc |
---|---|---|---|---|
Simon pedro | 30 | 12 | 30 | 24 |
José chepe | 18 | 12 | 24 | 18 |
Juan pablo II | 25 | 11 | 27.5 | 20 |
Benedicto | 30 | 11 | 30 | 23.33 |
Francisco | 15 | 11 | 23.33 | 15 |
Nada que decir... Simple
python -m pip install "pandas==1.1.1" "psycopg2-binary==2.8.5" "SQLAlchemy==1.3.19"
import pandas as pd
URI="postgresql+psycopg2://CiscoUser:[email protected]:5432/test"
df_alumnos = pd.read_sql("Alumnos", URI)
SQL = """
SELECT
"Alumnos"."nombre" AS "NombreAlumno",
"Cursos"."nombre" AS "NombreCurso"
FROM "Alumnos"
INNER JOIN "Cusos"
ON "Alumnos"."curso_id" = "Cursos"."id"
"""
df_alumnos_con_nombre_Y_curso = pd.read_sql(SQL, URI)
Supongamos el siguiente codigo base para todos los ejemplos.
from sqlalchemy import create_engine
URI="postgresql+psycopg2://CiscoUser:[email protected]:5432/test"
DB_ENGINE = create_engine(URI)
Si queremos convertir la lista de alumnos en un documento JSON o un DataFrame
import json
SQL = 'SELECT * FROM "Alumnos"'
with DB_ENGINE.connect() as conn:
alumnos_cursor = conn.execute(SQL)
alumnos = [dict(alumno) for alumno in alumnos_cursor]
alumnos_json = json.dumps(alumnos)
df_alumnos = pd.DataFrame(alumnos)
Notese que en el codigo anterior, la sentencia SQL podria ser de cualquier tipo,
por ejemplo un UPDATE
, la unica diferencia es la respuesta de
conn.execute
no seria un iterable con los resultados.
SQL = 'SELECT "id","nombre" FROM "Alumnos"'
with DB_ENGINE.connect() as conn:
alumnos_cursor = conn.execute(SQL)
for alumno in alumnos_cursor:
# la variable alumno es una instancia de <class 'sqlalchemy.engine.result.RowProxy'>
# y puede ser accedida como tupla o diccionario, se recomienda
# cambiar el tipo antes de enviarla a otra funcion
# imprime el id, ya que se accede como tupla
print(alumno[0])
# imprime el nombre
print(alumno[1])
# imprime el id
print(alumno["id"])
# imprime el nombre
print(alumno["nombre"])
Si la tarea que se va a realizar con cada alumno consume demasiada memoria, o base de datos es muy grande y cada registro puede ser procesado de forma independiente, lo ideal es no traerlos todos al mismo tiempo, en lugar de ello, lo aconsejable es traer a cada alumno y realizar la operacion, por ejemplo:
import time
SQL = 'SELECT * FROM "Alumnos"'
with DB_ENGINE.connect() as conn:
# alumnos_cursor es solo una variable, no toda la base
# de datos.
alumnos_cursor = conn.execute(SQL)
# trae a cada alumno de a 1 en 1, y los golpea durante 60 seg
for alumno in alumnos_cursor:
t0 = time.time()
while time.time() - t0 < 60:
golpear(dict(alumno))
Para escribir datos lo podemos hacer de la siguiente forma
alumnos_list = [
{"name": "Daniel", "age": 11},
{"name": "Carolina", "age": 13}
]
SQL = """INSERT INTO Alumnos("Nombre", "edad)
VALUES(%(name)s, %(age)s)"""
with DB_ENGINE.connect() as conn:
print("Registrando alumnos")
conn.execute(SQL, *alumnos_list)
print(f"{len(alumnos_list)} alumnos fueron registrados con exito")
## Lo siguiente es equivalente
alumnos_list = [
("Daniel", 11),
("Carolina", 13)
]
SQL = """INSERT INTO Alumnos("Nombre", "edad)
VALUES(%s, %s)"""
with DB_ENGINE.connect() as conn:
print("Registrando alumnos")
conn.execute(SQL, *alumnos_list)
print(f"{len(alumnos_list)} alumnos fueron registrados con exito")