Zack Ives, May 1999 / Updated August 2001
The Tukwila query processing components are designed to be self-contained modules that can be swapped out as needed. Each of the main components (reformulator, optimizer, execution engine, and wrappers) are separate code modules, each optionally in a different language and on a different platform. A sockets-based communication interface with a standardized request model allow us to interchange parts.
Tukwila now uses a subset of the W3C XQuery (formerly Quilt) XML query language to specify queries. The optimizer component of the system includes a Java Swing-based GUI that allows the user to interactively pose queries and see results. The optimizer JAR file also includes library routines to directly invoke the optimizer and the execution system from within your own applications. See the tukwila.InvokeXQuery documentation for more details.
Currently the query optimizer only understands queries without nesting. Nested queries are supported by the underlying execution system, and if demand is sufficient, I will add support to the optimizer for this feature.
The query execution engine constantly monitors port 7776 (by default) for query execution requests. A request is simply a physical query plan in the format described later in this document. The request is encapsulated in a SOAP HTTP request, and the result is returned in XML as a SOAP HTTP response.
A query response will generally contain data (i.e. query results), unless the query does not execute to completion. The state of individual operators in a query plan (including their cardinalities) is also returned.
The query result string, which is described in a later section, will begin with an execution status. This status should be used as the basis for the optimizer's decision to terminate successfully, terminate with error, or re-optimize a query. If re-optimization is performed, the optimizer will simply send a new query plan through the same socket to the execution system.
The basic layout for a query plan is as follows:
The header is fairly simple, and most of it can be repeated verbatim. Much of the header information is for the benefit of XML parsers and for future use. Note that "auser" is the default user ID for many of the wrappers we have, so you should use that by default.
<?xml version="1.0"?>
<!DOCTYPE PLAN SYSTEM "plan.dtd">
<PLAN ID="PlanName" VERSION="1.10" REQUESTOR="RequestID"
RESULT="QueryRootNodeID" USER="auser">
<FRAGMENT NUM="ExecutionStep">
<GRAPH>
<!-- Either XML output or relational output -->
<XML SOURCE="FragmentRootNodeID" /> | <OUTPUT NODE="FragmentRootNodeID"
/>
<N ID="NodeID" OP="Operator" attributes CARD="EstimatedSize">
</N>
<!-- More nodes here -->
</GRAPH>
<RULES>
<R ID="RuleID">
<WHEN>Event</WHEN>
<IF>ConditionTest</IF>
<THEN>Action</THEN>
<ELSE>Action</ELSE>
</R>
<!-- More rules here -->
</RULES>
</FRAGMENT>
<!-- More fragments here -->
<!-- Global rules begin here -->
<RULES>
<R ID="RuleID" ACTIVE="state">
<WHEN>Event</WHEN>
<IF>ConditionTest</IF>
<THEN>Action</THEN>
<ELSE>Action</ELSE>
</R>
<!-- More rules here -->
</RULES>
</PLAN>
For the most part, constructing a plan is simply a matter of "filling in the template" above. Each plan and request will get a unique ID.
Each fragment within a request is given a number representing an execution step, and it materializes its results. Fragments are sequenced in order by execution step, and then executed from lowest to highest step number. (A future extension to Tukwila may allow for multiple fragments to execute in the same step, but that is not currently supported.) The use of rules and fragments is likely to be minimal in a hand-authored plan.
Within the fragment are an operator graph and a set of rules specific to the fragment. The operator graph includes an <OUTPUT> or <XML> element which specifies which node is the root of the fragment (and whose output should thus be written to disk), and the NODE/SOURCE parameter of this attribute must reference one of the nodes in the fragment graph. Nodes are specified with the <N> element, and include operator type, expected cardinality of result, any children, and various other parameters.
<N ID="NodeID" OP="JOIN" TYPE="HASH"
CHILDREN="LeftChildNodeID RightChildNodeID" SPACE="KBytesToUse"
CARD="ExpectedCard">
<WHERE>LeftChildNodeID.LeftChildAttrib =
RightChildNodeID.RightChildAttrib</WHERE>
...
</N>
<N ID="NodeID" OP="SELECT" CHILD="ChildID"
CARD="ExpectedCard">
<WHERE>ChildID.Attrib {relop}
{expression}</WHERE>
</N>
<N ID="NodeID" OP="PREFETCH" TIMEOUT="TimeIn_ms"
CARD="ExpectedCard">
<HOST>HostName:Port</HOST>
<SERVICE>WrapperService</SERVICE>
<QUERY>SQLQuery</QUERY>
</N>
<N ID="NodeID" OP="WRAPPER" TIMEOUT="TimeIn_ms"
CARD="ExpectedCard">
<HOST>HostName:Port</HOST>
<SERVICE>WrapperService</SERVICE>
<QUERY>SQLQuery</QUERY>
</N>
<N ID="NodeID" OP="PROJECT" CHILD="ChildNodeID"
CARD="ExpectedCard">
<ATTR>ChildNodeID.attribute</ATTR>
<ATTR>ChildNodeID.attribute</ATTR>
...
</N>
<N ID="NodeID" OP="COLLECTOR" CHILDREN="ChildNodeID1
ChildNodeID2 ..."
SPACE="KBytesToUse" CARD="ExpectedCard">
</N>
<N ID="NodeID" OP="SORT" CHILD="ChildNodeID"
ORDER="ASCENDING | DESCENDING" SPACE="KBytesToUse" CARD="ExpectedCard">
<ATTR>ChildNodeID.attribute</ATTR>
<ATTR>ChildNodeID.attribute</ATTR>
</N>
<N ID="NodeID" OP="AGGREGATE" CHILD="ChildNodeID"
SPACE="KBytesToUse" CARD="ExpectedCard">
<SUM>ChildNodeID.attribute</SUM> |
<MAX>ChildNodeID.attribute</MAX> |
<MIN>ChildNodeID.attribute</MIN> |
<COUNT>ChildNodeID.attribute</COUNT> |
<AVG>ChildNodeID.attribute</AVG>
...
</N>
<N ID="NodeID" OP="UNION" CHILDREN="ChildNodeID1
ChildNodeID2" SPACE="KBytesToUse" CARD="ExpectedCard">
</N>
<N ID="NodeID" OP="XSCAN" FILE="http://location/file.xml"
VALIDATE="YES | NO" CARD="ExpectedCard">
<PATH NAME="Binding1">root/path1</PATH>
<PATH NAME="Binding2" INLINE="TYPE">Binding1/path2</PATH>
</N>
<N ID="NodeID" OP="XLITERAL" CHILD="ChildNodeID"
VALUE="LiteralString" CARD="ExpectedCard">
</N>
<N ID="NodeID" OP="XLOOKUP" CHILD="ChildNodeID" CARD="ExpectedCard">
<ATTR>ChildNodeID.attribute</ATTR>
</N>
<N ID="NodeID" OP="XATTRIBUTE" CHILD="ChildNodeID"
LABEL="Label" CARD="ExpectedCard">
</N>
<N ID="NodeID" OP="XELEMENT" CHILD="ChildNodeID"
LABEL="Label" COUNT="count" CARD="ExpectedCard">
</N>
<N ID="NodeID" OP="XNEST" CHILD="OuterNodeID1 InnerNodeID2"
CARD="ExpectedCard">
<WHERE>OuterNodeID.LeftChildAttrib = InnerNodeID.RightChildAttrib</WHERE>
...
</N>
A rule R is formally a quintuple <event, condition, action, active_flag, owner>, where the owner is a node, fragment, or plan; and the active_flag must be TRUE for the rule to be in the active set.
As an event E occurs, it is sent to the Tukwila event handler. The event handler checks a hash table to see if any rules in the active set match E; if so, it places the event into the event queue. Each event is individually dequeued in the order it was detected, and now all matching rules are triggered serially before the next event is processed. The condition for each rule is tested, and if it is met the actions are executed sequentially. If the optional ELSE clause is used, an alternate series of actions may be executed. Formally, the rules' conditions should all be tested in parallel, and all actions should be executed in parallel; our implementation does both of these serially.
There are no synchronization or locking semantics, so rules are not allowed to have conflicting actions; the rule generator must prevent this from happening. Rules are not executed for operators which have been terminated, in order to prevent deadlock and race conditions. Once a rule has been executed, it becomes inactive, unless one of its composite actions is to reactivate the same rule.
The execution system returns results and a status message within a SOAP envelope. Ordinarily, the results will be enclosed first, and then status messages will come afterwards.
Status:
One status line (with terminating newline) per node in the query plan, with the following items, each separated by a space:
A series of messages generated during parsing and execution, preceded by the header "EXECUTION_MESSAGES" and a newline. These should be ignored by the optimizer, but are provided for debugging purposes.
A termination line reading "DONE" followed by a newline. This marks the end of the results.
Click here to download the sample plan. It does a self-join of the personal.xml file on data.
<?xml version="1.0"?> <!DOCTYPE plan SYSTEM "plan.dtd"> <!--Query execution plan for Tukwila System, zives 8/31/98 --> <!-- Header: user, requesting machine, etc. --> <PLAN ID="Example" VERSION="1.10" REQUESTOR="127.128.129.130" RESULT="result" USER="auser"> <FRAGMENT NUM="0"> <GRAPH> <XML SOURCE="result"/> <N ID="result" OP="XELEMENT" CHILD="inner2" LABEL="outer" COUNT="2" CARD="150"></N> <N ID="inner2" OP="XELEMENT" CHILD="lookup2" LABEL="firstname" COUNT="1" CARD="150"></N> <N ID="lookup2" OP="XLOOKUP" CHILD="inner" CARD="150"> <ATTR>inner.ng</ATTR> </N> <N ID="inner" OP="XELEMENT" CHILD="text" LABEL="lastname" COUNT="2" CARD="150"></N> <N ID="text" OP="XLITERAL" CHILD="lookup" VALUE="myText" CARD="150"></N> <N ID="lookup" OP="XLOOKUP" CHILD="joins" CARD="150"> <ATTR>joins.nf</ATTR> </N> <N ID="joins" OP="JOIN" TYPE="DOUBLE_PIPELINED" CHILDREN="sel node2" SPACE="300" CARD="150"> <WHERE>sel.nf = node2.nf</WHERE> </N> <N ID="sel" OP="SELECT" CHILD="node1" CARD="300"> <WHERE>node1.nf = "Boss"</WHERE> </N> <N ID="node1" OP="XSCAN" FILE="personal.xml" VALIDATE="YES" CARD="1000"> <PATH NAME="p">root."personnel"."person"</PATH> <PATH NAME="n">p."name"</PATH> <PATH NAME="nf">n~"family"</PATH> <PATH NAME="ng">n~"given"</PATH> <PATH NAME="w">p~"name"</PATH> <PATH NAME="wf">w~"family"</PATH> <PATH NAME="wg">w~"given"</PATH> </N> <N ID="node2" OP="XSCAN" FILE="personal2.xml" VALIDATE="YES" CARD="1000"> <PATH NAME="p">root."personnel"."person"</PATH> <PATH NAME="n">p."name"</PATH> <PATH NAME="nf">n~"family"</PATH> </N> </GRAPH> </FRAGMENT> </PLAN>