From db059f99b0df11143e96b0c33e0829388d7c98c3 Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Fri, 15 Mar 2024 18:40:38 -0500 Subject: [PATCH] =?UTF-8?q?Strategies=20to=20improve=20query=20performance?= =?UTF-8?q?:=20operations,=20number=20and=20size=20=E2=80=A6=20(#5215)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Strategies to improve query performance: operations, number and size of parquet files Fixes #5108 - Add query performance strategies to optimize-queries * Apply suggestions from code review * Apply suggestions from code review * Update content/influxdb/cloud-dedicated/query-data/execute-queries/optimize-queries.md Co-authored-by: Andrew Lamb * chore(v3): WIP query perf Fixes #5108 * chore(v3): WIP: Query performance * chore(v3): WIP: Explain the EXPLAIN report and indicators of query expense, performance problems. * WIP: optimize queries - how to read a query plan, operators * WIP: Read a query plan example 2 * WIP: moved how to read a query plan to its own page. * WIP(v3): operators * chore(v3): WIP add query plan info from DataFusion slides and @NGA-TRAN * chore(v3): WIP read a query plan - explain tree format and reorganize * WIP: query plan - Adds Query Plan reference - Completes Analyze a Query Plan, pending cleanup, continue at :471 - Added image from Nga's blog - Updates EXPLAIN doc - TODO: Create public docs for https://github.com/influxdata/docs.influxdata.io/blob/main/content/operations/specifications/iox_runbooks/slow-queries.md * chore(spelling): Vale config changes - Add vale to package.json and use Yarn to manage the binary. You can use `npx vale` to run manually. - Move InfluxData spelling ignore list into the style. - Reorganize custom (product) spelling lists to comply with Vale 3.x - Add InfluxDB v3 terms * chore(spelling): Vale config changes - Add vale to package.json and use Yarn to manage the binary. You can use `npx vale` to run manually. - Move InfluxData spelling ignore list into the style. - Reorganize custom (product) spelling lists to comply with Vale 3.x - Add InfluxDB v3 terms * chore(v3): Reorg of query troubleshooting and optimizing docs - Adds query-data/troubleshoot-and-optimize - Splits optimize docs into troubleshoot and optimize docs - Moves Flight response doc to flight-responses.md * chore: Fixes broken links, typos, missing content, etc. - Fixes various errors and style violations reported by Vale. - Fixes broken links and missing content in glossaries. - Fixes missing and extraneous whitespace. * Apply suggestions from code review * Update content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/analyze-query-plan.md * chore(clustered): Query plan: Apply review suggestions Co-Authored-By: Nga Tran <20850014+NGA-TRAN@users.noreply.github.com> * feature(v3): Analyze a query plan: - Apply code formatting to plan implementor names - Simplify some points - Add links * add query plan html diagram (#5365) * Update content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/analyze-query-plan.md Co-authored-by: Scott Anderson * Update content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/optimize-queries.md Co-authored-by: Scott Anderson * Update content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/trace.md Co-authored-by: Scott Anderson * Update content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/trace.md Co-authored-by: Scott Anderson * Update content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/trace.md Co-authored-by: Scott Anderson * Update content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/analyze-query-plan.md Co-authored-by: Scott Anderson * fix(v3): finish the EXPLAIN descriptions and examples * chore(tests): Setup a python venv in test containers * fix(ci): Vale vocab * fix(v3): Punctuation typo * chore(ci): Update README * fix(v3): Apply review suggestions and capitalization Co-authored-by: Scott Anderson * fix(v3): Add note to optimize page and revise troubleshoot * fix(v3): optimize-queries link --------- Co-authored-by: Andrew Lamb Co-authored-by: Nga Tran <20850014+NGA-TRAN@users.noreply.github.com> Co-authored-by: Scott Anderson --- .ci/vale/styles/InfluxDataDocs/Branding.yml | 37 +- .ci/vale/styles/InfluxDataDocs/Spelling.yml | 27 +- .../styles/InfluxDataDocs/Terms/influxdb.txt | 126 +++ .../InfluxDataDocs/Terms/query-functions.txt | 22 + .../styles/InfluxDataDocs/Terms/telegraf.txt | 1 + .ci/vale/styles/InfluxDataDocs/WordList.yml | 2 +- .ci/vale/styles/Vocab/InfluxData/accept.txt | 96 --- .ci/vale/styles/Vocab/InfluxData/reject.txt | 1 - .../vocabularies}/Cloud-Dedicated/accept.txt | 0 .../vocabularies}/Cloud-Dedicated/reject.txt | 0 .../vocabularies}/Cloud-Serverless/accept.txt | 0 .../vocabularies}/Cloud-Serverless/reject.txt | 0 .../config/vocabularies/Clustered/accept.txt | 1 + .../config/vocabularies/Clustered/reject.txt | 6 + .ci/vale/vale.sh | 7 +- .vale.ini | 2 - CONTRIBUTING.md | 102 ++- assets/styles/layouts/article/_captions.scss | 4 + .../layouts/article/_html-diagrams.scss | 76 ++ content/influxdb/cloud-dedicated/.vale.ini | 2 +- .../execute-queries/optimize-queries.md | 442 ---------- .../troubleshoot-and-optimize/_index.md | 23 + .../analyze-query-plan.md | 771 ++++++++++++++++++ .../flight-responses.md} | 33 +- .../optimize-queries.md | 64 ++ .../system-information.md | 108 +++ .../troubleshoot-and-optimize/trace.md | 247 ++++++ .../troubleshoot-and-optimize/troubleshoot.md | 44 + .../reference/client-libraries/v2/arduino.md | 4 +- .../reference/client-libraries/v2/kotlin.md | 4 +- .../cloud-dedicated/reference/glossary.md | 64 +- .../reference/internals/query-plan.md | 392 +++++++++ .../cloud-dedicated/reference/sql/explain.md | 89 +- .../best-practices/schema-design.md | 15 +- content/influxdb/cloud-serverless/.vale.ini | 2 +- .../query-data/optimize-queries.md | 105 --- .../troubleshoot-and-optimize/_index.md | 23 + .../analyze-query-plan.md | 769 +++++++++++++++++ .../flight-responses.md} | 50 +- .../optimize-queries.md | 69 ++ .../troubleshoot-and-optimize/troubleshoot.md | 44 + .../reference/client-libraries/v2/arduino.md | 4 +- .../reference/client-libraries/v2/kotlin.md | 4 +- .../cloud-serverless/reference/glossary.md | 70 +- .../reference/internals/durability.md | 6 +- .../reference/internals/query-plan.md | 392 +++++++++ .../cloud-serverless/reference/sql/explain.md | 89 +- content/influxdb/clustered/.vale.ini | 2 +- .../clustered/install/configure-cluster.md | 2 +- .../clustered/install/prerequisites.md | 2 +- .../query-data/execute-queries/_index.md | 2 +- .../execute-queries/optimize-queries.md | 115 --- .../query-data/influxql/explore-schema.md | 24 +- .../troubleshoot-and-optimize/_index.md | 23 + .../analyze-query-plan.md | 771 ++++++++++++++++++ .../flight-responses.md} | 18 +- .../optimize-queries.md | 62 ++ .../troubleshoot-and-optimize/troubleshoot.md | 44 + .../reference/client-libraries/v2/arduino.md | 4 +- .../v2/javascript/nodejs/write.md | 2 +- .../reference/client-libraries/v2/kotlin.md | 4 +- .../influxdb/clustered/reference/glossary.md | 81 +- .../reference/internals/query-plan.md | 392 +++++++++ .../clustered/reference/sql/_index.md | 135 +-- .../clustered/reference/sql/explain.md | 89 +- .../reference/sql/information-schema.md | 4 +- content/influxdb/v1/concepts/glossary.md | 7 +- .../v2/api-guide/client-libraries/arduino.md | 4 +- .../v2/api-guide/client-libraries/kotlin.md | 4 +- content/influxdb/v2/reference/glossary.md | 26 +- .../shortcodes/html-diagram/query-plan.html | 34 + package.json | 1 + static/img/influxdb/3-0-query-plan-tree.png | Bin 0 -> 350714 bytes test.Dockerfile | 67 +- test.sh | 2 +- test/README.md | 4 + test/requirements.txt | 2 +- test/run-tests.sh | 8 +- 78 files changed, 5206 insertions(+), 1168 deletions(-) create mode 100644 .ci/vale/styles/InfluxDataDocs/Terms/influxdb.txt create mode 100644 .ci/vale/styles/InfluxDataDocs/Terms/telegraf.txt delete mode 100644 .ci/vale/styles/Vocab/InfluxData/accept.txt delete mode 100644 .ci/vale/styles/Vocab/InfluxData/reject.txt rename .ci/vale/styles/{Vocab => config/vocabularies}/Cloud-Dedicated/accept.txt (100%) rename .ci/vale/styles/{Vocab => config/vocabularies}/Cloud-Dedicated/reject.txt (100%) rename .ci/vale/styles/{Vocab => config/vocabularies}/Cloud-Serverless/accept.txt (100%) rename .ci/vale/styles/{Vocab => config/vocabularies}/Cloud-Serverless/reject.txt (100%) create mode 100644 .ci/vale/styles/config/vocabularies/Clustered/accept.txt create mode 100644 .ci/vale/styles/config/vocabularies/Clustered/reject.txt delete mode 100644 content/influxdb/cloud-dedicated/query-data/execute-queries/optimize-queries.md create mode 100644 content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/_index.md create mode 100644 content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/analyze-query-plan.md rename content/influxdb/cloud-dedicated/query-data/{execute-queries/troubleshoot.md => troubleshoot-and-optimize/flight-responses.md} (93%) create mode 100644 content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/optimize-queries.md create mode 100644 content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/system-information.md create mode 100644 content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/trace.md create mode 100644 content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/troubleshoot.md create mode 100644 content/influxdb/cloud-dedicated/reference/internals/query-plan.md delete mode 100644 content/influxdb/cloud-serverless/query-data/optimize-queries.md create mode 100644 content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/_index.md create mode 100644 content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/analyze-query-plan.md rename content/influxdb/cloud-serverless/query-data/{execute-queries/troubleshoot.md => troubleshoot-and-optimize/flight-responses.md} (92%) create mode 100644 content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/optimize-queries.md create mode 100644 content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/troubleshoot.md create mode 100644 content/influxdb/cloud-serverless/reference/internals/query-plan.md delete mode 100644 content/influxdb/clustered/query-data/execute-queries/optimize-queries.md create mode 100644 content/influxdb/clustered/query-data/troubleshoot-and-optimize/_index.md create mode 100644 content/influxdb/clustered/query-data/troubleshoot-and-optimize/analyze-query-plan.md rename content/influxdb/clustered/query-data/{execute-queries/troubleshoot.md => troubleshoot-and-optimize/flight-responses.md} (95%) create mode 100644 content/influxdb/clustered/query-data/troubleshoot-and-optimize/optimize-queries.md create mode 100644 content/influxdb/clustered/query-data/troubleshoot-and-optimize/troubleshoot.md create mode 100644 content/influxdb/clustered/reference/internals/query-plan.md create mode 100644 layouts/shortcodes/html-diagram/query-plan.html create mode 100644 static/img/influxdb/3-0-query-plan-tree.png diff --git a/.ci/vale/styles/InfluxDataDocs/Branding.yml b/.ci/vale/styles/InfluxDataDocs/Branding.yml index c55ff4634..012620353 100644 --- a/.ci/vale/styles/InfluxDataDocs/Branding.yml +++ b/.ci/vale/styles/InfluxDataDocs/Branding.yml @@ -6,7 +6,40 @@ ignorecase: true swap: # NOTE: The left-hand (bad) side can match the right-hand (good) side; Vale # will ignore any alerts that match the intended form. - "FlightSQL": Flight SQL + "anaconda": Anaconda + "(?i)api": API + "arrow": Arrow + "authtoken": authToken + "Authtoken": AuthToken + "chronograf": Chronograf + "cli": CLI + "(?i)clockface": Clockface + "the\b[?compactor": the Compactor + "data explorer": Data Explorer + "datetime": dateTime + "dedupe": deduplicate + "(?i)executionplan": ExecutionPlan + "fieldkey": fieldKey + "fieldtype": fieldType + "flight": Flight + "(?i)flightquery": FlightQuery + "(?i)FlightSQL": Flight SQL + "/b(?i)influxdata/b": InfluxData + "/w*/b(?i)influxdb": InfluxDB + "(?i)influxql": InfluxQL + "influxer": Influxer + "the\b[?ingester": the Ingester + "(?i)iox": v3 "java[ -]?scripts?": JavaScript - "SQL Alchemy": SQLAlchemy + "kapa": Kapacitor + "logicalplan": LogicalPlan + "the\b[?object store": the Object store "a {{% product-name %}}": an {{% product-name %}} + "Pandas": pandas + " parquet": Parquet + "the\b[?querier": the Querier + "SQL Alchemy": SQLAlchemy + "superset": Superset + "tagkey": tagKey + "telegraf": Telegraf + "telegraph": Telegraf diff --git a/.ci/vale/styles/InfluxDataDocs/Spelling.yml b/.ci/vale/styles/InfluxDataDocs/Spelling.yml index e34b0aabe..d7bb17a89 100644 --- a/.ci/vale/styles/InfluxDataDocs/Spelling.yml +++ b/.ci/vale/styles/InfluxDataDocs/Spelling.yml @@ -6,6 +6,31 @@ scope: - ~table.cell ignore: # Located at StylesPath/ignore1.txt + - InfluxDataDocs/Terms/influxdb.txt - InfluxDataDocs/Terms/configuration-terms.txt - InfluxDataDocs/Terms/query-functions.txt - + - InfluxDataDocs/Terms/telegraf.txt +filters: + # Ignore Hugo, layout, and design words. + - 'Flexbox' + - '(?i)frontmatter' + - '(?i)shortcode(s?)' + - '(?i)tooltip(s?)' + # Ignore all words starting with 'py'. + # e.g., 'PyYAML'. + - '[pP]y.*\b' + # Ignore underscore-delimited words. + # e.g., avg_temp + - '\b\w+_\w+\b' + - '\b_\w+\b' + # Ignore SQL variables. + - '(?i)AS \w+' + # Ignore custom words + - '(?i)deduplicat(ion|e|ed|es|ing)' + - '(?i)downsampl(e|ing|ed|es)' + - 'InfluxDB-specific' + - '(?i)repartition(ed|s|ing)' + - '(?i)subcommand(s?)' + - '(?i)union(ing|ed|s)?' + - 'unsignedLong' + - 'US (East|West|Central|North|South|Northeast|Northwest|Southeast|Southwest)' diff --git a/.ci/vale/styles/InfluxDataDocs/Terms/influxdb.txt b/.ci/vale/styles/InfluxDataDocs/Terms/influxdb.txt new file mode 100644 index 000000000..feaac0799 --- /dev/null +++ b/.ci/vale/styles/InfluxDataDocs/Terms/influxdb.txt @@ -0,0 +1,126 @@ +api +apis +args +authtoken +authz +boolean +booleans +bundler +bundlers +chronograf +cli +clockface +cloud +codeblock +compactor +conda +csv +dashboarding +datagram +datasource +datetime +deserialize +downsample +dotenv +enum +executionplan +fieldkey +fieldtype +file_groups +flighquery +Grafana +groupId +gzip +gzipped +homogenous +hostname +hostUrl +hostURL +HostURL +implementor +implementors +influxctl +influxd +influxdata.com +influx3 +ingester +ingesters +iox +kapacitor +lat +locf +logicalplan +logstash +lon +lookahead +lookbehind +metaquery +metaqueries +middleware +namespace +noaa +npm +oauth +output_ordering +pandas +param +performant +projection +protofiles +pushdown +querier +rearchitect +rearchitected +redoc +remediations +repartition +retention_policy +retryable +rp +serializable +serializer +serverless +shortcode +signout +Splunk +SQLAlchemy +stderr +stdout +subcommand +subcommands +subnet +subnets +subprocessor +subprocessors +subquery +subqueries +substring +substrings +superset +svg +syntaxes +tagkey +tagset +telegraf +telegraf's +tombstoned +tsm +uint +uinteger +unescaped +ungroup +ungrouped +unprocessable +unix +unmarshal +unmarshalled +unpackage +upsample +upsert +urls +venv +VSCode +WALs +Webpack +xpath +XPath diff --git a/.ci/vale/styles/InfluxDataDocs/Terms/query-functions.txt b/.ci/vale/styles/InfluxDataDocs/Terms/query-functions.txt index b47e8c5ff..e74c2be31 100644 --- a/.ci/vale/styles/InfluxDataDocs/Terms/query-functions.txt +++ b/.ci/vale/styles/InfluxDataDocs/Terms/query-functions.txt @@ -21,6 +21,7 @@ begin between bit bit_length +bitwise both by cascade @@ -314,6 +315,10 @@ to_timestamp_micros to_timestamp_millis to_timestamp_seconds +# SQL_DATE_TIME_KEYWORDS +dow +doy + # SQL_INFO_SYSTEM_FUNCTIONS # Source: https://github.com/influxdata/influxdb_iox/blob/4f9c901dcfece5fcc4d17cfecb6ec45a0dccda5a/flightsql/src/sql_info array @@ -328,21 +333,38 @@ cos tan asin acos +acosh +asinh atan +atanh atan2 +cbrt exp +gcd +isnan +iszero +lcm log ln log2 log10 +nanvl sqrt pow floor ceil round +# InfluxQL operators +bitfield + # is_aggregate_function # Source: https://github.com/influxdata/influxdb_iox/blob/4f9c901dcfece5fcc4d17cfecb6ec45a0dccda5a/influxdb_influxql_parser/src/functions.rs +approx_distinct +approx_median +approx_percentile_cont +approx_percentile_cont_with_weight +covar cumulative_sum derivative difference diff --git a/.ci/vale/styles/InfluxDataDocs/Terms/telegraf.txt b/.ci/vale/styles/InfluxDataDocs/Terms/telegraf.txt new file mode 100644 index 000000000..00140d688 --- /dev/null +++ b/.ci/vale/styles/InfluxDataDocs/Terms/telegraf.txt @@ -0,0 +1 @@ +[Tt]elegraf diff --git a/.ci/vale/styles/InfluxDataDocs/WordList.yml b/.ci/vale/styles/InfluxDataDocs/WordList.yml index aaa64b220..5d1402467 100644 --- a/.ci/vale/styles/InfluxDataDocs/WordList.yml +++ b/.ci/vale/styles/InfluxDataDocs/WordList.yml @@ -41,7 +41,7 @@ swap: cellular network: mobile network chapter: documents|pages|sections check box: checkbox - check: select + # check: select # CLI: command-line tool click on: click|click in # Cloud: Google Cloud Platform|GCP diff --git a/.ci/vale/styles/Vocab/InfluxData/accept.txt b/.ci/vale/styles/Vocab/InfluxData/accept.txt deleted file mode 100644 index f9629c742..000000000 --- a/.ci/vale/styles/Vocab/InfluxData/accept.txt +++ /dev/null @@ -1,96 +0,0 @@ -\b.*_.*\b -Anaconda -APIs? -Arrow -authToken -AuthToken -[Bb]oolean -bundlers? -[Cc]hronograf -CLI -Clockface -[Cc]loud -codeblock -conda -csv -CSV -Data Explorer -dashboarding -datasource -dateTime -deserialize -[Dd]ownsampl.*\b -dotenv -enum -Flight -FlightQuery -Grafana -groupId -gzip(ped)? -homogenous -hostname -hostUrl -hostURL -HostURL -influxctl -[Ii]nflux[Dd]ata -influxdb? -InfluxDB -influxql -InfluxQL -influx3 -iox -IOx -Kapacitor -lat -locf -[Ll]ogstash -lon -lookahead -lookbehind -middleware -namespace -noaa -NOAA -npm -OAuth -pandas -performant -pushdown -pyarrow -Py.*\b -pyinflux -rearchitect(ed)? -Redoc -retention_policy -rp -serializable -serializer -[Ss]erverless -shortcode -Splunk -SQLAlchemy -stdout -subnet -subquer(y|ies) -substring -Superset -svg -tagset -[Tt]elegraf -[Tt]ombstoned -tsm|TSM -uint|UINT -uinteger -unescaped -unprocessable -unix -upsample -upsert -urls -venv -VSCode -WALs? -Webpack -xpath -XPath diff --git a/.ci/vale/styles/Vocab/InfluxData/reject.txt b/.ci/vale/styles/Vocab/InfluxData/reject.txt deleted file mode 100644 index f4052edd7..000000000 --- a/.ci/vale/styles/Vocab/InfluxData/reject.txt +++ /dev/null @@ -1 +0,0 @@ -Pandas \ No newline at end of file diff --git a/.ci/vale/styles/Vocab/Cloud-Dedicated/accept.txt b/.ci/vale/styles/config/vocabularies/Cloud-Dedicated/accept.txt similarity index 100% rename from .ci/vale/styles/Vocab/Cloud-Dedicated/accept.txt rename to .ci/vale/styles/config/vocabularies/Cloud-Dedicated/accept.txt diff --git a/.ci/vale/styles/Vocab/Cloud-Dedicated/reject.txt b/.ci/vale/styles/config/vocabularies/Cloud-Dedicated/reject.txt similarity index 100% rename from .ci/vale/styles/Vocab/Cloud-Dedicated/reject.txt rename to .ci/vale/styles/config/vocabularies/Cloud-Dedicated/reject.txt diff --git a/.ci/vale/styles/Vocab/Cloud-Serverless/accept.txt b/.ci/vale/styles/config/vocabularies/Cloud-Serverless/accept.txt similarity index 100% rename from .ci/vale/styles/Vocab/Cloud-Serverless/accept.txt rename to .ci/vale/styles/config/vocabularies/Cloud-Serverless/accept.txt diff --git a/.ci/vale/styles/Vocab/Cloud-Serverless/reject.txt b/.ci/vale/styles/config/vocabularies/Cloud-Serverless/reject.txt similarity index 100% rename from .ci/vale/styles/Vocab/Cloud-Serverless/reject.txt rename to .ci/vale/styles/config/vocabularies/Cloud-Serverless/reject.txt diff --git a/.ci/vale/styles/config/vocabularies/Clustered/accept.txt b/.ci/vale/styles/config/vocabularies/Clustered/accept.txt new file mode 100644 index 000000000..0a3224ec9 --- /dev/null +++ b/.ci/vale/styles/config/vocabularies/Clustered/accept.txt @@ -0,0 +1 @@ +clustered diff --git a/.ci/vale/styles/config/vocabularies/Clustered/reject.txt b/.ci/vale/styles/config/vocabularies/Clustered/reject.txt new file mode 100644 index 000000000..5bbfb6575 --- /dev/null +++ b/.ci/vale/styles/config/vocabularies/Clustered/reject.txt @@ -0,0 +1,6 @@ +API token +bucket name +Cloud Dedicated +cloud-dedicated +Cloud Serverless +cloud-serverless diff --git a/.ci/vale/vale.sh b/.ci/vale/vale.sh index 5d98f5111..88578555a 100644 --- a/.ci/vale/vale.sh +++ b/.ci/vale/vale.sh @@ -1,10 +1,13 @@ # Lint cloud-dedicated docspath=. contentpath=$docspath/content -vale --config=$contentpath/influxdb/cloud-dedicated/.vale.ini --output=line --relative --minAlertLevel=error $contentpath/influxdb/cloud-dedicated +npx vale --config=$contentpath/influxdb/cloud-dedicated/.vale.ini --output=line --relative --minAlertLevel=error $contentpath/influxdb/cloud-dedicated # Lint cloud-serverless -vale --config=$contentpath/influxdb/cloud-serverless/.vale.ini --output=line --relative --minAlertLevel=error $contentpath/influxdb/cloud-serverless +npx vale --config=$contentpath/influxdb/cloud-serverless/.vale.ini --output=line --relative --minAlertLevel=error $contentpath/influxdb/cloud-serverless + +# Lint clustered +npx vale --config=$contentpath/influxdb/clustered/.vale.ini --output=line --relative --minAlertLevel=error $contentpath/influxdb/clustered # Lint telegraf # vale --config=$docspath/.vale.ini --output=line --relative --minAlertLevel=error $contentpath/telegraf \ No newline at end of file diff --git a/.vale.ini b/.vale.ini index 8b7c068a6..5b3a9322b 100644 --- a/.vale.ini +++ b/.vale.ini @@ -1,7 +1,5 @@ StylesPath = ".ci/vale/styles" -Vocab = InfluxData - MinAlertLevel = warning Packages = Google, Hugo diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e600dbcec..c40c52b05 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ What constitutes a "substantial" change is at the discretion of InfluxData docum _**Note:** Typo and broken link fixes are greatly appreciated and do not require signing the CLA._ -*If you're new to contributing or you're looking for an easy update, check out our [good-first-issues](https://github.com/influxdata/docs-v2/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue).* +*If you're new to contributing or you're looking for an easy update, see [`docs-v2` good-first-issues](https://github.com/influxdata/docs-v2/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue).* ## Make suggested updates @@ -22,16 +22,39 @@ _**Note:** Typo and broken link fixes are greatly appreciated and do not require To run the documentation locally, follow the instructions provided in the README. ### Install and run Vale -Use the [Vale](https://vale.sh/) style linter to check spelling and enforce style guidelines. -To install Vale, follow the instructions to install the [Vale CLI](https://vale.sh/docs/vale-cli/installation/) for your system and the [integration](https://vale.sh/docs/integrations/guide/) for your editor. -The `docs-v2` repository contains `.vale.ini` files that configure InfluxData spelling and style rules used by the [Vale CLI](https://vale.sh/docs/vale-cli/installation/) and editor extensions, such as [Vale VSCode](https://marketplace.visualstudio.com/items?itemName=ChrisChinchilla.vale-vscode). -When run (with the CLI or an editor extension) Vale searches for a `.vale.ini` file in the directory of the file being linted. +Use the [Vale](https://vale.sh/) style linter for spellchecking and enforcing style guidelines. +The docs-v2 `package.json` includes a Vale dependency that installs the Vale binary when you run `yarn`. +After you use `yarn` to install Vale, you can run `npx vale` to execute Vale commands. -To lint multiple directories with specified configuration files and generate a report, run the `.ci/vale/vale.sh` script. +_To install Vale globally or use a different package manager, follow the [Vale CLI installation](https://vale.sh/docs/vale-cli/installation/) for your system._ + +#### Integrate with your editor + +To integrate Vale with VSCode: + +1. Install the [Vale VSCode](https://marketplace.visualstudio.com/items?itemName=ChrisChinchilla.vale-vscode) extension. +2. In the extension settings, set the `Vale:Vale CLI:Path` value to the path of your Vale binary. +Use the path `${workspaceFolder}/node_modules/.bin/vale` for the Vale binary that you installed with Yarn. + +To use with an editor other than VSCode, see the [Vale integration guide](https://vale.sh/docs/integrations/guide/). + +#### Lint product directories + +The `docs-v2` repository includes a shell script that lints product directories using the `InfluxDataDocs` style rules and product-specific vocabularies, and then generates a report. +To run the script, enter the following command in your terminal: + +```sh +sh .ci/vale/vale.sh +``` + +#### Configure style rules + +The `docs-v2` repository contains `.vale.ini` files that configure a custom `InfluxDataDocs` style with spelling and style rules. +When you run `vale ` (from the CLI or an editor extension), it searches for a `.vale.ini` file in the directory of the file being linted. `docs-v2` style rules are located at `.ci/vale/styles/`. -The easiest way to add accepted or rejected spellings is to enter your terms (or regular expression patterns) into the Vocabulary files at `.ci/vale/styles/Vocab`. +The easiest way to add accepted or rejected spellings is to enter your terms (or regular expression patterns) into the Vocabulary files at `.ci/vale/styles/config/vocabularies`. To learn more about configuration and rules, see [Vale configuration](https://vale.sh/docs/topics/config). @@ -46,17 +69,17 @@ Push your changes up to your forked repository, then [create a new pull request] ### Markdown All of our documentation is written in [Markdown](https://en.wikipedia.org/wiki/Markdown). -### Semantic Linefeeds -Use [semantic linefeeds](http://rhodesmill.org/brandon/2012/one-sentence-per-line/). +### Semantic line feeds +Use [semantic line feeds](http://rhodesmill.org/brandon/2012/one-sentence-per-line/). Separating each sentence with a new line makes it easy to parse diffs with the human eye. -**Diff without semantic linefeeds:** +**Diff without semantic line feeds:** ``` diff -Data is taking off. This data is time series. You need a database that specializes in time series. You should check out InfluxDB. +Data is taking off. This data is time series. You need a database that specializes in time series. You need InfluxDB. ``` -**Diff with semantic linefeeds:** +**Diff with semantic line feeds:** ``` diff Data is taking off. This data is time series. @@ -397,8 +420,7 @@ Provide the following arguments: ``` ### Tabbed Content -Shortcodes are available for creating "tabbed" content (content that is changed by a users' selection). -Ther following three must be used: +To create "tabbed" content (content that is changed by a users' selection), use the following three shortcodes in combination: `{{< tabs-wrapper >}}` This shortcode creates a wrapper or container for the tabbed content. @@ -774,25 +796,25 @@ This is useful for maintaining and referencing sample code variants in their /shared/text/example1/example.py ``` -2. Include the files, e.g. in code tabs -````md - {{% code-tabs-wrapper %}} - {{% code-tabs %}} - [Javascript](#js) - [Python](#py) - {{% /code-tabs %}} - {{% code-tab-content %}} - ```js - {{< get-shared-text "example1/example.js" >}} - ``` - {{% /code-tab-content %}} - {{% code-tab-content %}} - ```py - {{< get-shared-text "example1/example.py" >}} - ``` - {{% /code-tab-content %}} - {{% /code-tabs-wrapper %}} -```` +2. Include the files--for example, in code tabs: + ````md + {{% code-tabs-wrapper %}} + {{% code-tabs %}} + [Javascript](#js) + [Python](#py) + {{% /code-tabs %}} + {{% code-tab-content %}} + ```js + {{< get-shared-text "example1/example.js" >}} + ``` + {{% /code-tab-content %}} + {{% code-tab-content %}} + ```py + {{< get-shared-text "example1/example.py" >}} + ``` + {{% /code-tab-content %}} + {{% /code-tabs-wrapper %}} + ```` #### Include specific files from the same directory To include the text from one file in another file in the same @@ -861,8 +883,8 @@ The following table shows which children types use which frontmatter properties: ### Inline icons The `icon` shortcode allows you to inject icons in paragraph text. It's meant to clarify references to specific elements in the InfluxDB user interface. -This shortcode supports clockface (the UI) v2 and v3. -Specify the version to use as the 2nd argument. The default version is `v3`. +This shortcode supports Clockface (the UI) v2 and v3. +Specify the version to use as the second argument. The default version is `v3`. ``` {{< icon "icon-name" "v2" >}} @@ -935,8 +957,8 @@ Below is a list of available icons (some are aliases): ### InfluxDB UI left navigation icons In many cases, documentation references an item in the left nav of the InfluxDB UI. Provide a visual example of the navigation item using the `nav-icon` shortcode. -This shortcode supports clockface (the UI) v2 and v3. -Specify the version to use as the 2nd argument. The default version is `v3`. +This shortcode supports Clockface (the UI) v2 and v3. +Specify the version to use as the second argument. The default version is `v3`. ``` {{< nav-icon "tasks" "v2" >}} @@ -988,15 +1010,15 @@ The following options are available: - quarter ### Tooltips -Use the `{{< tooltips >}}` shortcode to add tooltips to text. -The **1st** argument is the text shown in the tooltip. -The **2nd** argument is the highlighted text that triggers the tooltip. +Use the `{{< tooltip >}}` shortcode to add tooltips to text. +The **first** argument is the text shown in the tooltip. +The **second** argument is the highlighted text that triggers the tooltip. ```md I like {{< tooltip "Butterflies are awesome!" "butterflies" >}}. ``` -The example above renders as "I like butterflies" with "butterflies" highlighted. +The rendered output is "I like butterflies" with "butterflies" highlighted. When you hover over "butterflies," a tooltip appears with the text: "Butterflies are awesome!" ### Flux sample data tables diff --git a/assets/styles/layouts/article/_captions.scss b/assets/styles/layouts/article/_captions.scss index 420525b02..20dfc3976 100644 --- a/assets/styles/layouts/article/_captions.scss +++ b/assets/styles/layouts/article/_captions.scss @@ -26,3 +26,7 @@ h2,h3,h4,h5,h6 { opacity: 1; } } + +#query-plan-diagram + .caption { + margin-top: 0; +} diff --git a/assets/styles/layouts/article/_html-diagrams.scss b/assets/styles/layouts/article/_html-diagrams.scss index 3124fa929..1e12d912b 100644 --- a/assets/styles/layouts/article/_html-diagrams.scss +++ b/assets/styles/layouts/article/_html-diagrams.scss @@ -457,6 +457,82 @@ table tr.point{ } } +////////////////////////////// QUERY PLAN DIAGRAM ////////////////////////////// + +#query-plan-diagram { + display: flex; + flex-direction: column; + font-size: 1rem; + margin: 3rem 0 3.5rem; + max-width: 800px; + + .plan-column { + padding: 0 .5rem; + } + + .plan-block { + background: $article-code-bg; + color: $article-code; + text-align: center; + padding: 1rem 1.5rem; + border-radius: $radius * 2; + } + .plan-arrow { + margin: .5rem auto; + height: 1.5rem; + width: 1px; + border-left: 1px solid $article-code; + position: relative; + + &:before { + content: "\25B2"; + position: absolute; + top: .25rem; + left: -.32rem; + color: $article-code; + line-height: 0; + } + &.split { + width: 50%; + margin-top: 2rem; + height: 1rem; + display: flex; + justify-content: center; + border-width: 1px 1px 0 1px; + border-style: solid; + border-color: $article-code; + + &:before { + position: relative; + top: -1.25rem; + left: -0.26rem; + width: 0; + margin-left: .2rem; + } + &:after { + content: ""; + display: block; + height: 1rem; + width: 0; + border-left: 1px solid $article-code; + margin: -1rem 0; + } + } + } + .plan-single-column { + display: flex; + justify-content: center; + } + .plan-double-column { + display: flex; + justify-content: space-around; + + .plan-column { + // width: 50%; + } + } +} + //////////////////////////////////////////////////////////////////////////////// ///////////////////////////////// MEDIA QUERIES //////////////////////////////// //////////////////////////////////////////////////////////////////////////////// diff --git a/content/influxdb/cloud-dedicated/.vale.ini b/content/influxdb/cloud-dedicated/.vale.ini index 51d9de472..2814e61f3 100644 --- a/content/influxdb/cloud-dedicated/.vale.ini +++ b/content/influxdb/cloud-dedicated/.vale.ini @@ -1,6 +1,6 @@ StylesPath = "../../../.ci/vale/styles" -Vocab = InfluxData, Cloud-Dedicated +Vocab = Cloud-Dedicated MinAlertLevel = warning diff --git a/content/influxdb/cloud-dedicated/query-data/execute-queries/optimize-queries.md b/content/influxdb/cloud-dedicated/query-data/execute-queries/optimize-queries.md deleted file mode 100644 index 83f320b5d..000000000 --- a/content/influxdb/cloud-dedicated/query-data/execute-queries/optimize-queries.md +++ /dev/null @@ -1,442 +0,0 @@ ---- -title: Optimize queries -description: > - Optimize your SQL and InfluxQL queries to improve performance and reduce their memory and compute (CPU) requirements. -weight: 401 -menu: - influxdb_cloud_dedicated: - name: Optimize queries - parent: Execute queries -influxdb/cloud-dedicated/tags: [query, sql, influxql] -related: - - /influxdb/cloud-dedicated/query-data/sql/ - - /influxdb/cloud-dedicated/query-data/influxql/ - - /influxdb/cloud-dedicated/query-data/execute-queries/troubleshoot/ - - /influxdb/cloud-dedicated/reference/client-libraries/v3/ ---- - -Use the following tools to help you identify performance bottlenecks and troubleshoot problems in queries: - - - -- [EXPLAIN and ANALYZE](#explain-and-analyze) -- [Enable trace logging](#enable-trace-logging) - - [Avoid unnecessary tracing](#avoid-unnecessary-tracing) - - [Syntax](#syntax) - - [Example](#example) - - [Tracing response header](#tracing-response-header) - - [Trace response header syntax](#trace-response-header-syntax) - - [Inspect Flight response headers](#inspect-flight-response-headers) -- [Retrieve query information](#retrieve-query-information) - - - -## EXPLAIN and ANALYZE - -To view the query engine's execution plan and metrics for an SQL or InfluxQL query, prepend [`EXPLAIN`](/influxdb/cloud-dedicated/reference/sql/explain/) or [`EXPLAIN ANALYZE`](/influxdb/cloud-dedicated/reference/sql/explain/#explain-analyze) to the query. -The report can reveal query bottlenecks such as a large number of table scans or parquet files, and can help triage the question, "Is the query slow due to the amount of work required or due to a problem with the schema, compactor, etc.?" - -The following example shows how to use the InfluxDB v3 Python client library and pandas to view `EXPLAIN` and `EXPLAIN ANALYZE` results for a query: - - - -{{% code-placeholders "DATABASE_(NAME|TOKEN)" %}} - - -```python -from influxdb_client_3 import InfluxDBClient3 -import pandas as pd -import tabulate # Required for pandas.to_markdown() - -# Instantiate an InfluxDB client. -client = InfluxDBClient3(token = f"DATABASE_TOKEN", - host = f"{{< influxdb/host >}}", - database = f"DATABASE_NAME") - -sql_explain = '''EXPLAIN - SELECT temp - FROM home - WHERE time >= now() - INTERVAL '90 days' - AND room = 'Kitchen' - ORDER BY time''' - -table = client.query(sql_explain) -df = table.to_pandas() -print(df.to_markdown(index=False)) - -assert df.shape == (2, 2), f'Expect {df.shape} to have 2 columns, 2 rows' -assert 'physical_plan' in df.plan_type.values, "Expect physical_plan" -assert 'logical_plan' in df.plan_type.values, "Expect logical_plan" -``` - -{{< expand-wrapper >}} -{{% expand "View EXPLAIN example results" %}} -| plan_type | plan | -|:--------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| logical_plan | Projection: home.temp | -| | Sort: home.time ASC NULLS LAST | -| | Projection: home.temp, home.time | -| | TableScan: home projection=[room, temp, time], full_filters=[home.time >= TimestampNanosecond(1688676582918581320, None), home.room = Dictionary(Int32, Utf8("Kitchen"))] | -| physical_plan | ProjectionExec: expr=[temp@0 as temp] | -| | SortExec: expr=[time@1 ASC NULLS LAST] | -| | EmptyExec: produce_one_row=false | -{{% /expand %}} -{{< /expand-wrapper >}} - - - -```python -sql_explain_analyze = '''EXPLAIN ANALYZE - SELECT * - FROM home - WHERE time >= now() - INTERVAL '90 days' - ORDER BY time''' - -table = client.query(sql_explain_analyze) -df = table.to_pandas() -print(df.to_markdown(index=False)) - -assert df.shape == (1,2) -assert 'Plan with Metrics' in df.plan_type.values, "Expect plan metrics" - -client.close() -``` -{{% /code-placeholders %}} - -Replace the following: - -- {{% code-placeholder-key %}}`DATABASE_NAME`{{% /code-placeholder-key %}}: your {{% product-name %}} database -- {{% code-placeholder-key %}}`DATABASE_TOKEN`{{% /code-placeholder-key %}}: a [database token](/influxdb/cloud-dedicated/admin/tokens/) with sufficient permissions to the specified database - -{{< expand-wrapper >}} -{{% expand "View EXPLAIN ANALYZE example results" %}} -| plan_type | plan | -|:------------------|:-----------------------------------------------------------------------------------------------------------------------| -| Plan with Metrics | ProjectionExec: expr=[temp@0 as temp], metrics=[output_rows=0, elapsed_compute=1ns] | -| | SortExec: expr=[time@1 ASC NULLS LAST], metrics=[output_rows=0, elapsed_compute=1ns, spill_count=0, spilled_bytes=0] | -| | EmptyExec: produce_one_row=false, metrics=[] -{{% /expand %}} -{{< /expand-wrapper >}} - -## Enable trace logging - -When you enable trace logging for a query, InfluxDB propagates your _trace ID_ through system processes and collects additional log information. - -InfluxDB Support can then use the trace ID that you provide to filter, collate, and analyze log information for the query run. -The tracing system follows the [OpenTelemetry traces](https://opentelemetry.io/docs/concepts/signals/traces/) model for providing observability into a request. - -{{% warn %}} -#### Avoid unnecessary tracing - -Only enable tracing for a query when you need to request troubleshooting help from InfluxDB Support. -To manage resources, InfluxDB has an upper limit for the number of trace requests. -Too many traces can cause InfluxDB to evict log information. -{{% /warn %}} - -To enable tracing for a query, include the `influx-trace-id` header in your query request. - -### Syntax - -Use the following syntax for the `influx-trace-id` header: - -```http -influx-trace-id: TRACE_ID:1112223334445:0:1 -``` - -In the header value, replace the following: - -- `TRACE_ID`: a unique string, 8-16 bytes long, encoded as hexadecimal (32 maximum hex characters). - The trace ID should uniquely identify the query run. -- `:1112223334445:0:1`: InfluxDB constant values (required, but ignored) - -### Example - -The following examples show how to create and pass a trace ID to enable query tracing in InfluxDB: - -{{< tabs-wrapper >}} -{{% tabs %}} -[Python with FlightCallOptions](#) -[Python with FlightClientMiddleware](#python-with-flightclientmiddleware) -{{% /tabs %}} -{{% tab-content %}} - -Use the `InfluxDBClient3` InfluxDB Python client and pass the `headers` argument in the -`query()` method. - - - -{{% code-placeholders "DATABASE_(NAME|TOKEN)|APP_REQUEST_ID" %}} - - - -```python -from influxdb_client_3 import InfluxDBClient3 -import secrets - -def use_flightcalloptions_trace_header(): - print('# Use FlightCallOptions to enable tracing.') - client = InfluxDBClient3(token=f"DATABASE_TOKEN", - host=f"{{< influxdb/host >}}", - database=f"DATABASE_NAME") - - # Generate a trace ID for the query: - # 1. Generate a random 8-byte value as bytes. - # 2. Encode the value as hexadecimal. - random_bytes = secrets.token_bytes(8) - trace_id = random_bytes.hex() - - # Append required constants to the trace ID. - trace_value = f"{trace_id}:1112223334445:0:1" - - # Encode the header key and value as bytes. - # Create a list of header tuples. - headers = [((b"influx-trace-id", trace_value.encode('utf-8')))] - - sql = "SELECT * FROM home WHERE time >= now() - INTERVAL '30 days'" - influxql = "SELECT * FROM home WHERE time >= -90d" - - # Use the query() headers argument to pass the list as FlightCallOptions. - client.query(sql, headers=headers) - - client.close() - -use_flightcalloptions_trace_header() -``` - -{{% /code-placeholders %}} - -{{% /tab-content %}} -{{% tab-content %}} - -Use the `InfluxDBClient3` InfluxDB Python client and `flight.ClientMiddleware` to pass and inspect headers. - -### Tracing response header - -With tracing enabled and a valid trace ID in the request, InfluxDB's `DoGet` action response contains a header with the trace ID that you sent. - -#### Trace response header syntax - -```http -trace-id: TRACE_ID -``` - -### Inspect Flight response headers - -To inspect Flight response headers when using a client library, pass a `FlightClientMiddleware` instance. -that defines a middleware callback function for the `onHeadersReceived` event (the particular function name you use depends on the client library language). - -The following example uses Python client middleware that adds request headers and extracts the trace ID from the `DoGet` response headers: - - - -{{% code-placeholders "DATABASE_(NAME|TOKEN)|APP_REQUEST_ID" %}} - - - -```python -import pyarrow.flight as flight - -class TracingClientMiddleWareFactory(flight.ClientMiddleware): - # Defines a custom middleware factory that returns a middleware instance. - def __init__(self): - self.request_headers = [] - self.response_headers = [] - self.traces = [] - - def addRequestHeader(self, header): - self.request_headers.append(header) - - def addResponseHeader(self, header): - self.response_headers.append(header) - - def addTrace(self, traceid): - self.traces.append(traceid) - - def createTrace(self, traceid): - # Append InfluxDB constants to the trace ID. - trace = f"{traceid}:1112223334445:0:1" - - # To the list of request headers, - # add a tuple with the header key and value as bytes. - self.addRequestHeader((b"influx-trace-id", trace.encode('utf-8'))) - - def start_call(self, info): - return TracingClientMiddleware(info.method, self) - -class TracingClientMiddleware(flight.ClientMiddleware): - # Defines middleware with client event callback methods. - def __init__(self, method, callback_obj): - self._method = method - self.callback = callback_obj - - def call_completed(self, exception): - print('callback: call_completed') - if(exception): - print(f" ...with exception: {exception}") - - def sending_headers(self): - print('callback: sending_headers: ', self.callback.request_headers) - if len(self.callback.request_headers) > 0: - return dict(self.callback.request_headers) - - def received_headers(self, headers): - self.callback.addResponseHeader(headers) - # For the DO_GET action, extract the trace ID from the response headers. - if str(self._method) == "FlightMethod.DO_GET" and "trace-id" in headers: - trace_id = headers["trace-id"][0] - self.callback.addTrace(trace_id) - -from influxdb_client_3 import InfluxDBClient3 -import secrets - -def use_middleware_trace_header(): - print('# Use Flight client middleware to enable tracing.') - - # Instantiate the middleware. - res = TracingClientMiddleWareFactory() - - # Instantiate the client, passing in the middleware instance that provides - # event callbacks for the request. - client = InfluxDBClient3(token=f"DATABASE_TOKEN", - host=f"{{< influxdb/host >}}", - database=f"DATABASE_NAME", - flight_client_options={"middleware": (res,)}) - - # Generate a trace ID for the query: - # 1. Generate a random 8-byte value as bytes. - # 2. Encode the value as hexadecimal. - random_bytes = secrets.token_bytes(8) - trace_id = random_bytes.hex() - - res.createTrace(trace_id) - - sql = "SELECT * FROM home WHERE time >= now() - INTERVAL '30 days'" - - client.query(sql) - client.close() - assert trace_id in res.traces[0], "Expect trace ID in DoGet response." - -use_middleware_trace_header() -``` -{{% /code-placeholders %}} - -{{% /tab-content %}} -{{< /tabs-wrapper >}} - -Replace the following: - -- {{% code-placeholder-key %}}`DATABASE_NAME`{{% /code-placeholder-key %}}: your {{% product-name %}} database -- {{% code-placeholder-key %}}`DATABASE_TOKEN`{{% /code-placeholder-key %}}: a [database token](/influxdb/cloud-dedicated/admin/tokens/) with sufficient permissions to the specified database - -{{% note %}} -Store or log your query trace ID to ensure you can provide it to InfluxDB Support for troubleshooting. -{{% /note %}} - -After you run your query with tracing enabled, do the following: - -- Remove the tracing header from subsequent runs of the query (to [avoid unnecessary tracing](#avoid-unnecessary-tracing)). -- Provide the trace ID in a request to InfluxDB Support. - -## Retrieve query information - -In addition to the SQL standard `information_schema`, {{% product-name %}} contains _system_ tables that provide access to -InfluxDB-specific information. -The information in each system table is scoped to the namespace you're querying; -you can only retrieve system information for that particular instance. - -To get information about queries you've run on the current instance, use SQL to query the [`system.queries` table](/influxdb/cloud-dedicated/reference/internals/system-tables/#systemqueries-measurement), which contains information from the querier instance currently handling queries. -If you [enabled trace logging for the query](#enable-trace-logging-for-a-query), the `trace-id` appears in the `system.queries.trace_id` column for the query. - -The `system.queries` table is an InfluxDB v3 **debug feature**. -To enable the feature and query `system.queries`, include an `"iox-debug"` header set to `"true"` and use SQL to query the table. - -The following sample code shows how to use the Python client library to do the following: - -1. Enable tracing for a query. -2. Retrieve the trace ID record from `system.queries`. - - - -{{% code-placeholders "DATABASE_(NAME|TOKEN)|APP_REQUEST_ID" %}} - - - -```python -from influxdb_client_3 import InfluxDBClient3 -import secrets -import pandas - -def get_query_information(): - print('# Get query information') - - client = InfluxDBClient3(token = f"DATABASE_TOKEN", - host = f"{{< influxdb/host >}}", - database = f"DATABASE_NAME") - - random_bytes = secrets.token_bytes(16) - trace_id = random_bytes.hex() - trace_value = (f"{trace_id}:1112223334445:0:1").encode('utf-8') - sql = "SELECT * FROM home WHERE time >= now() - INTERVAL '30 days'" - - try: - client.query(sql, headers=[(b'influx-trace-id', trace_value)]) - client.close() - except Exception as e: - print("Query error: ", e) - - client = InfluxDBClient3(token = f"DATABASE_TOKEN", - host = f"{{< influxdb/host >}}", - database = f"DATABASE_NAME") - - import time - df = pandas.DataFrame() - - for i in range(0, 5): - time.sleep(1) - # Use SQL - # To query the system.queries table for your trace ID, pass the following: - # - the iox-debug: true request header - # - an SQL query for the trace_id column - reader = client.query(f'''SELECT compute_duration, query_type, query_text, - success, trace_id - FROM system.queries - WHERE issue_time >= now() - INTERVAL '1 day' - AND trace_id = '{trace_id}' - ORDER BY issue_time DESC - ''', - headers=[(b"iox-debug", b"true")], - mode="reader") - - df = reader.read_all().to_pandas() - if df.shape[0]: - break - - assert df.shape == (1, 5), f"Expect a row for the query trace ID." - print(df) - -get_query_information() -``` -{{% /code-placeholders %}} - -The output is similar to the following: - -```text -compute_duration query_type query_text success trace_id - 0 days sql SELECT compute_duration, quer... True 67338... -``` diff --git a/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/_index.md b/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/_index.md new file mode 100644 index 000000000..62ce34ab8 --- /dev/null +++ b/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/_index.md @@ -0,0 +1,23 @@ +--- +title: Troubleshoot and optimize queries +description: > + Troubleshoot errors and optimize performance for SQL and InfluxQL queries in InfluxDB. + Use observability tools to view query execution and metrics. +weight: 201 +menu: + influxdb_cloud_dedicated: + name: Troubleshoot and optimize queries + parent: Query data +influxdb/cloud-dedicated/tags: [query, performance, observability, errors, sql, influxql] +related: + - /influxdb/cloud-dedicated/query-data/sql/ + - /influxdb/cloud-dedicated/query-data/influxql/ +aliases: + - /influxdb/cloud-dedicated/query-data/execute-queries/troubleshoot/ + +--- + +Troubleshoot errors and optimize performance for SQL and InfluxQL queries in {{% product-name %}}. +Use observability tools to view query execution and metrics. + +{{< children >}} diff --git a/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/analyze-query-plan.md b/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/analyze-query-plan.md new file mode 100644 index 000000000..e829faa3a --- /dev/null +++ b/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/analyze-query-plan.md @@ -0,0 +1,771 @@ +--- +title: Analyze a query plan +description: > + Learn how to read and analyze a query plan to + understand how a query is executed and find performance bottlenecks. +weight: 401 +menu: + influxdb_cloud_dedicated: + name: Analyze a query plan + parent: Troubleshoot and optimize queries +influxdb/cloud-dedicated/tags: [query, sql, influxql, observability, query plan] +related: + - /influxdb/cloud-dedicated/query-data/sql/ + - /influxdb/cloud-dedicated/query-data/influxql/ + - /influxdb/cloud-dedicated/reference/internals/query-plans/ + - /influxdb/cloud-dedicated/reference/internals/storage-engine +--- + +Learn how to read and analyze a [query plan](/influxdb/cloud-dedicated/reference/glossary/#query-plan) to +understand query execution steps and data organization, and find performance bottlenecks. + +When you query InfluxDB v3, the Querier devises a query plan for executing the query. +The engine tries to determine the optimal plan for the query structure and data. +By learning how to generate and interpret reports for the query plan, +you can better understand how the query is executed and identify bottlenecks that affect the performance of your query. + +For example, if the query plan reveals that your query reads a large number of Parquet files, +you can then take steps to [optimize your query](/influxdb/cloud-dedicated/query-data/optimize-queries/), such as add filters to read less data or +configure your cluster to store fewer and larger files. + +- [Use EXPLAIN keywords to view a query plan](#use-explain-keywords-to-view-a-query-plan) +- [Read an EXPLAIN report](#read-an-explain-report) +- [Read a query plan](#read-a-query-plan) + - [Example physical plan for a SELECT - ORDER BY query](#example-physical-plan-for-a-select---order-by-query) + - [Example `EXPLAIN` report for an empty result set](#example-explain-report-for-an-empty-result-set) +- [Analyze a query plan for leading edge data](#analyze-a-query-plan-for-leading-edge-data) + - [Sample data](#sample-data) + - [Sample query](#sample-query) + - [EXPLAIN report for the leading edge data query](#explain-report-for-the-leading-edge-data-query) + - [Locate the physical plan](#locate-the-physical-plan) + - [Read the physical plan](#read-the-physical-plan) + - [Data scanning nodes (ParquetExec and RecordBatchesExec)](#data-scanning-nodes-parquetexec-and-recordbatchesexec) + - [Analyze branch structures](#analyze-branch-structures) + +## Use EXPLAIN keywords to view a query plan + +Use the `EXPLAIN` keyword (and the optional [`ANALYZE`](/influxdb/cloud-dedicated/reference/sql/explain/#explain-analyze) and [`VERBOSE`](/influxdb/cloud-dedicated/reference/sql/explain/#explain-analyze-verbose) keywords) to view the query plans for a query. + +{{% expand-wrapper %}} +{{% expand "Use Python and pandas to view an EXPLAIN report" %}} + +The following example shows how to use the InfluxDB v3 Python client library and pandas to view the `EXPLAIN` report for a query: + + + + + +{{% code-placeholders "DATABASE_(NAME|TOKEN)" %}} + +```python +from influxdb_client_3 import InfluxDBClient3 +import pandas as pd +import tabulate # Required for pandas.to_markdown() + +# Instantiate an InfluxDB client. +client = InfluxDBClient3(token = f"TOKEN", + host = f"{{< influxdb/host >}}", + database = f"DATABASE_NAME") + +sql_explain = '''EXPLAIN + SELECT temp + FROM home + WHERE time >= now() - INTERVAL '7 days' + AND room = 'Kitchen' + ORDER BY time''' + +table = client.query(sql_explain) +df = table.to_pandas() +print(df.to_markdown(index=False)) + +assert df.shape == (2, 2), f'Expect {df.shape} to have 2 columns, 2 rows' +assert 'physical_plan' in df.plan_type.values, "Expect physical_plan" +assert 'logical_plan' in df.plan_type.values, "Expect logical_plan" +``` + +{{% /code-placeholders %}} + +Replace the following: + +- {{% code-placeholder-key %}}`DATABASE_NAME`{{% /code-placeholder-key %}}: your {{% product-name %}} database +- {{% code-placeholder-key %}}`TOKEN`{{% /code-placeholder-key %}}: a [token](/influxdb/cloud-dedicated/admin/tokens/) with sufficient permissions to the specified database + +{{% /expand %}} +{{% /expand-wrapper %}} + +## Read an EXPLAIN report + +When you [use `EXPLAIN` keywords to view a query plan](#use-explain-keywords-to-view-a-query-plan), the report contains the following: + +- two columns: `plan_type` and `plan` +- one row for the [logical plan](/influxdb/cloud-dedicated/reference/internals/query-plans/#logical-plan) (`logical_plan`) +- one row for the [physical plan](/influxdb/cloud-dedicated/reference/internals/query-plans/#physical-plan) (`physical_plan`) + +## Read a query plan + +Plans are in _tree format_--each plan is an upside-down tree in which +execution and data flow from _leaf nodes_, the innermost steps in the plan, to outer _branch nodes_. +Whether reading a logical or physical plan, keep the following in mind: + +- Start at the the _leaf nodes_ and read upward. +- At the top of the plan, the _root node_ represents the final, encompassing step. + +In a [physical plan](/influxdb/cloud-dedicated/reference/internals/query-plan/#physical-plan), each step is an [`ExecutionPlan` node](/influxdb/cloud-dedicated/reference/internals/query-plan/#execution-plan-nodes) that receives expressions for input data and output requirements, and computes a partition of data. + +Use the following steps to analyze a query plan and estimate how much work is required to complete the query. +The same steps apply regardless of how large or complex the plan might seem. + +1. Start from the furthest indented steps (the _leaf nodes_), and read upward. +2. Understand the job of each [`ExecutionPlan` node](/influxdb/cloud-dedicated/reference/internals/query-plan/#executionplan-nodes)--for example, a [`UnionExec`](/influxdb/cloud-dedicated/reference/internals/query-plan/#unionexec) node encompassing the leaf nodes means that the `UnionExec` concatenates the output of all the leaves. +3. For each expression, answer the following questions: + - What is the shape and size of data input to the plan? + - What is the shape and size of data output from the plan? + +The remainder of this guide walks you through analyzing a physical plan. +Understanding the sequence, role, input, and output of nodes in your query plan can help you estimate the overall workload and find potential bottlenecks in the query. + +### Example physical plan for a SELECT - ORDER BY query + +The following example shows how to read an `EXPLAIN` report and a physical query plan. + +Given `h20` measurement data and the following query: + +```sql +EXPLAIN SELECT city, min_temp, time FROM h2o ORDER BY city ASC, time DESC; +``` + +The output is similar to the following: + +#### EXPLAIN report + +```sql +| plan_type | plan | ++---------------+--------------------------------------------------------------------------+ +| logical_plan | Sort: h2o.city ASC NULLS LAST, h2o.time DESC NULLS FIRST | +| | TableScan: h2o projection=[city, min_temp, time] | +| physical_plan | SortPreservingMergeExec: [city@0 ASC NULLS LAST,time@2 DESC] | +| | UnionExec | +| | SortExec: expr=[city@0 ASC NULLS LAST,time@2 DESC] | +| | ParquetExec: file_groups={...}, projection=[city, min_temp, time] | +| | SortExec: expr=[city@0 ASC NULLS LAST,time@2 DESC] | +| | ParquetExec: file_groups={...}, projection=[city, min_temp, time] | +| | | +``` + +{{% caption %}} +Output from `EXPLAIN SELECT city, min_temp, time FROM h2o ORDER BY city ASC, time DESC;` +{{% /caption %}} + +Each step, or _node_, in the physical plan is an `ExecutionPlan` name and the key-value _expressions_ that contain relevant parts of the query--for example, the first node in the [`EXPLAIN` report](#explain-report) physical plan is a `ParquetExec` execution plan: + +```text +ParquetExec: file_groups={...}, projection=[city, min_temp, time] +``` + +Because `ParquetExec` and `RecordBatchesExec` nodes retrieve and scan data in InfluxDB queries, every query plan starts with one or more of these nodes. + +#### Physical plan data flow + +Data flows _up_ in a query plan. + +The following diagram shows the data flow and sequence of nodes in the [`EXPLAIN` report](#explain-report) physical plan: + + +{{< html-diagram/query-plan >}} + + +{{% caption %}} +Execution and data flow in the [`EXPLAIN` report](#explain-report) physical plan. +`ParquetExec` nodes execute in parallel and `UnionExec` combines their output. +{{% /caption %}} + +The following steps summarize the [physical plan execution and data flow](#physical-plan-data-flow): + +1. Two `ParquetExec` plans, in parallel, read data from Parquet files: + - Each `ParquetExec` node processes one or more _file groups_. + - Each file group contains one or more Parquet file paths. + - A `ParquetExec` node processes its groups in parallel, reading each group's files sequentially. + - The output is a stream of data to the corresponding `SortExec` node. +2. The `SortExec` nodes, in parallel, sort the data by `city` (ascending) and `time` (descending). Sorting is required by the `SortPreservingMergeExec` plan. +3. The `UnionExec` node concatenates the streams to union the output of the parallel `SortExec` nodes. +4. The `SortPreservingMergeExec` node merges the previously sorted and unioned data from `UnionExec`. + +### Example `EXPLAIN` report for an empty result set + +If your table doesn't contain data for the time range in your query, the physical plan starts with an `EmptyExec` leaf node--for example: + +{{% code-callout "EmptyExec"%}} + +```sql +ProjectionExec: expr=[temp@0 as temp] + SortExec: expr=[time@1 ASC NULLS LAST] + EmptyExec: produce_one_row=false +``` + +{{% /code-callout %}} + +## Analyze a query plan for leading edge data + +The following sections guide you through analyzing a physical query plan for a typical time series use case--aggregating recently written (_leading edge_) data. +Although the query and plan are more complex than in the [preceding example](#example-physical-plan-for-a-select---order-by-query), you'll follow the same [steps to read the query plan](#read-a-query-plan). +After learning how to read the query plan, you'll have an understanding of `ExecutionPlans`, data flow, and potential query bottlenecks. + +### Sample data + +Consider the following `h20` data, represented as "chunks" of line protocol, written to InfluxDB: + +```text +// h20 data +// The following data represents 5 batches, or "chunks", of line protocol +// written to InfluxDB. +// - Chunks 1-4 are ingested and each is persisted to a separate partition file in storage. +// - Chunk 5 is ingested and not yet persisted to storage. +// - Chunks 1 and 2 cover short windows of time that don't overlap times in other chunks. +// - Chunks 3 and 4 cover larger windows of time and the time ranges overlap each other. +// - Chunk 5 contains the largest time range and overlaps with chunk 4, the Parquet file with the largest time-range. +// - In InfluxDB, a chunk never duplicates its own data. +// +// Chunk 1: stored Parquet file +// - time range: 50-249 +// - no duplicates in its own chunk +// - no overlap with any other chunks +[ +"h2o,state=MA,city=Bedford min_temp=71.59 150", +"h2o,state=MA,city=Boston min_temp=70.4, 50", +"h2o,state=MA,city=Andover max_temp=69.2, 249", +], + +// Chunk 2: stored Parquet file +// - time range: 250-349 +// - no duplicates in its own chunk +// - no overlap with any other chunks +// - adds a new field (area) +[ +"h2o,state=CA,city=SF min_temp=79.0,max_temp=87.2,area=500u 300", +"h2o,state=CA,city=SJ min_temp=75.5,max_temp=84.08 349", +"h2o,state=MA,city=Bedford max_temp=78.75,area=742u 300", +"h2o,state=MA,city=Boston min_temp=65.4 250", +], + +// Chunk 3: stored Parquet file +// - time range: 350-500 +// - no duplicates in its own chunk +// - overlaps chunk 4 +[ +"h2o,state=CA,city=SJ min_temp=77.0,max_temp=90.7 450", +"h2o,state=CA,city=SJ min_temp=69.5,max_temp=88.2 500", +"h2o,state=MA,city=Boston min_temp=68.4 350", +], + +// Chunk 4: stored Parquet file +// - time range: 400-600 +// - no duplicates in its own chunk +// - overlaps chunk 3 +[ + "h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 600", + "h2o,state=CA,city=SJ min_temp=69.5,max_temp=89.2 600", // duplicates row 3 in chunk 5 + "h2o,state=MA,city=Bedford max_temp=80.75,area=742u 400", // overlaps chunk 3 + "h2o,state=MA,city=Boston min_temp=65.40,max_temp=82.67 400", // overlaps chunk 3 +], + +// Chunk 5: Ingester data +// - time range: 550-700 +// - overlaps and duplicates data in chunk 4 +[ +"h2o,state=MA,city=Bedford max_temp=88.75,area=742u 600", // overlaps chunk 4 +"h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 650", +"h2o,state=CA,city=SJ min_temp=68.5,max_temp=90.0 600", // duplicates row 2 in chunk 4 +"h2o,state=CA,city=SJ min_temp=75.5,max_temp=84.08 700", +"h2o,state=MA,city=Boston min_temp=67.4 550", // overlaps chunk 4 +] +``` + +The following query selects all the data: + +```sql +SELECT state, city, min_temp, max_temp, area, time +FROM h2o +ORDER BY state asc, city asc, time desc; +``` + +The output is the following: + +```sql ++-------+---------+----------+----------+------+--------------------------------+ +| state | city | min_temp | max_temp | area | time | ++-------+---------+----------+----------+------+--------------------------------+ +| CA | SF | 68.4 | 85.7 | 500 | 1970-01-01T00:00:00.000000650Z | +| CA | SF | 68.4 | 85.7 | 500 | 1970-01-01T00:00:00.000000600Z | +| CA | SF | 79.0 | 87.2 | 500 | 1970-01-01T00:00:00.000000300Z | +| CA | SJ | 75.5 | 84.08 | | 1970-01-01T00:00:00.000000700Z | +| CA | SJ | 68.5 | 90.0 | | 1970-01-01T00:00:00.000000600Z | +| CA | SJ | 69.5 | 88.2 | | 1970-01-01T00:00:00.000000500Z | +| CA | SJ | 77.0 | 90.7 | | 1970-01-01T00:00:00.000000450Z | +| CA | SJ | 75.5 | 84.08 | | 1970-01-01T00:00:00.000000349Z | +| MA | Andover | | 69.2 | | 1970-01-01T00:00:00.000000249Z | +| MA | Bedford | | 88.75 | 742 | 1970-01-01T00:00:00.000000600Z | +| MA | Bedford | | 80.75 | 742 | 1970-01-01T00:00:00.000000400Z | +| MA | Bedford | | 78.75 | 742 | 1970-01-01T00:00:00.000000300Z | +| MA | Bedford | 71.59 | | | 1970-01-01T00:00:00.000000150Z | +| MA | Boston | 67.4 | | | 1970-01-01T00:00:00.000000550Z | +| MA | Boston | 65.4 | 82.67 | | 1970-01-01T00:00:00.000000400Z | +| MA | Boston | 68.4 | | | 1970-01-01T00:00:00.000000350Z | +| MA | Boston | 65.4 | | | 1970-01-01T00:00:00.000000250Z | +| MA | Boston | 70.4 | | | 1970-01-01T00:00:00.000000050Z | ++-------+---------+----------+----------+------+--------------------------------+ +``` + +### Sample query + +The following query selects leading edge data from the [sample data](#sample-data): + +```sql +SELECT city, count(1) +FROM h2o +WHERE time >= to_timestamp(200) AND time < to_timestamp(700) + AND state = 'MA' +GROUP BY city +ORDER BY city ASC; +``` + +The output is the following: + +```sql ++---------+-----------------+ +| city | COUNT(Int64(1)) | ++---------+-----------------+ +| Andover | 1 | +| Bedford | 3 | +| Boston | 4 | ++---------+-----------------+ +``` + +### EXPLAIN report for the leading edge data query + +The following query generates the `EXPLAIN` report for the preceding [sample query](#sample-query): + +```sql +EXPLAIN SELECT city, count(1) +FROM h2o +WHERE time >= to_timestamp(200) AND time < to_timestamp(700) + AND state = 'MA' +GROUP BY city +ORDER BY city ASC; +``` + +{{< expand-wrapper >}} +{{% expand "EXPLAIN report for a leading edge data query" %}} + +```sql +| plan_type | plan | +| logical_plan | Sort: h2o.city ASC NULLS LAST | +| | Aggregate: groupBy=[[h2o.city]], aggr=[[COUNT(Int64(1))]] | +| | TableScan: h2o projection=[city], full_filters=[h2o.time >= TimestampNanosecond(200, None), h2o.time < TimestampNanosecond(700, None), h2o.state = Dictionary(Int32, Utf8("MA"))] | +| physical_plan | SortPreservingMergeExec: [city@0 ASC NULLS LAST] | +| | SortExec: expr=[city@0 ASC NULLS LAST] | +| | AggregateExec: mode=FinalPartitioned, gby=[city@0 as city], aggr=[COUNT(Int64(1))] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | RepartitionExec: partitioning=Hash([city@0], 4), input_partitions=4 | +| | AggregateExec: mode=Partial, gby=[city@0 as city], aggr=[COUNT(Int64(1))] | +| | RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=3 | +| | UnionExec | +| | ProjectionExec: expr=[city@0 as city] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | FilterExec: time@2 >= 200 AND time@2 < 700 AND state@1 = MA | +| | ParquetExec: file_groups={2 groups: [[1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/243db601-f3f1-401b-afda-82160d8cc1a8.Parquet], [1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/f5fb7c7d-16ac-49ba-a811-69578d05843f.Parquet]]}, projection=[city, state, time], output_ordering=[state@1 ASC, city@0 ASC, time@2 ASC], predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA, pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 | +| | ProjectionExec: expr=[city@1 as city] | +| | DeduplicateExec: [state@2 ASC,city@1 ASC,time@3 ASC] | +| | SortPreservingMergeExec: [state@2 ASC,city@1 ASC,time@3 ASC,__chunk_order@0 ASC] | +| | UnionExec | +| | SortExec: expr=[state@2 ASC,city@1 ASC,time@3 ASC,__chunk_order@0 ASC] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | FilterExec: time@3 >= 200 AND time@3 < 700 AND state@2 = MA | +| | RecordBatchesExec: chunks=1, projection=[__chunk_order, city, state, time] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | FilterExec: time@3 >= 200 AND time@3 < 700 AND state@2 = MA | +| | ParquetExec: file_groups={2 groups: [[1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/2cbb3992-4607-494d-82e4-66c480123189.Parquet], [1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/9255eb7f-2b51-427b-9c9b-926199c85bdf.Parquet]]}, projection=[__chunk_order, city, state, time], output_ordering=[state@2 ASC, city@1 ASC, time@3 ASC, __chunk_order@0 ASC], predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA, pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 | +``` + +{{% caption %}} +`EXPLAIN` report for a typical leading edge data query +{{% /caption %}} + +{{% /expand %}} +{{< /expand-wrapper >}} + +The comments in the [sample data](#sample-data) tell you which data chunks _overlap_ or duplicate data in other chunks. +Two chunks of data overlap if there are portions of time for which data exists in both chunks. +_You'll learn how to [recognize overlapping and duplicate data](#recognize-overlapping-and-duplicate-data) in a query plan later in this guide._ + +Unlike the sample data, your data likely doesn't tell you where overlaps or duplicates exist. +A physical plan can reveal overlaps and duplicates in your data and how they affect your queries--for example, after learning how to read a physical plan, you might summarize the data scanning steps as follows: + +- Query execution starts with two `ParquetExec` and one `RecordBatchesExec` execution plans that run in parallel. +- The first `ParquetExec` node reads two files that don't overlap any other files and don't duplicate data; the files don't require deduplication. +- The second `ParquetExec` node reads two files that overlap each other and overlap the ingested data scanned in the `RecordBatchesExec` node; the query plan must include the deduplication process for these nodes before completing the query. + +The remaining sections analyze `ExecutionPlan` node structure and arguments in the example physical plan. +The example includes DataFusion and InfluxDB-specific [`ExecutionPlan` nodes](/influxdb/cloud-dedicated/reference/internals/query-plans/#executionplan-nodes). + +### Locate the physical plan + +To begin analyzing the physical plan for the query, find the row in the [`EXPLAIN` report](#explain-report-for-the-leading-edge-data-query) where the `plan_type` column has the value `physical_plan`. +The `plan` column for the row contains the physical plan. + +### Read the physical plan + +The following sections follow the steps to [read a query plan](#read-a-query-plan) and examine the physical plan nodes and their input and output. + +{{% note %}} +To [read the execution flow of a query plan](#read-a-query-plan), always start from the innermost (leaf) nodes and read up toward the top outermost root node. +{{% /note %}} + +#### Physical plan leaf nodes + +Query physical plan leaf node structures + +{{% caption %}} +Leaf node structures in the physical plan +{{% /caption %}} + +### Data scanning nodes (ParquetExec and RecordBatchesExec) + +The [example physical plan](#physical-plan-leaf-nodes) contains three [leaf nodes](#physical-plan-leaf-nodes)--the innermost nodes where the execution flow begins: + +- [`ParquetExec`](/influxdb/cloud-dedicated/reference/internals/query-plan/#parquetexec) nodes retrieve and scan data from Parquet files in the [Object store](/influxdb/cloud-dedicated/reference/internals/storage-engine/#object-store) +- a [`RecordBatchesExec`](/influxdb/cloud-dedicated/reference/internals/query-plan/#recordbatchesexec) node retrieves recently written, yet-to-be-persisted data from the [Ingester](/influxdb/cloud-dedicated/reference/internals/storage-engine/#ingester) + +Because `ParquetExec` and `RecordBatchesExec` retrieve and scan data for a query, every query plan starts with one or more of these nodes. + +The number of `ParquetExec` and `RecordBatchesExec` nodes and their parameter values can tell you which data (and how much) is retrieved for your query, and how efficiently the plan handles the organization (for example, partitioning and deduplication) of your data. + +For convenience, this guide uses the names _ParquetExec_A_ and _ParquetExec_B_ for the `ParquetExec` nodes in the [example physical plan](#physical-plan-leaf-nodes) . +Reading from the top of the physical plan, **ParquetExec_A** is the first leaf node in the physical plan and **ParquetExec_B** is the last (bottom) leaf node. + +_The names indicate the nodes' locations in the report, not their order of execution._ + +- [ParquetExec_A](#parquetexec_a) +- [RecordBatchesExec](#recordbatchesexec) +- [ParquetExec_B](#parquetexec_b) + +#### ParquetExec_A + +```sql +ParquetExec: file_groups={2 groups: [[1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/243db601-f3f1-401b-afda-82160d8cc1a8.Parquet], [1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/f5fb7c7d-16ac-49ba-a811-69578d05843f.Parquet]]}, projection=[city, state, time], output_ordering=[state@1 ASC, city@0 ASC, time@2 ASC], predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA, pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 | +``` + +{{% caption %}} +ParquetExec_A, the first ParquetExec node +{{% /caption %}} + +ParquetExec_A has the following traits: + +##### `file_groups` + +A _file group_ is a list of files for the operator to read. +Files are referenced by path: + +- `1/1/b862a7e9b.../243db601-....parquet` +- `1/1/b862a7e9b.../f5fb7c7d-....parquet` + +The path structure represents how your data is organized. +You can use the file paths to gather more information about the query--for example: + +- to find file information (for example: size and number of rows) in the catalog +- to download the Parquet file from the Object store for debugging +- to find how many partitions the query reads + +A path has the following structure: + +```text +///.Parquet + 1 / 1 /b862a7e9b329ee6a4.../243db601-f3f1-4....Parquet +``` + +- `namespace_id`: the namespace (database) being queried +- `table_id`: the table (measurement) being queried +- `partition_hash_id`: the partition this file belongs to. +You can count partition IDs to find how many partitions the query reads. +- `uuid_of_the_file`: the file identifier. + +`ParquetExec` processes groups in parallel and reads the files in each group sequentially. + +```text +file_groups={2 groups: [[1/1/b862a7e9b329ee6a4/243db601....parquet], [1/1/b862a7e9b329ee6a4/f5fb7c7d....parquet]]} +``` + +- `{2 groups: [[file], [file]}`: ParquetExec_A receives two groups with one file per group. +Therefore, ParquetExec_A reads two files in parallel. + +##### `projection` + +`projection` lists the table columns for the `ExecutionPlan` to read and output. + +```text +projection=[city, state, time] +``` + +- `[city, state, time]`: the [sample data](#sample-data) contains many columns, but the [sample query](#sample-query) requires the Querier to read only three + +##### `output_ordering` + +`output_ordering` specifies the sort order for the `ExecutionPlan` output. +The Query planner passes the parameter if the output should be ordered and if the planner knows the order. + +```text +output_ordering=[state@2 ASC, city@1 ASC, time@3 ASC, __chunk_order@0 ASC] +``` + +When storing data to Parquet files, InfluxDB sorts the data to improve storage compression and query efficiency and the planner tries to preserve that order for as long as possible. +Generally, the `output_ordering` value that `ParquetExec` receives is the ordering (or a subset of the ordering) of stored data. + +_By design, [`RecordBatchesExec`](#recordbatchesexec) data isn't sorted._ + +In the example, the planner specifies that ParquetExec_A use the existing sort order `state ASC, city ASC, time ASC,` for output. + +{{% note %}} +To view the sort order of your stored data, generate an `EXPLAIN` report for a `SELECT ALL` query--for example: + +```sql +EXPLAIN SELECT * FROM TABLE_NAME WHERE time > now() - interval '1 hour' +``` + +Reduce the time range if the query returns too much data. +{{% /note %}} + +##### `predicate` + +`predicate` is the data filter specified in the query. + +```text +predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA +``` + +##### `pruning predicate` + +`pruning_predicate` is created from the [`predicate`](#predicate) value and is the predicate actually used for pruning data and files from the chosen partitions. +The default filters files by `time`. + +```text +pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 +``` + +_Before the physical plan is generated, an additional `partition pruning` step uses predicates on partitioning columns to prune partitions._ + +#### `RecordBatchesExec` + +```sql +RecordBatchesExec: chunks=1, projection=[__chunk_order, city, state, time] +``` + +{{% caption %}}RecordBatchesExec{{% /caption %}} + +[`RecordBatchesExec`](/influxdb/cloud-dedicated/reference/internals/query-plans/#recordbatchesexec) is an InfluxDB-specific `ExecutionPlan` implementation that retrieves recently written, yet-to-be-persisted data from the [Ingester](/influxdb/cloud-dedicated/reference/internals/storage-engine/#ingester). + +In the example, `RecordBatchesExec` contains the following expressions: + +##### `chunks` + +`chunks` is the number of data chunks received from the [Ingester](/influxdb/cloud-dedicated/reference/internals/storage-engine/#ingester). + +```text +chunks=1 +``` + +- `chunks=1`: `RecordBatchesExec` receives one data chunk. + +##### `projection` + +The `projection` list specifies the columns or expressions for the node to read and output. + +```text +[__chunk_order, city, state, time] +``` + +- `__chunk_order`: orders chunks and files for deduplication +- `city, state, time`: the same columns specified in [`ParquetExec_A projection`](#projection-1) + +{{% note %}} +The presence of `__chunk_order` in data scanning nodes indicates that data overlaps, and is possibly duplicated, among the nodes. +{{% /note %}} + +#### ParquetExec_B + +The bottom leaf node in the [example physical plan](#physical-plan-leaf-nodes) is another `ParquetExec` operator, _ParquetExec_B_. + +##### ParquetExec_B expressions + +```sql +ParquetExec: + file_groups={2 groups: [[1/1/b862a7e9b.../2cbb3992-....Parquet], + [1/1/b862a7e9b.../9255eb7f-....Parquet]]}, + projection=[__chunk_order, city, state, time], + output_ordering=[state@2 ASC, city@1 ASC, time@3 ASC, __chunk_order@0 ASC], + predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA, + pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 +``` + +{{% caption %}}ParquetExec_B, the second ParquetExec{{% /caption %}} + +Because ParquetExec_B has overlaps, the `projection` and `output_ordering` expressions use the `__chunk_order` column used in [`RecordBatchesExec` `projection`](#projection-1). + +{{% note %}} +The presence of `__chunk_order` in data scanning nodes indicates that data overlaps, and is possibly duplicated, among the nodes. +{{% /note %}} + +The remaining ParquetExec_B expressions are similar to those in [ParquetExec_A](#parquetexec_a). + +##### How a query plan distributes data for scanning + +If you compare [`file_group`](#file_groups) paths in [ParquetExec_A](#parquetexec_a) to those in [ParquetExec_B](#parquetexec_b), you'll notice that both contain files from the same partition: + +{{% code-callout "b862a7e9b329ee6a4..." %}} + +```text +1/1/b862a7e9b329ee6a4.../... +``` + +{{% /code-callout %}} + +The planner may distribute files from the same partition to different scan nodes for several reasons, including optimizations for handling [overlaps](#how-a-query-plan-distributes-data-for-scanning)--for example: + +- to separate non-overlapped files from overlapped files to minimize work required for deduplication (which is the case in this example) +- to distribute non-overlapped files to increase parallel execution + +### Analyze branch structures + +After data is output from a data scanning node, it flows up to the next parent (outer) node. + +In the example plan: + +- Each leaf node is the first step in a branch of nodes planned for processing the scanned data. +- The three branches execute in parallel. +- After the leaf node, each branch contains the following similar node structure: + +```sql +... +CoalesceBatchesExec: target_batch_size=8192 + FilterExec: time@3 >= 200 AND time@3 < 700 AND state@2 = MA + ... +``` + +- `FilterExec: time@3 >= 200 AND time@3 < 700 AND state@2 = MA`: filters data for the condition `time@3 >= 200 AND time@3 < 700 AND state@2 = MA`, and guarantees that all data is pruned. +- `CoalesceBatchesExec: target_batch_size=8192`: combines small batches into larger batches. See the DataFusion [`CoalesceBatchesExec`] documentation. + +#### Sorting yet-to-be-persisted data + +In the `RecordBatchesExec` branch, the node that follows `CoalesceBatchesExec` is a `SortExec` node: + +```sql +SortExec: expr=[state@2 ASC,city@1 ASC,time@3 ASC,__chunk_order@0 ASC] +``` + +The node uses the specified expression `state ASC, city ASC, time ASC, __chunk_order ASC` to sort the yet-to-be-persisted data. +Neither ParquetExec_A nor ParquetExec_B contain a similar node because data in the Object store is already sorted (by the [Ingester](/influxdb/cloud-dedicated/reference/internals/storage-engine/#ingester) or the [Compactor](/influxdb/cloud-dedicated/reference/internals/storage-engine/#compactor)) in the given order; the query plan only needs to sort data that arrives from the [Ingester](/influxdb/cloud-dedicated/reference/internals/storage-engine/#ingester). + +#### Recognize overlapping and duplicate data + +In the example physical plan, the ParquetExec_B and `RecordBatchesExec` nodes share the following parent nodes: + +```sql +... +DeduplicateExec: [state@2 ASC,city@1 ASC,time@3 ASC] + SortPreservingMergeExec: [state@2 ASC,city@1 ASC,time@3 ASC,__chunk_order@0 ASC] + UnionExec + ... +``` + +{{% caption %}}Overlapped data node structure{{% /caption %}} + +1. `UnionExec`: unions multiple streams of input data by concatenating the partitions. `UnionExec` doesn't do any merging and is fast to execute. +2. `SortPreservingMergeExec: [state@2 ASC,city@1 ASC,time@3 ASC,__chunk_order@0 ASC]`: merges already sorted data; indicates that preceding data (from nodes below it) is already sorted. The output data is a single sorted stream. +3. `DeduplicateExec: [state@2 ASC,city@1 ASC,time@3 ASC]`: deduplicates an input stream of sorted data. + Because `SortPreservingMergeExec` ensures a single sorted stream, it often, but not always, precedes `DeduplicateExec`. + +A `DeduplicateExec` node indicates that encompassed nodes have [_overlapped data_](/influxdb/cloud-dedicated/reference/internals/query-plan/#overlapping-data-and-deduplication)--data in a file or batch have timestamps in the same range as data in another file or batch. +Due to how InfluxDB organizes data, data is never duplicated _within_ a file. + +In the example, the `DeduplicateExec` node encompasses ParquetExec_B and the `RecordBatchesExec` node, which indicates that ParquetExec_B [file group](#file_groups) files overlap the yet-to-be persisted data. + +The following [sample data](#sample-data) excerpt shows overlapping data between a file and [Ingester](/influxdb/cloud-dedicated/reference/internals/storage-engine/#ingester) data: + +```text +// Chunk 4: stored Parquet file +// - time range: 400-600 +[ + "h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 600", +], + +// Chunk 5: Ingester data +// - time range: 550-700 +// - overlaps and duplicates data in chunk 4 +[ +"h2o,state=MA,city=Bedford max_temp=88.75,area=742u 600", // overlaps chunk 4 +... +"h2o,state=MA,city=Boston min_temp=67.4 550", // overlaps chunk 4 +] +``` + +If files or ingested data overlap, the Querier must include the `DeduplicateExec` in the query plan to remove any duplicates. +`DeduplicateExec` doesn't necessarily indicate that data is duplicated. +If a plan reads many files and performs deduplication on all of them, it might be for the following reasons: + +- the files contain duplicate data +- the Object store has many small overlapped files that the Compactor hasn't compacted yet. After compaction, your query may perform better because it has fewer files to read +- the Compactor isn't keeping up. If the data isn't duplicated and you still have many small overlapping files after compaction, then you might want to review the Compactor's workload and add more resources as needed + +A leaf node that doesn't have a `DeduplicateExec` node in its branch doesn't require deduplication and doesn't overlap other files or [Ingester](/influxdb/cloud-dedicated/reference/internals/storage-engine/#ingester) data--for example, ParquetExec_A has no overlaps: + +```sql +ProjectionExec:... + CoalesceBatchesExec:... + FilterExec:... + ParquetExec:... +``` + +{{% caption %}} +The absence of a `DeduplicateExec` node means that files don't overlap. +{{% /caption %}} + +##### Data scan output + +`ProjectionExec` nodes filter columns so that only the `city` column remains in the output: + +```sql +`ProjectionExec: expr=[city@0 as city]` +``` + +##### Final processing + +After deduplicating and filtering data in each leaf node, the plan combines the output and then applies aggregation and sorting operators for the final result: + +```sql +| physical_plan | SortPreservingMergeExec: [city@0 ASC NULLS LAST] | +| | SortExec: expr=[city@0 ASC NULLS LAST] | +| | AggregateExec: mode=FinalPartitioned, gby=[city@0 as city], aggr=[COUNT(Int64(1))] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | RepartitionExec: partitioning=Hash([city@0], 4), input_partitions=4 | +| | AggregateExec: mode=Partial, gby=[city@0 as city], aggr=[COUNT(Int64(1))] | +| | RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=3 | +| | UnionExec +``` + +{{% caption %}} +Operator structure for aggregating, sorting, and final output. +{{% /caption %}} + +- `UnionExec`: unions data streams. Note that the number of output streams is the same as the number of input streams--the `UnionExec` node is an intermediate step to downstream operators that actually merge or split data streams. +- `RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=3`: Splits three input streams into four output streams in round-robin fashion. The plan splits streams to increase parallel execution. +- `AggregateExec: mode=Partial, gby=[city@0 as city], aggr=[COUNT(Int64(1))]`: Groups data as specified in the [query](#sample-query): `city, count(1)`. + This node aggregates each of the four streams separately, and then outputs four streams, indicated by `mode=Partial`--the data isn't fully aggregated. +- `RepartitionExec: partitioning=Hash([city@0], 4), input_partitions=4`: Repartitions data on `Hash([city])` and into four streams--each stream contains data for one city. +- `AggregateExec: mode=FinalPartitioned, gby=[city@0 as city], aggr=[COUNT(Int64(1))]`: Applies the final aggregation (`aggr=[COUNT(Int64(1))]`) to the data. `mode=FinalPartitioned` indicates that the data has already been partitioned (by city) and doesn't need further grouping by `AggregateExec`. +- `SortExec: expr=[city@0 ASC NULLS LAST]`: Sorts the four streams of data, each on `city`, as specified in the query. +- `SortPreservingMergeExec: [city@0 ASC NULLS LAST]`: Merges and sorts the four sorted streams for the final output. + +In the preceding examples, the `EXPLAIN` report shows the query plan without executing the query. +To view runtime metrics, such as execution time for a plan and its operators, use [`EXPLAIN ANALYZE`](/influxdb/cloud-dedicated/reference/sql/explain/#explain-analyze) to generate the report and [tracing](/influxdb/cloud-dedicated/query-data/optimize-queries/#enable-trace-logging) for further debugging, if necessary. diff --git a/content/influxdb/cloud-dedicated/query-data/execute-queries/troubleshoot.md b/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/flight-responses.md similarity index 93% rename from content/influxdb/cloud-dedicated/query-data/execute-queries/troubleshoot.md rename to content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/flight-responses.md index 8556702d0..1eabdf9a1 100644 --- a/content/influxdb/cloud-dedicated/query-data/execute-queries/troubleshoot.md +++ b/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/flight-responses.md @@ -6,28 +6,22 @@ weight: 401 menu: influxdb_cloud_dedicated: name: Understand Flight responses - parent: Execute queries -influxdb/cloud-dedicated/tags: [query, sql, influxql] + parent: Troubleshoot and optimize queries +influxdb/cloud-dedicated/tags: [query, errors, flight] +related: + - /influxdb/cloud-dedicated/query-data/sql/ + - /influxdb/cloud-dedicated/query-data/influxql/ + - /influxdb/cloud-dedicated/reference/client-libraries/v3/ --- Learn how to handle responses and troubleshoot errors encountered when querying {{% product-name %}} with Flight+gRPC and Arrow Flight clients. - - - [InfluxDB Flight responses](#influxdb-flight-responses) - [Stream](#stream) - [Schema](#schema) - - [Example](#example) - [RecordBatch](#recordbatch) - [InfluxDB status and error codes](#influxdb-status-and-error-codes) - [Troubleshoot errors](#troubleshoot-errors) - - [Internal Error: Received RST_STREAM](#internal-error-received-rst_stream) - - [Internal Error: stream terminated by RST_STREAM with NO_ERROR](#internal-error-stream-terminated-by-rst_stream-with-no_error) - - [Invalid Argument: Invalid ticket](#invalid-argument-invalid-ticket) - - [Timeout: Deadline exceeded](#timeout-deadline-exceeded) - - [Unauthenticated: Unauthenticated](#unauthenticated-unauthenticated) - - [Unauthorized: Permission denied](#unauthorized-permission-denied) - - [FlightUnavailableError: Could not get default pem root certs](#flightunavailableerror-could-not-get-default-pem-root-certs) ## InfluxDB Flight responses @@ -42,7 +36,7 @@ For example, if you use the [`influxdb3-python` Python client library](/influxdb InfluxDB responds with one of the following: - A [stream](#stream) in Arrow IPC streaming format -- An [error status code](#influxdb-error-codes) and an optional `details` field that contains the status and a message that describes the error +- An [error status code](#influxdb-status-and-error-codes) and an optional `details` field that contains the status and a message that describes the error ### Stream @@ -129,7 +123,7 @@ In gRPC, every call returns a status object that contains an integer code and a During a request, the gRPC client and server may each return a status--for example: - The server fails to process the query; responds with status `internal error` and gRPC status `13`. -- The request is missing a database token; the server responds with status `unauthenticated` and gRPC status `16`. +- The request is missing a [token](/influxdb/cloud-dedicated/admin/tokens/); the server responds with status `unauthenticated` and gRPC status `16`. - The server responds with a stream, but the client loses the connection due to a network failure and returns status `unavailable`. gRPC defines the integer [status codes](https://grpc.github.io/grpc/core/status_8h.html) and definitions for servers and clients and @@ -170,7 +164,6 @@ _For a list of gRPC codes that servers and clients may return, see [Status codes {{% /expand %}} {{< /expand-wrapper >}} - ### Troubleshoot errors #### Internal Error: Received RST_STREAM @@ -188,8 +181,6 @@ Flight returned internal error, with message: Received RST_STREAM with error cod - Server might have closed the connection due to an internal error. - The client exceeded the server's maximum number of concurrent streams. - - #### Internal Error: stream terminated by RST_STREAM with NO_ERROR **Example**: @@ -205,8 +196,6 @@ pyarrow._flight.FlightInternalError: Flight returned internal error, with messag - Possible network disruption, even if it's temporary. - The server might have reached its maximum capacity or other internal limits. - - #### Invalid Argument: Invalid ticket **Example**: @@ -221,8 +210,6 @@ pyarrow.lib.ArrowInvalid: Flight returned invalid argument error, with message: - The request is missing the database name or some other required metadata value. - The request contains bad query syntax. - - #### Timeout: Deadline exceeded @@ -249,8 +236,6 @@ Flight returned unauthenticated error, with message: unauthenticated. gRPC clien - Token is missing from the request. - The specified token doesn't exist for the specified organization. - - #### Unauthorized: Permission denied **Example**: @@ -264,8 +249,6 @@ pyarrow._flight.FlightUnauthorizedError: Flight returned unauthorized error, wit - The specified token doesn't have read permission for the specified database. - - #### FlightUnavailableError: Could not get default pem root certs **Example**: diff --git a/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/optimize-queries.md b/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/optimize-queries.md new file mode 100644 index 000000000..acf8d9ca9 --- /dev/null +++ b/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/optimize-queries.md @@ -0,0 +1,64 @@ +--- +title: Optimize queries +description: > + Optimize queries to improve performance and reduce their memory and compute (CPU) requirements in InfluxDB. + Learn how to use observability tools to analyze query execution and view metrics. +weight: 201 +menu: + influxdb_cloud_dedicated: + name: Optimize queries + parent: Troubleshoot and optimize queries +influxdb/cloud-dedicated/tags: [query, performance, observability, errors, sql, influxql] +related: + - /influxdb/cloud-dedicated/query-data/sql/ + - /influxdb/cloud-dedicated/query-data/influxql/ + - /influxdb/cloud-dedicated/query-data/execute-queries/analyze-query-plan/ +aliases: + - /influxdb/cloud-dedicated/query-data/execute-queries/optimize-queries/ +--- + +Optimize SQL and InfluxQL queries to improve performance and reduce their memory and compute (CPU) requirements. +Learn how to use observability tools to analyze query execution and view metrics. + +- [Why is my query slow?](#why-is-my-query-slow) +- [Strategies for improving query performance](#strategies-for-improving-query-performance) +- [Analyze and troubleshoot queries](#analyze-and-troubleshoot-queries) + +## Why is my query slow? + +Query performance depends on time range and complexity. +If a query is slower than you expect, it might be due to the following reasons: + +- It queries data from a large time range. +- It includes intensive operations, such as querying many string values or `ORDER BY` sorting or re-sorting large amounts of data. + +## Strategies for improving query performance + +The following design strategies generally improve query performance and resource use: + +- Follow [schema design best practices](/influxdb/cloud-dedicated/write-data/best-practices/schema-design/) to make querying easier and more performant. +- Query only the data you need--for example, include a [`WHERE` clause](/influxdb/cloud-dedicated/reference/sql/where/) that filters data by a time range. + InfluxDB v3 stores data in a Parquet file for each measurement and day, and retrieves files from the Object store to answer a query. + The smaller the time range in your query, the fewer files InfluxDB needs to retrieve from the Object store. +- [Downsample data](/influxdb/cloud-dedicated/process-data/downsample/) to reduce the amount of data you need to query. + +Some bottlenecks may be out of your control and are the result of a suboptimal execution plan, such as: + +- Applying the same sort (`ORDER BY`) to already sorted data. +- Retrieving many Parquet files from the Object store--the same query performs better if it retrieves fewer - though, larger - files. +- Querying many overlapped Parquet files. +- Performing a large number of table scans. + +{{% note %}} +#### Analyze query plans to view metrics and recognize bottlenecks + +To view runtime metrics for a query, such as the number of files scanned, use the [`EXPLAIN ANALYZE` keywords](/influxdb/cloud-dedicated/reference/sql/explain/#explain-analyze) and learn how to [analyze a query plan](/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/analyze-query-plan/). +{{% /note %}} + +## Analyze and troubleshoot queries + +Use the following tools to analyze and troubleshoot queries and find performance bottlenecks: + +- [Analyze a query plan](/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/analyze-query-plan/) +- [Enable trace logging for a query](/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/trace/) +- [Retrieve `system.queries` information for a query](/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/system-information/) diff --git a/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/system-information.md b/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/system-information.md new file mode 100644 index 000000000..7bb927cfc --- /dev/null +++ b/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/system-information.md @@ -0,0 +1,108 @@ +--- +title: Retrieve system information for a query +description: > + Learn how to use the system.queries debug feature to retrieve system information for a query in InfluxDB Cloud Dedicated. +weight: 401 +menu: + influxdb_cloud_dedicated: + name: Retrieve system information + parent: Troubleshoot and optimize queries +influxdb/cloud-dedicated/tags: [query, observability] +related: + - /influxdb/cloud-dedicated/query-data/sql/ + - /influxdb/cloud-dedicated/query-data/influxql/ + - /influxdb/cloud-dedicated/reference/client-libraries/v3/ +--- + +Learn how to retrieve system information for a query in {{% product-name %}}. + +In addition to the SQL standard `information_schema`, {{% product-name %}} contains _system_ tables that provide access to +InfluxDB-specific information. +The information in each system table is scoped to the namespace you're querying; +you can only retrieve system information for that particular instance. + +To get information about queries you've run on the current instance, use SQL to query the [`system.queries` table](/influxdb/cloud-dedicated/reference/internals/system-tables/#systemqueries-measurement), which contains information from the Querier instance currently handling queries. +If you [enabled trace logging](/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/trace/) for the query, the `trace-id` appears in the `system.queries.trace_id` column for the query. + +The `system.queries` table is an InfluxDB v3 **debug feature**. +To enable the feature and query `system.queries`, include an `"iox-debug"` header set to `"true"` and use SQL to query the table. + +The following sample code shows how to use the Python client library to do the following: + +1. Enable tracing for a query. +2. Retrieve the trace ID record from `system.queries`. + + + +{{% code-placeholders "DATABASE_(NAME|TOKEN)|APP_REQUEST_ID" %}} + + + +```python +from influxdb_client_3 import InfluxDBClient3 +import secrets +import pandas + +def get_query_information(): + print('# Get query information') + + client = InfluxDBClient3(token = f"DATABASE_TOKEN", + host = f"{{< influxdb/host >}}", + database = f"DATABASE_NAME") + + random_bytes = secrets.token_bytes(16) + trace_id = random_bytes.hex() + trace_value = (f"{trace_id}:1112223334445:0:1").encode('utf-8') + sql = "SELECT * FROM home WHERE time >= now() - INTERVAL '30 days'" + + try: + client.query(sql, headers=[(b'influx-trace-id', trace_value)]) + client.close() + except Exception as e: + print("Query error: ", e) + + client = InfluxDBClient3(token = f"DATABASE_TOKEN", + host = f"{{< influxdb/host >}}", + database = f"DATABASE_NAME") + + import time + df = pandas.DataFrame() + + for i in range(0, 5): + time.sleep(1) + # Use SQL + # To query the system.queries table for your trace ID, pass the following: + # - the iox-debug: true request header + # - an SQL query for the trace_id column + reader = client.query(f'''SELECT compute_duration, query_type, query_text, + success, trace_id + FROM system.queries + WHERE issue_time >= now() - INTERVAL '1 day' + AND trace_id = '{trace_id}' + ORDER BY issue_time DESC + ''', + headers=[(b"iox-debug", b"true")], + mode="reader") + + df = reader.read_all().to_pandas() + if df.shape[0]: + break + + assert df.shape == (1, 5), f"Expect a row for the query trace ID." + print(df) + +get_query_information() +``` + +{{% /code-placeholders %}} + +The output is similar to the following: + +```text +compute_duration query_type query_text success trace_id + 0 days sql SELECT compute_duration, quer... True 67338... +``` diff --git a/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/trace.md b/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/trace.md new file mode 100644 index 000000000..f520b4ab3 --- /dev/null +++ b/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/trace.md @@ -0,0 +1,247 @@ +--- +title: Enable trace logging +description: > + Enable trace logging for a query in InfluxDB Cloud Dedicated. +weight: 401 +menu: + influxdb_cloud_dedicated: + name: Enable trace logging + parent: Troubleshoot and optimize queries +influxdb/cloud-dedicated/tags: [query, observability] +related: + - /influxdb/cloud-dedicated/query-data/sql/ + - /influxdb/cloud-dedicated/query-data/influxql/ + - /influxdb/cloud-dedicated/reference/client-libraries/v3/ +--- + +Learn how to enable trace logging to help you identify performance bottlenecks and troubleshoot problems in queries. + +When you enable trace logging for a query, InfluxDB propagates your _trace ID_ through system processes and collects additional log information. +InfluxData Support can then use the trace ID that you provide to filter, collate, and analyze log information for the query run. +The tracing system follows the [OpenTelemetry traces](https://opentelemetry.io/docs/concepts/signals/traces/) model for providing observability into a request. + +{{% warn %}} + +#### Avoid unnecessary tracing + +Only enable tracing for a query when you need to request troubleshooting help from InfluxDB Support. +To manage resources, InfluxDB has an upper limit for the number of trace requests. +Too many traces can cause InfluxDB to evict log information. +{{% /warn %}} + +To enable tracing for a query, include the `influx-trace-id` header in your query request. + +#### Syntax + +Use the following syntax for the `influx-trace-id` header: + +```http +influx-trace-id: TRACE_ID:1112223334445:0:1 +``` + +In the header value, replace the following: + +- `TRACE_ID`: a unique string, 8-16 bytes long, encoded as hexadecimal (32 maximum hex characters). + The trace ID should uniquely identify the query run. +- `:1112223334445:0:1`: InfluxDB constant values (required, but ignored) + +#### Example + +The following examples show how to create and pass a trace ID to enable query tracing in InfluxDB: + +{{< tabs-wrapper >}} +{{% tabs %}} +[Python with FlightCallOptions](#) +[Python with FlightClientMiddleware](#python-with-flightclientmiddleware) +{{% /tabs %}} +{{% tab-content %}} + +Use the `InfluxDBClient3` InfluxDB Python client and pass the `headers` argument in the +`query()` method. + + + +{{% code-placeholders "DATABASE_(NAME|TOKEN)|APP_REQUEST_ID" %}} + + + +```python +from influxdb_client_3 import InfluxDBClient3 +import secrets + +def use_flightcalloptions_trace_header(): + print('# Use FlightCallOptions to enable tracing.') + client = InfluxDBClient3(token=f"DATABASE_TOKEN", + host=f"{{< influxdb/host >}}", + database=f"DATABASE_NAME") + + # Generate a trace ID for the query: + # 1. Generate a random 8-byte value as bytes. + # 2. Encode the value as hexadecimal. + random_bytes = secrets.token_bytes(8) + trace_id = random_bytes.hex() + + # Append required constants to the trace ID. + trace_value = f"{trace_id}:1112223334445:0:1" + + # Encode the header key and value as bytes. + # Create a list of header tuples. + headers = [((b"influx-trace-id", trace_value.encode('utf-8')))] + + sql = "SELECT * FROM home WHERE time >= now() - INTERVAL '30 days'" + influxql = "SELECT * FROM home WHERE time >= -90d" + + # Use the query() headers argument to pass the list as FlightCallOptions. + client.query(sql, headers=headers) + + client.close() + +use_flightcalloptions_trace_header() +``` + +{{% /code-placeholders %}} + +{{% /tab-content %}} +{{% tab-content %}} + +Use the `InfluxDBClient3` InfluxDB Python client and `flight.ClientMiddleware` to pass and inspect headers. + +#### Tracing response header + +With tracing enabled and a valid trace ID in the request, InfluxDB's `DoGet` action response contains a header with the trace ID that you sent. + +##### Trace response header syntax + +```http +trace-id: TRACE_ID +``` + +#### Inspect Flight response headers + +To inspect Flight response headers when using a client library, pass a `FlightClientMiddleware` instance +that defines a middleware callback function for the `onHeadersReceived` event (the particular function name you use depends on the client library language). + +The following example uses Python client middleware that adds request headers and extracts the trace ID from the `DoGet` response headers: + + + +{{% code-placeholders "DATABASE_(NAME|TOKEN)|APP_REQUEST_ID" %}} + + + +```python +import pyarrow.flight as flight + +class TracingClientMiddleWareFactory(flight.ClientMiddleware): + # Defines a custom middleware factory that returns a middleware instance. + def __init__(self): + self.request_headers = [] + self.response_headers = [] + self.traces = [] + + def addRequestHeader(self, header): + self.request_headers.append(header) + + def addResponseHeader(self, header): + self.response_headers.append(header) + + def addTrace(self, traceid): + self.traces.append(traceid) + + def createTrace(self, traceid): + # Append InfluxDB constants to the trace ID. + trace = f"{traceid}:1112223334445:0:1" + + # To the list of request headers, + # add a tuple with the header key and value as bytes. + self.addRequestHeader((b"influx-trace-id", trace.encode('utf-8'))) + + def start_call(self, info): + return TracingClientMiddleware(info.method, self) + +class TracingClientMiddleware(flight.ClientMiddleware): + # Defines middleware with client event callback methods. + def __init__(self, method, callback_obj): + self._method = method + self.callback = callback_obj + + def call_completed(self, exception): + print('callback: call_completed') + if(exception): + print(f" ...with exception: {exception}") + + def sending_headers(self): + print('callback: sending_headers: ', self.callback.request_headers) + if len(self.callback.request_headers) > 0: + return dict(self.callback.request_headers) + + def received_headers(self, headers): + self.callback.addResponseHeader(headers) + # For the DO_GET action, extract the trace ID from the response headers. + if str(self._method) == "FlightMethod.DO_GET" and "trace-id" in headers: + trace_id = headers["trace-id"][0] + self.callback.addTrace(trace_id) + +from influxdb_client_3 import InfluxDBClient3 +import secrets + +def use_middleware_trace_header(): + print('# Use Flight client middleware to enable tracing.') + + # Instantiate the middleware. + res = TracingClientMiddleWareFactory() + + # Instantiate the client, passing in the middleware instance that provides + # event callbacks for the request. + client = InfluxDBClient3(token=f"DATABASE_TOKEN", + host=f"{{< influxdb/host >}}", + database=f"DATABASE_NAME", + flight_client_options={"middleware": (res,)}) + + # Generate a trace ID for the query: + # 1. Generate a random 8-byte value as bytes. + # 2. Encode the value as hexadecimal. + random_bytes = secrets.token_bytes(8) + trace_id = random_bytes.hex() + + res.createTrace(trace_id) + + sql = "SELECT * FROM home WHERE time >= now() - INTERVAL '30 days'" + + client.query(sql) + client.close() + assert trace_id in res.traces[0], "Expect trace ID in DoGet response." + +use_middleware_trace_header() +``` + +{{% /code-placeholders %}} + +{{% /tab-content %}} +{{< /tabs-wrapper >}} + +Replace the following: + +- {{% code-placeholder-key %}}`DATABASE_NAME`{{% /code-placeholder-key %}}: your {{% product-name %}} database +- {{% code-placeholder-key %}}`DATABASE_TOKEN`{{% /code-placeholder-key %}}: a [database token](/influxdb/cloud-dedicated/admin/tokens/) with sufficient permissions to the specified database + +{{% note %}} +Store or log your query trace ID to ensure you can provide it to InfluxData Support for troubleshooting. +{{% /note %}} + +After you run your query with tracing enabled, do the following: + +- Remove the tracing header from subsequent runs of the query (to [avoid unnecessary tracing](#avoid-unnecessary-tracing)). +- Provide the trace ID in a request to InfluxData Support. + +### Retrieve system information for a query + +If you enable trace logging for a query, the `trace-id` appears in the [`system.queries` table](/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/system-information). diff --git a/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/troubleshoot.md b/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/troubleshoot.md new file mode 100644 index 000000000..b0ec24bfa --- /dev/null +++ b/content/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/troubleshoot.md @@ -0,0 +1,44 @@ +--- +title: Troubleshoot queries +description: > + Troubleshoot SQL and InfluxQL queries in InfluxDB. +weight: 201 +menu: + influxdb_cloud_dedicated: + name: Troubleshoot queries + parent: Troubleshoot and optimize queries +influxdb/cloud-dedicated/tags: [query, performance, observability, errors, sql, influxql] +related: + - /influxdb/cloud-dedicated/query-data/sql/ + - /influxdb/cloud-dedicated/query-data/influxql/ + - /influxdb/cloud-dedicated/reference/client-libraries/v3/ +aliases: + - /influxdb/cloud-dedicated/query-data/execute-queries/troubleshoot/ +--- + +Troubleshoot SQL and InfluxQL queries that return unexpected results. + +- [Why doesn't my query return data?](#why-doesnt-my-query-return-data) +- [Optimize slow or expensive queries](#optimize-slow-or-expensive-queries) + +## Why doesn't my query return data? + +If a query doesn't return any data, it might be due to the following: + +- Your data falls outside the time range (or other conditions) in the query--for example, the InfluxQL `SHOW TAG VALUES` command uses a default time range of 1 day. +- The query (InfluxDB server) timed out. +- The query client timed out. + +If a query times out or returns an error, it might be due to the following: + +- a bad request +- a server or network problem +- it queries too much data + +[Understand Arrow Flight responses](/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/flight-responses/) and error messages for queries. + +## Optimize slow or expensive queries + +If a query is slow or uses too many compute resources, limit the amount of data that it queries. + +See how to [optimize queries](/influxdb/cloud-dedicated/query-data/troubleshoot-and-optimize/optimize-queries/) and use tools to view runtime metrics, identify bottlenecks, and debug queries. diff --git a/content/influxdb/cloud-dedicated/reference/client-libraries/v2/arduino.md b/content/influxdb/cloud-dedicated/reference/client-libraries/v2/arduino.md index 3f42ca94d..57a03df3e 100644 --- a/content/influxdb/cloud-dedicated/reference/client-libraries/v2/arduino.md +++ b/content/influxdb/cloud-dedicated/reference/client-libraries/v2/arduino.md @@ -23,8 +23,8 @@ prepend: [**Compare tools you can use**](/influxdb/cloud-dedicated/get-started/#tools-to-use) to interact with {{% product-name %}}. --- -Arduino is an open-source hardware and software platform used for building electronics projects. +Arduino is an open source hardware and software platform used for building electronics projects. The documentation for this client library is available on GitHub. -Arduino InfluxDB client \ No newline at end of file +Arduino InfluxDB client diff --git a/content/influxdb/cloud-dedicated/reference/client-libraries/v2/kotlin.md b/content/influxdb/cloud-dedicated/reference/client-libraries/v2/kotlin.md index 0a8e1c2c9..2d383dfe6 100644 --- a/content/influxdb/cloud-dedicated/reference/client-libraries/v2/kotlin.md +++ b/content/influxdb/cloud-dedicated/reference/client-libraries/v2/kotlin.md @@ -22,8 +22,8 @@ prepend: [**Compare tools you can use**](/influxdb/cloud-dedicated/get-started/#tools-to-use) to interact with {{% product-name %}}. --- -Kotlin is an open-source programming language that runs on the Java Virtual Machine (JVM). +Kotlin is an open source programming language that runs on the Java Virtual Machine (JVM). The documentation for this client library is available on GitHub. -Kotlin InfluxDB client \ No newline at end of file +Kotlin InfluxDB client diff --git a/content/influxdb/cloud-dedicated/reference/glossary.md b/content/influxdb/cloud-dedicated/reference/glossary.md index bca35dfb2..1034d43b3 100644 --- a/content/influxdb/cloud-dedicated/reference/glossary.md +++ b/content/influxdb/cloud-dedicated/reference/glossary.md @@ -77,7 +77,7 @@ InfluxData typically recommends batch sizes of 5,000-10,000 points. In some use cases, performance may improve with significantly smaller or larger batches. Related entries: -[line protocol](#line-protocol), +[line protocol](#line-protocol-lp), [point](#point) ### batch size @@ -260,7 +260,7 @@ Aggregating high resolution data into lower resolution data to preserve disk spa ### duration -A data type that represents a duration of time (1s, 1m, 1h, 1d). +A data type that represents a duration of time--for example, `1s`, `1m`, `1h`, `1d`. Retention periods are set using durations. Related entries: @@ -291,7 +291,7 @@ WHERE A key-value pair in InfluxDB's data structure that records a data value. Generally, field values change over time. -Fields are required in InfluxDB's data structure. +Fields are required in InfluxDB's data structure. Related entries: [field key](#field-key), @@ -337,9 +337,6 @@ Related entries: A file block is a fixed-length chunk of data read into memory when requested by an application. -Related entries: -[block](#block) - ### float A real number written with a decimal point dividing the integer and fractional parts (`1.0`, `3.14`, `-20.1`). @@ -408,7 +405,6 @@ Related entries: [measurement](#measurement), [tag key](#tag-key), - ### influx `influx` is a command line interface (CLI) that interacts with the InfluxDB v1.x and v2.x server. @@ -426,7 +422,7 @@ and other required processes. ### InfluxDB -An open-source time series database (TSDB) developed by InfluxData. +An open source time series database (TSDB) developed by InfluxData. Written in Go and optimized for fast, high-availability storage and retrieval of time series data in fields such as operations monitoring, application metrics, Internet of Things sensor data, and real-time analytics. @@ -467,7 +463,7 @@ The IOx storage engine (InfluxDB v3 storage engine) is a real-time, columnar database optimized for time series data built in Rust on top of [Apache Arrow](https://arrow.apache.org/) and [DataFusion](https://arrow.apache.org/datafusion/user-guide/introduction.html). -IOx replaces the [TSM](#tsm) storage engine. +IOx replaces the [TSM (Time Structured Merge tree)](#tsm-time-structured-merge-tree) storage engine. ## J @@ -497,11 +493,13 @@ and array data types. ### keyword A keyword is reserved by a program because it has special meaning. -Every programming language has a set of keywords (reserved names) that cannot be used as an identifier. +Every programming language has a set of keywords (reserved names) that cannot be used as identifiers--for example, +you can't use `SELECT` (an SQL keyword) as a variable name in an SQL query. -See a list of [SQL keywords](/influxdb/cloud-dedicated/reference/sql/#keywords). +See keyword lists: - +- [SQL keywords](/influxdb/cloud-dedicated/reference/sql/#keywords) +- [InfluxQL keywords](/influxdb/cloud-dedicated/reference/influxql/#keywords) ## L @@ -571,7 +569,6 @@ Related entries: [cluster](#cluster), [server](#server) - ### now The local server's nanosecond timestamp. @@ -612,7 +609,7 @@ Owners have read/write permissions. Users can have owner roles for databases and other resources. Role permissions are separate from API token permissions. For additional -information on API tokens, see [token](#tokens). +information on API tokens, see [token](#token). ### output plugin @@ -719,6 +716,15 @@ An InfluxDB query returns time series data. See [Query data in InfluxDB](/influxdb/cloud-dedicated/query-data/). +### query plan + +A sequence of steps (_nodes_) that the InfluxDB Querier devises and executes to calculate the result of the query in the least amount of time. +A _logical plan_ is a high level representation of a query and doesn't consider cluster configuration or data organization. +A _physical plan_ represents the query execution plan and data flow through plan nodes that read (_scan_), deduplicate, merge, filter, and sort data. +A physical plan is optimized for the cluster configuration and data organization. + +See [Query plans](/influxdb/cloud-dedicated/reference/internals/query-plans/). + ## R ### REPL @@ -744,8 +750,7 @@ relative to [now](#now). The minimum retention period is **one hour**. Related entries: -[bucket](#bucket), -[shard group duration](#shard-group-duration) +[bucket](#bucket) ### retention policy (RP) @@ -786,6 +791,18 @@ Related entries: [timestamp](#timestamp), [unix timestamp](#unix-timestamp) +### row + +A row in a [table](#table) represents a specific record or instance of data. +[Column](#column) values in a row represent specific attributes or properties of the instance. +Each row has a [primary key](/#primary-key) that makes the row unique from other rows in the table. + +Related entries: +[column](#column), +[primary key](#primary-key), +[series](#series), +[table](#table) + ## S ### schema @@ -804,8 +821,8 @@ Related entries: ### secret -Secrets are key-value pairs that contain information you want to control access -o, such as API keys, passwords, or certificates. +Secrets are key-value pairs that contain information you want to control access +to, such as API keys, passwords, or certificates. ### selector @@ -942,7 +959,6 @@ Related entries: The key of a tag key-value pair. Tag keys are strings and store metadata. - Related entries: [field key](#field-key), [tag](#tag), @@ -1018,6 +1034,14 @@ There are different types of API tokens: Related entries: [Manage token](/influxdb/cloud-dedicated/admin/tokens/) +### transformation + +Data transformation refers to the process of converting or modifying input data from one format, value, or structure to another. + +InfluxQL [transformation functions](/influxdb/cloud-dedicated/reference/influxql/functions/transformations/) modify and return values in each row of queried data, but do not return an aggregated value across those rows. + +Related entries: [aggregate](#aggregate), [function](#function), [selector](#selector) + ### TSM (Time Structured Merge tree) The InfluxDB v1 and v2 data storage format that allows greater compaction and @@ -1078,7 +1102,7 @@ InfluxDB users are granted permission to access to InfluxDB. ### values per second -The preferred measurement of the rate at which data are persisted to InfluxDB. +The preferred measurement of the rate at which data is persisted to InfluxDB. Write speeds are generally quoted in values per second. To calculate the values per second rate, multiply the number of points written diff --git a/content/influxdb/cloud-dedicated/reference/internals/query-plan.md b/content/influxdb/cloud-dedicated/reference/internals/query-plan.md new file mode 100644 index 000000000..6d3c196b3 --- /dev/null +++ b/content/influxdb/cloud-dedicated/reference/internals/query-plan.md @@ -0,0 +1,392 @@ +--- +title: Query plans +description: > + A query plan is a sequence of steps that the InfluxDB Querier devises and executes to calculate the result of a query in the least amount of time. + InfluxDB query plans include DataFusion and InfluxDB logical plan and execution plan nodes for scanning, deduplicating, filtering, merging, and sorting data. +weight: 201 +menu: + influxdb_cloud_dedicated: + name: Query plans + parent: InfluxDB internals +influxdb/cloud-dedicated/tags: [query, sql, influxql] +related: + - /influxdb/cloud-dedicated/query-data/sql/ + - /influxdb/cloud-dedicated/query-data/influxql/ + - /influxdb/cloud-dedicated/query-data/execute-queries/analyze-query-plan/ + - /influxdb/cloud-dedicated/query-data/execute-queries/troubleshoot/ + - /influxdb/cloud-dedicated/reference/internals/storage-engine/ +--- + +A query plan is a sequence of steps that the InfluxDB v3 [Querier](/influxdb/cloud-dedicated/reference/internals/storage-engine/#querier) devises and executes to calculate the result of a query. +The Querier uses DataFusion and Arrow to build and execute query plans +that call DataFusion and InfluxDB-specific operators that read data from the [Object store](/influxdb/cloud-dedicated/reference/internals/storage-engine/#object-store), and the [Ingester](/influxdb/cloud-dedicated/reference/internals/storage-engine/#ingester), and apply query transformations, such as deduplicating, filtering, aggregating, merging, projecting, and sorting to calculate the final result. + +Like many other databases, the [Querier](/influxdb/cloud-dedicated/reference/internals/storage-engine/#querier) contains a Query Optimizer. +After it parses an incoming query, the [Querier](/influxdb/cloud-dedicated/reference/internals/storage-engine/#querier) builds a _logical plan_--a sequence of high-level steps such as scanning, filtering, and sorting, required for the query. +Following the logical plan, the [Querier](/influxdb/cloud-dedicated/reference/internals/storage-engine/#querier) then builds the optimal _physical plan_ to calculate the correct result in the least amount of time. +The plan takes advantage of data partitioning by the [Ingester](/influxdb/cloud-dedicated/reference/internals/storage-engine/#ingester) to parallelize plan operations and prune unnecessary data before executing the plan. +The [Querier](/influxdb/cloud-dedicated/reference/internals/storage-engine/#querier) also applies common techniques of predicate and projection pushdown to further prune data as early as possible. + +- [Display syntax](#display-syntax) + - [Example logical and physical plan](#example-logical-and-physical-plan) +- [Data flow](#data-flow) +- [Logical plan](#logical-plan) +- [`LogicalPlan` nodes](#logicalplan-nodes) + - [`TableScan`](#tablescan) + - [`Projection`](#projection) + - [`Filter`](#filter) + - [`Sort`](#sort) +- [Physical plan](#physical-plan) +- [`ExecutionPlan` nodes](#executionplan-nodes) + - [`DeduplicateExec`](#deduplicateexec) + - [`EmptyExec`](#emptyexec) + - [`FilterExec`](#filterexec) + - [`ParquetExec`](#parquetexec) + - [`ProjectionExec`](#projectionexec) + - [`RecordBatchesExec`](#recordbatchesexec) + - [`SortExec`](#sortexec) + - [`SortPreservingMergeExec`](#sortpreservingmergeexec) +- [Overlapping data and deduplication](#overlapping-data-and-deduplication) + - [Example of overlapping data](#example-of-overlapping-data) +- [DataFusion query plans](#datafusion-query-plans) + +## Display syntax + +[Logical](#logical-plan) and [physical query plans](#physical-plan) are represented (for example, in an `EXPLAIN` report) in _tree syntax_. + +- Each plan is represented as an upside-down tree composed of _nodes_. +- A parent node awaits the output of its child nodes. +- Data flows up from the bottom innermost nodes of the tree to the outermost _root node_ at the top. + +### Example logical and physical plan + +The following query generates an `EXPLAIN` report that includes a logical and a physical plan: + +```sql +EXPLAIN SELECT city, min_temp, time FROM h2o ORDER BY city ASC, time DESC; +``` + +The output is the following: + +#### Figure 1. EXPLAIN report + +```sql +| plan_type | plan | ++---------------+--------------------------------------------------------------------------+ +| logical_plan | Sort: h2o.city ASC NULLS LAST, h2o.time DESC NULLS FIRST | +| | TableScan: h2o projection=[city, min_temp, time] | +| physical_plan | SortPreservingMergeExec: [city@0 ASC NULLS LAST,time@2 DESC] | +| | UnionExec | +| | SortExec: expr=[city@0 ASC NULLS LAST,time@2 DESC] | +| | ParquetExec: file_groups={...}, projection=[city, min_temp, time] | +| | SortExec: expr=[city@0 ASC NULLS LAST,time@2 DESC] | +| | ParquetExec: file_groups={...}, projection=[city, min_temp, time] | +| | | +``` + +{{% caption %}} +Output from `EXPLAIN SELECT city, min_temp, time FROM h2o ORDER BY city ASC, time DESC;` +{{% /caption %}} + +The leaf nodes in the [Figure 1](#figure-1-explain-report) physical plan are parallel `ParquetExec` nodes: + +```text + ParquetExec: file_groups={...}, projection=[city, min_temp, time] +... + ParquetExec: file_groups={...}, projection=[city, min_temp, time] +``` + +## Data flow + +A [physical plan](#physical-plan) node represents a specific implementation of `ExecutionPlan` that receives an input stream, applies expressions for filtering and sorting, and then yields an output stream to its parent node. + +The following diagram shows the data flow and sequence of `ExecutionPlan` nodes in the [Figure 1](#figure-1-explain-report) physical plan: + + +{{< html-diagram/query-plan >}} + + +{{% product-name %}} includes the following plan expressions: + +## Logical plan + +A logical plan for a query: + +- is a high-level plan that expresses the "intent" of a query and the steps required for calculating the result. +- requires information about the data schema +- is independent of the [physical execution](#physical-plan), cluster configuration, data source (Ingester or Object store), or how data is organized or partitioned +- is displayed as a tree of [DataFusion `LogicalPlan` nodes](#logical-plan-nodes) + +## `LogicalPlan` nodes + +Each node in an {{% product-name %}} logical plan tree represents a [`LogicalPlan` implementation](https://docs.rs/datafusion/latest/datafusion/logical_expr/enum.LogicalPlan.html#variants) that receives criteria extracted from the query and applies relational operators and optimizations for transforming input data to an output table. + +The following are some `LogicalPlan` nodes used in InfluxDB logical plans. + +### `TableScan` + +[`Tablescan`](https://docs.rs/datafusion/latest/datafusion/logical_expr/struct.TableScan.html) retrieves rows from a table provider by reference or from the context. + +### `Projection` + +[`Projection`](https://docs.rs/datafusion/latest/datafusion/logical_expr/struct.Projection.html) evaluates an arbitrary list of expressions on the input; equivalent to an SQL `SELECT` statement with an expression list. + +### `Filter` + +[`Filter`](https://docs.rs/datafusion/latest/datafusion/logical_expr/struct.Filter.html) filters rows from the input that do not satisfy the specified expression; equivalent to an SQL `WHERE` clause with a predicate expression. + +### `Sort` + +[`Sort`](https://docs.rs/datafusion/latest/datafusion/logical_expr/struct.Sort.html) sorts the input according to a list of sort expressions; used to implement SQL `ORDER BY`. + +For details and a list of `LogicalPlan` implementations, see [`Enum datafusion::logical_expr::LogicalPlan` Variants](https://docs.rs/datafusion/latest/datafusion/logical_expr/enum.LogicalPlan.html#variants) in the DataFusion documentation. + +## Physical plan + +A physical plan, or _execution plan_, for a query: + +- is an optimized plan that derives from the [logical plan](#logical-plan) and contains the low-level steps for query execution. +- considers the cluster configuration (for example, CPU and memory allocation) and data organization (for example: partitions, the number of files, and whether files overlap)--for example: + - If you run the same query with the same data on different clusters with different configurations, each cluster may generate a different physical plan for the query. + - If you run the same query on the same cluster at different times, the physical plan may differ each time, depending on the data at query time. +- if generated using `ANALYZE`, includes runtime metrics sampled during query execution +- is displayed as a tree of [`ExecutionPlan` nodes](#execution-plan-nodes) + +## `ExecutionPlan` nodes + +Each node in an {{% product-name %}} physical plan represents a call to a specific implementation of the [DataFusion `ExecutionPlan`](https://docs.rs/datafusion/latest/datafusion/physical_plan/trait.ExecutionPlan.html) +that receives input data, query criteria expressions, and an output schema. + +The following are some `ExecutionPlan` nodes used in InfluxDB physical plans. + +### `DeduplicateExec` + +InfluxDB `DeduplicateExec` takes an input stream of `RecordBatch` sorted on `sort_key` and applies InfluxDB-specific deduplication logic. +The output is dependent on the order of the input rows that have the same key. + +### `EmptyExec` + +DataFusion [`EmptyExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/empty/struct.EmptyExec.html) is an execution plan for an empty relation and indicates that the table doesn't contain data for the time range of the query. + +### `FilterExec` + +The execution plan for the [`Filter`](#filter) `LogicalPlan`. + +DataFusion [`FilterExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/filter/struct.FilterExec.html) evaluates a boolean predicate against all input batches to determine which rows to include in the output batches. + +### `ParquetExec` + +DataFusion [`ParquetExec`](https://docs.rs/datafusion/latest/datafusion/datasource/physical_plan/parquet/struct.ParquetExec.html) scans one or more Parquet partitions. + +#### `ParquetExec` expressions + +##### `file_groups` + +A _file group_ is a list of files to scan. +Files are referenced by path: + +- `1/1/b862a7e9b.../243db601-....parquet` +- `1/1/b862a7e9b.../f5fb7c7d-....parquet` + +In InfluxDB v3, the path structure represents how data is organized. + +A path has the following structure: + +```text +///.parquet + 1 / 1 /b862a7e9b329ee6a4.../243db601-f3f1-4....parquet +``` + +- `namespace_id`: the namespace (database) being queried +- `table_id`: the table (measurement) being queried +- `partition_hash_id`: the partition this file belongs to. +You can count partition IDs to find how many partitions the query reads. +- `uuid_of_the_file`: the file identifier. + +`ParquetExec` processes groups in parallel and reads the files in each group sequentially. + +##### `projection` + +`projection` lists the table columns that the query plan needs to read to execute the query. +The parameter name `projection` refers to _projection pushdown_, the action of filtering columns. + +Consider the following sample data that contains many columns: + +```text +h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 600 +``` + +| table | state | city | min_temp | max_temp | area | time | +|:-----:|:-----:|:----:|:--------:|:--------:|:----:|:----:| +| h2o | CA | SF | 68.4 | 85.7 | 500u | 600 | + +However, the following SQL query specifies only three columns (`city`, `state`, and `time`): + +```sql +SELECT city, count(1) +FROM h2o +WHERE time >= to_timestamp(200) AND time < to_timestamp(700) + AND state = 'MA' +GROUP BY city +ORDER BY city ASC; +``` + +When processing the query, the [Querier](/influxdb/cloud-dedicated/reference/internals/storage-engine/#querier) specifies the three required columns in the projection and the projection is "pushed down" to leaf nodes--columns not specified are pruned as early as possible during query execution. + +```text +projection=[city, state, time] +``` + +##### `output_ordering` + +`output_ordering` specifies the sort order for the output. +The Querier specifies `output_ordering` if the output should be ordered and if the [Querier](/influxdb/cloud-dedicated/reference/internals/storage-engine/#querier) knows the order. + +When storing data to Parquet files, InfluxDB sorts the data to improve storage compression and query efficiency and the planner tries to preserve that order for as long as possible. +Generally, the `output_ordering` value that `ParquetExec` receives is the ordering (or a subset of the ordering) of stored data. + +_By design, [`RecordBatchesExec`](#recordbatchesexec) data isn't sorted._ + +In the following example, the query planner specifies the output sort order `state ASC, city ASC, time ASC,`: + +```text +output_ordering=[state@2 ASC, city@1 ASC, time@3 ASC, __chunk_order@0 ASC] +``` + +##### `predicate` + +`predicate` is the data filter specified in the query and used for row filtering when scanning Parquet files. + +For example, given the following SQL query: + +```sql +SELECT city, count(1) +FROM h2o +WHERE time >= to_timestamp(200) AND time < to_timestamp(700) + AND state = 'MA' +GROUP BY city +ORDER BY city ASC; +``` + +The `predicate` value is the boolean expression in the `WHERE` statement: + +```text +predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA +``` + +##### `pruning predicate` + +`pruning_predicate` is created from the [`predicate`](#predicate) value and is used for pruning data and files from the chosen partitions. + +For example, given the following `predicate` parsed from the SQL: + +```text +predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA, +``` + +The Querier creates the following `pruning_predicate`: + +```text +pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 +``` + +The default filters files by `time`. + +_Before the physical plan is generated, an additional `partition pruning` step uses predicates on partitioning columns to prune partitions._ + +### `ProjectionExec` + +DataFusion [`ProjectionExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/projection/struct.ProjectionExec.html) evaluates an arbitrary list of expressions on the input; the execution plan for the [`Projection`](#projection) `LogicalPlan`. + +### `RecordBatchesExec` + +The InfluxDB `RecordBatchesExec` implementation retrieves and scans recently written, yet-to-be-persisted, data from the InfluxDB v3 [Ingester](/influxdb/cloud-dedicated/reference/internals/storage-engine/#ingester). + +When generating the plan, the [Querier](/influxdb/cloud-dedicated/reference/internals/storage-engine/#querier) sends the query criteria, such as database, table, and columns, to the [Ingester](/influxdb/cloud-dedicated/reference/internals/storage-engine/#ingester) to retrieve data not yet persisted to Parquet files. +If the [Ingester](/influxdb/cloud-dedicated/reference/internals/storage-engine/#ingester) has data that meets the criteria (the chunk size is non-zero), then the plan includes `RecordBatchesExec`. + +#### `RecordBatchesExec` attributes + +##### `chunks` + +`chunks` is the number of data chunks from the [Ingester](/influxdb/cloud-dedicated/reference/internals/storage-engine/#ingester). +Often one (`1`), but it can be many. + +##### `projection` + +`projection` specifies a list of columns to read and output. + +`__chunk_order` in a list of columns is an InfluxDB-generated column used to keep the chunks and files ordered for deduplication--for example: + +```text +projection=[__chunk_order, city, state, time] +``` + +For details and other DataFusion `ExecutionPlan` implementations, see [`Struct datafusion::datasource::physical_plan` implementors](https://docs.rs/datafusion/latest/datafusion/physical_plan/trait.ExecutionPlan.html) in the DataFusion documentation. + +### `SortExec` + +The execution plan for the [`Sort`](#sort) `LogicalPlan`. + +DataFusion [`SortExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/sorts/sort/struct.SortExec.html) supports sorting datasets that are larger than the memory allotted by the memory manager, by spilling to disk. + +### `SortPreservingMergeExec` + +DataFusion [`SortPreservingMergeExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/sorts/sort_preserving_merge/struct.SortPreservingMergeExec.html) takes an input execution plan and a list of sort expressions and, provided each partition of the input plan is sorted with respect to these sort expressions, yields a single partition sorted with respect to them. + +#### `UnionExec` + +DataFusion [`UnionExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/union/struct.UnionExec.html) is the `UNION ALL` execution plan for combining multiple inputs that have the same schema. +`UnionExec` concatenates the partitions and does not mix or copy data within or across partitions. + +## Overlapping data and deduplication + +_Overlapping data_ refers to files or batches in which the time ranges (represented by timestamps) intersect. +Two _chunks_ of data overlap if both chunks contain data for the same portion of time. + +### Example of overlapping data + +For example, the following chunks represent line protocol written to InfluxDB: + +```text +// Chunk 4: stored parquet file +// - time range: 400-600 +// - no duplicates in its own chunk +// - overlaps chunk 3 +[ + "h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 600", + "h2o,state=CA,city=SJ min_temp=69.5,max_temp=89.2 600", // duplicates row 3 in chunk 5 + "h2o,state=MA,city=Bedford max_temp=80.75,area=742u 400", // overlaps chunk 3 + "h2o,state=MA,city=Boston min_temp=65.40,max_temp=82.67 400", // overlaps chunk 3 +], + +// Chunk 5: Ingester data +// - time range: 550-700 +// - overlaps & duplicates data in chunk 4 +[ +"h2o,state=MA,city=Bedford max_temp=88.75,area=742u 600", // overlaps chunk 4 +"h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 650", +"h2o,state=CA,city=SJ min_temp=68.5,max_temp=90.0 600", // duplicates row 2 in chunk 4 +"h2o,state=CA,city=SJ min_temp=75.5,max_temp=84.08 700", +"h2o,state=MA,city=Boston min_temp=67.4 550", // overlaps chunk 4 +] +``` + +- `Chunk 4` spans the time range `400-600` and represents data persisted to a Parquet file in the [Object store](/influxdb/cloud-dedicated/reference/internals/storage-engine/#object-store). +- `Chunk 5` spans the time range `550-700` and represents yet-to-be persisted data from the [Ingester](/influxdb/cloud-dedicated/reference/internals/storage-engine/#ingester). +- The chunks overlap the range `550-600`. + +If data overlaps at query time, the [Querier](/influxdb/cloud-dedicated/reference/internals/storage-engine/#querier) must include the _deduplication_ process in the query plan, which uses the same multi-column sort-merge operators used by the [Ingester](/influxdb/cloud-dedicated/reference/internals/storage-engine/#ingester). +Compared to an ingestion plan that uses sort-merge operators, a query plan is more complex and ensures that data streams through the plan after deduplication. + +Because sort-merge operations used in deduplication have a non-trivial execution cost, InfluxDB v3 tries to avoid the need for deduplication. +Due to how InfluxDB organizes data, a Parquet file never contains duplicates of the data it stores; only overlapped data can contain duplicates. +During compaction, the [Compactor](/influxdb/cloud-dedicated/reference/internals/storage-engine/#compactor) sorts stored data to reduce overlaps and optimize query performance. +For data that doesn't have overlaps, the [Querier](/influxdb/cloud-dedicated/reference/internals/storage-engine/#querier) doesn't need to include the deduplication process and the query plan can further distribute non-overlapping data for parallel processing. + +## DataFusion query plans + +For more information about DataFusion query plans and the DataFusion API used in InfluxDB v3, see the following: + +- [Query Planning and Execution Overview](https://docs.rs/datafusion/latest/datafusion/index.html#query-planning-and-execution-overview) in the DataFusion documentation. +- [Plan representations](https://docs.rs/datafusion/latest/datafusion/#plan-representations) in the DataFusion documentation. diff --git a/content/influxdb/cloud-dedicated/reference/sql/explain.md b/content/influxdb/cloud-dedicated/reference/sql/explain.md index 128717124..fdbf0bae8 100644 --- a/content/influxdb/cloud-dedicated/reference/sql/explain.md +++ b/content/influxdb/cloud-dedicated/reference/sql/explain.md @@ -1,31 +1,41 @@ --- title: EXPLAIN command description: > - The `EXPLAIN` command shows the logical and physical execution plan for the - specified SQL statement. + The `EXPLAIN` command returns the logical and physical execution plans for the specified SQL statement. menu: influxdb_cloud_dedicated: name: EXPLAIN command parent: SQL reference weight: 207 +related: + - /influxdb/cloud-dedicated/reference/internals/query-plan/ + - /influxdb/cloud-dedicated/query-data/execute-queries/analyze-query-plan/ + - /influxdb/cloud-dedicated/query-data/execute-queries/troubleshoot/ --- -The `EXPLAIN` command returns the logical and physical execution plan for the +The `EXPLAIN` command returns the [logical plan](/influxdb/cloud-dedicated/reference/internals/query-plan/#logical-plan) and the [physical plan](/influxdb/cloud-dedicated/reference/internals/query-plan/#physical-plan) for the specified SQL statement. ```sql EXPLAIN [ANALYZE] [VERBOSE] statement ``` -- [EXPLAIN](#explain) -- [EXPLAIN ANALYZE](#explain-analyze) +- [`EXPLAIN`](#explain) + - [Example `EXPLAIN`](#example-explain) +- [`EXPLAIN ANALYZE`](#explain-analyze) + - [Example `EXPLAIN ANALYZE`](#example-explain-analyze) +- [`EXPLAIN ANALYZE VERBOSE`](#explain-analyze-verbose) + - [Example `EXPLAIN ANALYZE VERBOSE`](#example-explain-analyze-verbose) -## EXPLAIN +## `EXPLAIN` -Returns the execution plan of a statement. +Returns the logical plan and physical (execution) plan of a statement. To output more details, use `EXPLAIN VERBOSE`. -##### Example EXPLAIN ANALYZE +`EXPLAIN` doesn't execute the statement. +To execute the statement and view runtime metrics, use [`EXPLAIN ANALYZE`](#explain-analyze). + +### Example `EXPLAIN` ```sql EXPLAIN @@ -39,20 +49,30 @@ GROUP BY room {{< expand-wrapper >}} {{% expand "View `EXPLAIN` example output" %}} -| plan_type | plan | -| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| logical_plan | Projection: home.room, AVG(home.temp) AS temp Aggregate: groupBy=[[home.room]], aggr=[[AVG(home.temp)]] TableScan: home projection=[room, temp] | -| physical_plan | ProjectionExec: expr=[room@0 as room, AVG(home.temp)@1 as temp] AggregateExec: mode=FinalPartitioned, gby=[room@0 as room], aggr=[AVG(home.temp)] CoalesceBatchesExec: target_batch_size=8192 RepartitionExec: partitioning=Hash([Column { name: "room", index: 0 }], 4), input_partitions=4 RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1 AggregateExec: mode=Partial, gby=[room@0 as room], aggr=[AVG(home.temp)] ParquetExec: limit=None, partitions={1 group: [[136/316/1120/1ede0031-e86e-06e5-12ba-b8e6fd76a202.parquet]]}, projection=[room, temp] | +| | plan_type | plan | +|---:|:--------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0 | logical_plan | Projection: home.room, AVG(home.temp) AS temp | +| | | Aggregate: groupBy=[[home.room]], aggr=[[AVG(home.temp)]] | +| | | TableScan: home projection=[room, temp] | +| 1 | physical_plan | ProjectionExec: expr=[room@0 as room, AVG(home.temp)@1 as temp] | +| | | AggregateExec: mode=FinalPartitioned, gby=[room@0 as room], aggr=[AVG(home.temp)] | +| | | CoalesceBatchesExec: target_batch_size=8192 | +| | | RepartitionExec: partitioning=Hash([room@0], 8), input_partitions=8 | +| | | AggregateExec: mode=Partial, gby=[room@0 as room], aggr=[AVG(home.temp)] | +| | | ParquetExec: file_groups={8 groups: [[70434/116281/404d73cea0236530ea94f5470701eb814a8f0565c0e4bef5a2d2e33dfbfc3567/1be334e8-0af8-00da-2615-f67cd4be90f7.parquet, 70434/116281/b7a9e7c57fbfc3bba9427e4b3e35c89e001e2e618b0c7eb9feb4d50a3932f4db/d29370d4-262f-0d32-2459-fe7b099f682f.parquet], [70434/116281/c14418ba28a22a3abb693a1cb326a63b62dc611aec58c9bed438fdafd3bc5882/8b29ae98-761f-0550-2fe4-ee77503658e9.parquet], [70434/116281/fa677477eed622ae8123da1251aa7c351f801e2ee2f0bc28c0fe3002a30b3563/65bb4dc3-04e1-0e02-107a-90cee83c51b0.parquet], [70434/116281/db162bdd30261019960dd70da182e6ebd270284569ecfb5deffea7e65baa0df9/2505e079-67c5-06d9-3ede-89aca542dd18.parquet], [70434/116281/0c025dcccae8691f5fd70b0f131eea4ca6fafb95a02f90a3dc7bb015efd3ab4f/3f3e44c3-b71e-0ca4-3dc7-8b2f75b9ff86.parquet], ...]}, projection=[room, temp] | {{% /expand %}} {{< /expand-wrapper >}} -## EXPLAIN ANALYZE +## `EXPLAIN ANALYZE` -Returns the execution plan and metrics of a statement. -To output more information, use `EXPLAIN ANALYZE VERBOSE`. +Executes a statement and returns the execution plan and runtime metrics of the statement. +The report includes the [logical plan](/influxdb/cloud-dedicated/reference/internals/query-plan/#logical-plan) and the [physical plan](/influxdb/cloud-dedicated/reference/internals/query-plan/#physical-plan) annotated with execution counters, number of rows produced, and runtime metrics sampled during the query execution. -##### Example EXPLAIN ANALYZE +If the plan requires reading lots of data files, `EXPLAIN` and `EXPLAIN ANALYZE` may truncate the list of files in the report. +To output more information, including intermediate plans and paths for all scanned Parquet files, use [`EXPLAIN ANALYZE VERBOSE`](#explain-analyze-verbose). + +### Example `EXPLAIN ANALYZE` ```sql EXPLAIN ANALYZE @@ -60,15 +80,44 @@ SELECT room, avg(temp) AS temp FROM home +WHERE time >= '2023-01-01' AND time <= '2023-12-31' GROUP BY room ``` {{< expand-wrapper >}} {{% expand "View `EXPLAIN ANALYZE` example output" %}} -| plan_type | plan | -| :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Plan with Metrics | CoalescePartitionsExec, metrics=[output_rows=2, elapsed_compute=8.892µs, spill_count=0, spilled_bytes=0, mem_used=0] ProjectionExec: expr=[room@0 as room, AVG(home.temp)@1 as temp], metrics=[output_rows=2, elapsed_compute=3.608µs, spill_count=0, spilled_bytes=0, mem_used=0] AggregateExec: mode=FinalPartitioned, gby=[room@0 as room], aggr=[AVG(home.temp)], metrics=[output_rows=2, elapsed_compute=121.771µs, spill_count=0, spilled_bytes=0, mem_used=0] CoalesceBatchesExec: target_batch_size=8192, metrics=[output_rows=2, elapsed_compute=23.711µs, spill_count=0, spilled_bytes=0, mem_used=0] RepartitionExec: partitioning=Hash([Column { name: "room", index: 0 }], 4), input_partitions=4, metrics=[repart_time=25.117µs, fetch_time=1.614597ms, send_time=6.705µs] RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1, metrics=[repart_time=1ns, fetch_time=319.754µs, send_time=2.067µs] AggregateExec: mode=Partial, gby=[room@0 as room], aggr=[AVG(home.temp)], metrics=[output_rows=2, elapsed_compute=75.615µs, spill_count=0, spilled_bytes=0, mem_used=0] ParquetExec: limit=None, partitions={1 group: [[136/316/1120/1ede0031-e86e-06e5-12ba-b8e6fd76a202.parquet]]}, projection=[room, temp], metrics=[output_rows=26, elapsed_compute=1ns, spill_count=0, spilled_bytes=0, mem_used=0, pushdown_rows_filtered=0, bytes_scanned=290, row_groups_pruned=0, num_predicate_creation_errors=0, predicate_evaluation_errors=0, page_index_rows_filtered=0, time_elapsed_opening=100.37µs, page_index_eval_time=2ns, time_elapsed_scanning_total=157.086µs, time_elapsed_processing=226.644µs, pushdown_eval_time=2ns, time_elapsed_scanning_until_data=116.875µs] | +| | plan_type | plan | +|---:|:------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0 | Plan with Metrics | ProjectionExec: expr=[room@0 as room, AVG(home.temp)@1 as temp], metrics=[output_rows=2, elapsed_compute=4.768µs] | +| | | AggregateExec: mode=FinalPartitioned, gby=[room@0 as room], aggr=[AVG(home.temp)], ordering_mode=Sorted, metrics=[output_rows=2, elapsed_compute=140.405µs] | +| | | CoalesceBatchesExec: target_batch_size=8192, metrics=[output_rows=2, elapsed_compute=6.821µs] | +| | | RepartitionExec: partitioning=Hash([room@0], 8), input_partitions=8, preserve_order=true, sort_exprs=room@0 ASC, metrics=[output_rows=2, elapsed_compute=18.408µs, repart_time=59.698µs, fetch_time=1.057882762s, send_time=5.83µs] | +| | | AggregateExec: mode=Partial, gby=[room@0 as room], aggr=[AVG(home.temp)], ordering_mode=Sorted, metrics=[output_rows=2, elapsed_compute=137.577µs] | +| | | RepartitionExec: partitioning=RoundRobinBatch(8), input_partitions=6, preserve_order=true, sort_exprs=room@0 ASC, metrics=[output_rows=46, elapsed_compute=26.637µs, repart_time=6ns, fetch_time=399.971411ms, send_time=6.658µs] | +| | | ProjectionExec: expr=[room@0 as room, temp@2 as temp], metrics=[output_rows=46, elapsed_compute=3.102µs] | +| | | CoalesceBatchesExec: target_batch_size=8192, metrics=[output_rows=46, elapsed_compute=25.585µs] | +| | | FilterExec: time@1 >= 1672531200000000000 AND time@1 <= 1703980800000000000, metrics=[output_rows=46, elapsed_compute=26.51µs] | +| | | ParquetExec: file_groups={6 groups: [[70434/116281/404d73cea0236530ea94f5470701eb814a8f0565c0e4bef5a2d2e33dfbfc3567/1be334e8-0af8-00da-2615-f67cd4be90f7.parquet], [70434/116281/c14418ba28a22a3abb693a1cb326a63b62dc611aec58c9bed438fdafd3bc5882/8b29ae98-761f-0550-2fe4-ee77503658e9.parquet], [70434/116281/fa677477eed622ae8123da1251aa7c351f801e2ee2f0bc28c0fe3002a30b3563/65bb4dc3-04e1-0e02-107a-90cee83c51b0.parquet], [70434/116281/db162bdd30261019960dd70da182e6ebd270284569ecfb5deffea7e65baa0df9/2505e079-67c5-06d9-3ede-89aca542dd18.parquet], [70434/116281/0c025dcccae8691f5fd70b0f131eea4ca6fafb95a02f90a3dc7bb015efd3ab4f/3f3e44c3-b71e-0ca4-3dc7-8b2f75b9ff86.parquet], ...]}, projection=[room, time, temp], output_ordering=[room@0 ASC, time@1 ASC], predicate=time@6 >= 1672531200000000000 AND time@6 <= 1703980800000000000, pruning_predicate=time_max@0 >= 1672531200000000000 AND time_min@1 <= 1703980800000000000, required_guarantees=[], metrics=[output_rows=46, elapsed_compute=6ns, predicate_evaluation_errors=0, bytes_scanned=3279, row_groups_pruned_statistics=0, file_open_errors=0, file_scan_errors=0, pushdown_rows_filtered=0, num_predicate_creation_errors=0, row_groups_pruned_bloom_filter=0, page_index_rows_filtered=0, time_elapsed_opening=398.462968ms, time_elapsed_processing=1.626106ms, time_elapsed_scanning_total=1.36822ms, page_index_eval_time=33.474µs, pushdown_eval_time=14.267µs, time_elapsed_scanning_until_data=1.27694ms] | {{% /expand %}} -{{< /expand-wrapper >}} \ No newline at end of file +{{< /expand-wrapper >}} + +## `EXPLAIN ANALYZE VERBOSE` + +Executes a statement and returns the execution plan, runtime metrics, and additional details helpful for debugging the statement. + +The report includes the following: + +- the [logical plan](/influxdb/cloud-dedicated/reference/internals/query-plan/#logical-plan) +- the [physical plan](/influxdb/cloud-dedicated/reference/internals/query-plan/#physical-plan) annotated with execution counters, number of rows produced, and runtime metrics sampled during the query execution +- Information truncated in the `EXPLAIN` report--for example, the paths for all [Parquet files retrieved for the query](/influxdb/cloud-dedicated/reference/internals/query-plan/#file_groups). +- All intermediate physical plans that DataFusion and the [Querier](/influxdb/cloud-dedicated/reference/internals/storage-engine/#querier) generate before generating the final physical plan--helpful in debugging to see when an [`ExecutionPlan` node](/influxdb/cloud-dedicated/reference/internals/query-plan/#executionplan-nodes) is added or removed, and how InfluxDB optimizes the query. + +### Example `EXPLAIN ANALYZE VERBOSE` + +```SQL +EXPLAIN ANALYZE VERBOSE SELECT temp FROM home +WHERE time >= now() - INTERVAL '7 days' AND room = 'Kitchen' +ORDER BY time +``` diff --git a/content/influxdb/cloud-dedicated/write-data/best-practices/schema-design.md b/content/influxdb/cloud-dedicated/write-data/best-practices/schema-design.md index 0c193444a..3d350e78c 100644 --- a/content/influxdb/cloud-dedicated/write-data/best-practices/schema-design.md +++ b/content/influxdb/cloud-dedicated/write-data/best-practices/schema-design.md @@ -13,7 +13,6 @@ menu: Use the following guidelines to design your [schema](/influxdb/cloud-dedicated/reference/glossary/#schema) for simpler and more performant queries. - - [InfluxDB data structure](#influxdb-data-structure) - [Primary keys](#primary-keys) @@ -23,16 +22,13 @@ for simpler and more performant queries. - [Measurements can contain up to 250 columns](#measurements-can-contain-up-to-250-columns) - [Design for performance](#design-for-performance) - [Avoid wide schemas](#avoid-wide-schemas) - - [Avoid too many tags](#avoid-too-many-tags) - [Avoid sparse schemas](#avoid-sparse-schemas) - - [Writing individual fields with different timestamps](#writing-individual-fields-with-different-timestamps) - [Measurement schemas should be homogenous](#measurement-schemas-should-be-homogenous) + - [Use the best data type for your data](#use-the-best-data-type-for-your-data) - [Design for query simplicity](#design-for-query-simplicity) - [Keep measurement names, tags, and fields simple](#keep-measurement-names-tags-and-fields-simple) - [Avoid keywords and special characters](#avoid-keywords-and-special-characters) - - ## InfluxDB data structure The InfluxDB data model organizes time series data into buckets and measurements. @@ -120,6 +116,7 @@ The following guidelines help to optimize query performance: - [Avoid wide schemas](#avoid-wide-schemas) - [Avoid sparse schemas](#avoid-sparse-schemas) - [Measurement schemas should be homogenous](#measurement-schemas-should-be-homogenous) +- [Use the best data type for your data](#use-the-best-data-type-for-your-data) ### Avoid wide schemas @@ -208,7 +205,7 @@ different sources and each source returns data with different tag and field sets {{% /flex-content %}} {{< /flex >}} -These sets of data written to the same measurement will result in a measurement +These sets of data written to the same measurement result in a measurement full of null values (also known as a _sparse schema_): | time | source | src | code | currency | crypto | price | cost | volume | @@ -225,6 +222,12 @@ full of null values (also known as a _sparse schema_): {{% /expand %}} {{< /expand-wrapper >}} +### Use the best data type for your data + +When writing data to a field, use the most appropriate [data type](/influxdb/cloud-dedicated/reference/glossary/#data-type) for your data--write integers as integers, decimals as floats, and booleans as booleans. +A query against a field that stores integers outperforms a query against string data; +querying over many long string values can negatively affect performance. + ## Design for query simplicity Naming conventions for measurements, tag keys, and field keys can simplify or diff --git a/content/influxdb/cloud-serverless/.vale.ini b/content/influxdb/cloud-serverless/.vale.ini index 9cc9e95cc..4110ad9d2 100644 --- a/content/influxdb/cloud-serverless/.vale.ini +++ b/content/influxdb/cloud-serverless/.vale.ini @@ -1,6 +1,6 @@ StylesPath = "../../../.ci/vale/styles" -Vocab = InfluxData, Cloud-Serverless +Vocab = Cloud-Serverless MinAlertLevel = warning diff --git a/content/influxdb/cloud-serverless/query-data/optimize-queries.md b/content/influxdb/cloud-serverless/query-data/optimize-queries.md deleted file mode 100644 index 7843a3bc3..000000000 --- a/content/influxdb/cloud-serverless/query-data/optimize-queries.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -title: Optimize queries -description: > - Optimize your SQL and InfluxQL queries to improve performance and reduce their memory and compute (CPU) requirements. -weight: 401 -menu: - influxdb_cloud_serverless: - name: Optimize queries - parent: Query data -influxdb/cloud-serverless/tags: [query, sql, influxql] -related: - - /influxdb/cloud-serverless/query-data/sql/ - - /influxdb/cloud-serverless/query-data/influxql/ - - /influxdb/cloud-serverless/query-data/execute-queries/troubleshoot/ - - /influxdb/cloud-serverless/reference/client-libraries/v3/ -aliases: - - /influxdb/cloud-serverless/query-data/execute-queries/optimize-queries/ ---- - -## Troubleshoot query performance - -Use the following tools to help you identify performance bottlenecks and troubleshoot problems in queries: - -- [Troubleshoot query performance](#troubleshoot-query-performance) - - [EXPLAIN and ANALYZE](#explain-and-analyze) - - [Enable trace logging](#enable-trace-logging) - -### EXPLAIN and ANALYZE - -To view the query engine's execution plan and metrics for an SQL query, prepend [`EXPLAIN`](/influxdb/cloud-serverless/reference/sql/explain/) or [`EXPLAIN ANALYZE`](/influxdb/cloud-serverless/reference/sql/explain/#explain-analyze) to the query. -The report can reveal query bottlenecks such as a large number of table scans or parquet files, and can help triage the question, "Is the query slow due to the amount of work required or due to a problem with the schema, compactor, etc.?" - -The following example shows how to use the InfluxDB v3 Python client library and pandas to view `EXPLAIN` and `EXPLAIN ANALYZE` results for a query: - - - - -{{% code-placeholders "BUCKET_NAME|API_TOKEN|APP_REQUEST_ID" %}} -```python -from influxdb_client_3 import InfluxDBClient3 -import pandas as pd -import tabulate # Required for pandas.to_markdown() - -def explain_and_analyze(): - print('Use SQL EXPLAIN and ANALYZE to view query plan information.') - - # Instantiate an InfluxDB client. - client = InfluxDBClient3(token = f"API_TOKEN", - host = f"{{< influxdb/host >}}", - database = f"BUCKET_NAME") - - sql_explain = '''EXPLAIN SELECT * - FROM home - WHERE time >= now() - INTERVAL '90 days' - ORDER BY time''' - - table = client.query(sql_explain) - df = table.to_pandas() - - sql_explain_analyze = '''EXPLAIN ANALYZE SELECT * - FROM home - WHERE time >= now() - INTERVAL '90 days' - ORDER BY time''' - - table = client.query(sql_explain_analyze) - - # Combine the Dataframes and output the plan information. - df = pd.concat([df, table.to_pandas()]) - - assert df.shape == (3, 2) and df.columns.to_list() == ['plan_type', 'plan'] - print(df[['plan_type', 'plan']].to_markdown(index=False)) - - client.close() - -explain_and_analyze() -``` -{{% /code-placeholders %}} - -Replace the following: - -- {{% code-placeholder-key %}}`BUCKET_NAME`{{% /code-placeholder-key %}}: the [bucket](/influxdb/cloud-serverless/admin/buckets/) to query -- {{% code-placeholder-key %}}`API_TOKEN`{{% /code-placeholder-key %}}: a [token](/influxdb/cloud-serverless/admin/tokens/) with sufficient permissions to the specified database - -The output is similar to the following: - -```markdown -| plan_type | plan | -|:------------------|:---------------------------------------------------------------------------------------------------------------------------------------------| -| logical_plan | Sort: home.time ASC NULLS LAST | -| | TableScan: home projection=[co, hum, room, sensor, temp, time], full_filters=[home.time >= TimestampNanosecond(1688491380936276013, None)] | -| physical_plan | SortExec: expr=[time@5 ASC NULLS LAST] | -| | EmptyExec: produce_one_row=false | -| Plan with Metrics | SortExec: expr=[time@5 ASC NULLS LAST], metrics=[output_rows=0, elapsed_compute=1ns, spill_count=0, spilled_bytes=0] | -| | EmptyExec: produce_one_row=false, metrics=[] -``` - -### Enable trace logging - -Customers with an {{% product-name %}} [annual or support contract](https://www.influxdata.com/influxdb-cloud-pricing/) can [contact InfluxData Support](https://support.influxdata.com/) to enable tracing and request help troubleshooting your query. -With tracing enabled, InfluxDB Support can trace system processes and analyze log information for a query instance. -The tracing system follows the [OpenTelemetry traces](https://opentelemetry.io/docs/concepts/signals/traces/) model for providing observability into a request. diff --git a/content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/_index.md b/content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/_index.md new file mode 100644 index 000000000..a03a674f3 --- /dev/null +++ b/content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/_index.md @@ -0,0 +1,23 @@ +--- +title: Troubleshoot and optimize queries +description: > + Troubleshoot errors and optimize performance for SQL and InfluxQL queries in InfluxDB. + Use observability tools to view query execution and metrics. +weight: 201 +menu: + influxdb_cloud_serverless: + name: Troubleshoot and optimize queries + parent: Query data +influxdb/cloud-serverless/tags: [query, performance, observability, errors, sql, influxql] +related: + - /influxdb/cloud-serverless/query-data/sql/ + - /influxdb/cloud-serverless/query-data/influxql/ +aliases: + - /influxdb/cloud-serverless/query-data/execute-queries/troubleshoot/ + +--- + +Troubleshoot errors and optimize performance for SQL and InfluxQL queries in {{% product-name %}}. +Use observability tools to view query execution and metrics. + +{{< children >}} diff --git a/content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/analyze-query-plan.md b/content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/analyze-query-plan.md new file mode 100644 index 000000000..b1d733281 --- /dev/null +++ b/content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/analyze-query-plan.md @@ -0,0 +1,769 @@ +--- +title: Analyze a query plan +description: > + Learn how to read and analyze a query plan to + understand how a query is executed and find performance bottlenecks. +weight: 401 +menu: + influxdb_cloud_serverless: + name: Analyze a query plan + parent: Troubleshoot and optimize queries +influxdb/cloud-serverless/tags: [query, sql, influxql, observability, query plan] +related: + - /influxdb/cloud-serverless/query-data/sql/ + - /influxdb/cloud-serverless/query-data/influxql/ + - /influxdb/cloud-serverless/reference/internals/query-plans/ +--- + +Learn how to read and analyze a [query plan](/influxdb/cloud-serverless/reference/glossary/#query-plan) to +understand query execution steps and data organization, and find performance bottlenecks. + +When you query InfluxDB v3, the Querier devises a query plan for executing the query. +The engine tries to determine the optimal plan for the query structure and data. +By learning how to generate and interpret reports for the query plan, +you can better understand how the query is executed and identify bottlenecks that affect the performance of your query. + +For example, if the query plan reveals that your query reads a large number of Parquet files, +you can then take steps to [optimize your query](/influxdb/cloud-serverless/query-data/optimize-queries/), such as add filters to read less data. + +- [Use EXPLAIN keywords to view a query plan](#use-explain-keywords-to-view-a-query-plan) +- [Read an EXPLAIN report](#read-an-explain-report) +- [Read a query plan](#read-a-query-plan) + - [Example physical plan for a SELECT - ORDER BY query](#example-physical-plan-for-a-select---order-by-query) + - [Example `EXPLAIN` report for an empty result set](#example-explain-report-for-an-empty-result-set) +- [Analyze a query plan for leading edge data](#analyze-a-query-plan-for-leading-edge-data) + - [Sample data](#sample-data) + - [Sample query](#sample-query) + - [EXPLAIN report for the leading edge data query](#explain-report-for-the-leading-edge-data-query) + - [Locate the physical plan](#locate-the-physical-plan) + - [Read the physical plan](#read-the-physical-plan) + - [Data scanning nodes (ParquetExec and RecordBatchesExec)](#data-scanning-nodes-parquetexec-and-recordbatchesexec) + - [Analyze branch structures](#analyze-branch-structures) + +## Use EXPLAIN keywords to view a query plan + +Use the `EXPLAIN` keyword (and the optional [`ANALYZE`](/influxdb/cloud-serverless/reference/sql/explain/#explain-analyze) and [`VERBOSE`](/influxdb/cloud-serverless/reference/sql/explain/#explain-analyze-verbose) keywords) to view the query plans for a query. + +{{% expand-wrapper %}} +{{% expand "Use Python and pandas to view an EXPLAIN report" %}} + +The following example shows how to use the InfluxDB v3 Python client library and pandas to view the `EXPLAIN` report for a query: + + + + + +{{% code-placeholders "DATABASE_(NAME|TOKEN)" %}} + +```python +from influxdb_client_3 import InfluxDBClient3 +import pandas as pd +import tabulate # Required for pandas.to_markdown() + +# Instantiate an InfluxDB client. +client = InfluxDBClient3(token = f"TOKEN", + host = f"{{< influxdb/host >}}", + database = f"BUCKET_NAME") + +sql_explain = '''EXPLAIN + SELECT temp + FROM home + WHERE time >= now() - INTERVAL '7 days' + AND room = 'Kitchen' + ORDER BY time''' + +table = client.query(sql_explain) +df = table.to_pandas() +print(df.to_markdown(index=False)) + +assert df.shape == (2, 2), f'Expect {df.shape} to have 2 columns, 2 rows' +assert 'physical_plan' in df.plan_type.values, "Expect physical_plan" +assert 'logical_plan' in df.plan_type.values, "Expect logical_plan" +``` + +{{% /code-placeholders %}} + +Replace the following: + +- {{% code-placeholder-key %}}`BUCKET_NAME`{{% /code-placeholder-key %}}: your {{% product-name %}} bucket +- {{% code-placeholder-key %}}`TOKEN`{{% /code-placeholder-key %}}: a [token](/influxdb/cloud-serverless/admin/tokens/) with sufficient permissions to the specified bucket + +{{% /expand %}} +{{% /expand-wrapper %}} + +## Read an EXPLAIN report + +When you [use `EXPLAIN` keywords to view a query plan](#use-explain-keywords-to-view-a-query-plan), the report contains the following: + +- two columns: `plan_type` and `plan` +- one row for the [logical plan](/influxdb/cloud-serverless/reference/internals/query-plans/#logical-plan) (`logical_plan`) +- one row for the [physical plan](/influxdb/cloud-serverless/reference/internals/query-plans/#physical-plan) (`physical_plan`) + +## Read a query plan + +Plans are in _tree format_--each plan is an upside-down tree in which +execution and data flow from _leaf nodes_, the innermost steps in the plan, to outer _branch nodes_. +Whether reading a logical or physical plan, keep the following in mind: + +- Start at the the _leaf nodes_ and read upward. +- At the top of the plan, the _root node_ represents the final, encompassing execution step. + +In a [physical plan](/influxdb/cloud-serverless/reference/internals/query-plan/#physical-plan), each step is an [`ExecutionPlan` node](/influxdb/cloud-serverless/reference/internals/query-plan/#executionplan-nodes) that receives expressions for input data and output requirements, and computes a partition of data. + +Use the following steps to analyze a query plan and estimate how much work is required to complete the query. +The same steps apply regardless of how large or complex the plan might seem. + +1. Start from the furthest indented steps (the _leaf nodes_), and read upward. +2. Understand the job of each [`ExecutionPlan` node](/influxdb/cloud-serverless/reference/internals/query-plan/#executionplan-nodes)--for example, a [`UnionExec`](/influxdb/cloud-serverless/reference/internals/query-plan/#unionexec) node encompassing the leaf nodes means that the `UnionExec` concatenates the output of all the leaves. +3. For each expression, answer the following questions: + - What is the shape and size of data input to the plan? + - What is the shape and size of data output from the plan? + +The remainder of this guide walks you through analyzing a physical plan. +Understanding the sequence, role, input, and output of nodes in your query plan can help you estimate the overall workload and find potential bottlenecks in the query. + +### Example physical plan for a SELECT - ORDER BY query + +The following example shows how to read an `EXPLAIN` report and a physical query plan. + +Given `h20` measurement data and the following query: + +```sql +EXPLAIN SELECT city, min_temp, time FROM h2o ORDER BY city ASC, time DESC; +``` + +The output is similar to the following: + +#### EXPLAIN report + +```sql +| plan_type | plan | ++---------------+--------------------------------------------------------------------------+ +| logical_plan | Sort: h2o.city ASC NULLS LAST, h2o.time DESC NULLS FIRST | +| | TableScan: h2o projection=[city, min_temp, time] | +| physical_plan | SortPreservingMergeExec: [city@0 ASC NULLS LAST,time@2 DESC] | +| | UnionExec | +| | SortExec: expr=[city@0 ASC NULLS LAST,time@2 DESC] | +| | ParquetExec: file_groups={...}, projection=[city, min_temp, time] | +| | SortExec: expr=[city@0 ASC NULLS LAST,time@2 DESC] | +| | ParquetExec: file_groups={...}, projection=[city, min_temp, time] | +| | | +``` + +{{% caption %}} +Output from `EXPLAIN SELECT city, min_temp, time FROM h2o ORDER BY city ASC, time DESC;` +{{% /caption %}} + +Each step, or _node_, in the physical plan is an `ExecutionPlan` name and the key-value _expressions_ that contain relevant parts of the query--for example, the first node in the [`EXPLAIN` report](#explain-report) physical plan is a `ParquetExec` execution plan: + +```text +ParquetExec: file_groups={...}, projection=[city, min_temp, time] +``` + +Because `ParquetExec` and `RecordBatchesExec` nodes retrieve and scan data in InfluxDB queries, every query plan starts with one or more of these nodes. + +#### Physical plan data flow + +Data flows _up_ in a query plan. + +The following diagram shows the data flow and sequence of nodes in the [`EXPLAIN` report](#explain-report) physical plan: + + +{{< html-diagram/query-plan >}} + + +{{% caption %}} +Execution and data flow in the [`EXPLAIN` report](#explain-report) physical plan. +`ParquetExec` nodes execute in parallel and `UnionExec` combines their output. +{{% /caption %}} + +The following steps summarize the [physical plan execution and data flow](#physical-plan-data-flow): + +1. Two `ParquetExec` plans, in parallel, read data from Parquet files: + - Each `ParquetExec` node processes one or more _file groups_. + - Each file group contains one or more Parquet file paths. + - A `ParquetExec` node processes its groups in parallel, reading each group's files sequentially. + - The output is a stream of data to the corresponding `SortExec` node. +2. The `SortExec` nodes, in parallel, sort the data by `city` (ascending) and `time` (descending). Sorting is required by the `SortPreservingMergeExec` plan. +3. The `UnionExec` node concatenates the streams to union the output of the parallel `SortExec` nodes. +4. The `SortPreservingMergeExec` node merges the previously sorted and unioned data from `UnionExec`. + +### Example `EXPLAIN` report for an empty result set + +If your table doesn't contain data for the time range in your query, the physical plan starts with an `EmptyExec` leaf node--for example: + +{{% code-callout "EmptyExec"%}} + +```sql +ProjectionExec: expr=[temp@0 as temp] + SortExec: expr=[time@1 ASC NULLS LAST] + EmptyExec: produce_one_row=false +``` + +{{% /code-callout %}} + +## Analyze a query plan for leading edge data + +The following sections guide you through analyzing a physical query plan for a typical time series use case--aggregating recently written (_leading edge_) data. +Although the query and plan are more complex than in the [preceding example](#example-physical-plan-for-a-select---order-by-query), you'll follow the same [steps to read the query plan](#read-a-query-plan). +After learning how to read the query plan, you'll have an understanding of `ExecutionPlans`, data flow, and potential query bottlenecks. + +### Sample data + +Consider the following `h20` data, represented as "chunks" of line protocol, written to InfluxDB: + +```text +// h20 data +// The following data represents 5 batches, or "chunks", of line protocol +// written to InfluxDB. +// - Chunks 1-4 are ingested and each is persisted to a separate partition file in storage. +// - Chunk 5 is ingested and not yet persisted to storage. +// - Chunks 1 and 2 cover short windows of time that don't overlap times in other chunks. +// - Chunks 3 and 4 cover larger windows of time and the time ranges overlap each other. +// - Chunk 5 contains the largest time range and overlaps with chunk 4, the Parquet file with the largest time-range. +// - In InfluxDB, a chunk never duplicates its own data. +// +// Chunk 1: stored Parquet file +// - time range: 50-249 +// - no duplicates in its own chunk +// - no overlap with any other chunks +[ +"h2o,state=MA,city=Bedford min_temp=71.59 150", +"h2o,state=MA,city=Boston min_temp=70.4, 50", +"h2o,state=MA,city=Andover max_temp=69.2, 249", +], + +// Chunk 2: stored Parquet file +// - time range: 250-349 +// - no duplicates in its own chunk +// - no overlap with any other chunks +// - adds a new field (area) +[ +"h2o,state=CA,city=SF min_temp=79.0,max_temp=87.2,area=500u 300", +"h2o,state=CA,city=SJ min_temp=75.5,max_temp=84.08 349", +"h2o,state=MA,city=Bedford max_temp=78.75,area=742u 300", +"h2o,state=MA,city=Boston min_temp=65.4 250", +], + +// Chunk 3: stored Parquet file +// - time range: 350-500 +// - no duplicates in its own chunk +// - overlaps chunk 4 +[ +"h2o,state=CA,city=SJ min_temp=77.0,max_temp=90.7 450", +"h2o,state=CA,city=SJ min_temp=69.5,max_temp=88.2 500", +"h2o,state=MA,city=Boston min_temp=68.4 350", +], + +// Chunk 4: stored Parquet file +// - time range: 400-600 +// - no duplicates in its own chunk +// - overlaps chunk 3 +[ + "h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 600", + "h2o,state=CA,city=SJ min_temp=69.5,max_temp=89.2 600", // duplicates row 3 in chunk 5 + "h2o,state=MA,city=Bedford max_temp=80.75,area=742u 400", // overlaps chunk 3 + "h2o,state=MA,city=Boston min_temp=65.40,max_temp=82.67 400", // overlaps chunk 3 +], + +// Chunk 5: Ingester data +// - time range: 550-700 +// - overlaps and duplicates data in chunk 4 +[ +"h2o,state=MA,city=Bedford max_temp=88.75,area=742u 600", // overlaps chunk 4 +"h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 650", +"h2o,state=CA,city=SJ min_temp=68.5,max_temp=90.0 600", // duplicates row 2 in chunk 4 +"h2o,state=CA,city=SJ min_temp=75.5,max_temp=84.08 700", +"h2o,state=MA,city=Boston min_temp=67.4 550", // overlaps chunk 4 +] +``` + +The following query selects all the data: + +```sql +SELECT state, city, min_temp, max_temp, area, time +FROM h2o +ORDER BY state asc, city asc, time desc; +``` + +The output is the following: + +```sql ++-------+---------+----------+----------+------+--------------------------------+ +| state | city | min_temp | max_temp | area | time | ++-------+---------+----------+----------+------+--------------------------------+ +| CA | SF | 68.4 | 85.7 | 500 | 1970-01-01T00:00:00.000000650Z | +| CA | SF | 68.4 | 85.7 | 500 | 1970-01-01T00:00:00.000000600Z | +| CA | SF | 79.0 | 87.2 | 500 | 1970-01-01T00:00:00.000000300Z | +| CA | SJ | 75.5 | 84.08 | | 1970-01-01T00:00:00.000000700Z | +| CA | SJ | 68.5 | 90.0 | | 1970-01-01T00:00:00.000000600Z | +| CA | SJ | 69.5 | 88.2 | | 1970-01-01T00:00:00.000000500Z | +| CA | SJ | 77.0 | 90.7 | | 1970-01-01T00:00:00.000000450Z | +| CA | SJ | 75.5 | 84.08 | | 1970-01-01T00:00:00.000000349Z | +| MA | Andover | | 69.2 | | 1970-01-01T00:00:00.000000249Z | +| MA | Bedford | | 88.75 | 742 | 1970-01-01T00:00:00.000000600Z | +| MA | Bedford | | 80.75 | 742 | 1970-01-01T00:00:00.000000400Z | +| MA | Bedford | | 78.75 | 742 | 1970-01-01T00:00:00.000000300Z | +| MA | Bedford | 71.59 | | | 1970-01-01T00:00:00.000000150Z | +| MA | Boston | 67.4 | | | 1970-01-01T00:00:00.000000550Z | +| MA | Boston | 65.4 | 82.67 | | 1970-01-01T00:00:00.000000400Z | +| MA | Boston | 68.4 | | | 1970-01-01T00:00:00.000000350Z | +| MA | Boston | 65.4 | | | 1970-01-01T00:00:00.000000250Z | +| MA | Boston | 70.4 | | | 1970-01-01T00:00:00.000000050Z | ++-------+---------+----------+----------+------+--------------------------------+ +``` + +### Sample query + +The following query selects leading edge data from the [sample data](#sample-data): + +```sql +SELECT city, count(1) +FROM h2o +WHERE time >= to_timestamp(200) AND time < to_timestamp(700) + AND state = 'MA' +GROUP BY city +ORDER BY city ASC; +``` + +The output is the following: + +```sql ++---------+-----------------+ +| city | COUNT(Int64(1)) | ++---------+-----------------+ +| Andover | 1 | +| Bedford | 3 | +| Boston | 4 | ++---------+-----------------+ +``` + +### EXPLAIN report for the leading edge data query + +The following query generates the `EXPLAIN` report for the preceding [sample query](#sample-query): + +```sql +EXPLAIN SELECT city, count(1) +FROM h2o +WHERE time >= to_timestamp(200) AND time < to_timestamp(700) + AND state = 'MA' +GROUP BY city +ORDER BY city ASC; +``` + +{{< expand-wrapper >}} +{{% expand "EXPLAIN report for a leading edge data query" %}} + +```sql +| plan_type | plan | +| logical_plan | Sort: h2o.city ASC NULLS LAST | +| | Aggregate: groupBy=[[h2o.city]], aggr=[[COUNT(Int64(1))]] | +| | TableScan: h2o projection=[city], full_filters=[h2o.time >= TimestampNanosecond(200, None), h2o.time < TimestampNanosecond(700, None), h2o.state = Dictionary(Int32, Utf8("MA"))] | +| physical_plan | SortPreservingMergeExec: [city@0 ASC NULLS LAST] | +| | SortExec: expr=[city@0 ASC NULLS LAST] | +| | AggregateExec: mode=FinalPartitioned, gby=[city@0 as city], aggr=[COUNT(Int64(1))] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | RepartitionExec: partitioning=Hash([city@0], 4), input_partitions=4 | +| | AggregateExec: mode=Partial, gby=[city@0 as city], aggr=[COUNT(Int64(1))] | +| | RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=3 | +| | UnionExec | +| | ProjectionExec: expr=[city@0 as city] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | FilterExec: time@2 >= 200 AND time@2 < 700 AND state@1 = MA | +| | ParquetExec: file_groups={2 groups: [[1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/243db601-f3f1-401b-afda-82160d8cc1a8.Parquet], [1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/f5fb7c7d-16ac-49ba-a811-69578d05843f.Parquet]]}, projection=[city, state, time], output_ordering=[state@1 ASC, city@0 ASC, time@2 ASC], predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA, pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 | +| | ProjectionExec: expr=[city@1 as city] | +| | DeduplicateExec: [state@2 ASC,city@1 ASC,time@3 ASC] | +| | SortPreservingMergeExec: [state@2 ASC,city@1 ASC,time@3 ASC,__chunk_order@0 ASC] | +| | UnionExec | +| | SortExec: expr=[state@2 ASC,city@1 ASC,time@3 ASC,__chunk_order@0 ASC] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | FilterExec: time@3 >= 200 AND time@3 < 700 AND state@2 = MA | +| | RecordBatchesExec: chunks=1, projection=[__chunk_order, city, state, time] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | FilterExec: time@3 >= 200 AND time@3 < 700 AND state@2 = MA | +| | ParquetExec: file_groups={2 groups: [[1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/2cbb3992-4607-494d-82e4-66c480123189.Parquet], [1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/9255eb7f-2b51-427b-9c9b-926199c85bdf.Parquet]]}, projection=[__chunk_order, city, state, time], output_ordering=[state@2 ASC, city@1 ASC, time@3 ASC, __chunk_order@0 ASC], predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA, pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 | +``` + +{{% caption %}} +`EXPLAIN` report for a typical leading edge data query +{{% /caption %}} + +{{% /expand %}} +{{< /expand-wrapper >}} + +The comments in the [sample data](#sample-data) tell you which data chunks _overlap_ or duplicate data in other chunks. +Two chunks of data overlap if there are portions of time for which data exists in both chunks. +_You'll learn how to [recognize overlapping and duplicate data](#recognize-overlapping-and-duplicate-data) in a query plan later in this guide._ + +Unlike the sample data, your data likely doesn't tell you where overlaps or duplicates exist. +A physical plan can reveal overlaps and duplicates in your data and how they affect your queries--for example, after learning how to read a physical plan, you might summarize the data scanning steps as follows: + +- Query execution starts with two `ParquetExec` and one `RecordBatchesExec` execution plans that run in parallel. +- The first `ParquetExec` node reads two files that don't overlap any other files and don't duplicate data; the files don't require deduplication. +- The second `ParquetExec` node reads two files that overlap each other and overlap the ingested data scanned in the `RecordBatchesExec` node; the query plan must include the deduplication process for these nodes before completing the query. + +The remaining sections analyze `ExecutionPlan` node structure and arguments in the example physical plan. +The example includes DataFusion and InfluxDB-specific [`ExecutionPlan` nodes](/influxdb/cloud-dedicated/reference/internals/query-plans/#executionplan-nodes). + +### Locate the physical plan + +To begin analyzing the physical plan for the query, find the row in the [`EXPLAIN` report](#explain-report-for-the-leading-edge-data-query) where the `plan_type` column has the value `physical_plan`. +The `plan` column for the row contains the physical plan. + +### Read the physical plan + +The following sections follow the steps to [read a query plan](#read-a-query-plan) and examine the physical plan nodes and their input and output. + +{{% note %}} +To [read the execution flow of a query plan](#read-a-query-plan), always start from the innermost (leaf) nodes and read up toward the top outermost root node. +{{% /note %}} + +#### Physical plan leaf nodes + +Query physical plan leaf node structures + +{{% caption %}} +Leaf node structures in the physical plan +{{% /caption %}} + +### Data scanning nodes (ParquetExec and RecordBatchesExec) + +The [example physical plan](#physical-plan-leaf-nodes) contains three [leaf nodes](#physical-plan-leaf-nodes)--the innermost nodes where the execution flow begins: + +- [`ParquetExec`](/influxdb/cloud-serverless/reference/internals/query-plans/#parquetexec) nodes retrieve and scan data from Parquet files in the [Object store](/influxdb/cloud-serverless/reference/internals/storage-engine/#object-store) +- a [`RecordBatchesExec`](/influxdb/cloud-serverless/reference/internals/query-plans/#recordbatchesexec) node retrieves recently written, yet-to-be-persisted data from the [Ingester](/influxdb/cloud-serverless/reference/internals/storage-engine/#ingester) + +Because `ParquetExec` and `RecordBatchesExec` retrieve and scan data for a query, every query plan starts with one or more of these nodes. + +The number of `ParquetExec` and `RecordBatchesExec` nodes and their parameter values can tell you which data (and how much) is retrieved for your query, and how efficiently the plan handles the organization (for example, partitioning and deduplication) of your data. + +For convenience, this guide uses the names _ParquetExec_A_ and _ParquetExec_B_ for the `ParquetExec` nodes in the [example physical plan](#physical-plan-leaf-nodes) . +Reading from the top of the physical plan, **ParquetExec_A** is the first leaf node in the physical plan and **ParquetExec_B** is the last (bottom) leaf node. + +_The names indicate the nodes' locations in the report, not their order of execution._ + +- [ParquetExec_A](#parquetexec_a) +- [RecordBatchesExec](#recordbatchesexec) +- [ParquetExec_B](#parquetexec_b) + +#### ParquetExec_A + +```sql +ParquetExec: file_groups={2 groups: [[1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/243db601-f3f1-401b-afda-82160d8cc1a8.Parquet], [1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/f5fb7c7d-16ac-49ba-a811-69578d05843f.Parquet]]}, projection=[city, state, time], output_ordering=[state@1 ASC, city@0 ASC, time@2 ASC], predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA, pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 | +``` + +{{% caption %}} +ParquetExec_A, the first ParquetExec node +{{% /caption %}} + +ParquetExec_A has the following traits: + +##### `file_groups` + +A _file group_ is a list of files for the operator to read. +Files are referenced by path: + +- `1/1/b862a7e9b.../243db601-....parquet` +- `1/1/b862a7e9b.../f5fb7c7d-....parquet` + +The path structure represents how your data is organized. +You can use the file paths to gather more information about the query--for example: + +- to find file information (for example: size and number of rows) in the catalog +- to download the Parquet file from the Object store for debugging +- to find how many partitions the query reads + +A path has the following structure: + +```text +///.Parquet + 1 / 1 /b862a7e9b329ee6a4.../243db601-f3f1-4....Parquet +``` + +- `namespace_id`: the namespace (database) being queried +- `table_id`: the table (measurement) being queried +- `partition_hash_id`: the partition this file belongs to. +You can count partition IDs to find how many partitions the query reads. +- `uuid_of_the_file`: the file identifier. + +`ParquetExec` processes groups in parallel and reads the files in each group sequentially. + +```text +file_groups={2 groups: [[1/1/b862a7e9b329ee6a4/243db601....parquet], [1/1/b862a7e9b329ee6a4/f5fb7c7d....parquet]]} +``` + +- `{2 groups: [[file], [file]}`: ParquetExec_A receives two groups with one file per group. +Therefore, ParquetExec_A reads two files in parallel. + +##### `projection` + +`projection` lists the table columns for the `ExecutionPlan` to read and output. + +```text +projection=[city, state, time] +``` + +- `[city, state, time]`: the [sample data](#sample-data) contains many columns, but the [sample query](#sample-query) requires the Querier to read only three + +##### `output_ordering` + +`output_ordering` specifies the sort order for the `ExecutionPlan` output. +The Query planner passes the parameter if the output should be ordered and if the planner knows the order. + +```text +output_ordering=[state@2 ASC, city@1 ASC, time@3 ASC, __chunk_order@0 ASC] +``` + +When storing data to Parquet files, InfluxDB sorts the data to improve storage compression and query efficiency and the planner tries to preserve that order for as long as possible. +Generally, the `output_ordering` value that `ParquetExec` receives is the ordering (or a subset of the ordering) of stored data. + +_By design, [`RecordBatchesExec`](#recordbatchesexec) data isn't sorted._ + +In the example, the planner specifies that ParquetExec_A use the existing sort order `state ASC, city ASC, time ASC,` for output. + +{{% note %}} +To view the sort order of your stored data, generate an `EXPLAIN` report for a `SELECT ALL` query--for example: + +```sql +EXPLAIN SELECT * FROM TABLE_NAME WHERE time > now() - interval '1 hour' +``` + +Reduce the time range if the query returns too much data. +{{% /note %}} + +##### `predicate` + +`predicate` is the data filter specified in the query. + +```text +predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA +``` + +##### `pruning predicate` + +`pruning_predicate` is created from the [`predicate`](#predicate) value and is the predicate actually used for pruning data and files from the chosen partitions. +The default filters files by `time`. + +```text +pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 +``` + +_Before the physical plan is generated, an additional `partition pruning` step uses predicates on partitioning columns to prune partitions._ + +#### `RecordBatchesExec` + +```sql +RecordBatchesExec: chunks=1, projection=[__chunk_order, city, state, time] +``` + +{{% caption %}}RecordBatchesExec{{% /caption %}} + +[`RecordBatchesExec`](/influxdb/cloud-serverless/reference/internals/query-plans/#recordbatchesexec) is an InfluxDB-specific `ExecutionPlan` implementation that retrieves recently written, yet-to-be-persisted data from the [Ingester](/influxdb/cloud-serverless/reference/internals/storage-engine/#ingester). + +In the example, `RecordBatchesExec` contains the following expressions: + +##### `chunks` + +`chunks` is the number of data chunks received from the [Ingester](/influxdb/cloud-serverless/reference/internals/storage-engine/#ingester). + +```text +chunks=1 +``` + +- `chunks=1`: `RecordBatchesExec` receives one data chunk. + +##### `projection` + +The `projection` list specifies the columns or expressions for the node to read and output. + +```text +[__chunk_order, city, state, time] +``` + +- `__chunk_order`: orders chunks and files for deduplication +- `city, state, time`: the same columns specified in [`ParquetExec_A projection`](#projection-1) + +{{% note %}} +The presence of `__chunk_order` in data scanning nodes indicates that data overlaps, and is possibly duplicated, among the nodes. +{{% /note %}} + +#### ParquetExec_B + +The bottom leaf node in the [example physical plan](#physical-plan-leaf-nodes) is another `ParquetExec` operator, _ParquetExec_B_. + +##### ParquetExec_B expressions + +```sql +ParquetExec: + file_groups={2 groups: [[1/1/b862a7e9b.../2cbb3992-....Parquet], + [1/1/b862a7e9b.../9255eb7f-....Parquet]]}, + projection=[__chunk_order, city, state, time], + output_ordering=[state@2 ASC, city@1 ASC, time@3 ASC, __chunk_order@0 ASC], + predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA, + pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 +``` + +{{% caption %}}ParquetExec_B, the second ParquetExec{{% /caption %}} + +Because ParquetExec_B has overlaps, the `projection` and `output_ordering` expressions use the `__chunk_order` column used in [`RecordBatchesExec` `projection`](#projection-1). + +{{% note %}} +The presence of `__chunk_order` in data scanning nodes indicates that data overlaps, and is possibly duplicated, among the nodes. +{{% /note %}} + +The remaining ParquetExec_B expressions are similar to those in [ParquetExec_A](#parquetexec_a). + +##### How a query plan distributes data for scanning + +If you compare [`file_group`](#file_groups) paths in [ParquetExec_A](#parquetexec_a) to those in [ParquetExec_B](#parquetexec_b), you'll notice that both contain files from the same partition: + +{{% code-callout "b862a7e9b329ee6a4..." %}} + +```text +1/1/b862a7e9b329ee6a4.../... +``` + +{{% /code-callout %}} + +The planner may distribute files from the same partition to different scan nodes for several reasons, including optimizations for handling [overlaps](#how-a-query-plan-distributes-data-for-scanning)--for example: + +- to separate non-overlapped files from overlapped files to minimize work required for deduplication (which is the case in this example) +- to distribute non-overlapped files to increase parallel execution + +### Analyze branch structures + +After data is output from a data scanning node, it flows up to the next parent (outer) node. + +In the example plan: + +- Each leaf node is the first step in a branch of nodes planned for processing the scanned data. +- The three branches execute in parallel. +- After the leaf node, each branch contains the following similar node structure: + +```sql +... +CoalesceBatchesExec: target_batch_size=8192 + FilterExec: time@3 >= 200 AND time@3 < 700 AND state@2 = MA + ... +``` + +- `FilterExec: time@3 >= 200 AND time@3 < 700 AND state@2 = MA`: filters data for the condition `time@3 >= 200 AND time@3 < 700 AND state@2 = MA`, and guarantees that all data is pruned. +- `CoalesceBatchesExec: target_batch_size=8192`: combines small batches into larger batches. See the DataFusion [`CoalesceBatchesExec`] documentation. + +#### Sorting yet-to-be-persisted data + +In the `RecordBatchesExec` branch, the node that follows `CoalesceBatchesExec` is a `SortExec` node: + +```sql +SortExec: expr=[state@2 ASC,city@1 ASC,time@3 ASC,__chunk_order@0 ASC] +``` + +The node uses the specified expression `state ASC, city ASC, time ASC, __chunk_order ASC` to sort the yet-to-be-persisted data. +Neither ParquetExec_A nor ParquetExec_B contain a similar node because data in the Object store is already sorted (by the [Ingester](/influxdb/cloud-serverless/reference/internals/storage-engine/#ingester) or the [Compactor](/influxdb/cloud-serverless/reference/internals/storage-engine/#compactor)) in the given order; the query plan only needs to sort data that arrives from the [Ingester](/influxdb/cloud-serverless/reference/internals/storage-engine/#ingester). + +#### Recognize overlapping and duplicate data + +In the example physical plan, the ParquetExec_B and `RecordBatchesExec` nodes share the following parent nodes: + +```sql +... +DeduplicateExec: [state@2 ASC,city@1 ASC,time@3 ASC] + SortPreservingMergeExec: [state@2 ASC,city@1 ASC,time@3 ASC,__chunk_order@0 ASC] + UnionExec + ... +``` + +{{% caption %}}Overlapped data node structure{{% /caption %}} + +1. `UnionExec`: unions multiple streams of input data by concatenating the partitions. `UnionExec` doesn't do any merging and is fast to execute. +2. `SortPreservingMergeExec: [state@2 ASC,city@1 ASC,time@3 ASC,__chunk_order@0 ASC]`: merges already sorted data; indicates that preceding data (from nodes below it) is already sorted. The output data is a single sorted stream. +3. `DeduplicateExec: [state@2 ASC,city@1 ASC,time@3 ASC]`: deduplicates an input stream of sorted data. + Because `SortPreservingMergeExec` ensures a single sorted stream, it often, but not always, precedes `DeduplicateExec`. + +A `DeduplicateExec` node indicates that encompassed nodes have [_overlapped data_](/influxdb/cloud-serverless/reference/internals/query-plans/#overlapping-data-and-deduplication)--data in a file or batch have timestamps in the same range as data in another file or batch. +Due to how InfluxDB organizes data, data is never duplicated _within_ a file. + +In the example, the `DeduplicateExec` node encompasses ParquetExec_B and the `RecordBatchesExec` node, which indicates that ParquetExec_B [file group](#file_groups) files overlap the yet-to-be persisted data. + +The following [sample data](#sample-data) excerpt shows overlapping data between a file and Ingester data: + +```text +// Chunk 4: stored Parquet file +// - time range: 400-600 +[ + "h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 600", +], + +// Chunk 5: Ingester data +// - time range: 550-700 +// - overlaps and duplicates data in chunk 4 +[ +"h2o,state=MA,city=Bedford max_temp=88.75,area=742u 600", // overlaps chunk 4 +... +"h2o,state=MA,city=Boston min_temp=67.4 550", // overlaps chunk 4 +] +``` + +If files or ingested data overlap, the Querier must include the `DeduplicateExec` in the query plan to remove any duplicates. +`DeduplicateExec` doesn't necessarily indicate that data is duplicated. +If a plan reads many files and performs deduplication on all of them, it might be for the following reasons: + +- the files contain duplicate data +- the Object store has many small overlapped files that the Compactor hasn't compacted yet. After compaction, your query may perform better because it has fewer files to read +- the Compactor isn't keeping up + +A leaf node that doesn't have a `DeduplicateExec` node in its branch doesn't require deduplication and doesn't overlap other files or [Ingester](/influxdb/cloud-serverless/reference/internals/storage-engine/#ingester) data--for example, ParquetExec_A has no overlaps: + +```sql +ProjectionExec:... + CoalesceBatchesExec:... + FilterExec:... + ParquetExec:... +``` + +{{% caption %}} +The absence of a `DeduplicateExec` node means that files don't overlap. +{{% /caption %}} + +##### Data scan output + +`ProjectionExec` nodes filter columns so that only the `city` column remains in the output: + +```sql +`ProjectionExec: expr=[city@0 as city]` +``` + +##### Final processing + +After deduplicating and filtering data in each leaf node, the plan combines the output and then applies aggregation and sorting operators for the final result: + +```sql +| physical_plan | SortPreservingMergeExec: [city@0 ASC NULLS LAST] | +| | SortExec: expr=[city@0 ASC NULLS LAST] | +| | AggregateExec: mode=FinalPartitioned, gby=[city@0 as city], aggr=[COUNT(Int64(1))] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | RepartitionExec: partitioning=Hash([city@0], 4), input_partitions=4 | +| | AggregateExec: mode=Partial, gby=[city@0 as city], aggr=[COUNT(Int64(1))] | +| | RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=3 | +| | UnionExec +``` + +{{% caption %}} +Operator structure for aggregating, sorting, and final output. +{{% /caption %}} + +- `UnionExec`: unions data streams. Note that the number of output streams is the same as the number of input streams--the `UnionExec` node is an intermediate step to downstream operators that actually merge or split data streams. +- `RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=3`: Splits three input streams into four output streams in round-robin fashion. The plan splits streams to increase parallel execution. +- `AggregateExec: mode=Partial, gby=[city@0 as city], aggr=[COUNT(Int64(1))]`: Groups data as specified in the [query](#sample-query): `city, count(1)`. + This node aggregates each of the four streams separately, and then outputs four streams, indicated by `mode=Partial`--the data isn't fully aggregated. +- `RepartitionExec: partitioning=Hash([city@0], 4), input_partitions=4`: Repartitions data on `Hash([city])` and into four streams--each stream contains data for one city. +- `AggregateExec: mode=FinalPartitioned, gby=[city@0 as city], aggr=[COUNT(Int64(1))]`: Applies the final aggregation (`aggr=[COUNT(Int64(1))]`) to the data. `mode=FinalPartitioned` indicates that the data has already been partitioned (by city) and doesn't need further grouping by `AggregateExec`. +- `SortExec: expr=[city@0 ASC NULLS LAST]`: Sorts the four streams of data, each on `city`, as specified in the query. +- `SortPreservingMergeExec: [city@0 ASC NULLS LAST]`: Merges and sorts the four sorted streams for the final output. + +In the preceding examples, the `EXPLAIN` report shows the query plan without executing the query. +To view runtime metrics, such as execution time for a plan and its operators, use [`EXPLAIN ANALYZE`](/influxdb/cloud-serverless/reference/sql/explain/#explain-analyze) to generate the report and [tracing](/influxdb/cloud-serverless/query-data/optimize-queries/#enable-trace-logging) for further debugging, if necessary. diff --git a/content/influxdb/cloud-serverless/query-data/execute-queries/troubleshoot.md b/content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/flight-responses.md similarity index 92% rename from content/influxdb/cloud-serverless/query-data/execute-queries/troubleshoot.md rename to content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/flight-responses.md index c19d29f0f..91ddf7a2e 100644 --- a/content/influxdb/cloud-serverless/query-data/execute-queries/troubleshoot.md +++ b/content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/flight-responses.md @@ -6,28 +6,22 @@ weight: 401 menu: influxdb_cloud_serverless: name: Understand Flight responses - parent: Execute queries -influxdb/cloud-serverless/tags: [query, sql, influxql] + parent: Troubleshoot and optimize queries +influxdb/cloud-serverless/tags: [query, errors, flight] +related: + - /influxdb/cloud-serverless/query-data/sql/ + - /influxdb/cloud-serverless/query-data/influxql/ + - /influxdb/cloud-serverless/reference/client-libraries/v3/ --- Learn how to handle responses and troubleshoot errors encountered when querying {{% product-name %}} with Flight+gRPC and Arrow Flight clients. - - - [InfluxDB Flight responses](#influxdb-flight-responses) - [Stream](#stream) - [Schema](#schema) - - [Example](#example) - [RecordBatch](#recordbatch) - [InfluxDB status and error codes](#influxdb-status-and-error-codes) - [Troubleshoot errors](#troubleshoot-errors) - - [Internal Error: Received RST_STREAM](#internal-error-received-rst_stream) - - [Internal Error: stream terminated by RST_STREAM with NO_ERROR](#internal-error-stream-terminated-by-rst_stream-with-no_error) - - [Invalid Argument Error: bucket not found](#invalid-argument-error-bucket-bucket_id-not-found) - - [Invalid Argument: Invalid ticket](#invalid-argument-invalid-ticket) - - [Unauthenticated: Unauthenticated](#unauthenticated-unauthenticated) - - [Unauthenticated: read: is unauthorized](#unauthenticated-readbucket_id-is-unauthorized) - - [FlightUnavailableError: Could not get default pem root certs](#flightunavailableerror-could-not-get-default-pem-root-certs) ## InfluxDB Flight responses @@ -42,7 +36,7 @@ For example, if you use the [`influxdb3-python` Python client library](/influxdb InfluxDB responds with one of the following: - A [stream](#stream) in Arrow IPC streaming format -- An [error status code](#influxdb-error-codes) and an optional `details` field that contains the status and a message that describes the error +- An [error status code](#influxdb-status-and-error-codes) and an optional `details` field that contains the status and a message that describes the error ### Stream @@ -81,7 +75,8 @@ SELECT co, delete, hum, room, temp, time The Python client library outputs the following schema representation: -```py + +```python Schema: co: int64 -- field metadata -- @@ -128,7 +123,7 @@ In gRPC, every call returns a status object that contains an integer code and a During a request, the gRPC client and server may each return a status--for example: - The server fails to process the query; responds with status `internal error` and gRPC status `13`. -- The request is missing an API token; the server responds with status `unauthenticated` and gRPC status `16`. +- The request is missing a [token](/influxdb/cloud-serverless/admin/tokens/); the server responds with status `unauthenticated` and gRPC status `16`. - The server responds with a stream, but the client loses the connection due to a network failure and returns status `unavailable`. gRPC defines the integer [status codes](https://grpc.github.io/grpc/core/status_8h.html) and definitions for servers and clients and @@ -169,14 +164,13 @@ _For a list of gRPC codes that servers and clients may return, see [Status codes {{% /expand %}} {{< /expand-wrapper >}} - ### Troubleshoot errors #### Internal Error: Received RST_STREAM **Example**: -```sh +```structuredtext Flight returned internal error, with message: Received RST_STREAM with error code 2. gRPC client debug context: UNKNOWN:Error received from peer ipv4:34.196.233.7:443 {grpc_message:"Received RST_STREAM with error code 2"} ``` @@ -187,12 +181,11 @@ Flight returned internal error, with message: Received RST_STREAM with error cod - Server might have closed the connection due to an internal error. - The client exceeded the server's maximum number of concurrent streams. - - #### Internal Error: stream terminated by RST_STREAM with NO_ERROR **Example**: + ```sh pyarrow._flight.FlightInternalError: Flight returned internal error, with message: stream terminated by RST_STREAM with error code: NO_ERROR. gRPC client debug context: UNKNOWN:Error received from peer ipv4:3.123.149.45:443 {created_time:"2023-07-26T14:12:44.992317+02:00", grpc_status:13, grpc_message:"stream terminated by RST_STREAM with error code: NO_ERROR"}. Client context: OK ``` @@ -203,12 +196,11 @@ pyarrow._flight.FlightInternalError: Flight returned internal error, with messag - Possible network disruption, even if it's temporary. - The server might have reached its maximum capacity or other internal limits. - - #### Invalid Argument Error: bucket not found **Example**: + ```sh ArrowInvalid: Flight returned invalid argument error, with message: bucket "otel5" not found. gRPC client debug context: UNKNOWN:Error received from peer ipv4:3.123.149.45:443 {grpc_message:"bucket \"otel5\" not found", grpc_status:3, created_time:"2023-08-09T16:37:30.093946+01:00"}. Client context: IOError: Server never sent a data message. Detail: Internal ``` @@ -217,12 +209,11 @@ ArrowInvalid: Flight returned invalid argument error, with message: bucket "otel - The specified bucket doesn't exist. - - #### Invalid Argument: Invalid ticket **Example**: + ```sh pyarrow.lib.ArrowInvalid: Flight returned invalid argument error, with message: Invalid ticket. Error: Invalid ticket. gRPC client debug context: UNKNOWN:Error received from peer ipv4:54.158.68.83:443 {created_time:"2023-08-31T17:56:42.909129-05:00", grpc_status:3, grpc_message:"Invalid ticket. Error: Invalid ticket"}. Client context: IOError: Server never sent a data message. Detail: Internal ``` @@ -232,12 +223,11 @@ pyarrow.lib.ArrowInvalid: Flight returned invalid argument error, with message: - The request is missing the bucket name or some other required metadata value. - The request contains bad query syntax. - - -##### Unauthenticated: Unauthenticated +#### Unauthenticated: Unauthenticated **Example**: + ```sh Flight returned unauthenticated error, with message: unauthenticated. gRPC client debug context: UNKNOWN:Error received from peer ipv4:34.196.233.7:443 {grpc_message:"unauthenticated", grpc_status:16, created_time:"2023-08-28T15:38:33.380633-05:00"}. Client context: IOError: Server never sent a data message. Detail: Internal ``` @@ -247,12 +237,11 @@ Flight returned unauthenticated error, with message: unauthenticated. gRPC clien - Token is missing from the request. - The specified token doesn't exist for the specified organization. - - -#### Unauthenticated: read: is unauthorized +#### Unauthorized: Permission denied **Example**: + ```sh Flight returned unauthenticated error, with message: read:orgs/28d1f2f565460a6c/buckets/756fa4f8c8ba6913 is unauthorized. gRPC client debug context: UNKNOWN:Error received from peer ipv4:54.174.236.48:443 {grpc_message:"read:orgs/28d1f2f565460a6c/buckets/756fa4f8c8ba6913 is unauthorized", grpc_status:16, created_time:"2023-08-28T15:42:04.462655-05:00"}. Client context: IOError: Server never sent a data message. Detail: Internal ``` @@ -261,14 +250,13 @@ Flight returned unauthenticated error, with message: read:orgs/28d1f2f565460a6c/ - The specified token doesn't have read permission for the specified bucket. - - #### FlightUnavailableError: Could not get default pem root certs **Example**: If unable to locate a root certificate for _gRPC+TLS_, the Flight client returns errors similar to the following: + ```sh UNKNOWN:Failed to load file... filename:"/usr/share/grpc/roots.pem", children:[UNKNOWN:No such file or directory diff --git a/content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/optimize-queries.md b/content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/optimize-queries.md new file mode 100644 index 000000000..be151b629 --- /dev/null +++ b/content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/optimize-queries.md @@ -0,0 +1,69 @@ +--- +title: Optimize queries +description: > + Optimize queries to improve performance and reduce their memory and compute (CPU) requirements in InfluxDB. + Learn how to use observability tools to analyze query execution and view metrics. +weight: 201 +menu: + influxdb_cloud_serverless: + name: Optimize queries + parent: Troubleshoot and optimize queries +influxdb/cloud-serverless/tags: [query, performance, observability, errors, sql, influxql] +related: + - /influxdb/cloud-serverless/query-data/sql/ + - /influxdb/cloud-serverless/query-data/influxql/ + - /influxdb/cloud-serverless/query-data/execute-queries/analyze-query-plan/ +aliases: + - /influxdb/cloud-serverless/query-data/execute-queries/optimize-queries/ +--- + +Optimize SQL and InfluxQL queries to improve performance and reduce their memory and compute (CPU) requirements. +Learn how to use observability tools to analyze query execution and view metrics. + +- [Why is my query slow?](#why-is-my-query-slow) +- [Strategies for improving query performance](#strategies-for-improving-query-performance) +- [Analyze and troubleshoot queries](#analyze-and-troubleshoot-queries) + +## Why is my query slow? + +Query performance depends on time range and complexity. +If a query is slower than you expect, it might be due to the following reasons: + +- It queries data from a large time range. +- It includes intensive operations, such as querying many string values or `ORDER BY` sorting or re-sorting large amounts of data. + +## Strategies for improving query performance + +The following design strategies generally improve query performance and resource use: + +- Follow [schema design best practices](/influxdb/cloud-serverless/write-data/best-practices/schema-design/) to make querying easier and more performant. +- Query only the data you need--for example, include a [`WHERE` clause](/influxdb/cloud-serverless/reference/sql/where/) that filters data by a time range. + InfluxDB v3 stores data in a Parquet file for each measurement and day, and retrieves files from the Object store to answer a query. + The smaller the time range in your query, the fewer files InfluxDB needs to retrieve from the Object store. +- [Downsample data](/influxdb/cloud-serverless/process-data/downsample/) to reduce the amount of data you need to query. + +Some bottlenecks may be out of your control and are the result of a suboptimal execution plan, such as: + +- Applying the same sort (`ORDER BY`) to already sorted data. +- Retrieving many Parquet files from the Object store--the same query performs better if it retrieves fewer - though, larger - files. +- Querying many overlapped Parquet files. +- Performing a large number of table scans. + +{{% note %}} +#### Analyze query plans to view metrics and recognize bottlenecks + +To view runtime metrics for a query, such as the number of files scanned, use the [`EXPLAIN ANALYZE` keywords](/influxdb/cloud-serverless/reference/sql/explain/#explain-analyze) and learn how to [analyze a query plan](/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/analyze-query-plan/). +{{% /note %}} + +## Analyze and troubleshoot queries + +Use the following tools to analyze and troubleshoot queries and find performance bottlenecks: + +- [Analyze a query plan](/influxdb/cloud-serverless/query-data/analyze-query-plan/) +- [Enable trace logging for a query](#enable-trace-logging-for-a-query) + +### Enable trace logging for a query + +Customers with an {{% product-name %}} [annual or support contract](https://www.influxdata.com/influxdb-cloud-pricing/) can [contact InfluxData Support](https://support.influxdata.com/) to enable tracing and request help troubleshooting your query. +With tracing enabled, InfluxData Support can trace system processes and analyze log information for a query instance. +The tracing system follows the [OpenTelemetry traces](https://opentelemetry.io/docs/concepts/signals/traces/) model for providing observability into a request. diff --git a/content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/troubleshoot.md b/content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/troubleshoot.md new file mode 100644 index 000000000..cabb4acaa --- /dev/null +++ b/content/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/troubleshoot.md @@ -0,0 +1,44 @@ +--- +title: Troubleshoot queries +description: > + Troubleshoot SQL and InfluxQL queries in InfluxDB. +weight: 201 +menu: + influxdb_cloud_serverless: + name: Troubleshoot queries + parent: Troubleshoot and optimize queries +influxdb/cloud-serverless/tags: [query, performance, observability, errors, sql, influxql] +related: + - /influxdb/cloud-serverless/query-data/sql/ + - /influxdb/cloud-serverless/query-data/influxql/ + - /influxdb/cloud-serverless/reference/client-libraries/v3/ +aliases: + - /influxdb/cloud-serverless/query-data/execute-queries/troubleshoot/ +--- + +Troubleshoot SQL and InfluxQL queries that return unexpected results. + +- [Why doesn't my query return data?](#why-doesnt-my-query-return-data) +- [Optimize slow or expensive queries](#optimize-slow-or-expensive-queries) + +## Why doesn't my query return data? + +If a query doesn't return any data, it might be due to the following: + +- Your data falls outside the time range (or other conditions) in the query--for example, the InfluxQL `SHOW TAG VALUES` command uses a default time range of 1 day. +- The query (InfluxDB server) timed out. +- The query client timed out. + +If a query times out or returns an error, it might be due to the following: + +- a bad request +- a server or network problem +- it queries too much data + +[Understand Arrow Flight responses](/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/flight-responses/) and error messages for queries. + +## Optimize slow or expensive queries + +If a query is slow or uses too many compute resources, limit the amount of data that it queries. + +See how to [optimize queries](/influxdb/cloud-serverless/query-data/troubleshoot-and-optimize/optimize-queries/) and use tools to view runtime metrics, identify bottlenecks, and debug queries. diff --git a/content/influxdb/cloud-serverless/reference/client-libraries/v2/arduino.md b/content/influxdb/cloud-serverless/reference/client-libraries/v2/arduino.md index 823a8a83b..1fde8c377 100644 --- a/content/influxdb/cloud-serverless/reference/client-libraries/v2/arduino.md +++ b/content/influxdb/cloud-serverless/reference/client-libraries/v2/arduino.md @@ -23,8 +23,8 @@ prepend: [**Compare tools you can use**](/influxdb/cloud-serverless/get-started/#tools-to-use) to interact with {{% product-name %}}. --- -Arduino is an open-source hardware and software platform used for building electronics projects. +Arduino is an open source hardware and software platform used for building electronics projects. The documentation for this client library is available on GitHub. -Arduino InfluxDB client \ No newline at end of file +Arduino InfluxDB client diff --git a/content/influxdb/cloud-serverless/reference/client-libraries/v2/kotlin.md b/content/influxdb/cloud-serverless/reference/client-libraries/v2/kotlin.md index 13f111dd2..2eb41b715 100644 --- a/content/influxdb/cloud-serverless/reference/client-libraries/v2/kotlin.md +++ b/content/influxdb/cloud-serverless/reference/client-libraries/v2/kotlin.md @@ -22,8 +22,8 @@ prepend: [**Compare tools you can use**](/influxdb/cloud-serverless/get-started/#tools-to-use) to interact with {{% product-name %}}. --- -Kotlin is an open-source programming language that runs on the Java Virtual Machine (JVM). +Kotlin is an open source programming language that runs on the Java Virtual Machine (JVM). The documentation for this client library is available on GitHub. -Kotlin InfluxDB client \ No newline at end of file +Kotlin InfluxDB client diff --git a/content/influxdb/cloud-serverless/reference/glossary.md b/content/influxdb/cloud-serverless/reference/glossary.md index cfd2a7396..c8c078857 100644 --- a/content/influxdb/cloud-serverless/reference/glossary.md +++ b/content/influxdb/cloud-serverless/reference/glossary.md @@ -79,7 +79,7 @@ InfluxData typically recommends batch sizes of 5,000-10,000 points. In some use cases, performance may improve with significantly smaller or larger batches. Related entries: -[line protocol](#line-protocol), +[line protocol](#line-protocol-lp), [point](#point) ### batch size @@ -270,7 +270,7 @@ Aggregating high resolution data into lower resolution data to preserve disk spa ### duration -A data type that represents a duration of time (1s, 1m, 1h, 1d). +A data type that represents a duration of time--for example, `1s`, `1m`, `1h`, `1d`. Retention periods are set using durations. Related entries: @@ -301,7 +301,7 @@ WHERE A key-value pair in InfluxDB's data structure that records a data value. Generally, field values change over time. -Fields are required in InfluxDB's data structure. +Fields are required in InfluxDB's data structure. Related entries: [field key](#field-key), @@ -347,9 +347,6 @@ Related entries: A file block is a fixed-length chunk of data read into memory when requested by an application. -Related entries: -[block](#block) - ### float A real number written with a decimal point dividing the integer and fractional parts (`1.0`, `3.14`, `-20.1`). @@ -413,11 +410,10 @@ Identifiers are tokens that refer to specific database objects such as database names, field keys, measurement names, tag keys, etc. Related entries: -[database](#database) +[database](#database), [field key](#field-key), [measurement](#measurement), -[tag key](#tag-key), - +[tag key](#tag-key) ### influx @@ -438,7 +434,7 @@ and other required processes. ### InfluxDB -An open-source time series database (TSDB) developed by InfluxData. +An open source time series database (TSDB) developed by InfluxData. Written in Go and optimized for fast, high-availability storage and retrieval of time series data in fields such as operations monitoring, application metrics, Internet of Things sensor data, and real-time analytics. @@ -478,7 +474,7 @@ Related entries: The IOx (InfluxDB v3) storage engine is a real-time, columnar database optimized for time series data built in Rust on top of [Apache Arrow](https://arrow.apache.org/) and [DataFusion](https://arrow.apache.org/datafusion/user-guide/introduction.html). -IOx replaces the [TSM](#tsm) storage engine. +IOx replaces the [TSM (Time Structured Merge tree)](#tsm-time-structured-merge-tree) storage engine. ## J @@ -508,11 +504,13 @@ and array data types. ### keyword A keyword is reserved by a program because it has special meaning. -Every programming language has a set of keywords (reserved names) that cannot be used as an identifier. +Every programming language has a set of keywords (reserved names) that cannot be used as identifiers--for example, +you can't use `SELECT` (an SQL keyword) as a variable name in an SQL query. -See a list of [SQL keywords](/influxdb/cloud-serverless/reference/sql/#keywords). +See keyword lists: - +- [SQL keywords](/influxdb/cloud-serverless/reference/sql/#keywords) +- [InfluxQL keywords](/influxdb/cloud-serverless/reference/influxql/#keywords) ## L @@ -582,7 +580,6 @@ Related entries: [cluster](#cluster), [server](#server) - ### now The local server's nanosecond timestamp. @@ -623,7 +620,7 @@ Owners have read/write permissions. Users can have owner roles for databases and other resources. Role permissions are separate from API token permissions. -For additional information on API tokens, see [token](#tokens). +For additional information on API tokens, see [token](#token). ### output plugin @@ -730,6 +727,15 @@ An InfluxDB query returns time series data. See [Query data in InfluxDB](/influxdb/cloud-serverless/query-data/). +### query plan + +A sequence of steps (_nodes_) that the InfluxDB Querier devises and executes to calculate the result of the query in the least amount of time. +A _logical plan_ is a high level representation of a query and doesn't consider cluster configuration or data organization. +A _physical plan_ represents the query execution plan and data flow through plan nodes that read (_scan_), deduplicate, merge, filter, and sort data. +A physical plan is optimized for the cluster configuration and data organization. + +See [Query plans](/influxdb/cloud-serverless/reference/internals/query-plans/). + ## R ### REPL @@ -755,8 +761,7 @@ relative to [now](#now). The minimum retention period is **one hour**. Related entries: -[bucket](#bucket), -[shard group duration](#shard-group-duration) +[bucket](#bucket) ### retention policy (RP) @@ -797,6 +802,18 @@ Related entries: [timestamp](#timestamp), [unix timestamp](#unix-timestamp) +### row + +A row in a [table](#table) represents a specific record or instance of data. +[Column](#column) values in a row represent specific attributes or properties of the instance. +Each row has a [primary key](/#primary-key) that makes the row unique from other rows in the table. + +Related entries: +[column](#column), +[primary key](#primary-key), +[series](#series), +[table](#table) + ## S ### schema @@ -815,8 +832,8 @@ Related entries: ### secret -Secrets are key-value pairs that contain information you want to control access -o, such as API keys, passwords, or certificates. +Secrets are key-value pairs that contain information you want to control access +to, such as API keys, passwords, or certificates. ### selector @@ -886,7 +903,7 @@ A series key identifies a particular series by measurement, tag set, and field k For example: -``` +```text # measurement, tag set, field key h2o_level, location=santa_monica, h2o_feet ``` @@ -953,7 +970,6 @@ Related entries: The key of a tag key-value pair. Tag keys are strings and store metadata. - Related entries: [field key](#field-key), [tag](#tag), @@ -1025,6 +1041,14 @@ Tokens provide authorization to perform specific actions in InfluxDB. Related entries: [Manage tokens](/influxdb/cloud-serverless/admin/tokens/) +### transformation + +Data transformation refers to the process of converting or modifying input data from one format, value, or structure to another. + +InfluxQL [transformation functions](/influxdb/cloud-serverless/reference/influxql/functions/transformations/) modify and return values in each row of queried data, but do not return an aggregated value across those rows. + +Related entries: [aggregate](#aggregate), [function](#function), [selector](#selector) + ### TSM (Time Structured Merge tree) The InfluxDB v1 and v2 data storage format that allows greater compaction and @@ -1085,7 +1109,7 @@ InfluxDB users are granted permission to access to InfluxDB. ### values per second -The preferred measurement of the rate at which data are persisted to InfluxDB. +The preferred measurement of the rate at which data is persisted to InfluxDB. Write speeds are generally quoted in values per second. To calculate the values per second rate, multiply the number of points written diff --git a/content/influxdb/cloud-serverless/reference/internals/durability.md b/content/influxdb/cloud-serverless/reference/internals/durability.md index 5a7208359..d92f167a8 100644 --- a/content/influxdb/cloud-serverless/reference/internals/durability.md +++ b/content/influxdb/cloud-serverless/reference/internals/durability.md @@ -9,7 +9,7 @@ menu: influxdb_cloud_serverless: name: Data durability parent: InfluxDB Cloud internals -influxdb/cloud-dedicated/tags: [backups, internals] +influxdb/cloud-serverless/tags: [backups, internals] related: - https://docs.aws.amazon.com/AmazonS3/latest/userguide/DataDurability.html, AWS S3 Data Durabililty --- @@ -43,7 +43,7 @@ youngest data in the Parquet file ages out of retention. ## Data ingest When data is written to {{< product-name >}}, the data is first written to a -Write-Ahead-Log (WAL) on locally-attached storage on the ingester node before +Write-Ahead-Log (WAL) on locally attached storage on the ingester node before the write request is acknowledged. After acknowledging the write request, the ingester holds the data in memory temporarily and then writes the contents of the WAL to Parquet files in object storage and updates the InfluxDB catalog to @@ -55,7 +55,7 @@ the WAL to the Parquet files before shutting down. {{< product-name >}} implements the following data backup strategies: -- **Backup of WAL file**: The WAL file is written on locally-attached storage. +- **Backup of WAL file**: The WAL file is written on locally attached storage. If an ingester process fails, the new ingester simply reads the WAL file on startup and continues normal operation. WAL files are maintained until their contents have been written to the Parquet files in object storage. diff --git a/content/influxdb/cloud-serverless/reference/internals/query-plan.md b/content/influxdb/cloud-serverless/reference/internals/query-plan.md new file mode 100644 index 000000000..15e3fb1c9 --- /dev/null +++ b/content/influxdb/cloud-serverless/reference/internals/query-plan.md @@ -0,0 +1,392 @@ +--- +title: Query plans +description: > + A query plan is a sequence of steps that the InfluxDB Querier devises and executes to calculate the result of a query in the least amount of time. + InfluxDB query plans include DataFusion and InfluxDB logical plan and execution plan nodes for scanning, deduplicating, filtering, merging, and sorting data. +weight: 201 +menu: + influxdb_cloud_serverless: + name: Query plans + parent: InfluxDB internals +influxdb/cloud-serverless/tags: [query, sql, influxql] +related: + - /influxdb/cloud-serverless/query-data/sql/ + - /influxdb/cloud-serverless/query-data/influxql/ + - /influxdb/cloud-serverless/query-data/execute-queries/analyze-query-plan/ + - /influxdb/cloud-serverless/query-data/execute-queries/troubleshoot/ + - /influxdb/cloud-serverless/reference/internals/storage-engine/ +--- + +A query plan is a sequence of steps that the InfluxDB v3 [Querier](/influxdb/cloud-serverless/reference/internals/storage-engine/#querier) devises and executes to calculate the result of a query. +The Querier uses DataFusion and Arrow to build and execute query plans +that call DataFusion and InfluxDB-specific operators that read data from the [Object store](/influxdb/cloud-serverless/reference/internals/storage-engine/#object-store), and the [Ingester](/influxdb/cloud-serverless/reference/internals/storage-engine/#ingester), and apply query transformations, such as deduplicating, filtering, aggregating, merging, projecting, and sorting to calculate the final result. + +Like many other databases, the [Querier](/influxdb/cloud-serverless/reference/internals/storage-engine/#querier) contains a Query Optimizer. +After it parses an incoming query, the [Querier](/influxdb/cloud-serverless/reference/internals/storage-engine/#querier) builds a _logical plan_--a sequence of high-level steps such as scanning, filtering, and sorting, required for the query. +Following the logical plan, the [Querier](/influxdb/cloud-serverless/reference/internals/storage-engine/#querier) then builds the optimal _physical plan_ to calculate the correct result in the least amount of time. +The plan takes advantage of data partitioning by the [Ingester](/influxdb/cloud-serverless/reference/internals/storage-engine/#ingester) to parallelize plan operations and prune unnecessary data before executing the plan. +The [Querier](/influxdb/cloud-serverless/reference/internals/storage-engine/#querier) also applies common techniques of predicate and projection pushdown to further prune data as early as possible. + +- [Display syntax](#display-syntax) + - [Example logical and physical plan](#example-logical-and-physical-plan) +- [Data flow](#data-flow) +- [Logical plan](#logical-plan) +- [`LogicalPlan` nodes](#logicalplan-nodes) + - [`TableScan`](#tablescan) + - [`Projection`](#projection) + - [`Filter`](#filter) + - [`Sort`](#sort) +- [Physical plan](#physical-plan) +- [`ExecutionPlan` nodes](#executionplan-nodes) + - [`DeduplicateExec`](#deduplicateexec) + - [`EmptyExec`](#emptyexec) + - [`FilterExec`](#filterexec) + - [`ParquetExec`](#parquetexec) + - [`ProjectionExec`](#projectionexec) + - [`RecordBatchesExec`](#recordbatchesexec) + - [`SortExec`](#sortexec) + - [`SortPreservingMergeExec`](#sortpreservingmergeexec) +- [Overlapping data and deduplication](#overlapping-data-and-deduplication) + - [Example of overlapping data](#example-of-overlapping-data) +- [DataFusion query plans](#datafusion-query-plans) + +## Display syntax + +[Logical](#logical-plan) and [physical query plans](#physical-plan) are represented (for example, in an `EXPLAIN` report) in _tree syntax_. + +- Each plan is represented as an upside-down tree composed of _nodes_. +- A parent node awaits the output of its child nodes. +- Data flows up from the bottom innermost nodes of the tree to the outermost _root node_ at the top. + +### Example logical and physical plan + +The following query generates an `EXPLAIN` report that includes a logical and a physical plan: + +```sql +EXPLAIN SELECT city, min_temp, time FROM h2o ORDER BY city ASC, time DESC; +``` + +The output is the following: + +#### Figure 1. EXPLAIN report + +```sql +| plan_type | plan | ++---------------+--------------------------------------------------------------------------+ +| logical_plan | Sort: h2o.city ASC NULLS LAST, h2o.time DESC NULLS FIRST | +| | TableScan: h2o projection=[city, min_temp, time] | +| physical_plan | SortPreservingMergeExec: [city@0 ASC NULLS LAST,time@2 DESC] | +| | UnionExec | +| | SortExec: expr=[city@0 ASC NULLS LAST,time@2 DESC] | +| | ParquetExec: file_groups={...}, projection=[city, min_temp, time] | +| | SortExec: expr=[city@0 ASC NULLS LAST,time@2 DESC] | +| | ParquetExec: file_groups={...}, projection=[city, min_temp, time] | +| | | +``` + +{{% caption %}} +Output from `EXPLAIN SELECT city, min_temp, time FROM h2o ORDER BY city ASC, time DESC;` +{{% /caption %}} + +The leaf nodes in the [Figure 1](#figure-1-explain-report) physical plan are parallel `ParquetExec` nodes: + +```text + ParquetExec: file_groups={...}, projection=[city, min_temp, time] +... + ParquetExec: file_groups={...}, projection=[city, min_temp, time] +``` + +## Data flow + +A [physical plan](#physical-plan) node represents a specific implementation of `ExecutionPlan` that receives an input stream, applies expressions for filtering and sorting, and then yields an output stream to its parent node. + +The following diagram shows the data flow and sequence of `ExecutionPlan` nodes in the [Figure 1](#figure-1-explain-report) physical plan: + + +{{< html-diagram/query-plan >}} + + +{{% product-name %}} includes the following plan expressions: + +## Logical plan + +A logical plan for a query: + +- is a high-level plan that expresses the "intent" of a query and the steps required for calculating the result. +- requires information about the data schema +- is independent of the [physical execution](#physical-plan), cluster configuration, data source (Ingester or Object store), or how data is organized or partitioned +- is displayed as a tree of [DataFusion `LogicalPlan` nodes](#logical-plan-nodes) + +## `LogicalPlan` nodes + +Each node in an {{% product-name %}} logical plan tree represents a [`LogicalPlan` implementation](https://docs.rs/datafusion/latest/datafusion/logical_expr/enum.LogicalPlan.html#variants) that receives criteria extracted from the query and applies relational operators and optimizations for transforming input data to an output table. + +The following are some `LogicalPlan` nodes used in InfluxDB logical plans. + +### `TableScan` + +[`Tablescan`](https://docs.rs/datafusion/latest/datafusion/logical_expr/struct.TableScan.html) retrieves rows from a table provider by reference or from the context. + +### `Projection` + +[`Projection`](https://docs.rs/datafusion/latest/datafusion/logical_expr/struct.Projection.html) evaluates an arbitrary list of expressions on the input; equivalent to an SQL `SELECT` statement with an expression list. + +### `Filter` + +[`Filter`](https://docs.rs/datafusion/latest/datafusion/logical_expr/struct.Filter.html) filters rows from the input that do not satisfy the specified expression; equivalent to an SQL `WHERE` clause with a predicate expression. + +### `Sort` + +[`Sort`](https://docs.rs/datafusion/latest/datafusion/logical_expr/struct.Sort.html) sorts the input according to a list of sort expressions; used to implement SQL `ORDER BY`. + +For details and a list of `LogicalPlan` implementations, see [`Enum datafusion::logical_expr::LogicalPlan` Variants](https://docs.rs/datafusion/latest/datafusion/logical_expr/enum.LogicalPlan.html#variants) in the DataFusion documentation. + +## Physical plan + +A physical plan, or _execution plan_, for a query: + +- is an optimized plan that derives from the [logical plan](#logical-plan) and contains the low-level steps for query execution. +- considers the cluster configuration (for example, CPU and memory allocation) and data organization (for example: partitions, the number of files, and whether files overlap)--for example: + - If you run the same query with the same data on different clusters with different configurations, each cluster may generate a different physical plan for the query. + - If you run the same query on the same cluster at different times, the physical plan may differ each time, depending on the data at query time. +- if generated using `ANALYZE`, includes runtime metrics sampled during query execution +- is displayed as a tree of [`ExecutionPlan` nodes](#execution-plan-nodes) + +## `ExecutionPlan` nodes + +Each node in an {{% product-name %}} physical plan represents a call to a specific implementation of the [DataFusion `ExecutionPlan`](https://docs.rs/datafusion/latest/datafusion/physical_plan/trait.ExecutionPlan.html) +that receives input data, query criteria expressions, and an output schema. + +The following are some `ExecutionPlan` nodes used in InfluxDB physical plans. + +### `DeduplicateExec` + +InfluxDB `DeduplicateExec` takes an input stream of `RecordBatch` sorted on `sort_key` and applies InfluxDB-specific deduplication logic. +The output is dependent on the order of the input rows that have the same key. + +### `EmptyExec` + +DataFusion [`EmptyExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/empty/struct.EmptyExec.html) is an execution plan for an empty relation and indicates that the table doesn't contain data for the time range of the query. + +### `FilterExec` + +The execution plan for the [`Filter`](#filter) `LogicalPlan`. + +DataFusion [`FilterExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/filter/struct.FilterExec.html) evaluates a boolean predicate against all input batches to determine which rows to include in the output batches. + +### `ParquetExec` + +DataFusion [`ParquetExec`](https://docs.rs/datafusion/latest/datafusion/datasource/physical_plan/parquet/struct.ParquetExec.html) scans one or more Parquet partitions. + +#### `ParquetExec` expressions + +##### `file_groups` + +A _file group_ is a list of files to scan. +Files are referenced by path: + +- `1/1/b862a7e9b.../243db601-....parquet` +- `1/1/b862a7e9b.../f5fb7c7d-....parquet` + +In InfluxDB v3, the path structure represents how data is organized. + +A path has the following structure: + +```text +///.parquet + 1 / 1 /b862a7e9b329ee6a4.../243db601-f3f1-4....parquet +``` + +- `namespace_id`: the namespace (database) being queried +- `table_id`: the table (measurement) being queried +- `partition_hash_id`: the partition this file belongs to. +You can count partition IDs to find how many partitions the query reads. +- `uuid_of_the_file`: the file identifier. + +`ParquetExec` processes groups in parallel and reads the files in each group sequentially. + +##### `projection` + +`projection` lists the table columns that the query plan needs to read to execute the query. +The parameter name `projection` refers to _projection pushdown_, the action of filtering columns. + +Consider the following sample data that contains many columns: + +```text +h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 600 +``` + +| table | state | city | min_temp | max_temp | area | time | +|:-----:|:-----:|:----:|:--------:|:--------:|:----:|:----:| +| h2o | CA | SF | 68.4 | 85.7 | 500u | 600 | + +However, the following SQL query specifies only three columns (`city`, `state`, and `time`): + +```sql +SELECT city, count(1) +FROM h2o +WHERE time >= to_timestamp(200) AND time < to_timestamp(700) + AND state = 'MA' +GROUP BY city +ORDER BY city ASC; +``` + +When processing the query, the [Querier](/influxdb/cloud-serverless/reference/internals/storage-engine/#querier) specifies the three required columns in the projection and the projection is "pushed down" to leaf nodes--columns not specified are pruned as early as possible during query execution. + +```text +projection=[city, state, time] +``` + +##### `output_ordering` + +`output_ordering` specifies the sort order for the output. +The Querier specifies `output_ordering` if the output should be ordered and if the [Querier](/influxdb/cloud-serverless/reference/internals/storage-engine/#querier) knows the order. + +When storing data to Parquet files, InfluxDB sorts the data to improve storage compression and query efficiency and the planner tries to preserve that order for as long as possible. +Generally, the `output_ordering` value that `ParquetExec` receives is the ordering (or a subset of the ordering) of stored data. + +_By design, [`RecordBatchesExec`](#recordbatchesexec) data isn't sorted._ + +In the following example, the query planner specifies the output sort order `state ASC, city ASC, time ASC,`: + +```text +output_ordering=[state@2 ASC, city@1 ASC, time@3 ASC, __chunk_order@0 ASC] +``` + +##### `predicate` + +`predicate` is the data filter specified in the query and used for row filtering when scanning Parquet files. + +For example, given the following SQL query: + +```sql +SELECT city, count(1) +FROM h2o +WHERE time >= to_timestamp(200) AND time < to_timestamp(700) + AND state = 'MA' +GROUP BY city +ORDER BY city ASC; +``` + +The `predicate` value is the boolean expression in the `WHERE` statement: + +```text +predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA +``` + +##### `pruning predicate` + +`pruning_predicate` is created from the [`predicate`](#predicate) value and is used for pruning data and files from the chosen partitions. + +For example, given the following `predicate` parsed from the SQL: + +```text +predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA, +``` + +The Querier creates the following `pruning_predicate`: + +```text +pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 +``` + +The default filters files by `time`. + +_Before the physical plan is generated, an additional `partition pruning` step uses predicates on partitioning columns to prune partitions._ + +### `ProjectionExec` + +DataFusion [`ProjectionExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/projection/struct.ProjectionExec.html) evaluates an arbitrary list of expressions on the input; the execution plan for the [`Projection`](#projection) `LogicalPlan`. + +### `RecordBatchesExec` + +The InfluxDB `RecordBatchesExec` implementation retrieves and scans recently written, yet-to-be-persisted, data from the InfluxDB v3 [Ingester](/influxdb/cloud-serverless/reference/internals/storage-engine/#ingester). + +When generating the plan, the [Querier](/influxdb/cloud-serverless/reference/internals/storage-engine/#querier) sends the query criteria, such as database (bucket), table (measurement), and columns, to the [Ingester](/influxdb/cloud-serverless/reference/internals/storage-engine/#ingester) to retrieve data not yet persisted to Parquet files. +If the [Ingester](/influxdb/cloud-serverless/reference/internals/storage-engine/#ingester) has data that meets the criteria (the chunk size is non-zero), then the plan includes `RecordBatchesExec`. + +#### `RecordBatchesExec` attributes + +##### `chunks` + +`chunks` is the number of data chunks from the [Ingester](/influxdb/cloud-serverless/reference/internals/storage-engine/#ingester). +Often one (`1`), but it can be many. + +##### `projection` + +`projection` specifies a list of columns to read and output. + +`__chunk_order` in a list of columns is an InfluxDB-generated column used to keep the chunks and files ordered for deduplication--for example: + +```text +projection=[__chunk_order, city, state, time] +``` + +For details and other DataFusion `ExecutionPlan` implementations, see [`Struct datafusion::datasource::physical_plan` implementors](https://docs.rs/datafusion/latest/datafusion/physical_plan/trait.ExecutionPlan.html) in the DataFusion documentation. + +### `SortExec` + +The execution plan for the [`Sort`](#sort) `LogicalPlan`. + +DataFusion [`SortExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/sorts/sort/struct.SortExec.html) supports sorting datasets that are larger than the memory allotted by the memory manager, by spilling to disk. + +### `SortPreservingMergeExec` + +DataFusion [`SortPreservingMergeExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/sorts/sort_preserving_merge/struct.SortPreservingMergeExec.html) takes an input execution plan and a list of sort expressions and, provided each partition of the input plan is sorted with respect to these sort expressions, yields a single partition sorted with respect to them. + +#### `UnionExec` + +DataFusion [`UnionExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/union/struct.UnionExec.html) is the `UNION ALL` execution plan for combining multiple inputs that have the same schema. +`UnionExec` concatenates the partitions and does not mix or copy data within or across partitions. + +## Overlapping data and deduplication + +_Overlapping data_ refers to files or batches in which the time ranges (represented by timestamps) intersect. +Two _chunks_ of data overlap if both chunks contain data for the same portion of time. + +### Example of overlapping data + +For example, the following chunks represent line protocol written to InfluxDB: + +```text +// Chunk 4: stored parquet file +// - time range: 400-600 +// - no duplicates in its own chunk +// - overlaps chunk 3 +[ + "h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 600", + "h2o,state=CA,city=SJ min_temp=69.5,max_temp=89.2 600", // duplicates row 3 in chunk 5 + "h2o,state=MA,city=Bedford max_temp=80.75,area=742u 400", // overlaps chunk 3 + "h2o,state=MA,city=Boston min_temp=65.40,max_temp=82.67 400", // overlaps chunk 3 +], + +// Chunk 5: Ingester data +// - time range: 550-700 +// - overlaps & duplicates data in chunk 4 +[ +"h2o,state=MA,city=Bedford max_temp=88.75,area=742u 600", // overlaps chunk 4 +"h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 650", +"h2o,state=CA,city=SJ min_temp=68.5,max_temp=90.0 600", // duplicates row 2 in chunk 4 +"h2o,state=CA,city=SJ min_temp=75.5,max_temp=84.08 700", +"h2o,state=MA,city=Boston min_temp=67.4 550", // overlaps chunk 4 +] +``` + +- `Chunk 4` spans the time range `400-600` and represents data persisted to a Parquet file in the [Object store](/influxdb/cloud-serverless/reference/internals/storage-engine/#object-store). +- `Chunk 5` spans the time range `550-700` and represents yet-to-be persisted data from the [Ingester](/influxdb/cloud-serverless/reference/internals/storage-engine/#ingester). +- The chunks overlap the range `550-600`. + +If data overlaps at query time, the [Querier](/influxdb/cloud-serverless/reference/internals/storage-engine/#querier) must include the _deduplication_ process in the query plan, which uses the same multi-column sort-merge operators used by the [Ingester](/influxdb/cloud-serverless/reference/internals/storage-engine/#ingester). +Compared to an ingestion plan that uses sort-merge operators, a query plan is more complex and ensures that data streams through the plan after deduplication. + +Because sort-merge operations used in deduplication have a non-trivial execution cost, InfluxDB v3 tries to avoid the need for deduplication. +Due to how InfluxDB organizes data, a Parquet file never contains duplicates of the data it stores; only overlapped data can contain duplicates. +During compaction, the [Compactor](/influxdb/cloud-serverless/reference/internals/storage-engine/#compactor) sorts stored data to reduce overlaps and optimize query performance. +For data that doesn't have overlaps, the [Querier](/influxdb/cloud-serverless/reference/internals/storage-engine/#querier) doesn't need to include the deduplication process and the query plan can further distribute non-overlapping data for parallel processing. + +## DataFusion query plans + +For more information about DataFusion query plans and the DataFusion API used in InfluxDB v3, see the following: + +- [Query Planning and Execution Overview](https://docs.rs/datafusion/latest/datafusion/index.html#query-planning-and-execution-overview) in the DataFusion documentation. +- [Plan representations](https://docs.rs/datafusion/latest/datafusion/#plan-representations) in the DataFusion documentation. diff --git a/content/influxdb/cloud-serverless/reference/sql/explain.md b/content/influxdb/cloud-serverless/reference/sql/explain.md index a68b1aa4a..c627f6cf2 100644 --- a/content/influxdb/cloud-serverless/reference/sql/explain.md +++ b/content/influxdb/cloud-serverless/reference/sql/explain.md @@ -1,31 +1,41 @@ --- title: EXPLAIN command description: > - The `EXPLAIN` command shows the logical and physical execution plan for the - specified SQL statement. + The `EXPLAIN` command returns the logical and physical execution plans for the specified SQL statement. menu: influxdb_cloud_serverless: name: EXPLAIN command parent: SQL reference weight: 207 +related: + - /influxdb/cloud-serverless/reference/internals/query-plan/ + - /influxdb/cloud-serverless/query-data/execute-queries/analyze-query-plan/ + - /influxdb/cloud-serverless/query-data/execute-queries/troubleshoot/ --- -The `EXPLAIN` command returns the logical and physical execution plan for the +The `EXPLAIN` command returns the [logical plan](/influxdb/cloud-serverless/reference/internals/query-plan/#logical-plan) and the [physical plan](/influxdb/cloud-serverless/reference/internals/query-plan/#physical-plan) for the specified SQL statement. ```sql EXPLAIN [ANALYZE] [VERBOSE] statement ``` -- [EXPLAIN](#explain) -- [EXPLAIN ANALYZE](#explain-analyze) +- [`EXPLAIN`](#explain) + - [Example `EXPLAIN`](#example-explain) +- [`EXPLAIN ANALYZE`](#explain-analyze) + - [Example `EXPLAIN ANALYZE`](#example-explain-analyze) +- [`EXPLAIN ANALYZE VERBOSE`](#explain-analyze-verbose) + - [Example `EXPLAIN ANALYZE VERBOSE`](#example-explain-analyze-verbose) -## EXPLAIN +## `EXPLAIN` -Returns the execution plan of a statement. +Returns the logical plan and physical (execution) plan of a statement. To output more details, use `EXPLAIN VERBOSE`. -##### Example EXPLAIN ANALYZE +`EXPLAIN` doesn't execute the statement. +To execute the statement and view runtime metrics, use [`EXPLAIN ANALYZE`](#explain-analyze). + +### Example `EXPLAIN` ```sql EXPLAIN @@ -39,20 +49,30 @@ GROUP BY room {{< expand-wrapper >}} {{% expand "View `EXPLAIN` example output" %}} -| plan_type | plan | -| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| logical_plan | Projection: home.room, AVG(home.temp) AS temp Aggregate: groupBy=[[home.room]], aggr=[[AVG(home.temp)]] TableScan: home projection=[room, temp] | -| physical_plan | ProjectionExec: expr=[room@0 as room, AVG(home.temp)@1 as temp] AggregateExec: mode=FinalPartitioned, gby=[room@0 as room], aggr=[AVG(home.temp)] CoalesceBatchesExec: target_batch_size=8192 RepartitionExec: partitioning=Hash([Column { name: "room", index: 0 }], 4), input_partitions=4 RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1 AggregateExec: mode=Partial, gby=[room@0 as room], aggr=[AVG(home.temp)] ParquetExec: limit=None, partitions={1 group: [[136/316/1120/1ede0031-e86e-06e5-12ba-b8e6fd76a202.parquet]]}, projection=[room, temp] | +| | plan_type | plan | +|---:|:--------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0 | logical_plan | Projection: home.room, AVG(home.temp) AS temp | +| | | Aggregate: groupBy=[[home.room]], aggr=[[AVG(home.temp)]] | +| | | TableScan: home projection=[room, temp] | +| 1 | physical_plan | ProjectionExec: expr=[room@0 as room, AVG(home.temp)@1 as temp] | +| | | AggregateExec: mode=FinalPartitioned, gby=[room@0 as room], aggr=[AVG(home.temp)] | +| | | CoalesceBatchesExec: target_batch_size=8192 | +| | | RepartitionExec: partitioning=Hash([room@0], 8), input_partitions=8 | +| | | AggregateExec: mode=Partial, gby=[room@0 as room], aggr=[AVG(home.temp)] | +| | | ParquetExec: file_groups={8 groups: [[70434/116281/404d73cea0236530ea94f5470701eb814a8f0565c0e4bef5a2d2e33dfbfc3567/1be334e8-0af8-00da-2615-f67cd4be90f7.parquet, 70434/116281/b7a9e7c57fbfc3bba9427e4b3e35c89e001e2e618b0c7eb9feb4d50a3932f4db/d29370d4-262f-0d32-2459-fe7b099f682f.parquet], [70434/116281/c14418ba28a22a3abb693a1cb326a63b62dc611aec58c9bed438fdafd3bc5882/8b29ae98-761f-0550-2fe4-ee77503658e9.parquet], [70434/116281/fa677477eed622ae8123da1251aa7c351f801e2ee2f0bc28c0fe3002a30b3563/65bb4dc3-04e1-0e02-107a-90cee83c51b0.parquet], [70434/116281/db162bdd30261019960dd70da182e6ebd270284569ecfb5deffea7e65baa0df9/2505e079-67c5-06d9-3ede-89aca542dd18.parquet], [70434/116281/0c025dcccae8691f5fd70b0f131eea4ca6fafb95a02f90a3dc7bb015efd3ab4f/3f3e44c3-b71e-0ca4-3dc7-8b2f75b9ff86.parquet], ...]}, projection=[room, temp] | {{% /expand %}} {{< /expand-wrapper >}} -## EXPLAIN ANALYZE +## `EXPLAIN ANALYZE` -Returns the execution plan and metrics of a statement. -To output more information, use `EXPLAIN ANALYZE VERBOSE`. +Executes a statement and returns the execution plan and runtime metrics of the statement. +The report includes the [logical plan](/influxdb/cloud-serverless/reference/internals/query-plan/#logical-plan) and the [physical plan](/influxdb/cloud-serverless/reference/internals/query-plan/#physical-plan) annotated with execution counters, number of rows produced, and runtime metrics sampled during the query execution. -##### Example EXPLAIN ANALYZE +If the plan requires reading lots of data files, `EXPLAIN` and `EXPLAIN ANALYZE` may truncate the list of files in the report. +To output more information, including intermediate plans and paths for all scanned Parquet files, use [`EXPLAIN ANALYZE VERBOSE`](#explain-analyze-verbose). + +### Example `EXPLAIN ANALYZE` ```sql EXPLAIN ANALYZE @@ -60,15 +80,44 @@ SELECT room, avg(temp) AS temp FROM home +WHERE time >= '2023-01-01' AND time <= '2023-12-31' GROUP BY room ``` {{< expand-wrapper >}} {{% expand "View `EXPLAIN ANALYZE` example output" %}} -| plan_type | plan | -| :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Plan with Metrics | CoalescePartitionsExec, metrics=[output_rows=2, elapsed_compute=8.892µs, spill_count=0, spilled_bytes=0, mem_used=0] ProjectionExec: expr=[room@0 as room, AVG(home.temp)@1 as temp], metrics=[output_rows=2, elapsed_compute=3.608µs, spill_count=0, spilled_bytes=0, mem_used=0] AggregateExec: mode=FinalPartitioned, gby=[room@0 as room], aggr=[AVG(home.temp)], metrics=[output_rows=2, elapsed_compute=121.771µs, spill_count=0, spilled_bytes=0, mem_used=0] CoalesceBatchesExec: target_batch_size=8192, metrics=[output_rows=2, elapsed_compute=23.711µs, spill_count=0, spilled_bytes=0, mem_used=0] RepartitionExec: partitioning=Hash([Column { name: "room", index: 0 }], 4), input_partitions=4, metrics=[repart_time=25.117µs, fetch_time=1.614597ms, send_time=6.705µs] RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1, metrics=[repart_time=1ns, fetch_time=319.754µs, send_time=2.067µs] AggregateExec: mode=Partial, gby=[room@0 as room], aggr=[AVG(home.temp)], metrics=[output_rows=2, elapsed_compute=75.615µs, spill_count=0, spilled_bytes=0, mem_used=0] ParquetExec: limit=None, partitions={1 group: [[136/316/1120/1ede0031-e86e-06e5-12ba-b8e6fd76a202.parquet]]}, projection=[room, temp], metrics=[output_rows=26, elapsed_compute=1ns, spill_count=0, spilled_bytes=0, mem_used=0, pushdown_rows_filtered=0, bytes_scanned=290, row_groups_pruned=0, num_predicate_creation_errors=0, predicate_evaluation_errors=0, page_index_rows_filtered=0, time_elapsed_opening=100.37µs, page_index_eval_time=2ns, time_elapsed_scanning_total=157.086µs, time_elapsed_processing=226.644µs, pushdown_eval_time=2ns, time_elapsed_scanning_until_data=116.875µs] | +| | plan_type | plan | +|---:|:------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0 | Plan with Metrics | ProjectionExec: expr=[room@0 as room, AVG(home.temp)@1 as temp], metrics=[output_rows=2, elapsed_compute=4.768µs] | +| | | AggregateExec: mode=FinalPartitioned, gby=[room@0 as room], aggr=[AVG(home.temp)], ordering_mode=Sorted, metrics=[output_rows=2, elapsed_compute=140.405µs] | +| | | CoalesceBatchesExec: target_batch_size=8192, metrics=[output_rows=2, elapsed_compute=6.821µs] | +| | | RepartitionExec: partitioning=Hash([room@0], 8), input_partitions=8, preserve_order=true, sort_exprs=room@0 ASC, metrics=[output_rows=2, elapsed_compute=18.408µs, repart_time=59.698µs, fetch_time=1.057882762s, send_time=5.83µs] | +| | | AggregateExec: mode=Partial, gby=[room@0 as room], aggr=[AVG(home.temp)], ordering_mode=Sorted, metrics=[output_rows=2, elapsed_compute=137.577µs] | +| | | RepartitionExec: partitioning=RoundRobinBatch(8), input_partitions=6, preserve_order=true, sort_exprs=room@0 ASC, metrics=[output_rows=46, elapsed_compute=26.637µs, repart_time=6ns, fetch_time=399.971411ms, send_time=6.658µs] | +| | | ProjectionExec: expr=[room@0 as room, temp@2 as temp], metrics=[output_rows=46, elapsed_compute=3.102µs] | +| | | CoalesceBatchesExec: target_batch_size=8192, metrics=[output_rows=46, elapsed_compute=25.585µs] | +| | | FilterExec: time@1 >= 1672531200000000000 AND time@1 <= 1703980800000000000, metrics=[output_rows=46, elapsed_compute=26.51µs] | +| | | ParquetExec: file_groups={6 groups: [[70434/116281/404d73cea0236530ea94f5470701eb814a8f0565c0e4bef5a2d2e33dfbfc3567/1be334e8-0af8-00da-2615-f67cd4be90f7.parquet], [70434/116281/c14418ba28a22a3abb693a1cb326a63b62dc611aec58c9bed438fdafd3bc5882/8b29ae98-761f-0550-2fe4-ee77503658e9.parquet], [70434/116281/fa677477eed622ae8123da1251aa7c351f801e2ee2f0bc28c0fe3002a30b3563/65bb4dc3-04e1-0e02-107a-90cee83c51b0.parquet], [70434/116281/db162bdd30261019960dd70da182e6ebd270284569ecfb5deffea7e65baa0df9/2505e079-67c5-06d9-3ede-89aca542dd18.parquet], [70434/116281/0c025dcccae8691f5fd70b0f131eea4ca6fafb95a02f90a3dc7bb015efd3ab4f/3f3e44c3-b71e-0ca4-3dc7-8b2f75b9ff86.parquet], ...]}, projection=[room, time, temp], output_ordering=[room@0 ASC, time@1 ASC], predicate=time@6 >= 1672531200000000000 AND time@6 <= 1703980800000000000, pruning_predicate=time_max@0 >= 1672531200000000000 AND time_min@1 <= 1703980800000000000, required_guarantees=[], metrics=[output_rows=46, elapsed_compute=6ns, predicate_evaluation_errors=0, bytes_scanned=3279, row_groups_pruned_statistics=0, file_open_errors=0, file_scan_errors=0, pushdown_rows_filtered=0, num_predicate_creation_errors=0, row_groups_pruned_bloom_filter=0, page_index_rows_filtered=0, time_elapsed_opening=398.462968ms, time_elapsed_processing=1.626106ms, time_elapsed_scanning_total=1.36822ms, page_index_eval_time=33.474µs, pushdown_eval_time=14.267µs, time_elapsed_scanning_until_data=1.27694ms] | {{% /expand %}} -{{< /expand-wrapper >}} \ No newline at end of file +{{< /expand-wrapper >}} + +## `EXPLAIN ANALYZE VERBOSE` + +Executes a statement and returns the execution plan, runtime metrics, and additional details helpful for debugging the statement. + +The report includes the following: + +- the [logical plan](/influxdb/cloud-serverless/reference/internals/query-plan/#logical-plan) +- the [physical plan](/influxdb/cloud-serverless/reference/internals/query-plan/#physical-plan) annotated with execution counters, number of rows produced, and runtime metrics sampled during the query execution +- Information truncated in the `EXPLAIN` report--for example, the paths for all [Parquet files retrieved for the query](/influxdb/cloud-serverless/reference/internals/query-plan/#file_groups). +- All intermediate physical plans that DataFusion and the [Querier](/influxdb/cloud-serverless/reference/internals/storage-engine/#querier) generate before generating the final physical plan--helpful in debugging to see when an [`ExecutionPlan` node](/influxdb/cloud-serverless/reference/internals/query-plan/#executionplan-nodes) is added or removed, and how InfluxDB optimizes the query. + +### Example `EXPLAIN ANALYZE VERBOSE` + +```SQL +EXPLAIN ANALYZE VERBOSE SELECT temp FROM home +WHERE time >= now() - INTERVAL '7 days' AND room = 'Kitchen' +ORDER BY time +``` diff --git a/content/influxdb/clustered/.vale.ini b/content/influxdb/clustered/.vale.ini index fb93d6412..b13ab3d8e 100644 --- a/content/influxdb/clustered/.vale.ini +++ b/content/influxdb/clustered/.vale.ini @@ -1,6 +1,6 @@ StylesPath = "../../../.ci/vale/styles" -Vocab = InfluxData, Clustered +Vocab = Clustered MinAlertLevel = warning diff --git a/content/influxdb/clustered/install/configure-cluster.md b/content/influxdb/clustered/install/configure-cluster.md index ba42a8527..128397997 100644 --- a/content/influxdb/clustered/install/configure-cluster.md +++ b/content/influxdb/clustered/install/configure-cluster.md @@ -463,7 +463,7 @@ spec: secretKey: value: S3_SECRET_KEY - # Bucket that the parquet files will be stored in + # Bucket that the Parquet files will be stored in bucket: S3_BUCKET_NAME # This value is required for AWS S3, it may or may not be required for other providers. diff --git a/content/influxdb/clustered/install/prerequisites.md b/content/influxdb/clustered/install/prerequisites.md index 3a79b6f6f..65f46ea21 100644 --- a/content/influxdb/clustered/install/prerequisites.md +++ b/content/influxdb/clustered/install/prerequisites.md @@ -14,7 +14,7 @@ weight: 101 InfluxDB Clustered requires the following prerequisites: - **Kubernetes cluster**: version 1.25 or higher -- **Object storage**: AWS S3 or S3-compatible storage used to store the InfluxDB parquet files. +- **Object storage**: AWS S3 or S3-compatible storage used to store the InfluxDB Parquet files. {{% note %}} We **strongly** recommend that you enable object versioning in your object store. diff --git a/content/influxdb/clustered/query-data/execute-queries/_index.md b/content/influxdb/clustered/query-data/execute-queries/_index.md index 9e13c25f4..daf4a4483 100644 --- a/content/influxdb/clustered/query-data/execute-queries/_index.md +++ b/content/influxdb/clustered/query-data/execute-queries/_index.md @@ -14,7 +14,7 @@ aliases: - /influxdb/clustered/query-data/influxql/execute-queries/ --- -Use tools and libraries to query data stored in an {{% product-name %}} bucket. +Use tools and libraries to query data stored in an {{% product-name %}} database. InfluxDB client libraries and Flight clients can use the Flight+gRPC protocol to query with SQL or InfluxQL and retrieve data in the [Arrow in-memory format](https://arrow.apache.org/docs/format/Columnar.html). HTTP clients can use the InfluxDB v1 `/query` REST API to query with InfluxQL and retrieve data in JSON format. diff --git a/content/influxdb/clustered/query-data/execute-queries/optimize-queries.md b/content/influxdb/clustered/query-data/execute-queries/optimize-queries.md deleted file mode 100644 index bc6cc067c..000000000 --- a/content/influxdb/clustered/query-data/execute-queries/optimize-queries.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: Optimize queries -description: > - Optimize your SQL and InfluxQL queries to improve performance and reduce their memory and compute (CPU) requirements. -weight: 401 -menu: - influxdb_clustered: - name: Optimize queries - parent: Execute queries -influxdb/clustered/tags: [query, sql, influxql] -related: - - /influxdb/clustered/query-data/sql/ - - /influxdb/clustered/query-data/influxql/ - - /influxdb/clustered/query-data/execute-queries/troubleshoot/ - - /influxdb/clustered/reference/client-libraries/v3/ ---- - -Use the following tools to help you identify performance bottlenecks and troubleshoot problems in queries: - - - -- [EXPLAIN and ANALYZE](#explain-and-analyze) - - - -### EXPLAIN and ANALYZE - -To view the query engine's execution plan and metrics for an SQL query, prepend [`EXPLAIN`](/influxdb/clustered/reference/sql/explain/) or [`EXPLAIN ANALYZE`](/influxdb/clustered/reference/sql/explain/#explain-analyze) to the query. -The report can reveal query bottlenecks such as a large number of table scans or parquet files, and can help triage the question, "Is the query slow due to the amount of work required or due to a problem with the schema, compactor, etc.?" - -The following example shows how to use the InfluxDB v3 Python client library and pandas to view `EXPLAIN` and `EXPLAIN ANALYZE` results for a query: - - - -{{% code-placeholders "DATABASE_(NAME|TOKEN)" %}} - - -```python -from influxdb_client_3 import InfluxDBClient3 -import pandas as pd -import tabulate # Required for pandas.to_markdown() - -# Instantiate an InfluxDB client. -client = InfluxDBClient3(token = f"DATABASE_TOKEN", - host = f"{{< influxdb/host >}}", - database = f"DATABASE_NAME") - -sql_explain = '''EXPLAIN - SELECT temp - FROM home - WHERE time >= now() - INTERVAL '90 days' - AND room = 'Kitchen' - ORDER BY time''' - -table = client.query(sql_explain) -df = table.to_pandas() -print(df.to_markdown(index=False)) - -assert df.shape == (2, 2), f'Expect {df.shape} to have 2 columns, 2 rows' -assert 'physical_plan' in df.plan_type.values, "Expect physical_plan" -assert 'logical_plan' in df.plan_type.values, "Expect logical_plan" -``` - -{{< expand-wrapper >}} -{{% expand "View EXPLAIN example results" %}} -| plan_type | plan | -|:--------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| logical_plan | Projection: home.temp | -| | Sort: home.time ASC NULLS LAST | -| | Projection: home.temp, home.time | -| | TableScan: home projection=[room, temp, time], full_filters=[home.time >= TimestampNanosecond(1688676582918581320, None), home.room = Dictionary(Int32, Utf8("Kitchen"))] | -| physical_plan | ProjectionExec: expr=[temp@0 as temp] | -| | SortExec: expr=[time@1 ASC NULLS LAST] | -| | EmptyExec: produce_one_row=false | -{{% /expand %}} -{{< /expand-wrapper >}} - - - -```python -sql_explain_analyze = '''EXPLAIN ANALYZE - SELECT * - FROM home - WHERE time >= now() - INTERVAL '90 days' - ORDER BY time''' - -table = client.query(sql_explain_analyze) -df = table.to_pandas() -print(df.to_markdown(index=False)) - -assert df.shape == (1,2) -assert 'Plan with Metrics' in df.plan_type.values, "Expect plan metrics" - -client.close() -``` -{{% /code-placeholders %}} - -Replace the following: - -- {{% code-placeholder-key %}}`DATABASE_NAME`{{% /code-placeholder-key %}}: your {{% product-name %}} database -- {{% code-placeholder-key %}}`DATABASE_TOKEN`{{% /code-placeholder-key %}}: a [database token](/influxdb/cloud-dedicated/admin/tokens/) with sufficient permissions to the specified database - -{{< expand-wrapper >}} -{{% expand "View EXPLAIN ANALYZE example results" %}} -| plan_type | plan | -|:------------------|:-----------------------------------------------------------------------------------------------------------------------| -| Plan with Metrics | ProjectionExec: expr=[temp@0 as temp], metrics=[output_rows=0, elapsed_compute=1ns] | -| | SortExec: expr=[time@1 ASC NULLS LAST], metrics=[output_rows=0, elapsed_compute=1ns, spill_count=0, spilled_bytes=0] | -| | EmptyExec: produce_one_row=false, metrics=[] -{{% /expand %}} -{{< /expand-wrapper >}} diff --git a/content/influxdb/clustered/query-data/influxql/explore-schema.md b/content/influxdb/clustered/query-data/influxql/explore-schema.md index 31ac818c5..440facddb 100644 --- a/content/influxdb/clustered/query-data/influxql/explore-schema.md +++ b/content/influxdb/clustered/query-data/influxql/explore-schema.md @@ -42,10 +42,10 @@ Use InfluxQL `SHOW` statements to return information about your data schema. The following examples use data provided in [sample data sets](/influxdb/clustered/reference/sample-data/). To run the example queries and return identical results, follow the instructions provided for each sample data set to write the data to your {{% product-name %}} -bucket. +database. {{% /note %}} -- [List measurements in a bucket](#list-measurements-in-a-bucket) +- [List measurements in a database](#list-measurements-in-a-database) - [List measurements that contain specific tag key-value pairs](#list-measurements-that-contain-specific-tag-key-value-pairs) - [List measurements that match a regular expression](#list-measurements-that-match-a-regular-expression) - [List field keys in a measurement](#list-field-keys-in-a-measurement) @@ -56,10 +56,10 @@ bucket. - [List tag values for tags that match a regular expression](#list-tag-values-for-tags-that-match-a-regular-expression) - [List tag values associated with a specific tag key-value pair](#list-tag-values-associated-with-a-specific-tag-key-value-pair) -## List measurements in a bucket +## List measurements in a database Use [`SHOW MEASUREMENTS`](/influxdb/clustered/reference/influxql/show/#show-measurements) -to list measurements in your InfluxDB bucket. +to list measurements in your InfluxDB database. ```sql SHOW MEASUREMENTS @@ -83,7 +83,7 @@ name: measurements {{% /expand %}} {{< /expand-wrapper >}} -#### List measurements that contain specific tag key-value pairs +### List measurements that contain specific tag key-value pairs To return only measurements with specific tag key-value pairs, include a `WHERE` clause with tag key-value pairs to query for. @@ -107,7 +107,7 @@ name: measurements {{% /expand %}} {{< /expand-wrapper >}} -#### List measurements that match a regular expression +### List measurements that match a regular expression To return only measurements with names that match a [regular expression](/influxdb/clustered/reference/influxql/regular-expressions/), @@ -137,7 +137,7 @@ name: measurements Use [`SHOW FIELD KEYS`](/influxdb/clustered/reference/influxql/show/#show-field-keys) to return all field keys in a measurement. Include a `FROM` clause to specify the measurement. -If no measurement is specified, the query returns all field keys in the bucket. +If no measurement is specified, the query returns all field keys in the database. ```sql SHOW FIELD KEYS FROM home @@ -164,7 +164,7 @@ name: home Use [`SHOW TAG KEYS`](/influxdb/clustered/reference/influxql/show/#show-field-keys) to return all tag keys in a measurement. Include a `FROM` clause to specify the measurement. -If no measurement is specified, the query returns all tag keys in the bucket. +If no measurement is specified, the query returns all tag keys in the database. ```sql SHOW TAG KEYS FROM home_actions @@ -186,7 +186,7 @@ name: home_actions {{% /expand %}} {{< /expand-wrapper >}} -#### List tag keys in measurements that contain a specific tag key-value pair +### List tag keys in measurements that contain a specific tag key-value pair To return all tag keys measurements that contain specific tag key-value pairs, include a `WHERE` clause with the tag key-value pairs to query for. @@ -264,7 +264,7 @@ name: weather {{% /expand %}} {{< /expand-wrapper >}} -#### List tag values for multiple tags +### List tag values for multiple tags To return tag values for multiple specific tag keys, use the `IN` operator in the `WITH` clause to compare `KEY` to a list of tag keys. @@ -290,7 +290,7 @@ name: home_actions {{% /expand %}} {{< /expand-wrapper >}} -#### List tag values for tags that match a regular expression +### List tag values for tags that match a regular expression To return only tag values from tags keys that match a regular expression, use regular expression comparison operators in your `WITH` clause to compare `KEY` @@ -324,7 +324,7 @@ name: home_actions {{% /expand %}} {{< /expand-wrapper >}} -#### List tag values associated with a specific tag key-value pair +### List tag values associated with a specific tag key-value pair To list tag values for tags associated with a specific tag key-value pair: diff --git a/content/influxdb/clustered/query-data/troubleshoot-and-optimize/_index.md b/content/influxdb/clustered/query-data/troubleshoot-and-optimize/_index.md new file mode 100644 index 000000000..4117fbd9f --- /dev/null +++ b/content/influxdb/clustered/query-data/troubleshoot-and-optimize/_index.md @@ -0,0 +1,23 @@ +--- +title: Troubleshoot and optimize queries +description: > + Troubleshoot errors and optimize performance for SQL and InfluxQL queries in InfluxDB. + Use observability tools to view query execution and metrics. +weight: 201 +menu: + influxdb_clustered: + name: Troubleshoot and optimize queries + parent: Query data +influxdb/clustered/tags: [query, performance, observability, errors, sql, influxql] +related: + - /influxdb/clustered/query-data/sql/ + - /influxdb/clustered/query-data/influxql/ +aliases: + - /influxdb/clustered/query-data/execute-queries/troubleshoot/ + +--- + +Troubleshoot errors and optimize performance for SQL and InfluxQL queries in {{% product-name %}}. +Use observability tools to view query execution and metrics. + +{{< children >}} diff --git a/content/influxdb/clustered/query-data/troubleshoot-and-optimize/analyze-query-plan.md b/content/influxdb/clustered/query-data/troubleshoot-and-optimize/analyze-query-plan.md new file mode 100644 index 000000000..dc88ac07f --- /dev/null +++ b/content/influxdb/clustered/query-data/troubleshoot-and-optimize/analyze-query-plan.md @@ -0,0 +1,771 @@ +--- +title: Analyze a query plan +description: > + Learn how to read and analyze a query plan to + understand how a query is executed and find performance bottlenecks. +weight: 401 +menu: + influxdb_clustered: + name: Analyze a query plan + parent: Troubleshoot and optimize queries +influxdb/clustered/tags: [query, sql, influxql, observability, query plan] +related: + - /influxdb/clustered/query-data/sql/ + - /influxdb/clustered/query-data/influxql/ + - /influxdb/clustered/reference/internals/query-plan/ + - /influxdb/clustered/reference/internals/storage-engine +--- + +Learn how to read and analyze a [query plan](/influxdb/clustered/reference/glossary/#query-plan) to +understand query execution steps and data organization, and find performance bottlenecks. + +When you query InfluxDB v3, the Querier devises a query plan for executing the query. +The engine tries to determine the optimal plan for the query structure and data. +By learning how to generate and interpret reports for the query plan, +you can better understand how the query is executed and identify bottlenecks that affect the performance of your query. + +For example, if the query plan reveals that your query reads a large number of Parquet files, +you can then take steps to [optimize your query](/influxdb/clustered/query-data/optimize-queries/), such as add filters to read less data or +configure your cluster to store fewer and larger files. + +- [Use EXPLAIN keywords to view a query plan](#use-explain-keywords-to-view-a-query-plan) +- [Read an EXPLAIN report](#read-an-explain-report) +- [Read a query plan](#read-a-query-plan) + - [Example physical plan for a SELECT - ORDER BY query](#example-physical-plan-for-a-select---order-by-query) + - [Example `EXPLAIN` report for an empty result set](#example-explain-report-for-an-empty-result-set) +- [Analyze a query plan for leading edge data](#analyze-a-query-plan-for-leading-edge-data) + - [Sample data](#sample-data) + - [Sample query](#sample-query) + - [EXPLAIN report for the leading edge data query](#explain-report-for-the-leading-edge-data-query) + - [Locate the physical plan](#locate-the-physical-plan) + - [Read the physical plan](#read-the-physical-plan) + - [Data scanning nodes (ParquetExec and RecordBatchesExec)](#data-scanning-nodes-parquetexec-and-recordbatchesexec) + - [Analyze branch structures](#analyze-branch-structures) + +## Use EXPLAIN keywords to view a query plan + +Use the `EXPLAIN` keyword (and the optional [`ANALYZE`](/influxdb/clustered/reference/sql/explain/#explain-analyze) and [`VERBOSE`](/influxdb/clustered/reference/sql/explain/#explain-analyze-verbose) keywords) to view the query plans for a query. + +{{% expand-wrapper %}} +{{% expand "Use Python and pandas to view an EXPLAIN report" %}} + +The following example shows how to use the InfluxDB v3 Python client library and pandas to view the `EXPLAIN` report for a query: + + + + + +{{% code-placeholders "DATABASE_(NAME|TOKEN)" %}} + +```python +from influxdb_client_3 import InfluxDBClient3 +import pandas as pd +import tabulate # Required for pandas.to_markdown() + +# Instantiate an InfluxDB client. +client = InfluxDBClient3(token = f"TOKEN", + host = f"{{< influxdb/host >}}", + database = f"DATABASE_NAME") + +sql_explain = '''EXPLAIN + SELECT temp + FROM home + WHERE time >= now() - INTERVAL '7 days' + AND room = 'Kitchen' + ORDER BY time''' + +table = client.query(sql_explain) +df = table.to_pandas() +print(df.to_markdown(index=False)) + +assert df.shape == (2, 2), f'Expect {df.shape} to have 2 columns, 2 rows' +assert 'physical_plan' in df.plan_type.values, "Expect physical_plan" +assert 'logical_plan' in df.plan_type.values, "Expect logical_plan" +``` + +{{% /code-placeholders %}} + +Replace the following: + +- {{% code-placeholder-key %}}`DATABASE_NAME`{{% /code-placeholder-key %}}: your {{% product-name %}} database +- {{% code-placeholder-key %}}`TOKEN`{{% /code-placeholder-key %}}: a [token](/influxdb/clustered/admin/tokens/) with sufficient permissions to the specified database + +{{% /expand %}} +{{% /expand-wrapper %}} + +## Read an EXPLAIN report + +When you [use `EXPLAIN` keywords to view a query plan](#use-explain-keywords-to-view-a-query-plan), the report contains the following: + +- two columns: `plan_type` and `plan` +- one row for the [logical plan](/influxdb/clustered/reference/internals/query-plan/#logical-plan) (`logical_plan`) +- one row for the [physical plan](/influxdb/clustered/reference/internals/query-plan/#physical-plan) (`physical_plan`) + +## Read a query plan + +Plans are in _tree format_--each plan is an upside-down tree in which +execution and data flow from _leaf nodes_, the innermost steps in the plan, to outer _branch nodes_. +Whether reading a logical or physical plan, keep the following in mind: + +- Start at the the _leaf nodes_ and read upward. +- At the top of the plan, the _root node_ represents the final, encompassing step. + +In a [physical plan](/influxdb/clustered/reference/internals/query-plan/#physical-plan), each step is an [`ExecutionPlan` node](/influxdb/clustered/reference/internals/query-plan/#executionplan-nodes) that receives expressions for input data and output requirements, and computes a partition of data. + +Use the following steps to analyze a query plan and estimate how much work is required to complete the query. +The same steps apply regardless of how large or complex the plan might seem. + +1. Start from the furthest indented steps (the _leaf nodes_), and read upward. +2. Understand the job of each [`ExecutionPlan` node](/influxdb/clustered/reference/internals/query-plan/#executionplan-nodes)--for example, a [`UnionExec`](/influxdb/clustered/reference/internals/query-plan/#unionexec) node encompassing the leaf nodes means that the `UnionExec` concatenates the output of all the leaves. +3. For each expression, answer the following questions: + - What is the shape and size of data input to the plan? + - What is the shape and size of data output from the plan? + +The remainder of this guide walks you through analyzing a physical plan. +Understanding the sequence, role, input, and output of nodes in your query plan can help you estimate the overall workload and find potential bottlenecks in the query. + +### Example physical plan for a SELECT - ORDER BY query + +The following example shows how to read an `EXPLAIN` report and a physical query plan. + +Given `h20` measurement data and the following query: + +```sql +EXPLAIN SELECT city, min_temp, time FROM h2o ORDER BY city ASC, time DESC; +``` + +The output is similar to the following: + +#### EXPLAIN report + +```sql +| plan_type | plan | ++---------------+--------------------------------------------------------------------------+ +| logical_plan | Sort: h2o.city ASC NULLS LAST, h2o.time DESC NULLS FIRST | +| | TableScan: h2o projection=[city, min_temp, time] | +| physical_plan | SortPreservingMergeExec: [city@0 ASC NULLS LAST,time@2 DESC] | +| | UnionExec | +| | SortExec: expr=[city@0 ASC NULLS LAST,time@2 DESC] | +| | ParquetExec: file_groups={...}, projection=[city, min_temp, time] | +| | SortExec: expr=[city@0 ASC NULLS LAST,time@2 DESC] | +| | ParquetExec: file_groups={...}, projection=[city, min_temp, time] | +| | | +``` + +{{% caption %}} +Output from `EXPLAIN SELECT city, min_temp, time FROM h2o ORDER BY city ASC, time DESC;` +{{% /caption %}} + +Each step, or _node_, in the physical plan is an `ExecutionPlan` name and the key-value _expressions_ that contain relevant parts of the query--for example, the first node in the [`EXPLAIN` report](#explain-report) physical plan is a `ParquetExec` execution plan: + +```text +ParquetExec: file_groups={...}, projection=[city, min_temp, time] +``` + +Because `ParquetExec` and `RecordBatchesExec` nodes retrieve and scan data in InfluxDB queries, every query plan starts with one or more of these nodes. + +#### Physical plan data flow + +Data flows _up_ in a query plan. + +The following diagram shows the data flow and sequence of nodes in the [`EXPLAIN` report](#explain-report) physical plan: + + +{{< html-diagram/query-plan >}} + + +{{% caption %}} +Execution and data flow in the [`EXPLAIN` report](#explain-report) physical plan. +`ParquetExec` nodes execute in parallel and `UnionExec` combines their output. +{{% /caption %}} + +The following steps summarize the [physical plan execution and data flow](#physical-plan-data-flow): + +1. Two `ParquetExec` plans, in parallel, read data from Parquet files: + - Each `ParquetExec` node processes one or more _file groups_. + - Each file group contains one or more Parquet file paths. + - A `ParquetExec` node processes its groups in parallel, reading each group's files sequentially. + - The output is a stream of data to the corresponding `SortExec` node. +2. The `SortExec` nodes, in parallel, sort the data by `city` (ascending) and `time` (descending). Sorting is required by the `SortPreservingMergeExec` plan. +3. The `UnionExec` node concatenates the streams to union the output of the parallel `SortExec` nodes. +4. The `SortPreservingMergeExec` node merges the previously sorted and unioned data from `UnionExec`. + +### Example `EXPLAIN` report for an empty result set + +If your table doesn't contain data for the time range in your query, the physical plan starts with an `EmptyExec` leaf node--for example: + +{{% code-callout "EmptyExec"%}} + +```sql +ProjectionExec: expr=[temp@0 as temp] + SortExec: expr=[time@1 ASC NULLS LAST] + EmptyExec: produce_one_row=false +``` + +{{% /code-callout %}} + +## Analyze a query plan for leading edge data + +The following sections guide you through analyzing a physical query plan for a typical time series use case--aggregating recently written (_leading edge_) data. +Although the query and plan are more complex than in the [preceding example](#example-physical-plan-for-a-select---order-by-query), you'll follow the same [steps to read the query plan](#read-a-query-plan). +After learning how to read the query plan, you'll have an understanding of `ExecutionPlans`, data flow, and potential query bottlenecks. + +### Sample data + +Consider the following `h20` data, represented as "chunks" of line protocol, written to InfluxDB: + +```text +// h20 data +// The following data represents 5 batches, or "chunks", of line protocol +// written to InfluxDB. +// - Chunks 1-4 are ingested and each is persisted to a separate partition file in storage. +// - Chunk 5 is ingested and not yet persisted to storage. +// - Chunks 1 and 2 cover short windows of time that don't overlap times in other chunks. +// - Chunks 3 and 4 cover larger windows of time and the time ranges overlap each other. +// - Chunk 5 contains the largest time range and overlaps with chunk 4, the Parquet file with the largest time-range. +// - In InfluxDB, a chunk never duplicates its own data. +// +// Chunk 1: stored Parquet file +// - time range: 50-249 +// - no duplicates in its own chunk +// - no overlap with any other chunks +[ +"h2o,state=MA,city=Bedford min_temp=71.59 150", +"h2o,state=MA,city=Boston min_temp=70.4, 50", +"h2o,state=MA,city=Andover max_temp=69.2, 249", +], + +// Chunk 2: stored Parquet file +// - time range: 250-349 +// - no duplicates in its own chunk +// - no overlap with any other chunks +// - adds a new field (area) +[ +"h2o,state=CA,city=SF min_temp=79.0,max_temp=87.2,area=500u 300", +"h2o,state=CA,city=SJ min_temp=75.5,max_temp=84.08 349", +"h2o,state=MA,city=Bedford max_temp=78.75,area=742u 300", +"h2o,state=MA,city=Boston min_temp=65.4 250", +], + +// Chunk 3: stored Parquet file +// - time range: 350-500 +// - no duplicates in its own chunk +// - overlaps chunk 4 +[ +"h2o,state=CA,city=SJ min_temp=77.0,max_temp=90.7 450", +"h2o,state=CA,city=SJ min_temp=69.5,max_temp=88.2 500", +"h2o,state=MA,city=Boston min_temp=68.4 350", +], + +// Chunk 4: stored Parquet file +// - time range: 400-600 +// - no duplicates in its own chunk +// - overlaps chunk 3 +[ + "h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 600", + "h2o,state=CA,city=SJ min_temp=69.5,max_temp=89.2 600", // duplicates row 3 in chunk 5 + "h2o,state=MA,city=Bedford max_temp=80.75,area=742u 400", // overlaps chunk 3 + "h2o,state=MA,city=Boston min_temp=65.40,max_temp=82.67 400", // overlaps chunk 3 +], + +// Chunk 5: Ingester data +// - time range: 550-700 +// - overlaps and duplicates data in chunk 4 +[ +"h2o,state=MA,city=Bedford max_temp=88.75,area=742u 600", // overlaps chunk 4 +"h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 650", +"h2o,state=CA,city=SJ min_temp=68.5,max_temp=90.0 600", // duplicates row 2 in chunk 4 +"h2o,state=CA,city=SJ min_temp=75.5,max_temp=84.08 700", +"h2o,state=MA,city=Boston min_temp=67.4 550", // overlaps chunk 4 +] +``` + +The following query selects all the data: + +```sql +SELECT state, city, min_temp, max_temp, area, time +FROM h2o +ORDER BY state asc, city asc, time desc; +``` + +The output is the following: + +```sql ++-------+---------+----------+----------+------+--------------------------------+ +| state | city | min_temp | max_temp | area | time | ++-------+---------+----------+----------+------+--------------------------------+ +| CA | SF | 68.4 | 85.7 | 500 | 1970-01-01T00:00:00.000000650Z | +| CA | SF | 68.4 | 85.7 | 500 | 1970-01-01T00:00:00.000000600Z | +| CA | SF | 79.0 | 87.2 | 500 | 1970-01-01T00:00:00.000000300Z | +| CA | SJ | 75.5 | 84.08 | | 1970-01-01T00:00:00.000000700Z | +| CA | SJ | 68.5 | 90.0 | | 1970-01-01T00:00:00.000000600Z | +| CA | SJ | 69.5 | 88.2 | | 1970-01-01T00:00:00.000000500Z | +| CA | SJ | 77.0 | 90.7 | | 1970-01-01T00:00:00.000000450Z | +| CA | SJ | 75.5 | 84.08 | | 1970-01-01T00:00:00.000000349Z | +| MA | Andover | | 69.2 | | 1970-01-01T00:00:00.000000249Z | +| MA | Bedford | | 88.75 | 742 | 1970-01-01T00:00:00.000000600Z | +| MA | Bedford | | 80.75 | 742 | 1970-01-01T00:00:00.000000400Z | +| MA | Bedford | | 78.75 | 742 | 1970-01-01T00:00:00.000000300Z | +| MA | Bedford | 71.59 | | | 1970-01-01T00:00:00.000000150Z | +| MA | Boston | 67.4 | | | 1970-01-01T00:00:00.000000550Z | +| MA | Boston | 65.4 | 82.67 | | 1970-01-01T00:00:00.000000400Z | +| MA | Boston | 68.4 | | | 1970-01-01T00:00:00.000000350Z | +| MA | Boston | 65.4 | | | 1970-01-01T00:00:00.000000250Z | +| MA | Boston | 70.4 | | | 1970-01-01T00:00:00.000000050Z | ++-------+---------+----------+----------+------+--------------------------------+ +``` + +### Sample query + +The following query selects leading edge data from the [sample data](#sample-data): + +```sql +SELECT city, count(1) +FROM h2o +WHERE time >= to_timestamp(200) AND time < to_timestamp(700) + AND state = 'MA' +GROUP BY city +ORDER BY city ASC; +``` + +The output is the following: + +```sql ++---------+-----------------+ +| city | COUNT(Int64(1)) | ++---------+-----------------+ +| Andover | 1 | +| Bedford | 3 | +| Boston | 4 | ++---------+-----------------+ +``` + +### EXPLAIN report for the leading edge data query + +The following query generates the `EXPLAIN` report for the preceding [sample query](#sample-query): + +```sql +EXPLAIN SELECT city, count(1) +FROM h2o +WHERE time >= to_timestamp(200) AND time < to_timestamp(700) + AND state = 'MA' +GROUP BY city +ORDER BY city ASC; +``` + +{{< expand-wrapper >}} +{{% expand "EXPLAIN report for a leading edge data query" %}} + +```sql +| plan_type | plan | +| logical_plan | Sort: h2o.city ASC NULLS LAST | +| | Aggregate: groupBy=[[h2o.city]], aggr=[[COUNT(Int64(1))]] | +| | TableScan: h2o projection=[city], full_filters=[h2o.time >= TimestampNanosecond(200, None), h2o.time < TimestampNanosecond(700, None), h2o.state = Dictionary(Int32, Utf8("MA"))] | +| physical_plan | SortPreservingMergeExec: [city@0 ASC NULLS LAST] | +| | SortExec: expr=[city@0 ASC NULLS LAST] | +| | AggregateExec: mode=FinalPartitioned, gby=[city@0 as city], aggr=[COUNT(Int64(1))] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | RepartitionExec: partitioning=Hash([city@0], 4), input_partitions=4 | +| | AggregateExec: mode=Partial, gby=[city@0 as city], aggr=[COUNT(Int64(1))] | +| | RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=3 | +| | UnionExec | +| | ProjectionExec: expr=[city@0 as city] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | FilterExec: time@2 >= 200 AND time@2 < 700 AND state@1 = MA | +| | ParquetExec: file_groups={2 groups: [[1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/243db601-f3f1-401b-afda-82160d8cc1a8.Parquet], [1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/f5fb7c7d-16ac-49ba-a811-69578d05843f.Parquet]]}, projection=[city, state, time], output_ordering=[state@1 ASC, city@0 ASC, time@2 ASC], predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA, pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 | +| | ProjectionExec: expr=[city@1 as city] | +| | DeduplicateExec: [state@2 ASC,city@1 ASC,time@3 ASC] | +| | SortPreservingMergeExec: [state@2 ASC,city@1 ASC,time@3 ASC,__chunk_order@0 ASC] | +| | UnionExec | +| | SortExec: expr=[state@2 ASC,city@1 ASC,time@3 ASC,__chunk_order@0 ASC] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | FilterExec: time@3 >= 200 AND time@3 < 700 AND state@2 = MA | +| | RecordBatchesExec: chunks=1, projection=[__chunk_order, city, state, time] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | FilterExec: time@3 >= 200 AND time@3 < 700 AND state@2 = MA | +| | ParquetExec: file_groups={2 groups: [[1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/2cbb3992-4607-494d-82e4-66c480123189.Parquet], [1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/9255eb7f-2b51-427b-9c9b-926199c85bdf.Parquet]]}, projection=[__chunk_order, city, state, time], output_ordering=[state@2 ASC, city@1 ASC, time@3 ASC, __chunk_order@0 ASC], predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA, pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 | +``` + +{{% caption %}} +`EXPLAIN` report for a typical leading edge data query +{{% /caption %}} + +{{% /expand %}} +{{< /expand-wrapper >}} + +The comments in the [sample data](#sample-data) tell you which data chunks _overlap_ or duplicate data in other chunks. +Two chunks of data overlap if there are portions of time for which data exists in both chunks. +_You'll learn how to [recognize overlapping and duplicate data](#recognize-overlapping-and-duplicate-data) in a query plan later in this guide._ + +Unlike the sample data, your data likely doesn't tell you where overlaps or duplicates exist. +A physical plan can reveal overlaps and duplicates in your data and how they affect your queries--for example, after learning how to read a physical plan, you might summarize the data scanning steps as follows: + +- Query execution starts with two `ParquetExec` and one `RecordBatchesExec` execution plans that run in parallel. +- The first `ParquetExec` node reads two files that don't overlap any other files and don't duplicate data; the files don't require deduplication. +- The second `ParquetExec` node reads two files that overlap each other and overlap the ingested data scanned in the `RecordBatchesExec` node; the query plan must include the deduplication process for these nodes before completing the query. + +The remaining sections analyze `ExecutionPlan` node structure and arguments in the example physical plan. +The example includes DataFusion and InfluxDB-specific [`ExecutionPlan` nodes](/influxdb/cloud-dedicated/reference/internals/query-plans/#executionplan-nodes). + +### Locate the physical plan + +To begin analyzing the physical plan for the query, find the row in the [`EXPLAIN` report](#explain-report-for-the-leading-edge-data-query) where the `plan_type` column has the value `physical_plan`. +The `plan` column for the row contains the physical plan. + +### Read the physical plan + +The following sections follow the steps to [read a query plan](#read-a-query-plan) and examine the physical plan nodes and their input and output. + +{{% note %}} +To [read the execution flow of a query plan](#read-a-query-plan), always start from the innermost (leaf) nodes and read up toward the top outermost root node. +{{% /note %}} + +#### Physical plan leaf nodes + +Query physical plan leaf node structures + +{{% caption %}} +Leaf node structures in the physical plan +{{% /caption %}} + +### Data scanning nodes (ParquetExec and RecordBatchesExec) + +The [example physical plan](#physical-plan-leaf-nodes) contains three [leaf nodes](#physical-plan-leaf-nodes)--the innermost nodes where the execution flow begins: + +- [`ParquetExec`](/influxdb/clustered/reference/internals/query-plan/#parquetexec) nodes retrieve and scan data from Parquet files in the [Object store](/influxdb/clustered/reference/internals/storage-engine/#object-store) +- a [`RecordBatchesExec`](/influxdb/clustered/reference/internals/query-plan/#recordbatchesexec) node retrieves recently written, yet-to-be-persisted data from the [Ingester](/influxdb/clustered/reference/internals/storage-engine/#ingester) + +Because `ParquetExec` and `RecordBatchesExec` retrieve and scan data for a query, every query plan starts with one or more of these nodes. + +The number of `ParquetExec` and `RecordBatchesExec` nodes and their parameter values can tell you which data (and how much) is retrieved for your query, and how efficiently the plan handles the organization (for example, partitioning and deduplication) of your data. + +For convenience, this guide uses the names _ParquetExec_A_ and _ParquetExec_B_ for the `ParquetExec` nodes in the [example physical plan](#physical-plan-leaf-nodes) . +Reading from the top of the physical plan, **ParquetExec_A** is the first leaf node in the physical plan and **ParquetExec_B** is the last (bottom) leaf node. + +_The names indicate the nodes' locations in the report, not their order of execution._ + +- [ParquetExec_A](#parquetexec_a) +- [RecordBatchesExec](#recordbatchesexec) +- [ParquetExec_B](#parquetexec_b) + +#### ParquetExec_A + +```sql +ParquetExec: file_groups={2 groups: [[1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/243db601-f3f1-401b-afda-82160d8cc1a8.Parquet], [1/1/b862a7e9b329ee6a418cde191198eaeb1512753f19b87a81def2ae6c3d0ed237/f5fb7c7d-16ac-49ba-a811-69578d05843f.Parquet]]}, projection=[city, state, time], output_ordering=[state@1 ASC, city@0 ASC, time@2 ASC], predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA, pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 | +``` + +{{% caption %}} +ParquetExec_A, the first ParquetExec node +{{% /caption %}} + +ParquetExec_A has the following traits: + +##### `file_groups` + +A _file group_ is a list of files for the operator to read. +Files are referenced by path: + +- `1/1/b862a7e9b.../243db601-....parquet` +- `1/1/b862a7e9b.../f5fb7c7d-....parquet` + +The path structure represents how your data is organized. +You can use the file paths to gather more information about the query--for example: + +- to find file information (for example: size and number of rows) in the catalog +- to download the Parquet file from the Object store for debugging +- to find how many partitions the query reads + +A path has the following structure: + +```text +///.Parquet + 1 / 1 /b862a7e9b329ee6a4.../243db601-f3f1-4....Parquet +``` + +- `namespace_id`: the namespace (database) being queried +- `table_id`: the table (measurement) being queried +- `partition_hash_id`: the partition this file belongs to. +You can count partition IDs to find how many partitions the query reads. +- `uuid_of_the_file`: the file identifier. + +`ParquetExec` processes groups in parallel and reads the files in each group sequentially. + +```text +file_groups={2 groups: [[1/1/b862a7e9b329ee6a4/243db601....parquet], [1/1/b862a7e9b329ee6a4/f5fb7c7d....parquet]]} +``` + +- `{2 groups: [[file], [file]}`: ParquetExec_A receives two groups with one file per group. +Therefore, ParquetExec_A reads two files in parallel. + +##### `projection` + +`projection` lists the table columns for the `ExecutionPlan` to read and output. + +```text +projection=[city, state, time] +``` + +- `[city, state, time]`: the [sample data](#sample-data) contains many columns, but the [sample query](#sample-query) requires the Querier to read only three + +##### `output_ordering` + +`output_ordering` specifies the sort order for the `ExecutionPlan` output. +The Query planner passes the parameter if the output should be ordered and if the planner knows the order. + +```text +output_ordering=[state@2 ASC, city@1 ASC, time@3 ASC, __chunk_order@0 ASC] +``` + +When storing data to Parquet files, InfluxDB sorts the data to improve storage compression and query efficiency and the planner tries to preserve that order for as long as possible. +Generally, the `output_ordering` value that `ParquetExec` receives is the ordering (or a subset of the ordering) of stored data. + +_By design, [`RecordBatchesExec`](#recordbatchesexec) data isn't sorted._ + +In the example, the planner specifies that ParquetExec_A use the existing sort order `state ASC, city ASC, time ASC,` for output. + +{{% note %}} +To view the sort order of your stored data, generate an `EXPLAIN` report for a `SELECT ALL` query--for example: + +```sql +EXPLAIN SELECT * FROM TABLE_NAME WHERE time > now() - interval '1 hour' +``` + +Reduce the time range if the query returns too much data. +{{% /note %}} + +##### `predicate` + +`predicate` is the data filter specified in the query. + +```text +predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA +``` + +##### `pruning predicate` + +`pruning_predicate` is created from the [`predicate`](#predicate) value and is the predicate actually used for pruning data and files from the chosen partitions. +The default filters files by `time`. + +```text +pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 +``` + +_Before the physical plan is generated, an additional `partition pruning` step uses predicates on partitioning columns to prune partitions._ + +#### `RecordBatchesExec` + +```sql +RecordBatchesExec: chunks=1, projection=[__chunk_order, city, state, time] +``` + +{{% caption %}}RecordBatchesExec{{% /caption %}} + +[`RecordBatchesExec`](/influxdb/clustered/reference/internals/query-plan/#recordbatchesexec) is an InfluxDB-specific `ExecutionPlan` implementation that retrieves recently written, yet-to-be-persisted data from the [Ingester](/influxdb/clustered/reference/internals/storage-engine/#ingester). + +In the example, `RecordBatchesExec` contains the following expressions: + +##### `chunks` + +`chunks` is the number of data chunks received from the [Ingester](/influxdb/clustered/reference/internals/storage-engine/#ingester). + +```text +chunks=1 +``` + +- `chunks=1`: `RecordBatchesExec` receives one data chunk. + +##### `projection` + +The `projection` list specifies the columns or expressions for the node to read and output. + +```text +[__chunk_order, city, state, time] +``` + +- `__chunk_order`: orders chunks and files for deduplication +- `city, state, time`: the same columns specified in [`ParquetExec_A projection`](#projection-1) + +{{% note %}} +The presence of `__chunk_order` in data scanning nodes indicates that data overlaps, and is possibly duplicated, among the nodes. +{{% /note %}} + +#### ParquetExec_B + +The bottom leaf node in the [example physical plan](#physical-plan-leaf-nodes) is another `ParquetExec` operator, _ParquetExec_B_. + +##### ParquetExec_B expressions + +```sql +ParquetExec: + file_groups={2 groups: [[1/1/b862a7e9b.../2cbb3992-....Parquet], + [1/1/b862a7e9b.../9255eb7f-....Parquet]]}, + projection=[__chunk_order, city, state, time], + output_ordering=[state@2 ASC, city@1 ASC, time@3 ASC, __chunk_order@0 ASC], + predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA, + pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 +``` + +{{% caption %}}ParquetExec_B, the second ParquetExec{{% /caption %}} + +Because ParquetExec_B has overlaps, the `projection` and `output_ordering` expressions use the `__chunk_order` column used in [`RecordBatchesExec` `projection`](#projection-1). + +{{% note %}} +The presence of `__chunk_order` in data scanning nodes indicates that data overlaps, and is possibly duplicated, among the nodes. +{{% /note %}} + +The remaining ParquetExec_B expressions are similar to those in [ParquetExec_A](#parquetexec_a). + +##### How a query plan distributes data for scanning + +If you compare [`file_group`](#file_groups) paths in [ParquetExec_A](#parquetexec_a) to those in [ParquetExec_B](#parquetexec_b), you'll notice that both contain files from the same partition: + +{{% code-callout "b862a7e9b329ee6a4..." %}} + +```text +1/1/b862a7e9b329ee6a4.../... +``` + +{{% /code-callout %}} + +The planner may distribute files from the same partition to different scan nodes for several reasons, including optimizations for handling [overlaps](#how-a-query-plan-distributes-data-for-scanning)--for example: + +- to separate non-overlapped files from overlapped files to minimize work required for deduplication (which is the case in this example) +- to distribute non-overlapped files to increase parallel execution + +### Analyze branch structures + +After data is output from a data scanning node, it flows up to the next parent (outer) node. + +In the example plan: + +- Each leaf node is the first step in a branch of nodes planned for processing the scanned data. +- The three branches execute in parallel. +- After the leaf node, each branch contains the following similar node structure: + +```sql +... +CoalesceBatchesExec: target_batch_size=8192 + FilterExec: time@3 >= 200 AND time@3 < 700 AND state@2 = MA + ... +``` + +- `FilterExec: time@3 >= 200 AND time@3 < 700 AND state@2 = MA`: filters data for the condition `time@3 >= 200 AND time@3 < 700 AND state@2 = MA`, and guarantees that all data is pruned. +- `CoalesceBatchesExec: target_batch_size=8192`: combines small batches into larger batches. See the DataFusion [`CoalesceBatchesExec`] documentation. + +#### Sorting yet-to-be-persisted data + +In the `RecordBatchesExec` branch, the node that follows `CoalesceBatchesExec` is a `SortExec` node: + +```sql +SortExec: expr=[state@2 ASC,city@1 ASC,time@3 ASC,__chunk_order@0 ASC] +``` + +The node uses the specified expression `state ASC, city ASC, time ASC, __chunk_order ASC` to sort the yet-to-be-persisted data. +Neither ParquetExec_A nor ParquetExec_B contain a similar node because data in the Object store is already sorted (by the [Ingester](/influxdb/clustered/reference/internals/storage-engine/#ingester) or the [Compactor](/influxdb/clustered/reference/internals/storage-engine/#compactor)) in the given order; the query plan only needs to sort data that arrives from the [Ingester](/influxdb/clustered/reference/internals/storage-engine/#ingester). + +#### Recognize overlapping and duplicate data + +In the example physical plan, the ParquetExec_B and `RecordBatchesExec` nodes share the following parent nodes: + +```sql +... +DeduplicateExec: [state@2 ASC,city@1 ASC,time@3 ASC] + SortPreservingMergeExec: [state@2 ASC,city@1 ASC,time@3 ASC,__chunk_order@0 ASC] + UnionExec + ... +``` + +{{% caption %}}Overlapped data node structure{{% /caption %}} + +1. `UnionExec`: unions multiple streams of input data by concatenating the partitions. `UnionExec` doesn't do any merging and is fast to execute. +2. `SortPreservingMergeExec: [state@2 ASC,city@1 ASC,time@3 ASC,__chunk_order@0 ASC]`: merges already sorted data; indicates that preceding data (from nodes below it) is already sorted. The output data is a single sorted stream. +3. `DeduplicateExec: [state@2 ASC,city@1 ASC,time@3 ASC]`: deduplicates an input stream of sorted data. + Because `SortPreservingMergeExec` ensures a single sorted stream, it often, but not always, precedes `DeduplicateExec`. + +A `DeduplicateExec` node indicates that encompassed nodes have [_overlapped data_](/influxdb/clustered/reference/internals/query-plan/#overlapping-data-and-deduplication)--data in a file or batch have timestamps in the same range as data in another file or batch. +Due to how InfluxDB organizes data, data is never duplicated _within_ a file. + +In the example, the `DeduplicateExec` node encompasses ParquetExec_B and the `RecordBatchesExec` node, which indicates that ParquetExec_B [file group](#file_groups) files overlap the yet-to-be persisted data. + +The following [sample data](#sample-data) excerpt shows overlapping data between a file and [Ingester](/influxdb/clustered/reference/internals/storage-engine/#ingester) data: + +```text +// Chunk 4: stored Parquet file +// - time range: 400-600 +[ + "h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 600", +], + +// Chunk 5: Ingester data +// - time range: 550-700 +// - overlaps and duplicates data in chunk 4 +[ +"h2o,state=MA,city=Bedford max_temp=88.75,area=742u 600", // overlaps chunk 4 +... +"h2o,state=MA,city=Boston min_temp=67.4 550", // overlaps chunk 4 +] +``` + +If files or ingested data overlap, the Querier must include the `DeduplicateExec` in the query plan to remove any duplicates. +`DeduplicateExec` doesn't necessarily indicate that data is duplicated. +If a plan reads many files and performs deduplication on all of them, it might be for the following reasons: + +- the files contain duplicate data +- the Object store has many small overlapped files that the Compactor hasn't compacted yet. After compaction, your query may perform better because it has fewer files to read +- the Compactor isn't keeping up. If the data isn't duplicated and you still have many small overlapping files after compaction, then you might want to review the Compactor's workload and add more resources as needed + +A leaf node that doesn't have a `DeduplicateExec` node in its branch doesn't require deduplication and doesn't overlap other files or [Ingester](/influxdb/clustered/reference/internals/storage-engine/#ingester) data--for example, ParquetExec_A has no overlaps: + +```sql +ProjectionExec:... + CoalesceBatchesExec:... + FilterExec:... + ParquetExec:... +``` + +{{% caption %}} +The absence of a `DeduplicateExec` node means that files don't overlap. +{{% /caption %}} + +##### Data scan output + +`ProjectionExec` nodes filter columns so that only the `city` column remains in the output: + +```sql +`ProjectionExec: expr=[city@0 as city]` +``` + +##### Final processing + +After deduplicating and filtering data in each leaf node, the plan combines the output and then applies aggregation and sorting operators for the final result: + +```sql +| physical_plan | SortPreservingMergeExec: [city@0 ASC NULLS LAST] | +| | SortExec: expr=[city@0 ASC NULLS LAST] | +| | AggregateExec: mode=FinalPartitioned, gby=[city@0 as city], aggr=[COUNT(Int64(1))] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | RepartitionExec: partitioning=Hash([city@0], 4), input_partitions=4 | +| | AggregateExec: mode=Partial, gby=[city@0 as city], aggr=[COUNT(Int64(1))] | +| | RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=3 | +| | UnionExec +``` + +{{% caption %}} +Operator structure for aggregating, sorting, and final output. +{{% /caption %}} + +- `UnionExec`: unions data streams. Note that the number of output streams is the same as the number of input streams--the `UnionExec` node is an intermediate step to downstream operators that actually merge or split data streams. +- `RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=3`: Splits three input streams into four output streams in round-robin fashion. The plan splits streams to increase parallel execution. +- `AggregateExec: mode=Partial, gby=[city@0 as city], aggr=[COUNT(Int64(1))]`: Groups data as specified in the [query](#sample-query): `city, count(1)`. + This node aggregates each of the four streams separately, and then outputs four streams, indicated by `mode=Partial`--the data isn't fully aggregated. +- `RepartitionExec: partitioning=Hash([city@0], 4), input_partitions=4`: Repartitions data on `Hash([city])` and into four streams--each stream contains data for one city. +- `AggregateExec: mode=FinalPartitioned, gby=[city@0 as city], aggr=[COUNT(Int64(1))]`: Applies the final aggregation (`aggr=[COUNT(Int64(1))]`) to the data. `mode=FinalPartitioned` indicates that the data has already been partitioned (by city) and doesn't need further grouping by `AggregateExec`. +- `SortExec: expr=[city@0 ASC NULLS LAST]`: Sorts the four streams of data, each on `city`, as specified in the query. +- `SortPreservingMergeExec: [city@0 ASC NULLS LAST]`: Merges and sorts the four sorted streams for the final output. + +In the preceding examples, the `EXPLAIN` report shows the query plan without executing the query. +To view runtime metrics, such as execution time for a plan and its operators, use [`EXPLAIN ANALYZE`](/influxdb/clustered/reference/sql/explain/#explain-analyze) to generate the report and use [tracing](/influxdb/clustered/query-data/optimize-queries/#enable-trace-logging) for further debugging, if necessary. diff --git a/content/influxdb/clustered/query-data/execute-queries/troubleshoot.md b/content/influxdb/clustered/query-data/troubleshoot-and-optimize/flight-responses.md similarity index 95% rename from content/influxdb/clustered/query-data/execute-queries/troubleshoot.md rename to content/influxdb/clustered/query-data/troubleshoot-and-optimize/flight-responses.md index c90678b25..e9b3170e9 100644 --- a/content/influxdb/clustered/query-data/execute-queries/troubleshoot.md +++ b/content/influxdb/clustered/query-data/troubleshoot-and-optimize/flight-responses.md @@ -7,27 +7,22 @@ weight: 401 menu: influxdb_clustered: name: Understand Flight responses - parent: Execute queries + parent: Troubleshoot and optimize queries influxdb/clustered/tags: [query, sql, influxql] +related: + - /influxdb/clustered/query-data/sql/ + - /influxdb/clustered/query-data/influxql/ + - /influxdb/clustered/reference/client-libraries/v3/ --- Learn how to handle responses and troubleshoot errors encountered when querying {{% product-name %}} with Flight+gRPC and Arrow Flight clients. - - - [InfluxDB Flight responses](#influxdb-flight-responses) - [Stream](#stream) - [Schema](#schema) - - [Example](#example) - [RecordBatch](#recordbatch) - [InfluxDB status and error codes](#influxdb-status-and-error-codes) - [Troubleshoot errors](#troubleshoot-errors) - - [Internal Error: Received RST_STREAM](#internal-error-received-rst_stream) - - [Internal Error: stream terminated by RST_STREAM with NO_ERROR](#internal-error-stream-terminated-by-rst_stream-with-no_error) - - [Invalid Argument: Invalid ticket](#invalid-argument-invalid-ticket) - - [Unauthenticated: Unauthenticated](#unauthenticated-unauthenticated) - - [Unauthorized: Permission denied](#unauthorized-permission-denied) - - [FlightUnavailableError: Could not get default pem root certs](#flightunavailableerror-could-not-get-default-pem-root-certs) ## InfluxDB Flight responses @@ -42,7 +37,7 @@ For example, if you use the [`influxdb3-python` Python client library](/influxdb InfluxDB responds with one of the following: - A [stream](#stream) in Arrow IPC streaming format -- An [error status code](#influxdb-error-codes) and an optional `details` field that contains the status and a message that describes the error +- An [error status code](#influxdb-status-and-error-codes) and an optional `details` field that contains the status and a message that describes the error ### Stream @@ -169,7 +164,6 @@ _For a list of gRPC codes that servers and clients may return, see [Status codes {{% /expand %}} {{< /expand-wrapper >}} - ### Troubleshoot errors #### Internal Error: Received RST_STREAM diff --git a/content/influxdb/clustered/query-data/troubleshoot-and-optimize/optimize-queries.md b/content/influxdb/clustered/query-data/troubleshoot-and-optimize/optimize-queries.md new file mode 100644 index 000000000..18644f588 --- /dev/null +++ b/content/influxdb/clustered/query-data/troubleshoot-and-optimize/optimize-queries.md @@ -0,0 +1,62 @@ +--- +title: Optimize queries +description: > + Optimize queries to improve performance and reduce their memory and compute (CPU) requirements in InfluxDB. + Learn how to use observability tools to analyze query execution and view metrics. +weight: 201 +menu: + influxdb_clustered: + name: Optimize queries + parent: Troubleshoot and optimize queries +influxdb/clustered/tags: [query, performance, observability, errors, sql, influxql] +related: + - /influxdb/clustered/query-data/sql/ + - /influxdb/clustered/query-data/influxql/ +aliases: + - /influxdb/clustered/query-data/execute-queries/optimize-queries/ + - /influxdb/clustered/query-data/execute-queries/analyze-query-plan/ +--- + +Optimize SQL and InfluxQL queries to improve performance and reduce their memory and compute (CPU) requirements. +Learn how to use observability tools to analyze query execution and view metrics. + +- [Why is my query slow?](#why-is-my-query-slow) +- [Strategies for improving query performance](#strategies-for-improving-query-performance) +- [Analyze and troubleshoot queries](#analyze-and-troubleshoot-queries) + +## Why is my query slow? + +Query performance depends on time range and complexity. +If a query is slower than you expect, it might be due to the following reasons: + +- It queries data from a large time range. +- It includes intensive operations, such as querying many string values or `ORDER BY` sorting or re-sorting large amounts of data. + +## Strategies for improving query performance + +The following design strategies generally improve query performance and resource use: + +- Follow [schema design best practices](/influxdb/clustered/write-data/best-practices/schema-design/) to make querying easier and more performant. +- Query only the data you need--for example, include a [`WHERE` clause](/influxdb/clustered/reference/sql/where/) that filters data by a time range. + InfluxDB v3 stores data in a Parquet file for each measurement and day, and retrieves files from the Object store to answer a query. + The smaller the time range in your query, the fewer files InfluxDB needs to retrieve from the Object store. + +- [Downsample data](/influxdb/clustered/process-data/downsample/) to reduce the amount of data you need to query. + +Some bottlenecks may be out of your control and are the result of a suboptimal execution plan, such as: + +- Applying the same sort (`ORDER BY`) to already sorted data. +- Retrieving many Parquet files from the Object store--the same query performs better if it retrieves fewer - though, larger - files. +- Querying many overlapped Parquet files. +- Performing a large number of table scans. + +{{% note %}} +#### Analyze query plans to view metrics and recognize bottlenecks + +To view runtime metrics for a query, such as the number of files scanned, use the [`EXPLAIN ANALYZE` keywords](/influxdb/clustered/reference/sql/explain/#explain-analyze) and learn how to [analyze a query plan](/influxdb/clustered/query-data/troubleshoot-and-optimize/analyze-query-plan/). +{{% /note %}} + +## Analyze and troubleshoot queries + +Learn how to [analyze a query plan](/influxdb/clustered/query-data/troubleshoot-and-optimize/analyze-query-plan/) +to troubleshoot queries and find performance bottlenecks. diff --git a/content/influxdb/clustered/query-data/troubleshoot-and-optimize/troubleshoot.md b/content/influxdb/clustered/query-data/troubleshoot-and-optimize/troubleshoot.md new file mode 100644 index 000000000..7454c13ed --- /dev/null +++ b/content/influxdb/clustered/query-data/troubleshoot-and-optimize/troubleshoot.md @@ -0,0 +1,44 @@ +--- +title: Troubleshoot queries +description: > + Troubleshoot SQL and InfluxQL queries in InfluxDB. +weight: 201 +menu: + influxdb_clustered: + name: Troubleshoot queries + parent: Troubleshoot and optimize queries +influxdb/clustered/tags: [query, performance, observability, errors, sql, influxql] +related: + - /influxdb/clustered/query-data/sql/ + - /influxdb/clustered/query-data/influxql/ + - /influxdb/clustered/reference/client-libraries/v3/ +aliases: + - /influxdb/clustered/query-data/execute-queries/troubleshoot/ +--- + +Troubleshoot SQL and InfluxQL queries that return unexpected results. + +- [Why doesn't my query return data?](#why-doesnt-my-query-return-data) +- [Optimize slow or expensive queries](#optimize-slow-or-expensive-queries) + +## Why doesn't my query return data? + +If a query doesn't return any data, it might be due to the following: + +- Your data falls outside the time range (or other conditions) in the query--for example, the InfluxQL `SHOW TAG VALUES` command uses a default time range of 1 day. +- The query (InfluxDB server) timed out. +- The query client timed out. + +If a query times out or returns an error, it might be due to the following: + +- a bad request +- a server or network problem +- it queries too much data + +[Understand Arrow Flight responses](/influxdb/clustered/query-data/troubleshoot-and-optimize/flight-responses/) and error messages for queries. + +## Optimize slow or expensive queries + +If a query is slow or uses too many compute resources, limit the amount of data that it queries. + +See how to [optimize queries](/influxdb/clustered/query-data/troubleshoot-and-optimize/optimize-queries/) and use tools to view runtime metrics, identify bottlenecks, and debug queries. diff --git a/content/influxdb/clustered/reference/client-libraries/v2/arduino.md b/content/influxdb/clustered/reference/client-libraries/v2/arduino.md index a8d8fc337..cfa9cebf0 100644 --- a/content/influxdb/clustered/reference/client-libraries/v2/arduino.md +++ b/content/influxdb/clustered/reference/client-libraries/v2/arduino.md @@ -23,8 +23,8 @@ prepend: [**Compare tools you can use**](/influxdb/clustered/get-started/#tools-to-use) to interact with {{% product-name %}}. --- -Arduino is an open-source hardware and software platform used for building electronics projects. +Arduino is an open source hardware and software platform used for building electronics projects. The documentation for this client library is available on GitHub. -Arduino InfluxDB client \ No newline at end of file +Arduino InfluxDB client diff --git a/content/influxdb/clustered/reference/client-libraries/v2/javascript/nodejs/write.md b/content/influxdb/clustered/reference/client-libraries/v2/javascript/nodejs/write.md index 09158a6bd..3bfa329d5 100644 --- a/content/influxdb/clustered/reference/client-libraries/v2/javascript/nodejs/write.md +++ b/content/influxdb/clustered/reference/client-libraries/v2/javascript/nodejs/write.md @@ -77,7 +77,7 @@ The JavaScript client library includes the following convenient features for wri .floatField('value', 24.0) ``` -5. Use the `writePoint()` method to write the point to your InfluxDB bucket. +5. Use the `writePoint()` method to write the point to your InfluxDB database. Finally, use the `close()` method to flush all pending writes. The example logs the new data point followed by "WRITE FINISHED" to stdout. diff --git a/content/influxdb/clustered/reference/client-libraries/v2/kotlin.md b/content/influxdb/clustered/reference/client-libraries/v2/kotlin.md index 5a588438c..ff7d19f49 100644 --- a/content/influxdb/clustered/reference/client-libraries/v2/kotlin.md +++ b/content/influxdb/clustered/reference/client-libraries/v2/kotlin.md @@ -22,8 +22,8 @@ prepend: [**Compare tools you can use**](/influxdb/clustered/get-started/#tools-to-use) to interact with {{% product-name %}}. --- -Kotlin is an open-source programming language that runs on the Java Virtual Machine (JVM). +Kotlin is an open source programming language that runs on the Java Virtual Machine (JVM). The documentation for this client library is available on GitHub. -Kotlin InfluxDB client \ No newline at end of file +Kotlin InfluxDB client diff --git a/content/influxdb/clustered/reference/glossary.md b/content/influxdb/clustered/reference/glossary.md index 1496a7062..c067954a5 100644 --- a/content/influxdb/clustered/reference/glossary.md +++ b/content/influxdb/clustered/reference/glossary.md @@ -77,7 +77,7 @@ InfluxData typically recommends batch sizes of 5,000-10,000 points. In some use cases, performance may improve with significantly smaller or larger batches. Related entries: -[line protocol](#line-protocol), +[line protocol](#line-protocol-lp), [point](#point) ### batch size @@ -133,9 +133,12 @@ to the workload of a single customer. ### collect -Collect and write time series data to InfluxDB using line protocol, Telegraf, -the InfluxDB v1 and v2 HTTP APIs, v1 and v2 `influx` command line interface (CLI), -and InfluxDB client libraries. +Collect and write time series data to InfluxDB using line protocol and any of the following tools: + +- Telegraf +- the InfluxDB v1 or v2 HTTP APIs +- v1 or v2 `influx` command line interface (CLI) +- InfluxDB client libraries ### collection interval @@ -149,7 +152,7 @@ Related entries: Collection jitter prevents every input plugin from collecting metrics simultaneously, which can have a measurable effect on the system. -For each collection interval, every Telegraf input plugin will sleep for a random +For each collection interval, every Telegraf input plugin sleeps for a random time between zero and the collection jitter before collecting the metrics. Related entries: @@ -260,7 +263,7 @@ Aggregating high resolution data into lower resolution data to preserve disk spa ### duration -A data type that represents a duration of time (1s, 1m, 1h, 1d). +A data type that represents a duration of time--for example, `1s`, `1m`, `1h`, `1d`. Retention periods are set using durations. Related entries: @@ -291,7 +294,7 @@ WHERE A key-value pair in InfluxDB's data structure that records a data value. Generally, field values change over time. -Fields are required in InfluxDB's data structure. +Fields are required in InfluxDB's data structure. Related entries: [field key](#field-key), @@ -337,9 +340,6 @@ Related entries: A file block is a fixed-length chunk of data read into memory when requested by an application. -Related entries: -[block](#block) - ### float A real number written with a decimal point dividing the integer and fractional parts (`1.0`, `3.14`, `-20.1`). @@ -359,7 +359,7 @@ Related entries: Flush jitter prevents every Telegraf output plugin from sending writes simultaneously, which can overwhelm some data sinks. -Each flush interval, every Telegraf output plugin will sleep for a random time +Each flush interval, every Telegraf output plugin sleeps for a random time between zero and the flush jitter before emitting metrics. Flush jitter smooths out write spikes when running a large number of Telegraf instances. @@ -408,7 +408,6 @@ Related entries: [measurement](#measurement), [tag key](#tag-key), - ### influx `influx` is a command line interface (CLI) that interacts with the InfluxDB v1.x and v2.x server. @@ -426,7 +425,7 @@ and other required processes. ### InfluxDB -An open-source time series database (TSDB) developed by InfluxData. +An open source time series database (TSDB) developed by InfluxData. Written in Go and optimized for fast, high-availability storage and retrieval of time series data in fields such as operations monitoring, application metrics, Internet of Things sensor data, and real-time analytics. @@ -463,10 +462,10 @@ Related entries: ### IOx -The IOx (InfluxDB v3) storage engine is real-time, columnar database optimized for time series +The IOx (InfluxDB v3) storage engine is a real-time, columnar database optimized for time series data built in Rust on top of [Apache Arrow](https://arrow.apache.org/) and [DataFusion](https://arrow.apache.org/datafusion/user-guide/introduction.html). -IOx replaces the [TSM](#tsm) storage engine. +IOx replaces the [TSM (Time Structured Merge tree)](#tsm-time-structured-merge-tree) storage engine. ## J @@ -496,11 +495,13 @@ and array data types. ### keyword A keyword is reserved by a program because it has special meaning. -Every programming language has a set of keywords (reserved names) that cannot be used as an identifier. +Every programming language has a set of keywords (reserved names) that cannot be used as identifiers--for example, +you can't use `SELECT` (an SQL keyword) as a variable name in an SQL query. -See a list of [SQL keywords](/influxdb/clustered/reference/sql/#keywords). +See keyword lists: - +- [SQL keywords](/influxdb/clustered/reference/sql/#keywords) +- [InfluxQL keywords](/influxdb/clustered/reference/influxql/#keywords) ## L @@ -570,7 +571,6 @@ Related entries: [cluster](#cluster), [server](#server) - ### now The local server's nanosecond timestamp. @@ -611,7 +611,7 @@ Owners have read/write permissions. Users can have owner roles for databases and other resources. Role permissions are separate from API token permissions. For additional -information on API tokens, see [token](#tokens). +information on API tokens, see [token](#token). ### output plugin @@ -718,6 +718,15 @@ An InfluxDB query returns time series data. See [Query data in InfluxDB](/influxdb/clustered/query-data/). +### query plan + +A sequence of steps (_nodes_) that the InfluxDB Querier devises and executes to calculate the result of the query in the least amount of time. +A _logical plan_ is a high level representation of a query and doesn't consider cluster configuration or data organization. +A _physical plan_ represents the query execution plan and data flow through plan nodes that read (_scan_), deduplicate, merge, filter, and sort data. +A physical plan is optimized for the cluster configuration and data organization. + +See [Query plans](/influxdb/clustered/reference/internals/query-plans/). + ## R ### REPL @@ -744,7 +753,6 @@ The minimum retention period is **one hour**. Related entries: [bucket](#bucket), -[shard group duration](#shard-group-duration) ### retention policy (RP) @@ -785,6 +793,18 @@ Related entries: [timestamp](#timestamp), [unix timestamp](#unix-timestamp) +### row + +A row in a [table](#table) represents a specific record or instance of data. +[Column](#column) values in a row represent specific attributes or properties of the instance. +Each row has a [primary key](/#primary-key) that makes the row unique from other rows in the table. + +Related entries: +[column](#column), +[primary key](#primary-key), +[series](#series), +[table](#table) + ## S ### schema @@ -803,8 +823,8 @@ Related entries: ### secret -Secrets are key-value pairs that contain information you want to control access -o, such as API keys, passwords, or certificates. +Secrets are key-value pairs that contain information you want to control access +to, such as API keys, passwords, or certificates. ### selector @@ -831,7 +851,7 @@ Related entries: The number of unique measurement, tag set, and field key combinations in an InfluxDB database. -For example, assume that an InfluxDB bucket has one measurement. +For example, assume that an InfluxDB database has one measurement. The single measurement has two tag keys: `email` and `status`. If there are three different `email`s, and each email address is associated with two different `status`es, the series cardinality for the measurement is 6 @@ -874,7 +894,7 @@ A series key identifies a particular series by measurement, tag set, and field k For example: -``` +```text # measurement, tag set, field key h2o_level, location=santa_monica, h2o_feet ``` @@ -941,7 +961,6 @@ Related entries: The key of a tag key-value pair. Tag keys are strings and store metadata. - Related entries: [field key](#field-key), [tag](#tag), @@ -1017,6 +1036,14 @@ There are different types of API tokens: Related entries: [Manage token](/influxdb/clustered/admin/tokens/) +### transformation + +Data transformation refers to the process of converting or modifying input data from one format, value, or structure to another. + +InfluxQL [transformation functions](/influxdb/clustered/reference/influxql/functions/transformations/) modify and return values in each row of queried data, but do not return an aggregated value across those rows. + +Related entries: [aggregate](#aggregate), [function](#function), [selector](#selector) + ### TSM (Time Structured Merge tree) The InfluxDB v1 and v2 data storage format that allows greater compaction and @@ -1077,7 +1104,7 @@ InfluxDB users are granted permission to access to InfluxDB. ### values per second -The preferred measurement of the rate at which data are persisted to InfluxDB. +The preferred measurement of the rate at which data is persisted to InfluxDB. Write speeds are generally quoted in values per second. To calculate the values per second rate, multiply the number of points written diff --git a/content/influxdb/clustered/reference/internals/query-plan.md b/content/influxdb/clustered/reference/internals/query-plan.md new file mode 100644 index 000000000..011500208 --- /dev/null +++ b/content/influxdb/clustered/reference/internals/query-plan.md @@ -0,0 +1,392 @@ +--- +title: Query plans +description: > + A query plan is a sequence of steps that the InfluxDB Querier devises and executes to calculate the result of a query in the least amount of time. + InfluxDB query plans include DataFusion and InfluxDB logical plan and execution plan nodes for scanning, deduplicating, filtering, merging, and sorting data. +weight: 201 +menu: + influxdb_clustered: + name: Query plans + parent: InfluxDB internals +influxdb/clustered/tags: [query, sql, influxql] +related: + - /influxdb/clustered/query-data/sql/ + - /influxdb/clustered/query-data/influxql/ + - /influxdb/clustered/query-data/execute-queries/analyze-query-plan/ + - /influxdb/clustered/query-data/execute-queries/troubleshoot/ + - /influxdb/clustered/reference/internals/storage-engine/ +--- + +A query plan is a sequence of steps that the InfluxDB v3 [Querier](/influxdb/clustered/reference/internals/storage-engine/#querier) devises and executes to calculate the result of a query. +The Querier uses DataFusion and Arrow to build and execute query plans +that call DataFusion and InfluxDB-specific operators that read data from the [Object store](/influxdb/clustered/reference/internals/storage-engine/#object-store), and the [Ingester](/influxdb/clustered/reference/internals/storage-engine/#ingester), and apply query transformations, such as deduplicating, filtering, aggregating, merging, projecting, and sorting to calculate the final result. + +Like many other databases, the [Querier](/influxdb/clustered/reference/internals/storage-engine/#querier) contains a Query Optimizer. +After it parses an incoming query, the [Querier](/influxdb/clustered/reference/internals/storage-engine/#querier) builds a _logical plan_--a sequence of high-level steps such as scanning, filtering, and sorting, required for the query. +Following the logical plan, the [Querier](/influxdb/clustered/reference/internals/storage-engine/#querier) then builds the optimal _physical plan_ to calculate the correct result in the least amount of time. +The plan takes advantage of data partitioning by the [Ingester](/influxdb/clustered/reference/internals/storage-engine/#ingester) to parallelize plan operations and prune unnecessary data before executing the plan. +The [Querier](/influxdb/clustered/reference/internals/storage-engine/#querier) also applies common techniques of predicate and projection pushdown to further prune data as early as possible. + +- [Display syntax](#display-syntax) + - [Example logical and physical plan](#example-logical-and-physical-plan) +- [Data flow](#data-flow) +- [Logical plan](#logical-plan) +- [`LogicalPlan` nodes](#logicalplan-nodes) + - [`TableScan`](#tablescan) + - [`Projection`](#projection) + - [`Filter`](#filter) + - [`Sort`](#sort) +- [Physical plan](#physical-plan) +- [`ExecutionPlan` nodes](#executionplan-nodes) + - [`DeduplicateExec`](#deduplicateexec) + - [`EmptyExec`](#emptyexec) + - [`FilterExec`](#filterexec) + - [`ParquetExec`](#parquetexec) + - [`ProjectionExec`](#projectionexec) + - [`RecordBatchesExec`](#recordbatchesexec) + - [`SortExec`](#sortexec) + - [`SortPreservingMergeExec`](#sortpreservingmergeexec) +- [Overlapping data and deduplication](#overlapping-data-and-deduplication) + - [Example of overlapping data](#example-of-overlapping-data) +- [DataFusion query plans](#datafusion-query-plans) + +## Display syntax + +[Logical](#logical-plan) and [physical query plans](#physical-plan) are represented (for example, in an `EXPLAIN` report) in _tree syntax_. + +- Each plan is represented as an upside-down tree composed of _nodes_. +- A parent node awaits the output of its child nodes. +- Data flows up from the bottom innermost nodes of the tree to the outermost _root node_ at the top. + +### Example logical and physical plan + +The following query generates an `EXPLAIN` report that includes a logical and a physical plan: + +```sql +EXPLAIN SELECT city, min_temp, time FROM h2o ORDER BY city ASC, time DESC; +``` + +The output is the following: + +#### Figure 1. EXPLAIN report + +```sql +| plan_type | plan | ++---------------+--------------------------------------------------------------------------+ +| logical_plan | Sort: h2o.city ASC NULLS LAST, h2o.time DESC NULLS FIRST | +| | TableScan: h2o projection=[city, min_temp, time] | +| physical_plan | SortPreservingMergeExec: [city@0 ASC NULLS LAST,time@2 DESC] | +| | UnionExec | +| | SortExec: expr=[city@0 ASC NULLS LAST,time@2 DESC] | +| | ParquetExec: file_groups={...}, projection=[city, min_temp, time] | +| | SortExec: expr=[city@0 ASC NULLS LAST,time@2 DESC] | +| | ParquetExec: file_groups={...}, projection=[city, min_temp, time] | +| | | +``` + +{{% caption %}} +Output from `EXPLAIN SELECT city, min_temp, time FROM h2o ORDER BY city ASC, time DESC;` +{{% /caption %}} + +The leaf nodes in the [Figure 1](#figure-1-explain-report) physical plan are parallel `ParquetExec` nodes: + +```text + ParquetExec: file_groups={...}, projection=[city, min_temp, time] +... + ParquetExec: file_groups={...}, projection=[city, min_temp, time] +``` + +## Data flow + +A [physical plan](#physical-plan) node represents a specific implementation of `ExecutionPlan` that receives an input stream, applies expressions for filtering and sorting, and then yields an output stream to its parent node. + +The following diagram shows the data flow and sequence of `ExecutionPlan` nodes in the [Figure 1](#figure-1-explain-report) physical plan: + + +{{< html-diagram/query-plan >}} + + +{{% product-name %}} includes the following plan expressions: + +## Logical plan + +A logical plan for a query: + +- is a high-level plan that expresses the "intent" of a query and the steps required for calculating the result. +- requires information about the data schema +- is independent of the [physical execution](#physical-plan), cluster configuration, data source (Ingester or Object store), or how data is organized or partitioned +- is displayed as a tree of [DataFusion `LogicalPlan` nodes](#logical-plan-nodes) + +## `LogicalPlan` nodes + +Each node in an {{% product-name %}} logical plan tree represents a [`LogicalPlan` implementation](https://docs.rs/datafusion/latest/datafusion/logical_expr/enum.LogicalPlan.html#variants) that receives criteria extracted from the query and applies relational operators and optimizations for transforming input data to an output table. + +The following are some `LogicalPlan` nodes used in InfluxDB logical plans. + +### `TableScan` + +[`Tablescan`](https://docs.rs/datafusion/latest/datafusion/logical_expr/struct.TableScan.html) retrieves rows from a table provider by reference or from the context. + +### `Projection` + +[`Projection`](https://docs.rs/datafusion/latest/datafusion/logical_expr/struct.Projection.html) evaluates an arbitrary list of expressions on the input; equivalent to an SQL `SELECT` statement with an expression list. + +### `Filter` + +[`Filter`](https://docs.rs/datafusion/latest/datafusion/logical_expr/struct.Filter.html) filters rows from the input that do not satisfy the specified expression; equivalent to an SQL `WHERE` clause with a predicate expression. + +### `Sort` + +[`Sort`](https://docs.rs/datafusion/latest/datafusion/logical_expr/struct.Sort.html) sorts the input according to a list of sort expressions; used to implement SQL `ORDER BY`. + +For details and a list of `LogicalPlan` implementations, see [`Enum datafusion::logical_expr::LogicalPlan` Variants](https://docs.rs/datafusion/latest/datafusion/logical_expr/enum.LogicalPlan.html#variants) in the DataFusion documentation. + +## Physical plan + +A physical plan, or _execution plan_, for a query: + +- is an optimized plan that derives from the [logical plan](#logical-plan) and contains the low-level steps for query execution. +- considers the cluster configuration (for example, CPU and memory allocation) and data organization (for example: partitions, the number of files, and whether files overlap)--for example: + - If you run the same query with the same data on different clusters with different configurations, each cluster may generate a different physical plan for the query. + - If you run the same query on the same cluster at different times, the physical plan may differ each time, depending on the data at query time. +- if generated using `ANALYZE`, includes runtime metrics sampled during query execution +- is displayed as a tree of [`ExecutionPlan` nodes](#execution-plan-nodes) + +## `ExecutionPlan` nodes + +Each node in an {{% product-name %}} physical plan represents a call to a specific implementation of the [DataFusion `ExecutionPlan`](https://docs.rs/datafusion/latest/datafusion/physical_plan/trait.ExecutionPlan.html) +that receives input data, query criteria expressions, and an output schema. + +The following are some `ExecutionPlan` nodes used in InfluxDB physical plans. + +### `DeduplicateExec` + +InfluxDB `DeduplicateExec` takes an input stream of `RecordBatch` sorted on `sort_key` and applies InfluxDB-specific deduplication logic. +The output is dependent on the order of the input rows that have the same key. + +### `EmptyExec` + +DataFusion [`EmptyExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/empty/struct.EmptyExec.html) is an execution plan for an empty relation and indicates that the table doesn't contain data for the time range of the query. + +### `FilterExec` + +The execution plan for the [`Filter`](#filter) `LogicalPlan`. + +DataFusion [`FilterExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/filter/struct.FilterExec.html) evaluates a boolean predicate against all input batches to determine which rows to include in the output batches. + +### `ParquetExec` + +DataFusion [`ParquetExec`](https://docs.rs/datafusion/latest/datafusion/datasource/physical_plan/parquet/struct.ParquetExec.html) scans one or more Parquet partitions. + +#### `ParquetExec` expressions + +##### `file_groups` + +A _file group_ is a list of files to scan. +Files are referenced by path: + +- `1/1/b862a7e9b.../243db601-....parquet` +- `1/1/b862a7e9b.../f5fb7c7d-....parquet` + +In InfluxDB v3, the path structure represents how data is organized. + +A path has the following structure: + +```text +///.parquet + 1 / 1 /b862a7e9b329ee6a4.../243db601-f3f1-4....parquet +``` + +- `namespace_id`: the namespace (database) being queried +- `table_id`: the table (measurement) being queried +- `partition_hash_id`: the partition this file belongs to. +You can count partition IDs to find how many partitions the query reads. +- `uuid_of_the_file`: the file identifier. + +`ParquetExec` processes groups in parallel and reads the files in each group sequentially. + +##### `projection` + +`projection` lists the table columns that the query plan needs to read to execute the query. +The parameter name `projection` refers to _projection pushdown_, the action of filtering columns. + +Consider the following sample data that contains many columns: + +```text +h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 600 +``` + +| table | state | city | min_temp | max_temp | area | time | +|:-----:|:-----:|:----:|:--------:|:--------:|:----:|:----:| +| h2o | CA | SF | 68.4 | 85.7 | 500u | 600 | + +However, the following SQL query specifies only three columns (`city`, `state`, and `time`): + +```sql +SELECT city, count(1) +FROM h2o +WHERE time >= to_timestamp(200) AND time < to_timestamp(700) + AND state = 'MA' +GROUP BY city +ORDER BY city ASC; +``` + +When processing the query, the [Querier](/influxdb/cloud-dedicated/reference/internals/storage-engine/#querier) specifies the three required columns in the projection and the projection is "pushed down" to leaf nodes--columns not specified are pruned as early as possible during query execution. + +```text +projection=[city, state, time] +``` + +##### `output_ordering` + +`output_ordering` specifies the sort order for the output. +The Querier specifies `output_ordering` if the output should be ordered and if the [Querier](/influxdb/clustered/reference/internals/storage-engine/#querier) knows the order. + +When storing data to Parquet files, InfluxDB sorts the data to improve storage compression and query efficiency and the planner tries to preserve that order for as long as possible. +Generally, the `output_ordering` value that `ParquetExec` receives is the ordering (or a subset of the ordering) of stored data. + +_By design, [`RecordBatchesExec`](#recordbatchesexec) data isn't sorted._ + +In the following example, the query planner specifies the output sort order `state ASC, city ASC, time ASC,`: + +```text +output_ordering=[state@2 ASC, city@1 ASC, time@3 ASC, __chunk_order@0 ASC] +``` + +##### `predicate` + +`predicate` is the data filter specified in the query and used for row filtering when scanning Parquet files. + +For example, given the following SQL query: + +```sql +SELECT city, count(1) +FROM h2o +WHERE time >= to_timestamp(200) AND time < to_timestamp(700) + AND state = 'MA' +GROUP BY city +ORDER BY city ASC; +``` + +The `predicate` value is the boolean expression in the `WHERE` statement: + +```text +predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA +``` + +##### `pruning predicate` + +`pruning_predicate` is created from the [`predicate`](#predicate) value and is used for pruning data and files from the chosen partitions. + +For example, given the following `predicate` parsed from the SQL: + +```text +predicate=time@5 >= 200 AND time@5 < 700 AND state@4 = MA, +``` + +The Querier creates the following `pruning_predicate`: + +```text +pruning_predicate=time_max@0 >= 200 AND time_min@1 < 700 AND state_min@2 <= MA AND MA <= state_max@3 +``` + +The default filters files by `time`. + +_Before the physical plan is generated, an additional `partition pruning` step uses predicates on partitioning columns to prune partitions._ + +### `ProjectionExec` + +DataFusion [`ProjectionExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/projection/struct.ProjectionExec.html) evaluates an arbitrary list of expressions on the input; the execution plan for the [`Projection`](#projection) `LogicalPlan`. + +### `RecordBatchesExec` + +The InfluxDB `RecordBatchesExec` implementation retrieves and scans recently written, yet-to-be-persisted, data from the InfluxDB v3 [Ingester](/influxdb/clustered/reference/internals/storage-engine/#ingester). + +When generating the plan, the [Querier](/influxdb/clustered/reference/internals/storage-engine/#querier) sends the query criteria, such as database, table, and columns, to the [Ingester](/influxdb/clustered/reference/internals/storage-engine/#ingester) to retrieve data not yet persisted to Parquet files. +If the [Ingester](/influxdb/clustered/reference/internals/storage-engine/#ingester) has data that meets the criteria (the chunk size is non-zero), then the plan includes `RecordBatchesExec`. + +#### `RecordBatchesExec` attributes + +##### `chunks` + +`chunks` is the number of data chunks from the [Ingester](/influxdb/clustered/reference/internals/storage-engine/#ingester). +Often one (`1`), but it can be many. + +##### `projection` + +`projection` specifies a list of columns to read and output. + +`__chunk_order` in a list of columns is an InfluxDB-generated column used to keep the chunks and files ordered for deduplication--for example: + +```text +projection=[__chunk_order, city, state, time] +``` + +For details and other DataFusion `ExecutionPlan` implementations, see [`Struct datafusion::datasource::physical_plan` implementors](https://docs.rs/datafusion/latest/datafusion/physical_plan/trait.ExecutionPlan.html) in the DataFusion documentation. + +### `SortExec` + +The execution plan for the [`Sort`](#sort) `LogicalPlan`. + +DataFusion [`SortExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/sorts/sort/struct.SortExec.html) supports sorting datasets that are larger than the memory allotted by the memory manager, by spilling to disk. + +### `SortPreservingMergeExec` + +DataFusion [`SortPreservingMergeExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/sorts/sort_preserving_merge/struct.SortPreservingMergeExec.html) takes an input execution plan and a list of sort expressions and, provided each partition of the input plan is sorted with respect to these sort expressions, yields a single partition sorted with respect to them. + +#### `UnionExec` + +DataFusion [`UnionExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/union/struct.UnionExec.html) is the `UNION ALL` execution plan for combining multiple inputs that have the same schema. +`UnionExec` concatenates the partitions and does not mix or copy data within or across partitions. + +## Overlapping data and deduplication + +_Overlapping data_ refers to files or batches in which the time ranges (represented by timestamps) intersect. +Two _chunks_ of data overlap if both chunks contain data for the same portion of time. + +### Example of overlapping data + +For example, the following chunks represent line protocol written to InfluxDB: + +```text +// Chunk 4: stored parquet file +// - time range: 400-600 +// - no duplicates in its own chunk +// - overlaps chunk 3 +[ + "h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 600", + "h2o,state=CA,city=SJ min_temp=69.5,max_temp=89.2 600", // duplicates row 3 in chunk 5 + "h2o,state=MA,city=Bedford max_temp=80.75,area=742u 400", // overlaps chunk 3 + "h2o,state=MA,city=Boston min_temp=65.40,max_temp=82.67 400", // overlaps chunk 3 +], + +// Chunk 5: Ingester data +// - time range: 550-700 +// - overlaps & duplicates data in chunk 4 +[ +"h2o,state=MA,city=Bedford max_temp=88.75,area=742u 600", // overlaps chunk 4 +"h2o,state=CA,city=SF min_temp=68.4,max_temp=85.7,area=500u 650", +"h2o,state=CA,city=SJ min_temp=68.5,max_temp=90.0 600", // duplicates row 2 in chunk 4 +"h2o,state=CA,city=SJ min_temp=75.5,max_temp=84.08 700", +"h2o,state=MA,city=Boston min_temp=67.4 550", // overlaps chunk 4 +] +``` + +- `Chunk 4` spans the time range `400-600` and represents data persisted to a Parquet file in the [Object store](/influxdb/clustered/reference/internals/storage-engine/#object-store). +- `Chunk 5` spans the time range `550-700` and represents yet-to-be persisted data from the [Ingester](/influxdb/clustered/reference/internals/storage-engine/#ingester). +- The chunks overlap the range `550-600`. + +If data overlaps at query time, the [Querier](/influxdb/clustered/reference/internals/storage-engine/#querier) must include the _deduplication_ process in the query plan, which uses the same multi-column sort-merge operators used by the [Ingester](/influxdb/clustered/reference/internals/storage-engine/#ingester). +Compared to an ingestion plan that uses sort-merge operators, a query plan is more complex and ensures that data streams through the plan after deduplication. + +Because sort-merge operations used in deduplication have a non-trivial execution cost, InfluxDB v3 tries to avoid the need for deduplication. +Due to how InfluxDB organizes data, a Parquet file never contains duplicates of the data it stores; only overlapped data can contain duplicates. +During compaction, the [Compactor](/influxdb/clustered/reference/internals/storage-engine/#compactor) sorts stored data to reduce overlaps and optimize query performance. +For data that doesn't have overlaps, the [Querier](/influxdb/clustered/reference/internals/storage-engine/#querier) doesn't need to include the deduplication process and the query plan can further distribute non-overlapping data for parallel processing. + +## DataFusion query plans + +For more information about DataFusion query plans and the DataFusion API used in InfluxDB v3, see the following: + +- [Query Planning and Execution Overview](https://docs.rs/datafusion/latest/datafusion/index.html#query-planning-and-execution-overview) in the DataFusion documentation. +- [Plan representations](https://docs.rs/datafusion/latest/datafusion/#plan-representations) in the DataFusion documentation. diff --git a/content/influxdb/clustered/reference/sql/_index.md b/content/influxdb/clustered/reference/sql/_index.md index e9cf9a917..bb214e16b 100644 --- a/content/influxdb/clustered/reference/sql/_index.md +++ b/content/influxdb/clustered/reference/sql/_index.md @@ -68,7 +68,7 @@ A literal is an explicit value not represented by an identifier. ### String literals -String literals are surrounded by single quotes. +String literals are surrounded by single quotes. ```sql 'santa_monica' @@ -113,7 +113,7 @@ Boolean literals are either `TRUE` or `FALSE`. ## Duration units -Interval literals specify a length or unit of time. +Interval literals specify a length or unit of time. ```sql INTERVAL '4 minutes' @@ -127,16 +127,16 @@ The following units of time are supported: - milliseconds - seconds - minutes -- hours -- days +- hours +- days - weeks -- months +- months - years - century ## Operators -Operators are reserved words or characters which perform certain operations, including comparisons and arithmetic. +Operators are reserved words or characters which perform certain operations, including comparisons and arithmetic. ### Arithmetic operators @@ -278,6 +278,7 @@ Use the SQL `SELECT` statement to query data from a specific measurement or meas ```sql SELECT * FROM "h2o_feet" ``` + ### WHERE clause Use the `WHERE` clause to filter results based on `fields`, `tags`, and `timestamps`. @@ -290,17 +291,18 @@ Rows that evaluate as `FALSE` are omitted from the result set. ```sql SELECT * FROM "h2o_feet" WHERE "water_level" <= 9 ``` + ```sql -SELECT +SELECT * -FROM +FROM "h2o_feet" -WHERE +WHERE "location" = 'santa_monica' AND "level description" = 'below 3 feet' ``` -### JOIN clause +### JOIN clause Use the `JOIN` clause to join data from multiple measurements (tables). The following joins are supported: @@ -326,60 +328,60 @@ Use the `JOIN` clause to join data from multiple measurements (tables). The fol The `INNER JOIN` clause gathers data where there is a match between the two measurements being joined. ```sql -SELECT +SELECT * -FROM - h2o_feet - INNER JOIN h2o_temperature ON h2o_feet.location = h2o_temperature.location +FROM + h2o_feet + INNER JOIN h2o_temperature ON h2o_feet.location = h2o_temperature.location AND h2o_feet.time = h2o_temperature.time ``` -The `LEFT JOIN` and `LEFT OUTER JOIN` clauses gather data from all rows in the left table regardless of whether there is a match in the right table. +The `LEFT JOIN` and `LEFT OUTER JOIN` clauses gather data from all rows in the left table regardless of whether there is a match in the right table. ```sql -SELECT +SELECT * -FROM - h2o_feet - LEFT OUTER JOIN h2o_temperature ON h2o_feet.location = h2o_temperature.location +FROM + h2o_feet + LEFT OUTER JOIN h2o_temperature ON h2o_feet.location = h2o_temperature.location AND h2o_feet.time = h2o_temperature.time ``` The `RIGHT JOIN` and `RIGHT OUTER JOIN` clauses gather data from all rows in the right table regardless of whether there is a match in the left table ```sql -SELECT +SELECT * -FROM - h2o_feet - RIGHT OUTER JOIN h2o_temperature ON h2o_feet.location = h2o_temperature.location +FROM + h2o_feet + RIGHT OUTER JOIN h2o_temperature ON h2o_feet.location = h2o_temperature.location AND h2o_feet.time = h2o_temperature.time ``` The `FULL JOIN` and `FULL OUTER JOIN` clauses return all rows from the left and the right side of the join with `NULL` values where there is no match. ```sql -SELECT +SELECT * -FROM +FROM h2o_feet - FULL JOIN h2o_temperature ON h2o_feet.location = h2o_temperature.location + FULL JOIN h2o_temperature ON h2o_feet.location = h2o_temperature.location AND h2o_feet.time = h2o_temperature.time ``` -### GROUP BY clause +### GROUP BY clause Use the `GROUP BY` clause to group query results based on specified column values. `GROUP BY` **requires** an aggregate or selector function in the `SELECT` statement. #### Examples ```sql -SELECT +SELECT MEAN("water_level"), "location" -FROM +FROM "h2o_feet" -GROUP BY +GROUP BY "location" ``` @@ -391,14 +393,14 @@ GROUP BY #### Examples ```sql -SELECT +SELECT MEAN("water_level"), "location" -FROM +FROM "h2o_feet" -GROUP BY +GROUP BY "location" -HAVING +HAVING MEAN("water_level") > 4 ORDER BY "location" @@ -406,23 +408,23 @@ ORDER BY ### UNION clause -The `UNION` clause combines the results of two or more `SELECT` statements without returning any duplicate rows. `UNION ALL` returns all results, including duplicates. +The `UNION` clause combines the results of two or more `SELECT` statements without returning any duplicate rows. `UNION ALL` returns all results, including duplicates. #### Examples ```sql -SELECT +SELECT 'pH' -FROM +FROM "h2o_pH" -UNION ALL -SELECT +UNION ALL +SELECT "location" -FROM +FROM "h2o_quality" ``` -### ORDER BY clause +### ORDER BY clause The `ORDER BY` clause orders results by specified columns and order. Sort data based on fields, tags, and timestamps. @@ -434,12 +436,12 @@ The following orders are supported: #### Examples ```sql -SELECT +SELECT "water_level", "location" -FROM +FROM "h2o_feet" -ORDER BY +ORDER BY "location", "time" DESC ``` @@ -452,50 +454,50 @@ The defined limit should be a non-negative integer. #### Examples ```sql -SELECT +SELECT "water_level", "location" -FROM +FROM "h2o_feet" -LIMIT +LIMIT 10 ``` -### WITH clause +### WITH clause The `WITH` clause provides a way to write auxiliary statements for use in a larger query. -It can help break down large, complicated queries into simpler forms. +It can help break down large, complicated queries into simpler forms. ```sql WITH summary_data as -(SELECT degrees, location, time +(SELECT degrees, location, time FROM average_temperature) SELECT * FROM summary_data ``` -### OVER clause +### OVER clause The `OVER` clause is used with SQL window functions. A **window function** performs a calculation across a set of table rows that are related in some way to the current row. While similar to aggregate functions, window functions output results into rows retaining their separate identities. ```sql -SELECT +SELECT time, - water_level -FROM + water_level +FROM ( - SELECT + SELECT time, "water_level", row_number() OVER ( - order by + ORDER BY water_level desc - ) as rn - FROM + ) as rn + FROM h2o_feet ) -WHERE +WHERE rn <= 3; ``` @@ -526,7 +528,7 @@ SHOW columns FROM ## Functions -Following is a list of supported functions by type. +Following is a list of supported functions by type. ### Aggregate functions @@ -559,7 +561,8 @@ GROUP BY "location" ### Selector functions -Selector functions are unique to InfluxDB. They behave like aggregate functions in that they take a row of data and compute it down to a single value. However, selectors are unique in that they return a **time value** in addition to the computed value. In short, selectors return an aggregated value along with a timestamp. +Selector functions are unique to InfluxDB. They behave like aggregate functions in that they take a row of data and compute it down to a single value. +However, selectors are unique in that they return a **time value** in addition to the computed value. In short, selectors return an aggregated value along with a timestamp. | Function | Description | | :--------------- | :-------------------------------------------------------------- | @@ -571,12 +574,12 @@ Selector functions are unique to InfluxDB. They behave like aggregate functions #### Examples ```sql -SELECT +SELECT SELECTOR_MAX("pH", time)['value'], SELECTOR_MAX("pH", time)['time'] FROM "h2o_pH" -SELECT +SELECT SELECTOR_LAST("water_level", time)['value'], SELECTOR_LAST("water_level", time)['time'] FROM "h2o_feet" @@ -591,8 +594,8 @@ WHERE time >= timestamp '2019-09-10T00:00:00Z' AND time <= timestamp '2019-09-19 | DATE_BIN() | Bins the input timestamp into a specified interval. | | DATE_TRUNC() | Truncates a timestamp expression based on the date part specified, such as hour, day, or month. | | DATE_PART() | Returns the specified part of a date. | -| NOW() | Returns the current time (UTC). | - +| NOW() | Returns the current time (UTC). | + #### Examples ```sql @@ -618,7 +621,6 @@ GROUP BY time | APPROX_PERCENTILE_CONT | Returns the approximate percentile of input values. | | APPROX_PERCENTILE_CONT_WITH_WEIGHT | Returns the approximate percentile of input values with weight. | - ### Math functions | Function | Description | @@ -650,7 +652,6 @@ GROUP BY time | COALESCE | Returns the first argument that is not null. If all arguments are null, then `COALESCE` will return nulls. | | NULLIF | Returns a null value if value1 equals value2, otherwise returns value1. | - ### Regular expression functions | Function | Description | diff --git a/content/influxdb/clustered/reference/sql/explain.md b/content/influxdb/clustered/reference/sql/explain.md index 76c4f2edf..c0d602149 100644 --- a/content/influxdb/clustered/reference/sql/explain.md +++ b/content/influxdb/clustered/reference/sql/explain.md @@ -1,31 +1,41 @@ --- title: EXPLAIN command description: > - The `EXPLAIN` command shows the logical and physical execution plan for the - specified SQL statement. + The `EXPLAIN` command returns the logical and physical execution plans for the specified SQL statement. menu: influxdb_clustered: name: EXPLAIN command parent: SQL reference weight: 207 +related: + - /influxdb/clustered/reference/internals/query-plan/ + - /influxdb/clustered/query-data/execute-queries/analyze-query-plan/ + - /influxdb/clustered/query-data/execute-queries/troubleshoot/ --- -The `EXPLAIN` command returns the logical and physical execution plan for the +The `EXPLAIN` command returns the [logical plan](/influxdb/clustered/reference/internals/query-plan/#logical-plan) and the [physical plan](/influxdb/clustered/reference/internals/query-plan/#physical-plan) for the specified SQL statement. ```sql EXPLAIN [ANALYZE] [VERBOSE] statement ``` -- [EXPLAIN](#explain) -- [EXPLAIN ANALYZE](#explain-analyze) +- [`EXPLAIN`](#explain) + - [Example `EXPLAIN`](#example-explain) +- [`EXPLAIN ANALYZE`](#explain-analyze) + - [Example `EXPLAIN ANALYZE`](#example-explain-analyze) +- [`EXPLAIN ANALYZE VERBOSE`](#explain-analyze-verbose) + - [Example `EXPLAIN ANALYZE VERBOSE`](#example-explain-analyze-verbose) -## EXPLAIN +## `EXPLAIN` -Returns the execution plan of a statement. +Returns the logical plan and physical (execution) plan of a statement. To output more details, use `EXPLAIN VERBOSE`. -##### Example EXPLAIN ANALYZE +`EXPLAIN` doesn't execute the statement. +To execute the statement and view runtime metrics, use [`EXPLAIN ANALYZE`](#explain-analyze). + +### Example `EXPLAIN` ```sql EXPLAIN @@ -39,20 +49,30 @@ GROUP BY room {{< expand-wrapper >}} {{% expand "View `EXPLAIN` example output" %}} -| plan_type | plan | -| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| logical_plan | Projection: home.room, AVG(home.temp) AS temp Aggregate: groupBy=[[home.room]], aggr=[[AVG(home.temp)]] TableScan: home projection=[room, temp] | -| physical_plan | ProjectionExec: expr=[room@0 as room, AVG(home.temp)@1 as temp] AggregateExec: mode=FinalPartitioned, gby=[room@0 as room], aggr=[AVG(home.temp)] CoalesceBatchesExec: target_batch_size=8192 RepartitionExec: partitioning=Hash([Column { name: "room", index: 0 }], 4), input_partitions=4 RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1 AggregateExec: mode=Partial, gby=[room@0 as room], aggr=[AVG(home.temp)] ParquetExec: limit=None, partitions={1 group: [[136/316/1120/1ede0031-e86e-06e5-12ba-b8e6fd76a202.parquet]]}, projection=[room, temp] | +| | plan_type | plan | +|---:|:--------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0 | logical_plan | Projection: home.room, AVG(home.temp) AS temp | +| | | Aggregate: groupBy=[[home.room]], aggr=[[AVG(home.temp)]] | +| | | TableScan: home projection=[room, temp] | +| 1 | physical_plan | ProjectionExec: expr=[room@0 as room, AVG(home.temp)@1 as temp] | +| | | AggregateExec: mode=FinalPartitioned, gby=[room@0 as room], aggr=[AVG(home.temp)] | +| | | CoalesceBatchesExec: target_batch_size=8192 | +| | | RepartitionExec: partitioning=Hash([room@0], 8), input_partitions=8 | +| | | AggregateExec: mode=Partial, gby=[room@0 as room], aggr=[AVG(home.temp)] | +| | | ParquetExec: file_groups={8 groups: [[70434/116281/404d73cea0236530ea94f5470701eb814a8f0565c0e4bef5a2d2e33dfbfc3567/1be334e8-0af8-00da-2615-f67cd4be90f7.parquet, 70434/116281/b7a9e7c57fbfc3bba9427e4b3e35c89e001e2e618b0c7eb9feb4d50a3932f4db/d29370d4-262f-0d32-2459-fe7b099f682f.parquet], [70434/116281/c14418ba28a22a3abb693a1cb326a63b62dc611aec58c9bed438fdafd3bc5882/8b29ae98-761f-0550-2fe4-ee77503658e9.parquet], [70434/116281/fa677477eed622ae8123da1251aa7c351f801e2ee2f0bc28c0fe3002a30b3563/65bb4dc3-04e1-0e02-107a-90cee83c51b0.parquet], [70434/116281/db162bdd30261019960dd70da182e6ebd270284569ecfb5deffea7e65baa0df9/2505e079-67c5-06d9-3ede-89aca542dd18.parquet], [70434/116281/0c025dcccae8691f5fd70b0f131eea4ca6fafb95a02f90a3dc7bb015efd3ab4f/3f3e44c3-b71e-0ca4-3dc7-8b2f75b9ff86.parquet], ...]}, projection=[room, temp] | {{% /expand %}} {{< /expand-wrapper >}} -## EXPLAIN ANALYZE +## `EXPLAIN ANALYZE` -Returns the execution plan and metrics of a statement. -To output more information, use `EXPLAIN ANALYZE VERBOSE`. +Executes a statement and returns the execution plan and runtime metrics of the statement. +The report includes the [logical plan](/influxdb/clustered/reference/internals/query-plan/#logical-plan) and the [physical plan](/influxdb/clustered/reference/internals/query-plan/#physical-plan) annotated with execution counters, number of rows produced, and runtime metrics sampled during the query execution. -##### Example EXPLAIN ANALYZE +If the plan requires reading lots of data files, `EXPLAIN` and `EXPLAIN ANALYZE` may truncate the list of files in the report. +To output more information, including intermediate plans and paths for all scanned Parquet files, use [`EXPLAIN ANALYZE VERBOSE`](#explain-analyze-verbose). + +### Example `EXPLAIN ANALYZE` ```sql EXPLAIN ANALYZE @@ -60,15 +80,44 @@ SELECT room, avg(temp) AS temp FROM home +WHERE time >= '2023-01-01' AND time <= '2023-12-31' GROUP BY room ``` {{< expand-wrapper >}} {{% expand "View `EXPLAIN ANALYZE` example output" %}} -| plan_type | plan | -| :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Plan with Metrics | CoalescePartitionsExec, metrics=[output_rows=2, elapsed_compute=8.892µs, spill_count=0, spilled_bytes=0, mem_used=0] ProjectionExec: expr=[room@0 as room, AVG(home.temp)@1 as temp], metrics=[output_rows=2, elapsed_compute=3.608µs, spill_count=0, spilled_bytes=0, mem_used=0] AggregateExec: mode=FinalPartitioned, gby=[room@0 as room], aggr=[AVG(home.temp)], metrics=[output_rows=2, elapsed_compute=121.771µs, spill_count=0, spilled_bytes=0, mem_used=0] CoalesceBatchesExec: target_batch_size=8192, metrics=[output_rows=2, elapsed_compute=23.711µs, spill_count=0, spilled_bytes=0, mem_used=0] RepartitionExec: partitioning=Hash([Column { name: "room", index: 0 }], 4), input_partitions=4, metrics=[repart_time=25.117µs, fetch_time=1.614597ms, send_time=6.705µs] RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1, metrics=[repart_time=1ns, fetch_time=319.754µs, send_time=2.067µs] AggregateExec: mode=Partial, gby=[room@0 as room], aggr=[AVG(home.temp)], metrics=[output_rows=2, elapsed_compute=75.615µs, spill_count=0, spilled_bytes=0, mem_used=0] ParquetExec: limit=None, partitions={1 group: [[136/316/1120/1ede0031-e86e-06e5-12ba-b8e6fd76a202.parquet]]}, projection=[room, temp], metrics=[output_rows=26, elapsed_compute=1ns, spill_count=0, spilled_bytes=0, mem_used=0, pushdown_rows_filtered=0, bytes_scanned=290, row_groups_pruned=0, num_predicate_creation_errors=0, predicate_evaluation_errors=0, page_index_rows_filtered=0, time_elapsed_opening=100.37µs, page_index_eval_time=2ns, time_elapsed_scanning_total=157.086µs, time_elapsed_processing=226.644µs, pushdown_eval_time=2ns, time_elapsed_scanning_until_data=116.875µs] | +| | plan_type | plan | +|---:|:------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0 | Plan with Metrics | ProjectionExec: expr=[room@0 as room, AVG(home.temp)@1 as temp], metrics=[output_rows=2, elapsed_compute=4.768µs] | +| | | AggregateExec: mode=FinalPartitioned, gby=[room@0 as room], aggr=[AVG(home.temp)], ordering_mode=Sorted, metrics=[output_rows=2, elapsed_compute=140.405µs] | +| | | CoalesceBatchesExec: target_batch_size=8192, metrics=[output_rows=2, elapsed_compute=6.821µs] | +| | | RepartitionExec: partitioning=Hash([room@0], 8), input_partitions=8, preserve_order=true, sort_exprs=room@0 ASC, metrics=[output_rows=2, elapsed_compute=18.408µs, repart_time=59.698µs, fetch_time=1.057882762s, send_time=5.83µs] | +| | | AggregateExec: mode=Partial, gby=[room@0 as room], aggr=[AVG(home.temp)], ordering_mode=Sorted, metrics=[output_rows=2, elapsed_compute=137.577µs] | +| | | RepartitionExec: partitioning=RoundRobinBatch(8), input_partitions=6, preserve_order=true, sort_exprs=room@0 ASC, metrics=[output_rows=46, elapsed_compute=26.637µs, repart_time=6ns, fetch_time=399.971411ms, send_time=6.658µs] | +| | | ProjectionExec: expr=[room@0 as room, temp@2 as temp], metrics=[output_rows=46, elapsed_compute=3.102µs] | +| | | CoalesceBatchesExec: target_batch_size=8192, metrics=[output_rows=46, elapsed_compute=25.585µs] | +| | | FilterExec: time@1 >= 1672531200000000000 AND time@1 <= 1703980800000000000, metrics=[output_rows=46, elapsed_compute=26.51µs] | +| | | ParquetExec: file_groups={6 groups: [[70434/116281/404d73cea0236530ea94f5470701eb814a8f0565c0e4bef5a2d2e33dfbfc3567/1be334e8-0af8-00da-2615-f67cd4be90f7.parquet], [70434/116281/c14418ba28a22a3abb693a1cb326a63b62dc611aec58c9bed438fdafd3bc5882/8b29ae98-761f-0550-2fe4-ee77503658e9.parquet], [70434/116281/fa677477eed622ae8123da1251aa7c351f801e2ee2f0bc28c0fe3002a30b3563/65bb4dc3-04e1-0e02-107a-90cee83c51b0.parquet], [70434/116281/db162bdd30261019960dd70da182e6ebd270284569ecfb5deffea7e65baa0df9/2505e079-67c5-06d9-3ede-89aca542dd18.parquet], [70434/116281/0c025dcccae8691f5fd70b0f131eea4ca6fafb95a02f90a3dc7bb015efd3ab4f/3f3e44c3-b71e-0ca4-3dc7-8b2f75b9ff86.parquet], ...]}, projection=[room, time, temp], output_ordering=[room@0 ASC, time@1 ASC], predicate=time@6 >= 1672531200000000000 AND time@6 <= 1703980800000000000, pruning_predicate=time_max@0 >= 1672531200000000000 AND time_min@1 <= 1703980800000000000, required_guarantees=[], metrics=[output_rows=46, elapsed_compute=6ns, predicate_evaluation_errors=0, bytes_scanned=3279, row_groups_pruned_statistics=0, file_open_errors=0, file_scan_errors=0, pushdown_rows_filtered=0, num_predicate_creation_errors=0, row_groups_pruned_bloom_filter=0, page_index_rows_filtered=0, time_elapsed_opening=398.462968ms, time_elapsed_processing=1.626106ms, time_elapsed_scanning_total=1.36822ms, page_index_eval_time=33.474µs, pushdown_eval_time=14.267µs, time_elapsed_scanning_until_data=1.27694ms] | {{% /expand %}} -{{< /expand-wrapper >}} \ No newline at end of file +{{< /expand-wrapper >}} + +## `EXPLAIN ANALYZE VERBOSE` + +Executes a statement and returns the execution plan, runtime metrics, and additional details helpful for debugging the statement. + +The report includes the following: + +- the [logical plan](/influxdb/clustered/reference/internals/query-plan/#logical-plan) +- the [physical plan](/influxdb/clustered/reference/internals/query-plan/#physical-plan) annotated with execution counters, number of rows produced, and runtime metrics sampled during the query execution +- Information truncated in the `EXPLAIN` report--for example, the paths for all [Parquet files retrieved for the query](/influxdb/clustered/reference/internals/query-plan/#file_groups). +- All intermediate physical plans that DataFusion and the [Querier](/influxdb/clustered/reference/internals/storage-engine/#querier) generate before generating the final physical plan--helpful in debugging to see when an [`ExecutionPlan` node](/influxdb/clustered/reference/internals/query-plan/#executionplan-nodes) is added or removed, and how InfluxDB optimizes the query. + +### Example `EXPLAIN ANALYZE VERBOSE` + +```SQL +EXPLAIN ANALYZE VERBOSE SELECT temp FROM home +WHERE time >= now() - INTERVAL '7 days' AND room = 'Kitchen' +ORDER BY time +``` diff --git a/content/influxdb/clustered/reference/sql/information-schema.md b/content/influxdb/clustered/reference/sql/information-schema.md index 4ae307d36..a7cc55e6d 100644 --- a/content/influxdb/clustered/reference/sql/information-schema.md +++ b/content/influxdb/clustered/reference/sql/information-schema.md @@ -30,7 +30,7 @@ by columns in a table. ## SHOW TABLES -Returns information about tables (measurements) in an InfluxDB bucket. +Returns information about tables (measurements) in an InfluxDB database. ```sql SHOW TABLES @@ -57,7 +57,7 @@ _Measurements are those that use the **`iox` table schema**._ ## SHOW COLUMNS -Returns information about the schema of a table (measurement) in an InfluxDB bucket. +Returns information about the schema of a table (measurement) in an InfluxDB database. ```sql SHOW COLUMNS FROM example_table diff --git a/content/influxdb/v1/concepts/glossary.md b/content/influxdb/v1/concepts/glossary.md index 59897db1a..da30c706b 100644 --- a/content/influxdb/v1/concepts/glossary.md +++ b/content/influxdb/v1/concepts/glossary.md @@ -35,7 +35,6 @@ An InfluxQL query that runs automatically and periodically within a database. Continuous queries require a function in the `SELECT` clause and must include a `GROUP BY time()` clause. See [Continuous Queries](/influxdb/v1/query_language/continuous_queries/). - Related entries: [function](/influxdb/v1/concepts/glossary/#function) ## database @@ -151,7 +150,7 @@ Related entries: [field set](/influxdb/v1/concepts/glossary/#field-set), [series ## points per second -A deprecated measurement of the rate at which data are persisted to InfluxDB. +A deprecated measurement of the rate at which data is persisted to InfluxDB. The schema allows and even encourages the recording of multiple metric values per point, rendering points per second ambiguous. Write speeds are generally quoted in values per second, a more precise metric. @@ -184,7 +183,7 @@ Related entries: [duration](/influxdb/v1/concepts/glossary/#duration), [measurem ## schema -How the data are organized in InfluxDB. +How data is organized in InfluxDB. The fundamentals of the InfluxDB schema are databases, retention policies, series, measurements, tag keys, tag values, and field keys. See [Schema Design](/influxdb/v1/concepts/schema_and_data_layout/) for more information. @@ -364,7 +363,7 @@ See [Authentication and Authorization](/influxdb/v1/administration/authenticatio ## values per second -The preferred measurement of the rate at which data are persisted to InfluxDB. Write speeds are generally quoted in values per second. +The preferred measurement of the rate at which data is persisted to InfluxDB. Write speeds are generally quoted in values per second. To calculate the values per second rate, multiply the number of points written per second by the number of values stored per point. For example, if the points have four fields each, and a batch of 5000 points is written 10 times per second, then the values per second rate is `4 field values per point * 5000 points per batch * 10 batches per second = 200,000 values per second`. diff --git a/content/influxdb/v2/api-guide/client-libraries/arduino.md b/content/influxdb/v2/api-guide/client-libraries/arduino.md index e8c072c39..1078814a1 100644 --- a/content/influxdb/v2/api-guide/client-libraries/arduino.md +++ b/content/influxdb/v2/api-guide/client-libraries/arduino.md @@ -14,8 +14,8 @@ menu: weight: 201 --- -Arduino is an open-source hardware and software platform used for building electronics projects. +Arduino is an open source hardware and software platform used for building electronics projects. The documentation for this client library is available on GitHub. -Arduino InfluxDB client \ No newline at end of file +Arduino InfluxDB client diff --git a/content/influxdb/v2/api-guide/client-libraries/kotlin.md b/content/influxdb/v2/api-guide/client-libraries/kotlin.md index 23614fb14..5e1f8ae59 100644 --- a/content/influxdb/v2/api-guide/client-libraries/kotlin.md +++ b/content/influxdb/v2/api-guide/client-libraries/kotlin.md @@ -13,8 +13,8 @@ menu: weight: 201 --- -Kotlin is an open-source programming language that runs on the Java Virtual Machine (JVM). +Kotlin is an open source programming language that runs on the Java Virtual Machine (JVM). The documentation for this client library is available on GitHub. -Kotlin InfluxDB client \ No newline at end of file +Kotlin InfluxDB client diff --git a/content/influxdb/v2/reference/glossary.md b/content/influxdb/v2/reference/glossary.md index 2900f8e3a..185ec057c 100644 --- a/content/influxdb/v2/reference/glossary.md +++ b/content/influxdb/v2/reference/glossary.md @@ -106,7 +106,6 @@ A bucket is a named location where time series data is stored. All buckets have a [retention period](#retention-period). A bucket belongs to an organization. - ### bucket schema In InfluxDB Cloud, an explicit bucket schema lets you strictly enforce the data that can be written into one or more measurements in a bucket by defining the column names, tags, fields, and data types allowed for each measurement. By default, buckets in InfluxDB {{< current-version >}} have an `implicit` schema that lets you write data without restrictions on columns, fields, or data types. @@ -279,6 +278,7 @@ InfluxDB supports the following data types: | time | dateTime | For more information about different data types, see: + - [annotated CSV](/influxdb/v2/reference/syntax/annotated-csv/) - [extended annotated CSV](/influxdb/cloud/reference/syntax/annotated-csv/extended/#datatype) - [line protocol](/influxdb/v2/reference/syntax/line-protocol/#data-types-and-format) @@ -306,7 +306,7 @@ Aggregating high resolution data into lower resolution data to preserve disk spa ### duration -A data type that represents a duration of time (1s, 1m, 1h, 1d). +A data type that represents a duration of time--for example, `1s`, `1m`, `1h`, `1d`. Retention policies are set using durations. Data older than the duration is automatically dropped from the database. @@ -468,10 +468,10 @@ Related entries: In Flux, an implicit block is a possibly empty sequence of statements within matching braces ({ }) that includes the following types: - - Universe: Encompasses all Flux source text. - - Package: Each package includes a package block that contains Flux source text for the package. - - File: Each file has a file block containing Flux source text in the file. - - Function: Each function literal has a function block with Flux source text (even if not explicitly declared). +- Universe: Encompasses all Flux source text. +- Package: Each package includes a package block that contains Flux source text for the package. +- File: Each file has a file block containing Flux source text in the file. +- Function: Each function literal has a function block with Flux source text (even if not explicitly declared). Related entries: [explicit block](#explicit-block), [block](#block) @@ -485,7 +485,7 @@ Related entries: [explicit block](#explicit-block), [block](#block) ### InfluxDB -An open-source time series database (TSDB) developed by InfluxData. +An open source time series database (TSDB) developed by InfluxData. Written in Go and optimized for fast, high-availability storage and retrieval of time series data in fields such as operations monitoring, application metrics, Internet of Things sensor data, and real-time analytics. ### InfluxDB UI @@ -712,7 +712,6 @@ Learn about the [option assignment](/flux/v0/spec/assignment-scope/#option-assig A workspace for a group of users. All dashboards, tasks, buckets, members, and so on, belong to an organization. - ### owner A type of role for a user. @@ -720,7 +719,7 @@ Owners have read/write permissions. Users can have owner roles for bucket and organization resources. Role permissions are separate from API token permissions. For additional -information on API tokens, see [token](#tokens). +information on API tokens, see [token](#token). ### output plugin @@ -794,6 +793,7 @@ A Flux predicate function is an anonymous function that returns `true` or `false based on one or more [predicate expressions](#predicate-expression). ###### Example predicate function + ```js (r) => r.foo == "bar" and r.baz != "quz" ``` @@ -954,6 +954,7 @@ The series cardinality would remain unchanged at `6`, as `firstname` is already | cliff@influxdata.com | finish | clifford | ##### Query for cardinality: + - **Flux:** [influxdb.cardinality()](/flux/v0/stdlib/influxdb/cardinality/) - **InfluxQL:** [SHOW CARDINALITY](/influxdb/v1/query_language/spec/#show-cardinality) @@ -1000,7 +1001,7 @@ A shard belongs to a single [shard group](#shard-group). For more information, see [Shards and shard groups (OSS)](/influxdb/v2/reference/internals/shards/). -Related entries: [series](#series), [shard duration](#shard-duration), +Related entries: [series](#series), [shard group duration](#shard-group-duration), [shard group](#shard-group), [tsm](#tsm-time-structured-merge-tree) ### shard group @@ -1013,7 +1014,7 @@ The interval spanned by each shard group is the [shard group duration](#shard-gr For more information, see [Shards and shard groups (OSS)](/influxdb/v2/reference/internals/shards/). Related entries: [bucket](#bucket), [retention period](#retention-period), -[series](#series), [shard](#shard), [shard duration](#shard-duration) +[series](#series), [shard](#shard), [shard group duration](#shard-group-duration) ### shard group duration @@ -1254,7 +1255,7 @@ Users are added as a member of an organization and are given a unique API token. ### values per second -The preferred measurement of the rate at which data are persisted to InfluxDB. +The preferred measurement of the rate at which data is persisted to InfluxDB. Write speeds are generally quoted in values per second. To calculate the values per second rate, multiply the number of points written @@ -1264,7 +1265,6 @@ written 10 times per second, the values per second rate is: **4 field values per point** × **5000 points per batch** × **10 batches per second** = **200,000 values per second** - Related entries: [batch](#batch), [field](#field), [point](#point) ### variable diff --git a/layouts/shortcodes/html-diagram/query-plan.html b/layouts/shortcodes/html-diagram/query-plan.html new file mode 100644 index 000000000..3d5f809bc --- /dev/null +++ b/layouts/shortcodes/html-diagram/query-plan.html @@ -0,0 +1,34 @@ +
+
+
+
+ SortPreservingMergeExec +
+
+
+ UnionExec +
+
+
+
+
+
+
+ SortExec +
+
+
+ ParquetExec +
+
+
+
+ SortExec +
+
+
+ ParquetExec +
+
+
+
\ No newline at end of file diff --git a/package.json b/package.json index cd923c920..a8518734b 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "description": "InfluxDB documentation", "license": "MIT", "devDependencies": { + "@vvago/vale": "^3.0.7", "autoprefixer": ">=10.2.5", "hugo-extended": ">=0.101.0", "postcss": ">=8.4.31", diff --git a/static/img/influxdb/3-0-query-plan-tree.png b/static/img/influxdb/3-0-query-plan-tree.png new file mode 100644 index 0000000000000000000000000000000000000000..fad64092a353c3d3389d1ec345083748cfae0b18 GIT binary patch literal 350714 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

K423wFbgiLZJjfJMM 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�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�EgF~JA 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 .env.products' # Install Telegraf for use in tests. # Follow the install instructions (https://docs.influxdata.com/telegraf/v1/install/?t=curl), except for sudo (which isn't available in Docker). -# influxdata-archive_compat.key GPG Fingerprint: 9D539D90D3328DC7D6C8D3B9D8FF8E1F7DF8B07E -RUN curl -s https://repos.influxdata.com/influxdata-archive.key > influxdata-archive.key \ - && \ -echo '943666881a1b8d9b849b74caebf02d3465d6beb716510d86a39f6c8e8dac7515 influxdata-archive.key' | sha256sum -c && cat influxdata-archive.key | gpg --dearmor | tee /etc/apt/trusted.gpg.d/influxdata-archive.gpg > /dev/null \ - && \ -echo 'deb [signed-by=/etc/apt/trusted.gpg.d/influxdata-archive.gpg] https://repos.influxdata.com/debian stable main' | tee /etc/apt/sources.list.d/influxdata.list \ - && \ -apt-get update && apt-get install telegraf +# influxdata-archive_compat.key GPG fingerprint: +# 9D53 9D90 D332 8DC7 D6C8 D3B9 D8FF 8E1F 7DF8 B07E +RUN wget -q https://repos.influxdata.com/influxdata-archive_compat.key + +RUN echo '393e8779c89ac8d958f81f942f9ad7fb82a25e133faddaf92e15b16e6ac9ce4c influxdata-archive_compat.key' | sha256sum -c && cat influxdata-archive_compat.key | gpg --dearmor | tee /etc/apt/trusted.gpg.d/influxdata-archive_compat.gpg > /dev/null + +RUN echo 'deb [signed-by=/etc/apt/trusted.gpg.d/influxdata-archive_compat.gpg] https://repos.influxdata.com/debian stable main' | tee /etc/apt/sources.list.d/influxdata.list + +RUN apt-get update && apt-get install telegraf # Install influx v2 Cloud CLI for use in tests. # Follow the install instructions(https://portal.influxdata.com/downloads/), except for sudo (which isn't available in Docker). # influxdata-archive_compat.key GPG fingerprint: # 9D53 9D90 D332 8DC7 D6C8 D3B9 D8FF 8E1F 7DF8 B07E -RUN wget -q https://repos.influxdata.com/influxdata-archive_compat.key \ - && \ -echo '393e8779c89ac8d958f81f942f9ad7fb82a25e133faddaf92e15b16e6ac9ce4c influxdata-archive_compat.key' | sha256sum -c && cat influxdata-archive_compat.key | gpg --dearmor | tee /etc/apt/trusted.gpg.d/influxdata-archive_compat.gpg > /dev/null \ - && \ -echo 'deb [signed-by=/etc/apt/trusted.gpg.d/influxdata-archive_compat.gpg] https://repos.influxdata.com/debian stable main' | tee /etc/apt/sources.list.d/influxdata.list \ - && \ -apt-get update && apt-get install influxdb2-cli +RUN wget -q https://repos.influxdata.com/influxdata-archive_compat.key + +RUN echo '393e8779c89ac8d958f81f942f9ad7fb82a25e133faddaf92e15b16e6ac9ce4c influxdata-archive_compat.key' | sha256sum -c && cat influxdata-archive_compat.key | gpg --dearmor | tee /etc/apt/trusted.gpg.d/influxdata-archive_compat.gpg > /dev/null + +RUN echo 'deb [signed-by=/etc/apt/trusted.gpg.d/influxdata-archive_compat.gpg] https://repos.influxdata.com/debian stable main' | tee /etc/apt/sources.list.d/influxdata.list + +RUN apt-get update && apt-get install influxdb2-cli ENV TEMP_DIR=/usr/src/app/test/tmp ENTRYPOINT [ "run-tests.sh" ] diff --git a/test.sh b/test.sh index 44193d318..74afdf966 100755 --- a/test.sh +++ b/test.sh @@ -52,7 +52,7 @@ docker compose up test # If you want to examine files or run commands for debugging tests, # start the container and use `exec` to open an interactive shell--for example: -# docker compose start test && docker compose exec -it test /bin/bash +# docker compose run -it --entrypoint=/bin/bash test # To build and run a new container and debug test failures, use `docker compose run` which runs a one-off command in a new container. Pass additional flags to be used by the container's entrypoint and the test runners it executes--for example: diff --git a/test/README.md b/test/README.md index f27eef281..57582adda 100644 --- a/test/README.md +++ b/test/README.md @@ -62,6 +62,8 @@ _Note_: `pytest --codeblocks` uses Python's `subprocess.run()` to execute shell To assert (and display) the expected output of your code, follow the code block with the `` comment tag, and then the expected output in a code block--for example: + + ```python print("Hello, world!") ``` @@ -74,6 +76,8 @@ If successful, the output is the following: Hello, world! ``` + + pytest-codeblocks has features for skipping tests and marking blocks as failed. To learn more, see the pytest-codeblocks README and tests. diff --git a/test/requirements.txt b/test/requirements.txt index 8f4640da8..d8f4274a5 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -5,8 +5,8 @@ pytest-codeblocks==0.16.1 python-dotenv==1.0.0 pytest-dotenv==0.5.2 # Code sample dependencies -## TODO: install these using virtual environments in the docs and remove from here. influxdb3-python +influxdb3-python-cli pandas ## Tabulate for printing pandas DataFrames. tabulate \ No newline at end of file diff --git a/test/run-tests.sh b/test/run-tests.sh index a7f30d15a..59c989908 100644 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -1,5 +1,8 @@ #!/bin/bash +# This script is used to run tests for the InfluxDB documentation. +# The script is designed to be run in a Docker container. It is used to substitute placeholder values. + # Function to check if an option is present in the arguments has_option() { local target="$1" @@ -26,7 +29,7 @@ fi BASE_DIR=$(pwd) cd $TEMP_DIR -for file in `find . -type f` ; do +for file in `find . -type f \( -iname '*.md' \)` ; do if [ -f "$file" ]; then echo "PRETEST: substituting values in $file" @@ -93,6 +96,9 @@ mkdir -p ~/Downloads && rm -rf ~/Downloads/* # Clean up installed files from previous runs. gpg -q --batch --yes --delete-key D8FF8E1F7DF8B07E > /dev/null 2>&1 +# Activate the Python virtual environment configured in the Dockerfile. +. /opt/venv/bin/activate + # Run test commands with options provided in the CMD of the Dockerfile. # pytest rootdir is the directory where pytest.ini is located (/test). if [ -d ./content/influxdb/cloud-dedicated/ ]; then