vectrojs 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +61 -54
- package/mcp/server.mjs +1 -1
- package/package.json +1 -1
- package/src/table.mjs +71 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
A performance release. No API changes.
|
|
6
|
+
|
|
7
|
+
**Faster group-by**
|
|
8
|
+
- SQL `SUM` no longer computes a hidden count of non-missing values when the
|
|
9
|
+
column has none missing, and sum, mean, min and max skip the per-row
|
|
10
|
+
missing-value check on such columns.
|
|
11
|
+
- On many threads, a group-by remembers its key range and merges the
|
|
12
|
+
threads' partial results with a WebAssembly kernel instead of a JavaScript
|
|
13
|
+
loop. This also removes the 31-thread limit for queries with many groups.
|
|
14
|
+
- Measured on 2M orders (`SELECT product_id, sum(amount) ... GROUP BY 1`,
|
|
15
|
+
`compare/bench-2m-groupby.mjs`, numbers in `compare/results.json`): with
|
|
16
|
+
1,000 products, 3.0 ms on 1 thread (DuckDB 2.0: 6.8 ms) and 2.3 ms on 8
|
|
17
|
+
threads (DuckDB 2.0: 2.4 ms). Before this release Vectro was 1.9x slower
|
|
18
|
+
than DuckDB on 8 threads in this case; it is now level or faster at every
|
|
19
|
+
size from 100 to 100,000 products.
|
|
20
|
+
|
|
3
21
|
## 0.1.0 (first release)
|
|
4
22
|
|
|
5
23
|
The first public release. Everything below is new.
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ and compiles each query, filter included, into one SIMD WebAssembly loop, then
|
|
|
5
5
|
runs it on every CPU core. It runs in Node, in browsers (any page) and on
|
|
6
6
|
Cloudflare Workers, with no native addon, compiler or server.
|
|
7
7
|
|
|
8
|
-
> Version 0.1.
|
|
8
|
+
> Version 0.1.1. Node 20+ and modern browsers. MIT license.
|
|
9
9
|
|
|
10
10
|
**Website and docs: [vectrojs.dev](https://vectrojs.dev)**. [Quick start](https://vectrojs.dev/docs/quick-start) · [API reference](https://vectrojs.dev/api) · [Race it in your browser](https://vectrojs.dev/live) · [Benchmarks](https://vectrojs.dev/benchmarks)
|
|
11
11
|
|
|
@@ -82,10 +82,10 @@ What changed: [CHANGELOG.md](CHANGELOG.md). Problems and ideas:
|
|
|
82
82
|
|
|
83
83
|
| Keys | Vectro | DuckDB |
|
|
84
84
|
|---|---|---|
|
|
85
|
-
| region + day (2,920 pairs) |
|
|
86
|
-
| price (100K amounts) |
|
|
87
|
-
| account (1M big IDs) |
|
|
88
|
-
| account + region (5.7M pairs) |
|
|
85
|
+
| region + day (2,920 pairs) | 6 ms | 39 ms |
|
|
86
|
+
| price (100K amounts) | 27 ms | 49 ms |
|
|
87
|
+
| account (1M big IDs) | 36 ms | 88 ms |
|
|
88
|
+
| account + region (5.7M pairs) | 50 ms | 139 ms |
|
|
89
89
|
|
|
90
90
|
Vectro's groups also come back sorted by key; DuckDB's come back unsorted.
|
|
91
91
|
- **Missing values** are NaN for numbers and `''` for text. `sum`, `mean`,
|
|
@@ -124,9 +124,9 @@ What changed: [CHANGELOG.md](CHANGELOG.md). Problems and ideas:
|
|
|
124
124
|
by the hash table first, or, with many of them, sorted by key range on
|
|
125
125
|
every thread (as a group-by with many groups) and counted in runs; values
|
|
126
126
|
a whole step apart (IDs like 1e9 + 7919k) get the byte map too. On 10M rows
|
|
127
|
-
vs DuckDB (`compare/bench-distinct.mjs`): 365 days
|
|
128
|
-
day
|
|
129
|
-
step: 24 ms vs 46); accounts per region (5.7M pairs)
|
|
127
|
+
vs DuckDB (`compare/bench-distinct.mjs`): 365 days 5 ms vs 5; regions per
|
|
128
|
+
day 9 ms vs 36; 1M accounts 11 ms vs 51 (1M random IDs up to 2^40, no common
|
|
129
|
+
step, from an earlier run: 24 ms vs 46); accounts per region (5.7M pairs) 35 ms vs 103.
|
|
130
130
|
- **Compressed columns:** `table.compress()` (or `engine.table(cols, { compress:
|
|
131
131
|
true })`, `parseCSV(text, { compress: true })`) stores each number and
|
|
132
132
|
category column in 1, 2 or 4 bytes per row when every value comes back
|
|
@@ -198,11 +198,11 @@ What changed: [CHANGELOG.md](CHANGELOG.md). Problems and ideas:
|
|
|
198
198
|
dates, and comparing text with `<` / `>`. Each function runs once per
|
|
199
199
|
distinct value, not per row (a category column has few labels), and the
|
|
200
200
|
answer is a category column again, so it groups, sorts and joins like any
|
|
201
|
-
other: `GROUP BY UPPER(region)` on 10M rows
|
|
202
|
-
'-' || qty` as a key 85 ms (
|
|
203
|
-
are rounded to the month first), ILIKE on a column
|
|
201
|
+
other: `GROUP BY UPPER(region)` on 10M rows 31 ms (DuckDB 47), `region ||
|
|
202
|
+
'-' || qty` as a key 85 ms (106), `STRFTIME(ts, '%Y-%m')` 51 ms (70; times
|
|
203
|
+
are rounded to the month first), ILIKE on a column 30 ms (54). A CASE whose
|
|
204
204
|
results are all quoted labels is worked out as label codes in the fused
|
|
205
|
-
pass (CASE text + group
|
|
205
|
+
pass (CASE text + group 6.2 ms vs 25). Missing text
|
|
206
206
|
is `''`, as everywhere in Vectro, and positions count characters from 1 as
|
|
207
207
|
in DuckDB.
|
|
208
208
|
|
|
@@ -210,7 +210,7 @@ What changed: [CHANGELOG.md](CHANGELOG.md). Problems and ideas:
|
|
|
210
210
|
rows (FULL: plus the left rows without a match) followed by the right rows
|
|
211
211
|
whose keys no left row has. The join marks which right rows it matched as
|
|
212
212
|
it probes, so the rows without a match come from the same pass (RIGHT JOIN
|
|
213
|
-
+ group
|
|
213
|
+
+ group 30 vs 64 ms; FULL JOIN + count 19 vs 29 ms).
|
|
214
214
|
|
|
215
215
|
`compare/check-sql.mjs` runs 40 such queries (CTEs, set operations,
|
|
216
216
|
correlated subqueries, windows, percentiles, text functions on awkward
|
|
@@ -219,15 +219,15 @@ What changed: [CHANGELOG.md](CHANGELOG.md). Problems and ideas:
|
|
|
219
219
|
correlation on anything but equalities, MIN / MAX of text, and STRING_AGG.
|
|
220
220
|
The MCP server has
|
|
221
221
|
a `sql` tool. The same SQL text vs DuckDB on 10M orders + 100K customers
|
|
222
|
-
(`compare/bench-sql.mjs`, answers checked,
|
|
223
|
-
20 queries: filtered totals 3
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
correlated subquery 16.
|
|
228
|
-
|
|
229
|
-
vs
|
|
230
|
-
|
|
222
|
+
(`compare/bench-sql.mjs`, answers checked, best of 5): Vectro is faster on all
|
|
223
|
+
20 queries (1.3–6.0x): filtered totals 4.3 vs 10.1 ms, group by region 7.4 vs
|
|
224
|
+
36.8 ms, join + filter + group 19.8 vs 32.8 ms, top 10 customers 13.1 vs
|
|
225
|
+
50.3 ms, distinct customers per month 34.2 vs 82.5 ms, top 20 rows 9.1 vs
|
|
226
|
+
25.3 ms, CASE inside SUM 6.6 vs 39.6 ms, IN (subquery) 23.4 vs 29.7 ms,
|
|
227
|
+
correlated subquery 16.8 vs 61.4 ms, top 3 per region with RANK 38 vs
|
|
228
|
+
139 ms, ROW_NUMBER over all rows 129 vs 221 ms, running total by day 9.8
|
|
229
|
+
vs 17.3 ms, MEDIAN per region 39.1 vs 83.9 ms, FULL JOIN + count 19.3 vs
|
|
230
|
+
29.3 ms. A filter like `rn <= 3` on ROW_NUMBER or RANK keeps
|
|
231
231
|
only the rows that can rank that high before sorting (picked on every
|
|
232
232
|
thread); percentiles of a few partitions count values in buckets on every
|
|
233
233
|
thread and select only within the wanted buckets. Window functions and
|
|
@@ -257,10 +257,10 @@ What changed: [CHANGELOG.md](CHANGELOG.md). Problems and ideas:
|
|
|
257
257
|
|
|
258
258
|
| Join | Vectro | DuckDB |
|
|
259
259
|
|---|---|---|
|
|
260
|
-
| × 100K customers |
|
|
261
|
-
| × 1M accounts |
|
|
262
|
-
| left join, half the keys match |
|
|
263
|
-
| two keys: region + day (2,920 rows) |
|
|
260
|
+
| × 100K customers | 77 ms | 210 ms |
|
|
261
|
+
| × 1M accounts | 151 ms | 251 ms |
|
|
262
|
+
| left join, half the keys match | 82 ms | 206 ms |
|
|
263
|
+
| two keys: region + day (2,920 rows) | 61 ms | 189 ms |
|
|
264
264
|
- **Text functions** on category columns: `startsWith(col, 'x')`,
|
|
265
265
|
`endsWith`, `contains`, `in(col, 'a', 'b')` and `like(col, 'EU-%')`. They run
|
|
266
266
|
once per distinct label (not per row) and become a few range checks on the
|
|
@@ -282,8 +282,8 @@ What changed: [CHANGELOG.md](CHANGELOG.md). Problems and ideas:
|
|
|
282
282
|
of 50,000+ rows run on every core: a parallel radix sort (11-bit digits,
|
|
283
283
|
digits that never change are skipped, whole numbers sort as offsets), then
|
|
284
284
|
every column is gathered in the new order on every core. Sorting 10M rows
|
|
285
|
-
by `-price` takes ~
|
|
286
|
-
sort: ~4.
|
|
285
|
+
by `-price` takes ~117 ms (one thread: ~350 ms; a JavaScript comparator
|
|
286
|
+
sort: ~4.1 s).
|
|
287
287
|
- **Async API:** every query has an `…Async` version that runs on the worker
|
|
288
288
|
threads while your server or page keeps working.
|
|
289
289
|
- **Expressions** are a small, safe language: numbers, columns, `+ - * /`,
|
|
@@ -383,25 +383,27 @@ formulas exact in both, answers compared; best of 3):
|
|
|
383
383
|
|
|
384
384
|
| Query | 100M rows: Vectro | DuckDB | 1 billion rows: Vectro | DuckDB |
|
|
385
385
|
|---|---|---|---|---|
|
|
386
|
-
| scan everything (count, revenue) |
|
|
387
|
-
| filter: region and quantity |
|
|
388
|
-
| the last day (1 chunk read) | 3 ms | 1 ms | 3 ms |
|
|
389
|
-
| group by region |
|
|
390
|
-
| top 10 of 1M customers |
|
|
391
|
-
| top 10 rows by price |
|
|
392
|
-
| join 1M customers, group by tier |
|
|
393
|
-
| distinct customers per region |
|
|
386
|
+
| scan everything (count, revenue) | 70 ms | 73 ms | 478 ms | 662 ms |
|
|
387
|
+
| filter: region and quantity | 100 ms | 87 ms | 691 ms | 729 ms |
|
|
388
|
+
| the last day (1 chunk read) | 3 ms | 1 ms | 3 ms | 4 ms |
|
|
389
|
+
| group by region | 142 ms | 149 ms | 1.05 s | 1.25 s |
|
|
390
|
+
| top 10 of 1M customers | 529 ms | 637 ms | 4.39 s | 4.50 s |
|
|
391
|
+
| top 10 rows by price | 15 ms | 100 ms | 11 ms | 843 ms |
|
|
392
|
+
| join 1M customers, group by tier | 126 ms | 85 ms | 880 ms | 627 ms |
|
|
393
|
+
| distinct customers per region | 268 ms | 650 ms | 2.43 s | 6.84 s |
|
|
394
394
|
| file on disk | 0.5 GB | 0.5 GB | 5.1 GB | 5.4 GB |
|
|
395
|
-
| peak memory (RSS) |
|
|
395
|
+
| peak memory (RSS) | 344 MB | 2.9 GB | **0.37 GB** | 6.9 GB |
|
|
396
396
|
|
|
397
397
|
A billion rows would take 37 GB as plain columns in memory (this laptop has
|
|
398
|
-
16 GB); Vectro's engine never used more than 163 MB for them.
|
|
399
|
-
or faster on 7 of the 8 queries
|
|
398
|
+
16 GB); Vectro's engine never used more than 163 MB for them. At a billion
|
|
399
|
+
rows Vectro is level or faster on 7 of the 8 queries (only the join is
|
|
400
|
+
slower, 1.4x); at 100M rows it is slower on three: the filter (1.1x), the
|
|
401
|
+
last day (3 vs 1 ms) and the join (1.5x). Top rows read only the blocks of rows whose
|
|
400
402
|
stored min / max can still beat the rows kept so far (here 1 chunk of 239),
|
|
401
403
|
so they take the same few milliseconds at any size. A join whose right side
|
|
402
404
|
has unique whole-number keys and a GROUP BY on its columns looks each left
|
|
403
405
|
key's group up in one array instead of joining rows; DuckDB is still faster
|
|
404
|
-
there (1.
|
|
406
|
+
there (1.4x at a billion rows, 1.5x at 100M).
|
|
405
407
|
|
|
406
408
|
## SQL over HTTP: Excel, Power BI, Google Sheets, pandas
|
|
407
409
|
|
|
@@ -589,9 +591,9 @@ no index to build or tune, results identical to brute force.
|
|
|
589
591
|
|
|
590
592
|
| Embeddings | Vectro | Plain JS loop (Float32Array) |
|
|
591
593
|
|---|---|---|
|
|
592
|
-
| 100,000 (154 MB) |
|
|
593
|
-
| 200,000 (307 MB) | 13.
|
|
594
|
-
| 500,000 (768 MB) | 22.8 ms | 179.3 ms (7.9×) |
|
|
594
|
+
| 100,000 (154 MB) | 9.3 ms | 36.6 ms (3.9×) |
|
|
595
|
+
| 200,000 (307 MB) | 13.6 ms | 72.6 ms (5.3×) |
|
|
596
|
+
| 500,000 (768 MB), an earlier run | 22.8 ms | 179.3 ms (7.9×) |
|
|
595
597
|
|
|
596
598
|
With a `SplitEngine`, every worker searches its own rows in one round trip
|
|
597
599
|
and the page keeps the best k. In Chrome (`examples/check/vectors.html?n=200000`),
|
|
@@ -763,25 +765,30 @@ iPhone) and Cloudflare Workers.
|
|
|
763
765
|
|
|
764
766
|
| Query | Vectro | DuckDB 2.0 | SQLite* | Arquero |
|
|
765
767
|
|---|---|---|---|---|
|
|
766
|
-
| Filtered revenue | 3.
|
|
767
|
-
| Dashboard, 4 aggregates |
|
|
768
|
-
| Group by region (8 groups) |
|
|
769
|
-
| Group by customer (100K groups) |
|
|
770
|
-
| Filter into a new table |
|
|
771
|
-
| New column | 3.
|
|
768
|
+
| Filtered revenue | 3.2 ms | 10.1 ms | 772 ms | 74 ms |
|
|
769
|
+
| Dashboard, 4 aggregates | 4.1 ms | 23.9 ms | 359 ms | 171 ms |
|
|
770
|
+
| Group by region (8 groups) | 6.5 ms | 39.7 ms | 2.59 s | 1.00 s |
|
|
771
|
+
| Group by customer (100K groups) | 8.1 ms | 13.2 ms | 3.90 s | 772 ms |
|
|
772
|
+
| Filter into a new table | 6.4 ms | 55.7 ms | 540 ms | 124 ms |
|
|
773
|
+
| New column | 3.7 ms | 32.3 ms | 1.50 s | 125 ms |
|
|
772
774
|
|
|
773
775
|
Best of 5, each system using all 8 cores (Arquero is single-threaded), the
|
|
774
|
-
same answers from every system.
|
|
775
|
-
(
|
|
776
|
+
same answers from every system. Vectro is 1.6–8.7x faster than DuckDB here
|
|
777
|
+
(3–9x on five of the six, 1.6x with 100K groups). \*SQLite from a separate
|
|
778
|
+
run (`npm run bench:sqlite`, best of 3).
|
|
776
779
|
|
|
777
780
|
Run them yourself: `cd compare && npm install && npm run bench`. Timings vary
|
|
778
781
|
with the machine's state (battery, background load), so compare systems in the
|
|
779
|
-
same run.
|
|
782
|
+
same run. The numbers in this README were measured on 1 November 2026
|
|
783
|
+
(commit b2a3033). These runs shared the laptop with other jobs (load average 3–8 for
|
|
784
|
+
the in-memory runs, up to 15 for the billion-row run), and ratios move by up
|
|
785
|
+
to about ±30% between runs. More comparisons in `compare/`: SQL (`bench-sql.mjs`: Vectro faster
|
|
780
786
|
on all 20 queries), joins (`bench-join.mjs`: faster on all 4), group-by
|
|
781
|
-
(`bench-groupby.mjs`: faster on all 4, 2.
|
|
787
|
+
(`bench-groupby.mjs`: faster on all 4, 2.4–2.8x with millions of groups),
|
|
782
788
|
distinct counts (`bench-distinct.mjs`: level or faster on all 4). On files
|
|
783
789
|
on disk (`bench-dataset.mjs`) DuckDB is still faster at joins with a GROUP
|
|
784
|
-
BY on the joined table (1.
|
|
790
|
+
BY on the joined table (1.4–1.5x), and at 100M rows also at the filter (1.1x)
|
|
791
|
+
and the last-day lookup (3 vs 1 ms).
|
|
785
792
|
|
|
786
793
|
## Layout
|
|
787
794
|
|
package/mcp/server.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { createInterface } from 'node:readline';
|
|
|
14
14
|
import { Engine, sql } from '../src/node.mjs';
|
|
15
15
|
import { LoadError, safePath as safePathIn, loadFile, columnsOf, kindOf } from '../src/serve/catalog.mjs';
|
|
16
16
|
|
|
17
|
-
const VERSION = '0.1.
|
|
17
|
+
const VERSION = '0.1.1';
|
|
18
18
|
const PROTOCOLS = ['2025-06-18', '2025-03-26', '2024-11-05'];
|
|
19
19
|
const MAX_ROWS = 500, DEFAULT_ROWS = 50;
|
|
20
20
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vectrojs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Multi-core analytics engine for JavaScript: columnar tables, SQL and fused SIMD WebAssembly kernels for Node, browsers and edge runtimes, with no native code. Serves tables to Power BI, Tableau, Metabase, Grafana, pandas and Polars.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/table.mjs
CHANGED
|
@@ -31,6 +31,10 @@ const IDENTITY = { sum: 0, count: 0, mean: 0, min: Infinity, max: -Infinity };
|
|
|
31
31
|
const MAX_GROUP_SLOTS = 2 ** 28; // threads * aggregations * groups (2GB of float64)
|
|
32
32
|
const PARALLEL_SORT_ROWS = 50_000; // smaller tables sort on the calling thread
|
|
33
33
|
const TIME_STEPS = [86_400_000, 3_600_000, 60_000, 1_000]; // a day, an hour, a minute, a second (ms): see #directGroups
|
|
34
|
+
// Column facts worked out once and remembered with the column's array
|
|
35
|
+
// (arrays are never written once in a table):
|
|
36
|
+
const MISSING = new WeakMap(); // { n: rows, missing: how many missing values }
|
|
37
|
+
const KEY_RANGE = new WeakMap(); // { n: rows, range: [min, max, how many aren't whole] } (see #directGroups)
|
|
34
38
|
|
|
35
39
|
export class Table {
|
|
36
40
|
// cols: Map name -> { data, dict, codes, strings }
|
|
@@ -578,6 +582,19 @@ export class Table {
|
|
|
578
582
|
return Math.floor((engine._directSlots ?? DIRECT_SLOTS) / (engine.T * totals));
|
|
579
583
|
}
|
|
580
584
|
|
|
585
|
+
// Is src a number column of this table with no missing values? Counted
|
|
586
|
+
// once per column (one pass on every thread) and remembered with its array.
|
|
587
|
+
*#noMissingG(src) {
|
|
588
|
+
const c = src && IDENT.test(src) ? this.cols.get(src) : null;
|
|
589
|
+
if (!c || c.dict || c.strings) return false;
|
|
590
|
+
let known = MISSING.get(c.data);
|
|
591
|
+
if (known?.n !== this.n) {
|
|
592
|
+
const k = c.enc ? { data: c.data, enc: c.enc, n: this.n, type: this.type === Float32Array ? 'f32' : 'f64' } : c.data;
|
|
593
|
+
MISSING.set(c.data, (known = { n: this.n, missing: (yield* this.engine.evalG(['count(k != k)'], { k }))[0] }));
|
|
594
|
+
}
|
|
595
|
+
return known.missing === 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
581
598
|
// Keys small enough to be group numbers as they are: a text column's codes
|
|
582
599
|
// or whole numbers 0..2^24, several combined as k1 * size2 + k2..., while
|
|
583
600
|
// the accumulators stay small (or no larger than the rows). Else null.
|
|
@@ -590,7 +607,9 @@ export class Table {
|
|
|
590
607
|
if (!this.n) { sizes.push(0); offsets.push(0); steps.push(1); continue; }
|
|
591
608
|
// whole numbers in a range under 2^24 (from their minimum: 202301..202412, IDs from 1e9, negatives)
|
|
592
609
|
const k = c.enc ? { data: c.data, enc: c.enc, n: this.n, type: this.type === Float32Array ? 'f32' : 'f64' } : c.data;
|
|
593
|
-
|
|
610
|
+
let known = KEY_RANGE.get(c.data);
|
|
611
|
+
if (known?.n !== this.n) KEY_RANGE.set(c.data, (known = { n: this.n, range: yield* engine.evalG(['min(k)', 'max(k)', 'count(k != floor(k))'], { k }) }));
|
|
612
|
+
const [lo, hi, frac] = known.range;
|
|
594
613
|
if (frac > 0 || !Number.isFinite(lo) || !Number.isFinite(hi)) return null;
|
|
595
614
|
// or times a whole number of days (hours, minutes, seconds) apart: numbered by that unit
|
|
596
615
|
let step = 1;
|
|
@@ -604,9 +623,16 @@ export class Table {
|
|
|
604
623
|
offsets.push(lo);
|
|
605
624
|
steps.push(step);
|
|
606
625
|
}
|
|
626
|
+
const fits = (K) => !(K > 2 ** 24 || engine.T * J * K * (engine.jit && K <= 1024 ? 4 : 1) > (engine._maxGroupSlots ?? MAX_GROUP_SLOTS) || (K > 65_536 && K > 8 * this.n));
|
|
627
|
+
// One key from a small positive number (IDs from 1, years from 1990):
|
|
628
|
+
// numbered from 0 with a few groups left empty, so the key column itself
|
|
629
|
+
// is the group number and needs no copy.
|
|
630
|
+
if (keys.length === 1 && steps[0] === 1 && offsets[0] > 0 && offsets[0] <= Math.max(1024, sizes[0]) && offsets[0] + sizes[0] <= maxK && fits(offsets[0] + sizes[0])) {
|
|
631
|
+
sizes[0] += offsets[0];
|
|
632
|
+
offsets[0] = 0;
|
|
633
|
+
}
|
|
607
634
|
const K = sizes.reduce((a, b) => a * b, 1);
|
|
608
|
-
|
|
609
|
-
if (K > 2 ** 24 || engine.T * J * K * C > (engine._maxGroupSlots ?? MAX_GROUP_SLOTS) || (K > 65_536 && K > 8 * this.n)) return null;
|
|
635
|
+
if (!fits(K)) return null;
|
|
610
636
|
if (K > maxK) { if (info) info.K = K; return null; }
|
|
611
637
|
const below = sizes.map((_, k) => sizes.slice(k + 1).reduce((a, b) => a * b, 1));
|
|
612
638
|
const keyColumn = (k, keep) => directKeys(keep, below[k], sizes[k], steps[k], offsets[k]); // key k of each kept group (codes for text)
|
|
@@ -747,12 +773,20 @@ export class Table {
|
|
|
747
773
|
*#runGroupsG(keys, groups, allItems, merged) {
|
|
748
774
|
const { engine } = this;
|
|
749
775
|
const { K } = groups;
|
|
750
|
-
|
|
776
|
+
// Over a column with no missing values, count(x) is the row count (always
|
|
777
|
+
// counted) and sum / mean / min / max need no check for missing values:
|
|
778
|
+
// SQL's SUM(x) is sum(x) and count(x), so this takes a whole accumulator off.
|
|
779
|
+
const full = new Set();
|
|
780
|
+
for (const it of allItems) if (!DISTINCT.has(it.fn) && (yield* this.#noMissingG(it.raw))) full.add(it);
|
|
781
|
+
const rowCount = (it) => it.fn === 'count' && (!it.arg || full.has(it));
|
|
782
|
+
const items = allItems.filter((it) => !DISTINCT.has(it.fn) && !rowCount(it));
|
|
751
783
|
// means need the count of values present: one more kernel item each
|
|
752
|
-
const kernelItems = [...items
|
|
784
|
+
const kernelItems = [...items.map((it) => (full.has(it) ? { fn: it.fn, arg: it.raw } : it)),
|
|
785
|
+
...items.filter((it) => it.present && !full.has(it)).map((it) => ({ fn: 'count', arg: it.present, of: it }))]; // means, mins, maxes
|
|
753
786
|
const totals = yield* this.#groupTotalsG(groups, kernelItems, merged);
|
|
754
787
|
if (!totals) return null; // (lookup groups the kernels can't take)
|
|
755
|
-
const
|
|
788
|
+
const totalOf = (it) => (rowCount(it) ? totals[0] : totals[1 + items.indexOf(it)]);
|
|
789
|
+
const presentOf = (it) => (full.has(it) ? totals[0] : totals[1 + kernelItems.findIndex((k) => k.of === it)]);
|
|
756
790
|
// Keep groups that have rows, sorted by key (codes for text keys, so
|
|
757
791
|
// dictionary order; NaN last), and build whole columns.
|
|
758
792
|
let keep = keptGroups(totals[0], K);
|
|
@@ -768,7 +802,7 @@ export class Table {
|
|
|
768
802
|
}
|
|
769
803
|
// each item's value for every group number
|
|
770
804
|
const perGroup = allItems.map((it) => {
|
|
771
|
-
const
|
|
805
|
+
const v = DISTINCT.has(it.fn) ? it.perGroup : totalOf(it);
|
|
772
806
|
return it.fn === 'mean' ? ratioOf(v, presentOf(it)) : it.fn === 'min' || it.fn === 'max' ? presentOnly(v, presentOf(it)) : v;
|
|
773
807
|
});
|
|
774
808
|
if (engine.jit && m >= PARALLEL_SORT_ROWS) return yield* this.#groupTableG(keys, groups, allItems, perGroup, keep);
|
|
@@ -963,29 +997,34 @@ export class Table {
|
|
|
963
997
|
interpretGroup(engine, t, analysis.items, predAst, cols, merged, groups.data, acc, K, this.n);
|
|
964
998
|
}
|
|
965
999
|
|
|
966
|
-
// Combine threads (and copies) into one value per group and aggregation
|
|
1000
|
+
// Combine threads (and copies) into one value per group and aggregation:
|
|
1001
|
+
// the thread blocks by fused passes added into thread 0's, 15 blocks a
|
|
1002
|
+
// pass (kernels, so fast from the first query; small ones run on this
|
|
1003
|
+
// thread), then the C copies of each group by a plain loop.
|
|
967
1004
|
totals = [];
|
|
968
1005
|
for (const [j, it] of analysis.items.entries()) {
|
|
969
|
-
|
|
970
|
-
if (
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
const
|
|
975
|
-
|
|
1006
|
+
let from = T;
|
|
1007
|
+
if (T > 1 && engine.jit) {
|
|
1008
|
+
for (let s0 = 1; s0 < T; s0 += 15) {
|
|
1009
|
+
const vars = { t0: acc.subarray(...slots(0, j)) };
|
|
1010
|
+
for (let s = s0; s < Math.min(T, s0 + 15); s++) vars[`t${s}`] = acc.subarray(...slots(s, j));
|
|
1011
|
+
const vnames = Object.keys(vars);
|
|
1012
|
+
const expr = it.reduce === 'min' || it.reduce === 'max'
|
|
1013
|
+
? vnames.slice(1).reduce((e, nm) => `${it.reduce}(${e}, ${nm})`, vnames[0])
|
|
1014
|
+
: vnames.join(' + ');
|
|
1015
|
+
yield* engine.evalG(expr, vars, vars.t0);
|
|
976
1016
|
}
|
|
977
|
-
|
|
978
|
-
|
|
1017
|
+
from = 1;
|
|
1018
|
+
}
|
|
1019
|
+
const a = engine.live(acc);
|
|
1020
|
+
if (C === 1 && from === 1) { const [lo, hi] = slots(0, j); totals.push(a.slice(lo, hi)); continue; }
|
|
1021
|
+
const op = it.reduce === 'min' ? Math.min : it.reduce === 'max' ? Math.max : (x, y) => x + y;
|
|
1022
|
+
const out = new Float64Array(K).fill(IDENTITY[it.reduce]);
|
|
1023
|
+
for (let s = 0; s < from; s++) {
|
|
1024
|
+
const [lo] = slots(s, j);
|
|
1025
|
+
for (let g = 0; g < K; g++) for (let c = 0; c < C; c++) out[g] = op(out[g], a[lo + g * C + c]);
|
|
979
1026
|
}
|
|
980
|
-
|
|
981
|
-
const vars = {};
|
|
982
|
-
for (let s = 0; s < T; s++) vars[`t${s}`] = acc.subarray(...slots(s, j));
|
|
983
|
-
const vnames = Object.keys(vars);
|
|
984
|
-
const expr = it.reduce === 'min' || it.reduce === 'max'
|
|
985
|
-
? vnames.slice(1).reduce((e, nm) => `${it.reduce}(${e}, ${nm})`, vnames[0])
|
|
986
|
-
: vnames.join(' + ');
|
|
987
|
-
yield* engine.evalG(expr, vars, vars.t0);
|
|
988
|
-
totals.push(Float64Array.from(engine.live(vars.t0)));
|
|
1027
|
+
totals.push(out);
|
|
989
1028
|
}
|
|
990
1029
|
engine.free(acc);
|
|
991
1030
|
}
|
|
@@ -1821,12 +1860,14 @@ function checkName(name) {
|
|
|
1821
1860
|
// (mean divides by the values present), min and max skip them, count(x)
|
|
1822
1861
|
// counts the values present (count(condition) still counts where it holds).
|
|
1823
1862
|
// arg is the bound source; returns the aggregation item.
|
|
1863
|
+
// (raw: the argument as given; where it is a column with no missing values,
|
|
1864
|
+
// the group-by uses it as it is and takes counts from the row count)
|
|
1824
1865
|
function missingRules(fn, arg) {
|
|
1825
1866
|
const present = `(${arg}) == (${arg})`;
|
|
1826
|
-
if (fn === 'count') return
|
|
1827
|
-
if (fn === 'min') return { fn, arg: `(${present} ? (${arg}) : Infinity)`, present }; // (present: NaN when nothing is)
|
|
1828
|
-
if (fn === 'max') return { fn, arg: `(${present} ? (${arg}) : -Infinity)`, present };
|
|
1829
|
-
return fn === 'mean' ? { fn, arg: `(${present} ? (${arg}) : 0)`, present } : { fn, arg: `(${present} ? (${arg}) : 0)
|
|
1867
|
+
if (fn === 'count') return isMask(parse(arg)) ? { fn, arg } : { fn, arg: present, raw: arg };
|
|
1868
|
+
if (fn === 'min') return { fn, arg: `(${present} ? (${arg}) : Infinity)`, present, raw: arg }; // (present: NaN when nothing is)
|
|
1869
|
+
if (fn === 'max') return { fn, arg: `(${present} ? (${arg}) : -Infinity)`, present, raw: arg };
|
|
1870
|
+
return fn === 'mean' ? { fn, arg: `(${present} ? (${arg}) : 0)`, present, raw: arg } : { fn, arg: `(${present} ? (${arg}) : 0)`, raw: arg }; // sum, mean
|
|
1830
1871
|
}
|
|
1831
1872
|
|
|
1832
1873
|
// a category column given as codes into a labels array: { codes, labels }
|