SQL Relational Databases
Managing relational, OLTP grocery data—using Amazon RDS, Azure SQL Database, and GCP Cloud SQL.
Getting Started
If you are interested in reproducing this work, here are the versions of Python and Python packages used.
import sys
print(sys.version)
3.9.6 (v3.9.6:db3ff76da1, Jun 28 2021, 11:49:53)
[Clang 6.0 (clang-600.0.57)]
!pip install pandas==1.3.4
!pip install psycopg2-binary==2.9.3
import pandas
import psycopg2
Importing Data
W3Schools provides a sample dataset in its SQL tutorial, which will be used in this work.
categories_df = pandas.read_csv(filepath_or_buffer = "../../dataset/w3schools/Categories.csv")
customers_df = pandas.read_csv(filepath_or_buffer = "../../dataset/w3schools/Customers.csv")
employees_df = pandas.read_csv(filepath_or_buffer = "../../dataset/w3schools/Employees.csv")
order_details_df = pandas.read_csv(filepath_or_buffer = "../../dataset/w3schools/OrderDetails.csv")
orders_df = pandas.read_csv(filepath_or_buffer = "../../dataset/w3schools/Orders.csv")
products_df = pandas.read_csv(filepath_or_buffer = "../../dataset/w3schools/Products.csv")
shippers_df = pandas.read_csv(filepath_or_buffer = "../../dataset/w3schools/Shippers.csv")
suppliers_df = pandas.read_csv(filepath_or_buffer = "../../dataset/w3schools/Suppliers.csv")
Exploring Data
categories_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8 entries, 0 to 7
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 CategoryID 8 non-null int64
1 CategoryName 8 non-null object
2 Description 8 non-null object
dtypes: int64(1), object(2)
memory usage: 320.0+ bytes
categories_df
CategoryID CategoryName Description
0 1 Beverages Soft drinks, coffees, teas, beers, and ales
1 2 Condiments Sweet and savory sauces, relishes, spreads, an...
2 3 Confections Desserts, candies, and sweet breads
3 4 Dairy Products Cheeses
4 5 Grains/Cereals Breads, crackers, pasta, and cereal
5 6 Meat/Poultry Prepared meats
6 7 Produce Dried fruit and bean curd
7 8 Seafood Seaweed and fish
customers_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 91 entries, 0 to 90
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 CustomerID 91 non-null int64
1 CustomerName 91 non-null object
2 ContactName 91 non-null object
3 Address 91 non-null object
4 City 91 non-null object
5 PostalCode 90 non-null object
6 Country 91 non-null object
dtypes: int64(1), object(6)
memory usage: 5.1+ KB
customers_df.head(n = 10)
CustomerID CustomerName ContactName Address City PostalCode Country
0 1 Alfreds Futterkiste Maria Anders Obere Str. 57 Berlin 12209 Germany
1 2 Ana Trujillo Emparedados y helados Ana Trujillo Avda. de la Constitución 2222 México D.F. 5021 Mexico
2 3 Antonio Moreno Taquería Antonio Moreno Mataderos 2312 México D.F. 5023 Mexico
3 4 Around the Horn Thomas Hardy 120 Hanover Sq. London WA1 1DP UK
4 5 Berglunds snabbköp Christina Berglund Berguvsvägen 8 Luleå S-958 22 Sweden
5 6 Blauer See Delikatessen Hanna Moos Forsterstr. 57 Mannheim 68306 Germany
6 7 Blondel père et fils Frédérique Citeaux 24, place Kléber Strasbourg 67000 France
7 8 Bólido Comidas preparadas Martín Sommer C/ Araquil, 67 Madrid 28023 Spain
8 9 Bon app' Laurence Lebihans 12, rue des Bouchers Marseille 13008 France
9 10 Bottom-Dollar Marketse Elizabeth Lincoln 23 Tsawassen Blvd. Tsawassen T2F 8M4 Canada
employees_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10 entries, 0 to 9
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 EmployeeID 10 non-null int64
1 LastName 10 non-null object
2 FirstName 10 non-null object
3 BirthDate 10 non-null object
4 Photo 10 non-null object
5 Notes 10 non-null object
dtypes: int64(1), object(5)
memory usage: 608.0+ bytes
employees_df
EmployeeID LastName FirstName BirthDate Photo Notes
0 1 Davolio Nancy 12/8/68 EmpID1.pic Education includes a BA in psychology from Col...
1 2 Fuller Andrew 2/19/52 EmpID2.pic Andrew received his BTS commercial and a Ph.D....
2 3 Leverling Janet 8/30/63 EmpID3.pic Janet has a BS degree in chemistry from Boston...
3 4 Peacock Margaret 9/19/58 EmpID4.pic Margaret holds a BA in English literature from...
4 5 Buchanan Steven 3/4/55 EmpID5.pic Steven Buchanan graduated from St. Andrews Uni...
5 6 Suyama Michael 7/2/63 EmpID6.pic Michael is a graduate of Sussex University (MA...
6 7 King Robert 5/29/60 EmpID7.pic Robert King served in the Peace Corps and trav...
7 8 Callahan Laura 1/9/58 EmpID8.pic Laura received a BA in psychology from the Uni...
8 9 Dodsworth Anne 7/2/69 EmpID9.pic Anne has a BA degree in English from St. Lawre...
9 10 West Adam 9/19/28 EmpID10.pic An old chum.
order_details_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 518 entries, 0 to 517
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 OrderDetailID 518 non-null int64
1 OrderID 518 non-null int64
2 ProductID 518 non-null int64
3 Quantity 518 non-null int64
dtypes: int64(4)
memory usage: 16.3 KB
order_details_df.head(n = 10)
OrderDetailID OrderID ProductID Quantity
0 1 10248 11 12
1 2 10248 42 10
2 3 10248 72 5
3 4 10249 14 9
4 5 10249 51 40
5 6 10250 41 10
6 7 10250 51 35
7 8 10250 65 15
8 9 10251 22 6
9 10 10251 57 15
orders_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 196 entries, 0 to 195
Data columns (total 5 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 OrderID 196 non-null int64
1 CustomerID 196 non-null int64
2 EmployeeID 196 non-null int64
3 OrderDate 196 non-null object
4 ShipperID 196 non-null int64
dtypes: int64(4), object(1)
memory usage: 7.8+ KB
orders_df.head(n = 10)
OrderID CustomerID EmployeeID OrderDate ShipperID
0 10248 90 5 7/4/96 3
1 10249 81 6 7/5/96 1
2 10250 34 4 7/8/96 2
3 10251 84 3 7/8/96 1
4 10252 76 4 7/9/96 2
5 10253 34 3 7/10/96 2
6 10254 14 5 7/11/96 2
7 10255 68 9 7/12/96 3
8 10256 88 3 7/15/96 2
9 10257 35 4 7/16/96 3
products_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 77 entries, 0 to 76
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 ProductID 77 non-null int64
1 ProductName 77 non-null object
2 SupplierID 77 non-null int64
3 CategoryID 77 non-null int64
4 Unit 77 non-null object
5 Price 77 non-null float64
dtypes: float64(1), int64(3), object(2)
memory usage: 3.7+ KB
products_df.head(n = 10)
ProductID ProductName SupplierID CategoryID Unit Price
0 1 Chais 1 1 10 boxes x 20 bags 18.00
1 2 Chang 1 1 24 - 12 oz bottles 19.00
2 3 Aniseed Syrup 1 2 12 - 550 ml bottles 10.00
3 4 Chef Anton's Cajun Seasoning 2 2 48 - 6 oz jars 22.00
4 5 Chef Anton's Gumbo Mix 2 2 36 boxes 21.35
5 6 Grandma's Boysenberry Spread 3 2 12 - 8 oz jars 25.00
6 7 Uncle Bob's Organic Dried Pears 3 7 12 - 1 lb pkgs. 30.00
7 8 Northwoods Cranberry Sauce 3 2 12 - 12 oz jars 40.00
8 9 Mishi Kobe Niku 4 6 18 - 500 g pkgs. 97.00
9 10 Ikura 4 8 12 - 200 ml jars 31.00
shippers_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3 entries, 0 to 2
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 ShipperID 3 non-null int64
1 ShipperName 3 non-null object
2 Phone 3 non-null object
dtypes: int64(1), object(2)
memory usage: 200.0+ bytes
shippers_df
ShipperID ShipperName Phone
0 1 Speedy Express (503) 555-9831
1 2 United Package (503) 555-3199
2 3 Federal Shipping (503) 555-9931
suppliers_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 29 entries, 0 to 28
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 SupplierID 29 non-null int64
1 SupplierName 29 non-null object
2 ContactName 29 non-null object
3 Address 29 non-null object
4 City 29 non-null object
5 PostalCode 29 non-null object
6 Country 29 non-null object
7 Phone 29 non-null object
dtypes: int64(1), object(7)
memory usage: 1.9+ KB
suppliers_df.head()
SupplierID SupplierName ContactName Address City PostalCode Country Phone
0 1 Exotic Liquid Charlotte Cooper 49 Gilbert St. Londona EC1 4SD UK (171) 555-2222
1 2 New Orleans Cajun Delights Shelley Burke P.O. Box 78934 New Orleans 70117 USA (100) 555-4822
2 3 Grandma Kelly's Homestead Regina Murphy 707 Oxford Rd. Ann Arbor 48104 USA (313) 555-5735
3 4 Tokyo Traders Yoshi Nagase 9-8 Sekimai Musashino-shi Tokyo 100 Japan (03) 3555-5011
4 5 Cooperativa de Quesos 'Las Cabras' Antonio del Valle Saavedra Calle del Rosal 4 Oviedo 33007 Spain (98) 598 76 54
Implementing Locally/On-Premise With PostgeSQL on Docker
Since Amazon RDS, Azure SQL Database, and GCP Cloud SQL are paid structured query language (SQL) server cloud services, it will be prudent to model your tasks locally (on-premise, i.e., your personal computer), before attempting to replicate the same tasks on the cloud and possibly incurring costs while doing so.
A PostgreSQL (or Postgres) relational database management system (RDBMS) image on Docker will be used as the local SQL server. The Psycopg package will be used as a Python interface to run queries on Postgres.
Connecting to database server
# Environment variables hidden for security purposes
db_connection = psycopg2.connect(
host = localhost_host,
port = localhost_port,
database = localhost_database,
user = localhost_user,
password = localhost_password
)
db_connection.autocommit = True
cursor = db_connection.cursor()
cursor.execute(query = "SELECT version()")
db_version = cursor.fetchone()
print(db_version)
('PostgreSQL 14.2 (Debian 14.2-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit',)
Resetting/dropping existing tables
The following tables are arranged in a way that dependent tables are dropped ahead of independent tables. Otherwise, an error will be thrown because you are dropping an independent table that is still being referenced by dependent table.
sql_drop_table = """
DROP TABLE "OrderDetails";
DROP TABLE "Products";
DROP TABLE "Categories";
DROP TABLE "Suppliers";
DROP TABLE "Orders";
DROP TABLE "Shippers";
DROP TABLE "Employees";
DROP TABLE "Customers";
"""
cursor.execute(query = sql_drop_table)
Creating tables
The following tables are arranged in a way that independent tables are created ahead of dependent tables. Otherwise, an error will be thrown if you create a table that references an independent table yet to be created.
sql_create_table = """
CREATE TABLE "Customers" (
"CustomerID" INT NOT NULL,
"CustomerName" VARCHAR(100) NOT NULL,
"ContactName" VARCHAR(100) NOT NULL,
"Address" VARCHAR(100) NOT NULL,
"City" VARCHAR(50) NOT NULL,
"PostalCode" VARCHAR(25) NOT NULL,
"Country" VARCHAR(25) NOT NULL,
PRIMARY KEY ("CustomerID")
);
CREATE TABLE "Employees" (
"EmployeeID" INT NOT NULL,
"LastName" VARCHAR(50) NOT NULL,
"FirstName" VARCHAR(50) NOT NULL,
"BirthDate" VARCHAR(100) NOT NULL,
"Photo" VARCHAR(250),
"Notes" VARCHAR(500),
PRIMARY KEY ("EmployeeID")
);
CREATE TABLE "Shippers" (
"ShipperID" INT NOT NULL,
"ShipperName" VARCHAR(50) NOT NULL,
"Phone" VARCHAR(15),
PRIMARY KEY ("ShipperID")
);
CREATE TABLE "Orders" (
"OrderID" INT NOT NULL,
"CustomerID" INT NOT NULL,
"EmployeeID" INT NOT NULL,
"OrderDate" VARCHAR(15),
"ShipperID" INT NOT NULL,
PRIMARY KEY ("OrderID"),
FOREIGN KEY ("CustomerID") REFERENCES "Customers",
FOREIGN KEY ("EmployeeID") REFERENCES "Employees",
FOREIGN KEY ("ShipperID") REFERENCES "Shippers"
);
CREATE TABLE "Suppliers" (
"SupplierID" INT NOT NULL,
"SupplierName" VARCHAR(50) NOT NULL,
"ContactName" VARCHAR(50) NOT NULL,
"Address" VARCHAR(100) NOT NULL,
"City" VARCHAR(50) NOT NULL,
"PostalCode" VARCHAR(25) NOT NULL,
"Country" VARCHAR(25) NOT NULL,
"Phone" VARCHAR(15),
PRIMARY KEY ("SupplierID")
);
CREATE TABLE "Categories" (
"CategoryID" INT NOT NULL,
"CategoryName" VARCHAR(50) NOT NULL,
"Description" VARCHAR(250) NOT NULL,
PRIMARY KEY ("CategoryID")
);
CREATE TABLE "Products" (
"ProductID" INT NOT NULL,
"ProductName" VARCHAR(100) NOT NULL,
"SupplierID" INT NOT NULL,
"CategoryID" INT NOT NULL,
"Unit" VARCHAR(100) NOT NULL,
"Price" FLOAT(2) NOT NULL,
PRIMARY KEY ("ProductID"),
FOREIGN KEY ("SupplierID") REFERENCES "Suppliers",
FOREIGN KEY ("CategoryID") REFERENCES "Categories"
);
CREATE TABLE "OrderDetails" (
"OrderDetailID" INT NOT NULL,
"OrderID" INT NOT NULL,
"ProductID" INT NOT NULL,
"Quantity" INT NOT NULL CHECK ("Quantity" >= 0),
PRIMARY KEY ("OrderDetailID"),
FOREIGN KEY ("OrderID") REFERENCES "Orders",
FOREIGN KEY ("ProductID") REFERENCES "Products"
);
"""
cursor.execute(query = sql_create_table)
Inserting Pandas dataframes into SQL tables
# Insert into Categories table
categories_tuples = [tuple(x) for x in categories_df.to_numpy()]
categories_cols = ','.join(list(categories_df.columns))
sql_select = """
INSERT INTO "Categories" VALUES(%s, %s, %s);
"""
cursor.executemany(sql_select, categories_tuples)
Repeat this for the other Pandas dataframes that need to be inserted into their respective tables.
Performing basic SELECT
-FROM
-WHERE
queries
Let’s confirm the numbers of rows and the data in the Categories
table.
sql_select = """
SELECT * FROM "Categories";
"""
cursor.execute(query = sql_select)
print("The Categories table has", cursor.rowcount, "rows.")
The Categories table has 8 rows.
row = cursor.fetchone()
while row is not None:
print("CategoryID:", row[0], ", CategoryName:", row[1], ", Description:", row[2])
row = cursor.fetchone()
CategoryID: 1 , CategoryName: Beverages , Description: Soft drinks, coffees, teas, beers, and ales
CategoryID: 2 , CategoryName: Condiments , Description: Sweet and savory sauces, relishes, spreads, and seasonings
CategoryID: 3 , CategoryName: Confections , Description: Desserts, candies, and sweet breads
CategoryID: 4 , CategoryName: Dairy Products , Description: Cheeses
CategoryID: 5 , CategoryName: Grains/Cereals , Description: Breads, crackers, pasta, and cereal
CategoryID: 6 , CategoryName: Meat/Poultry , Description: Prepared meats
CategoryID: 7 , CategoryName: Produce , Description: Dried fruit and bean curd
CategoryID: 8 , CategoryName: Seafood , Description: Seaweed and fish
Let’s now confirm that the other tables have the correct number of rows.
sql_select = """
SELECT * FROM "Customers";
"""
cursor.execute(query = sql_select)
print("The Customers table has", cursor.rowcount, "rows.")
The Customers table has 91 rows.
The Employees table has 10 rows.
The Shippers table has 3 rows.
The Orders table has 196 rows.
The Suppliers table has 29 rows.
The Products table has 77 rows.
The OrderDetails table has 518 rows.
Let’s see how many unique OrderDate
are in the Orders
table
sql_select = """
SELECT DISTINCT "OrderDate" FROM "Orders";
"""
cursor.execute(query = sql_select)
print("The Orders table has", cursor.rowcount, " unique OrderDate.")
The Orders table has 160 unique OrderDate.
Let’s see if there are products from the Products
table that are priced at over $100.00.
sql_select = """
SELECT "ProductID", "ProductName", "Price"
FROM "Products"
WHERE "Price" > 100.00;
"""
cursor.execute(query = sql_select)
row = cursor.fetchone()
while row is not None:
print("ProductID:", row[0], ", ProductName:", row[1], ", Price:", "{:.2f}".format(row[2]))
row = cursor.fetchone()
ProductID: 29 , ProductName: Thüringer Rostbratwurst , Price: 123.79
ProductID: 38 , ProductName: Côte de Blaye , Price: 263.50
Let’s see if there are orders that include any product priced at over $100.00.
sql_select = """
SELECT "OrderDetails"."OrderID", "Products"."ProductName", "Products"."Price"
FROM "Products", "OrderDetails"
WHERE "Products"."ProductID" = "OrderDetails"."ProductID"
AND "Products"."Price" > 100.00;
"""
cursor.execute(query = sql_select)
row = cursor.fetchone()
while row is not None:
print("Order ID:", row[0], "Product Name:", row[1], ", Price:", "${:.2f}".format(row[2]))
row = cursor.fetchone()
Order ID: 10268 Product Name: Thüringer Rostbratwurst , Price: $123.79
Order ID: 10290 Product Name: Thüringer Rostbratwurst , Price: $123.79
Order ID: 10305 Product Name: Thüringer Rostbratwurst , Price: $123.79
Order ID: 10329 Product Name: Côte de Blaye , Price: $263.50
Order ID: 10351 Product Name: Côte de Blaye , Price: $263.50
Order ID: 10353 Product Name: Côte de Blaye , Price: $263.50
Order ID: 10354 Product Name: Thüringer Rostbratwurst , Price: $123.79
Order ID: 10360 Product Name: Thüringer Rostbratwurst , Price: $123.79
Order ID: 10360 Product Name: Côte de Blaye , Price: $263.50
Order ID: 10369 Product Name: Thüringer Rostbratwurst , Price: $123.79
Order ID: 10372 Product Name: Côte de Blaye , Price: $263.50
Order ID: 10382 Product Name: Thüringer Rostbratwurst , Price: $123.79
Order ID: 10400 Product Name: Thüringer Rostbratwurst , Price: $123.79
Order ID: 10417 Product Name: Côte de Blaye , Price: $263.50
Order ID: 10424 Product Name: Côte de Blaye , Price: $263.50
Order ID: 10440 Product Name: Thüringer Rostbratwurst , Price: $123.79
Performing basic UNION
, INTERSECT
, and EXCEPT
set queries
Let’s see if there are any OrderID
s that include either ProductID
s priced at over $100.00. (The resulting set should be the same as above.)
sql_select = """
(SELECT "OrderDetails"."OrderID"
FROM "Products", "OrderDetails"
WHERE "Products"."ProductID" = "OrderDetails"."ProductID"
AND "OrderDetails"."ProductID" = 29)
UNION ALL
(SELECT "OrderDetails"."OrderID"
FROM "Products", "OrderDetails"
WHERE "Products"."ProductID" = "OrderDetails"."ProductID"
AND "OrderDetails"."ProductID" = 38);
"""
cursor.execute(query = sql_select)
row = cursor.fetchone()
while row is not None:
print("Order ID:", row[0])
row = cursor.fetchone()
Order ID: 10268
Order ID: 10290
Order ID: 10305
Order ID: 10354
Order ID: 10360
Order ID: 10369
Order ID: 10382
Order ID: 10400
Order ID: 10440
Order ID: 10329
Order ID: 10351
Order ID: 10353
Order ID: 10360
Order ID: 10372
Order ID: 10417
Order ID: 10424
Let’s see if there are any OrderID
s that include both ProductID
s priced at over $100.00.
sql_select = """
(SELECT "OrderDetails"."OrderID"
FROM "Products", "OrderDetails"
WHERE "Products"."ProductID" = "OrderDetails"."ProductID"
AND "OrderDetails"."ProductID" = 29)
INTERSECT
(SELECT "OrderDetails"."OrderID"
FROM "Products", "OrderDetails"
WHERE "Products"."ProductID" = "OrderDetails"."ProductID"
AND "OrderDetails"."ProductID" = 38);
"""
cursor.execute(query = sql_select)
row = cursor.fetchone()
while row is not None:
print("Order ID:", row[0])
row = cursor.fetchone()
Order ID: 10360
Let’s see if there are any OrderID
s that include ProductID
29, but not include ProductID
38.
sql_select = """
(SELECT "OrderDetails"."OrderID"
FROM "Products", "OrderDetails"
WHERE "Products"."ProductID" = "OrderDetails"."ProductID"
AND "OrderDetails"."ProductID" = 29)
EXCEPT
(SELECT "OrderDetails"."OrderID"
FROM "Products", "OrderDetails"
WHERE "Products"."ProductID" = "OrderDetails"."ProductID"
AND "OrderDetails"."ProductID" = 38);
"""
cursor.execute(query = sql_select)
row = cursor.fetchone()
while row is not None:
print("Order ID:", row[0])
row = cursor.fetchone()
Order ID: 10354
Order ID: 10382
Order ID: 10305
Order ID: 10268
Order ID: 10440
Order ID: 10400
Order ID: 10369
Order ID: 10290
sql_select = """
SELECT *
FROM "Products"
INNER JOIN "Categories"
ON "Products"."CategoryID" = "Categories"."CategoryID"
ORDER BY "ProductName" ASC
LIMIT 10;
"""
cursor.execute(query = sql_select)
row = cursor.fetchone()
while row is not None:
print(
"{",
"\"ProductID\":", row[0], ",",
"\"ProductName\":", "\"" + row[1] + "\",",
"\"SupplierID\":", row[2], ",",
"\"CategoryID\":", row[3], ",",
"\"Unit\":", "\"" + row[4] + "\",",
"\"Price\":", row[5], ",",
"\"CategoryID\":", row[6], ",",
"\"CategoryName\":", "\"" + row[7] + "\",",
"\"CategoryDescription\":", "\"" + row[8] + "\"",
"}")
row = cursor.fetchone()
{ "ProductID": 17 , "ProductName": "Alice Mutton", "SupplierID": 7 , "CategoryID": 6 , "Unit": "20 - 1 kg tins", "Price": 39.0 , "CategoryID": 6 , "CategoryName": "Meat/Poultry", "CategoryDescription": "Prepared meats" }
{ "ProductID": 3 , "ProductName": "Aniseed Syrup", "SupplierID": 1 , "CategoryID": 2 , "Unit": "12 - 550 ml bottles", "Price": 10.0 , "CategoryID": 2 , "CategoryName": "Condiments", "CategoryDescription": "Sweet and savory sauces, relishes, spreads, and seasonings" }
{ "ProductID": 40 , "ProductName": "Boston Crab Meat", "SupplierID": 19 , "CategoryID": 8 , "Unit": "24 - 4 oz tins", "Price": 18.4 , "CategoryID": 8 , "CategoryName": "Seafood", "CategoryDescription": "Seaweed and fish" }
{ "ProductID": 60 , "ProductName": "Camembert Pierrot", "SupplierID": 28 , "CategoryID": 4 , "Unit": "15 - 300 g rounds", "Price": 34.0 , "CategoryID": 4 , "CategoryName": "Dairy Products", "CategoryDescription": "Cheeses" }
{ "ProductID": 18 , "ProductName": "Carnarvon Tigers", "SupplierID": 7 , "CategoryID": 8 , "Unit": "16 kg pkg.", "Price": 62.5 , "CategoryID": 8 , "CategoryName": "Seafood", "CategoryDescription": "Seaweed and fish" }
{ "ProductID": 1 , "ProductName": "Chais", "SupplierID": 1 , "CategoryID": 1 , "Unit": "10 boxes x 20 bags", "Price": 18.0 , "CategoryID": 1 , "CategoryName": "Beverages", "CategoryDescription": "Soft drinks, coffees, teas, beers, and ales" }
{ "ProductID": 2 , "ProductName": "Chang", "SupplierID": 1 , "CategoryID": 1 , "Unit": "24 - 12 oz bottles", "Price": 19.0 , "CategoryID": 1 , "CategoryName": "Beverages", "CategoryDescription": "Soft drinks, coffees, teas, beers, and ales" }
{ "ProductID": 39 , "ProductName": "Chartreuse verte", "SupplierID": 18 , "CategoryID": 1 , "Unit": "750 cc per bottle", "Price": 18.0 , "CategoryID": 1 , "CategoryName": "Beverages", "CategoryDescription": "Soft drinks, coffees, teas, beers, and ales" }
{ "ProductID": 4 , "ProductName": "Chef Anton's Cajun Seasoning", "SupplierID": 2 , "CategoryID": 2 , "Unit": "48 - 6 oz jars", "Price": 22.0 , "CategoryID": 2 , "CategoryName": "Condiments", "CategoryDescription": "Sweet and savory sauces, relishes, spreads, and seasonings" }
{ "ProductID": 5 , "ProductName": "Chef Anton's Gumbo Mix", "SupplierID": 2 , "CategoryID": 2 , "Unit": "36 boxes", "Price": 21.35 , "CategoryID": 2 , "CategoryName": "Condiments", "CategoryDescription": "Sweet and savory sauces, relishes, spreads, and seasonings" }
sql_select = """
SELECT "CategoryName", AVG("Price")
FROM "Products"
INNER JOIN "Categories"
ON "Products"."CategoryID" = "Categories"."CategoryID"
GROUP BY "CategoryName";
"""
cursor.execute(query = sql_select)
row = cursor.fetchone()
Disconnecting from database server
cursor.close()
db_connection.close()