Explanation: An RAI model groups related types and rules—like a Snowflake schema for tables—defining data entities and their relationships.
This guide maps common SQL operations to their RAI equivalents, helping users with a SQL background transition smoothly to using RAI.
The following table maps the core RAI concepts discussed in the Core Concepts guide to their SQL equivalents:
Explanation: An RAI model groups related types and rules—like a Snowflake schema for tables—defining data entities and their relationships.
Explanation: An RAI type is like a SQL table or view—a set of entities—but it’s schemaless, so properties (i.e., fields) can be added on the fly.
Explanation: An object is a row‑like record—an instance of a type—but it can belong to several types, not just one.
Explanation: An RAI property is a column‑like attribute, but it may be single‑ or multi‑valued and isn’t type‑fixed.
Explanation: RAI rules merge SQL‑style constraints, triggers, and stored procedures—enforcing integrity, reacting to changes, and performing complex logic and data transforms.
Explanation: RAI queries fetch data like SQL but offer more reasoning power to answer questions SQL can’t.
While RAI and SQL share some similarities, there are several key differences that you should be aware of when transitioning from SQL to RAI.
Explanation: RAI types can add properties on the fly and store mixed value types; SQL tables lock columns and data types upfront.
Explanation: An object can belong to several types; an SQL row exists in only one table.
Explanation: RAI never changes the stored facts—it derives new states; SQL can rewrite rows with UPDATE.
Explanation: RAI aggregates operate on distinct values; SQL aggregates scan every row, counting duplicates unless DISTINCT
is used.
RAI models are logical containers that group related types and rules together.
To create a model, import the relationalai
package and create a Model
object:
import relationalai as rai
# Additional recommended imports:from relationalai.std import alias # For customizing result column namesfrom relationalai.std import aggregates # For functions like count, sum, min, max, etc.
model = rai.Model("MyModel")
CREATE SCHEMA IF NOT EXISTS MyModel;USE SCHEMA MyModel;
See What is a Model? for more information about models.
Use the model.Type()
constructor to define a new type in RAI:
# Create a new type named 'Person'. Person is an instance of the Type class.Person = model.Type("Person")# Optionally, declare properties used by the Person type.Person.id.declare()
CREATE TABLE IF NOT EXISTS Person ( id INT);
Notes:
Types are schemaless and do not require explicit column definitions. However, if you prefer a structured approach, you may use:
Property.declare()
to explicitly declare single-valued properties.Property.has_many()
to declare multi-valued properties.Properties do not have a fixed data type, unlike columns in SQL tables. The same property may reference an integer for one object and to a float for another object.
Use the model.Type()
constructor with the source
argument to define a type based on a Snowflake table:
# NOTE: `<db>`, `<schema>`, and `<table>` are placeholders for your database,# schema, and table names, respectively.Person = model.Type("Person", source="<db>.<schema>.<table>")
# If the table has a column that you intend to use as a multi-valued property,# you must declare it as multi-valued using Property.has_many(). Otherwise, all# columns are interpreted as single-valued properties. For example, your source# data may have an 'email' column that you may want to use as a multi-valued# property in your model to store multiple email addresses for the same person.Person.email.has_many()
See Defining Objects from Rows in Snowflake Tables for details.
Use the Type.add()
method to define new objects in a rule:
with model.rule(): Person.add(id=1)
INSERT INTO Person (id) VALUES (1);
Properties defined in Type.add()
serve as the object’s primary key.
See Defining Objects in Rules for details.
Use the Instance.set()
method to set the value of a single-valued property for an object:
with model.rule(): person = Person.add(id=1) # Person.add() returns an Instance object person.set(name="Bob", birthday="1994-03-14")
-- Dynamically add the 'name' and 'birthday' columns to the 'Person' table.ALTER TABLE PersonADD COLUMN IF NOT EXISTS name VARCHAR(255),ADD COLUMN IF NOT EXISTS birthday DATE;
-- Insert the new record with the 'birthday' propertyINSERT INTO Person (id, name, birthday) VALUES (1, 'Bob', '1994-03-14');
See Single-Valued Properties for more information.
Use the InstanceProperty.extend()
method to add values to a multi-valued property:
with model.rule(): person = Person.add(id=1) person.set(name="Bob", birthday="1994-03-14") # person.emails returns an InstanceProperty object. If the emails property # has never been set before, it is created by the .extend() method. person.emails.extend(["bob@example.com", "bob@company.com"])
-- Multi-valued properties would be modeled using separate tables with a-- foreign key to the main table.CREATE TABLE IF NOT EXISTS PersonEmails ( person_id INT, email VARCHAR(255), FOREIGN KEY (person_id) REFERENCES Person(id));
-- Insert the email addresses for the person with id=1INSERT INTO PersonEmails (person_id, email) VALUES(1, 'bob@example.com'),(1, 'bob@company.com');
See Multi-Valued Properties for details.
Relationships are properties that link objects to other objects in the model. They may be defined based on foreign keys in the source data or created explicitly in rules.
Use the Type.define()
method to define relationships based on foreign keys in your source data:
# Define types sourced from Snowflake tablesEmployee = model.Type("Employee", source="<db>.<schema>.employees")Department = model.Type("Department", source="<db>.<schema>.departments")
# Define the 'department' property on Employee, linking to Department via# Employee.department_id -> Department.idEmployee.define(department=(Department, "department_id", "id"))
CREATE TABLE Employee ( id INT, name TEXT, department_id INT, FOREIGN KEY (department_id) REFERENCES Department(id));
CREATE TABLE Department ( id INT, name TEXT);
By default, define()
creates a single-valued property.
To define a multi-valued property, use the Property.has_many()
method:
Employee = model.Type("Employee", source="<db>.<schema>.employees")Department = model.Type("Department", source="<db>.<schema>.departments")
# Define a multi-valued relationship between Employee and DepartmentEmployee.define(departments=(Department, "department_id", "id"))Employee.departments.has_many()
CREATE TABLE Employee ( id INT, name TEXT);
CREATE TABLE Department ( id INT, name TEXT);
-- Join table for the many-to-many relationshipCREATE TABLE EmployeeDepartments ( employee_id INT, department_id INT, FOREIGN KEY (employee_id) REFERENCES Employee(id), FOREIGN KEY (department_id) REFERENCES Department(id));
Notes:
Employee
object is associated with one Department
object via the department_id
foreign key in the employees
table.EmployeeDepartment
represents the many-to-many relationship.You can define relationships between objects directly in rules by setting properties that link objects:
with model.rule(): # Create new Employee and Department objects employee = Employee.add(id=1, name="Alice") department = Department.add(id=10, name="Engineering")
# Set the 'department' property of the employee to the department object employee.set(department=department)
-- Insert departmentALTER TABLE DepartmentADD COLUMN IF NOT EXISTS id INT,ADD COLUMN IF NOT EXISTS name TEXT;INSERT INTO Department (id, name) VALUES (10, 'Engineering');
-- Insert employee with department_idALTER TABLE EmployeeADD COLUMN IF NOT EXISTS id INT,ADD COLUMN IF NOT EXISTS name TEXT,ADD COLUMN IF NOT EXISTS department_id INT;INSERT INTO Employee (id, name, department_id) VALUES (1, 'Alice', 10);
If an employee can belong to multiple departments, you can define a multi-valued relationship:
# Declare 'departments' as a multi-valued propertyEmployee.departments.has_many()
with model.rule(): # Create Employee and Department objects employee = Employee.add(id=1, name="Bob") dept_engineering = Department.add(id=10, name="Engineering") dept_research = Department.add(id=20, name="Research")
# Assign multiple departments to the employee employee.departments.extend([dept_engineering, dept_research])
-- Insert departmentsALTER TABLE DepartmentADD COLUMN IF NOT EXISTS id INT,ADD COLUMN IF NOT EXISTS name TEXT;INSERT INTO Department (id, name) VALUES (10, 'Engineering'), (20, 'Research');
-- Insert employeeALTER TABLE EmployeeADD COLUMN IF NOT EXISTS id INT,ADD COLUMN IF NOT EXISTS name TEXT;INSERT INTO Employee (id, name) VALUES (1, 'Bob');
-- Insert into join table to represent the multi-valued relationshipCREATE TABLE IF NOT EXISTS EmployeeDepartments ( employee_id INT, department_id INT, FOREIGN KEY (employee_id) REFERENCES Employee(id), FOREIGN KEY (department_id) REFERENCES Department(id));INSERT INTO EmployeeDepartments (employee_id, department_id) VALUES(1, 10), -- Bob is in Engineering(1, 20); -- Bob is in Research
Notes:
department
property links an employee to one department.departments
property allows an employee to be associated with multiple departments.Employee.departments.has_many()
declares departments
as a multi-valued property.InstanceProperty.extend()
method is used to add multiple departments to an employee.You can perform joins by navigating relationships between objects using the dot (.
) operator:
with model.query() as select: employee = Employee() # employee.department.name accesses the name property of the related Department object. employee.department.name == "Sales" response = select(employee.name)
SELECT e.nameFROM Employee eJOIN Department d ON e.department_id = d.idWHERE d.name = 'Sales';
For a multi-valued relationship, you can access related objects similarly:
with model.query() as select: employee = Employee() employee.departments.name == "Engineering" # Multi-valued property response = select(employee.name)
SELECT e.nameFROM Employee eJOIN EmployeeDepartment ed ON e.id = ed.employee_idJOIN Department d ON ed.department_id = d.idWHERE d.name = 'Engineering';
Rules define objects, set properties, and establish relationships between objects. Queries retrieve information from the model based on the rules you’ve defined.
Both rules and queries are written using RAI’s Python query-builder syntax:
import relationalai as rai
# 1. Create a model.model = rai.Model("MyModel")
# 2. Define types.Person = model.Type("Person")Adult = model.Type("Adult")
# 3. Define a rule to create a new Person object.with model.rule(): person = Person.add(id=1) person.set(name="Alice", age=30)
# 4. Define a rule that assigns Person objects to the Adult type if their age# property is greater than or equal to 18.with model.rule(): person = Person() person.age >= 18 person.set(Adult)
# 5. Get the names and ages of all adults.with model.query() as select: adult = Adult() response = select(adult.name, adult.age)
-- 1. Create a schema and switch to it.CREATE SCHEMA IF NOT EXISTS MyModel;USE SCHEMA MyModel;
-- 2. Create tables for Person and Adult types.CREATE TABLE IF NOT EXISTS Person;CREATE TABLE IF NOT EXISTS Adult;
-- 3. Add columns to the Person table and insert a record.ALTER TABLE PersonADD COLUMN IF NOT EXISTS id INT,ADD COLUMN IF NOT EXISTS name VARCHAR,ADD COLUMN IF NOT EXISTS age INT;INSERT INTO Person (id, name, age) VALUES (1, 'Alice', 30);
-- 4. Insert records into the Adult table corresponding to Person records-- with age >= 18.ALTER TABLE AdultADD COLUMN IF NOT EXISTS id INT;INSERT INTO Adult (id) SELECT id FROM Person WHERE age >= 18;
-- 5. Get the names and ages of all adults.SELECT name, ageFROM Person p, Adult aWHERE p.id = a.id;
Notes:
model.rule()
and model.query()
context managers, respectively.See Executing Queries and Capturing Knowledge in Rules for more information.
Use the alias()
function to customize the names of columns in the query results:
from relationalai.std import alias
with model.query() as select: person = Person() response = select(alias(person.name, "full_name"))
SELECT name AS full_name FROM Person;
By default, RAI queries return a pandas DataFrame
.
Set format="snowpark"
in the query to return a Snowpark DataFrame instead:
with model.query(format="snowpark") as select: person = Person() response = select(person.name)
To sort objects or values within a query, use the rank_asc()
and rank_desc()
functions to compute the rank of each object and include the rank as the first column in the query results:
from relationalai.std import aggregates
with model.query() as select: person = Person() rank = aggregates.rank_asc(person.age) rank <= 10 # Filter results where rank is at most 10 response = select(rank, person.name, person.age)
WITH ranked_persons AS ( SELECT RANK() OVER (ORDER BY age ASC) AS rank, name, age FROM Person)SELECT rank, name, ageFROM ranked_personsWHERE rank <= 10;
Both .rank_asc()
and .rank_desc()
work similarly to SQL’s RANK()
function.
The computed rank can be assigned to a variable and used in additional filters or aggregations.
As an alternative, you can also sort results after the query is executed using the pandas .sort_values()
method or the Snowpark .sort()
method, depending on the format of the query results:
# pandas formatwith model.query() as select: person = Person() response = select(person.name, person.age)
response.results.sort_values(by='age', inplace=True)
# Snowpark formatwith model.query(format="snowpark") as select: person = Person() response = select(person.name, person.age)
response.results.sort('age')
SELECT name, age FROM Person ORDER BY age;
Use the bottom()
function to limit the number of results returned by a query:
from relationalai.std import aggregates
with model.query() as select: person = Person() # Limit the results to 10 people. aggregates.bottom(10, person) response = select(person.name, person.age)
SELECT name, age FROM Person LIMIT 10;
bottom()
can also be used to rank results in ascending order and simultaneously limit the number of results:
with model.query() as select: person = Person() # Rank results in ascending order by age and limit to the 10 smallest ages. rank = aggregates.bottom(10, person.age) response = select(rank, person.name, person.age)
WITH ranked_persons AS ( SELECT RANK() OVER (ORDER BY age ASC) AS rank, name, age FROM Person)SELECT rank, name, ageFROM ranked_personsWHERE rank <= 10;
Alternatively, top()
can be used to both rank results in descending order and limit the number of results:
with model.query() as select: person = Person() # Rank results in descending order by age and limit to the top 10 ages. rank = aggregates.top(10, person.age) response = select(rank, person.name, person.age)
WITH ranked_persons AS ( SELECT RANK() OVER (ORDER BY age DESC) AS rank, name, age FROM Person)SELECT rank, name, ageFROM ranked_personsWHERE rank <= 10;
Conditions in rules and queries are added line-by-line and implicitly combined using AND
:
from relationalai.std.dates import date
with model.query() as select: employee = Employee() # Filter employees who started after January 1, 2021 employee.start_date > date(2021, 1, 1) # Filter employees in the Engineering or Research departments employee.department.in_([Department(name="Engineering"), Department(name="Research")]) response = select(employee.id, employee.name)
SELECT e.id, e.nameFROM Employee eJOIN Department d ON e.department_id = d.idWHERE e.start_date > '2021-01-01' AND d.name IN ('Engineering', 'Research');
Filter expressions are created using the following operators:
Operator | Description |
---|---|
== | Equal to |
!= | Not equal to |
> | Greater than |
>= | Greater than or equal to |
< | Less than |
<= | Less than or equal to |
.in_() | In a list of values |
See Filtering Objects by Property Value for more details.
Python’s logical operators, like and
, or
, and not
, expect boolean operands.
However, RAI filter expressions, like employee.start_date > date(2021, 1, 1)
, do not return boolean values, since their truth value is only determined once the query or rule is evaluated.
For this reason, you cannot use Python’s logical operators to combine filter expressions in RAI.
To express or
in RAI, use the model.match(multiple=True)
context manager:
with model.query() as select: employee = Employee() # Filter employees who started before January 1, 2020 or after January 1, 2021 with model.match(multiple=True) as matched: with employee.start_date < date(2020, 1, 1): matched.add(employee) with employee.start_date > date(2021, 1, 1): matched.add(employee) response = select(matched.id, matched.name)
-- model.match(multiple=True) is equivalent to using UNION ALL in SQL to combine-- the results of multiple queries.SELECT id, nameFROM EmployeeWHERE start_date < '2020-01-01'UNION ALLSELECT id, nameFROM EmployeeWHERE start_date > '2021-01-01';
-- Logically, this is equivalent to using OR.SELECT id, nameFROM EmployeeWHERE start_date < '2020-01-01' OR start_date > '2021-01-01';
See Conditional Logic for more information on model.match()
.
You can check for the existence or non-existence of certain conditions or related objects in RAI using model.found()
and model.not_found()
, similar to SQL’s EXISTS
and NOT EXISTS
clauses:
# Get the names of employees who belong to the Research department, and the# names of other departments they belong to.with model.query() as select: employee = Employee() # Check if the employee is in the Research department with model.found(): employee.departments.name == "Research" # Retrieve other departments the employee belongs to other_department = employee.departments other_department.name != "Research" response = select(employee.name, other_department.name)
SELECT e.name, d_other.nameFROM Employee eJOIN EmployeeDepartments ed1 ON e.id = ed1.employee_idJOIN Department d1 ON ed1.department_id = d1.idJOIN EmployeeDepartments ed2 ON e.id = ed2.employee_idJOIN Department d_other ON ed2.department_id = d_other.idWHERE EXISTS ( SELECT 1 FROM EmployeeDepartments ed JOIN Department d ON ed.department_id = d.id WHERE ed.employee_id = e.id AND d.name = 'Research')AND d_other.name != 'Research';
# Get the names of employees who do NOT belong to the Research department, and# the names of departments they belong to.with model.query() as select: employee = Employee() # Check if the employee is NOT in the Research department with model.not_found(): employee.departments.name == "Research" # Retrieve departments the employee belongs to department = employee.departments response = select(employee.name, department.name)
SELECT e.name, d.nameFROM Employee eJOIN EmployeeDepartments ed ON e.id = ed.employee_idJOIN Department d ON ed.department_id = d.idWHERE NOT EXISTS ( SELECT 1 FROM EmployeeDepartments ed2 JOIN Department d2 ON ed2.department_id = d2.id WHERE ed2.employee_id = e.id AND d2.name = 'Research');
Use the functions in the aggregates
module to perform aggregations in queries.
Unlike SQL, where aggregate function operate over each row in a column, RAI aggregate functions operate on the set of values assigned to a property.
If multiple objects have the same value property, it only contributes once to the aggregation.
from relationalai.std import aggregates, alias
with model.query() as select: sale = Sale() # WARNING: The following may not work as you expect in RAI. This sums all # *distinct* amounts across all sales, rather than all amounts. total_sales = aggregates.sum(sale.amount) response = select(alias(total_sales, "total_sales"))
SELECT SUM(DISTINCT amount) AS total_salesFROM Sale;
# To sum all amounts, including duplicates, include the sale variable in the sum function:with model.query() as select: sale = Sale() # Sum the amounts for each sale object. total_sales = aggregates.sum(sale, sale.amount) response = select(alias(total_sales, "total_sales"))
SELECT SUM(amount) AS total_salesFROM Sale;
Passing the sale
object as the first argument to aggregates.sum()
allows RAI to aggregate over (sale, sale.amount)
pairs instead of just the sale.amount
values.
When multiple arguments are provided to an aggregate function like .sum()
, the aggregation is performed on the last argument.
Use the per
parameter to group results in an aggregation:
with model.query() as select: salesperson = Salesperson() sale = salesperson.sales total = aggregates.sum(sale, sale.amount, per=[salesperson]) response = select(salesperson.name, total)
SELECT s.name, SUM(sa.amount) AS totalFROM Salesperson sJOIN Sales sa ON sa.salesperson_id = s.idGROUP BY s.id, s.name;
Additional filters on results of aggregations can be included in the query:
with model.query() as select: salesperson = Salesperson() sale = salesperson.sales count = aggregates.count(sale, per=[salesperson]) count > 10 # Filter salespeople with more than 10 sales response = select(salesperson.name, alias(count, "count"))
SELECT s.name, COUNT(*) AS countFROM Salesperson sJOIN Sales sa ON sa.salesperson_id = s.idGROUP BY s.id, s.nameHAVING COUNT(*) > 10;
Filters are applied in the order they are written in the query and only affect aggregations that follow them:
from relationalai.std import aggregates
with model.query() as select: sale = Sale() # First aggregation: Compute total sales amount across all sales. total_sales = aggregates.sum(sale, sale.amount) # Apply a filter after the first aggregation. sale.amount > 1000 # Second aggregation: Count the number of large sales. count_large_sales = aggregates.count(sale) response = select( alias(total_sales, "total_sales"), alias(count_large_sales, "count_large_sales"), )
-- Compute total sales amount across all salesWITH total_sales AS ( SELECT SUM(amount) AS total_sales FROM Sale),-- Filter sales with amount > 1000 and count themcount_large_sales AS ( SELECT COUNT(*) AS count_large_sales FROM Sale WHERE amount > 1000)-- Select both resultsSELECT total_sales.total_sales, count_large_sales.count_large_salesFROM total_sales, count_large_sales;
See Using Aggregate Functions for more details.
You can implement conditional logic in RAI using the model.match()
and model.case()
context managers, similar to SQL’s CASE
statement:
with model.query() as select: student = Student() with model.match() as letter_grade: # letter_grade is a variable with student.grade >= 90: letter_grade.add("A") # Set the value of letter_grade to "A" with student.grade >= 80: letter_grade.add("B") with student.grade >= 70: letter_grade.add("C") with student.grade >= 60: letter_grade.add("D") # Default case if none of the above conditions are met. with model.case(): letter_grade.add("F") response = select(student.name, alias(letter_grade, "letter_grade"))
SELECT name, CASE WHEN grade >= 90 THEN 'A' WHEN grade >= 80 THEN 'B' WHEN grade >= 70 THEN 'C' WHEN grade >= 60 THEN 'D' ELSE 'F' END AS letter_gradeFROM Student;
For each object in a rule or query, model.match()
applies the first condition that evaluates to True
and ignores the rest.
You can use model.match(multiple=True)
to apply multiple conditions.
In a query, this works like a UNION ALL
in SQL:
with model.query() as select: person = Person() # Assign status based on age group. multiple=True allows multiple matches, # so people who are over 60 will have status set to both "Senior" and "Adult". with model.match(multiple=True) as status: with model.case(): person.age >= 60 status.add("Senior") with model.case(): person.age >= 18 status.add("Adult") with model.case(): person.age < 18 status.add("Minor") response = select(person.name, status)
SELECT name, 'Senior' AS statusFROM PersonWHERE age >= 60UNION ALLSELECT name, 'Adult' AS statusFROM PersonWHERE age >= 18UNION ALLSELECT name, 'Minor' AS statusFROM PersonWHERE age < 18;
In a rule, model.match()
can be used to conditionally set types and properties of objects:
with model.rule(): person = Person() with model.match(): # If the person's age is at least 18, assign them to the Adult type. with person.age >= 18: person.set(Adult) # Otherwise, assign them to the Minor type. with model.case(): person.set(Minor)
-- Insert persons aged 18 or older into the Adult tableINSERT INTO Adult (person_id)SELECT idFROM PersonWHERE age >= 18;
-- Insert persons not in the Adult table into the Minor tableINSERT INTO Minor (person_id)SELECT p.idFROM Person pWHERE NOT EXISTS ( SELECT 1 FROM Adult a WHERE a.person_id = p.id);
# Using multiple=Truewith model.rule(): person = Person() with model.match(multiple=True): with person.age >= 60: person.set(Senior) with person.age >= 18: person.set(Adult) with person.age < 18: person.set(Minor)
-- Insert 'Senior' classificationsINSERT INTO Senior (person_id)SELECT idFROM PersonWHERE age >= 60;
-- Insert 'Adult' classificationsINSERT INTO Adult (person_id)SELECT idFROM PersonWHERE age >= 18;
-- Insert 'Minor' classificationsINSERT INTO Minor (person_id)SELECT idFROM PersonWHERE age < 18;
See Expressing if
Using model.case()
and Expressing if-else
Using model.match()
for more examples of conditional logic in RAI.
RAI does not have a NULL
value.
Instead, property values are either present or missing.
You can check for missing values and provide default values in RAI, similar to SQL’s IS NULL
, IS NOT NULL
, and COALESCE
.
Use the InstanceProperty.has_value()
method to check that a property is not missing:
with model.query() as select: person = Person() person.age.has_value() response = select(person.name)
SELECT name FROM Person WHERE age IS NOT NULL;
Use model.not_found()
with InstanceProperty.has_value()
to check for missing values:
with model.query() as select: person = Person() with model.not_found(): person.age.has_value() response = select(person.name)
SELECT name FROM Person WHERE age IS NULL;
Use the InstanceProperty.or_()
method to provide a default value for a property when it is missing:
with model.query() as select: person = Person() response = select(person.name, person.age.or_(0))
SELECT name, COALESCE(age, 0) AS age FROM Person;
You can export Python functions as SQL stored procedures using the @model.export()
decorator.
from typing import Tuple
@model.export("<db>.<schema>")def get_friends_of_person(id: int) -> Tuple[str]: person = Person(id=id) friend = person.friends return friend.name
-- NOTE: Replace <db> and <schema> with the names of the database and schema where-- the stored procedure will be created. The stored procedure name will be the same-- as the function name.CREATE FUNCTION get_friends_of_person(id INT)RETURNS TABLE(name VARCHAR)ASBEGIN -- Function logic hereEND;
See Exporting SQL Stored Procedures for more details.