SELECT

SELECT Statement #

[WITH cte_name ...]
SELECT [DISTINCT [ON (expr, ...)]] expr [AS alias] [, ...]
[[FROM [schema.]relation | cte | (expr | SELECT ...) [, ...]
 [JOIN [schema.]relation | cte | (expr | SELECT ...) ON (expr) [, ...] | USING (column)]
 [USE INDEX (name)]]
[WHERE expr]
[GROUP BY column_order | alias | expr[, ...] [HAVING expr]]
[ORDER BY column_order | alias | expr [ASC | DESC] [, ...]]
[LIMIT expr] [OFFSET expr]
[FORMAT type]

Retrieve rows from a table, CTE result, expression, or subquery.

If the object schema is not defined, the object will be searched in the public schema.

Select operation is distributed and transactional and will be executed on one or more backends if tables are involved. Amelie is designed to split the work between partitions and backend workers (PUSHDOWN).

Operations such as GROUP BY, ORDER BY and JOIN will be executed individually per backend in parallel. After the successful execution, the results of each computation are merged together, processed, and returned.

Currently, only INNER JOIN is supported and will be extended in future releases. Joining partitioned tables directly has some limitations.

Select can be part of a multi-statement transaction and executed inside expressions (as subquery).

Amelie generates an optimized parallel plan for executing multi-statement transactions and CTE. It combines requests (pipeline) to reduce wait times and speed up the execution of non-dependable CTE statements.

Amelie is designed for short ACID transactions and fast real-time analytics. It is not intended for long, complex, multi-statement transactions or queries that generate large amounts of data. There are configurable limits for transaction size.

The FORMAT clause can be used to specify the format of the result.

Index and Keys matching #

By default, Amelie will try to use the most suitable index by matching keys by analyzing the WHERE expression. It tries to identify whether the operation requires a range scan (start, stop conditions) or point lookup (by key). This logic applies to the UPDATE and DELETE operations as well.

It is possible to force a query to use a specific index by using the USE INDEX clause for each table target. Primary index keys are used for partitioning, where the primary or secondary index can be used by backends for data scan inside partitions.

Distributed JOIN and Shared Tables #

By default, all tables are PARTITIONED and distributed. Partitions will be created on each backend worker for parallel access or modification.

Due to the distributed database nature, the way partitioned tables are operated has some limitations. The primary goal is to eliminate distributed round-trips to the backend workers and ideally execute all transaction statements in one goal.

Partitioned tables cannot be directly joined with other partitioned tables, and the same limitation applies to subqueries. Instead, the transaction must use CTE to achieve the same effect. Amelie treats CTE as separate statements to combine and execute non-dependable statements in one operation on backend workers. The query planner tries to rewrite queries using CTE whenever it can.

Another efficient way to JOIN partitioned tables is by using shared tables.

SHARED tables are not partitioned (single partition) and are available for concurrent direct read access from any backend worker.

The purpose of shared tables is to support efficient Parallel JOIN operations with other partitioned tables, where shared tables are used as dictionary tables. However, frequently updating a shared table is less efficient since it requires coordination and exclusive access.

Shared tables can be joined with other shared tables or CTE results without limitations. However, currently, only one partitioned table can be directly joined with shared tables.

Subqueries #

Currently, subqueries can be made to shared tables, CTE results, and expressions. Subqueries to other partitioned tables are not supported directly. Instead, CTE must be used. It will guarantee that a query to the partitioned table is executed only once and not nested.


-- get the number of hits for the last hour for each device
create table example (
  time      timestamp,
  device_id int,
  primary key(time, device_id)
);

insert into example values ('2024-12-12 13:53:52.712025', 1);
insert into example values ('2024-12-12 14:20:52.712025', 1);
insert into example values ('2024-12-12 14:25:52.712025', 2);
insert into example values ('2024-12-12 14:27:52.712025', 1);

-- do parallel GROUP BY and ORDER BY on each backend
select device_id, count(*) as hits
from example
where time >= timestamp '2024-12-12 14:30:00' - interval '1 hour'
group by 1
order by 1
format 'json-obj';
[{"device_id": 1, "hits": 3}, {"device_id": 2, "hits": 1}]
-- using generated stored and resolved columns to
-- group inserts by 1 hour per device_id and aggregate hits
create table example (
  time      timestamp as ( time::date_bin(interval '1 hour') ) stored,
  device_id int,
  hits      int default 1 as ( hits + 1 ) resolved,
  primary key(time, device_id)
);

insert into example(time, device_id) values ('2024-12-12 13:53:52.712025', 1);
insert into example(time, device_id) values ('2024-12-12 14:20:52.712025', 1);
insert into example(time, device_id) values ('2024-12-12 14:25:52.712025', 2);
insert into example(time, device_id) values ('2024-12-12 14:27:52.712025', 1);

-- GROUP BY is not needed, since rows are already aggregated
select time, device_id, hits
from example
order by 1
format 'json-obj-pretty';
[{
  "time": "2024-12-12 13:00:00+02",
  "device_id": 1,
  "hits": 1
}, {
  "time": "2024-12-12 14:00:00+02",
  "device_id": 2,
  "hits": 1
}, {
  "time": "2024-12-12 14:00:00+02",
  "device_id": 1,
  "hits": 2
}]
-- similarity search using vector
create table example (id serial primary key, embedding vector);
insert into example (embedding) values ([3,2,0,1,4]);
insert into example (embedding) values ([2,2,0,1,3]);
insert into example (embedding) values ([1,3,0,1,4]);
select * from example;
[[0, [3, 2, 0, 1, 4]], [1, [2, 2, 0, 1, 3]], [2, [1, 3, 0, 1, 4]]]

-- order rows by similarity
select id, embedding::cos_distance(vector [1,3,1,2,0])
from example
order by 2 desc;
[[1, 0.481455], [2, 0.403715], [1, 0.391419]]

-- find the most alike row
select id from example
order by embedding::cos_distance(vector [1,3,1,2,0]) desc
limit 1;
[0]
-- JSON expressions
select {"at": current_timestamp, "id": system.config().uuid};
[{
  "at": "2024-09-26 16:14:00.722393+03",
  "id": "a74fbf39-cc9d-314e-a33e-3aa47559ffe5"
}]

select {"total": select count(*) from example};
[{
  "total": 3
}]
-- distributed JOIN using shared dictionary table
create table collection(id int primary key);
create shared table dict(id int primary key) with (type = 'hash');

insert into dict values (1), (3);
insert into collection values (1), (2), (3);

select c.* from collection c join dict on (dict.id = c.id);
[[1], [3]]
-- distributed JOIN using CTE
create table t1 (id int primary key);
create table t2 (id int primary key);

insert into t1 values (1), (3);
insert into t2 values (1), (2), (3);

-- t1_result will be passed down for parallel JOIN with t2
with t1_result (id) as (
    select * from t1
) select t2.* from t2 join t1_result on (t2.id = t1_result.id);
[[1], [3]]