From e617ff9b5922d8dc3380c2497ef5f7b955c8bb94 Mon Sep 17 00:00:00 2001 From: Josipmrden <josip.mrden@memgraph.io> Date: Tue, 24 Oct 2023 22:20:05 +0200 Subject: [PATCH] Provide textual information for inefficient plans with notifications (#1343) --- libs/CMakeLists.txt | 138 +++++++++------- libs/setup.sh | 7 +- src/query/CMakeLists.txt | 45 ++--- src/query/interpreter.cpp | 38 +++-- src/query/metadata.cpp | 4 +- src/query/metadata.hpp | 3 +- src/query/plan/hint_provider.cpp | 24 +++ src/query/plan/hint_provider.hpp | 255 +++++++++++++++++++++++++++++ src/query/plan/preprocess.hpp | 11 ++ tests/unit/CMakeLists.txt | 4 +- tests/unit/query_hint_provider.cpp | 166 +++++++++++++++++++ 11 files changed, 598 insertions(+), 97 deletions(-) create mode 100644 src/query/plan/hint_provider.cpp create mode 100644 src/query/plan/hint_provider.hpp create mode 100644 tests/unit/query_hint_provider.cpp diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt index 411b180ab..b9dd80fe6 100644 --- a/libs/CMakeLists.txt +++ b/libs/CMakeLists.txt @@ -4,7 +4,8 @@ include(GNUInstallDirs) include(ProcessorCount) ProcessorCount(NPROC) -if (NPROC EQUAL 0) + +if(NPROC EQUAL 0) set(NPROC 1) endif() @@ -12,6 +13,7 @@ find_package(Boost 1.78 REQUIRED) find_package(BZip2 1.0.6 REQUIRED) find_package(Threads REQUIRED) set(GFLAGS_NOTHREADS OFF) + # NOTE: config/generate.py depends on the gflags help XML format. find_package(gflags REQUIRED) find_package(fmt 8.0.1) @@ -23,24 +25,27 @@ set(LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}) function(import_header_library name include_dir) add_library(${name} INTERFACE IMPORTED GLOBAL) set_property(TARGET ${name} PROPERTY - INTERFACE_INCLUDE_DIRECTORIES ${include_dir}) + INTERFACE_INCLUDE_DIRECTORIES ${include_dir}) string(TOUPPER ${name} _upper_name) set(${_upper_name}_INCLUDE_DIR ${include_dir} CACHE FILEPATH - "Path to ${name} include directory" FORCE) + "Path to ${name} include directory" FORCE) mark_as_advanced(${_upper_name}_INCLUDE_DIR) add_library(lib::${name} ALIAS ${name}) endfunction(import_header_library) function(import_library name type location include_dir) add_library(${name} ${type} IMPORTED GLOBAL) - if (${ARGN}) + + if(${ARGN}) # Optional argument is the name of the external project that we need to # depend on. add_dependencies(${name} ${ARGN0}) else() add_dependencies(${name} ${name}-proj) endif() + set_property(TARGET ${name} PROPERTY IMPORTED_LOCATION ${location}) + # We need to create the include directory first in order to be able to add it # as an include directory. The header files in the include directory will be # generated later during the build process. @@ -60,29 +65,34 @@ function(add_external_project name) set(options NO_C_COMPILER) set(one_value_kwargs SOURCE_DIR BUILD_IN_SOURCE) set(multi_value_kwargs CMAKE_ARGS DEPENDS INSTALL_COMMAND BUILD_COMMAND - CONFIGURE_COMMAND) + CONFIGURE_COMMAND) cmake_parse_arguments(KW "${options}" "${one_value_kwargs}" "${multi_value_kwargs}" ${ARGN}) set(source_dir ${CMAKE_CURRENT_SOURCE_DIR}/${name}) - if (KW_SOURCE_DIR) + + if(KW_SOURCE_DIR) set(source_dir ${KW_SOURCE_DIR}) endif() + set(build_in_source 0) - if (KW_BUILD_IN_SOURCE) + + if(KW_BUILD_IN_SOURCE) set(build_in_source ${KW_BUILD_IN_SOURCE}) endif() - if (NOT KW_NO_C_COMPILER) + + if(NOT KW_NO_C_COMPILER) set(KW_CMAKE_ARGS -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} ${KW_CMAKE_ARGS}) endif() + ExternalProject_Add(${name}-proj DEPENDS ${KW_DEPENDS} - PREFIX ${source_dir} SOURCE_DIR ${source_dir} - BUILD_IN_SOURCE ${build_in_source} - CONFIGURE_COMMAND ${KW_CONFIGURE_COMMAND} - CMAKE_ARGS -DCMAKE_BUILD_TYPE=Release - -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} - -DCMAKE_INSTALL_PREFIX=${source_dir} - ${KW_CMAKE_ARGS} - INSTALL_COMMAND ${KW_INSTALL_COMMAND} - BUILD_COMMAND ${KW_BUILD_COMMAND}) + PREFIX ${source_dir} SOURCE_DIR ${source_dir} + BUILD_IN_SOURCE ${build_in_source} + CONFIGURE_COMMAND ${KW_CONFIGURE_COMMAND} + CMAKE_ARGS -DCMAKE_BUILD_TYPE=Release + -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} + -DCMAKE_INSTALL_PREFIX=${source_dir} + ${KW_CMAKE_ARGS} + INSTALL_COMMAND ${KW_INSTALL_COMMAND} + BUILD_COMMAND ${KW_BUILD_COMMAND}) endfunction(add_external_project) # Calls `add_external_project`, sets NAME_LIBRARY, NAME_INCLUDE_DIR variables @@ -91,9 +101,9 @@ macro(import_external_library name type library_location include_dir) add_external_project(${name} ${ARGN}) string(TOUPPER ${name} _upper_name) set(${_upper_name}_LIBRARY ${library_location} CACHE FILEPATH - "Path to ${name} library" FORCE) + "Path to ${name} library" FORCE) set(${_upper_name}_INCLUDE_DIR ${include_dir} CACHE FILEPATH - "Path to ${name} include directory" FORCE) + "Path to ${name} include directory" FORCE) mark_as_advanced(${_upper_name}_LIBRARY ${_upper_name}_INCLUDE_DIR) import_library(${name} ${type} ${${_upper_name}_LIBRARY} ${${_upper_name}_INCLUDE_DIR}) endmacro(import_external_library) @@ -115,10 +125,10 @@ import_external_library(antlr4 STATIC ${CMAKE_CURRENT_SOURCE_DIR}/antlr4/runtime/Cpp/include/antlr4-runtime SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/antlr4/runtime/Cpp CMAKE_ARGS # http://stackoverflow.com/questions/37096062/get-a-basic-c-program-to-compile-using-clang-on-ubuntu-16/38385967#38385967 - -DWITH_LIBCXX=OFF # because of debian bug - -DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=true - -DCMAKE_CXX_STANDARD=20 - -DANTLR_BUILD_CPP_TESTS=OFF + -DWITH_LIBCXX=OFF # because of debian bug + -DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=true + -DCMAKE_CXX_STANDARD=20 + -DANTLR_BUILD_CPP_TESTS=OFF BUILD_COMMAND $(MAKE) antlr4_static INSTALL_COMMAND $(MAKE) install) @@ -126,6 +136,7 @@ import_external_library(antlr4 STATIC import_external_library(benchmark STATIC ${CMAKE_CURRENT_SOURCE_DIR}/benchmark/${CMAKE_INSTALL_LIBDIR}/libbenchmark.a ${CMAKE_CURRENT_SOURCE_DIR}/benchmark/include + # Skip testing. The tests don't compile with Clang 8. CMAKE_ARGS -DBENCHMARK_ENABLE_TESTING=OFF) @@ -141,15 +152,15 @@ add_subdirectory(rapidcheck EXCLUDE_FROM_ALL) # setup google test add_external_project(gtest SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/googletest) set(GTEST_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/googletest/include - CACHE PATH "Path to gtest and gmock include directory" FORCE) + CACHE PATH "Path to gtest and gmock include directory" FORCE) set(GMOCK_LIBRARY ${CMAKE_CURRENT_SOURCE_DIR}/googletest/lib/libgmock.a - CACHE FILEPATH "Path to gmock library" FORCE) + CACHE FILEPATH "Path to gmock library" FORCE) set(GMOCK_MAIN_LIBRARY ${CMAKE_CURRENT_SOURCE_DIR}/googletest/lib/libgmock_main.a - CACHE FILEPATH "Path to gmock_main library" FORCE) + CACHE FILEPATH "Path to gmock_main library" FORCE) set(GTEST_LIBRARY ${CMAKE_CURRENT_SOURCE_DIR}/googletest/lib/libgtest.a - CACHE FILEPATH "Path to gtest library" FORCE) + CACHE FILEPATH "Path to gtest library" FORCE) set(GTEST_MAIN_LIBRARY ${CMAKE_CURRENT_SOURCE_DIR}/googletest/lib/libgtest_main.a - CACHE FILEPATH "Path to gtest_main library" FORCE) + CACHE FILEPATH "Path to gtest_main library" FORCE) mark_as_advanced(GTEST_INCLUDE_DIR GMOCK_LIBRARY GMOCK_MAIN_LIBRARY GTEST_LIBRARY GTEST_MAIN_LIBRARY) import_library(gtest STATIC ${GTEST_LIBRARY} ${GTEST_INCLUDE_DIR} gtest-proj) import_library(gtest_main STATIC ${GTEST_MAIN_LIBRARY} ${GTEST_INCLUDE_DIR} gtest-proj) @@ -167,10 +178,10 @@ import_external_library(rocksdb STATIC ${CMAKE_CURRENT_SOURCE_DIR}/rocksdb/lib/librocksdb.a ${CMAKE_CURRENT_SOURCE_DIR}/rocksdb/include CMAKE_ARGS -DUSE_RTTI=ON - -DWITH_TESTS=OFF - -DGFLAGS_NOTHREADS=OFF - -DCMAKE_INSTALL_LIBDIR=lib - -DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=true + -DWITH_TESTS=OFF + -DGFLAGS_NOTHREADS=OFF + -DCMAKE_INSTALL_LIBDIR=lib + -DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=true BUILD_COMMAND $(MAKE) rocksdb) # Setup libbcrypt @@ -179,8 +190,8 @@ import_external_library(libbcrypt STATIC ${CMAKE_CURRENT_SOURCE_DIR}/libbcrypt CONFIGURE_COMMAND sed s/-Wcast-align// -i ${CMAKE_CURRENT_SOURCE_DIR}/libbcrypt/crypt_blowfish/Makefile BUILD_COMMAND make -C ${CMAKE_CURRENT_SOURCE_DIR}/libbcrypt - CC=${CMAKE_C_COMPILER} - CXX=${CMAKE_CXX_COMPILER} + CC=${CMAKE_C_COMPILER} + CXX=${CMAKE_CXX_COMPILER} INSTALL_COMMAND true) # Setup mgclient @@ -188,16 +199,16 @@ import_external_library(mgclient STATIC ${CMAKE_CURRENT_SOURCE_DIR}/mgclient/lib/libmgclient.a ${CMAKE_CURRENT_SOURCE_DIR}/mgclient/include CMAKE_ARGS -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} - -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} - -DBUILD_TESTING=OFF - -DBUILD_CPP_BINDINGS=ON) + -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} + -DBUILD_TESTING=OFF + -DBUILD_CPP_BINDINGS=ON) find_package(OpenSSL REQUIRED) target_link_libraries(mgclient INTERFACE ${OPENSSL_LIBRARIES}) add_external_project(mgconsole SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/mgconsole CMAKE_ARGS - -DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_BINARY_DIR} + -DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_BINARY_DIR} BUILD_COMMAND $(MAKE) mgconsole) add_custom_target(mgconsole DEPENDS mgconsole-proj) @@ -214,14 +225,15 @@ import_external_library(librdkafka STATIC ${CMAKE_CURRENT_SOURCE_DIR}/librdkafka/lib/librdkafka.a ${CMAKE_CURRENT_SOURCE_DIR}/librdkafka/include CMAKE_ARGS -DRDKAFKA_BUILD_STATIC=ON - -DRDKAFKA_BUILD_EXAMPLES=OFF - -DRDKAFKA_BUILD_TESTS=OFF - -DWITH_ZSTD=OFF - -DENABLE_LZ4_EXT=OFF - -DCMAKE_INSTALL_LIBDIR=lib - -DWITH_SSL=ON - # If we want SASL, we need to install it on build machines - -DWITH_SASL=OFF) + -DRDKAFKA_BUILD_EXAMPLES=OFF + -DRDKAFKA_BUILD_TESTS=OFF + -DWITH_ZSTD=OFF + -DENABLE_LZ4_EXT=OFF + -DCMAKE_INSTALL_LIBDIR=lib + -DWITH_SSL=ON + + # If we want SASL, we need to install it on build machines + -DWITH_SASL=OFF) target_link_libraries(librdkafka INTERFACE ${OPENSSL_LIBRARIES} ZLIB::ZLIB) import_library(librdkafka++ STATIC @@ -242,24 +254,24 @@ import_external_library(pulsar STATIC ${CMAKE_CURRENT_SOURCE_DIR}/pulsar/install/include BUILD_IN_SOURCE 1 CONFIGURE_COMMAND cmake pulsar-client-cpp - -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_SOURCE_DIR}/pulsar/install - -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} - -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} - -DBUILD_DYNAMIC_LIB=OFF - -DBUILD_STATIC_LIB=ON - -DBUILD_TESTS=OFF - -DLINK_STATIC=ON - -DPROTOC_PATH=${PROTOBUF_ROOT}/bin/protoc - -DBOOST_ROOT=${BOOST_ROOT} - -DCMAKE_PREFIX_PATH=${PROTOBUF_ROOT} - -DProtobuf_INCLUDE_DIRS=${PROTOBUF_ROOT}/include - -DBUILD_PYTHON_WRAPPER=OFF - -DBUILD_PERF_TOOLS=OFF - -DUSE_LOG4CXX=OFF - BUILD_COMMAND $(MAKE) pulsarStaticWithDeps) + -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_SOURCE_DIR}/pulsar/install + -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} + -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} + -DBUILD_DYNAMIC_LIB=OFF + -DBUILD_STATIC_LIB=ON + -DBUILD_TESTS=OFF + -DLINK_STATIC=ON + -DPROTOC_PATH=${PROTOBUF_ROOT}/bin/protoc + -DBOOST_ROOT=${BOOST_ROOT} + -DCMAKE_PREFIX_PATH=${PROTOBUF_ROOT} + -DProtobuf_INCLUDE_DIRS=${PROTOBUF_ROOT}/include + -DBUILD_PYTHON_WRAPPER=OFF + -DBUILD_PERF_TOOLS=OFF + -DUSE_LOG4CXX=OFF + BUILD_COMMAND $(MAKE) pulsarStaticWithDeps) add_dependencies(pulsar-proj protobuf) -if (${MG_ARCH} STREQUAL "ARM64") +if(${MG_ARCH} STREQUAL "ARM64") set(MG_LIBRDTSC_CMAKE_ARGS -DLIBRDTSC_ARCH_x86=OFF -DLIBRDTSC_ARCH_ARM64=ON) endif() @@ -280,3 +292,5 @@ add_subdirectory(absl EXCLUDE_FROM_ALL) set_path_external_library(jemalloc STATIC ${CMAKE_CURRENT_SOURCE_DIR}/jemalloc/lib/libjemalloc.a ${CMAKE_CURRENT_SOURCE_DIR}/jemalloc/include/) + +import_header_library(rangev3 ${CMAKE_CURRENT_SOURCE_DIR}/rangev3/include) diff --git a/libs/setup.sh b/libs/setup.sh index 638c1b9f2..028797679 100755 --- a/libs/setup.sh +++ b/libs/setup.sh @@ -125,6 +125,7 @@ declare -A primary_urls=( ["ctre"]="http://$local_cache_host/file/hanickadot/compile-time-regular-expressions/v3.7.2/single-header/ctre.hpp" ["absl"]="https://$local_cache_host/git/abseil-cpp.git" ["jemalloc"]="https://$local_cache_host/git/jemalloc.git" + ["range-v3"]="https://$local_cache_host/git/ericniebler/range-v3.git" ) # The goal of secondary urls is to have links to the "source of truth" of @@ -153,6 +154,7 @@ declare -A secondary_urls=( ["ctre"]="https://raw.githubusercontent.com/hanickadot/compile-time-regular-expressions/v3.7.2/single-header/ctre.hpp" ["absl"]="https://github.com/abseil/abseil-cpp.git" ["jemalloc"]="https://github.com/jemalloc/jemalloc.git" + ["range-v3"]="https://github.com/ericniebler/range-v3.git" ) # antlr @@ -255,7 +257,6 @@ cd .. absl_ref="20230125.3" repo_clone_try_double "${primary_urls[absl]}" "${secondary_urls[absl]}" "absl" "$absl_ref" - # jemalloc ea6b3e973b477b8061e0076bb257dbd7f3faa756 JEMALLOC_COMMIT_VERSION="5.2.1" repo_clone_try_double "${secondary_urls[jemalloc]}" "${secondary_urls[jemalloc]}" "jemalloc" "$JEMALLOC_COMMIT_VERSION" @@ -272,3 +273,7 @@ MALLOC_CONF="retain:false,percpu_arena:percpu,oversize_threshold:0,muzzy_decay_m make -j$CPUS install popd + +#range-v3 release-0.12.0 +range_v3_ref="release-0.12.0" +repo_clone_try_double "${primary_urls[range-v3]}" "${secondary_urls[range-v3]}" "rangev3" "$range_v3_ref" diff --git a/src/query/CMakeLists.txt b/src/query/CMakeLists.txt index 162c0f793..39e508ed1 100644 --- a/src/query/CMakeLists.txt +++ b/src/query/CMakeLists.txt @@ -15,6 +15,7 @@ set(mg_query_sources interpret/eval.cpp interpreter.cpp metadata.cpp + plan/hint_provider.cpp plan/operator.cpp plan/preprocess.cpp plan/pretty_print.cpp @@ -44,22 +45,25 @@ set(mg_query_sources add_library(mg-query STATIC ${mg_query_sources}) target_include_directories(mg-query PUBLIC ${CMAKE_SOURCE_DIR}/include) target_link_libraries(mg-query PUBLIC dl - cppitertools - Python3::Python - mg-integrations-pulsar - mg-integrations-kafka - mg-storage-v2 - mg-license - mg-utils - mg-kvstore - mg-memory - mg::csv - mg-flags - mg-dbms - mg-events) + cppitertools + rangev3 + Python3::Python + mg-integrations-pulsar + mg-integrations-kafka + mg-storage-v2 + mg-license + mg-utils + mg-kvstore + mg-memory + mg::csv + mg-flags + mg-dbms + mg-events) + if(NOT "${MG_PYTHON_PATH}" STREQUAL "") set(Python3_ROOT_DIR "${MG_PYTHON_PATH}") endif() + if("${MG_PYTHON_VERSION}" STREQUAL "") find_package(Python3 3.5 REQUIRED COMPONENTS Development) else() @@ -67,7 +71,6 @@ else() endif() # Generate Antlr openCypher parser - set(opencypher_frontend ${CMAKE_CURRENT_SOURCE_DIR}/frontend/opencypher) set(opencypher_generated ${opencypher_frontend}/generated) set(opencypher_lexer_grammar ${opencypher_frontend}/grammar/MemgraphCypherLexer.g4) @@ -90,15 +93,15 @@ add_custom_command( OUTPUT ${antlr_opencypher_generated_src} ${antlr_opencypher_generated_include} COMMAND ${CMAKE_COMMAND} -E make_directory ${opencypher_generated} COMMAND - java -jar ${CMAKE_SOURCE_DIR}/libs/antlr-4.10.1-complete.jar - -Dlanguage=Cpp -visitor -package antlropencypher - -o ${opencypher_generated} - ${opencypher_lexer_grammar} ${opencypher_parser_grammar} + java -jar ${CMAKE_SOURCE_DIR}/libs/antlr-4.10.1-complete.jar + -Dlanguage=Cpp -visitor -package antlropencypher + -o ${opencypher_generated} + ${opencypher_lexer_grammar} ${opencypher_parser_grammar} WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" DEPENDS - ${opencypher_lexer_grammar} ${opencypher_parser_grammar} - ${opencypher_frontend}/grammar/CypherLexer.g4 - ${opencypher_frontend}/grammar/Cypher.g4) + ${opencypher_lexer_grammar} ${opencypher_parser_grammar} + ${opencypher_frontend}/grammar/CypherLexer.g4 + ${opencypher_frontend}/grammar/Cypher.g4) add_custom_target(generate_opencypher_parser DEPENDS ${antlr_opencypher_generated_src} ${antlr_opencypher_generated_include}) diff --git a/src/query/interpreter.cpp b/src/query/interpreter.cpp index 90de5d4d2..f692552a6 100644 --- a/src/query/interpreter.cpp +++ b/src/query/interpreter.cpp @@ -56,6 +56,7 @@ #include "query/interpret/eval.hpp" #include "query/interpret/frame.hpp" #include "query/metadata.hpp" +#include "query/plan/hint_provider.hpp" #include "query/plan/planner.hpp" #include "query/plan/profile.hpp" #include "query/plan/vertex_count_cache.hpp" @@ -1610,6 +1611,11 @@ PreparedQuery PrepareCypherQuery(ParsedQuery parsed_query, std::map<std::string, auto plan = CypherQueryToPlan(parsed_query.stripped_query.hash(), std::move(parsed_query.ast_storage), cypher_query, parsed_query.parameters, plan_cache, dba); + auto hints = plan::ProvidePlanHints(&plan->plan(), plan->symbol_table()); + for (const auto &hint : hints) { + notifications->emplace_back(SeverityLevel::INFO, NotificationCode::PLAN_HINTING, hint); + } + TryCaching(plan->ast_storage(), frame_change_collector); summary->insert_or_assign("cost_estimate", plan->cost()); auto rw_type_checker = plan::ReadWriteTypeChecker(); @@ -1646,7 +1652,8 @@ PreparedQuery PrepareCypherQuery(ParsedQuery parsed_query, std::map<std::string, } PreparedQuery PrepareExplainQuery(ParsedQuery parsed_query, std::map<std::string, TypedValue> *summary, - InterpreterContext *interpreter_context, CurrentDB ¤t_db) { + std::vector<Notification> *notifications, InterpreterContext *interpreter_context, + CurrentDB ¤t_db) { const std::string kExplainQueryStart = "explain "; MG_ASSERT(utils::StartsWith(utils::ToLowerCase(parsed_query.stripped_query.query()), kExplainQueryStart), "Expected stripped query to start with '{}'", kExplainQueryStart); @@ -1673,6 +1680,11 @@ PreparedQuery PrepareExplainQuery(ParsedQuery parsed_query, std::map<std::string CypherQueryToPlan(parsed_inner_query.stripped_query.hash(), std::move(parsed_inner_query.ast_storage), cypher_query, parsed_inner_query.parameters, plan_cache, dba); + auto hints = plan::ProvidePlanHints(&cypher_query_plan->plan(), cypher_query_plan->symbol_table()); + for (const auto &hint : hints) { + notifications->emplace_back(SeverityLevel::INFO, NotificationCode::PLAN_HINTING, hint); + } + std::stringstream printed_plan; plan::PrettyPrint(*dba, &cypher_query_plan->plan(), &printed_plan); @@ -1696,9 +1708,9 @@ PreparedQuery PrepareExplainQuery(ParsedQuery parsed_query, std::map<std::string } PreparedQuery PrepareProfileQuery(ParsedQuery parsed_query, bool in_explicit_transaction, - std::map<std::string, TypedValue> *summary, InterpreterContext *interpreter_context, - CurrentDB ¤t_db, utils::MemoryResource *execution_memory, - std::optional<std::string> const &username, + std::map<std::string, TypedValue> *summary, std::vector<Notification> *notifications, + InterpreterContext *interpreter_context, CurrentDB ¤t_db, + utils::MemoryResource *execution_memory, std::optional<std::string> const &username, std::atomic<TransactionStatus> *transaction_status, std::shared_ptr<utils::AsyncTimer> tx_timer, FrameChangeCollector *frame_change_collector) { @@ -1766,6 +1778,12 @@ PreparedQuery PrepareProfileQuery(ParsedQuery parsed_query, bool in_explicit_tra CypherQueryToPlan(parsed_inner_query.stripped_query.hash(), std::move(parsed_inner_query.ast_storage), cypher_query, parsed_inner_query.parameters, plan_cache, dba); TryCaching(cypher_query_plan->ast_storage(), frame_change_collector); + + auto hints = plan::ProvidePlanHints(&cypher_query_plan->plan(), cypher_query_plan->symbol_table()); + for (const auto &hint : hints) { + notifications->emplace_back(SeverityLevel::INFO, NotificationCode::PLAN_HINTING, hint); + } + auto rw_type_checker = plan::ReadWriteTypeChecker(); rw_type_checker.InferRWType(const_cast<plan::LogicalOperator &>(cypher_query_plan->plan())); @@ -3732,13 +3750,13 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string, current_db_, memory_resource, &query_execution->notifications, username_, &transaction_status_, current_timeout_timer_, &*frame_change_collector_); } else if (utils::Downcast<ExplainQuery>(parsed_query.query)) { - prepared_query = - PrepareExplainQuery(std::move(parsed_query), &query_execution->summary, interpreter_context_, current_db_); + prepared_query = PrepareExplainQuery(std::move(parsed_query), &query_execution->summary, + &query_execution->notifications, interpreter_context_, current_db_); } else if (utils::Downcast<ProfileQuery>(parsed_query.query)) { - prepared_query = - PrepareProfileQuery(std::move(parsed_query), in_explicit_transaction_, &query_execution->summary, - interpreter_context_, current_db_, &query_execution->execution_memory_with_exception, - username_, &transaction_status_, current_timeout_timer_, &*frame_change_collector_); + prepared_query = PrepareProfileQuery(std::move(parsed_query), in_explicit_transaction_, &query_execution->summary, + &query_execution->notifications, interpreter_context_, current_db_, + &query_execution->execution_memory_with_exception, username_, + &transaction_status_, current_timeout_timer_, &*frame_change_collector_); } else if (utils::Downcast<DumpQuery>(parsed_query.query)) { prepared_query = PrepareDumpQuery(std::move(parsed_query), current_db_); } else if (utils::Downcast<IndexQuery>(parsed_query.query)) { diff --git a/src/query/metadata.cpp b/src/query/metadata.cpp index 47b207bc0..ade17eb5c 100644 --- a/src/query/metadata.cpp +++ b/src/query/metadata.cpp @@ -1,4 +1,4 @@ -// Copyright 2022 Memgraph Ltd. +// Copyright 2023 Memgraph Ltd. // // Use of this software is governed by the Business Source License // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source @@ -62,6 +62,8 @@ constexpr std::string_view GetCodeString(const NotificationCode code) { return "IndexDoesNotExist"sv; case NotificationCode::NONEXISTENT_CONSTRAINT: return "ConstraintDoesNotExist"sv; + case NotificationCode::PLAN_HINTING: + return "PlanHinting"sv; case NotificationCode::REGISTER_REPLICA: return "RegisterReplica"sv; case NotificationCode::REPLICA_PORT_WARNING: diff --git a/src/query/metadata.hpp b/src/query/metadata.hpp index 9f72ea9de..ca3914047 100644 --- a/src/query/metadata.hpp +++ b/src/query/metadata.hpp @@ -1,4 +1,4 @@ -// Copyright 2022 Memgraph Ltd. +// Copyright 2023 Memgraph Ltd. // // Use of this software is governed by the Business Source License // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source @@ -39,6 +39,7 @@ enum class NotificationCode : uint8_t { LOAD_CSV_TIP, NONEXISTENT_INDEX, NONEXISTENT_CONSTRAINT, + PLAN_HINTING, REPLICA_PORT_WARNING, REGISTER_REPLICA, SET_REPLICA, diff --git a/src/query/plan/hint_provider.cpp b/src/query/plan/hint_provider.cpp new file mode 100644 index 000000000..3d6d7e86f --- /dev/null +++ b/src/query/plan/hint_provider.cpp @@ -0,0 +1,24 @@ +// Copyright 2023 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#include "hint_provider.hpp" + +namespace memgraph::query::plan { + +std::vector<std::string> ProvidePlanHints(const LogicalOperator *plan_root, const SymbolTable &symbol_table) { + PlanHintsProvider plan_hinter(symbol_table); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) + const_cast<LogicalOperator *>(plan_root)->Accept(plan_hinter); + + return plan_hinter.hints(); +} + +} // namespace memgraph::query::plan diff --git a/src/query/plan/hint_provider.hpp b/src/query/plan/hint_provider.hpp new file mode 100644 index 000000000..9f320ec39 --- /dev/null +++ b/src/query/plan/hint_provider.hpp @@ -0,0 +1,255 @@ +// Copyright 2023 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +/// @file +/// This file provides textual information about possible inefficiencies in the query planner. +/// An inefficiency is for example having a sequential scan with filtering, without the usage of indices. + +#pragma once + +#include <algorithm> +#include <vector> + +#include <boost/algorithm/string.hpp> +#include <range/v3/view.hpp> + +#include "query/plan/operator.hpp" +#include "query/plan/preprocess.hpp" +#include "utils/logging.hpp" +#include "utils/string.hpp" + +namespace memgraph::query::plan { + +std::vector<std::string> ProvidePlanHints(const LogicalOperator *plan_root, const SymbolTable &symbol_table); + +class PlanHintsProvider final : public HierarchicalLogicalOperatorVisitor { + public: + explicit PlanHintsProvider(const SymbolTable &symbol_table) : symbol_table_(symbol_table) {} + + std::vector<std::string> &hints() { return hints_; } + + using HierarchicalLogicalOperatorVisitor::PostVisit; + using HierarchicalLogicalOperatorVisitor::PreVisit; + using HierarchicalLogicalOperatorVisitor::Visit; + + bool Visit(Once & /*unused*/) override { return true; } + + bool PreVisit(Filter & /*unused*/) override { return true; } + + bool PostVisit(Filter &op) override { + HintIndexUsage(op); + return true; + } + + bool PreVisit(ScanAll & /*unused*/) override { return true; } + + bool PostVisit(ScanAll & /*unused*/) override { return true; } + + bool PreVisit(Expand & /*unused*/) override { return true; } + + bool PostVisit(Expand & /*expand*/) override { return true; } + + bool PreVisit(ExpandVariable & /*unused*/) override { return true; } + + bool PostVisit(ExpandVariable & /*unused*/) override { return true; } + + bool PreVisit(Merge &op) override { + op.input()->Accept(*this); + op.merge_match_->Accept(*this); + return false; + } + + bool PostVisit(Merge & /*unused*/) override { return true; } + + bool PreVisit(Optional &op) override { + op.input()->Accept(*this); + op.optional_->Accept(*this); + return false; + } + + bool PostVisit(Optional & /*unused*/) override { return true; } + + bool PreVisit(Cartesian &op) override { + op.left_op_->Accept(*this); + op.right_op_->Accept(*this); + return false; + } + + bool PostVisit(Cartesian & /*unused*/) override { return true; } + + bool PreVisit(Union &op) override { + op.left_op_->Accept(*this); + op.right_op_->Accept(*this); + return false; + } + + bool PostVisit(Union & /*unused*/) override { return true; } + + bool PreVisit(CreateNode & /*unused*/) override { return true; } + bool PostVisit(CreateNode & /*unused*/) override { return true; } + + bool PreVisit(CreateExpand & /*unused*/) override { return true; } + bool PostVisit(CreateExpand & /*unused*/) override { return true; } + + bool PreVisit(ScanAllByLabel & /*unused*/) override { return true; } + bool PostVisit(ScanAllByLabel & /*unused*/) override { return true; } + + bool PreVisit(ScanAllByLabelPropertyRange & /*unused*/) override { return true; } + bool PostVisit(ScanAllByLabelPropertyRange & /*unused*/) override { return true; } + + bool PreVisit(ScanAllByLabelPropertyValue & /*unused*/) override { return true; } + bool PostVisit(ScanAllByLabelPropertyValue & /*unused*/) override { return true; } + + bool PreVisit(ScanAllByLabelProperty & /*unused*/) override { return true; } + bool PostVisit(ScanAllByLabelProperty & /*unused*/) override { return true; } + + bool PreVisit(ScanAllById & /*unused*/) override { return true; } + bool PostVisit(ScanAllById & /*unused*/) override { return true; } + + bool PreVisit(ConstructNamedPath & /*unused*/) override { return true; } + bool PostVisit(ConstructNamedPath & /*unused*/) override { return true; } + + bool PreVisit(Produce & /*unused*/) override { return true; } + bool PostVisit(Produce & /*unused*/) override { return true; } + + bool PreVisit(EmptyResult & /*unused*/) override { return true; } + bool PostVisit(EmptyResult & /*unused*/) override { return true; } + + bool PreVisit(Delete & /*unused*/) override { return true; } + bool PostVisit(Delete & /*unused*/) override { return true; } + + bool PreVisit(SetProperty & /*unused*/) override { return true; } + bool PostVisit(SetProperty & /*unused*/) override { return true; } + + bool PreVisit(SetProperties & /*unused*/) override { return true; } + bool PostVisit(SetProperties & /*unused*/) override { return true; } + + bool PreVisit(SetLabels & /*unused*/) override { return true; } + bool PostVisit(SetLabels & /*unused*/) override { return true; } + + bool PreVisit(RemoveProperty & /*unused*/) override { return true; } + bool PostVisit(RemoveProperty & /*unused*/) override { return true; } + + bool PreVisit(RemoveLabels & /*unused*/) override { return true; } + bool PostVisit(RemoveLabels & /*unused*/) override { return true; } + + bool PreVisit(EdgeUniquenessFilter & /*unused*/) override { return true; } + bool PostVisit(EdgeUniquenessFilter & /*unused*/) override { return true; } + + bool PreVisit(Accumulate & /*unused*/) override { return true; } + bool PostVisit(Accumulate & /*unused*/) override { return true; } + + bool PreVisit(Aggregate & /*unused*/) override { return true; } + bool PostVisit(Aggregate & /*unused*/) override { return true; } + + bool PreVisit(Skip & /*unused*/) override { return true; } + bool PostVisit(Skip & /*unused*/) override { return true; } + + bool PreVisit(Limit & /*unused*/) override { return true; } + bool PostVisit(Limit & /*unused*/) override { return true; } + + bool PreVisit(OrderBy & /*unused*/) override { return true; } + bool PostVisit(OrderBy & /*unused*/) override { return true; } + + bool PreVisit(Unwind & /*unused*/) override { return true; } + bool PostVisit(Unwind & /*unused*/) override { return true; } + + bool PreVisit(Distinct & /*unused*/) override { return true; } + bool PostVisit(Distinct & /*unused*/) override { return true; } + + bool PreVisit(CallProcedure & /*unused*/) override { return true; } + bool PostVisit(CallProcedure & /*unused*/) override { return true; } + + bool PreVisit(Foreach &op) override { + op.input()->Accept(*this); + op.update_clauses_->Accept(*this); + return false; + } + + bool PostVisit(Foreach & /*unused*/) override { return true; } + + bool PreVisit(EvaluatePatternFilter & /*unused*/) override { return true; } + + bool PostVisit(EvaluatePatternFilter & /*op*/) override { return true; } + + bool PreVisit(Apply &op) override { + op.input()->Accept(*this); + op.subquery_->Accept(*this); + return false; + } + + bool PostVisit(Apply & /*op*/) override { return true; } + + bool PreVisit(LoadCsv & /*unused*/) override { return true; } + + bool PostVisit(LoadCsv & /*op*/) override { return true; } + + private: + const SymbolTable &symbol_table_; + std::vector<std::string> hints_; + + bool DefaultPreVisit() override { LOG_FATAL("Operator not implemented for providing plan hints!"); } + + void HintIndexUsage(Filter &op) { + if (auto *maybe_scan_operator = dynamic_cast<ScanAll *>(op.input().get()); !maybe_scan_operator) { + return; + } + + auto const scan_symbol = dynamic_cast<ScanAll *>(op.input().get())->output_symbol_; + auto const scan_type = op.input()->GetTypeInfo(); + + Filters filters; + filters.CollectFilterExpression(op.expression_, symbol_table_); + const std::string filtered_labels = ExtractAndJoin(filters.FilteredLabels(scan_symbol), + [](const auto &item) { return fmt::format(":{0}", item.name); }); + const std::string filtered_properties = + ExtractAndJoin(filters.FilteredProperties(scan_symbol), [](const auto &item) { return item.name; }); + + if (filtered_labels.empty() && filtered_properties.empty()) { + return; + } + + if (scan_type == ScanAll::kType) { + if (!filtered_labels.empty() && !filtered_properties.empty()) { + hints_.push_back( + fmt::format("Sequential scan will be used on symbol `{0}` although there is a filter on labels {1} and " + "properties {2}. Consider " + "creating a label-property index.", + scan_symbol.name(), filtered_labels, filtered_properties)); + return; + } + + if (!filtered_labels.empty()) { + hints_.push_back(fmt::format( + "Sequential scan will be used on symbol `{0}` although there is a filter on labels {1}. Consider " + "creating a label index.", + scan_symbol.name(), filtered_labels)); + return; + } + return; + } + + if (scan_type == ScanAllByLabel::kType && !filtered_properties.empty()) { + hints_.push_back(fmt::format( + "Label index will be used on symbol `{0}` although there is also a filter on properties {1}. Consider " + "creating a label-property index.", + scan_symbol.name(), filtered_properties)); + return; + } + } + + std::string ExtractAndJoin(auto &&collection, auto &&projection) { + auto elements = collection | ranges::views::transform(projection); + return boost::algorithm::join(elements, ", "); + } +}; + +} // namespace memgraph::query::plan diff --git a/src/query/plan/preprocess.hpp b/src/query/plan/preprocess.hpp index 4f46cc0f0..f261545f7 100644 --- a/src/query/plan/preprocess.hpp +++ b/src/query/plan/preprocess.hpp @@ -345,6 +345,17 @@ class Filters final { return labels; } + auto FilteredProperties(const Symbol &symbol) const -> std::unordered_set<PropertyIx> { + std::unordered_set<PropertyIx> properties; + + for (const auto &filter : all_filters_) { + if (filter.type == FilterInfo::Type::Property && filter.property_filter->symbol_ == symbol) { + properties.insert(filter.property_filter->property_); + } + } + return properties; + } + /// Remove a filter; may invalidate iterators. /// Removal is done by comparing only the expression, so that multiple /// FilterInfo objects using the same original expression are removed. diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 5d0141be2..b0783b191 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -109,7 +109,7 @@ add_unit_test(query_plan_edge_cases.cpp ${CMAKE_SOURCE_DIR}/src/glue/communicati target_link_libraries(${test_prefix}query_plan_edge_cases mg-communication mg-query) add_unit_test(query_plan_match_filter_return.cpp) -target_link_libraries(${test_prefix}query_plan_match_filter_return mg-query mg-query mg-glue) +target_link_libraries(${test_prefix}query_plan_match_filter_return mg-query mg-glue) add_unit_test(query_plan_operator_to_string.cpp) target_link_libraries(${test_prefix}query_plan_operator_to_string mg-query) @@ -422,3 +422,5 @@ add_unit_test(distributed_lamport_clock.cpp) target_link_libraries(${test_prefix}distributed_lamport_clock mg-distributed) target_include_directories(${test_prefix}distributed_lamport_clock PRIVATE ${CMAKE_SOURCE_DIR}/include) +add_unit_test(query_hint_provider.cpp) +target_link_libraries(${test_prefix}query_hint_provider mg-query mg-glue) diff --git a/tests/unit/query_hint_provider.cpp b/tests/unit/query_hint_provider.cpp new file mode 100644 index 000000000..9973bf2ca --- /dev/null +++ b/tests/unit/query_hint_provider.cpp @@ -0,0 +1,166 @@ +// Copyright 2023 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#include <gtest/gtest.h> + +#include "query_plan_common.hpp" + +#include "query/db_accessor.hpp" +#include "query/frontend/semantic/symbol_table.hpp" +#include "query/plan/hint_provider.hpp" +#include "storage/v2/inmemory/storage.hpp" + +using namespace memgraph::query; +using namespace memgraph::query::plan; +using namespace memgraph::storage; + +class HintProviderSuite : public ::testing::Test { + protected: + std::unique_ptr<Storage> db = std::make_unique<InMemoryStorage>(); + std::optional<std::unique_ptr<Storage::Accessor>> storage_dba; + std::optional<memgraph::query::DbAccessor> dba; + LabelId label = db->NameToLabel("label"); + PropertyId property = db->NameToProperty("property"); + PropertyId other_property = db->NameToProperty("other_property"); + + std::vector<std::shared_ptr<LogicalOperator>> pattern_filters_{}; + + AstStorage storage; + SymbolTable symbol_table; + View view = View::OLD; + int symbol_count = 0; + + void SetUp() { + storage_dba.emplace(db->Access()); + dba.emplace(storage_dba->get()); + } + + Symbol NextSymbol() { return symbol_table.CreateSymbol("Symbol" + std::to_string(symbol_count++), true); } + + void VerifyHintMessages(LogicalOperator *plan, const std::vector<std::string> &expected_messages) { + auto messages = ProvidePlanHints(plan, symbol_table); + + ASSERT_EQ(expected_messages.size(), messages.size()); + + for (size_t i = 0; i < messages.size(); i++) { + const auto &expected_message = expected_messages[i]; + const auto &actual_message = messages[i]; + + ASSERT_EQ(expected_message, actual_message); + } + } + + std::vector<LabelIx> GetLabelIx(std::vector<LabelId> labels) { + std::vector<LabelIx> label_ixs{}; + for (const auto &label : labels) { + label_ixs.emplace_back(storage.GetLabelIx(db->LabelToName(label))); + } + + return label_ixs; + } +}; + +TEST_F(HintProviderSuite, HintWhenFilteringByLabel) { + auto scan_all = MakeScanAll(storage, symbol_table, "n"); + auto *filter_expr = storage.template Create<LabelsTest>(scan_all.node_->identifier_, GetLabelIx({label})); + + auto filter = std::make_shared<Filter>(scan_all.op_, pattern_filters_, filter_expr); + + const std::vector<std::string> expected_messages{ + "Sequential scan will be used on symbol `n` although there is a filter on labels :label. Consider " + "creating a label index."}; + + VerifyHintMessages(filter.get(), expected_messages); +} + +TEST_F(HintProviderSuite, DontHintWhenLabelOperatorPresent) { + auto scan_all_by_label = MakeScanAllByLabel(storage, symbol_table, "n", label); + auto produce = MakeProduce(scan_all_by_label.op_, nullptr); + + const std::vector<std::string> expected_messages{}; + + VerifyHintMessages(produce.get(), expected_messages); +} + +TEST_F(HintProviderSuite, HintWhenFilteringByLabelAndProperty) { + auto scan_all = MakeScanAll(storage, symbol_table, "n"); + auto *filter_expr = storage.template Create<AndOperator>( + storage.template Create<LabelsTest>(scan_all.node_->identifier_, GetLabelIx({label})), + EQ(PROPERTY_LOOKUP(*dba, scan_all.node_->identifier_, property), LITERAL(42))); + + auto filter = std::make_shared<Filter>(scan_all.op_, pattern_filters_, filter_expr); + + const std::vector<std::string> expected_messages{ + "Sequential scan will be used on symbol `n` although there is a filter on labels :label and properties property. " + "Consider " + "creating a label-property index."}; + + VerifyHintMessages(filter.get(), expected_messages); +} + +TEST_F(HintProviderSuite, DontHintWhenLabelPropertyOperatorPresent) { + auto scan_all_by_label_prop_value = MakeScanAllByLabelPropertyValue( + storage, symbol_table, "n", label, property, "property", storage.template Create<Identifier>("n")); + auto produce = MakeProduce(scan_all_by_label_prop_value.op_, nullptr); + + const std::vector<std::string> expected_messages{}; + + VerifyHintMessages(produce.get(), expected_messages); +} + +TEST_F(HintProviderSuite, DontHintWhenLabelPropertyOperatorPresentAndAdditionalPropertyFilterPresent) { + auto scan_all_by_label_prop_value = MakeScanAllByLabelPropertyValue( + storage, symbol_table, "n", label, property, "property", storage.template Create<Identifier>("n")); + + auto *filter_expr = + EQ(PROPERTY_LOOKUP(*dba, scan_all_by_label_prop_value.node_->identifier_, other_property), LITERAL(42)); + auto filter = std::make_shared<Filter>(scan_all_by_label_prop_value.op_, pattern_filters_, filter_expr); + const std::vector<std::string> expected_messages{}; + + VerifyHintMessages(filter.get(), expected_messages); +} + +TEST_F(HintProviderSuite, HintWhenLabelOperatorPresentButFilteringAlsoByProperty) { + auto scan_all_by_label = MakeScanAllByLabel(storage, symbol_table, "n", label); + auto *filter_expr = EQ(PROPERTY_LOOKUP(*dba, scan_all_by_label.node_->identifier_, property), LITERAL(42)); + + auto filter = std::make_shared<Filter>(scan_all_by_label.op_, pattern_filters_, filter_expr); + + const std::vector<std::string> expected_messages{ + "Label index will be used on symbol `n` although there is also a filter on properties property. " + "Consider " + "creating a label-property index."}; + + VerifyHintMessages(filter.get(), expected_messages); +} + +TEST_F(HintProviderSuite, DoubleHintWhenCartesianInFilters) { + auto first_scan_all = MakeScanAll(storage, symbol_table, "n"); + auto *first_filter_expr = storage.template Create<LabelsTest>(first_scan_all.node_->identifier_, GetLabelIx({label})); + auto first_filter = std::make_shared<Filter>(first_scan_all.op_, pattern_filters_, first_filter_expr); + + auto second_scan_all = MakeScanAll(storage, symbol_table, "m"); + auto *second_filter_expr = + storage.template Create<LabelsTest>(second_scan_all.node_->identifier_, GetLabelIx({label})); + auto second_filter = std::make_shared<Filter>(second_scan_all.op_, pattern_filters_, second_filter_expr); + + const std::vector<Symbol> empty_symbols{}; + + auto cartesian = std::make_shared<Cartesian>(first_filter, empty_symbols, second_filter, empty_symbols); + + const std::vector<std::string> expected_messages{ + "Sequential scan will be used on symbol `n` although there is a filter on labels :label. Consider " + "creating a label index.", + "Sequential scan will be used on symbol `m` although there is a filter on labels :label. Consider " + "creating a label index."}; + + VerifyHintMessages(cartesian.get(), expected_messages); +}