I am attempting to build an index that can be used for keyset pagination and statistcis. I am basing this work
on several articles including Database Pagination: Using OFFSET and Keyset in N1QL
The index:
CREATE INDEX `by_type_and_size_sg_idx_v1`
ON `sync_gateway_sw1`(`type`, META().id, `subType`, `changeType`, ENCODED_SIZE(self))
WHERE (`type` IS VALUED)
AND (NOT IFMISSINGORNULL(`softDelete`, FALSE))
AND (NOT IFMISSINGORNULL(`_deleted`, FALSE))
AND (META().`id` NOT LIKE "_sync:%")
the query:
SELECT meta(sg).`id`
FROM `sync_gateway_sw1` AS sg
WHERE (sg.`type` = "user")
AND (NOT IFMISSINGORNULL(sg.`softDelete`, FALSE))
AND (NOT IFMISSINGORNULL(sg.`_deleted`, FALSE))
AND (META(sg).`id` NOT LIKE "_sync:%")
ORDER BY sg.`type`, META(sg).`id`
OFFSET 0
LIMIT 50
To me, everything seems to satisfy the rules for pushing offset/limit down to the indexer (single keyspace, single span, exact predicate, order by key order matches, etc.), but obviously I’m wrong. No matter how I play, I can’t seem to get this partial index to push down or get rid of what seem to be extra steps (i.e. re-filtering).
The full query plan is below. Note that when I remove the last three filters from the index/query the limit does get pushed down, but I still end up with a filter, project, and limit step (again, even though the limit gets pushed down). So that makes it feel like the filters for the partial are a factor with the original query above.
My questions are:
- What prevents original query from getting pushed down?
- Why do I have the filter step in the query plans (why reapplied)?
- Is there a way to get this working with the indexer doing the lifting and have it set up for keyset pagination?
While I want to make this work, I’m more interested in truly understanding why it doesn’t.
The full query plan is below:
{
"#operator": "Sequence",
"~children": [
{
"#operator": "Sequence",
"~children": [
{
"#operator": "IndexScan3",
"as": "sg",
"covers": [
"cover ((`sg`.`type`))",
"cover ((meta(`sg`).`id`))",
"cover ((`sg`.`subType`))",
"cover ((`sg`.`changeType`))",
"cover (encoded_size(`sg`))",
"cover ((meta(`sg`).`id`))"
],
"filter_covers": {
"cover (((`sg`.`type`) is valued))": true,
"cover ((not ((meta(`sg`).`id`) like \"_sync:%\")))": true,
"cover ((not ifmissingornull((`sg`.`_deleted`), false)))": true,
"cover ((not ifmissingornull((`sg`.`softDelete`), false)))": true
},
"index": "by_type_and_size_sg_idx_v1",
"index_id": "49f2f5e4012a8f69",
"index_order": [
{
"keypos": 0
},
{
"keypos": 1
},
{
"keypos": 2
},
{
"keypos": 3
},
{
"keypos": 4
}
],
"index_projection": {
"entry_keys": [
0,
2,
3,
4
],
"primary_key": true
},
"keyspace": "sync_gateway_sw1",
"namespace": "default",
"spans": [
{
"range": [
{
"high": "\"location\"",
"inclusion": 3,
"low": "\"location\""
},
{
"inclusion": 0,
"low": "null"
}
]
}
],
"using": "gsi"
},
{
"#operator": "Parallel",
"maxParallelism": 1,
"~child": {
"#operator": "Sequence",
"~children": [
{
"#operator": "Filter",
"condition": "((((cover ((`sg`.`type`)) = \"location\") and cover ((not ifmissingornull((`sg`.`softDelete`), false)))) and cover ((not ifmissingornull((`sg`.`_deleted`), false)))) and cover ((not ((meta(`sg`).`id`) like \"_sync:%\"))))"
},
{
"#operator": "InitialProject",
"result_terms": [
{
"expr": "cover ((meta(`sg`).`id`))"
}
]
},
{
"#operator": "FinalProject"
}
]
}
}
]
},
{
"#operator": "Limit",
"expr": "50"
}
]
}