Writing HogQL queries in Python
Contents
❗️ This guide is intended only for development of PostHog itself. If you're looking for documentation on writing HogQL (or SQL) queries, go to the SQL docs.
HogQL is our layer on top of ClickHouse SQL which provides nice features such as:
- Automatic person/group/etc property joins depending on the team/context
- Customisable database schema per team
- Flexible AST-powered templating language for building queries.
Query templates
HogQL queries are built up from AST (Abstract Syntax Tree) nodes.
You can build the nodes yourself, or use the helpers parse_expr and parse_select to convert HogQL strings into AST nodes:
Few things to note:
parse_selectparses fullSELECTqueries whileparse_exprparses any expression (1+1oreventor even a subquery(select 1)). It's not possible to parse parts of a select query, such aslimit 10.- Placeholders like
{where}are just nodes of typeast.Placeholder(field='where'). You can leave them in, and callstmt = replace_placeholders(stmt, { where: parse_expr('1') })later. - We wrote one AST node ourselves:
ast.Constant(value=num_last_days). We did it to sanitize the value by make sure it's treated as a constant. We might simplify constants further (e.g.parse_constor just{days: 2}), but we're not there yet.
Placeholder expansion allows at most 1,000 placeholders and shares a five-second deadline across the query.
All placeholders also share a 64 MiB budget in Hog VM memory units, charged using each expression's peak stack usage, including temporary values.
This accounting is not a limit on Python process memory.
The range() builtin checks its result size against the remaining VM allowance before allocating the list.
Queries that exceed these limits fail during expansion; reduce the number or size of the placeholder expressions to stay within them.
Pattern matching during query preparation
Expressions inside HogQL placeholders execute in the Python HogVM.
Its regex and LIKE functions and operators accept patterns up to 16,384 characters.
Larger patterns raise a HogVMException; shorten the pattern before matching.
Subject strings have no separate matching limit and use the VM's existing 64 MiB stack memory budget, so matching can process multi-megabyte response bodies.
These limits also apply to extractRegex, which still returns an empty string for invalid regex syntax.
Regex matching uses RE2 syntax, so backreferences and lookaround are unsupported.
SQL LIKE and ILIKE patterns sent to ClickHouse are not subject to these VM limits. For non-nullable materialized columns, patterns above 16,384 characters skip the optional sentinel-based rewrite and use the normal property read.
AST nodes
If you want more control, you can build the AST nodes directly. The same query above can be written as:
You can mix and match parse_expr and ast nodes as you please. The example above still took a shortcut for the where clause because it was easier to write.
Snowflake date formatting
For direct Snowflake queries, formatDateTime requires a literal format string and translates supported strftime specifiers into Snowflake format elements.
The printer binds the translated format as a query parameter, preserving literal quotes and backslashes in the parameter value.
Pass the printed SQL and HogQLContext.values together to the database driver.
Database schema and features
The HogQL database schema is in flux. You will soon be able to explore it in the PostHog app itself.
The most up to date resource is hogql/database.py on Github. At the time of writing, these tables were available:
Some tables have some fields that are actually "lazy tables". When accessed they will add a join to the table. The events table is such an example:
If you access pdi.person.properties.$browser, we make a join via persons (this is a HogQL table name, not ClickHouse name). We do a bunch of argmax magic in the join, and inline all accessed properties within the subquery for performance. For the user, it looks just like simple property access.
If you access poe.properties.$browser, we will actually access the field person_properties on the events table.
In practice, you should avoid both and access person.properties.$browser, which will choose the right approach for you.
Add new tables and fields as needed! Just make sure each table has a team_id column.
Internal marketing queries can read cached session dimensions from posthog.web_sessions_dimensional_preaggregated.
Rows include a precompute job ID and the person ID at computation time; readers must resolve current identities separately.