diff --git a/docs/diagnostics/LogicalOrInJoinQuerySection.md b/docs/diagnostics/LogicalOrInJoinQuerySection.md new file mode 100644 index 00000000000..c3978049a00 --- /dev/null +++ b/docs/diagnostics/LogicalOrInJoinQuerySection.md @@ -0,0 +1,78 @@ +# Логическое 'ИЛИ' в соединениях запроса (LogicalOrInJoinQuerySection) + + +## Описание диагностики +Диагностика выявляет использование оператора `ИЛИ` в условиях соединений таблиц. + +Присутствие операторов `ИЛИ` в условиях соединений может привести к тому, что СУБД не сможет использовать +индексы таблиц и будет выполнять сканирование, что увеличит время работы запроса и вероятность возникновения блокировок. + +Ошибка может быть решена "разнесением" предикатов условия с `ИЛИ` на разные пакеты запросов с объединением + +ВАЖНО: +Диагностика контролирует наличие предикатов в условии `ИЛИ`, над различными полями, так как использование оператора `ИЛИ` +над вариантами одного поля при выполнении запроса на стороне SQL автоматически преобразуется в условие IN. +## Примеры +1) Ошибка не будет зафиксирована при использовании `ИЛИ` над вариантами одного поля + +```bsl +ЛЕВОЕ СОЕДИНЕНИЕ Справочник.ВидыНоменклатуры КАК ВидыНоменклатуры //Тест работы на вложенном соединении +ПО СправочникНоменклатура.ВидНоменклатуры = ВидыНоменклатуры.Ссылка + И (СправочникНоменклатура.СрокГодности > 1 + ИЛИ СправочникНоменклатура.СрокГодности < 10) +``` +2) При использовании оператора `ИЛИ` над различными полями ошибка будет зафиксирована для каждого вхождения оператора + +```bsl +ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг +ПО РеализацияТоваровУслугТовары.Ссылка = РеализацияТоваровУслуг.Ссылка + И (РеализацияТоваровУслугТовары.Сумма > 0 + ИЛИ РеализацияТоваровУслугТовары.СуммаНДС > 0 + ИЛИ РеализацияТоваровУслугТовары.СуммаСНДС > 0) + +``` + +Такие конструкции предлагается исправлять вынесением в отдельные пакеты запросов с объединением: + +```bsl +ВЫБРАТЬ * +ИЗ +ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг +ПО РеализацияТоваровУслугТовары.Ссылка = РеализацияТоваровУслуг.Ссылка + И РеализацияТоваровУслугТовары.Сумма > 0 + +ОБЪЕДИНИТЬ ВСЕ + +ВЫБРАТЬ * +ИЗ +ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг +ПО РеализацияТоваровУслугТовары.Ссылка = РеализацияТоваровУслуг.Ссылка + И РеализацияТоваровУслугТовары.СуммаНДС > 0 + +ОБЪЕДИНИТЬ ВСЕ + +ВЫБРАТЬ * +ИЗ +ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг +ПО РеализацияТоваровУслугТовары.Ссылка = РеализацияТоваровУслуг.Ссылка + И РеализацияТоваровУслугТовары.СуммаСНДС > 0 +``` + +3) Диагностика также отработает для вложенных соединений с использованием `ИЛИ` в условиях. + +```bsl +Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТоваровУслугТовары +ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг +ПО РеализацияТоваровУслугТовары.Ссылка = РеализацияТоваровУслуг.Ссылка +ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Номенклатура КАК СправочникНоменклатура + ЛЕВОЕ СОЕДИНЕНИЕ Справочник.ВидыНоменклатуры КАК ВидыНоменклатуры //Тест работы на вложенном соединении + ПО СправочникНоменклатура.ВидНоменклатуры = ВидыНоменклатуры.Ссылка + И (СправочникНоменклатура.СрокГодности > 1 + ИЛИ ВидыНоменклатуры.ЗапрещенаПродажаЧерезПатент = ИСТИНА) + +``` +Рекомендуется исправление аналогично п.2 заменой вложенного соединения на соединение с созданием промежуточной временной таблицы + +## Источники +- [Стандарт - Эффективные условия запросов, п.2](https://its.1c.ru/db/v8std/content/658/hdoc) +- [Использование логического ИЛИ в условиях - Типичные причины неоптимальной работы запросов и методы оптимизации](https://its.1c.ru/db/content/metod8dev/src/developers/scalability/standards/i8105842.htm#or) diff --git a/docs/en/diagnostics/LogicalOrInJoinQuerySection.md b/docs/en/diagnostics/LogicalOrInJoinQuerySection.md new file mode 100644 index 00000000000..3876aa449c0 --- /dev/null +++ b/docs/en/diagnostics/LogicalOrInJoinQuerySection.md @@ -0,0 +1,80 @@ +# Logical 'OR' in 'JOIN' query section (LogicalOrInJoinQuerySection) + + +## Description + +Diagnostics reveals the use of the `OR` operator in the conditions of table joins. + +The presence of the `OR` operators in connection conditions may cause the DBMS to be unable to use +table indexes and perform scans, which will increase query running time and the likelihood of locks. + +The error can be solved by "spreading" the predicates of the condition with `OR` into different query packages with combining + +IMPORTANT: +Diagnostics monitors the presence of predicates in the condition `OR`, over various fields, since the use of the operator `OR` +When executing a query on the SQL side, the control over the variants of one field is automatically converted to the IN condition. + +## Examples +1) The error will not be fixed when using `OR` over variants of a single field. + +```bsl +LEFT JOIN Catalog.NomenclatureTypes КАК NomenclatureTypes + ON CatalogNomenclature.NomenclatureType = NomenclatureTypes.Reference + AND (CatalogNomenclature.ExpirationDate > 1 + OR CatalogNomenclature.ExpirationDate < 10) +``` + +2) When using the `OR` operator over various fields, the error will be fixed for each occurrence of the operator. + +```bsl +INNER JOIN Document.GoodsServicesSaling КАК GoodsServicesSaling +ON GoodsServicesSalingGoods.Reference = GoodsServicesSaling.Reference + AND (GoodsServicesSalingGoods.Amount > 0 + OR GoodsServicesSalingGoods.AmountVAT > 0 + OR GoodsServicesSalingGoods.AmountWithVAT > 0) + +``` + +It is proposed to correct such constructions by placing requests in separate packages with combining: + +```bsl +SELECT * +FROM +INNER JOIN Document.GoodsServicesSaling КАК GoodsServicesSaling +ON GoodsServicesSalingGoods.Reference = GoodsServicesSaling.Reference + AND GoodsServicesSalingGoods.Amount > 0 + +UNION ALL + +SELECT * +FROM +INNER JOIN Document.GoodsServicesSaling КАК GoodsServicesSaling +ON GoodsServicesSalingGoods.Reference = GoodsServicesSaling.Reference + AND GoodsServicesSalingGoods.AmountVAT > 0 + +UNION ALL + +SELECT * +FROM +INNER JOIN Document.GoodsServicesSaling КАК GoodsServicesSaling +ON GoodsServicesSalingGoods.Reference = GoodsServicesSaling.Reference + AND GoodsServicesSalingGoods.AmountWithVAT > 0 +``` + +3) Diagnostics will also work for nested connections using `OR` in conditions. + +```bsl +Document.GoodsServicesSaling.Goods КАК GoodsServicesSalingGoods +INNER JOIN Document.GoodsServicesSaling КАК GoodsServicesSaling +ON GoodsServicesSalingGoods.Reference = GoodsServicesSaling.Reference +LEFT JOIN Catalog.Nomenclature КАК CatalogNomenclature + LEFT JOIN Catalog.NomenclatureTypes КАК NomenclatureTypes + ON CatalogNomenclature.NomenclatureType = NomenclatureTypes.Reference + AND (CatalogNomenclature.ExpirationDate > 1 + OR NomenclatureTypes.SaleThroughAPatentIsProhibited = TRUE) + +``` +A fix similar to paragraph 2 is recommended by replacing the nested connection with a connection with the creation of an intermediate temporary table. +## Sources +- [Standard: Effective Query Conditions, Clause 2 (RU)](https://its.1c.ru/db/v8std/content/658/hdoc) +- [Typical Causes of Suboptimal Query Performance and Optimization Techniques: Using Logical OR in Conditions (RU)](https://its.1c.ru/db/content/metod8dev/src/developers/scalability/standards/i8105842.htm#or) diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/diagnostics/LogicalOrInJoinQuerySectionDiagnostic.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/diagnostics/LogicalOrInJoinQuerySectionDiagnostic.java new file mode 100644 index 00000000000..4cd00be75c8 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/diagnostics/LogicalOrInJoinQuerySectionDiagnostic.java @@ -0,0 +1,85 @@ +/* + * This file is a part of BSL Language Server. + * + * Copyright (c) 2018-2025 + * Alexey Sosnoviy , Nikita Fedkin and contributors + * + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * BSL Language Server is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * BSL Language Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with BSL Language Server. + */ +package com.github._1c_syntax.bsl.languageserver.diagnostics; + +import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticMetadata; +import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticSeverity; +import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticTag; +import com.github._1c_syntax.bsl.languageserver.diagnostics.metadata.DiagnosticType; +import com.github._1c_syntax.bsl.languageserver.utils.Trees; +import com.github._1c_syntax.bsl.parser.SDBLParser; +import org.antlr.v4.runtime.tree.ParseTree; + +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +@DiagnosticMetadata( + type = DiagnosticType.CODE_SMELL, + severity = DiagnosticSeverity.INFO, + minutesToFix = 1, + tags = { + DiagnosticTag.SQL, + DiagnosticTag.PERFORMANCE, + DiagnosticTag.UNPREDICTABLE + } + +) +public class LogicalOrInJoinQuerySectionDiagnostic extends AbstractSDBLVisitorDiagnostic { + + @Override + public ParseTree visitQuery(SDBLParser.QueryContext ctx) { + //При посещении запроса сразу отбираем контексты соединений, которые в цикле обрабатываем отдельным методом. + Trees.findAllRuleNodes(ctx, SDBLParser.RULE_joinPart). + forEach(jpt -> processJoinPart((SDBLParser.JoinPartContext) jpt)); + + return ctx; + } + + private void processJoinPart(SDBLParser.JoinPartContext ctx) { + + //Инициализируем поток для коллекции условий соединения, каждое условие преобразуем в логическое выражение. + ctx.condition.condidions.stream().map(SDBLParser.PredicateContext::logicalExpression) + //фильтрация null отсекает условия, не содержащие составных логических конструкций. + .filter(Objects::nonNull) + //Каждое условие дополнительно отбирается по наличию более чем одного различных полей. + .filter(this::isMultipleFieldsExpression) + //По оставшимся условиям проводится цикл с поиском операторов "ИЛИ" + // и вложенным циклом фиксации ошибки диагностики для каждого оператора + .forEach( + exp -> Trees.findAllTokenNodes(exp, SDBLParser.OR) + .forEach(diagnosticStorage::addDiagnostic)); + + } + + private boolean isMultipleFieldsExpression(SDBLParser.LogicalExpressionContext exp){ + + //От контекста логического выражения спускаемся до контекста колонки, + // далее поток текстового представления колонок собираем в Set. + //По наличию в Set более чем одного элемента проверяем использование различных полей в условии + Set expFields = Trees.findAllRuleNodes(exp, SDBLParser.RULE_column).stream() + .map(ParseTree::getText) + .collect(Collectors.toSet()); + return expFields.size() > 1; + } + +} diff --git a/src/main/resources/com/github/_1c_syntax/bsl/languageserver/configuration/parameters-schema.json b/src/main/resources/com/github/_1c_syntax/bsl/languageserver/configuration/parameters-schema.json index 3a20edf4672..d6642b35ecf 100644 --- a/src/main/resources/com/github/_1c_syntax/bsl/languageserver/configuration/parameters-schema.json +++ b/src/main/resources/com/github/_1c_syntax/bsl/languageserver/configuration/parameters-schema.json @@ -1070,6 +1070,16 @@ }, "$id": "#/definitions/LineLength" }, + "LogicalOrInJoinQuerySection": { + "description": "Logical 'OR' in 'JOIN' query section", + "default": true, + "type": [ + "boolean", + "object" + ], + "title": "Logical 'OR' in 'JOIN' query section", + "$id": "#/definitions/LogicalOrInJoinQuerySection" + }, "LogicalOrInTheWhereSectionOfQuery": { "description": "Using a logical \"OR\" in the \"WHERE\" section of a query", "default": true, diff --git a/src/main/resources/com/github/_1c_syntax/bsl/languageserver/configuration/schema.json b/src/main/resources/com/github/_1c_syntax/bsl/languageserver/configuration/schema.json index 8bcf9df256f..81c3e8681b8 100644 --- a/src/main/resources/com/github/_1c_syntax/bsl/languageserver/configuration/schema.json +++ b/src/main/resources/com/github/_1c_syntax/bsl/languageserver/configuration/schema.json @@ -266,6 +266,9 @@ "LineLength": { "$ref": "parameters-schema.json#/definitions/LineLength" }, + "LogicalOrInJoinQuerySection": { + "$ref": "parameters-schema.json#/definitions/LogicalOrInJoinQuerySection" + }, "LogicalOrInTheWhereSectionOfQuery": { "$ref": "parameters-schema.json#/definitions/LogicalOrInTheWhereSectionOfQuery" }, diff --git a/src/main/resources/com/github/_1c_syntax/bsl/languageserver/diagnostics/LogicalOrInJoinQuerySectionDiagnostic_en.properties b/src/main/resources/com/github/_1c_syntax/bsl/languageserver/diagnostics/LogicalOrInJoinQuerySectionDiagnostic_en.properties new file mode 100644 index 00000000000..c027d94262c --- /dev/null +++ b/src/main/resources/com/github/_1c_syntax/bsl/languageserver/diagnostics/LogicalOrInJoinQuerySectionDiagnostic_en.properties @@ -0,0 +1,2 @@ +diagnosticMessage='OR' operator is detected in JOIN condition +diagnosticName=Logical 'OR' in 'JOIN' query section diff --git a/src/main/resources/com/github/_1c_syntax/bsl/languageserver/diagnostics/LogicalOrInJoinQuerySectionDiagnostic_ru.properties b/src/main/resources/com/github/_1c_syntax/bsl/languageserver/diagnostics/LogicalOrInJoinQuerySectionDiagnostic_ru.properties new file mode 100644 index 00000000000..f480525f4a2 --- /dev/null +++ b/src/main/resources/com/github/_1c_syntax/bsl/languageserver/diagnostics/LogicalOrInJoinQuerySectionDiagnostic_ru.properties @@ -0,0 +1,2 @@ +diagnosticMessage=Обнаружен оператор 'ИЛИ' в условии соединения +diagnosticName=Логическое 'ИЛИ' в соединениях запроса diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/diagnostics/LogicalOrInJoinQuerySectionDiagnosticTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/diagnostics/LogicalOrInJoinQuerySectionDiagnosticTest.java new file mode 100644 index 00000000000..99ac5efe514 --- /dev/null +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/diagnostics/LogicalOrInJoinQuerySectionDiagnosticTest.java @@ -0,0 +1,54 @@ +/* + * This file is a part of BSL Language Server. + * + * Copyright (c) 2018-2025 + * Alexey Sosnoviy , Nikita Fedkin and contributors + * + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * BSL Language Server is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * BSL Language Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with BSL Language Server. + */ +package com.github._1c_syntax.bsl.languageserver.diagnostics; + +import org.eclipse.lsp4j.Diagnostic; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static com.github._1c_syntax.bsl.languageserver.util.Assertions.assertThat; + +class LogicalOrInJoinQuerySectionDiagnosticTest extends AbstractDiagnosticTest { + LogicalOrInJoinQuerySectionDiagnosticTest() { + super(LogicalOrInJoinQuerySectionDiagnostic.class); + } + + @Test + void test() { + + List diagnostics = getDiagnostics(); + + assertThat(diagnostics).hasSize(8); + + assertThat(diagnostics, true) + .hasRange(12, 62, 12, 65) + .hasRange(12, 108, 12, 111) + .hasRange(24, 14, 24, 17) + .hasRange(26, 14, 26, 17) + .hasRange(27, 14, 27, 17) + .hasRange(29, 14, 29, 17) + .hasRange(30, 14, 30, 17) + .hasRange(19, 15, 19, 18); + + } +} diff --git a/src/test/resources/diagnostics/LogicalOrInJoinQuerySectionDiagnostic.bsl b/src/test/resources/diagnostics/LogicalOrInJoinQuerySectionDiagnostic.bsl new file mode 100644 index 00000000000..4032a8cee20 --- /dev/null +++ b/src/test/resources/diagnostics/LogicalOrInJoinQuerySectionDiagnostic.bsl @@ -0,0 +1,42 @@ +Процедура ПолучиттьРеализациюТовара() + + Запрос = Новый Запрос; + Запрос.Текст = + "ВЫБРАТЬ + | РеализацияТоваровУслугТовары.Ссылка КАК Ссылка, + | РеализацияТоваровУслугТовары.Сумма > 0 + | ИЛИ РеализацияТоваровУслугТовары.СуммаСНДС > 0 КАК НенулеваяСумма + |ИЗ + | Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТоваровУслугТовары + | ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг + | ПО РеализацияТоваровУслугТовары.Ссылка = РеализацияТоваровУслуг.Ссылка + | И (РеализацияТоваровУслугТовары.Сумма > 0 ИЛИ РеализацияТоваровУслугТовары.СуммаНДС > 0 ИЛИ РеализацияТоваровУслугТовары.СуммаСНДС > 0) //Ошибка (2 срабатывания) + | ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Номенклатура КАК СправочникНоменклатура + | ЛЕВОЕ СОЕДИНЕНИЕ Справочник.ВидыНоменклатуры КАК ВидыНоменклатуры //Тест работы на вложенном соединении + | ПО СправочникНоменклатура.ВидНоменклатуры = ВидыНоменклатуры.Ссылка + | И (СправочникНоменклатура.СрокГодности > 1 + | ИЛИ СправочникНоменклатура.СрокГодности < 10) + | И (СправочникНоменклатура.СрокГодности > 1 + | ИЛИ ВидыНоменклатуры.ЗапрещенаПродажаЧерезПатент = ИСТИНА) //Ошибка + | ПО РеализацияТоваровУслугТовары.Номенклатура = СправочникНоменклатура.Ссылка + | И (СправочникНоменклатура.КодПоКВПД = ""1122"" + | ИЛИ СправочникНоменклатура.КодПоКВПД = ""1133"") + | И (СправочникНоменклатура.Артикул = ""0011"" + | ИЛИ СправочникНоменклатура.КодТРУ = ""0111"") //Ошибка + | И (СправочникНоменклатура.Артикул = ""0022"" + | ИЛИ СправочникНоменклатура.КодТРУ = ""0222"" + | ИЛИ СправочникНоменклатура.КодПоКВПД = ""2233"") //Ошибка (2 срабатывания) + | И (СправочникНоменклатура.КодПоКВПД = ""1122"" + | ИЛИ СправочникНоменклатура.КодПоКВПД = ""1133"" + | ИЛИ СправочникНоменклатура.КодТРУ = ""0222"")"; //Ошибка (2 срабатывания) + + РезультатЗапроса = Запрос.Выполнить(); + +КонецПроцедуры + +//Диагностика должна зафиксировать ошибку +// при использовании оператора "ИЛИ" в условии над различными полями таблицы. +// Если оператор "ИЛИ" в условии над одним полем, то ошибка не фиксируется, +// так как планировщик запросов имеет возможность преобразовывать такое условие в IN, тем самым оптимизируя. + +//Итоговое количество срабатываний - 8. \ No newline at end of file