mxrb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.de-DE.md +108 -0
- data/README.md +172 -0
- data/README.pt-BR.md +109 -0
- data/bin/mxrb +1613 -0
- data/docs/README.md +10 -0
- data/docs/de-DE/README.md +24 -0
- data/docs/de-DE/architectural-patterns.md +26 -0
- data/docs/de-DE/architectural-standard.md +48 -0
- data/docs/de-DE/architecture.md +76 -0
- data/docs/de-DE/compiler.md +162 -0
- data/docs/de-DE/conventions.md +19 -0
- data/docs/de-DE/design-system.md +52 -0
- data/docs/de-DE/entity-dsl.md +53 -0
- data/docs/de-DE/native-runtime-quality-report.md +74 -0
- data/docs/de-DE/oql-sql.md +211 -0
- data/docs/de-DE/platform-api-integration.md +36 -0
- data/docs/de-DE/platform-operations.md +243 -0
- data/docs/de-DE/project-structure.md +32 -0
- data/docs/de-DE/ruby-first-roadmap.md +116 -0
- data/docs/de-DE/scaffolds.md +59 -0
- data/docs/de-DE/semantic-refactoring.md +56 -0
- data/docs/de-DE/team-server.md +33 -0
- data/docs/de-DE/validation-matrix.md +64 -0
- data/docs/de-DE/vetclinic-acceptance.md +24 -0
- data/docs/de-DE/writing.md +138 -0
- data/docs/en-US/README.md +24 -0
- data/docs/en-US/architectural-patterns.md +151 -0
- data/docs/en-US/architectural-standard.md +89 -0
- data/docs/en-US/architecture.md +170 -0
- data/docs/en-US/compiler.md +157 -0
- data/docs/en-US/conventions.md +82 -0
- data/docs/en-US/design-system.md +163 -0
- data/docs/en-US/entity-dsl.md +53 -0
- data/docs/en-US/native-runtime-quality-report.md +73 -0
- data/docs/en-US/oql-sql.md +201 -0
- data/docs/en-US/platform-api-integration.md +36 -0
- data/docs/en-US/platform-operations.md +244 -0
- data/docs/en-US/project-structure.md +49 -0
- data/docs/en-US/ruby-first-roadmap.md +186 -0
- data/docs/en-US/scaffolds.md +57 -0
- data/docs/en-US/semantic-refactoring.md +198 -0
- data/docs/en-US/team-server.md +32 -0
- data/docs/en-US/validation-matrix.md +257 -0
- data/docs/en-US/vetclinic-acceptance.md +23 -0
- data/docs/en-US/writing.md +655 -0
- data/docs/pt-BR/README.md +24 -0
- data/docs/pt-BR/architectural-patterns.md +25 -0
- data/docs/pt-BR/architectural-standard.md +533 -0
- data/docs/pt-BR/architecture.md +300 -0
- data/docs/pt-BR/compiler.md +162 -0
- data/docs/pt-BR/conventions.md +19 -0
- data/docs/pt-BR/design-system.md +65 -0
- data/docs/pt-BR/entity-dsl.md +116 -0
- data/docs/pt-BR/native-runtime-quality-report.md +73 -0
- data/docs/pt-BR/oql-sql.md +208 -0
- data/docs/pt-BR/platform-api-integration.md +36 -0
- data/docs/pt-BR/platform-operations.md +260 -0
- data/docs/pt-BR/project-structure.md +32 -0
- data/docs/pt-BR/ruby-first-roadmap.md +239 -0
- data/docs/pt-BR/scaffolds.md +104 -0
- data/docs/pt-BR/semantic-refactoring.md +55 -0
- data/docs/pt-BR/team-server.md +59 -0
- data/docs/pt-BR/validation-matrix.md +86 -0
- data/docs/pt-BR/vetclinic-acceptance.md +50 -0
- data/docs/pt-BR/writing.md +177 -0
- data/examples/sudoku_evaluation.rb +14 -0
- data/examples/sudoku_functional_test.rb +28 -0
- data/lib/mxrb/architecture/graph.rb +124 -0
- data/lib/mxrb/architecture/validator.rb +98 -0
- data/lib/mxrb/benchmark.rb +33 -0
- data/lib/mxrb/compare.rb +390 -0
- data/lib/mxrb/compiler/adapter.rb +96 -0
- data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
- data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
- data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
- data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
- data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
- data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
- data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
- data/lib/mxrb/compiler/constants_materializer.rb +111 -0
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
- data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
- data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
- data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
- data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
- data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
- data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
- data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
- data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
- data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
- data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
- data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
- data/lib/mxrb/compiler/mda.rb +99 -0
- data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
- data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
- data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
- data/lib/mxrb/compiler/model_package.rb +116 -0
- data/lib/mxrb/compiler/model_values.rb +67 -0
- data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
- data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
- data/lib/mxrb/compiler/packager.rb +100 -0
- data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
- data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
- data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
- data/lib/mxrb/compiler/portable_packager.rb +356 -0
- data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
- data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
- data/lib/mxrb/compiler/project_materializer.rb +139 -0
- data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
- data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
- data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
- data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
- data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
- data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
- data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
- data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
- data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
- data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
- data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
- data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
- data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
- data/lib/mxrb/compiler/security_materializer.rb +104 -0
- data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
- data/lib/mxrb/compiler/settings_materializer.rb +25 -0
- data/lib/mxrb/compiler/source_model.rb +128 -0
- data/lib/mxrb/compiler/system_model_seed.rb +88 -0
- data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
- data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
- data/lib/mxrb/compiler/translation_materializer.rb +95 -0
- data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
- data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
- data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
- data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
- data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
- data/lib/mxrb/doctor.rb +116 -0
- data/lib/mxrb/dsl/builder.rb +1636 -0
- data/lib/mxrb/errors.rb +18 -0
- data/lib/mxrb/evaluation.rb +131 -0
- data/lib/mxrb/exporter.rb +1716 -0
- data/lib/mxrb/frontend/migrator.rb +775 -0
- data/lib/mxrb/functional.rb +307 -0
- data/lib/mxrb/github/annotator.rb +161 -0
- data/lib/mxrb/initializer.rb +257 -0
- data/lib/mxrb/integrity/validator.rb +136 -0
- data/lib/mxrb/io/bson_codec.rb +145 -0
- data/lib/mxrb/io/mpr_file.rb +558 -0
- data/lib/mxrb/io/mxunit_codec.rb +42 -0
- data/lib/mxrb/marketplace.rb +380 -0
- data/lib/mxrb/model/association.rb +98 -0
- data/lib/mxrb/model/attribute.rb +100 -0
- data/lib/mxrb/model/connector.rb +17 -0
- data/lib/mxrb/model/design_materializer.rb +133 -0
- data/lib/mxrb/model/design_migration.rb +126 -0
- data/lib/mxrb/model/design_system.rb +111 -0
- data/lib/mxrb/model/domain_model.rb +51 -0
- data/lib/mxrb/model/entity.rb +180 -0
- data/lib/mxrb/model/menu.rb +38 -0
- data/lib/mxrb/model/microflow.rb +100 -0
- data/lib/mxrb/model/module.rb +142 -0
- data/lib/mxrb/model/navigation.rb +127 -0
- data/lib/mxrb/model/page.rb +410 -0
- data/lib/mxrb/model/project.rb +187 -0
- data/lib/mxrb/model/unit.rb +48 -0
- data/lib/mxrb/module_initializer.rb +75 -0
- data/lib/mxrb/official_marketplace/content_api.rb +292 -0
- data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
- data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
- data/lib/mxrb/official_marketplace.rb +720 -0
- data/lib/mxrb/oql/analyzer.rb +155 -0
- data/lib/mxrb/oql/index_advisor.rb +85 -0
- data/lib/mxrb/oql/plan_analyzer.rb +185 -0
- data/lib/mxrb/oql/server.rb +132 -0
- data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
- data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
- data/lib/mxrb/oql/workload_analyzer.rb +175 -0
- data/lib/mxrb/oql/workload_baseline.rb +60 -0
- data/lib/mxrb/oql.rb +424 -0
- data/lib/mxrb/project_lifecycle.rb +76 -0
- data/lib/mxrb/protocols/plan.rb +86 -0
- data/lib/mxrb/protocols.rb +123 -0
- data/lib/mxrb/runtime/database_workspace.rb +677 -0
- data/lib/mxrb/runtime/docker_executor.rb +41 -0
- data/lib/mxrb/runtime/docker_workspace.rb +29 -0
- data/lib/mxrb/runtime/executor.rb +169 -0
- data/lib/mxrb/runtime/java_locator.rb +26 -0
- data/lib/mxrb/runtime/native.rb +474 -0
- data/lib/mxrb/runtime/sql_server_database.rb +146 -0
- data/lib/mxrb/runtime/toolchain.rb +82 -0
- data/lib/mxrb/scaffold/cli.rb +113 -0
- data/lib/mxrb/scaffold/generator.rb +246 -0
- data/lib/mxrb/scaffold/help.rb +80 -0
- data/lib/mxrb/scaffold/page_templates.rb +54 -0
- data/lib/mxrb/scaffold/recipes.rb +198 -0
- data/lib/mxrb/scaffold/registry.rb +82 -0
- data/lib/mxrb/scaffold/templates.rb +723 -0
- data/lib/mxrb/scaffold/transaction.rb +84 -0
- data/lib/mxrb/schema/tables.rb +55 -0
- data/lib/mxrb/semantic/analyzer.rb +473 -0
- data/lib/mxrb/semantic/batch_plan.rb +68 -0
- data/lib/mxrb/semantic/domain_mutator.rb +419 -0
- data/lib/mxrb/semantic/embedder.rb +51 -0
- data/lib/mxrb/semantic/extractor.rb +290 -0
- data/lib/mxrb/semantic/index.rb +623 -0
- data/lib/mxrb/semantic/inliner.rb +216 -0
- data/lib/mxrb/semantic/mover.rb +153 -0
- data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
- data/lib/mxrb/semantic/remover.rb +70 -0
- data/lib/mxrb/semantic/renamer.rb +211 -0
- data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
- data/lib/mxrb/semantic/vec_store.rb +75 -0
- data/lib/mxrb/team_server.rb +390 -0
- data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
- data/lib/mxrb/templates/project/11.12.1.json +32 -0
- data/lib/mxrb/templates/project/6.10.8.json +50 -0
- data/lib/mxrb/templates/project/7.17.0.json +50 -0
- data/lib/mxrb/templates/project/7.5.0.json +50 -0
- data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
- data/lib/mxrb/version.rb +5 -0
- data/lib/mxrb/widget_package.rb +338 -0
- data/lib/mxrb/widget_synchronizer.rb +31 -0
- data/lib/mxrb/writer.rb +3775 -0
- data/lib/mxrb.rb +188 -0
- data/marketplace/catalog.json +10 -0
- data/marketplace/modules/shared-kernel/domain/README.md +4 -0
- data/marketplace/modules/shared-kernel/module.rb +5 -0
- data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
- metadata +447 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rexml/document'
|
|
4
|
+
|
|
5
|
+
module Mxrb
|
|
6
|
+
module Oql
|
|
7
|
+
# Parses SQL Server SHOWPLAN XML without executing or rewriting the query.
|
|
8
|
+
# Missing-index entries are optimizer hypotheses, not DDL instructions.
|
|
9
|
+
class SqlServerPlanAnalyzer # rubocop:disable Metrics/ClassLength
|
|
10
|
+
LARGE_ROWS = 1_000
|
|
11
|
+
HIGH_NESTED_ROWS = 10_000
|
|
12
|
+
MISESTIMATE_RATIO = 10.0
|
|
13
|
+
|
|
14
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
15
|
+
def analyze(xml)
|
|
16
|
+
document = REXML::Document.new(xml.to_s)
|
|
17
|
+
root = document.root
|
|
18
|
+
raise ArgumentError, 'invalid SQL Server SHOWPLAN XML' unless root&.name == 'ShowPlanXML'
|
|
19
|
+
|
|
20
|
+
findings = [*operator_findings(root), *missing_index_findings(root)].freeze
|
|
21
|
+
statement = elements(root, 'StmtSimple').first
|
|
22
|
+
runtime = elements(root, 'QueryTimeStats').first
|
|
23
|
+
PlanReport.new(
|
|
24
|
+
:sql_server, !runtime.nil?, nil, attribute_number(runtime, 'ElapsedTime'),
|
|
25
|
+
attribute_number(statement, 'StatementSubTreeCost'), findings, xml.to_s.freeze
|
|
26
|
+
)
|
|
27
|
+
rescue REXML::ParseException => e
|
|
28
|
+
raise ArgumentError, "invalid SQL Server SHOWPLAN XML: #{e.message}"
|
|
29
|
+
end
|
|
30
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def operator_findings(root)
|
|
35
|
+
elements(root, 'RelOp').flat_map do |operator|
|
|
36
|
+
[
|
|
37
|
+
scan_finding(operator), nested_loop_finding(operator),
|
|
38
|
+
spill_finding(operator), misestimate_finding(operator)
|
|
39
|
+
].compact
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def scan_finding(operator)
|
|
44
|
+
physical = operator.attributes['PhysicalOp'].to_s
|
|
45
|
+
return unless ['Table Scan', 'Clustered Index Scan'].include?(physical)
|
|
46
|
+
return unless estimated_rows(operator) >= LARGE_ROWS
|
|
47
|
+
|
|
48
|
+
object = operator_object(operator)
|
|
49
|
+
PlanFinding.new(
|
|
50
|
+
:sql_server_scan, :warning, physical, object[:relation],
|
|
51
|
+
"#{physical} processes a large estimated row set.",
|
|
52
|
+
'Review predicate selectivity and the actual plan; a scan can still be optimal.',
|
|
53
|
+
operator_metrics(operator).freeze, object[:indexes].freeze
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def nested_loop_finding(operator)
|
|
58
|
+
return unless operator.attributes['PhysicalOp'] == 'Nested Loops'
|
|
59
|
+
return unless estimated_rows(operator) >= HIGH_NESTED_ROWS
|
|
60
|
+
|
|
61
|
+
operator_finding(
|
|
62
|
+
:sql_server_high_volume_nested_loop, operator,
|
|
63
|
+
'A nested-loops join processes a high estimated row volume.',
|
|
64
|
+
'Review indexes on inner join keys and compare hash or merge alternatives in the actual plan.'
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def spill_finding(operator)
|
|
69
|
+
spill = descendants(operator).find do |element|
|
|
70
|
+
%w[SpillToTempDb SortSpillDetails HashSpillDetails].include?(element.name)
|
|
71
|
+
end
|
|
72
|
+
return unless spill
|
|
73
|
+
|
|
74
|
+
operator_finding(
|
|
75
|
+
:sql_server_tempdb_spill, operator,
|
|
76
|
+
'The actual plan reports a spill to tempdb.',
|
|
77
|
+
'Reduce intermediate rows and review memory grants, cardinality estimates, sorts, and hashes.'
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def misestimate_finding(operator)
|
|
82
|
+
estimated = estimated_rows(operator)
|
|
83
|
+
actual = actual_rows(operator)
|
|
84
|
+
return if estimated.zero? || actual.zero?
|
|
85
|
+
return unless [estimated, actual].max >= 100 && ratio(estimated, actual) >= MISESTIMATE_RATIO
|
|
86
|
+
|
|
87
|
+
operator_finding(
|
|
88
|
+
:sql_server_cardinality_misestimation, operator,
|
|
89
|
+
'Estimated and actual operator rows differ by at least 10x.',
|
|
90
|
+
'Update statistics and inspect parameter sensitivity, predicates, and correlated columns.'
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
95
|
+
def missing_index_findings(root)
|
|
96
|
+
elements(root, 'MissingIndexGroup').map do |group|
|
|
97
|
+
index = elements(group, 'MissingIndex').first
|
|
98
|
+
columns = elements(group, 'Column').map do |column|
|
|
99
|
+
{ name: clean_identifier(column.attributes['Name']),
|
|
100
|
+
usage: column.parent.attributes['Usage'] }.freeze
|
|
101
|
+
end
|
|
102
|
+
relation = qualified_object(index)
|
|
103
|
+
impact = attribute_number(group, 'Impact')
|
|
104
|
+
PlanFinding.new(
|
|
105
|
+
:sql_server_missing_index, :hint, 'MissingIndex', relation,
|
|
106
|
+
"SQL Server estimates a missing-index impact of #{impact}%.",
|
|
107
|
+
'Treat this as a hypothesis: compare with existing indexes, write cost, and representative workload.',
|
|
108
|
+
{ impact_percent: impact }.freeze, columns.freeze
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
113
|
+
|
|
114
|
+
def operator_finding(rule, operator, message, suggestion)
|
|
115
|
+
object = operator_object(operator)
|
|
116
|
+
PlanFinding.new(
|
|
117
|
+
rule, :warning, operator.attributes['PhysicalOp'], object[:relation], message, suggestion,
|
|
118
|
+
operator_metrics(operator).freeze, object[:indexes].freeze
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def operator_object(operator)
|
|
123
|
+
object = descendants(operator).find { _1.name == 'Object' }
|
|
124
|
+
return { relation: nil, indexes: [] } unless object
|
|
125
|
+
|
|
126
|
+
index = clean_identifier(object.attributes['Index'])
|
|
127
|
+
{
|
|
128
|
+
relation: qualified_object(object),
|
|
129
|
+
indexes: index ? [{ name: index, definition: nil }.freeze] : []
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def qualified_object(element)
|
|
134
|
+
return unless element
|
|
135
|
+
|
|
136
|
+
%w[Database Schema Table].filter_map do |name|
|
|
137
|
+
clean_identifier(element.attributes[name])
|
|
138
|
+
end.join('.')
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def clean_identifier(value)
|
|
142
|
+
text = value.to_s
|
|
143
|
+
return if text.empty?
|
|
144
|
+
|
|
145
|
+
text.delete_prefix('[').delete_suffix(']')
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def operator_metrics(operator)
|
|
149
|
+
{
|
|
150
|
+
estimated_rows: estimated_rows(operator), actual_rows: actual_rows(operator),
|
|
151
|
+
estimated_cost: attribute_number(operator, 'EstimatedTotalSubtreeCost'),
|
|
152
|
+
actual_executions: runtime_counters(operator).sum do |counter|
|
|
153
|
+
attribute_number(counter, 'ActualExecutions')
|
|
154
|
+
end
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def estimated_rows(operator)
|
|
159
|
+
attribute_number(operator, 'EstimateRows')
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def actual_rows(operator)
|
|
163
|
+
runtime_counters(operator).sum { attribute_number(_1, 'ActualRows') }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def runtime_counters(operator)
|
|
167
|
+
descendants(operator).take_while { _1.name != 'RelOp' }
|
|
168
|
+
.select { _1.name == 'RunTimeCountersPerThread' }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def descendants(element)
|
|
172
|
+
result = []
|
|
173
|
+
element.each_recursive { result << _1 }
|
|
174
|
+
result
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def elements(element, name)
|
|
178
|
+
descendants(element).select { _1.name == name }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def attribute_number(element, name)
|
|
182
|
+
element ? element.attributes[name].to_f : 0.0
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def ratio(left, right)
|
|
186
|
+
[left, right].max / [left, right].min
|
|
187
|
+
end
|
|
188
|
+
end # rubocop:enable Metrics/ClassLength
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mxrb
|
|
4
|
+
module Oql
|
|
5
|
+
SqlServerWorkloadQuery = Data.define(
|
|
6
|
+
:query_id, :query, :calls, :total_time_ms, :mean_time_ms, :rows,
|
|
7
|
+
:logical_reads, :physical_reads, :temp_writes, :io_time_ms, :cache_hit_ratio
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
# Normalizes read-only SQL Server DMV snapshots into the shared workload report.
|
|
11
|
+
class SqlServerWorkloadAnalyzer
|
|
12
|
+
def analyze(query_rows:, table_rows:, index_rows:)
|
|
13
|
+
queries = query_rows.map { query_entry(_1) }.freeze
|
|
14
|
+
findings = [
|
|
15
|
+
*queries.filter_map { query_finding(_1) },
|
|
16
|
+
*table_rows.filter_map { table_finding(_1) },
|
|
17
|
+
*index_rows.filter_map { index_finding(_1) }
|
|
18
|
+
].freeze
|
|
19
|
+
WorkloadReport.new(:sql_server, queries, table_rows.freeze, index_rows.freeze, findings)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def query_entry(row) # rubocop:disable Metrics/AbcSize
|
|
25
|
+
calls = row['execution_count'].to_i
|
|
26
|
+
total = row['total_elapsed_time'].to_f / 1_000.0
|
|
27
|
+
SqlServerWorkloadQuery.new(
|
|
28
|
+
row['query_hash'], row['query_text'], calls, total,
|
|
29
|
+
calls.zero? ? 0.0 : total / calls, row['total_rows'].to_i,
|
|
30
|
+
row['total_logical_reads'].to_i, row['total_physical_reads'].to_i,
|
|
31
|
+
0, 0.0, cache_ratio(row)
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def cache_ratio(row)
|
|
36
|
+
logical = row['total_logical_reads'].to_i
|
|
37
|
+
physical = row['total_physical_reads'].to_i
|
|
38
|
+
return 1.0 if logical.zero?
|
|
39
|
+
|
|
40
|
+
(1.0 - physical.to_f / logical).clamp(0.0, 1.0).round(4)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def query_finding(query)
|
|
44
|
+
return unless query.total_time_ms >= 1_000 || query.mean_time_ms >= 100
|
|
45
|
+
|
|
46
|
+
WorkloadFinding.new(
|
|
47
|
+
:sql_server_expensive_query, :warning, query.query_id,
|
|
48
|
+
'The DMV snapshot reports substantial elapsed time for this query fingerprint.',
|
|
49
|
+
'Inspect Query Store history and the actual plan for regressions and parameter sensitivity.',
|
|
50
|
+
query.to_h.freeze
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def table_finding(row)
|
|
55
|
+
scans = row['user_scans'].to_i
|
|
56
|
+
seeks = row['user_seeks'].to_i
|
|
57
|
+
return unless scans > seeks && scans >= 100
|
|
58
|
+
|
|
59
|
+
WorkloadFinding.new(
|
|
60
|
+
:sql_server_scan_pressure, :warning, row['relation'],
|
|
61
|
+
'Index usage DMVs report more scans than seeks.',
|
|
62
|
+
'Correlate the table with expensive fingerprints before changing indexes.',
|
|
63
|
+
{ user_scans: scans, user_seeks: seeks }.freeze
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def index_finding(row)
|
|
68
|
+
return unless row['user_seeks'].to_i.zero? && row['user_scans'].to_i.zero?
|
|
69
|
+
return unless row['index_bytes'].to_i >= 1_048_576
|
|
70
|
+
|
|
71
|
+
WorkloadFinding.new(
|
|
72
|
+
:sql_server_unused_large_index, :warning, row['index_name'],
|
|
73
|
+
'A large index has no seeks or scans in the current DMV window.',
|
|
74
|
+
'Confirm restart time and representative workload before considering removal.',
|
|
75
|
+
{ index_bytes: row['index_bytes'].to_i }.freeze
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mxrb
|
|
4
|
+
module Oql
|
|
5
|
+
WorkloadQuery = Data.define(
|
|
6
|
+
:query_id, :query, :calls, :total_time_ms, :mean_time_ms, :rows,
|
|
7
|
+
:shared_hits, :shared_reads, :temp_writes, :io_time_ms, :cache_hit_ratio
|
|
8
|
+
)
|
|
9
|
+
WorkloadFinding = Data.define(:rule, :severity, :subject, :message, :suggestion, :metrics)
|
|
10
|
+
WorkloadReport = Data.define(:engine, :queries, :table_stats, :index_stats, :findings) do
|
|
11
|
+
def clean? = findings.none? { _1.severity == :warning }
|
|
12
|
+
def warnings? = !clean?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Interprets cumulative PostgreSQL statistics without pretending that one
|
|
16
|
+
# threshold is universally optimal. Findings retain the measured values so
|
|
17
|
+
# callers can rank them against their own latency and throughput budgets.
|
|
18
|
+
class WorkloadAnalyzer # rubocop:disable Metrics/ClassLength
|
|
19
|
+
TOTAL_TIME_MS = 1_000.0
|
|
20
|
+
MEAN_TIME_MS = 100.0
|
|
21
|
+
MIN_BUFFER_BLOCKS = 100
|
|
22
|
+
MIN_CACHE_HIT_RATIO = 0.90
|
|
23
|
+
HIGH_ROWS_PER_CALL = 10_000
|
|
24
|
+
LARGE_UNUSED_INDEX_BYTES = 1_048_576
|
|
25
|
+
HEAVY_SEQ_ROWS = 100_000
|
|
26
|
+
|
|
27
|
+
DEFAULT_THRESHOLDS = {
|
|
28
|
+
total_time_ms: TOTAL_TIME_MS, mean_time_ms: MEAN_TIME_MS,
|
|
29
|
+
min_buffer_blocks: MIN_BUFFER_BLOCKS, min_cache_hit_ratio: MIN_CACHE_HIT_RATIO,
|
|
30
|
+
high_rows_per_call: HIGH_ROWS_PER_CALL,
|
|
31
|
+
large_unused_index_bytes: LARGE_UNUSED_INDEX_BYTES, heavy_seq_rows: HEAVY_SEQ_ROWS
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
def initialize(**thresholds)
|
|
35
|
+
unknown = thresholds.keys - DEFAULT_THRESHOLDS.keys
|
|
36
|
+
raise ArgumentError, "unknown workload thresholds: #{unknown.join(', ')}" unless unknown.empty?
|
|
37
|
+
|
|
38
|
+
@thresholds = DEFAULT_THRESHOLDS.merge(thresholds).freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def analyze(query_rows:, table_rows:, index_rows:)
|
|
42
|
+
queries = query_rows.map { query_entry(_1) }.freeze
|
|
43
|
+
findings = [
|
|
44
|
+
*queries.flat_map { query_findings(_1) },
|
|
45
|
+
*table_rows.filter_map { table_finding(_1) },
|
|
46
|
+
*index_rows.filter_map { index_finding(_1) }
|
|
47
|
+
].freeze
|
|
48
|
+
WorkloadReport.new(:postgresql, queries, table_rows.freeze, index_rows.freeze, findings)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def query_entry(row) # rubocop:disable Metrics/AbcSize
|
|
54
|
+
hits = integer(row['shared_blks_hit'])
|
|
55
|
+
reads = integer(row['shared_blks_read'])
|
|
56
|
+
total_blocks = hits + reads
|
|
57
|
+
ratio = total_blocks.zero? ? 1.0 : hits.to_f / total_blocks
|
|
58
|
+
WorkloadQuery.new(
|
|
59
|
+
row['queryid'], row['query'], integer(row['calls']), float(row['total_exec_time']),
|
|
60
|
+
float(row['mean_exec_time']), integer(row['rows']), hits, reads,
|
|
61
|
+
integer(row['temp_blks_written']), float(row['blk_read_time']) + float(row['blk_write_time']),
|
|
62
|
+
ratio.round(4)
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def query_findings(entry)
|
|
67
|
+
[
|
|
68
|
+
cumulative_time(entry), slow_mean(entry), low_cache_hit(entry),
|
|
69
|
+
temporary_writes(entry), high_rows(entry)
|
|
70
|
+
].compact
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def cumulative_time(entry)
|
|
74
|
+
return unless entry.total_time_ms >= threshold(:total_time_ms)
|
|
75
|
+
|
|
76
|
+
query_finding(
|
|
77
|
+
:high_cumulative_time, entry,
|
|
78
|
+
'This query fingerprint consumes substantial cumulative execution time.',
|
|
79
|
+
'Prioritize it by total time, then inspect its real plan with db explain --analyze.'
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def slow_mean(entry)
|
|
84
|
+
return unless entry.mean_time_ms >= threshold(:mean_time_ms)
|
|
85
|
+
|
|
86
|
+
query_finding(
|
|
87
|
+
:high_mean_time, entry, 'Mean execution time exceeds 100 ms.',
|
|
88
|
+
'Inspect plan stability, cardinality estimates, locks, I/O, and returned row volume.'
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def low_cache_hit(entry)
|
|
93
|
+
blocks = entry.shared_hits + entry.shared_reads
|
|
94
|
+
return unless blocks >= threshold(:min_buffer_blocks) &&
|
|
95
|
+
entry.cache_hit_ratio < threshold(:min_cache_hit_ratio)
|
|
96
|
+
|
|
97
|
+
query_finding(
|
|
98
|
+
:low_cache_hit, entry, 'A significant share of shared blocks came from storage.',
|
|
99
|
+
'Check working-set size, access locality, indexes, and whether the query reads excess rows.'
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def temporary_writes(entry)
|
|
104
|
+
return unless entry.temp_writes.positive?
|
|
105
|
+
|
|
106
|
+
query_finding(
|
|
107
|
+
:temporary_block_writes, entry, 'The query wrote temporary blocks.',
|
|
108
|
+
'Inspect sorts and hashes, reduce intermediate rows, and review work_mem per workload.'
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def high_rows(entry)
|
|
113
|
+
return if entry.calls.zero? || entry.rows / entry.calls < threshold(:high_rows_per_call)
|
|
114
|
+
|
|
115
|
+
query_finding(
|
|
116
|
+
:high_rows_per_call, entry, 'The query returns or processes many rows per call.',
|
|
117
|
+
'Project only required columns and rows; consider pagination or a narrower aggregation.'
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def query_finding(rule, entry, message, suggestion)
|
|
122
|
+
WorkloadFinding.new(
|
|
123
|
+
rule, :warning, entry.query_id, message, suggestion,
|
|
124
|
+
{
|
|
125
|
+
calls: entry.calls, total_time_ms: entry.total_time_ms,
|
|
126
|
+
mean_time_ms: entry.mean_time_ms, rows: entry.rows,
|
|
127
|
+
cache_hit_ratio: entry.cache_hit_ratio, temp_writes: entry.temp_writes,
|
|
128
|
+
io_time_ms: entry.io_time_ms
|
|
129
|
+
}.freeze
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def table_finding(row) # rubocop:disable Metrics/MethodLength
|
|
134
|
+
seq_rows = integer(row['seq_tup_read'])
|
|
135
|
+
seq_scans = integer(row['seq_scan'])
|
|
136
|
+
index_scans = integer(row['idx_scan'])
|
|
137
|
+
return unless seq_rows >= threshold(:heavy_seq_rows) && seq_scans > index_scans
|
|
138
|
+
|
|
139
|
+
WorkloadFinding.new(
|
|
140
|
+
:table_sequential_pressure, :warning, qualified(row, 'relname'),
|
|
141
|
+
'Cumulative table statistics show more sequential than index scans.',
|
|
142
|
+
'Inspect the highest-cost query fingerprints before adding or changing indexes.',
|
|
143
|
+
{ seq_scan: seq_scans, seq_tup_read: seq_rows, idx_scan: index_scans,
|
|
144
|
+
live_rows: integer(row['n_live_tup']) }.freeze
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def index_finding(row)
|
|
149
|
+
return if truthy?(row['indisunique']) || truthy?(row['indisprimary'])
|
|
150
|
+
|
|
151
|
+
size = integer(row['index_bytes'])
|
|
152
|
+
return unless integer(row['idx_scan']).zero? && size >= threshold(:large_unused_index_bytes)
|
|
153
|
+
|
|
154
|
+
WorkloadFinding.new(
|
|
155
|
+
:unused_large_index, :warning, qualified(row, 'indexrelname'),
|
|
156
|
+
'A non-unique index occupies at least 1 MiB but has no scans in the current statistics window.',
|
|
157
|
+
'Confirm the statistics reset time and production workload before considering removal.',
|
|
158
|
+
{ idx_scan: 0, index_bytes: size, idx_tup_read: integer(row['idx_tup_read']) }.freeze
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def qualified(row, name_key)
|
|
163
|
+
[row['schemaname'], row[name_key]].compact.join('.')
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def truthy?(value)
|
|
167
|
+
%w[t true 1].include?(value.to_s.downcase)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def integer(value) = value.to_i
|
|
171
|
+
def float(value) = value.to_f
|
|
172
|
+
def threshold(name) = @thresholds.fetch(name)
|
|
173
|
+
end # rubocop:enable Metrics/ClassLength
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
module Mxrb
|
|
7
|
+
module Oql
|
|
8
|
+
WorkloadDelta = Data.define(:query_id, :metric, :before, :after, :change_percent)
|
|
9
|
+
WorkloadComparison = Data.define(:deltas) do
|
|
10
|
+
def regressions = deltas.select { _1.change_percent.positive? }
|
|
11
|
+
def improvements = deltas.select { _1.change_percent.negative? }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Serializable workload snapshot and deterministic regression comparison.
|
|
15
|
+
module WorkloadBaseline
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
METRICS = %w[total_time_ms mean_time_ms io_time_ms temp_writes rows].freeze
|
|
19
|
+
|
|
20
|
+
def dump(report)
|
|
21
|
+
JSON.pretty_generate(
|
|
22
|
+
version: 1, engine: report.engine, captured_at: Time.now.utc.iso8601,
|
|
23
|
+
queries: report.queries.to_h { [_1.query_id.to_s, _1.to_h] }
|
|
24
|
+
) << "\n"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def load(source)
|
|
28
|
+
payload = source.is_a?(Hash) ? source : JSON.parse(File.read(source))
|
|
29
|
+
raise ArgumentError, 'unsupported workload baseline version' unless payload['version'] == 1
|
|
30
|
+
|
|
31
|
+
payload
|
|
32
|
+
rescue JSON::ParserError, Errno::ENOENT => e
|
|
33
|
+
raise ArgumentError, "cannot read workload baseline: #{e.message}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def compare(report, baseline)
|
|
37
|
+
previous = load(baseline).fetch('queries')
|
|
38
|
+
deltas = report.queries.flat_map { query_deltas(_1, previous[_1.query_id.to_s]) }
|
|
39
|
+
WorkloadComparison.new(deltas.freeze)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def query_deltas(query, before)
|
|
43
|
+
return [] unless before
|
|
44
|
+
|
|
45
|
+
METRICS.filter_map do |metric|
|
|
46
|
+
delta(query.query_id, metric, Float(before.fetch(metric)), Float(query.to_h.fetch(metric.to_sym)))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
private_class_method :query_deltas
|
|
50
|
+
|
|
51
|
+
def delta(query_id, metric, before, after)
|
|
52
|
+
return if before.zero? && after.zero?
|
|
53
|
+
|
|
54
|
+
percent = before.zero? ? 100.0 : ((after - before) * 100.0 / before).round(2)
|
|
55
|
+
WorkloadDelta.new(query_id, metric.to_sym, before, after, percent)
|
|
56
|
+
end
|
|
57
|
+
private_class_method :delta
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|