Prepare Application for Migration¶
Once the distribution key is present on all appropriate tables, the application needs to include it in queries.
Add distribution key to queries¶
To execute queries efficiently for a specific tenant, Citus needs to route them to the appropriate node and run them there. Thus every query must identify which tenant it involves. For simple select, update, and delete queries this means that the where clause must filter by tenant id.
- Application code and any other ingestion processes that write to the tables should be updated to include the new columns.
- Running the application test suite against the modified schema on Citus is a good way to determine which areas of the code need to be modified.
- It’s a good idea to enable database logging. The logs can help uncover stray cross-shard queries in a multi-tenant app that should be converted to per-tenant queries.
There are helper libraries for a number of popular application frameworks that make it easy to include a tenant id in queries.
- Ruby on Rails migration - uses the activerecord-multi-tenant Ruby gem
- Django migration - uses the django-multitenant Python library
- ASP.NET migration - uses the 3rd party SAASkit
- Java Hibernate - a blog post about scoping queries to tenants
It’s possible to use the libraries for database writes first (including data ingestion), and later for read queries. The activerecord-multi-tenant gem for instance has a write-only mode that will modify only the write queries.
If you’re using a different ORM than those above, or doing multi-tenant queries more directly in SQL, follow these general principles. We’ll use our earlier example of the ecommerce application.
Suppose we want to get the details for an order. Distributed queries that filter on the tenant id run most efficiently in multi-tenant apps, so the change below makes the query faster (while both queries return the same results):
-- before SELECT * FROM orders WHERE order_id = 123; -- after SELECT * FROM orders WHERE order_id = 123 AND store_id = 42; -- <== added
The tenant id column is not just beneficial – but critical – for insert statements. Inserts must include a value for the tenant id column or else Citus will be unable to route the data to the correct shard and will raise an error.
Finally, when joining tables make sure to filter by tenant id too. For instance here is how to inspect how many “awesome wool pants” a given store has sold:
-- One way is to include store_id in the join and also -- filter by it in one of the queries SELECT sum(l.quantity) FROM line_items l INNER JOIN products p ON l.product_id = p.product_id AND l.store_id = p.store_id WHERE p.name='Awesome Wool Pants' AND l.store_id='8c69aa0d-3f13-4440-86ca-443566c1fc75' -- Equivalently you omit store_id from the join condition -- but filter both tables by it. This may be useful if -- building the query in an ORM SELECT sum(l.quantity) FROM line_items l INNER JOIN products p ON l.product_id = p.product_id WHERE p.name='Awesome Wool Pants' AND l.store_id='8c69aa0d-3f13-4440-86ca-443566c1fc75' AND p.store_id='8c69aa0d-3f13-4440-86ca-443566c1fc75'
Check for cross-node traffic¶
With large and complex application code-bases, certain queries generated by the application can often be overlooked, and thus won’t have a tenant_id filter on them. Citus’ parallel executor will still execute these queries successfully, and so, during testing, these queries remain hidden since the application still works fine. However, if a query doesn’t contain the tenant_id filter, Citus’ executor will hit every shard in parallel, but only one will return any data. This consumes resources needlessly, and may exhibit itself as a problem only when one moves to a higher-throughput production environment.
To prevent encountering such issues only after launching in production, one can set a config value to log queries which hit more than one shard. In a properly configured and migrated multi-tenant application, each query should only hit one shard at a time.
During testing, one can configure the following:
-- adjust for your own database's name of course ALTER DATABASE citus SET citus.multi_task_query_log_level = 'error';
Citus will then error out if it encounters queries which are going to hit more than one shard. Erroring out during testing allows the application developer to find and migrate such queries.
During a production launch, one can configure the same setting to log, instead of error out:
ALTER DATABASE citus SET citus.multi_task_query_log_level = 'log';
The configuration parameter section has more info on supported values for this setting.