forked from eldersantos/community
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
When a query with both ORDER BY and LIMIT is encountered, Cypher now only keeps a limited set of the input in heap. Before this commit, the whole input would be ordered, and then the top x rows kept. Now, Cypher builds a buffer of the top x hits, that it keeps ordered. Really large queries can now still be executed without leading to a OutOfMemoryError.
- Loading branch information
Showing
12 changed files
with
251 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
65 changes: 65 additions & 0 deletions
65
cypher/src/main/scala/org/neo4j/cypher/internal/executionplan/builders/TopPipeBuilder.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,65 @@ | |||
/** | |||
* Copyright (c) 2002-2012 "Neo Technology," | |||
* Network Engine for Objects in Lund AB [http://neotechnology.com] | |||
* | |||
* This file is part of Neo4j. | |||
* | |||
* Neo4j 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 3 of the License, or | |||
* (at your option) any later version. | |||
* | |||
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
package org.neo4j.cypher.internal.executionplan.builders | |||
|
|||
import org.neo4j.cypher.internal.pipes.TopPipe | |||
import org.neo4j.cypher.internal.executionplan.{ExecutionPlanInProgress, PlanBuilder} | |||
import org.neo4j.cypher.internal.commands.expressions.Add | |||
import org.neo4j.cypher.internal.commands.Slice | |||
import org.neo4j.helpers.ThisShouldNotHappenError | |||
|
|||
class TopPipeBuilder extends PlanBuilder with SortingPreparations { | |||
def apply(plan: ExecutionPlanInProgress) = { | |||
val newPlan = extractBeforeSort(plan) | |||
|
|||
val q = newPlan.query | |||
val sortItems = q.sort.map(_.token) | |||
val slice = q.slice.get.token | |||
val limit = slice match { | |||
case Slice(Some(skip), Some(l)) => Add(skip, l) | |||
case Slice(None, Some(l)) => l | |||
} | |||
|
|||
val resultPipe = new TopPipe(newPlan.pipe, sortItems.toList, limit) | |||
|
|||
val solvedSort = q.sort.map(_.solve) | |||
val solvedSlice = slice match { | |||
case Slice(Some(x), _) => Some(Unsolved(Slice(Some(x), None))) | |||
case Slice(None, _) => None | |||
case _ => throw new ThisShouldNotHappenError("Andres", "This builder should not be called for this query") | |||
} | |||
|
|||
val resultQ = q.copy(sort = solvedSort, slice = solvedSlice) | |||
|
|||
plan.copy(pipe = resultPipe, query = resultQ) | |||
} | |||
|
|||
def canWorkWith(plan: ExecutionPlanInProgress) = { | |||
val q = plan.query | |||
val extracted = q.extracted | |||
val unsolvedOrdering = q.sort.filter(_.unsolved).nonEmpty | |||
val limited = q.slice.exists(_.token.limit.nonEmpty) | |||
|
|||
extracted && unsolvedOrdering && limited | |||
} | |||
|
|||
def priority: Int = PlanBuilder.TopX | |||
} | |||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
cypher/src/main/scala/org/neo4j/cypher/internal/pipes/TopPipe.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,82 @@ | |||
/** | |||
* Copyright (c) 2002-2012 "Neo Technology," | |||
* Network Engine for Objects in Lund AB [http://neotechnology.com] | |||
* | |||
* This file is part of Neo4j. | |||
* | |||
* Neo4j 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 3 of the License, or | |||
* (at your option) any later version. | |||
* | |||
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
package org.neo4j.cypher.internal.pipes | |||
|
|||
import org.neo4j.cypher.internal.commands.SortItem | |||
import org.neo4j.cypher.internal.symbols.{NumberType, SymbolTable} | |||
import collection.mutable.ListBuffer | |||
import org.neo4j.cypher.internal.commands.expressions.Expression | |||
|
|||
/* | |||
* TopPipe is used when a query does a ORDER BY ... LIMIT query. Instead of ordering the whole result set and then | |||
* returning the matching top results, we only keep the top results in heap, which allows us to release memory earlier | |||
*/ | |||
class TopPipe(source: Pipe, sortDescription: List[SortItem], countExpression: Expression) extends PipeWithSource(source) with ExecutionContextComparer { | |||
def createResults(state: QueryState): Iterator[ExecutionContext] = { | |||
|
|||
var result = new ListBuffer[ExecutionContext]() | |||
var last: Option[ExecutionContext] = None | |||
val largerThanLast = (ctx: ExecutionContext) => last.forall(s => compareBy(s, ctx, sortDescription)) | |||
var size = 0 | |||
|
|||
val input = source.createResults(state) | |||
|
|||
if (input.isEmpty) | |||
Iterator() | |||
else { | |||
val first = input.next() | |||
val count = countExpression(first).asInstanceOf[Number].intValue() | |||
|
|||
val iter = new HeadAndTail(first, input) | |||
iter.foreach { | |||
case ctx => | |||
|
|||
if (size < count) { | |||
result += ctx | |||
size += 1 | |||
|
|||
if (largerThanLast(ctx)) { | |||
last = Some(ctx) | |||
} | |||
} else | |||
if (!largerThanLast(ctx)) { | |||
result -= last.get | |||
result += ctx | |||
result = result.sortWith((a, b) => compareBy(a, b, sortDescription)) | |||
|
|||
last = Some(result.last) | |||
} | |||
} | |||
} | |||
|
|||
|
|||
|
|||
result.toIterator | |||
} | |||
|
|||
def executionPlan() = "%s\rTopPipe(ORDER BY %s LIMIT %s)".format(source.executionPlan(), sortDescription.mkString(","), countExpression) | |||
|
|||
def symbols = source.symbols | |||
|
|||
def assertTypes(symbols: SymbolTable) { | |||
sortDescription.foreach(_.expression.assertTypes(symbols)) | |||
countExpression.evaluateType(NumberType(), symbols) | |||
} | |||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
cypher/src/test/scala/org/neo4j/cypher/internal/pipes/TopPipeTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,69 @@ | |||
/** | |||
* Copyright (c) 2002-2012 "Neo Technology," | |||
* Network Engine for Objects in Lund AB [http://neotechnology.com] | |||
* | |||
* This file is part of Neo4j. | |||
* | |||
* Neo4j 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 3 of the License, or | |||
* (at your option) any later version. | |||
* | |||
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
package org.neo4j.cypher.internal.pipes | |||
|
|||
import org.scalatest.Assertions | |||
import org.junit.Test | |||
import org.neo4j.cypher.internal.commands.SortItem | |||
import org.neo4j.cypher.internal.commands.expressions.{Literal, Identifier} | |||
import org.neo4j.cypher.internal.symbols.IntegerType | |||
|
|||
|
|||
class TopPipeTest extends Assertions { | |||
@Test def top10From5ReturnsAll() { | |||
val input = createFakePipeWith(5) | |||
val pipe = new TopPipe(input, List(SortItem(Identifier("a"), ascending = true)), Literal(10)) | |||
val result = pipe.createResults(QueryState()).map(ctx => ctx("a")).toList | |||
|
|||
assert(result === List(0, 1, 2, 3, 4)) | |||
} | |||
|
|||
@Test def top5From10ReturnsAll() { | |||
val input = createFakePipeWith(10) | |||
val pipe = new TopPipe(input, List(SortItem(Identifier("a"), ascending = true)), Literal(5)) | |||
val result = pipe.createResults(QueryState()).map(ctx => ctx("a")).toList | |||
|
|||
assert(result === List(0, 1, 2, 3, 4)) | |||
} | |||
|
|||
@Test def reversedTop5From10ReturnsAll() { | |||
val in = (0 until 100).toSeq.map(i => Map("a" -> i)).reverse | |||
val input = new FakePipe(in, "a" -> IntegerType()) | |||
|
|||
val pipe = new TopPipe(input, List(SortItem(Identifier("a"), ascending = true)), Literal(5)) | |||
val result = pipe.createResults(QueryState()).map(ctx => ctx("a")).toList | |||
|
|||
assert(result === List(0, 1, 2, 3, 4)) | |||
} | |||
|
|||
@Test def emptyInputIsNotAProblem() { | |||
val input = new FakePipe(Iterator(), "a" -> IntegerType()) | |||
|
|||
val pipe = new TopPipe(input, List(SortItem(Identifier("a"), ascending = true)), Literal(5)) | |||
val result = pipe.createResults(QueryState()).map(ctx => ctx("a")).toList | |||
|
|||
assert(result === List()) | |||
} | |||
|
|||
private def createFakePipeWith(count: Int): FakePipe = { | |||
val in = (0 until count).toSeq.map(i => Map("a" -> i)) | |||
new FakePipe(in, "a" -> IntegerType()) | |||
} | |||
} |