Common Table Expressions

Peewee supports the inclusion of common table expressions (CTEs) in all typesof queries. CTEs may be useful for:

  • Factoring out a common subquery.
  • Grouping or filtering by a column derived in the CTE’s result set.
  • Writing recursive queries.

To declare a Select query for use as a CTE, usecte() method, which wraps the query in a CTEobject. To indicate that a CTE should be included as part of aquery, use the Query.with_cte() method, passing a list of CTE objects.

Simple Example

For an example, let’s say we have some data points that consist of a key and afloating-point value. Let’s define our model and populate some test data:

  1. class Sample(Model):
  2. key = TextField()
  3. value = FloatField()
  4.  
  5. data = (
  6. ('a', (1.25, 1.5, 1.75)),
  7. ('b', (2.1, 2.3, 2.5, 2.7, 2.9)),
  8. ('c', (3.5, 3.5)))
  9.  
  10. # Populate data.
  11. for key, values in data:
  12. Sample.insert_many([(key, value) for value in values],
  13. fields=[Sample.key, Sample.value]).execute()

Let’s use a CTE to calculate, for each distinct key, which values wereabove-average for that key.

  1. # First we'll declare the query that will be used as a CTE. This query
  2. # simply determines the average value for each key.
  3. cte = (Sample
  4. .select(Sample.key, fn.AVG(Sample.value).alias('avg_value'))
  5. .group_by(Sample.key)
  6. .cte('key_avgs', columns=('key', 'avg_value')))
  7.  
  8. # Now we'll query the sample table, using our CTE to find rows whose value
  9. # exceeds the average for the given key. We'll calculate how far above the
  10. # average the given sample's value is, as well.
  11. query = (Sample
  12. .select(Sample.key, Sample.value)
  13. .join(cte, on=(Sample.key == cte.c.key))
  14. .where(Sample.value > cte.c.avg_value)
  15. .order_by(Sample.value)
  16. .with_cte(cte))

We can iterate over the samples returned by the query to see which samples hadabove-average values for their given group:

  1. >>> for sample in query:
  2. ... print(sample.key, sample.value)
  3.  
  4. # 'a', 1.75
  5. # 'b', 2.7
  6. # 'b', 2.9

Complex Example

For a more complete example, let’s consider the following query which usesmultiple CTEs to find per-product sales totals in only the top sales regions.Our model looks like this:

  1. class Order(Model):
  2. region = TextField()
  3. amount = FloatField()
  4. product = TextField()
  5. quantity = IntegerField()

Here is how the query might be written in SQL. This example can be found inthe postgresql documentation.

  1. WITH regional_sales AS (
  2. SELECT region, SUM(amount) AS total_sales
  3. FROM orders
  4. GROUP BY region
  5. ), top_regions AS (
  6. SELECT region
  7. FROM regional_sales
  8. WHERE total_sales > (SELECT SUM(total_sales) / 10 FROM regional_sales)
  9. )
  10. SELECT region,
  11. product,
  12. SUM(quantity) AS product_units,
  13. SUM(amount) AS product_sales
  14. FROM orders
  15. WHERE region IN (SELECT region FROM top_regions)
  16. GROUP BY region, product;

With Peewee, we would write:

  1. reg_sales = (Order
  2. .select(Order.region,
  3. fn.SUM(Order.amount).alias('total_sales'))
  4. .group_by(Order.region)
  5. .cte('regional_sales'))
  6.  
  7. top_regions = (reg_sales
  8. .select(reg_sales.c.region)
  9. .where(reg_sales.c.total_sales > (
  10. reg_sales.select(fn.SUM(reg_sales.c.total_sales) / 10)))
  11. .cte('top_regions'))
  12.  
  13. query = (Order
  14. .select(Order.region,
  15. Order.product,
  16. fn.SUM(Order.quantity).alias('product_units'),
  17. fn.SUM(Order.amount).alias('product_sales'))
  18. .where(Order.region.in_(top_regions.select(top_regions.c.region)))
  19. .group_by(Order.region, Order.product)
  20. .with_cte(regional_sales, top_regions))

Recursive CTEs

Peewee supports recursive CTEs. Recursive CTEs can be useful when, for example,you have a tree data-structure represented by a parent-link foreign key.Suppose, for example, that we have a hierarchy of categories for an onlinebookstore. We wish to generate a table showing all categories and theirabsolute depths, along with the path from the root to the category.

We’ll assume the following model definition, in which each category has aforeign-key to its immediate parent category:

  1. class Category(Model):
  2. name = TextField()
  3. parent = ForeignKeyField('self', backref='children', null=True)

To list all categories along with their depth and parents, we can use arecursive CTE:

  1. # Define the base case of our recursive CTE. This will be categories that
  2. # have a null parent foreign-key.
  3. Base = Category.alias()
  4. level = Value(1).alias('level')
  5. path = Base.name.alias('path')
  6. base_case = (Base
  7. .select(Base.name, Base.parent, level, path)
  8. .where(Base.parent.is_null())
  9. .cte('base', recursive=True))
  10.  
  11. # Define the recursive terms.
  12. RTerm = Category.alias()
  13. rlevel = (base_case.c.level + 1).alias('level')
  14. rpath = base_case.c.path.concat('->').concat(RTerm.name).alias('path')
  15. recursive = (RTerm
  16. .select(RTerm.name, RTerm.parent, rlevel, rpath)
  17. .join(base_case, on=(RTerm.parent == base_case.c.id)))
  18.  
  19. # The recursive CTE is created by taking the base case and UNION ALL with
  20. # the recursive term.
  21. cte = base_case.union_all(recursive)
  22.  
  23. # We will now query from the CTE to get the categories, their levels, and
  24. # their paths.
  25. query = (cte
  26. .select_from(cte.c.name, cte.c.level, cte.c.path)
  27. .order_by(cte.c.path))
  28.  
  29. # We can now iterate over a list of all categories and print their names,
  30. # absolute levels, and path from root -> category.
  31. for category in query:
  32. print(category.name, category.level, category.path)
  33.  
  34. # Example output:
  35. # root, 1, root
  36. # p1, 2, root->p1
  37. # c1-1, 3, root->p1->c1-1
  38. # c1-2, 3, root->p1->c1-2
  39. # p2, 2, root->p2
  40. # c2-1, 3, root->p2->c2-1