diff options
author | aidarsamer <aidarsamer@ydb.tech> | 2023-04-18 19:35:33 +0300 |
---|---|---|
committer | aidarsamer <aidarsamer@ydb.tech> | 2023-04-18 19:35:33 +0300 |
commit | b0dc1a49941be300fa164dcb9ae79235e5def21f (patch) | |
tree | 0a318ea0022bd085d3d127cf8c52fe40424bc059 | |
parent | 2a82c4821062c12e48bc7241f02ea8d72f19f8e8 (diff) | |
download | ydb-b0dc1a49941be300fa164dcb9ae79235e5def21f.tar.gz |
Add limit pushdown to OLAP tables if TopSort by PK columns
12 files changed, 228 insertions, 83 deletions
diff --git a/ydb/core/kqp/opt/physical/kqp_opt_phy.cpp b/ydb/core/kqp/opt/physical/kqp_opt_phy.cpp index 2cc8d54b9d0..bea79d22d2b 100644 --- a/ydb/core/kqp/opt/physical/kqp_opt_phy.cpp +++ b/ydb/core/kqp/opt/physical/kqp_opt_phy.cpp @@ -36,6 +36,7 @@ public: AddHandler(0, &TKqlStreamLookupTable::Match, HNDL(BuildStreamLookupTableStages)); AddHandler(0, [](auto) { return true; }, HNDL(RemoveRedundantSortByPk)); AddHandler(0, &TCoTake::Match, HNDL(ApplyLimitToReadTable)); + AddHandler(0, &TCoTopSort::Match, HNDL(ApplyLimitToOlapReadTable)); AddHandler(0, &TCoFlatMap::Match, HNDL(PushOlapFilter)); AddHandler(0, &TCoAggregateCombine::Match, HNDL(PushAggregateCombineToStage)); AddHandler(0, &TCoAggregateCombine::Match, HNDL(PushOlapAggregate)); @@ -163,6 +164,12 @@ protected: return output; } + TMaybeNode<TExprBase> ApplyLimitToOlapReadTable(TExprBase node, TExprContext& ctx) { + TExprBase output = KqpApplyLimitToOlapReadTable(node, ctx, KqpCtx); + DumpAppliedRule("ApplyLimitToOlapReadTable", node.Ptr(), output.Ptr(), ctx); + return output; + } + TMaybeNode<TExprBase> PushOlapFilter(TExprBase node, TExprContext& ctx) { TExprBase output = KqpPushOlapFilter(node, ctx, KqpCtx, TypesCtx); DumpAppliedRule("PushOlapFilter", node.Ptr(), output.Ptr(), ctx); diff --git a/ydb/core/kqp/opt/physical/kqp_opt_phy_helpers.cpp b/ydb/core/kqp/opt/physical/kqp_opt_phy_helpers.cpp index 61eafa6e489..ca85448ed3b 100644 --- a/ydb/core/kqp/opt/physical/kqp_opt_phy_helpers.cpp +++ b/ydb/core/kqp/opt/physical/kqp_opt_phy_helpers.cpp @@ -1,4 +1,7 @@ +#include "kqp_opt_phy_impl.h" + #include <ydb/core/kqp/common/kqp_yql.h> +#include <ydb/core/kqp/provider/yql_kikimr_provider.h> namespace NKikimr::NKqp::NOpt { @@ -167,4 +170,71 @@ NYql::NNodes::TDqStage ReplaceTableSourceSettings(NYql::NNodes::TDqStage stage, .Done(); } +bool IsSortKeyPrimary(const NYql::NNodes::TCoLambda& keySelector, const NYql::TKikimrTableDescription& tableDesc, + const TMaybe<THashSet<TStringBuf>>& passthroughFields) +{ + auto checkKey = [keySelector, &tableDesc, &passthroughFields] (NYql::NNodes::TExprBase key, ui32 index) { + if (!key.Maybe<TCoMember>()) { + return false; + } + + auto member = key.Cast<TCoMember>(); + if (member.Struct().Raw() != keySelector.Args().Arg(0).Raw()) { + return false; + } + + auto column = TString(member.Name().Value()); + auto columnIndex = tableDesc.GetKeyColumnIndex(column); + if (!columnIndex || *columnIndex != index) { + return false; + } + + if (passthroughFields && !passthroughFields->contains(column)) { + return false; + } + + return true; + }; + + auto lambdaBody = keySelector.Body(); + if (auto maybeTuple = lambdaBody.Maybe<TExprList>()) { + auto tuple = maybeTuple.Cast(); + for (size_t i = 0; i < tuple.Size(); ++i) { + if (!checkKey(tuple.Item(i), i)) { + return false; + } + } + } else { + if (!checkKey(lambdaBody, 0)) { + return false; + } + } + + return true; +} + +ESortDirection GetSortDirection(const NYql::NNodes::TExprBase& sortDirections) { + auto getDirection = [] (TExprBase expr) -> ESortDirection { + if (!expr.Maybe<TCoBool>()) { + return ESortDirection::Unknown; + } + + if (!FromString<bool>(expr.Cast<TCoBool>().Literal().Value())) { + return ESortDirection::Reverse; + } + + return ESortDirection::Forward; + }; + + auto direction = ESortDirection::None; + if (auto maybeList = sortDirections.Maybe<TExprList>()) { + for (const auto& expr : maybeList.Cast()) { + direction |= getDirection(expr); + } + } else { + direction |= getDirection(sortDirections); + } + return direction; +}; + } // namespace NKikimr::NKqp::NOpt diff --git a/ydb/core/kqp/opt/physical/kqp_opt_phy_impl.h b/ydb/core/kqp/opt/physical/kqp_opt_phy_impl.h index 87fe7377226..d71bdb58022 100644 --- a/ydb/core/kqp/opt/physical/kqp_opt_phy_impl.h +++ b/ydb/core/kqp/opt/physical/kqp_opt_phy_impl.h @@ -28,6 +28,26 @@ NYql::NNodes::TDqStage ReplaceStageArg(NYql::NNodes::TDqStage stage, size_t inpu NYql::NNodes::TDqStage ReplaceTableSourceSettings(NYql::NNodes::TDqStage stage, size_t inputIndex, NYql::NNodes::TKqpReadRangesSourceSettings settings, NYql::TExprContext& ctx); +bool IsSortKeyPrimary(const NYql::NNodes::TCoLambda& keySelector, const NYql::TKikimrTableDescription& tableDesc, + const TMaybe<THashSet<TStringBuf>>& passthroughFields = {}); + +enum ESortDirection : ui32 { + None = 0, + Forward = 1, + Reverse = 2, + Unknown = 4, +}; + +using ESortDirectionRaw = std::underlying_type<ESortDirection>::type; + +inline ESortDirection operator|(ESortDirection a, ESortDirection b) { + return ESortDirection(static_cast<ESortDirectionRaw>(a) | static_cast<ESortDirectionRaw>(b)); +} + +inline ESortDirection operator|=(ESortDirection& a, ESortDirection b) { return (a = a | b); } + +ESortDirection GetSortDirection(const NYql::NNodes::TExprBase& sortDirections); + } // NKikimr::NKqp::NOpt diff --git a/ydb/core/kqp/opt/physical/kqp_opt_phy_limit.cpp b/ydb/core/kqp/opt/physical/kqp_opt_phy_limit.cpp index e538b09367f..59863118c78 100644 --- a/ydb/core/kqp/opt/physical/kqp_opt_phy_limit.cpp +++ b/ydb/core/kqp/opt/physical/kqp_opt_phy_limit.cpp @@ -87,5 +87,104 @@ TExprBase KqpApplyLimitToReadTable(TExprBase node, TExprContext& ctx, const TKqp .Done(); } +TExprBase KqpApplyLimitToOlapReadTable(TExprBase node, TExprContext& ctx, const TKqpOptimizeContext& kqpCtx) { + if (!node.Maybe<TCoTopSort>()) { + return node; + } + auto topSort = node.Cast<TCoTopSort>(); + + // Column Shards always return result sorted by PK in ASC order + ESortDirection direction = GetSortDirection(topSort.SortDirections()); + if (direction != ESortDirection::Forward && direction != ESortDirection::Reverse) { + return node; + } + + auto maybeSkip = topSort.Input().Maybe<TCoSkip>(); + auto input = maybeSkip ? maybeSkip.Cast().Input() : topSort.Input(); + + bool isReadTable = input.Maybe<TKqpReadOlapTableRanges>().IsValid(); + + if (!isReadTable) { + return node; + } + + if (!kqpCtx.IsScanQuery()) { + return node; + } + + const bool isReadTableRanges = true; + auto& tableDesc = kqpCtx.Tables->ExistingTable(kqpCtx.Cluster, GetReadTablePath(input, isReadTableRanges)); + + if (tableDesc.Metadata->Kind != EKikimrTableKind::Olap) { + return node; + } + + auto settings = GetReadTableSettings(input, isReadTableRanges); + if (settings.ItemsLimit) { + return node; // already set + } + if (direction == ESortDirection::Reverse) { + settings.SetReverse(); + } + + auto keySelector = topSort.KeySelectorLambda(); + if (!IsSortKeyPrimary(keySelector, tableDesc)) { + // Column shards return data sorted by PK + // So we can pushdown limit only if query has sort by PK + return node; + } + + TMaybeNode<TExprBase> limitValue; + auto maybeTopSortCount = topSort.Count().Maybe<TCoUint64>(); + auto maybeSkipCount = maybeSkip.Count().Maybe<TCoUint64>(); + + if (maybeTopSortCount && (!maybeSkip || maybeSkipCount)) { + ui64 totalLimit = FromString<ui64>(maybeTopSortCount.Cast().Literal().Value()); + + if (maybeSkipCount) { + totalLimit += FromString<ui64>(maybeSkipCount.Cast().Literal().Value()); + } + + limitValue = Build<TCoUint64>(ctx, node.Pos()) + .Literal<TCoAtom>() + .Value(ToString(totalLimit)).Build() + .Done(); + } else { + limitValue = topSort.Count(); + if (maybeSkip) { + limitValue = Build<TCoPlus>(ctx, node.Pos()) + .Left(limitValue.Cast()) + .Right(maybeSkip.Cast().Count()) + .Done(); + } + } + + YQL_CLOG(TRACE, ProviderKqp) << "-- set limit items value to " << limitValue.Cast().Ref().Dump(); + + if (limitValue.Maybe<TCoUint64>()) { + settings.SetItemsLimit(limitValue.Cast().Ptr()); + } else { + settings.SetItemsLimit(Build<TDqPrecompute>(ctx, node.Pos()) + .Input(limitValue.Cast()) + .Done().Ptr()); + } + + input = BuildReadNode(node.Pos(), ctx, input, settings); + + if (maybeSkip) { + input = Build<TCoSkip>(ctx, node.Pos()) + .Input(input) + .Count(maybeSkip.Cast().Count()) + .Done(); + } + + return Build<TCoTopSort>(ctx, topSort.Pos()) + .Input(input) + .Count(topSort.Count()) + .SortDirections(topSort.SortDirections()) + .KeySelectorLambda(topSort.KeySelectorLambda()) + .Done(); +} + } // namespace NKikimr::NKqp::NOpt diff --git a/ydb/core/kqp/opt/physical/kqp_opt_phy_rules.h b/ydb/core/kqp/opt/physical/kqp_opt_phy_rules.h index 98fb1e57324..2c7e86912e5 100644 --- a/ydb/core/kqp/opt/physical/kqp_opt_phy_rules.h +++ b/ydb/core/kqp/opt/physical/kqp_opt_phy_rules.h @@ -29,6 +29,9 @@ NYql::NNodes::TExprBase KqpRemoveRedundantSortByPk(NYql::NNodes::TExprBase node, NYql::NNodes::TExprBase KqpApplyLimitToReadTable(NYql::NNodes::TExprBase node, NYql::TExprContext& ctx, const TKqpOptimizeContext& kqpCtx); +NYql::NNodes::TExprBase KqpApplyLimitToOlapReadTable(NYql::NNodes::TExprBase node, NYql::TExprContext& ctx, + const TKqpOptimizeContext& kqpCtx); + NYql::NNodes::TExprBase KqpPushOlapFilter(NYql::NNodes::TExprBase node, NYql::TExprContext& ctx, const TKqpOptimizeContext& kqpCtx, NYql::TTypeAnnotationContext& typesCtx); diff --git a/ydb/core/kqp/opt/physical/kqp_opt_phy_sort.cpp b/ydb/core/kqp/opt/physical/kqp_opt_phy_sort.cpp index f0e11f37c20..7d39948193f 100644 --- a/ydb/core/kqp/opt/physical/kqp_opt_phy_sort.cpp +++ b/ydb/core/kqp/opt/physical/kqp_opt_phy_sort.cpp @@ -41,39 +41,9 @@ TExprBase KqpRemoveRedundantSortByPk(TExprBase node, TExprContext& ctx, const TK input = flatmap.Input(); } - enum : ui32 { - SortDirectionNone = 0, - SortDirectionForward = 1, - SortDirectionReverse = 2, - SortDirectionUnknown = 4, - }; - - auto getDirection = [] (TExprBase expr) -> ui32 { - if (!expr.Maybe<TCoBool>()) { - return SortDirectionUnknown; - } - - if (!FromString<bool>(expr.Cast<TCoBool>().Literal().Value())) { - return SortDirectionReverse; - } - - return SortDirectionForward; - }; - - ui32 direction = SortDirectionNone; - - if (auto maybeList = sortDirections.Maybe<TExprList>()) { - for (const auto& expr : maybeList.Cast()) { - direction |= getDirection(expr); - if (direction != SortDirectionForward && direction != SortDirectionReverse) { - return node; - } - } - } else { - direction |= getDirection(sortDirections); - if (direction != SortDirectionForward && direction != SortDirectionReverse) { - return node; - } + auto direction = GetSortDirection(sortDirections); + if (direction != ESortDirection::Forward && direction != ESortDirection::Reverse) { + return node; } bool isReadTable = input.Maybe<TKqpReadTable>().IsValid(); @@ -82,48 +52,20 @@ TExprBase KqpRemoveRedundantSortByPk(TExprBase node, TExprContext& ctx, const TK return node; } auto& tableDesc = kqpCtx.Tables->ExistingTable(kqpCtx.Cluster, GetReadTablePath(input, isReadTableRanges)); - auto settings = GetReadTableSettings(input, isReadTableRanges); - - auto checkKey = [keySelector, &tableDesc, &passthroughFields] (TExprBase key, ui32 index) { - if (!key.Maybe<TCoMember>()) { - return false; - } - auto member = key.Cast<TCoMember>(); - if (member.Struct().Raw() != keySelector.Args().Arg(0).Raw()) { - return false; - } - - auto column = TString(member.Name().Value()); - auto columnIndex = tableDesc.GetKeyColumnIndex(column); - if (!columnIndex || *columnIndex != index) { - return false; - } - - if (passthroughFields && !passthroughFields->contains(column)) { - return false; - } + if(tableDesc.Metadata->Kind == EKikimrTableKind::Olap) { + // OLAP tables are read in parallel, so we need to keep the out sort. + return node; + } - return true; - }; + auto settings = GetReadTableSettings(input, isReadTableRanges); - auto lambdaBody = keySelector.Body(); - if (auto maybeTuple = lambdaBody.Maybe<TExprList>()) { - auto tuple = maybeTuple.Cast(); - for (size_t i = 0; i < tuple.Size(); ++i) { - if (!checkKey(tuple.Item(i), i)) { - return node; - } - } - } else { - if (!checkKey(lambdaBody, 0)) { - return node; - } + if (!IsSortKeyPrimary(keySelector, tableDesc, passthroughFields)) { + return node; } - bool olapTable = tableDesc.Metadata->Kind == EKikimrTableKind::Olap; - if (direction == SortDirectionReverse) { - if (!UseSource(kqpCtx, tableDesc) && !olapTable && kqpCtx.IsScanQuery()) { + if (direction == ESortDirection::Reverse) { + if (!UseSource(kqpCtx, tableDesc) && kqpCtx.IsScanQuery()) { return node; } @@ -135,8 +77,8 @@ TExprBase KqpRemoveRedundantSortByPk(TExprBase node, TExprContext& ctx, const TK settings.SetSorted(); input = BuildReadNode(input.Pos(), ctx, input, settings); - } else if (direction == SortDirectionForward) { - if (olapTable || UseSource(kqpCtx, tableDesc)) { + } else if (direction == ESortDirection::Forward) { + if (UseSource(kqpCtx, tableDesc)) { settings.SetSorted(); input = BuildReadNode(input.Pos(), ctx, input, settings); } diff --git a/ydb/core/kqp/ut/olap/kqp_olap_ut.cpp b/ydb/core/kqp/ut/olap/kqp_olap_ut.cpp index 94a30a2c3d7..b84fd91dfa5 100644 --- a/ydb/core/kqp/ut/olap/kqp_olap_ut.cpp +++ b/ydb/core/kqp/ut/olap/kqp_olap_ut.cpp @@ -1134,12 +1134,14 @@ Y_UNIT_TEST_SUITE(KqpOlap) { Cerr << result.QueryStats->query_plan() << Endl; Cerr << result.QueryStats->query_ast() << Endl; - node = FindPlanNodeByKv(plan, "Node Type", "Limit-TableFullScan"); + node = FindPlanNodeByKv(plan, "Node Type", "TopSort-TableFullScan"); UNIT_ASSERT(node.IsDefined()); reverse = FindPlanNodeByKv(node, "Reverse", "false"); UNIT_ASSERT(!reverse.IsDefined()); pushedLimit = FindPlanNodeByKv(node, "ReadLimit", "4"); UNIT_ASSERT(pushedLimit.IsDefined()); + limit = FindPlanNodeByKv(node, "Limit", "4"); + UNIT_ASSERT(limit.IsDefined()); // Check that Reverse flag is set in query plan it = tableClient.StreamExecuteScanQuery(selectQueryWithSort, scanSettings).GetValueSync(); @@ -1151,7 +1153,7 @@ Y_UNIT_TEST_SUITE(KqpOlap) { Cerr << result.QueryStats->query_plan() << Endl; Cerr << result.QueryStats->query_ast() << Endl; - node = FindPlanNodeByKv(plan, "Node Type", "Limit-TableFullScan"); + node = FindPlanNodeByKv(plan, "Node Type", "TopSort-TableFullScan"); UNIT_ASSERT(node.IsDefined()); reverse = FindPlanNodeByKv(node, "Reverse", "true"); UNIT_ASSERT(reverse.IsDefined()); diff --git a/ydb/tests/functional/clickbench/canondata/test.test_plans_column_/queries-original-plan-column-23 b/ydb/tests/functional/clickbench/canondata/test.test_plans_column_/queries-original-plan-column-23 index 2658d48f60e..b02e30f559a 100644 --- a/ydb/tests/functional/clickbench/canondata/test.test_plans_column_/queries-original-plan-column-23 +++ b/ydb/tests/functional/clickbench/canondata/test.test_plans_column_/queries-original-plan-column-23 @@ -30,11 +30,12 @@ "PlanNodeType": "Connection", "Plans": [ { - "Node Type": "Limit-TableFullScan", + "Node Type": "TopSort-TableFullScan", "Operators": [ { "Limit": "10", - "Name": "Limit" + "Name": "TopSort", + "TopSortBy": "$4.EventTime" }, { "Name": "TableFullScan", diff --git a/ydb/tests/functional/clickbench/canondata/test.test_plans_column_/queries-original-plan-column-24 b/ydb/tests/functional/clickbench/canondata/test.test_plans_column_/queries-original-plan-column-24 index 25e3bde2e7f..ae0d62718f4 100644 --- a/ydb/tests/functional/clickbench/canondata/test.test_plans_column_/queries-original-plan-column-24 +++ b/ydb/tests/functional/clickbench/canondata/test.test_plans_column_/queries-original-plan-column-24 @@ -30,11 +30,12 @@ "PlanNodeType": "Connection", "Plans": [ { - "Node Type": "Limit-TableFullScan", + "Node Type": "TopSort-TableFullScan", "Operators": [ { "Limit": "10", - "Name": "Limit" + "Name": "TopSort", + "TopSortBy": "$4.EventTime" }, { "Name": "TableFullScan", diff --git a/ydb/tests/functional/clickbench/canondata/test.test_run_determentistic_column_/queries-deterministic-results-24 b/ydb/tests/functional/clickbench/canondata/test.test_run_determentistic_column_/queries-deterministic-results-24 index bb222137018..0543ecb5022 100644 --- a/ydb/tests/functional/clickbench/canondata/test.test_run_determentistic_column_/queries-deterministic-results-24 +++ b/ydb/tests/functional/clickbench/canondata/test.test_run_determentistic_column_/queries-deterministic-results-24 @@ -29,7 +29,7 @@ }, { "EventTime": 1373832036000000, - "SearchPhrase": "\u0432\u0435\u0434\u043e\u043c\u043e\u0441\u043a\u0432\u044b \u0441 \u043b\u044f\u0439\u0441\u0430\u043d \u0431\u0430\u043b\u043b\u044b" + "SearchPhrase": "\u0430\u0440\u043c\u044f\u043d\u0441\u043a" }, { "EventTime": 1373832036000000, @@ -37,6 +37,6 @@ }, { "EventTime": 1373832036000000, - "SearchPhrase": "\u0430\u0440\u043c\u044f\u043d\u0441\u043a" + "SearchPhrase": "\u0432\u0435\u0434\u043e\u043c\u043e\u0441\u043a\u0432\u044b \u0441 \u043b\u044f\u0439\u0441\u0430\u043d \u0431\u0430\u043b\u043b\u044b" } ]
\ No newline at end of file diff --git a/ydb/tests/functional/clickbench/canondata/test.test_run_determentistic_row_/queries-deterministic-results-24 b/ydb/tests/functional/clickbench/canondata/test.test_run_determentistic_row_/queries-deterministic-results-24 index bb222137018..0543ecb5022 100644 --- a/ydb/tests/functional/clickbench/canondata/test.test_run_determentistic_row_/queries-deterministic-results-24 +++ b/ydb/tests/functional/clickbench/canondata/test.test_run_determentistic_row_/queries-deterministic-results-24 @@ -29,7 +29,7 @@ }, { "EventTime": 1373832036000000, - "SearchPhrase": "\u0432\u0435\u0434\u043e\u043c\u043e\u0441\u043a\u0432\u044b \u0441 \u043b\u044f\u0439\u0441\u0430\u043d \u0431\u0430\u043b\u043b\u044b" + "SearchPhrase": "\u0430\u0440\u043c\u044f\u043d\u0441\u043a" }, { "EventTime": 1373832036000000, @@ -37,6 +37,6 @@ }, { "EventTime": 1373832036000000, - "SearchPhrase": "\u0430\u0440\u043c\u044f\u043d\u0441\u043a" + "SearchPhrase": "\u0432\u0435\u0434\u043e\u043c\u043e\u0441\u043a\u0432\u044b \u0441 \u043b\u044f\u0439\u0441\u0430\u043d \u0431\u0430\u043b\u043b\u044b" } ]
\ No newline at end of file diff --git a/ydb/tests/functional/clickbench/data/queries-deterministic.sql b/ydb/tests/functional/clickbench/data/queries-deterministic.sql index bf87ac6b459..635c2486ec9 100644 --- a/ydb/tests/functional/clickbench/data/queries-deterministic.sql +++ b/ydb/tests/functional/clickbench/data/queries-deterministic.sql @@ -22,7 +22,7 @@ /*21*/ SELECT SearchPhrase, MIN(URL), COUNT(*) AS c FROM $data WHERE URL LIKE '%google%' AND SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY c DESC LIMIT 10; /*22*/ SELECT SearchPhrase, MIN(URL), MIN(Title), COUNT(*) AS c, COUNT(DISTINCT UserID) FROM $data WHERE Title LIKE '%Google%' AND URL NOT LIKE '%.google.%' AND SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY c DESC, SearchPhrase, column1, column2 LIMIT 10; /*23*/ SELECT * FROM $data WHERE URL LIKE '%google%' ORDER BY EventTime LIMIT 10; -/*24*/ SELECT SearchPhrase, EventTime FROM $data WHERE SearchPhrase <> '' ORDER BY EventTime LIMIT 10; +/*24*/ SELECT SearchPhrase, EventTime FROM $data WHERE SearchPhrase <> '' ORDER BY EventTime, SearchPhrase LIMIT 10; /*25*/ SELECT SearchPhrase FROM $data WHERE SearchPhrase <> '' ORDER BY SearchPhrase LIMIT 10; /*26*/ SELECT SearchPhrase, EventTime FROM $data WHERE SearchPhrase <> '' ORDER BY EventTime, SearchPhrase LIMIT 10; /*27*/ SELECT CounterID, AVG(length(URL)) AS l, COUNT(*) AS c FROM $data WHERE URL <> '' GROUP BY CounterID HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25; |