Die Akte holte Vorgesetzte und direkte Berichte in zwei Teilabfragen, je
eine mit eigenem Filter -- in der Annahme, der Filter schraenke die
Funktion ein. Tut er nicht. om_reporting_lines traegt ein `with
recursive` im Koerper und laesst sich deshalb nicht einbetten; sie rechnet
jedes Mal alle Zeilen aus, und der Filter wirft sie danach weg.
Auf dem Server gemessen:
explain analyze select * from om_reporting_lines(current_date)
where employee_id = ...;
-> Rows Removed by Filter: 783, Execution Time: 69 ms
Und ohne Filter dieselben 60 ms. Zwei Filter hiessen also zweimal
dieselbe Rechnung, rund 120 ms je Aufruf der Akte -- der groesste
Einzelposten der Seite.
Jetzt ein Aufruf, und die Auswahl trifft der Aufrufer. Dafuer wandern
einmal alle Zeilen herueber statt neun; ueber eine Verbindung im selben
Netz kostet das den Bruchteil dessen, was die zweite Rechnung kostete.
Der Kommentar, der das Gegenteil behauptete, ist durch die Messung
ersetzt.
167 lines
7.0 KiB
TypeScript
167 lines
7.0 KiB
TypeScript
import type { Tx } from "./db";
|
|
import { kontierungZum, kontierungenAbfrage, type KontierungsZeile } from "./cost-centers";
|
|
import { jsonArrayFrom, zeitstempel } from "./db/json";
|
|
import { todayIso } from "./format";
|
|
import { buildOrgMaps, orgMapsAbfragen } from "./org";
|
|
import { berichtslinienAbfrage, besetzungenAbfrage, orgAsOf, pickPlacements } from "./placement";
|
|
import { offeneStellenAbfrage, resolveOpenPositions, type OffeneStelle } from "./positions";
|
|
|
|
// Was die Mitarbeiterakte liest — in vier Rundreisen statt in zwölf.
|
|
//
|
|
// Vorher: die Person, dann acht Dinge in einem Promise.all, dann die Namen der
|
|
// Beteiligten. Das Promise.all war keine Gleichzeitigkeit — eine Transaktion
|
|
// hängt an einer Verbindung (lib/db/json.ts). Bei rund 36 ms Umlaufzeit war
|
|
// diese Seite damit die teuerste der Anwendung.
|
|
//
|
|
// Die drei Tabellen mit `selectAll()` stehen hier ausgeschrieben. Das ist
|
|
// keine Umständlichkeit: innerhalb von JSON formatiert Postgres timestamptz
|
|
// anders als der Treiber sonst, und `created_at` entscheidet in lib/history.ts
|
|
// darüber, was später passiert ist. Ausgeschrieben ist sichtbar, welche Spalte
|
|
// durch zeitstempel() geht — bei selectAll() wäre es unsichtbar falsch.
|
|
|
|
export async function loadEmployeeDetail(tx: Tx, id: string, today: string = todayIso()) {
|
|
// Die Person zuerst, allein: erst aus Eintritt und Austritt ergibt sich der
|
|
// Stichtag, zu dem ihre Organisation überhaupt eine Antwort hat. Eine
|
|
// Person, die am 01.09. anfängt, hat heute keine laufende Besetzung — zu
|
|
// heute gefragt lieferte om_reporting_lines() nichts, und die Akte
|
|
// behauptete „Keine Führungskraft", obwohl das Team eine hat.
|
|
const employee = await tx.selectFrom("employees").selectAll().where("id", "=", id).executeTakeFirst();
|
|
if (!employee) return null;
|
|
|
|
const asOf = orgAsOf(employee, today);
|
|
|
|
const g = await tx
|
|
.selectNoFrom((eb) => [
|
|
// Vorgesetzte und direkte Berichte stehen nirgends als Spalte — sie
|
|
// kommen aus om_reporting_lines().
|
|
//
|
|
// **Einmal** gerufen, nicht zweimal. Hier standen zwei Teilabfragen mit
|
|
// je einem Filter, in der Annahme, der Filter schränke die Funktion
|
|
// ein. Das tut er nicht: wegen des `with recursive` in ihrem Körper
|
|
// lässt sie sich nicht einbetten, sie rechnet jedes Mal alle Zeilen aus
|
|
// und der Filter wirft sie danach weg. Auf dem Server gemessen:
|
|
//
|
|
// explain analyze select * from om_reporting_lines(current_date)
|
|
// where employee_id = …;
|
|
// → Rows Removed by Filter: 783, Execution Time: 69 ms
|
|
//
|
|
// Zwei Filter hiessen also zweimal dieselbe Rechnung. Jetzt wandern
|
|
// einmal alle Zeilen herüber — ein paar hundert Kilobyte über eine
|
|
// Verbindung im selben Netz — und die Auswahl trifft der Aufrufer.
|
|
jsonArrayFrom(berichtslinienAbfrage(eb, asOf)).as("lines"),
|
|
|
|
jsonArrayFrom(
|
|
eb
|
|
.selectFrom("employee_history")
|
|
.select(["id", "employee_id", "event_date", "event_type", "description", "changes", "pending_id"])
|
|
.select((x) => zeitstempel(x.ref("created_at")).as("created_at"))
|
|
.where("employee_id", "=", id)
|
|
.orderBy("event_date", "desc")
|
|
.orderBy("created_at", "desc")
|
|
).as("history"),
|
|
|
|
jsonArrayFrom(
|
|
eb
|
|
.selectFrom("employee_dependents")
|
|
.select(["id", "employee_id", "first_name", "last_name", "relationship", "sv_nummer", "birth_date"])
|
|
.select((x) => zeitstempel(x.ref("created_at")).as("created_at"))
|
|
.where("employee_id", "=", id)
|
|
.orderBy("created_at")
|
|
).as("dependents"),
|
|
|
|
jsonArrayFrom(
|
|
eb
|
|
.selectFrom("employee_notes")
|
|
.select([
|
|
"id",
|
|
"employee_id",
|
|
"author_user_id",
|
|
"author_name",
|
|
"category",
|
|
"note_text",
|
|
"due_date",
|
|
"done",
|
|
"done_by",
|
|
])
|
|
.select((x) => [
|
|
zeitstempel(x.ref("created_at")).as("created_at"),
|
|
zeitstempel(x.ref("done_at")).as("done_at"),
|
|
])
|
|
.where("employee_id", "=", id)
|
|
.orderBy("created_at", "desc")
|
|
).as("notes"),
|
|
|
|
...orgMapsAbfragen(eb),
|
|
jsonArrayFrom(besetzungenAbfrage(eb, [id])).as("placementRows"),
|
|
// Die Kontierungen der Planstellen dieser Person. Welche gilt, hängt am
|
|
// Stichtag — und der ergibt sich erst aus Eintritt und Austritt, steht
|
|
// hier also schon fest.
|
|
jsonArrayFrom(
|
|
kontierungenAbfrage(eb).where("z.position_id", "in", (e) =>
|
|
e.selectFrom("position_assignments as pa").select("pa.position_id").where("pa.employee_id", "=", id)
|
|
)
|
|
).as("costRows"),
|
|
jsonArrayFrom(
|
|
eb
|
|
.selectFrom("onboarding_tasks")
|
|
.select(["item_key", "erledigt", "wert", "kommentar", "updated_by_name"])
|
|
.select((x) => zeitstempel(x.ref("updated_at")).as("updated_at"))
|
|
.where("employee_id", "=", id)
|
|
).as("onboarding"),
|
|
jsonArrayFrom(
|
|
eb
|
|
.selectFrom("offboarding_tasks")
|
|
.select(["item_key", "erledigt", "wert", "kommentar", "updated_by_name"])
|
|
.select((x) => zeitstempel(x.ref("updated_at")).as("updated_at"))
|
|
.where("employee_id", "=", id)
|
|
).as("offboarding"),
|
|
// Die offenen Planstellen bleiben bei heute: sie gehören zur
|
|
// Organisation, nicht zu dieser Person.
|
|
jsonArrayFrom(offeneStellenAbfrage(eb, today)).as("open"),
|
|
])
|
|
.executeTakeFirstOrThrow();
|
|
|
|
const orgMaps = buildOrgMaps(g.units as never, g.locations as never);
|
|
const line = g.lines.find((l) => l.employee_id === id) ?? null;
|
|
const reports = g.lines.filter((l) => l.acting_manager_id === id);
|
|
const placement = pickPlacements(g.placementRows as never, asOf).get(id) ?? null;
|
|
|
|
// Namen für die beteiligten Personen in einem Zug: die Vertretung, die
|
|
// formal zuständige Leitung und die direkten Berichte.
|
|
const relatedIds = Array.from(
|
|
new Set(
|
|
[line?.acting_manager_id, line?.formal_manager_id, ...reports.map((r) => r.employee_id)].filter(
|
|
(x): x is string => Boolean(x)
|
|
)
|
|
)
|
|
);
|
|
|
|
const [relatedRows, openPositions] = await Promise.all([
|
|
relatedIds.length
|
|
? tx
|
|
.selectFrom("employees")
|
|
.select(["id", "first_name", "last_name", "job_title", "status"])
|
|
.where("id", "in", relatedIds)
|
|
.execute()
|
|
: Promise.resolve([]),
|
|
resolveOpenPositions(tx, orgMaps, g.open as OffeneStelle[], today),
|
|
]);
|
|
|
|
return {
|
|
employee,
|
|
line,
|
|
reports,
|
|
history: g.history,
|
|
dependents: g.dependents,
|
|
notes: g.notes,
|
|
orgMaps,
|
|
placement,
|
|
// Die Kostenstelle des Sitzes, auf dem die Person am Stichtag sitzt.
|
|
kostenstelle: placement ? (kontierungZum(g.costRows as KontierungsZeile[], asOf).get(placement.positionId) ?? null) : null,
|
|
onboarding: g.onboarding,
|
|
offboarding: g.offboarding,
|
|
openPositions,
|
|
byId: new Map(relatedRows.map((e) => [e.id, e])),
|
|
};
|
|
}
|