@query-doctor/core 0.0.3 → 0.0.4
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.
- package/dist/index.cjs +222 -97
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +218 -97
- package/dist/index.js.map +1 -1
- package/dist/optimizer/genalgo.d.ts +9 -5
- package/dist/optimizer/genalgo.d.ts.map +1 -1
- package/dist/optimizer/genalgo.js +304 -0
- package/dist/optimizer/statistics.d.ts +1 -24
- package/dist/optimizer/statistics.d.ts.map +1 -1
- package/dist/optimizer/statistics.js +700 -0
- package/dist/package.json +25 -0
- package/dist/sql/analyzer.d.ts +3 -3
- package/dist/sql/analyzer.js +270 -0
- package/dist/sql/analyzer.test.d.ts +2 -0
- package/dist/sql/analyzer.test.d.ts.map +1 -0
- package/dist/sql/analyzer_test.js +584 -0
- package/dist/sql/builder.js +77 -0
- package/dist/sql/database.d.ts +5 -0
- package/dist/sql/database.d.ts.map +1 -1
- package/dist/sql/database.js +20 -0
- package/dist/sql/indexes.d.ts +8 -0
- package/dist/sql/indexes.d.ts.map +1 -0
- package/dist/sql/indexes.js +12 -0
- package/dist/sql/nudges.js +241 -0
- package/dist/sql/permutations.test.d.ts +2 -0
- package/dist/sql/permutations.test.d.ts.map +1 -0
- package/dist/sql/permutations_test.js +53 -0
- package/dist/sql/pg-identifier.d.ts +9 -0
- package/dist/sql/pg-identifier.d.ts.map +1 -0
- package/dist/sql/pg-identifier.test.d.ts +2 -0
- package/dist/sql/pg-identifier.test.d.ts.map +1 -0
- package/dist/sql/walker.d.ts +2 -2
- package/dist/sql/walker.js +295 -0
- package/package.json +2 -2
- package/dist/index.mjs +0 -24297
- package/dist/index.mjs.map +0 -1
- package/dist/sql/schema_dump.d.ts +0 -132
- package/dist/sql/schema_dump.d.ts.map +0 -1
- package/dist/sql/trace.d.ts +0 -1
- package/dist/sql/trace.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -704,9 +704,26 @@ var Analyzer = class {
|
|
|
704
704
|
// src/sql/database.ts
|
|
705
705
|
import { z } from "zod";
|
|
706
706
|
var PostgresVersion = z.string().brand("PostgresVersion");
|
|
707
|
+
async function dropIndex(tx, index) {
|
|
708
|
+
try {
|
|
709
|
+
await tx.exec(`
|
|
710
|
+
savepoint idx_drop;
|
|
711
|
+
drop index if exists ${index} cascade;
|
|
712
|
+
`);
|
|
713
|
+
return true;
|
|
714
|
+
} catch (error) {
|
|
715
|
+
await tx.exec(`rollback to idx_drop`);
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
707
719
|
|
|
708
|
-
// src/
|
|
709
|
-
|
|
720
|
+
// src/sql/indexes.ts
|
|
721
|
+
function isIndexSupported(index) {
|
|
722
|
+
return index.index_type === "btree";
|
|
723
|
+
}
|
|
724
|
+
function isIndexProbablyDroppable(index) {
|
|
725
|
+
return !index.is_primary && !index.is_unique;
|
|
726
|
+
}
|
|
710
727
|
|
|
711
728
|
// src/sql/builder.ts
|
|
712
729
|
var PostgresQueryBuilder = class _PostgresQueryBuilder {
|
|
@@ -788,7 +805,141 @@ var PostgresQueryBuilder = class _PostgresQueryBuilder {
|
|
|
788
805
|
}
|
|
789
806
|
};
|
|
790
807
|
|
|
808
|
+
// src/sql/pg-identifier.ts
|
|
809
|
+
var _PgIdentifier = class _PgIdentifier {
|
|
810
|
+
constructor(value, quoted) {
|
|
811
|
+
this.value = value;
|
|
812
|
+
this.quoted = quoted;
|
|
813
|
+
}
|
|
814
|
+
static fromString(identifier) {
|
|
815
|
+
const identifierRegex = /^[a-z_][a-zA-Z0-9_]*$/;
|
|
816
|
+
const match = identifier.match(/^"(.+)"$/);
|
|
817
|
+
if (match) {
|
|
818
|
+
return new _PgIdentifier(match[1], true);
|
|
819
|
+
}
|
|
820
|
+
const quoted = !identifierRegex.test(identifier) || this.reservedKeywords.has(identifier.toLowerCase());
|
|
821
|
+
return new _PgIdentifier(identifier, quoted);
|
|
822
|
+
}
|
|
823
|
+
toString() {
|
|
824
|
+
if (this.quoted) {
|
|
825
|
+
return `"${this.value.replace(/"/g, '""')}"`;
|
|
826
|
+
}
|
|
827
|
+
return this.value;
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
__publicField(_PgIdentifier, "reservedKeywords", /* @__PURE__ */ new Set([
|
|
831
|
+
"abort",
|
|
832
|
+
"analyze",
|
|
833
|
+
"binary",
|
|
834
|
+
"cluster",
|
|
835
|
+
"constraint",
|
|
836
|
+
"copy",
|
|
837
|
+
"do",
|
|
838
|
+
"explain",
|
|
839
|
+
"extend",
|
|
840
|
+
"listen",
|
|
841
|
+
"load",
|
|
842
|
+
"lock",
|
|
843
|
+
"move",
|
|
844
|
+
"new",
|
|
845
|
+
"none",
|
|
846
|
+
"notify",
|
|
847
|
+
"offset",
|
|
848
|
+
"reset",
|
|
849
|
+
"setof",
|
|
850
|
+
"show",
|
|
851
|
+
"unlisten",
|
|
852
|
+
"until",
|
|
853
|
+
"vacuum",
|
|
854
|
+
"verbose",
|
|
855
|
+
"all",
|
|
856
|
+
"any",
|
|
857
|
+
"asc",
|
|
858
|
+
"between",
|
|
859
|
+
"bit",
|
|
860
|
+
"both",
|
|
861
|
+
"case",
|
|
862
|
+
"cast",
|
|
863
|
+
"char",
|
|
864
|
+
"character",
|
|
865
|
+
"check",
|
|
866
|
+
"coalesce",
|
|
867
|
+
"collate",
|
|
868
|
+
"column",
|
|
869
|
+
"constraint",
|
|
870
|
+
"cross",
|
|
871
|
+
"current",
|
|
872
|
+
"current_date",
|
|
873
|
+
"current_time",
|
|
874
|
+
"current_timestamp",
|
|
875
|
+
"current_user",
|
|
876
|
+
"dec",
|
|
877
|
+
"decimal",
|
|
878
|
+
"default",
|
|
879
|
+
"desc",
|
|
880
|
+
"distinct",
|
|
881
|
+
"else",
|
|
882
|
+
"end",
|
|
883
|
+
"except",
|
|
884
|
+
"exists",
|
|
885
|
+
"extract",
|
|
886
|
+
"false",
|
|
887
|
+
"float",
|
|
888
|
+
"for",
|
|
889
|
+
"foreign",
|
|
890
|
+
"from",
|
|
891
|
+
"full",
|
|
892
|
+
"global",
|
|
893
|
+
"group",
|
|
894
|
+
"having",
|
|
895
|
+
"in",
|
|
896
|
+
"inner",
|
|
897
|
+
"intersect",
|
|
898
|
+
"into",
|
|
899
|
+
"is",
|
|
900
|
+
"join",
|
|
901
|
+
"leading",
|
|
902
|
+
"left",
|
|
903
|
+
"like",
|
|
904
|
+
"local",
|
|
905
|
+
"natural",
|
|
906
|
+
"nchar",
|
|
907
|
+
"not",
|
|
908
|
+
"null",
|
|
909
|
+
"nullif",
|
|
910
|
+
"numeric",
|
|
911
|
+
"on",
|
|
912
|
+
"or",
|
|
913
|
+
"order",
|
|
914
|
+
"outer",
|
|
915
|
+
"overlaps",
|
|
916
|
+
"position",
|
|
917
|
+
"precision",
|
|
918
|
+
"primary",
|
|
919
|
+
"public",
|
|
920
|
+
"references",
|
|
921
|
+
"right",
|
|
922
|
+
"select",
|
|
923
|
+
"session_user",
|
|
924
|
+
"some",
|
|
925
|
+
"substring",
|
|
926
|
+
"table",
|
|
927
|
+
"then",
|
|
928
|
+
"to",
|
|
929
|
+
"transaction",
|
|
930
|
+
"trim",
|
|
931
|
+
"true",
|
|
932
|
+
"union",
|
|
933
|
+
"unique",
|
|
934
|
+
"user",
|
|
935
|
+
"varchar",
|
|
936
|
+
"when",
|
|
937
|
+
"where"
|
|
938
|
+
]));
|
|
939
|
+
var PgIdentifier = _PgIdentifier;
|
|
940
|
+
|
|
791
941
|
// src/optimizer/genalgo.ts
|
|
942
|
+
import { blue as blue2, gray, green, magenta, red, yellow } from "colorette";
|
|
792
943
|
var _IndexOptimizer = class _IndexOptimizer {
|
|
793
944
|
constructor(db, statistics, existingIndexes, config = {}) {
|
|
794
945
|
this.db = db;
|
|
@@ -796,8 +947,12 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
796
947
|
this.existingIndexes = existingIndexes;
|
|
797
948
|
this.config = config;
|
|
798
949
|
}
|
|
799
|
-
async run(builder, indexes) {
|
|
800
|
-
const baseExplain = await this.
|
|
950
|
+
async run(builder, indexes, beforeQuery) {
|
|
951
|
+
const baseExplain = await this.testQueryWithStats(builder, async (tx) => {
|
|
952
|
+
if (beforeQuery) {
|
|
953
|
+
await beforeQuery(tx);
|
|
954
|
+
}
|
|
955
|
+
});
|
|
801
956
|
const baseCost = Number(baseExplain.Plan["Total Cost"]);
|
|
802
957
|
if (baseCost === 0) {
|
|
803
958
|
return {
|
|
@@ -805,76 +960,17 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
805
960
|
explainPlan: baseExplain
|
|
806
961
|
};
|
|
807
962
|
}
|
|
808
|
-
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
for (const { table, schema, columns } of permutedIndexes.values()) {
|
|
813
|
-
const permutations = permuteWithFeedback(columns);
|
|
814
|
-
let iter = permutations.next(PROCEED);
|
|
815
|
-
const previousCost = baseCost;
|
|
816
|
-
while (!iter.done) {
|
|
817
|
-
const columns2 = iter.value;
|
|
818
|
-
const existingIndex = this.indexAlreadyExists(table, columns2);
|
|
819
|
-
if (existingIndex) {
|
|
820
|
-
console.log(` <${gray("skip")}> ${gray(existingIndex.index_name)}`);
|
|
821
|
-
iter = permutations.next(PROCEED);
|
|
822
|
-
continue;
|
|
823
|
-
}
|
|
824
|
-
let indexDefinition = "?";
|
|
825
|
-
const indexName = this.indexName();
|
|
826
|
-
const { raw, colored } = this.toDefinition({
|
|
827
|
-
columns: columns2,
|
|
828
|
-
schema,
|
|
829
|
-
table
|
|
830
|
-
});
|
|
831
|
-
const shortenedSchema = schema === "public" ? "" : `"${schema}".`;
|
|
832
|
-
const indexDefinitionClean = `${shortenedSchema}"${table}"(${columns2.map((c) => `"${c.column}"`).join(", ")})`;
|
|
833
|
-
indexDefinition = colored;
|
|
834
|
-
const query = PostgresQueryBuilder.createIndex(
|
|
835
|
-
raw,
|
|
836
|
-
indexName
|
|
837
|
-
).introspect();
|
|
838
|
-
triedIndexes.set(indexName, {
|
|
839
|
-
schema,
|
|
840
|
-
table,
|
|
841
|
-
columns: columns2,
|
|
842
|
-
definition: indexDefinitionClean
|
|
843
|
-
});
|
|
844
|
-
const explain = await this.testQueryWithStats(builder, async (sql) => {
|
|
845
|
-
await sql.exec(query.build());
|
|
846
|
-
});
|
|
847
|
-
const explainCost = Number(explain.Plan["Total Cost"]);
|
|
848
|
-
const costDeltaPercentage = (previousCost - explainCost) / previousCost * 100;
|
|
849
|
-
if (previousCost > explainCost) {
|
|
850
|
-
console.log(
|
|
851
|
-
`${green(
|
|
852
|
-
`+${costDeltaPercentage.toFixed(2).padStart(5, "0")}%`
|
|
853
|
-
)} ${indexDefinition} `
|
|
854
|
-
);
|
|
855
|
-
iter = permutations.next(PROCEED);
|
|
856
|
-
} else {
|
|
857
|
-
console.log(
|
|
858
|
-
`${previousCost === explainCost ? ` ${gray("00.00%")}` : `${red(
|
|
859
|
-
`-${Math.abs(costDeltaPercentage).toFixed(2).padStart(5, "0")}%`
|
|
860
|
-
)}`} ${indexDefinition}`
|
|
861
|
-
);
|
|
862
|
-
iter = permutations.next(PROCEED);
|
|
863
|
-
}
|
|
864
|
-
nextStage.push({
|
|
865
|
-
name: indexName,
|
|
866
|
-
schema,
|
|
867
|
-
table,
|
|
868
|
-
columns: columns2
|
|
869
|
-
});
|
|
963
|
+
const toCreate = this.indexesToCreate(indexes);
|
|
964
|
+
const finalExplain = await this.testQueryWithStats(builder, async (tx) => {
|
|
965
|
+
if (beforeQuery) {
|
|
966
|
+
await beforeQuery(tx);
|
|
870
967
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
);
|
|
968
|
+
for (const permutation of toCreate) {
|
|
969
|
+
const createIndex = PostgresQueryBuilder.createIndex(
|
|
970
|
+
this.toDefinition(permutation).raw,
|
|
971
|
+
permutation.name
|
|
972
|
+
).introspect().build();
|
|
973
|
+
await tx.exec(createIndex);
|
|
878
974
|
}
|
|
879
975
|
});
|
|
880
976
|
const finalCost = Number(finalExplain.Plan["Total Cost"]);
|
|
@@ -893,14 +989,15 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
893
989
|
)} ${gray("If there's a better index, we haven't tried it")}`
|
|
894
990
|
);
|
|
895
991
|
}
|
|
896
|
-
const
|
|
992
|
+
const baseIndexes = this.findUsedIndexes(baseExplain.Plan);
|
|
993
|
+
const finalIndexes = this.findUsedIndexes(finalExplain.Plan);
|
|
897
994
|
return {
|
|
898
995
|
kind: "ok",
|
|
899
996
|
baseCost,
|
|
900
997
|
finalCost,
|
|
901
|
-
newIndexes,
|
|
902
|
-
existingIndexes:
|
|
903
|
-
triedIndexes,
|
|
998
|
+
newIndexes: finalIndexes.newIndexes,
|
|
999
|
+
existingIndexes: baseIndexes.existingIndexes,
|
|
1000
|
+
triedIndexes: new Map(toCreate.map((index) => [index.name, index])),
|
|
904
1001
|
baseExplainPlan: baseExplain,
|
|
905
1002
|
explainPlan: finalExplain
|
|
906
1003
|
};
|
|
@@ -924,6 +1021,37 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
924
1021
|
(index) => index.index_type === "btree" && index.table_name === table && index.index_columns.length === columns.length && index.index_columns.every((c, i) => columns[i].column === c.name)
|
|
925
1022
|
);
|
|
926
1023
|
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Derive the list of indexes [tableA(X, Y, Z), tableB(H, I, J)]
|
|
1026
|
+
**/
|
|
1027
|
+
indexesToCreate(rootCandidates) {
|
|
1028
|
+
const permutedIndexes = this.tableColumnIndexCandidates(rootCandidates);
|
|
1029
|
+
const nextStage = [];
|
|
1030
|
+
for (const { table, schema, columns } of permutedIndexes.values()) {
|
|
1031
|
+
const permutations = permuteWithFeedback(columns);
|
|
1032
|
+
let iter = permutations.next(PROCEED);
|
|
1033
|
+
while (!iter.done) {
|
|
1034
|
+
const columns2 = iter.value;
|
|
1035
|
+
const existingIndex = this.indexAlreadyExists(table, columns2);
|
|
1036
|
+
if (existingIndex) {
|
|
1037
|
+
iter = permutations.next(PROCEED);
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
const indexName = this.indexName();
|
|
1041
|
+
const shortenedSchema = schema === "public" ? "" : `"${schema}".`;
|
|
1042
|
+
const indexDefinitionClean = `${shortenedSchema}"${table}"(${columns2.map((c) => `"${c.column}"`).join(", ")})`;
|
|
1043
|
+
iter = permutations.next(PROCEED);
|
|
1044
|
+
nextStage.push({
|
|
1045
|
+
name: indexName,
|
|
1046
|
+
schema,
|
|
1047
|
+
table,
|
|
1048
|
+
columns: columns2,
|
|
1049
|
+
definition: indexDefinitionClean
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return nextStage;
|
|
1054
|
+
}
|
|
927
1055
|
toDefinition(permuted) {
|
|
928
1056
|
const make = (col, order, where, keyword) => {
|
|
929
1057
|
const baseColumn = `"${permuted.schema}"."${permuted.table}"(${permuted.columns.map((c) => {
|
|
@@ -946,16 +1074,15 @@ var _IndexOptimizer = class _IndexOptimizer {
|
|
|
946
1074
|
return { raw, colored };
|
|
947
1075
|
}
|
|
948
1076
|
/**
|
|
949
|
-
* Drop indexes that can be dropped
|
|
1077
|
+
* Drop indexes that can be dropped. Ignore the ones that can't
|
|
950
1078
|
*/
|
|
951
1079
|
async dropExistingIndexes(tx) {
|
|
952
1080
|
for (const index of this.existingIndexes) {
|
|
953
|
-
if (index
|
|
1081
|
+
if (!isIndexProbablyDroppable(index)) {
|
|
954
1082
|
continue;
|
|
955
1083
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
);
|
|
1084
|
+
const indexName = `${index.schema_name}.${index.index_name}`;
|
|
1085
|
+
await dropIndex(tx, indexName);
|
|
959
1086
|
}
|
|
960
1087
|
}
|
|
961
1088
|
whereClause(c, col, keyword) {
|
|
@@ -1144,13 +1271,7 @@ var ExportedStatsStatistics = z2.object({
|
|
|
1144
1271
|
});
|
|
1145
1272
|
var ExportedStatsColumns = z2.object({
|
|
1146
1273
|
columnName: z2.string(),
|
|
1147
|
-
stats: ExportedStatsStatistics.nullable()
|
|
1148
|
-
dataType: z2.string(),
|
|
1149
|
-
isNullable: z2.boolean(),
|
|
1150
|
-
numericScale: z2.number().nullable(),
|
|
1151
|
-
columnDefault: z2.string().nullable(),
|
|
1152
|
-
numericPrecision: z2.number().nullable(),
|
|
1153
|
-
characterMaximumLength: z2.number().nullable()
|
|
1274
|
+
stats: ExportedStatsStatistics.nullable()
|
|
1154
1275
|
});
|
|
1155
1276
|
var ExportedStatsIndex = z2.object({
|
|
1156
1277
|
indexName: z2.string(),
|
|
@@ -1618,12 +1739,6 @@ var _Statistics = class _Statistics {
|
|
|
1618
1739
|
json_agg(
|
|
1619
1740
|
json_build_object(
|
|
1620
1741
|
'columnName', c.column_name,
|
|
1621
|
-
'dataType', c.data_type,
|
|
1622
|
-
'isNullable', (c.is_nullable = 'YES')::boolean,
|
|
1623
|
-
'characterMaximumLength', c.character_maximum_length,
|
|
1624
|
-
'numericPrecision', c.numeric_precision,
|
|
1625
|
-
'numericScale', c.numeric_scale,
|
|
1626
|
-
'columnDefault', c.column_default,
|
|
1627
1742
|
'stats', (
|
|
1628
1743
|
SELECT json_build_object(
|
|
1629
1744
|
'starelid', s.starelid,
|
|
@@ -1719,6 +1834,7 @@ var _Statistics = class _Statistics {
|
|
|
1719
1834
|
COALESCE(pt.parent_table::text, t.relname) AS table_name,
|
|
1720
1835
|
i.relname AS index_name,
|
|
1721
1836
|
ix.indisprimary as is_primary,
|
|
1837
|
+
ix.indisunique as is_unique,
|
|
1722
1838
|
am.amname AS index_type,
|
|
1723
1839
|
array_agg(
|
|
1724
1840
|
CASE
|
|
@@ -1749,9 +1865,10 @@ var _Statistics = class _Statistics {
|
|
|
1749
1865
|
LEFT JOIN pg_attribute a ON a.attnum = k.attnum AND a.attrelid = t.oid
|
|
1750
1866
|
JOIN pg_namespace n ON t.relnamespace = n.oid
|
|
1751
1867
|
WHERE
|
|
1752
|
-
n.nspname
|
|
1868
|
+
n.nspname not like 'pg_%' and
|
|
1869
|
+
n.nspname <> 'information_schema'
|
|
1753
1870
|
GROUP BY
|
|
1754
|
-
n.nspname, COALESCE(pt.parent_table::text, t.relname), i.relname, am.amname, ix.indisprimary
|
|
1871
|
+
n.nspname, COALESCE(pt.parent_table::text, t.relname), i.relname, am.amname, ix.indisprimary, ix.indisunique
|
|
1755
1872
|
ORDER BY
|
|
1756
1873
|
COALESCE(pt.parent_table::text, t.relname), i.relname; -- @qd_introspection
|
|
1757
1874
|
`);
|
|
@@ -1774,13 +1891,17 @@ export {
|
|
|
1774
1891
|
ExportedStatsV1,
|
|
1775
1892
|
IndexOptimizer,
|
|
1776
1893
|
PROCEED,
|
|
1894
|
+
PgIdentifier,
|
|
1777
1895
|
PostgresQueryBuilder,
|
|
1778
1896
|
PostgresVersion,
|
|
1779
1897
|
SKIP,
|
|
1780
1898
|
Statistics,
|
|
1781
1899
|
StatisticsMode,
|
|
1782
1900
|
StatisticsSource,
|
|
1901
|
+
dropIndex,
|
|
1783
1902
|
ignoredIdentifier,
|
|
1903
|
+
isIndexProbablyDroppable,
|
|
1904
|
+
isIndexSupported,
|
|
1784
1905
|
parseNudges,
|
|
1785
1906
|
permuteWithFeedback
|
|
1786
1907
|
};
|