Skip to content

Commit f83f30b

Browse files
Joint-width: compute the essential-variable count e from FDs mined on the gathered data (minimum cover under data FDs, subsuming declared keys on the instance), replacing the all-join-variables placeholder; expose it as ucq_joint_compile_stats.n_enumerating with an H0 x->y test (e=1 vs 2)
1 parent 6725df8 commit f83f30b

5 files changed

Lines changed: 180 additions & 22 deletions

File tree

sql/provsql.common.sql

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4961,6 +4961,10 @@ CREATE OR REPLACE FUNCTION reachability_compile_stats(
49614961
* @param[out] n_bags number of bags in the decomposition
49624962
* @param[out] max_states peak number of DP states at any node
49634963
* @param[out] dd_size number of gates in the emitted d-D
4964+
* @param[out] n_enumerating maximum number of essential (enumerating) query
4965+
* variables over the disjuncts -- the @c e of the @f$2^{O(k^e)}@f$
4966+
* bound, with variables functionally determined by others (via FDs
4967+
* mined from the data) removed
49644968
*/
49654969
CREATE OR REPLACE FUNCTION ucq_joint_compile_stats(
49664970
IN disjunct_nvars INT[],
@@ -4979,7 +4983,8 @@ CREATE OR REPLACE FUNCTION ucq_joint_compile_stats(
49794983
OUT circuit_treewidth_lb INT,
49804984
OUT n_bags BIGINT,
49814985
OUT max_states BIGINT,
4982-
OUT dd_size BIGINT)
4986+
OUT dd_size BIGINT,
4987+
OUT n_enumerating INT)
49834988
AS 'provsql','ucq_joint_compile_stats'
49844989
LANGUAGE C IMMUTABLE PARALLEL SAFE;
49854990

@@ -5003,7 +5008,8 @@ CREATE OR REPLACE FUNCTION ucq_joint_compile_stats(
50035008
OUT circuit_treewidth_lb INT,
50045009
OUT n_bags BIGINT,
50055010
OUT max_states BIGINT,
5006-
OUT dd_size BIGINT)
5011+
OUT dd_size BIGINT,
5012+
OUT n_enumerating INT)
50075013
AS $$
50085014
DECLARE
50095015
dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
@@ -5023,9 +5029,10 @@ BEGIN
50235029
didx := didx + 1;
50245030
END LOOP;
50255031
SELECT s.probability, s.joint_treewidth, s.data_treewidth_lb,
5026-
s.circuit_treewidth_lb, s.n_bags, s.max_states, s.dd_size
5032+
s.circuit_treewidth_lb, s.n_bags, s.max_states, s.dd_size,
5033+
s.n_enumerating
50275034
INTO probability, joint_treewidth, data_treewidth_lb,
5028-
circuit_treewidth_lb, n_bags, max_states, dd_size
5035+
circuit_treewidth_lb, n_bags, max_states, dd_size, n_enumerating
50295036
FROM ucq_joint_compile_stats(dnv, adisj, arel, avars, aarity,
50305037
fact_rel, fact_elems, fact_arity, fact_tokens, fact_probs) s;
50315038
END;
@@ -5066,7 +5073,8 @@ CREATE OR REPLACE FUNCTION ucq_joint_compile_stats_tracked(
50665073
OUT circuit_treewidth_lb INT,
50675074
OUT n_bags BIGINT,
50685075
OUT max_states BIGINT,
5069-
OUT dd_size BIGINT)
5076+
OUT dd_size BIGINT,
5077+
OUT n_enumerating INT)
50705078
AS 'provsql','ucq_joint_compile_stats_tracked'
50715079
LANGUAGE C STABLE PARALLEL SAFE;
50725080

@@ -5086,7 +5094,8 @@ CREATE OR REPLACE FUNCTION ucq_joint_compile_stats_tracked(
50865094
OUT circuit_treewidth_lb INT,
50875095
OUT n_bags BIGINT,
50885096
OUT max_states BIGINT,
5089-
OUT dd_size BIGINT)
5097+
OUT dd_size BIGINT,
5098+
OUT n_enumerating INT)
50905099
AS $$
50915100
DECLARE
50925101
dnv INT[] := '{}'; adisj INT[] := '{}'; arel INT[] := '{}';
@@ -5106,9 +5115,10 @@ BEGIN
51065115
didx := didx + 1;
51075116
END LOOP;
51085117
SELECT s.probability, s.joint_treewidth, s.data_treewidth_lb,
5109-
s.circuit_treewidth_lb, s.n_bags, s.max_states, s.dd_size
5118+
s.circuit_treewidth_lb, s.n_bags, s.max_states, s.dd_size,
5119+
s.n_enumerating
51105120
INTO probability, joint_treewidth, data_treewidth_lb,
5111-
circuit_treewidth_lb, n_bags, max_states, dd_size
5121+
circuit_treewidth_lb, n_bags, max_states, dd_size, n_enumerating
51125122
FROM ucq_joint_compile_stats_tracked(dnv, adisj, arel, avars, aarity,
51135123
fact_rel, fact_elems, fact_arity, fact_tokens) s;
51145124
END;

src/UCQJointCompiler.cpp

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#include <map>
3838
#include <memory>
3939
#include <set>
40+
#include <tuple>
4041
#include <unordered_map>
4142
#include <vector>
4243

@@ -393,6 +394,118 @@ State join(const QueryCtx &q, const State &s1, const State &s2)
393394
// ToCS 2020), run over the joint graph (element + gate vertices).
394395
// ---------------------------------------------------------------------
395396

397+
/**
398+
* @brief Number of *essential* (enumerating) variables of a disjunct.
399+
*
400+
* The exponential parameter of the DP is not every join variable but only
401+
* those that must be enumerated: a variable functionally determined by others
402+
* does not multiply the partial-homomorphism state. This returns the size of
403+
* a minimum set @c S of join variables whose FD closure covers all of them
404+
* (the @c e of the @f$2^{O(k^e)}@f$ bound).
405+
*
406+
* The FDs are **mined from the gathered tuples** (@p enc.facts, already
407+
* post-selection): a column set determines a column when no two tuples of the
408+
* relation agree on the former and differ on the latter. This captures any
409+
* declared key (which necessarily holds on the data) along with FDs incidental
410+
* to this instance, and needs no catalog lookup. The closure is sound for the
411+
* instance being compiled: if the FD holds on these tuples the variable is
412+
* genuinely fixed in every world of this computation.
413+
*
414+
* The minimum-cover search is @f$2^{|V|}@f$ in the join-variable count @c |V|;
415+
* above @c MAX_FD_VARS it is skipped and the full count returned (a sound upper
416+
* bound), the design target being a handful of enumerating variables.
417+
*/
418+
unsigned essentialVarCount(const CQ &cq, const std::vector<bool> &appears,
419+
const JointEncoding &enc)
420+
{
421+
std::vector<unsigned> V; // the join variables
422+
for (unsigned v = 0; v < cq.n_vars; ++v)
423+
if (appears[v])
424+
V.push_back(v);
425+
const unsigned nV = static_cast<unsigned>(V.size());
426+
static constexpr unsigned MAX_FD_VARS = 12;
427+
if (nV <= 1 || nV > MAX_FD_VARS)
428+
return nV;
429+
430+
// Tuples per (relation, arity), as pointers into enc.facts.
431+
std::map<std::pair<unsigned, std::size_t>,
432+
std::vector<const std::vector<unsigned long> *> > byrel;
433+
for (const Fact &f : enc.facts)
434+
byrel[{f.relation_id, f.elements.size()}].push_back(&f.elements);
435+
436+
// Memoised data FD: do the columns in bitmask @c known determine column @c c
437+
// in the tuples of (@c rel, @c arity)?
438+
std::map<std::tuple<unsigned, std::size_t, std::uint64_t, unsigned>, bool> memo;
439+
auto dataFD = [&](unsigned rel, std::size_t arity,
440+
std::uint64_t known, unsigned c) -> bool {
441+
auto key = std::make_tuple(rel, arity, known, c);
442+
auto it = memo.find(key);
443+
if (it != memo.end())
444+
return it->second;
445+
bool holds = true;
446+
auto rit = byrel.find({rel, arity});
447+
if (rit != byrel.end()) {
448+
std::map<std::vector<unsigned long>, unsigned long> grp;
449+
for (const std::vector<unsigned long> *t : rit->second) {
450+
std::vector<unsigned long> kv;
451+
for (std::size_t p = 0; p < arity; ++p)
452+
if (known & (std::uint64_t{1} << p))
453+
kv.push_back((*t)[p]);
454+
auto git = grp.find(kv);
455+
if (git == grp.end())
456+
grp.emplace(std::move(kv), (*t)[c]);
457+
else if (git->second != (*t)[c]) {
458+
holds = false;
459+
break;
460+
}
461+
}
462+
}
463+
memo[key] = holds;
464+
return holds;
465+
};
466+
467+
std::unordered_map<unsigned, unsigned> bitOf;
468+
for (unsigned i = 0; i < nV; ++i)
469+
bitOf[V[i]] = i;
470+
const std::uint32_t fullMask = (std::uint32_t{1} << nV) - 1;
471+
472+
// FD closure of a determined set (bitmask over @c V).
473+
auto closure = [&](std::uint32_t det) -> std::uint32_t {
474+
bool changed = true;
475+
while (changed) {
476+
changed = false;
477+
for (const Atom &a : cq.atoms) {
478+
const std::size_t arity = a.vars.size();
479+
std::uint64_t knownPos = 0;
480+
for (std::size_t p = 0; p < arity; ++p)
481+
if (det & (std::uint32_t{1} << bitOf[a.vars[p]]))
482+
knownPos |= (std::uint64_t{1} << p);
483+
for (std::size_t p = 0; p < arity; ++p) {
484+
if (knownPos & (std::uint64_t{1} << p))
485+
continue; // this column already determined
486+
const unsigned bit = bitOf[a.vars[p]];
487+
if (dataFD(a.relation_id, arity, knownPos, static_cast<unsigned>(p))) {
488+
det |= (std::uint32_t{1} << bit);
489+
knownPos |= (std::uint64_t{1} << p);
490+
changed = true;
491+
}
492+
}
493+
}
494+
}
495+
return det;
496+
};
497+
498+
unsigned best = nV;
499+
for (std::uint32_t s = 0; s <= fullMask; ++s) {
500+
const unsigned pc = static_cast<unsigned>(__builtin_popcount(s));
501+
if (pc >= best)
502+
continue; // cannot beat the current minimum
503+
if (closure(s) == fullMask)
504+
best = pc;
505+
}
506+
return best;
507+
}
508+
396509
/** @brief Build the per-disjunct query context and the query stats. */
397510
void buildQueryCtx(const UCQ &ucq, const JointEncoding &enc,
398511
QueryCtx &q, UCQJointCompiler::Stats &stats)
@@ -420,14 +533,10 @@ void buildQueryCtx(const UCQ &ucq, const JointEncoding &enc,
420533
di.atoms_of_var[v] |= (std::uint64_t{1} << ai);
421534
appears[v] = true;
422535
}
423-
// The exponential parameter is the number of variables that occur in
424-
// an atom (the join variables); a variable in no atom never enters a
425-
// code. Key/FD determination (a later milestone) shrinks this.
426-
unsigned ne = 0;
427-
for (unsigned v = 0; v < di.n_vars; ++v)
428-
if (appears[v])
429-
++ne;
430-
stats.n_enumerating[d] = ne;
536+
// The exponential parameter is the number of *essential* join variables:
537+
// those that must be enumerated once the ones functionally determined by
538+
// others (via FDs mined from the gathered data) are removed.
539+
stats.n_enumerating[d] = essentialVarCount(cq, appears, enc);
431540
}
432541
stats.data_treewidth_lb = enc.data_treewidth_lb;
433542
stats.circuit_treewidth_lb = enc.circuit_treewidth_lb;

src/ucq_joint_evaluate.cpp

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -714,15 +714,20 @@ Datum ucq_joint_compile_stats(PG_FUNCTION_ARGS)
714714
provsql_error("ucq_joint_compile_stats: expected composite return type");
715715
tupdesc = BlessTupleDesc(tupdesc);
716716

717-
Datum values[7];
718-
bool nulls[7] = {false, false, false, false, false, false, false};
717+
Datum values[8];
718+
bool nulls[8] = {false, false, false, false, false, false, false, false};
719719
values[0] = Float8GetDatum(result.dd.probabilityEvaluation());
720720
values[1] = Int32GetDatum(static_cast<int32>(result.stats.joint_treewidth));
721721
values[2] = Int32GetDatum(static_cast<int32>(result.stats.data_treewidth_lb));
722722
values[3] = Int32GetDatum(static_cast<int32>(result.stats.circuit_treewidth_lb));
723723
values[4] = Int64GetDatum(static_cast<int64>(result.stats.nb_bags));
724724
values[5] = Int64GetDatum(static_cast<int64>(result.stats.max_states));
725725
values[6] = Int64GetDatum(static_cast<int64>(result.stats.dd_size));
726+
unsigned maxenum = 0;
727+
for (unsigned ev : result.stats.n_enumerating)
728+
if (ev > maxenum)
729+
maxenum = ev;
730+
values[7] = Int32GetDatum(static_cast<int32>(maxenum));
726731

727732
PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
728733
} catch (const std::exception &e) {
@@ -781,15 +786,20 @@ Datum ucq_joint_compile_stats_tracked(PG_FUNCTION_ARGS)
781786
provsql_error("ucq_joint_compile_stats: expected composite return type");
782787
tupdesc = BlessTupleDesc(tupdesc);
783788

784-
Datum values[7];
785-
bool nulls[7] = {false, false, false, false, false, false, false};
789+
Datum values[8];
790+
bool nulls[8] = {false, false, false, false, false, false, false, false};
786791
values[0] = Float8GetDatum(result.dd.probabilityEvaluation());
787792
values[1] = Int32GetDatum(static_cast<int32>(result.stats.joint_treewidth));
788793
values[2] = Int32GetDatum(static_cast<int32>(result.stats.data_treewidth_lb));
789794
values[3] = Int32GetDatum(static_cast<int32>(result.stats.circuit_treewidth_lb));
790795
values[4] = Int64GetDatum(static_cast<int64>(result.stats.nb_bags));
791796
values[5] = Int64GetDatum(static_cast<int64>(result.stats.max_states));
792797
values[6] = Int64GetDatum(static_cast<int64>(result.stats.dd_size));
798+
unsigned maxenum = 0;
799+
for (unsigned ev : result.stats.n_enumerating)
800+
if (ev > maxenum)
801+
maxenum = ev;
802+
values[7] = Int32GetDatum(static_cast<int32>(maxenum));
793803

794804
PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
795805
} catch (const std::exception &e) {

test/expected/ucq_joint.out

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,16 @@ ucq_two_disjuncts
2020
joint_treewidth|data_treewidth_lb|circuit_treewidth_lb
2121
1|1|0
2222
(1 row)
23+
e_fd_holds
24+
1
25+
(1 row)
26+
e_fd_broken
27+
2
28+
(1 row)
2329
ERROR: ProvSQL: ucq_joint: joint treewidth exceeds the configured maximum (1); fall back to the standard probability ladder
2430
CONTEXT: SQL statement "SELECT s.probability, s.joint_treewidth, s.data_treewidth_lb,
25-
s.circuit_treewidth_lb, s.n_bags, s.max_states, s.dd_size
26-
FROM ucq_joint_compile_stats(dnv, adisj, arel, avars, aarity,
31+
s.circuit_treewidth_lb, s.n_bags, s.max_states, s.dd_size,
32+
s.n_enumerating
33+
FROM ucq_joint_compile_stats(dnv, adisj, arel, avars, aarity,
2734
fact_rel, fact_elems, fact_arity, fact_tokens, fact_probs) s"
2835
PL/pgSQL function ucq_joint_compile_stats(jsonb,integer[],integer[],integer[],uuid[],double precision[]) line 19 at SQL statement

test/sql/ucq_joint.sql

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,28 @@ FROM ucq_joint_compile_stats(
102102
ARRAY[u('r0'),u('r1'),u('s02'),u('s13'),u('t2'),u('t3')]::uuid[],
103103
ARRAY[0.5,0.5,0.5,0.5,0.5,0.5]);
104104

105+
-- FD-aware essential-variable count (n_enumerating): a join variable
106+
-- functionally determined by others -- via an FD *mined from the gathered
107+
-- tuples* -- is not counted in the exponential parameter e. H0 =
108+
-- R(x),S(x,y),T(y): when S carries x -> y in the data, y is determined by x
109+
-- so e = 1; add a second y for some x and the FD breaks, so e = 2.
110+
SELECT n_enumerating AS e_fd_holds FROM ucq_joint_compile_stats(
111+
'{"disjuncts":[{"n_vars":2,"atoms":[
112+
{"rel":0,"vars":[0]},{"rel":1,"vars":[0,1]},{"rel":2,"vars":[1]}]}]}'::jsonb,
113+
ARRAY[0,0, 1,1, 2,2],
114+
ARRAY[0, 1, 0,2, 1,3, 2, 3],
115+
ARRAY[1,1, 2,2, 1,1],
116+
ARRAY[u('r0'),u('r1'),u('s02'),u('s13'),u('t2'),u('t3')]::uuid[],
117+
ARRAY[0.5,0.5,0.5,0.5,0.5,0.5]);
118+
SELECT n_enumerating AS e_fd_broken FROM ucq_joint_compile_stats(
119+
'{"disjuncts":[{"n_vars":2,"atoms":[
120+
{"rel":0,"vars":[0]},{"rel":1,"vars":[0,1]},{"rel":2,"vars":[1]}]}]}'::jsonb,
121+
ARRAY[0,0, 1,1,1, 2,2],
122+
ARRAY[0, 1, 0,2, 1,3, 0,3, 2, 3],
123+
ARRAY[1,1, 2,2,2, 1,1],
124+
ARRAY[u('r0'),u('r1'),u('s02'),u('s13'),u('s03'),u('t2'),u('t3')]::uuid[],
125+
ARRAY[0.5,0.5,0.5,0.5,0.5,0.5,0.5]);
126+
105127
-- Prop 4.2.11(b): a high-joint-width instance must be rejected (the
106128
-- caller then falls back to the ladder). With the joint-width cap
107129
-- lowered to 1, a triangle over S (Gaifman treewidth 2) is rejected by

0 commit comments

Comments
 (0)