Files
secondo/Optimizer/immediateplan.pl
2026-01-23 17:03:45 +08:00

2557 lines
82 KiB
Prolog

/*
----
This file is part of SECONDO.
Copyright (C) 2005, University in Hagen, Department of Computer Science,
Database Systems for New Applications.
SECONDO is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
SECONDO is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with SECONDO; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
----
//paragraph [10] title: [{\Large \bf ] [}]
//characters [1] formula: [$] [$]
//[ae] [\"{a}]
//[oe] [\"{o}]
//[ue] [\"{u}]
//[ss] [{\ss}]
//[Ae] [\"{A}]
//[Oe] [\"{O}]
//[Ue] [\"{U}]
//[**] [$**$]
//[toc] [\tableofcontents]
//[newpage] [\newpage]
[10] Modifications of the Query Optimizer for Secondo
Jan Engelkamp, August - September 2005
April 2006, Christian D[ue]ntgen. Adopted to work with recent optimizer.
[toc]
[newpage]
1 Introduction
The original query optimizer for the Secondo DBMS is contained
in a file named ~optimizer.pl~, which ca be found in the
~Optimizer~-directory of Secondo. Its idea and algorithm is described in detail
in R.H. G[ue]ting, T. Behr, V. T. de Almeida, Z. Ding, F. Hoffmann, and
M. Spiekermann, Secondo: An Extensible DBMS Architecture and Prototype.
Fernuniversit[ae]t Hagen, Informatik-Report 313, March 2004,
www.informatik.fernuni-hagen.de/import/pi4/papers/Secondo04.pdf, page 7 ff.
The present file named ~modifications.pl~ contains two modifications
of the query optimizer of Secondo, wich are:
1 an algorithm, that immediately constructs the cheapest plan for a
given query, instead of constructing a predicate order graph first and
then determining the cheapest plan by tracing the shortest path through
the graph.
2 several alternative implementations for the set that contains 'the
already created but not yet ticked off nodes', which needs the
algorithm, that constructs the cheapest plan for a given query.
To use these modifications please copy the present file as ~modifications.pl~
into the ~Optimizer~-directory of Secondo, start ~SecondoPL~ as usual
and then enter the Prolog-command ~consult('modifications.pl')~ respectively
~['modifications.pl']~.
Each contained modification can be turned on and off using the following
commands:
1 ~setOption(immediatePlan)~ extends respectively changes the knowledge-base of
the optimizer in that way, that it immediately constructs the cheapest plan
for given queries. ~delOption(immediatePlan)~ turns this modification off again.
2 The four alternative implementations for the set containing 'the
already created but not yet ticked off nodes' (REACHED-NODES)
are numbered 1, 2, 3 and 4. ~altRNSImplementation~ with one of the
numbers as its argument retracts the currently used implementation
from the knowledge-base and stores the chosen alternative
implementation instead. ~altRNSImplementation(off)~ reconsults the
file ~modifications.pl~ and consequently stores the original
implementation for the set REACHED-NODES again.
[newpage]
2 Immediate Construction of the Cheapest Plan for a given Query
2.1 The Idea and Algorithm
The original algorithm of the Secondo-optimizer first creates the
complete predicate order graph (POG) for a given query i. e. all its
nodes, edges, plan edges and cost edges and stores these elements as
facts into the knowledge-base. Only after the creation of the POG is
completed, the search of the shortest path through the POG starts.
(A POG for n predicates has "n!"[1] paths from the start-node 0 to
the destination-node "2^{n} - 1"[1].) The searches result i. e. the
shortest path through the graph is mapped onto the cheapest plan
for the given query.
The drawback of this algorithm is quite obvious: Always the complete graph with
all its "n * 2^{n-1}"[1] edges and "2^{n}"[1] nodes is created. (Not to mention
the plan and cost edges correponding to every edge of the graph.) So one can
assume, that it would cause a distinct improvement in efficiency of the above
described algorithm, if one would not create the complete POG first and then
search for the shortest path through it, but if the shortest path would
immediately be created.
An accordantly modified algorithm for the Secondo-optimizer is the following
one. In it two sets of nodes are used: REACHED-NODES for nodes that are already
created but not yet completely processed and TICKED-OFF-NODES for completely
processed nodes.
----
empty the set REACHED-NODES;
empty the set TICKED-OFF-NODES;
set the distance stored for the source-node to zero;
set the path stored for the source-node to [];
insert the source-node into the set REACHED-NODES;
while (REACHED-NODES is not empty) do
{ find the node cN in REACHED-NODES
with minimal distance to the source-node ;
construct/consider all successor-nodes of cN;
for (every single successor-node sN of cN) do
{ if (sN is in TICKED-OFF-NODES) then
{ do nothing; }
if (sN is not in TICKED-OFF-NODES but in REACHED-NODES) then
{ construct the edge from cN to sN;
construct every plan edge from cN to sN;
assign the estimated size of the interim result of sN to sN;
construct the cost edges from cN to sN;
find the cheapest of the cost edges from cN to sN;
correct the distance and path stored for sN, if necessary;
}
if (sN is neither in TICKED-OFF-NODES nor in REACHED-NODES) then
{ construct the edge from cN to sN;
construct every plan edge from cN to sN;
assign the estimated size of the interim result of sN to sN;
construct the cost edges from cN to sN;
find the cheapest of the cost edges from cN to sN;
calculate the distance and path of sN and store it;
insert sN in REACHED-NODES;
}
}
insert cN in TICKED-OFF-NODES;
delete cN from REACHED-NODES;
}
read the distance and path stored for the destination-node,
that is the result;
----
2.2 Determining the Benefits for the Optimizers Efficiency
A predicate order graph (POG) for n predicates is a n-dimensional cube. It contains "2^{n}"[1] nodes. Every node has n neighbours. Hence in every loop of the above described algorithm at most n neighbours of the one current node are created respectively considered. At most, because not every neighbour is also a successor of the current node: Only the source-node has "n"[1] successors, its successors do have just "n-1"[1] successors, their successors do have just "n-2"[1] successors and so on. The destination-node has no successor. In every loop only those edges with the current node as its source have to be created, that lead to a successor, that is not yet completely processed (i. e. that is not yet a member of the set TICKED-OFF-NODES). Hence it will never be necessary to create every edge of a POG, if this graph consists of more than two nodes.
How many successors of a current node are already ticked off (processed) depends not only on the progress that the processing of the algorithm has made so far, but in what order the nodes of the POG become the current nodes of the algorithms loop. Hence a general conclusion for the benefit of efficiency, that causes the fact, that those edges, that would lead to nodes, which are already ticked off, cannot be given.
Additionally while processing the algorithm, by far not every node of the POG
needs to be regarded as the current node. A program can stop as soon as the
destination-node is reached. (But then the above described algorithm has to
be changed a bit; see chapter 2.5.1.) Hence often a large part of the POG can
be pruned away. In R.H. G[ue]ting et al., Secondo: An Extensible DBMS
Architecture and Prototype, page 16, one can find a small table which shows,
that for some examples instead of 256 only 18, instead of 512 only 104 and
instead of 1024 only 109 nodes are considered while searching the shortest path
through a POG. Thus using the above described algorithm (respectively the one
described in chapter 2.5.1) one can achieve that a lot less nodes have to be
created than are contained in the complete POG. For example if the clause
~test6~ (see chapter 2.6.2) is called, only 38 are created but 128 nodes are in
the complete POG, and if ~test23~ is called, only 224 instead of 2.048 nodes are
created.
This fact does probably have a much bigger effect on the optimizers efficiency
than the one, that it is not necessary to create edges to successors which are
already ticked off. But as already mentioned the order in which nodes, that have
to be considered, become the current nodes of the algorithms loop depends on the
given query. Hence a general conclusion of how big the improvement coming from
the fact, that it is by far not necessary to create every node of the POG, can't
be given.
To make reliable conlusions regarding the overall benefits for the optimizers
efficiency and to find out, if the modification of the optimizer is in real more
efficient than the original one, it is necessary to measure and compare times
needed for optimizing example-queries using first the original optimizer and the
other time the modified one.
This will be done in a following chapter named 'Measuring und Comparing
Times needed for Optimization'.
2.3 Preparation
To extend respectively change the knowledge-base of the optimizer in that
way, that it immediately constructs the cheapest plan for given queries,
please enter ~setOption(immediatePlan)~ after starting ~SecondoPL~ as usual.
This will aso ensure, that this file (~immediateplan.pl~) is consulted.
~delOption(immediatePlan)~ turns this modification off, so one can use
the original implemented algorithm of the optimizer again.
The time needed for constructing respectively finding the cheapest plan
respectively shortest path will be given, if one uses ~setOption(immediatePlanTime)~.
There are goals in several clauses existing just for observing, how the
modified optimizer in detail works. They can be activated by switching on debugmode
~setOption(debug)~ and setting debug level to ~immPath~ (by ~debugLevel(immPath)~).
This will result in a lot of information about how the optimizer works being
printed on your screen during construction of the cheapest plan.
April 2006, Christian D[ue]ntgen. ~Observe~ was replaced by ~dm~ and/or ~dc~
commands. Set ~setOption(debug), debugLevel(immPath)~ to observe the algorithm.
Changed integration with optimizer.pl by removing dynamic code
modification (~immPathCreation/1~, ~immPathCreation/2~) with static predicate
~immPlanTranslate/4~ and ~optimizerOption/1~ for the sake of a common interface.
Switching of optimizer options is now handled in file ~calloptimizer.pl~.
Mai 2006, Christian D[ue]ntgen. Time measuring code moved to ``optimizer-pl'' as a
general optimizer option.
*/
:- dynamic immPathCreation/4.
:- dynamic createEmptyReachedNodesSet/1.
:- dynamic putIntoReachedNodesSet/3.
:- dynamic isInReachedNodesSet/2.
:- dynamic readAlreadyReachedNode/3.
:- dynamic getMinimalDistantNode/3.
:- dynamic deleteOutOfReachedNodesSet/3.
:- dynamic isEmpty/1.
% This is the toplevel predicate for this extension, called by optimizer.pl
% when ~optimizerOption(immediatePlan)~ is defined.
immPlanTranslate(Select from Rels where Preds, Stream, Select, Cost) :-
optimizerOption(immediatePlan),
immPathCreation(Rels, Preds, Stream, Cost),
!.
% Fall-back case, that should never be called
immPlanTranslate(_, _, _, _) :-
write('\nWARNING: Fall-back clause of immPlanTranslate/4.'),
throw(immPlanTranslate_fallback_case_reached), fail, !.
immPlanPrintWelcomeMod :-
nl, write('*** Instead of creating the predicate order'),
nl, write('*** graph and searching the shortest path'),
nl, write('*** through it, from now on the shortest path'),
nl, write('*** will immediately be created.'),
nl,
nl, write('*** Please type delOption(immediatePlan) to use'),
nl, write('*** the originally implemented algorithm of the'),
nl, write('*** Secondo-optimizer again.'), nl.
immPlanPrintWelcomePOG :-
nl, write('*** From now on the predicate order graph first'),
nl, write('*** will be created and after this the shortest'),
nl, write('*** path through it will be searched.'),
nl,
nl, write('*** Please type setOption(immediatePlan) to use the'),
nl, write('*** modification of the optimizer again,'),
nl, write('*** that immediately creates the shortest path.'), nl.
/*
[newpage]
2.4 Initial Activities
2.4.1 Sweeping the Knowledge-Base
Before the cheapest plan for a query can be constructed, one has to guarantee,
that no facts are part of the knowledge-base, that have been put into it, when
a former query has been evaluated.
*/
sweepKnowledgeBase(EmptyReachedNodesSet) :-
deleteArguments,
deleteNodes,
deleteEdges,
deletePlanEdges,
deleteVariables,
deleteSizes,
deleteCostEdges,
createEmptyReachedNodesSet(EmptyReachedNodesSet),
emptyTickedOffSet,
deleteMinDistNodes,
( optimizerOption(intOrders(_)) -> retractAllOrderInformation ; true ).
/*
The last call is only needed, if one wants to count or to analyse the nodes
that become the nodes with the minimal distance to the source-node while
creating the shortest path through the POG (see chapter 2.5.16).
2.4.2 Storing ARP-Tripples
Given a list of relations, they all are packed into
argument-relation-predicate-tripples of the form arp(arg(i), [relation], []).
All these trippels are stored into the knowledge-base.
*/
assertArguments(Rels, Partition) :-
length(Rels, Length),
reverse(Rels, RevRels),
partition(RevRels, Length, Partition).
/*
2.5 The Construction of the Cheapest Plan
2.5.1 Implementation of the Algorithm
*/
immPathCreation(Relations, Predicates, Plan, Cost) :-
sweepKnowledgeBase(EmptyReachedNodesSet),
assertArguments(Relations, Partition),
length(Predicates, NoOfPreds),
dc(immPath, ( write('Number of predicates in the query: '),
write(NoOfPreds), nl,
write('All the predicates of the query: '),
write(Predicates), nl, nl)),
SourceNodeNo = 0,
asserta(node(SourceNodeNo, [], Partition)),
putIntoReachedNodesSet(EmptyReachedNodesSet,
node(SourceNodeNo, 0, []),
ReachedNodesSet),
immPathCreationLoop(ReachedNodesSet, Predicates, NoOfPreds),
dc(immPath, (write('All nodes are processed.'), nl)),
DestinationNodeNo is 2^NoOfPreds -1,
getTickedOffNode(DestinationNodeNo,
node(DestinationNodeNo, Cost, ShortestPath)),
plan(ShortestPath, Plan).
immPathCreationLoop(ReachedNodesSet, Predicates, NoOfPreds) :-
% if
not(isEmpty(ReachedNodesSet)),
!,
% then
processReachedNodesSet(ReachedNodesSet, NewReachedNodesSet,
Predicates, NoOfPreds),
immPathCreationLoop(NewReachedNodesSet,
Predicates, NoOfPreds).
immPathCreationLoop(_, _, _) :-
% if isEmpty(ReachedNodesSet) return true
dc(immPath, (write('The set REACHED-NODES is empty.'), nl, nl)),
true.
/*
For a correct implementation of the above described algorithm, the next
clause ~processReachedNodesSet~ would have to be the following one:
----
processReachedNodesSet(ReachedNodesSet, NewNewReachedNodesSet,
Predicates, NoOfPreds) :-
getMinimalDistantNode(ReachedNodesSet, NDPNode,
NewReachedNodesSet),
mapNPPNode_NDPNode(NPPNode, NDPNode),
createSuccessors(NPPNode, NoOfPreds,
Predicates, SuccessorList),
processSuccessors(NewReachedNodesSet, NewNewReachedNodesSet,
NPPNode, NDPNode, SuccessorList),
tickOff(NDPNode).
----
But then inevitably all the nodes of the POG would be created, because
the criteria causing the loop to stop would be the emptiness of the set
REACHED-NODES (as intended and corresponding to the algorithm).
However the aim of the immediate construction of the shortest path
respectively cheapest plan is to stop the construction of nodes and
edges as soon as the destination-node is reached.
Thus the above described algorithm must be changed as follows:
----
empty the set REACHED-NODES;
empty the set TICKED-OFF-NODES;
set the distance stored for the source-node to zero;
set the path stored for the source-node to [];
insert the source-node into the set REACHED-NODES;
while (REACHED-NODES is not empty
and the destination-node is not yet reached) do
{ ... }
read the distance an path stored for the destination-node,
that is the result;
----
Now there are two criterias causing the loop to stop: first the emptiness
of the set REACHED-NODES and second the fact, that the destination-node
is reached.
This leads to the following implementation of ~processReachedNodesSet~:
*/
processReachedNodesSet(ReachedNodesSet, NewNewReachedNodesSet,
Predicates, NoOfPreds) :-
getMinimalDistantNode(ReachedNodesSet, NDPNode,
NewReachedNodesSet),
dc(immPath, (write('Minimal distant node: '), write(NDPNode), nl, nl)),
tickOff(NDPNode),
mapNPPNode_NDPNode(NPPNode, NDPNode),
dc(immPath, assertz(minDistNode(NPPNode))),
createSuccessors(NPPNode, NoOfPreds,
Predicates, SuccessorList),
dc(immPath, (write('Successors are created.'), nl, nl)),
% if
not(destinationNode(NDPNode, NoOfPreds)),
!,
% then
processSuccessors(NewReachedNodesSet, NewNewReachedNodesSet,
NPPNode, NDPNode, SuccessorList),
dc(immPath, (write('*** Successors processed. ***'), nl, nl)),
true.
processReachedNodesSet(_, EmptyReachedNodesSet, _, _) :-
% else i. e. the destination-node is reached
createEmptyReachedNodesSet(EmptyReachedNodesSet).
destinationNode(node(NodeNo, _, _), NoOfPreds) :-
NodeNo >= 2^NoOfPreds -1.
/*
The second clause
----
processReachedNodesSet(_, EmptyReachedNodesSet, _, _) :-
createEmptyReachedNodesSet(EmptyReachedNodesSet).
----
realizes some kind of trick. If the destination-node is reached, the
set REACHED-NODES ist emptied and thus the criteria causing the loop to stop
is fulfilled. It would be more neatly (and a more correct implementation of the
algorithm), if both of the two criterias 'the set REACHED-NODES is empty' and
'the destination-node is reached' would cause the loop to stop, but therefore
more complex clauses would be needed, which are by far less comprehensible.
For an exact implementation of the algorithm, there must also be a goal like
~deleteOutOfReachedNodesSet(SetIn, Node, SetOut)~ at the end, that deletes the
current node out of the set named REACHED-NODES. But a goal like that is not
needed, because ~getMinimalDistantNode(SetIn, Node, SetOut)~ not only delivers
the node with the current minimal distance to the source-node, but at the same
time deletes the node out of the given set of already reached nodes.
The call ~assertz(minDistNode(NPPNode))~ in the first clause is needed, if one wants
to count or to analyse the nodes that become the nodes with the minimal
distance to the source-node while creating the shortest path through the POG.
After the shortest path has been created, it is possible to call
~countMinDistNodes~ and ~writeMinDistNodes~. If these nodes are of no interest,
the above call should be commented out.
2.5.2 Creating the Successors of a Given Node
*/
createSuccessors(Node, NoOfPreds, Predicates, SuccessorList) :-
successorNodes(Node, NoOfPreds,
Predicates, 1, [], SuccessorList).
/*
The arguments of
----
successorNodes(node(NodeNo, NodePreds, Partition), NoOfPreds, Predicates,
PredPos, Accumulator, SuccessorList)
----
have the following meaning:
1 ~node(NodeNo, NodePreds, Partition)~ is the current node, of which
successors are created respectively considered. ~NodePreds~ is its list of
predicates.
2 ~NoOfPreds~ is the total of all existing predicates in the given query.
3 ~Predicates~ is a list containing all the queries predicates.
4 ~PredPos~ is the position of the currently for the construction of the next
successor considered predicate. It must be initialized with 1 when the clause
~successorNodes~ is called first. (It would be easier if it would be initialized
with 0, but in ~optimizer.pl~ the first predicate has the position 1 instead of
0. Thus here the same is done.)
5 ~Accumulator~ is an accumulator for the list of successors. Initially it
must be the empty list.
6 ~SuccessorList~ is the list of successors of the current node.
*/
successorNodes(_, NoOfPreds, _, PredPos,
SuccessorList, SuccessorList) :-
PredPos > NoOfPreds,
!. % All successors are constructed.
successorNodes(node(NodeNo, NodePreds, Partition),
NoOfPreds, [Predicate|RestAllPreds], PredPos,
Accumulator, SuccessorList) :-
% if
member(Predicate, NodePreds),
!,
% then
NewPredPos is PredPos + 1,
successorNodes(node(NodeNo, NodePreds, Partition),
NoOfPreds, RestAllPreds, NewPredPos,
Accumulator, SuccessorList).
successorNodes(node(NodeNo, NodePreds, Partition),
NoOfPreds, [Predicate|RestAllPreds], PredPos,
Accumulator, SuccessorList) :-
% else i.e. not(member(Predicate, NodePreds)),
PredNo is 2^(PredPos-1),
NewNodeNo is NodeNo + PredNo,
createSuccessor(NewNodeNo, Predicate, PredNo, NodePreds,
Partition, NewNode),
NewPredPos is PredPos + 1,
successorNodes(node(NodeNo, NodePreds, Partition),
NoOfPreds, RestAllPreds, NewPredPos,
[succ(NewNodeNo, NewNode, PredNo, Predicate)|Accumulator],
SuccessorList).
/*
2.5.3 Creating a New Node
*/
createSuccessor(NewNodeNo, _, _, _, _, NewNode) :-
% if
node(NewNodeNo, StoredNodePreds, Partition),
!,
% then
NewNode = node(NewNodeNo, StoredNodePreds, Partition).
createSuccessor(NewNodeNo, Predicate,
PredNo, NodePreds, Partition, NewNode) :-
% else
NewNode = node(NewNodeNo,
[Predicate|NodePreds], NewPartition),
assertz(NewNode),
copyPart(Predicate, PredNo, Partition, NewPartition),
retract(node(NewNodeNo, _, _)),
assertz(NewNode).
/*
To avoid unnecessary duplications of clauses and to use the clauses of the file
~optimizer.pl~ when possible, ~copyPart~ is used as a goal in ~createSuccessor~.
But ~copyPart~ can only be used if a node that contains ~NewPartition~
as its third element is already part of the knowledge-base. However
~NewPartition~ is not yet instantiated when ~copyPart~ is called. That's the
reason, why ~NewNode~ is first asserted and then, after ~NewPartition~ has been
instantiated, is rectracted and again (with the meanwhile instantiated
~NewPartition~) asserted.
2.5.4 Different Representations for Nodes
*/
createNDPNode(node(NodeNo, _, _), node(NodeNo, _, _)).
createNDPSuccessor(succ(NodeNo, _, _, _), node(NodeNo, _, _)).
mapNPPNode_NDPNode(node(NodeNo, Predicates, Partition), node(NodeNo, _, _)) :-
node(NodeNo, Predicates, Partition).
/*
There exist two structures in the knowledge-base named ~node~ containing three
elements each, that realize different representations for nodes.
They are called ~NPPNodes~ and ~NDPNodes~.
~NPPNode~ stands for number-predicates-partition-node.
Its form is ~node(NodeNo, Predicates, Partition)~.
~NDPNode~ stands for number-distance-path-node.
Its form is ~node(NodeNo, Distance, Path)~.
At first one might think that it is a bit awkward to use those different
~node~-structures in parallel. One might think that is would be better to use
only one structure like ~node(NodeNo, Predicates, Partition, Distance, Path)~.
But notice: Facts of the form ~node(NodeNo, Predicates, Partition)~ are part
of the knowledge-base, but ~node(NodeNo, Distance, Path)~-facts only exist
as part of the elements of the set that describes the alredy reached but not yet
ticked off nodes, called REACHED-NODES in the above described algorithm.
The first clause ~createNDPNode~ is currently not used but added for
comprehensibility.
2.5.5 Processing the Successors of a Node
*/
processSuccessors(ReachedNodesSet, ReachedNodesSet, _, _, []) :-
!.
processSuccessors(ReachedNodesSet, NewReachedNodesSet, NPPNode,
NDPNode, [Successor]) :-
dc(immPath, (write('Check Successor: '), write(Successor), nl)),
checkSuccessor(ReachedNodesSet, NewReachedNodesSet, NPPNode,
NDPNode, Successor),
dc(immPath, (write('Successor checked.'), nl, nl)),
true.
processSuccessors(ReachedNodesSet, NewNewReachedNodesSet, NPPNode,
NDPNode, [Successor|SuccessorList]) :-
dc(immPath, (write('Check Successor: '), write(Successor), nl)),
checkSuccessor(ReachedNodesSet, NewReachedNodesSet, NPPNode,
NDPNode, Successor),
dc(immPath, (write('Successor checked.'), nl, nl)),
processSuccessors(NewReachedNodesSet, NewNewReachedNodesSet,
NPPNode, NDPNode, SuccessorList).
/*
The true in the second clause is only needed for the above described workaround
concerning the search and replace of ~o b s e r v e~ if one wants to watch in
detail how the algorithm works.
Note: When ~getMinimalDistantNode~ is called as a goal finally a structure
containing a ~NDPNode~ is retracted from the knowledge-base. This takes place
even if a clause that called ~getMinimalDistantNode~ fails after calling this
goal. For details please consider the file ~boundary.pl~.
However the description here causes one to presume that a call of
~getMinimalDistantNode~ leads to a deletion of a ~NDPNode~ out of a set (that
is usually realized as a list in Prolog). Such kind of deletion out of a set
would not take place, if the clause ~processReachedNodesSet~ fails. Hence the
set named ~ReachedNodesSet~ in the clause ~immPathCreationLoop~ (the first
argument there) would not be empty after ~processReachedNodesSet~ failed, if
a set respectively a list would be used.
Because finally the call of ~getMinimalDistantNode~ retracts a fact out of the
knowledge-base, this takes place even if ~processReachedNodesSet~ fails.
Thus the first of the above clauses is necessary to guarantee, that a clause
calling ~processSuccessors~ doesn't even fail, if the destination-node, which
has no successors, is reached. This clause is currently not needed, because
the construction of the shortest path immediately stops, when the
destination-node is reached, but it is absolutely necessary, if all the nodes
and edges of the POG should be created.
2.5.6 Creating Edges to Successors
*/
:- dynamic checkSuccessor/5.
retractCheckSuccessorClauses :-
not(retractCheckSuccessorClause).
retractCheckSuccessorClause :-
retract(checkSuccessor(_, _, _, _, _) :- (_) ),
fail.
restoreCheckSuccessorClauses :-
retractCheckSuccessorClauses,
assert( checkSuccessor(ReachedNodesSet, ReachedNodesSet, _, _,
succ(SuccNodeNo, _, _, _)) :-
( isTickedOff(SuccNodeNo),
dc(immPath, (write('Successor is already ticked off.'), nl)),
!
)
),
assert( checkSuccessor(ReachedNodesSet, NewReachedNodesSet,
node(NodeNo, NodePreds, Partition), NDPNode,
succ(SuccNodeNo, _, PredNo, Predicate)) :-
( isInReachedNodesSet(ReachedNodesSet, node(SuccNodeNo, _, _)),
!,
dc(immPath, (write('Successor is not ticked off '),
write('but already reached.'), nl)),
createEdge(NodeNo, SuccNodeNo,
node(NodeNo, NodePreds, Partition),
PredNo, Predicate, Edge),
createPlanEdges(Edge, PlanEdges),
assignSize(Edge),
cheapestCostEdge(PlanEdges, CostEdge),
correctDistanceAndPath(NDPNode, SuccNodeNo, CostEdge,
ReachedNodesSet, NewReachedNodesSet)
)
),
assert( checkSuccessor(ReachedNodesSet, NewReachedNodesSet,
node(NodeNo, NodePreds, Partition), NDPNode,
succ(SuccNodeNo, _, PredNo, Predicate)) :-
( dc(immPath, (write('Successor is not ticked off '),
write('and not yet reached.'), nl)),
createEdge(NodeNo, SuccNodeNo,
node(NodeNo, NodePreds, Partition),
PredNo, Predicate, Edge),
createPlanEdges(Edge, PlanEdges),
assignSize(Edge),
cheapestCostEdge(PlanEdges, CostEdge),
dc(immPath, (write('Cheapest cost edge is determined: '),
write(CostEdge), nl)),
createNDPSuccessor(succ(SuccNodeNo, _, _, _), NDPSuccessor),
setDistanceAndPath(NDPNode, NDPSuccessor, CostEdge),
putIntoReachedNodesSet(ReachedNodesSet, NDPSuccessor,
NewReachedNodesSet)
)
).
:- restoreCheckSuccessorClauses.
/*
2.5.7 Creating a New Edge
*/
createEdge(NodeNo, NewNodeNo, Node, PredNo, Predicate, NewEdge) :-
% if
edge(NodeNo, NewNodeNo, Term, Result, Node, PredNo),
pred(Term,Predicate),
!,
% then
NewEdge = edge(NodeNo, NewNodeNo, Term, Result, Node, PredNo).
createEdge(NodeNo, NewNodeNo, Node, PredNo, Predicate, NewEdge) :-
% else
NewEdge = edge(NodeNo, NewNodeNo, _, _, Node, PredNo),
assertz(NewEdge),
newEdge(Predicate, PredNo, Node, NewEdge),
retract(edge(NodeNo, NewNodeNo, _, _, _, _)),
assertz(NewEdge).
/*
To avoid unnecessary duplications of clauses and to use the clauses of the file
~optimizer.pl~ when possible, ~newEdge~ is used as a goal in ~createEdge~.
But ~newEdge~ can only be used if a corresponding ~edge~-structure is already
part of the knowledge-base. However ~NewEdge~ is not yet instantiated when
~newEdge~ is called. That's the reason, why ~NewEdge~ is first asserted and
then, after ~newEdge~ has been called, is rectracted and again (with the
meanwhile instantiated elements) asserted.
2.5.8 Creating Plan Edges
*/
createPlanEdges(Edge, PlanEdges) :-
createPlanEdges(Edge),
allCorrPlanEdges(Edge, PlanEdges).
createPlanEdges(Edge) :-
not(createPlanEdge(Edge)).
createPlanEdge(edge(Source, Target, Term, Result, _, _)) :-
Term => Plan,
assert(planEdge(Source, Target, Plan, Result)),
fail.
/*
According to ~createPlanEdges~/0 and ~createPlanEdge~/0, which are defined in
~optimizer.pl~ the clauses ~createPlanEdges~/1 and ~createPlanEdge~/1 aim at
the creation of plan edges corresponding to a given Edge.
2.5.9 Getting Plan Edges
*/
allCorrPlanEdges(edge(Source, Target, _, Result, _, _), PlanEdges) :-
findall(planEdge(Source, Target, Plan, Result),
planEdge(Source, Target, Plan, Result),
PlanEdges).
getPlanEdges(EdgeList, PlanEdgeList) :-
getPlanEdges(EdgeList, [], PlanEdgeList).
getPlanEdges([], PlanEdgeList, PlanEdgeList).
getPlanEdges([Edge|EdgeList], Accumulator, PlanEdgeList) :-
allCorrPlanEdges(Edge, PlanEdges),
append(PlanEdges, Accumulator, AccumulatorPlus),
getPlanEdges(EdgeList, AccumulatorPlus, PlanEdgeList).
/*
The ~getPlanEdges~-clauses are currently not used, but have been useful for
testing. And they may be useful for further modifications of the optimizer.
2.5.10 Assigning Sizes and Selectivities
*/
assignSizestoSucc([]).
assignSizestoSucc([Edge|EdgeList]) :-
assignSize(Edge),
assignSizestoSucc(EdgeList).
assignSize(edge(Source, Target, Term, Result, _, _)) :-
assignSize(Source, Target, Term, Result).
/*
The ~assignSizestoSucc~-clauses are currently not used, but have been useful for
testing. And they may be useful for further modifications of the optimizer.
2.5.11 Creating Cost Edges
*/
createCostEdges([]).
createCostEdges([planEdge(Source, Target, Plan, Result)|PlanEdgeList]) :-
edgeSelectivity(Source, Target, Sel),
edge(Source, Target, Term, _, _, _),
pred(Term,Pred),
( optimizerOption(improvedcosts)
-> costterm(Plan, Source, Target, Result, Sel, Pred, Size, Cost)
; ( optimizerOption(nawracosts)
-> ( % nawracosts excludes immediatepath!
throw(error_Internal(immediateplan_createCostEdges(
[planEdge(Source, Target, Plan, Result)|PlanEdgeList])
::incompatible_option_nawracosts))
)
; cost(Plan, Sel, Pred, Size, Cost) % use standard cost functions
)
),
assert(costEdge(Source, Target, Plan, Result, Size, Cost)),
createCostEdges(PlanEdgeList).
/*
2.5.12 Getting the Cheapest Cost Edge
*/
cheapestCostEdge([planEdge(Source, Target, Plan, Result)|PlanEdgeList],
CostEdge) :-
createCostEdges([planEdge(Source, Target, Plan, Result)|PlanEdgeList]),
getCheapestCostEdge(Source, Target, CostEdge).
/*
Note: This clause could cause problems, if the given list of plan edges is
empty. But in the current context it is guaranteed, that the list is not empty.
*/
getCheapestCostEdge(Source, Target, CheapestCostEdge) :-
findall(costEdge(Source, Target, Plan, Result, Size, Cost),
costEdge(Source, Target, Plan, Result, Size, Cost),
CostEdges),
findCheapestCostEdge(CostEdges, CheapestCostEdge).
findCheapestCostEdge([CheapestCostEdge], CheapestCostEdge) :- !.
findCheapestCostEdge([costEdge(Source, Target, Plan,
Result, Size, Cost1)|Remainder],
costEdge(ResSource, ResTarget, ResPlan,
ResResult, ResSize, ResCost)) :-
findCheapestCostEdge(Remainder,costEdge(_, _, _, _, _, Cost2)),
% if
Cost1 =< Cost2,
!,
% then
costEdge(ResSource, ResTarget, ResPlan, ResResult, ResSize, ResCost)
= costEdge(Source, Target, Plan, Result, Size, Cost1).
findCheapestCostEdge([_|Remainder], CheapestCostEdge) :-
% else
findCheapestCostEdge(Remainder, CheapestCostEdge).
/*
One might expect, that the following clauses can be processed faster
than the above.
----
findCheapestCostEdge([costEdge(_, _, _, _, _, Cost1)|Remainder],
costEdge(Source2, Target2, Plan2,
Result2, Size2, Cost2)) :-
findCheapestCostEdge(Remainder,
costEdge(Source2, Target2, Plan2,
Result2, Size2, Cost2)),
% if
Cost1 >= Cost2,
!.
findCheapestCostEdge([CheapestCostEdge|_], CheapestCostEdge).
% else
----
But that is not true for a relatively small amount of elements in the
list, as tests have shown. For just a few elements in the list of
cost edges, the used clauses are a bit more efficient. And currently
just a few plan edges respectively cost edges are created for every
edge of the POG, hence the list always contains just a few elements.
2.5.13 Determining and Correcting Dinstances and Paths
*/
setDistanceAndPath(node(NodeNo, Distance, Path),
node(SuccNo, SuccDistance, SuccPath),
costEdge(NodeNo, SuccNo, Term, Result, Size, Cost)) :-
SuccDistance is Distance + Cost,
dc(immPath, (write('Distance of successor '), write(SuccNo),
write(' determined: '), write(SuccDistance), nl)),
append(Path,
[costEdge(NodeNo, SuccNo, Term, Result, Size, Cost)],
SuccPath).
correctDistanceAndPath(node(NodeNo, Distance, Path), SuccNo,
costEdge(NodeNo, SuccNo, Term, Result, Size, Cost),
ReachedNodesSet, NewNewReachedNodesSet) :-
readAlreadyReachedNode(ReachedNodesSet, SuccNo,
node(SuccNo, SuccDistance, SuccPath)),
NewSuccDistance is Distance + Cost,
% if
SuccDistance > NewSuccDistance,
!,
% then
deleteOutOfReachedNodesSet(ReachedNodesSet,
node(SuccNo, SuccDistance, SuccPath),
NewReachedNodesSet),
append(Path,
[costEdge(NodeNo, SuccNo, Term, Result, Size, Cost)],
NewSuccPath),
dc(immPath, (write('Distance of successor is corrected: '),
write(NewSuccDistance), nl)),
putIntoReachedNodesSet(NewReachedNodesSet,
node(SuccNo, NewSuccDistance, NewSuccPath),
NewNewReachedNodesSet).
correctDistanceAndPath(_, _, _, ReachedNodesSet, ReachedNodesSet).
% else do nothing
/*
2.5.14 Ticking Off Nodes - The Interface
The following rules just exist for comprehensibility.
Actually they are not absolutely necessary.
*/
% enable modifications by intOrder:
:- dynamic emptyTickedOffSet/0.
:- dynamic tickOff/1.
:- dynamic isTickedOff/1.
:- dynamic getTickedOffNode/2.
retractCurrentTOSImplementation :-
retract(emptyTickedOffSet :- (_)),
retract(tickOff(_) :- (_)),
retract(isTickedOff(_) :- (_)),
retract(getTickedOffNode(_, _)), !.
retractCurrentTOSImplementation :- !.
restoreTOSImplementation :-
retractCurrentTOSImplementation,
assert( emptyTickedOffSet :- ( emptyCenter ) ),
assert( tickOff(node(NodeNo, Distance, Path)) :-
( assert(center(NodeNo, node(NodeNo, Distance, Path))) )
),
assert( isTickedOff(NodeNo) :- ( center(NodeNo, _) ) ),
assert( getTickedOffNode(NodeNo, Node) :- ( center(NodeNo, Node) ) ).
:- restoreTOSImplementation.
/*
2.5.15 The Reached-Nodes Set - The Interface
The following rules primarily exist for comprehensibility. They don't do more
than map their calls onto calls of the abstract data type ~boundary~, that is
realized in the file ~boundary.pl~. But they form some kind of interface, that
is useful for further modifications (like the one described in chapter 3,
that would be by far more complex without this interface).
*/
retractCurrentRNSImplementation :-
retract(
createEmptyReachedNodesSet(_) :- (_)
),
retract(
putIntoReachedNodesSet(_, _, _) :- (_)
),
retract(
isInReachedNodesSet(_, _) :- (_)
),
retract(
readAlreadyReachedNode(_, _, _) :- (_)
),
retract(
getMinimalDistantNode(_, _, _) :- (_)
),
retract(
deleteOutOfReachedNodesSet(_, _, _) :- (_)
),
retract(
isEmpty(_) :- (_)
), !.
retractCurrentRNSImplementation :- !.
restoreRNSImplementation :-
retractCurrentRNSImplementation,
assert( createEmptyReachedNodesSet(ReachedNodesSet) :-
( b_empty(ReachedNodesSet) )
),
assert( putIntoReachedNodesSet(ReachedNodesSet, Node, NewReachedNodesSet) :-
( b_insert(ReachedNodesSet, Node, NewReachedNodesSet) )
),
assert( isInReachedNodesSet(ReachedNodesSet, node(NodeNo, _, _)) :-
( b_memberByName(ReachedNodesSet, NodeNo, node(NodeNo,_ , _)) )
),
assert( readAlreadyReachedNode(ReachedNodesSet, NodeNo, Node) :-
( b_memberByName(ReachedNodesSet, NodeNo, Node) )
),
assert( getMinimalDistantNode(ReachedNodesSet, Node, NewReachedNodesSet) :-
( b_removemin(ReachedNodesSet, Node, NewReachedNodesSet) )
),
assert( deleteOutOfReachedNodesSet(ReachedNodesSet, node(NodeNo, _, _),
NewReachedNodesSet) :-
( b_deleteByName(ReachedNodesSet, NodeNo, NewReachedNodesSet) )
),
assert( isEmpty(ReachedNodesSet) :-
( b_isEmpty(ReachedNodesSet),
dc(immPath, (write('There are no more reached '),
write('but not yet ticked off nodes.'), nl)),
true
)
).
:- restoreRNSImplementation.
/*
The true in the last clause is only needed for the above described
workaround concerning the search and replace of ~o b s e r v e~ if
one wants to watch in detail how the algorithm works.
2.5.16 The Nodes with Minimal Distance to the Source-Node
In the clause ~processReachedNodesSet~ a call ~assertz(minDistNode(NPPNode))~
is included but normally commented out (see chapter 2.5.1). It is only needed,
if one wants to count or to analyse the nodes that become the nodes with the
minimal distance to the source-node while creating the shortest path through
the POG. After the shortest path has been created, it is possible to call
~countMinDistNodes~ and ~writeMinDistNodes~.
*/
:- dynamic minDistNode/1.
deleteMinDistNodes :-
retractall(minDistNode(_)).
writeMinDistNodes :-
not(writeMinDistNode).
writeMinDistNode :-
minDistNode(node(NodeNo, _, _)),
write('Node: '), write(NodeNo), nl,
fail.
countMinDistNodes :-
findall(minDistNode(Node), minDistNode(Node), Nodes),
length(Nodes, Length),
write(Length), write(' nodes were found '),
write('(including the destination node).'), nl.
/*
2.6 Measuring und Comparing Times needed for Optimization
April 2006, Christian D[ue]ntgen:
The text of this and the following sub-sections of section 2 have not been adopted to the current
implementation (regarding integration into the optimizer and switching between options).
2.6.1 The Procedure
*/
checkCalcTime(Head):-
getTime(Head, Time2),
Time3 is 1000*Time2,
write('Time needed for calculating the overall result: '),
write(Time3), nl.
countNodes :-
findall(node(NodeNo, Predicates, Partition),
node(NodeNo, Predicates, Partition), Nodes),
length(Nodes, Length),
write(Length), write(' nodes were found.'), nl.
countPlanEdges :-
findall(planEdge(Source, Target, Plan, Result),
planEdge(Source, Target, Plan, Result),
PlanEdges),
length(PlanEdges, Length),
write(Length), write(' plan edges were found.'), nl.
/*
~checkCalcTime~ with the following test-clauses as its argument has been called ten times for each test-clause. Five times after ~immPathCreation(on, time)~ was called and five times after ~immPathCreation(off, time)~ was called.
Additionally the actually created nodes and plan edges have been count.
It was ensured that all needed selectivities were already available, hence it
was not necessary, that the optimizer had to call the Secondo-kernel. For to
ensure, that the kernel was never called, all test-clauses were called at least
once in advance, ~SecondoPL~ then was quitted and afterwards newly started and
~secondo('open database opt', Res).~ was not called in the context of the
surveys.
Thus the file ~storedSels.pl~ contained the following entries:
----
storedSel(orte:ort=plz:ort, 0.000560748).
storedSel(plz:pLZ=44225, 0.00233645).
storedSel(staedte:bev>270000, 0.344828).
storedSel(staedte:sName starts "S", 0.103448).
storedSel(orte:ort contains "dorf", 0.04).
storedSel((plz:pLZ mod 13)=0, 0.0630841).
storedSel(staedte:sName=orte:ort, 0.00155172).
storedSel(staedte:pLZ=plz:pLZ, 4.02836e-05).
storedSel(staedte:vorwahl=orte:vorwahl, 0.00275862).
storedSel(staedte:kennzeichen=orte:kennzeichen, 0.00293103).
storedSel(staedte:bev*1000=orte:bevT, 0.000172414).
storedSel(staedte:kennzeichen starts "A", 0.0344828).
storedSel(staedte:pLZ<60000, 1.01724).
storedSel(staedte:pLZ>50000, 0.0172414).
storedSel(orte:kennzeichen starts "A", 0.04).
storedSel(staedte:sName contains "burg", 0.137931).
storedSel(staedte:sName starts "D", 0.0862069).
storedSel(staedte:kennzeichen starts "D", 0.0862069).
storedSel(staedte:sName contains "dorf", 0.0344828).
storedSel(staedte:pLZ>40000, 0.0172414).
storedSel(staedte:pLZ<50000, 1.01724).
----
The used computer for the tests was a Fujitsu Siemens
Amilo L 1300 Notebook with an Intel Celeron M 370 1.5 GHz
processor and 512 MB RAM.
The operating system was Linux, distribution 9.3 from the SUSE distributor with kernel 2.6.11.4.
Furthermore SWI-Prolog 5.0.10 was used.
With the results of these calls a table was built with the following columns:
1 ~nop~ = number of predicates of the test-query
2 ~functor~ = the functor of the test-clause that leads to the evaluation
of the test-query
3 ~on/off~ = indicating, of ~immPathCreation(on)~ or ~immPathCreation(off)~
was called beforehand; 1 means ~on~, 0 means ~off~
4 ~non~ = number of nodes created
5 ~nope~ = number of plan edges created
6 ~ctimes~ = times needed for either creating the POG and searching the
shortest path through it or creating the shortest path immediately; five tests
cause five times
7 ~ttimes~ = times needed for calculating the overall (total) result; five
tests cause five times
8 ~actime~ = the average of the five ~ctimes~ (Measurement results that are
obviously out of bound are not considered.)
9 ~attime~ = the average of the five ~ttimes~ (Measurement results that are
obviously out of bound are not considered.)
A last column headed ~impactime~ gives the improvement of the average time, that
is needed for either creating the POG and searching the shortest path through it
or creating the shortest path immediately (~actime~), hence compares the calls
of similar test-clauses when once ~immPathCreation~ is set to ~on~ and once to
~off~. The given values have to be interpreted as follows: The average time that
needed the originally implemented algorithm for creating the POG and searching
the shortest path through it is set to 100 per cent. If the given
~impacttime~-value is n, then the implementation of the modified algorithm on
average only needs (100-n) per cent of it, hence n per cent of the originally
needed time is cut down.
In other words: An 'improvement' of n per cent means, that n per cent
of the time needed for creating the POG and searching the shortest path through
it is not needed, if one immediately creates the shortest path, hence one has
achieved a reduction of time of n per cent. (An improvement/reduction of 50 for
example means, that only half of the original time is still needed, an
improvement of 95, that only 5 per cent is still needed.)
In a first survey mainly typical every-day-life-queries were used, that had also
been used in other contexts for testing and explaining the Secondo-system. (The
results were already impressively.) In a subsequent survey more complex queries
with a lot more predicates have been tested.
2.6.2 The Test-Clauses
The following clauses are identical to the clauses ~example14~/2,
~example15~/2, ~example16~/2, ~example20~/2 and ~example21~/2 from the file
~optimizer.pl~. They are listed here just for a better clarity.
*/
test1(Query, Cost) :- optimize(
select * from [staedte as s, plz as p]
where [p:ort = s:sname, p:plz > 40000, (p:plz mod 5) = 0],
Query, Cost
).
test2(Query, Cost) :- optimize(
select * from staedte where bev > 500000,
Query, Cost
).
test3(Query, Cost) :- optimize(
select * from [staedte as s, plz as p]
where [s:sname = p:ort, p:plz > 40000],
Query, Cost
).
test4(Query, Cost) :- optimize(
select *
from [staedte as s, plz as p]
where [
p:ort = s:sname,
p:plz > 40000,
s:bev > 300000],
Query, Cost
).
test5(Query, Cost) :- optimize(
select *
from [staedte, plz as p1, plz as p2, plz as p3]
where [
sname = p1:ort,
p1:plz = p2:plz + 1,
p2:plz = p3:plz * 5],
Query, Cost
).
/*
The following clause is identical to ~example18~/2 without the
predicate 'bev is greater than 300.000'.
*/
test6(Query, Cost) :- optimize(
select *
from [staedte, plz as p1]
where [
sname = p1:ort,
bev < 500000,
p1:plz > 50000,
p1:plz < 60000,
kennzeichen starts "W",
p1:ort contains "burg",
p1:ort starts "M"],
Query, Cost
).
/*
Further tests:
*/
test7(Query, Cost) :- optimize(
select [sname, bev]
from [staedte, plz as p1]
where [
p1:ort starts "M",
kennzeichen starts "W",
p1:ort contains "burg",
bev < 500000,
p1:plz > 50000,
p1:plz < 60000,
sname = p1:ort
]
orderby [p1:ort asc, p1:plz desc],
Query, Cost
).
test8(Query, Cost) :- optimize(
select [sname, bev]
from staedte
where [bev>270000, sname starts "S"],
Query, Cost
).
test9(Query, Cost) :- optimize(
select *
from [orte as o, plz as p]
where [o:ort = p:ort, o:ort contains "dorf", (p:plz mod 13) = 0],
Query, Cost
).
test10(Query, Cost) :- optimize(
select [sname, bev div 100 as bevintausend]
from staedte,
Query, Cost
).
test11(Query, Cost) :- optimize(
select [o:ort, p1:plz, p2:plz]
from [orte as o, plz as p1, plz as p2]
where [
o:ort = p1:ort,
p2:plz = (p1:plz +1),
o:ort contains "dorf"]
orderby [o:ort asc, p2:plz desc],
Query, Cost
).
test12(Query, Cost) :- optimize(
select *
from [orte as o, plz as p]
where [o:ort = p:ort, p:plz = 44225],
Query, Cost
).
test13(Query, Cost) :- optimize(
select *
from [orte as o, plz as p]
where [
o:ort = p:ort,
p:plz > 50000,
o:ort contains "dorf"],
Query, Cost
).
test14(Query, Cost) :- optimize(
select count(*)
from [orte as o, plz as p1, plz as p2]
where [
o:ort = p1:ort,
p2:plz = p1:plz + 1,
(p2:plz mod 5) = 0,
p1:plz > 50000,
o:ort contains "dorf"],
Query, Cost
).
test15(Query, Cost) :- optimize(
select [ort, min(plz) as minplz,
max(plz) as maxplz, count(*) as cntplz]
from plz
where plz > 40000
groupby ort
orderby cntplz desc
first 10,
Query, Cost
).
test16(Query, Cost) :- optimize(
select *
from [staedte, plz as p1, orte as o]
where [
sname = p1:ort,
o:ort = p1:ort,
bev < 500000,
p1:plz > 50000,
p1:plz < 60000,
kennzeichen starts "W",
p1:ort contains "burg",
p1:ort starts "M"],
Query, Cost
).
/*
The following three clauses are identical to the clauses ~example17~/2,
~example18~/2 and ~example19~/2 from the file ~optimizer.pl~. They are listed
here just for a better clarity.
~test17~, ~test21~, ~test22~, ~test23~ and ~test25~ cause an 'Out of local
stack'-error, if ~immPathCreation~ is set to ~off~ (i. e. the originally
implemented algorithm is used). Thus Prolog has to be initialized with a bigger
local stack. For ~test17~ and ~test22~ a local stack of 4 MB is fine enough, for
~test23~ a local stack of 8 MB is needed, but for ~test21~ and ~test25~ even a
local stack of 16 MB is still too small.
For to use bigger local stacks please don't use the command ~SecondoPL~, but
~SecondoPL -L4M~ when for example the stack should be 4 MB.
Alternatively it is possible to use the command ~pl -L4M~. In this case the
following calls are necessary afterwards:
----
['calloptimizer.pl'].
['modifications.pl'].
----
*/
test17(Query, Cost) :- optimize(
select *
from [staedte, plz as p1, plz as p2, plz as p3]
where [
sname = p1:ort,
p1:plz = p2:plz + 1,
p2:plz = p3:plz * 5,
bev > 300000,
bev < 500000,
p2:plz > 50000,
p2:plz < 60000,
kennzeichen starts "W",
p3:ort contains "burg",
p3:ort starts "M"],
Query, Cost
).
test18(Query, Cost) :- optimize(
select *
from [staedte, plz as p1]
where [
sname = p1:ort,
bev > 300000,
bev < 500000,
p1:plz > 50000,
p1:plz < 60000,
kennzeichen starts "W",
p1:ort contains "burg",
p1:ort starts "M"],
Query, Cost
).
test19(Query, Cost) :- optimize(
select *
from [staedte, plz as p1, plz as p2]
where [
sname = p1:ort,
p1:plz = p2:plz + 1,
bev > 300000,
bev < 500000,
p1:plz > 50000,
p1:plz < 60000,
kennzeichen starts "W",
p1:ort contains "burg",
p1:ort starts "M"],
Query, Cost
).
test20(Query, Cost) :- optimize(
select *
from [staedte, orte as o, plz as p]
where [
sname = p:ort,
p:ort = o:ort,
sname = o:ort,
plz = p:plz,
vorwahl = o:vorwahl,
kennzeichen = o:kennzeichen,
bev * 1000 = o:bevt,
kennzeichen starts "A"
],
Query, Cost
).
test21(Query, Cost) :- optimize(
select *
from [staedte as s, orte as o, plz as p]
where [
s:plz = p:plz,
s:sname = o:ort,
p:ort starts "M",
p:ort contains "burg",
s:sname contains "burg",
s:bev > 300000,
s:bev < 500000,
s:bev * 1000 = o:bevt,
s:kennzeichen starts "A",
o:kennzeichen starts "A",
p:plz > 50000,
p:plz < 60000,
s:plz > 50000,
s:plz < 60000
],
Query, Cost
).
test22(Query, Cost) :- optimize(
select *
from [staedte as s, orte as o, plz as p]
where [
s:plz = p:plz,
s:sname = o:ort,
p:ort starts "M",
p:ort contains "burg",
s:bev > 300000,
s:bev < 500000,
s:bev * 1000 = o:bevt,
s:kennzeichen starts "A",
p:plz > 50000,
p:plz < 60000
],
Query, Cost
).
test23(Query, Cost) :- optimize(
select *
from [staedte as s, orte as o, plz as p]
where [
s:plz = p:plz,
s:sname = o:ort,
s:bev * 1000 = o:bevt,
s:vorwahl = o:vorwahl,
s:kennzeichen = o:kennzeichen,
o:ort = p:ort,
p:ort starts "M",
p:ort contains "burg",
s:sname contains "burg",
s:bev > 300000,
s:bev < 500000
],
Query, Cost
).
test24(Query, Cost) :- optimize(
select *
from [staedte as s, orte as o, plz as p]
where [
s:vorwahl = o:vorwahl,
s:kennzeichen = o:kennzeichen,
o:ort = p:ort,
p:ort starts "M",
p:ort contains "burg",
s:sname contains "burg",
s:bev > 300000,
s:bev < 500000,
s:kennzeichen starts "A"
],
Query, Cost
).
test25(Query, Cost) :- optimize(
select *
from [staedte as s, orte as o, plz as p]
where [
s:plz = p:plz,
s:sname = o:ort,
s:bev * 1000 = o:bevt,
s:vorwahl = o:vorwahl,
s:kennzeichen = o:kennzeichen,
o:ort = p:ort,
p:ort starts "M",
p:ort contains "burg",
s:sname contains "burg",
s:bev > 300000,
s:bev < 500000,
s:kennzeichen starts "A",
o:kennzeichen starts "A",
p:plz > 50000,
p:plz < 60000,
s:plz > 50000,
s:plz < 60000
],
Query, Cost
).
/*
2.6.3 The Results
In a first survey mainly typical every-day-life-queries were used, that had
also been used in other contexts for testing and explaining the Secondo-system.
The results in detail can be found in the file ~ResultFirstSurvey.html~,
attached to this file. They are really impressively.
The assumption is, that an improvement (reduction of time) is significant, if it
is at least five per cent. Thus in distinctly most of the test-cases the
improvement of this modification is significant. There is only one case
(~test8~) where the improvement is significantly negative. (This is tested
several times.) In ~test2~ with one predicate the improvement is also negative
(but not significantly, following the definition for significance made above).
One can easily see that the improvement is the better, the more predicates
are in a query - up to more than 70 per cent in ~test6~ and ~test7~ with seven
predicates each.
In a subsequent survey more complex queries with more predicates were
tested. The results can be found in the file ~ResultSecondSurvey.html~, also
attached to this file. Now the time-reduction is up to approximately 90 per cent
when there are eleven predicates in a query (~test23~) and often more than 75 per
cent. Unfortunately it is not possible to evaluate queries with up to fourteen
or seventeen predicates (~test21~ and ~test25~) - neither with the original
optimizer, nor with the modified one. (They ares just evaluated, if the
Prolog-interpreter is initialized with both a local and a global stack of 32
MB; see chapter 2.7.3.) The result is an
----
ERROR: Out of local stack
----
In nearly every case in the second survey the first of the five measurement
results is not considered because they are obviously out of bound. The reason
for this should be the clause ~sweepKnowledgeBase~. This clause deletes all
the nodes, edges, plan edges and so on, that are still stored in the knowledge-base. And there are just a few, if the formerly called test-clause represents
a query with just a few predicates, or a lot, if the formerly called test-clause
represents a query with many predicates. This hasnt't been noticeable in the first
survey, because there haven't been test-clauses with that much predicates.
2.7 Testing how many Predicates are evaluated at most
2.7.1 The Procedure
As shown above, the modified optimizer evaluates queries with up to
eleven predicates without any problems, but ~test21~ and ~test25~
containing more predicates cause errors. Thus it is an interesting
question, how many predicates the optimizer is able to process,
i. e. what is the maximum of predicates it is able to evaluate.
For this purpose the following test-clauses were built and the
results of their callings inserted in a table, which can be found
in the file ~MaxPredicatesTest.html~, attached to this file.
After first calling ~immPathCreation(off, time~) and second
~immPathCreation(on, time~) in both cases the following goals were
called, successively replacing the ~X~ with the numbers from 26 up
to 35.
----
sweepKnowledgeBase(_), checkCalcTime(testX(A,B)),
countNodes, countPlanEdges.
----
If an error-message occurred, first the local and if neccessary also
the global stack of Prolog was extended.
The set-up of the table of results is similar to the one of the
tables contained in ~ResultFirstSurvey.html~ and
~ResultSecondSurvey.html~. For details please consider chapter 2.6.1.
2.7.2 The Test-Clauses
The following five clauses contain the predicates of ~test25~, the
first one eleven, the second one twelve, the third one thirteen and
the fourth and fifth one fourteen each.
*/
test26(Query, Cost) :- optimize(
select *
from [staedte as s, orte as o, plz as p]
where [
s:plz = p:plz,
s:sname = o:ort,
s:bev * 1000 = o:bevt,
s:vorwahl = o:vorwahl,
s:kennzeichen = o:kennzeichen,
o:ort = p:ort,
p:ort starts "M",
p:ort contains "burg",
s:sname contains "burg",
s:bev > 300000,
s:plz < 60000
],
Query, Cost
).
test27(Query, Cost) :- optimize(
select *
from [staedte as s, orte as o, plz as p]
where [
s:plz = p:plz,
s:sname = o:ort,
s:bev * 1000 = o:bevt,
s:vorwahl = o:vorwahl,
s:kennzeichen = o:kennzeichen,
o:ort = p:ort,
p:ort starts "M",
p:ort contains "burg",
s:sname contains "burg",
s:bev > 300000,
s:bev < 500000,
s:plz < 60000
],
Query, Cost
).
test28(Query, Cost) :- optimize(
select *
from [staedte as s, orte as o, plz as p]
where [
s:plz = p:plz,
s:sname = o:ort,
s:bev * 1000 = o:bevt,
s:vorwahl = o:vorwahl,
s:kennzeichen = o:kennzeichen,
o:ort = p:ort,
p:ort starts "M",
p:ort contains "burg",
s:sname contains "burg",
s:bev > 300000,
s:bev < 500000,
s:kennzeichen starts "A",
s:plz < 60000
],
Query, Cost
).
test29(Query, Cost) :- optimize(
select *
from [staedte as s, orte as o, plz as p]
where [
s:plz = p:plz,
s:sname = o:ort,
s:bev * 1000 = o:bevt,
s:vorwahl = o:vorwahl,
s:kennzeichen = o:kennzeichen,
o:ort = p:ort,
p:ort starts "M",
p:ort contains "burg",
s:sname contains "burg",
s:bev > 300000,
s:bev < 500000,
s:kennzeichen starts "A",
o:kennzeichen starts "A",
s:plz < 60000
],
Query, Cost
).
/*
The following clause is nothing more than a modification of ~test29~
just containing the predicates in another order. It is only
evaluated, if Prolog was initialized with a local stack of 4 MB.
*/
test30(Query, Cost) :- optimize(
select *
from [staedte as s, orte as o, plz as p]
where [
p:ort starts "M",
p:ort contains "burg",
s:sname contains "burg",
s:bev > 300000,
s:bev < 500000,
s:kennzeichen starts "A",
o:kennzeichen starts "A",
s:plz = p:plz,
s:sname = o:ort,
s:bev * 1000 = o:bevt,
s:vorwahl = o:vorwahl,
s:kennzeichen = o:kennzeichen,
o:ort = p:ort,
s:plz < 60000
],
Query, Cost
).
/*
The following clause is a modification of ~test21~ containing one
predicate less and the other in just another order. It causes an
'Out of local stack'-error even if the local stack of Prolog was set to 16 MB.
*/
test31(Query, Cost) :- optimize(
select *
from [staedte as s, orte as o, plz as p]
where [
p:plz > 50000,
p:plz < 60000,
s:plz > 50000,
s:plz < 60000,
s:plz = p:plz,
s:sname = o:ort,
s:kennzeichen starts "A",
o:kennzeichen starts "A",
p:ort starts "M",
p:ort contains "burg",
s:sname contains "burg",
s:bev > 300000,
s:bev < 500000
],
Query, Cost
).
/*
The following clause is a modification of ~test21~ containing two
predicates less. It is evaluated if both the local and the global
stack of Prolog is set to 8 MB.
*/
test32(Query, Cost) :- optimize(
select *
from [staedte as s, orte as o, plz as p]
where [
s:plz = p:plz,
s:sname = o:ort,
s:sname contains "burg",
s:bev > 300000,
s:bev < 500000,
s:bev * 1000 = o:bevt,
s:kennzeichen starts "A",
o:kennzeichen starts "A",
p:plz > 50000,
p:plz < 60000,
s:plz > 50000,
s:plz < 60000
],
Query, Cost
).
test33(Query, Cost) :- optimize(
select *
from [staedte as s, orte as o, plz as p]
where [
p:ort starts "M",
p:ort contains "burg",
s:sname contains "burg",
s:sname contains "agde",
s:bev > 300000,
s:bev < 500000,
s:kennzeichen starts "A",
o:kennzeichen starts "A",
s:plz = p:plz,
s:sname = o:ort,
s:bev * 1000 = o:bevt,
s:vorwahl = o:vorwahl,
s:kennzeichen = o:kennzeichen,
o:ort = p:ort,
s:plz < 60000
],
Query, Cost
).
test34(Query, Cost) :- optimize(
select *
from [staedte as s, orte as o, plz as p]
where [
p:ort starts "M",
p:ort contains "burg",
s:sname contains "burg",
s:sname contains "agde",
p:ort contains "agde",
s:bev > 300000,
s:bev < 500000,
s:kennzeichen starts "A",
o:kennzeichen starts "A",
s:plz = p:plz,
s:sname = o:ort,
s:bev * 1000 = o:bevt,
s:vorwahl = o:vorwahl,
s:kennzeichen = o:kennzeichen,
o:ort = p:ort,
s:plz < 60000
],
Query, Cost
).
/*
The following clause causes an 'Out of local stack'-error
even if the local stack of Prolog was set to 32 MB.
*/
test35(Query, Cost) :- optimize(
select *
from [staedte as s, orte as o, plz as p]
where [
p:ort starts "M",
p:ort contains "burg",
s:sname contains "burg",
s:sname contains "ag",
s:sname contains "de",
p:ort contains "agde",
s:bev > 300000,
s:bev < 500000,
s:kennzeichen starts "A",
o:kennzeichen starts "A",
s:plz = p:plz,
s:sname = o:ort,
s:bev * 1000 = o:bevt,
s:vorwahl = o:vorwahl,
s:kennzeichen = o:kennzeichen,
o:ort = p:ort,
s:plz < 60000
],
Query, Cost
).
/*
2.7.3 The Results
The results can be found in the file ~MaxPredicatesTest.html~,
attached to this file. Again they show the benefits of the
modification.
But they also show, that it is not possible to give an exact maximum
of predicates, that the optimizer is able to evaluate. It is
somewhere around 15, if one doesn't want to extend the local and the
global stack of the Prolog-interpreter above 16 MB.
But ~test31~ only contains 13 predicates (and ~test21~ 14) and they
are not evaluated.
Just for interest both the local and the global stack were extended
to 32 MB, and in that case even ~test21~, ~test25~ and ~test31~
were evaluated. The results can be found in ~MaxPredicatesTest.html~.
Note that even ~test22~ with only 10 predicates needs
an extended local stack (4 MB), but ~test29~ representing a query
containig 14 predicates doesn't need that. ~test34~ representing a
query containing 16 predicates can be evaluated, if the interpreter
was initialized with both a local and a global stack of 16 MB.
An interesting fact is, that ~test30~, which is identical to
~test29~ just containing the predicates in another order, needs
an extended local stack (4 MB) but ~test29~ doesn't need it - though
both test-clauses cause the creation of 1.015 nodes and 2.551 plan edges.
The boundary of query-complexity, that the optimizer is able to
handle, is either only determined by the numbers of nodes and
plan edges, that have to be created while constructing the cheapest
plan. First ~test29~ and ~test30~ create the same number of nodes and plan
edges. And second one can easily see, that if the original optimizer is used, it
can cope with over 4.000 nodes and over 30.000 plan edges, if both
the local and the global stack of Prolog were set to 16 MB. But
while evaluating ~test31~ just about 3.300 nodes and 6.500 plan edges are
created and the optimizer already needs stacks of 32 MB.
[newpage]
3 Alternative Implementations for the Set REACHED-NODES
3.1 Introduction
Both optimizers, the original one and the modified one realized in
chapter two, have problems to evaluate queries containing a lot of
predicates. They cause an
----
ERROR: Out of local stack
----
Even if the local stack of the Prolog-interpreter is extended, the
optimizer is not able to cope with more than sixteen predicates in a
test-query.
Because the original implementation of the set REACHED-NODES is rather complex
(for details see the file ~boundary.pl~), it was worth trying, if more simple
implementations maybe need less memory-resources. Thus
four alternative and rather simple implementations are realized and
tested in the following chapters.
Please note, that the alternative implementations can only be used
together with the modified optimizer, realized in chapter two, not with
the original one.
1 Instead of using facts stored in the knowledge-base and additionally a
binary search tree the first implementation uses facts and a sorted list. Please
call ~altRNSImplementation(1)~ to use this implementation for the set of
already reached but not yet ticked off nodes.
2 In the second implementation the set REACHED-NODES is just a sorted
list. One has to call ~altRNSImplementation(2)~ to use this one.
3 In the third alternative, which can be chosen by calling
~altRNSImplementation(3)~, just facts stored in the knowledge-base altogether
constitute the set REACHED-NODES.
4 In the fourth implementation (~altRNSImplementation(4)~) also just facts
are stored in the knowledge-base. But the facts have another shape than those
used in implementation No. 3 and the clauses used for finding the node
with the currently minimal distance to the source-node have been changed.
For to use the original implementation again after using another, one has
to call ~altRNSImplementation(off)~.
3.2 Choosing one of the Implementations
There are four alternative implementations, numbered 1, 2, 3 and 4.
Please call ~altRNSImplementation~ with one of the numbers as its
argument to retract the currently used implementation from the
knowledge-base and to store the chosen alternative implementation
instead.
Call ~altRNSImplementation(off)~ to reconsult the file ~modifications.pl~ and
to consequently store the original implementation for the set REACHED-NODES
again.
*/
altRNSImplementation(off) :-
!,
restoreRNSImplementation,
nl, write('*** From now on the original implementation for'),
nl, write('*** the set REACHED-NODES is used again.'), nl.
altRNSImplementation(1) :-
!,
restoreRNSImplementation,
printWelcomeRNS,
altRNSImplementation1.
altRNSImplementation(2) :-
!,
restoreRNSImplementation,
printWelcomeRNS,
altRNSImplementation2.
altRNSImplementation(3) :-
!,
restoreRNSImplementation,
printWelcomeRNS,
altRNSImplementation3.
altRNSImplementation(4) :-
!,
restoreRNSImplementation,
printWelcomeRNS,
altRNSImplementation4.
altRNSImplementation(_) :-
nl, write('*** Please choose one of the alternatives 1, 2,'),
nl, write('*** 3 or 4 - or the original implementation.'), nl.
printWelcomeRNS :-
nl, write('*** From now on (only) the modified optimizer'),
nl, write('*** uses an alternative implementation for the'),
write(' set REACHED-NODES.'),
nl, write('*** Please call ''altRNSImplementation(off)'' to'),
nl, write('*** use the original implementation again.'), nl.
/*
3.3 The First Alternative: Facts and a Sorted List
The already reached but not yet ticked off nodes are stored in the
knowledge-base as facts of the shape
----
reachedNode(NodeNo, Distance, Path)
----
Additionally a sorted list (sorted by ~distance~) is used to find
the node with the minimal distance to the source-node faster than
by scanning the facts in the knowledge-base.
*/
altRNSImplementation1 :-
retractCurrentRNSImplementation,
asserta(
createEmptyReachedNodesSet([]) :-
( deleteAllReachedNodes )
),
asserta(
putIntoReachedNodesSet(ReachedNodesSet,
node(NodeNo, Distance, Path),
NewReachedNodesSet) :-
( assert(reachedNode(NodeNo, Distance, Path)),
putIntoDistanceList(ReachedNodesSet,
pair(Distance, NodeNo),
NewReachedNodesSet) )
),
asserta(
isInReachedNodesSet(_, node(NodeNo, _, _)) :-
( reachedNode(NodeNo, _, _) )
),
asserta(
readAlreadyReachedNode(_, NodeNo,
node(NodeNo, Distance, Path)) :-
( reachedNode(NodeNo, Distance, Path) )
),
asserta(
getMinimalDistantNode(ReachedNodesSet,
node(NodeNo, Distance, Path),
NewReachedNodesSet) :-
( deleteFirstOfDistanceList(ReachedNodesSet,
pair(_, NodeNo),
NewReachedNodesSet),
retract(reachedNode(NodeNo, Distance, Path)) )
),
asserta(
deleteOutOfReachedNodesSet(ReachedNodesSet,
node(NodeNo, Distance, _),
NewReachedNodesSet) :-
( deleteOutOfDistanceList(ReachedNodesSet,
pair(Distance, NodeNo),
NewReachedNodesSet),
retract(reachedNode(NodeNo, _, _)) )
),
asserta(
isEmpty([]) :-
( dc(immPath, (write('There are no more reached '),
write('but not yet ticked off nodes.'), nl)),
true )
).
:- dynamic reachedNode/3.
deleteAllReachedNodes :-
retractall(reachedNode(_, _, _)).
putIntoDistanceList([], Element, [Element]).
putIntoDistanceList(InList, pair(Distance, NodeNo), OutList) :-
InList = [pair(FirstDistance, _)|_],
% if
Distance =< FirstDistance,
!,
% then
OutList = [pair(Distance, NodeNo)|InList].
putIntoDistanceList([Element|InListRemainder],
pair(Distance, NodeNo),
[Element|OutListRemainder]) :-
% else
putIntoDistanceList(InListRemainder,
pair(Distance, NodeNo),
OutListRemainder).
deleteFirstOfDistanceList([pair(_, NodeNo)|ListRemainder],
pair(_, NodeNo),
ListRemainder).
deleteOutOfDistanceList([], _, []).
deleteOutOfDistanceList([Element|InListRemainder],
Element, InListRemainder) :-
!.
deleteOutOfDistanceList([OtherElement|InListRemainder],
Element, [OtherElement|OutListRemainder]) :-
deleteOutOfDistanceList(InListRemainder,
Element, OutListRemainder).
/*
3.4 The Second Alternative: Just a Sorted List
The set REACHED-NODES is just a sorted list.
*/
altRNSImplementation2 :-
retractCurrentRNSImplementation,
asserta(
createEmptyReachedNodesSet([]) :-
( true )
),
asserta(
putIntoReachedNodesSet(ReachedNodesSet,
node(NodeNo, Distance, Path),
NewReachedNodesSet) :-
( putIntoNodeList(ReachedNodesSet,
node(NodeNo, Distance, Path),
NewReachedNodesSet) )
),
asserta(
isInReachedNodesSet(ReachedNodesSet, Node) :-
( member(Node, ReachedNodesSet) )
),
asserta(
readAlreadyReachedNode(ReachedNodesSet, NodeNo,
node(NodeNo, Distance, Path)) :-
( member(node(NodeNo, Distance, Path),
ReachedNodesSet) )
),
asserta(
getMinimalDistantNode(ReachedNodesSet,
node(NodeNo, Distance, Path),
NewReachedNodesSet) :-
( deleteFirstOfNodeList(ReachedNodesSet,
node(NodeNo, Distance, Path),
NewReachedNodesSet) )
),
asserta(
deleteOutOfReachedNodesSet(ReachedNodesSet,
node(NodeNo, _, _),
NewReachedNodesSet) :-
( deleteOutOfNodeList(ReachedNodesSet,
node(NodeNo, _, _),
NewReachedNodesSet) )
),
asserta(
isEmpty([]) :-
( dc(immPath, (write('There are no more reached '),
write('but not yet ticked off nodes.'),
nl)),
true )
).
putIntoNodeList([], Element, [Element]).
putIntoNodeList(InList, node(NodeNo, Distance, Path), OutList) :-
InList = [node(_, FirstDistance, _)|_],
% if
Distance =< FirstDistance,
!,
% then
OutList = [node(NodeNo, Distance, Path)|InList].
putIntoNodeList([Element|InListRemainder],
node(NodeNo, Distance, Path),
[Element|OutListRemainder]) :-
% else
putIntoNodeList(InListRemainder,
node(NodeNo, Distance, Path),
OutListRemainder).
deleteFirstOfNodeList([node(NodeNo, Distance, Path)|ListRemainder],
node(NodeNo, Distance, Path),
ListRemainder).
deleteOutOfNodeList([], _, []).
deleteOutOfNodeList([Element|InListRemainder],
Element, InListRemainder) :-
!.
deleteOutOfNodeList([OtherElement|InListRemainder],
Element, [OtherElement|OutListRemainder]) :-
deleteOutOfNodeList(InListRemainder,
Element, OutListRemainder).
/*
3.5 The Third Alternative: Just Facts in the Knowledge-Base
The already reached but not yet ticked off nodes are stored in the
knowledge-base as facts of the shape
----
reachedNode(NodeNo, Distance, Path)
----
Hence it is rather costly to find the node with the minimal distance
to the source-node.
*/
altRNSImplementation3 :-
retractCurrentRNSImplementation,
asserta(
createEmptyReachedNodesSet(empty) :-
( deleteAllReachedNodes )
),
asserta(
putIntoReachedNodesSet(_,
node(NodeNo, Distance, Path),
full) :-
( assert(reachedNode(NodeNo, Distance, Path)) )
),
asserta(
isInReachedNodesSet(_, node(NodeNo, _, _)) :-
( reachedNode(NodeNo, _, _) )
),
asserta(
readAlreadyReachedNode(_, NodeNo,
node(NodeNo, Distance, Path)) :-
( reachedNode(NodeNo, Distance, Path) )
),
asserta(
getMinimalDistantNode(_, Node, NewReachedNodesSet) :-
( getMinimalDistantNode(Node, NewReachedNodesSet) )
),
asserta(
deleteOutOfReachedNodesSet(_, node(NodeNo, _, _),
NewReachedNodesSet) :-
( retract(reachedNode(NodeNo, _, _)),
checkReachedNodes(NewReachedNodesSet) )
),
asserta(
isEmpty(empty) :-
( dc(immPath, (write('There are no more reached '),
write('but not yet ticked off nodes.'),
nl)),
true )
).
getMinimalDistantNode(node(ResNodeNo, ResDistance, ResPath),
NewReachedNodesSet) :-
dc(immPath, (write('Searching the node with the minimal distance '),
write('to the source-node.'), nl)),
findall(reachedNode(NodeNo, Distance, Path),
reachedNode(NodeNo, Distance, Path),
ReachedNodes),
findMinimalDistantNode(ReachedNodes,
reachedNode(ResNodeNo, ResDistance, ResPath)),
retract(reachedNode(ResNodeNo, ResDistance, ResPath)),
dc(immPath, (write('Found the node with the minimal distance.'), nl)),
checkReachedNodes(NewReachedNodesSet).
findMinimalDistantNode([Node], Node) :-
!.
findMinimalDistantNode([reachedNode(_, Distance1,_)|Remainder],
MinDistNode) :-
findMinimalDistantNode(Remainder,
reachedNode(NodeNo2, Distance2, Path2)),
% if
Distance1 >= Distance2,
!,
% then
MinDistNode = reachedNode(NodeNo2, Distance2, Path2).
findMinimalDistantNode([Node|_], Node).
% else
checkReachedNodes(full) :-
% if
reachedNode(_, _, _),
!.
checkReachedNodes(empty).
% else
/*
3.6 The Fourth Alternative: Also just Facts in the Knowledge-Base
This alternative implementation is just some kind of modification of the
implementation No. 3.
Two changes have been made: First the already reached but not yet ticked off
nodes are stored in the knowledge-base as facts of the shape
----
reachedNode(Distance, NodeNo, Path)
----
~Distance~ becomes the first element of this structure, because the
Prolog-interpreter automatically builds an index regarding the first element.
Second instead of a list as a means for to find the nodes with the minimal
distance to the source-node, a direct search of the knowledge-base takes place.
*/
altRNSImplementation4 :-
retractCurrentRNSImplementation,
asserta(
createEmptyReachedNodesSet(empty) :-
( deleteAllReachedNodes )
),
asserta(
putIntoReachedNodesSet(_,
node(NodeNo, Distance, Path),
full) :-
( assert(reachedNode(Distance, NodeNo, Path)) )
),
asserta(
isInReachedNodesSet(_, node(NodeNo, _, _)) :-
( reachedNode(_, NodeNo, _) )
),
asserta(
readAlreadyReachedNode(_, NodeNo,
node(NodeNo, Distance, Path)) :-
( reachedNode(Distance, NodeNo, Path) )
),
asserta(
getMinimalDistantNode(_, Node, NewReachedNodesSet) :-
( getMinDistNode(Node, NewReachedNodesSet) )
),
asserta(
deleteOutOfReachedNodesSet(_, node(NodeNo, _, _),
NewReachedNodesSet) :-
( retract(reachedNode(_, NodeNo, _)),
checkReachedNodesSet(NewReachedNodesSet) )
),
asserta(
isEmpty(empty) :-
( dc(immPath, (write('There are no more reached '),
write('but not yet ticked off nodes.'),
nl)),
true )
).
getMinDistNode(node(ResNodeNo, ResDistance, ResPath),
NewReachedNodesSet) :-
dc(immPath, (write('Searching the node with the minimal distance '),
write('to the source-node.'), nl)),
reachedNode(Distance, NodeNo, Path),
findMinDistNode(reachedNode(Distance, NodeNo, Path),
reachedNode(ResDistance, ResNodeNo, ResPath)),
dc(immPath, (write('Found the node with the minimal distance.'), nl)),
retract(reachedNode(ResDistance, ResNodeNo, ResPath)),
checkReachedNodesSet(NewReachedNodesSet).
findMinDistNode(reachedNode(Distance1, _, _), MinDistNode) :-
% if
checkReachedNodesSet(Status),
Status == full,
reachedNode(Distance, NodeNo, Path),
Distance < Distance1,
% then
findMinDistNode(reachedNode(Distance, NodeNo, Path),
MinDistNode).
findMinDistNode(Node, Node).
% else
checkReachedNodesSet(full) :-
% if
reachedNode(_, _, _),
!.
checkReachedNodesSet(empty).
% else
/*
In theory the ~findMinDistNode~-clauses are not very efficient, because they
compare every fact with every other (and with itself), thus for n facts there
are "n^{2}"[1] camparisons. But the advantage of the clauses is, that they
avoid any kind of container for the facts (like a list in the 'findall and
search the cheapest element'-approach; see chapter 3.5). Thus their need for
stack-space is hopefully not that big as if a container were used.
3.7 Testing the Alternative Implementations
3.7.1 The Procedure
After switching over from one to another alternative implementation, the
following goals have been called
----
sweepKnowledgeBase(_), checkCalcTime(testX(A,B)),
checkCalcTime(testX(C,D)).
----
successively replacing the ~X~ with the numbers from 1 upwards.
3.7.2 The Results
All the results can be found in the file ~AltRNSImplemComparison.html~,
attached to this file.
The first thing that one may notice, if he has a look at the times, needed for
constructing the shortest path while the original implementation for
the set REACHED-NODES is used, is, that they are better (i. e. smaller) in many
cases, than they were in the surveys, carried out in chapter 2.6
(see ~ResultFirstSurvey.html~ and ~ResultSecondSurvey.html~). The reason for
this should be, that here ~sweepKnowledgeBase~ is called in advance. This
probably has a distinct effect on every ~ctime1~ because the call
~sweepKnowledgeBase~ within the clause ~immPathCreation~ can then be processed
without any effort.
A somehow surprising fact is, that the implementations No. 1 (facts in the
knowledge-base and additionally a sorted list) and No. 2 (just a sorted list)
are not really worse than the original implementation (facts in the
knowledge-base and additionally a binary search tree) as long as queries do
not contain more than ten predicates. For test-queries with
just a few predicates this was expected, but the results show, that the original
implementation is not significantly better until there are ten or more
predicates in a query. For many test-clauses representing queries with less
than ten predicates, often even the simplier implementations cause a better
(i. e. smaller) time needed for constructing the shortest path through the POG.
But the original implementation is clearly the better the more predicates
have to be evaluated and the simplier implementations lose any kind of
competition.
An interesting result is, that the alternative implementation No. 1 (facts in
the knowledge-base and additionally a sorted list) needs more time than No. 2
(just a sorted list) as long as there are less than ten predicates in a query,
but (in most cases) dinstinctly less time, if queries contain more than ten
predicates. Hence the complexity of elements contained in a list does only
have significant consequences, if the list is pretty long.
The simple alternative implementations No. 3 and No. 4
(just facts in the knowledge-base) are
not interesting in any sense. The time needed for evaluating a query is
always bigger than in the other cases. It is true, that the sizes of the
stacks, that needs the Prolog-interpreter are smaller for some test-clauses,
if one uses the alternative implementation No. 3,
but the optimizer always needs a lot more time. The only partly promissing
example is ~test33~, that doesn't need an enhancement of the local- and the
trail-stack, but can be evaluated in an acceptable time. (But the original
implementation of the set REACHED-NODES causes a very much better time
needed for creating the shortest path.)
The aim of the alternative implementations and the survey documented in
~AltRNSImplemComparison.html~ was to test, if maybe more simple structured sets
do not need that much stack-resources and hopefully cause 'Out of local
stack'-errors later than the original implementation. This could not be shown.
Only the clauses ~test30~ and ~test32~ can be evaluated with a smaller local
stack as it is needed while using the original implementation. But on the
other hand it is possible to evaluate ~test21~, ~test25~ and ~test34~ with a
local and a global stack of 32 MB each (~test34~ only needs 16 MB) with the
original implementation, but the others need more stack-resources.
[newpage]
4 Literature
The following list considers only those books and papers, that have
been very useful for learning Prolog and programming the above
described modifications of the original Secondo-optimizer.
W.F. Clocksin, C.S. Mellish, Programming in Prolog: using the ISO
standard, 5th Edition, Berlin, Heidelberg, New York, 2003.
R.H. G[ue]ting, Datenstrukturen, Fernuniversit[ae]t Hagen, 1999.
R.H. G[ue]ting, T. Behr, V. T. de Almeida, Z. Ding, F. Hoffmann,
and M. Spiekermann, Secondo: An Extensible DBMS Architecture and
Prototype. Fernuniversit[ae]t Hagen, Informatik-Report 313, March
2004, www.informatik.fernuni-hagen.de/import/pi4/papers/
Secondo04.pdf
R. H. G[ue]ting, D. Ansorge, T. Behr, M. Spiekermann, Secondo User
Manual, Version 3, September 21, 2004. Fernuniversit[ae]t Hagen,
www.informatik.fernuni-hagen.de/import/pi4/Secondo.html/
files/SecondoManual.pdf
R. H. G[ue]ting, V. T. de Almeida, D. Ansorge, T. Behr,
M. Spiekermann. Secondo Programmers Guide, Version 2,
October 5, 2004, Fernuniversit[ae]t Hagen,
www.informatik.fernuni-hagen.de/import/pi4/
Secondo.html/files/ProgrammersGuide.pdf
G. R[oe]hner, Informatik mit Prolog, Wiesbaden, 2002.
R. Sedgewick, Algorithmen, 2. Auflage, M[ue]nchen, 2002.
L. Sterling, E. Shapiro, Prolog, Fortgeschrittene
Programmiertechniken, Bonn, 1988.
W. Weisweber, Prolog, Logische Programmierung in der Praxis,
Bonn, 1997.
*/