Databases 17 min read

Master Peewee ORM: From Basic CRUD to Advanced Queries Compared with SQLAlchemy

This tutorial walks through Peewee’s lightweight ORM features—from creating, bulk inserting, updating, and deleting records to complex aggregations, window functions, CTEs, and PostgreSQL RETURNING clauses—while comparing its learning curve and performance to SQLAlchemy using real‑world Leapcell examples.

Code Mala Tang
Code Mala Tang
Code Mala Tang
Master Peewee ORM: From Basic CRUD to Advanced Queries Compared with SQLAlchemy

In Python database development, ORM tools simplify interaction with databases. Peewee, a lightweight yet powerful ORM, provides an elegant and efficient way to query databases. Whether deployed locally or on cloud platforms like Leapcell, Peewee delivers excellent performance. This article explores Peewee's query capabilities, compares it with SQLAlchemy, and demonstrates its advantages with real examples from the Leapcell ecosystem.

1. Basic Query Operations

(1) Creating Records

Assume a User model in a Leapcell‑like user service that stores username, email, and plan_type (subscription plan). Creating a new user with Peewee is straightforward:

from peewee import *
# Demonstration with SQLite; replace with actual DB in Leapcell deployment
db = SqliteDatabase('leapcell_users.db')

class User(Model):
    username = CharField()
    email = CharField(unique=True)
    plan_type = CharField()
    class Meta:
        database = db

db.connect()
db.create_tables([User])

# Create a new user
new_user = User.create(username='test_user', email='[email protected]', plan_type='basic')

The User.create() method accepts keyword arguments matching the model fields, inserts a new row, and returns the created instance.

(2) Bulk Insertion

For bulk data insertion, such as migrating users to Leapcell:

user_data = [
    {'username': 'user1', 'email': '[email protected]', 'plan_type': 'pro'},
    {'username': 'user2', 'email': '[email protected]', 'plan_type': 'basic'},
    {'username': 'user3', 'email': '[email protected]', 'plan_type': 'enterprise'}
]
# Bulk insert with insert_many()
with db.atomic():
    User.insert_many(user_data).execute()
insert_many()

accepts a list of dictionaries and inserts all rows in a single database operation, which is far more efficient than calling create() repeatedly.

(3) Updating Records

To update a user's subscription plan on Leapcell:

user_to_update = User.get(User.username == 'test_user')
user_to_update.plan_type = 'pro'
user_to_update.save()

# Bulk update: upgrade all "basic" plans to "pro"
query = User.update(plan_type='pro').where(User.plan_type == 'basic')
query.execute()

Use save() for single‑instance updates or Model.update().where() for bulk operations.

(4) Deleting Records

To remove a user from the platform:

user_to_delete = User.get(User.username == 'user1')
user_to_delete.delete_instance()

# Delete multiple users (e.g., inactive accounts)
query = User.delete().where(User.is_deleted == True)
query.execute()
delete_instance()

deletes a single row, while Model.delete().where() performs conditional deletions.

(5) Querying Records

Single Record Query

Retrieve a specific user with get() or get_by_id():

user = User.get_by_id(1)
print(user.username, user.email, user.plan_type)
# Query by other fields
user = User.get(User.email == '[email protected]')

If no matching record exists, these methods raise a DoesNotExist exception.

Multiple Record Query

Iterate over all users:

for user in User.select():
    print(user.username, user.email)
# Slicing and indexing
users_subset = User.select()[:5]
for user in users_subset:
    print(user.username)

For large result sets, use Select.iterator() to stream results efficiently.

Filtering Records

# Simple filter
pro_users = User.select().where(User.plan_type == 'pro')
# Complex condition with bitwise operators
active_pro_users = User.select().where((User.plan_type == 'pro') & (User.is_active == True))
# IN query
specific_emails = ['[email protected]', '[email protected]']
matching_users = User.select().where(User.email.in_(specific_emails))

Sorting Records

sorted_users = User.select().order_by(User.username)
# Descending by registration time (assuming a registered_at field)
recent_users = User.select().order_by(-User.registered_at)
# Multi‑field ordering
multi_sorted_users = User.select().order_by(User.plan_type, User.username)

Pagination and Counting

# Pagination (page 2, 10 items per page)
paged_users = User.select().order_by(User.id).paginate(2, 10)
for user in paged_users:
    print(user.username)
# Total count
user_count = User.select().count()
print(f"Total users: {user_count}")
# Count per plan type
pro_user_count = User.select().where(User.plan_type == 'pro').count()

Aggregation and Scalar Queries

from peewee import fn
# Count users per plan type
query = (User
         .select(User.plan_type, fn.Count(User.id).alias('count'))
         .group_by(User.plan_type))
for result in query:
    print(result.plan_type, result.count)
# Get a scalar value (e.g., max user ID)
max_id = User.select(fn.Max(User.id)).scalar()

Window Functions

from peewee import Window, fn
# Rank users within each plan type by registration date
query = User.select(
    User.username,
    User.plan_type,
    fn.RANK().over(
        order_by=[User.registered_at],
        partition_by=[User.plan_type]
    ).alias('registration_rank')
)
for user in query:
    print(user.username, user.plan_type, user.registration_rank)

Reusable Window Definitions

# Define a reusable window
win = Window(order_by=[User.registered_at], partition_by=[User.plan_type])
query = User.select(
    User.username,
    User.plan_type,
    fn.RANK().over(win).alias('rank1'),
    fn.DENSE_RANK().over(win).alias('rank2')
).window(win)

Multiple Window Definitions

win1 = Window(order_by=[User.registered_at]).alias('win1')
win2 = Window(partition_by=[User.plan_type]).alias('win2')
query = User.select(
    User.username,
    User.plan_type,
    fn.SUM(User.login_count).over(win1).alias('total_logins'),
    fn.AVG(User.login_count).over(win2).alias('avg_logins')
).window(win1, win2)

Frame Types: RANGE, ROWS, GROUPS

Control how window functions calculate results:

class Sample(Model):
    counter = IntegerField()
    value = FloatField()
    class Meta:
        database = db

db.create_tables([Sample])
# Insert test data
data = [(1, 10), (1, 20), (2, 1), (2, 3), (3, 100)]
Sample.insert_many(data, fields=[Sample.counter, Sample.value]).execute()
# Cumulative sum using ROWS frame type
query = Sample.select(
    Sample.counter,
    Sample.value,
    fn.SUM(Sample.value).over(
        order_by=[Sample.id],
        frame_type=Window.ROWS
    ).alias('rsum')
)
for sample in query:
    print(sample.counter, sample.value, sample.rsum)

Retrieving Data as Tuples or Dictionaries

# Return results as dictionaries
query = User.select(User.username, User.plan_type).dicts()
for user_dict in query:
    print(user_dict['username'], user_dict['plan_type'])
# Return results as tuples
query = User.select(User.username, User.plan_type).tuples()
for user_tuple in query:
    print(user_tuple[0], user_tuple[1])

RETURNING Clause (PostgreSQL)

from peewee import PostgresqlDatabase
# PostgreSQL connection
db = PostgresqlDatabase('leapcell_db', user='user', password='password', host='localhost', port=5432)

class User(Model):
    username = CharField()
    email = CharField(unique=True)
    plan_type = CharField()
    class Meta:
        database = db

# Update with RETURNING to fetch modified rows
query = (User
         .update(plan_type='enterprise')
         .where(User.username == 'test_user')
         .returning(User))
for updated_user in query.execute():
    print(updated_user.username, updated_user.plan_type)

Common Table Expressions (CTEs)

from peewee import CTE
class UserActivity(Model):
    user = ForeignKeyField(User)
    action_time = DateTimeField()
    class Meta:
        database = db

db.create_tables([UserActivity])
# CTE to calculate average activity interval per user
cte = (UserActivity
       .select(UserActivity.user,
               fn.AVG(fn.JULIANDAY(UserActivity.action_time) -
                      fn.JULIANDAY(fn.LAG(UserActivity.action_time).over(order_by=[UserActivity.user, UserActivity.action_time])))
       .alias('avg_interval')
       .group_by(UserActivity.user)
       .cte('user_activity_intervals'))
# Query users with average interval < 1 day
query = (User
         .select(User.username)
         .join(cte, on=(User.id == cte.c.user))
         .where(cte.c.avg_interval < 1)
         .with_cte(cte))
for user in query:
    print(user.username)

2. Comparison with SQLAlchemy

(1) Learning Curve

Peewee’s API is intuitive and easy to pick up, with query syntax that reads like natural language. SQLAlchemy, while feature‑rich, has a steeper learning curve, especially for advanced mapping and transaction management.

(2) Performance

For simple queries, performance is comparable. However, Peewee’s lightweight design and concise SQL generation give it an edge in complex operations and large data sets, whereas SQLAlchemy’s extensive feature set can introduce overhead.

(3) Suitable Scenarios

Peewee excels in rapid development and lightweight applications—ideal for platforms like Leapcell where simplicity and efficiency are priorities. SQLAlchemy is better suited for large‑scale enterprise applications that require complex relationships and advanced transaction handling.

3. Conclusion

Peewee offers an efficient way to work with databases and is a top choice for developers. Through practical Leapcell user‑service examples, this tutorial demonstrated Peewee’s capabilities in CRUD operations, aggregations, and advanced queries. Compared with SQLAlchemy, Peewee shines in learning curve, performance, and rapid development, making it the preferred ORM for agile, lightweight projects.

PythonORMPeeweeCRUDSQLAlchemyAdvanced Queries
Code Mala Tang
Written by

Code Mala Tang

Read source code together, write articles together, and enjoy spicy hot pot together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.