This is an automated email from the ASF dual-hosted git repository.
bamaer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 6abd5fd3ac Issue #8785 : Document parallel reading on Table Input
(#8801)
6abd5fd3ac is described below
commit 6abd5fd3ac0fec0e6e8464ba773e0783a9fa556a
Author: Matt Casters <[email protected]>
AuthorDate: Fri Oct 9 16:26:03 2026 +0200
Issue #8785 : Document parallel reading on Table Input (#8801)
* Issue #8785 : Document parallel reading on Table Input
* Issue #8785 : Describe NR_COPIES as a pipeline parameter
* Issue #8785 : Qualify parallel Table Input reading for connections,
snapshots, and Oracle CONCAT
---
.../ROOT/pages/pipeline/transforms/tableinput.adoc | 121 ++++++++++++++++++++-
1 file changed, 120 insertions(+), 1 deletion(-)
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableinput.adoc
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableinput.adoc
index 74da090b40..538d17f2ea 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableinput.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableinput.adoc
@@ -206,6 +206,125 @@ where id > ${PRM_ID} AND lastdate > ?;
- `+${startDate}+` is a pipeline variable
- `?` is a parameter provided by the input stream
+[#parallel-reading]
+== Parallel reading
+
+Keep `NR_COPIES` as a parameter of the pipeline. On the pipeline properties
*Parameters* tab, give it a default and a description so the copy count is
documented with the pipeline and easy to change for a run. Changing that one
value lets you test the limit of the source database's capacity. See
xref:fields-parameters-variables.adoc#_parameters[Parameters].
+
+A large read often keeps one database session busy on a single CPU core, while
the network and the disks still have capacity. Several copies of the same Table
Input share that read. Unless the copies share a connection, each copy runs the
query on its own session, and a predicate assigns that copy its own slice of
the rows.
+
+Every row has to land in exactly one copy. Each copy reads its own snapshot;
for a consistent result, read from a table that isn't being changed during the
run. Build the predicate so that it maps a key onto the copy number.
+
+=== Setup
+
+. Set pipeline parameter `NR_COPIES` to the number of copies for this run, for
example `4`.
+. Click the Table Input icon and set *Specify copies* to `+${NR_COPIES}+`. See
xref:pipeline/specify-copies.adoc[Specify copies]. The copies field accepts a
positive integer or a single variable.
+. Enable *Replace variables in script*. Hop substitutes the variables before
the database receives the SQL.
+. Add the slice predicate to the `WHERE` clause.
+
+`+${Internal.Transform.CopyNr}+` is the copy that is running this query. Copy
numbers start at `0`. The last copy is `NR_COPIES - 1`. See
xref:variables.adoc#_internal_variables[internal variables].
+
+Each copy is a thread with its own database connection on the engines that
honor the copy count, unless the copies share a connection. Don't use a
transactional run configuration; it makes all copies share one connection. See
xref:pipeline/specify-copies.adoc#supported-engines[which engines run copies in
parallel].
+
+The statements stay the same apart from the copy number. *Limit size* and a
SQL `LIMIT` apply inside each copy, so four copies with `LIMIT 100` return up
to 400 rows.
+
+The next transform receives the rows from every copy. One copy of that
transform merges the slices into a single stream.
+
+Leave Table Input unpartitioned. A partition schema sets the number of copies
itself.
+
+=== Integer keys
+
+Oracle, MySQL, MariaDB, and PostgreSQL accept the same predicate for a
non-negative integer key:
+
+[source,sql]
+----
+SELECT *
+FROM orders
+WHERE MOD(table_id, ${NR_COPIES}) = ${Internal.Transform.CopyNr}
+----
+
+`MOD` returns `0` through `NR_COPIES - 1` for a non-negative `table_id`, which
is the range of copy numbers. Combine it with `AND` when the statement already
has a `WHERE` clause.
+
+`MOD` of a negative number keeps the sign of that number on these databases. A
negative remainder matches none of the copy numbers, so that row is left
unread. A `NULL` key is left unread as well, because the comparison is unknown.
+
+`MOD(table_id, ...)` usually cannot use a plain index on `table_id`. Each copy
scans its slice. The extra sessions speed the read up when one session is
capped by a single server core and the storage and network can serve those
sessions.
+
+=== Other keys
+
+When the key is not a single non-negative integer, hash the columns and take
that value modulo `+${NR_COPIES}+`. A CRC, a hash, or one byte of an MD5 digest
all work. The expression has to stay non-negative, and it has to be the same in
every copy.
+
+The slices are even when the number of copies is a power of two (2, 4, 8, 16).
Any other count still returns every row, with a few copies receiving slightly
more rows than the others.
+
+==== Oracle
+
+`ORA_HASH` returns a bucket for an expression. The second argument is the
highest bucket number, and the function returns `0` through that number
inclusive. Four copies use buckets `0`, `1`, `2`, and `3`, so the second
argument is `+${NR_COPIES}+ - 1`. Hop replaces the variable first and the
database evaluates the subtraction. The third argument is a constant seed. Use
the same seed in every copy so the split stays stable. Oracle's example of 100
buckets passes `99`.
+
+[source,sql]
+----
+SELECT *
+FROM sales
+WHERE ORA_HASH(CONCAT(cust_id, prod_id), ${NR_COPIES} - 1, 12345)
+ = ${Internal.Transform.CopyNr}
+----
+
+Oracle computes the bucket as the raw hash modulo the number of copies. That
split is uniform when the number of copies is a power of two.
+
+`CONCAT` returns `NULL` only when both values are `NULL`, and that row is left
unread.
+
+==== MySQL and MariaDB
+
+`CRC32` returns an unsigned 32-bit checksum:
+
+[source,sql]
+----
+SELECT *
+FROM sales
+WHERE MOD(CRC32(CONCAT(cust_id, prod_id)), ${NR_COPIES})
+ = ${Internal.Transform.CopyNr}
+----
+
+The first byte of an MD5 digest is a number from `0` through `255`:
+
+[source,sql]
+----
+SELECT *
+FROM sales
+WHERE MOD(
+ CONV(SUBSTR(MD5(CONCAT(cust_id, prod_id)), 1, 2), 16, 10),
+ ${NR_COPIES}
+ ) = ${Internal.Transform.CopyNr}
+----
+
+`CONCAT` returns `NULL` when any argument is `NULL`, and that row is left
unread.
+
+==== PostgreSQL
+
+The first byte of an MD5 digest is a number from `0` through `255`:
+
+[source,sql]
+----
+SELECT *
+FROM sales
+WHERE MOD(
+ get_byte(decode(md5(cust_id::text || prod_id::text), 'hex'), 0),
+ ${NR_COPIES}
+ ) = ${Internal.Transform.CopyNr}
+----
+
+`hashtext` hashes the whole value. It can return a negative integer. Cast to
`bigint` before `ABS`, because `ABS` of the smallest 32-bit integer overflows.
+
+[source,sql]
+----
+SELECT *
+FROM sales
+WHERE MOD(
+ ABS(hashtext(cust_id::text || prod_id::text)::bigint),
+ ${NR_COPIES}
+ ) = ${Internal.Transform.CopyNr}
+----
+
+The `||` operator yields `NULL` when either side is `NULL`, and that row is
left unread. `hashtext` is the function PostgreSQL uses for hash indexes, so
every copy in one run shares it.
+
== Best Practices and Pro Tips
== Pro Tips
@@ -218,7 +337,7 @@ TIP: A cartesian join transform will combine a different
number of fields from m
TIP: When *Execute for each row* is off, Table Input waits until every
incoming hop has completed so it can concatenate the parameter rows. When the
SQL has no bind placeholder, the incoming rows are only drained so an upstream
header or sequencing transform can complete before the query runs.
-TIP: For better performance with large datasets you can use indexed columns in
`WHERE` clauses and avoid `SELECT *` and only retrieve needed fields.
+TIP: For better performance with large datasets you can use indexed columns in
`WHERE` clauses and avoid `SELECT *` and only retrieve needed fields. To spread
one table read across several database sessions, see <<parallel-reading>>.
== Dynamic SQL with Metadata Injection