-- ============================================================================ -- KREMPL PostgreSQL Schema -- Reduziert 61 Millionen MySQL-Zeilen auf 68k Zeilen mit Arrays -- ============================================================================ -- Datenbank erstellen (als postgres User ausführen) -- CREATE DATABASE krempl_data -- WITH ENCODING 'UTF8' -- LC_COLLATE='de_DE.UTF-8' -- LC_CTYPE='de_DE.UTF-8'; -- User erstellen -- CREATE USER krempl_user WITH PASSWORD 'PASSWORT_HIER_SETZEN'; -- GRANT ALL PRIVILEGES ON DATABASE krempl_data TO krempl_user; -- Nach \c krempl_data: -- GRANT ALL ON SCHEMA public TO krempl_user; -- ============================================================================ -- Extensions aktivieren -- ============================================================================ -- Trigram-Suche für ähnliche Strings CREATE EXTENSION IF NOT EXISTS pg_trgm; -- Schnellere Array-Operationen CREATE EXTENSION IF NOT EXISTS intarray; -- Query-Performance Tracking (optional) -- CREATE EXTENSION IF NOT EXISTS pg_stat_statements; -- ============================================================================ -- Tabelle 1: GERÄTE (ca. 1.4 Mio Zeilen) -- ============================================================================ DROP TABLE IF EXISTS geraete CASCADE; CREATE TABLE geraete ( id BIGINT PRIMARY KEY, nr VARCHAR(100), marke VARCHAR(100), typ VARCHAR(200), zusatz_nummer VARCHAR(100), modell_bezeichnung VARCHAR(200), produktionsstart VARCHAR(50), produktionsende VARCHAR(50), wgtext1 VARCHAR(200), wgtext2 VARCHAR(200), zusatz TEXT, bezeichnung_original TEXT, typ_de VARCHAR(200), typ_fr VARCHAR(200), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Standard-Indexe für schnelle Lookups CREATE INDEX idx_geraete_nr ON geraete(nr); CREATE INDEX idx_geraete_marke ON geraete(marke); CREATE INDEX idx_geraete_typ ON geraete(typ); -- Trigram-Indexe für LIKE-Suchen (z.B. "AEG%" oder "%123%") CREATE INDEX idx_geraete_nr_trgm ON geraete USING GIN(nr gin_trgm_ops); CREATE INDEX idx_geraete_typ_trgm ON geraete USING GIN(typ gin_trgm_ops); CREATE INDEX idx_geraete_marke_trgm ON geraete USING GIN(marke gin_trgm_ops); -- Full-Text Search Index (Kombination: nr + typ + marke) CREATE INDEX idx_geraete_fts ON geraete USING GIN(to_tsvector('german', COALESCE(nr,'') || ' ' || COALESCE(typ,'') || ' ' || COALESCE(marke,'') || ' ' || COALESCE(modell_bezeichnung,'') )); COMMENT ON TABLE geraete IS 'Geräte-Stammdaten aus Krempl (ca. 1.4 Mio Einträge)'; -- ============================================================================ -- Tabelle 2: ERSATZTEIL-MAPPING (68k Zeilen statt 61 Mio!) -- ============================================================================ DROP TABLE IF EXISTS ersatzteil_mapping CASCADE; CREATE TABLE ersatzteil_mapping ( ersatzteil_id BIGINT PRIMARY KEY, geraet_ids BIGINT[], -- Array statt Millionen einzelner Zeilen! geraet_count INT, last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- GIN Index für Array-Suche "Welche Ersatzteile passen zu Gerät X?" -- Ermöglicht: WHERE geraet_ids @> ARRAY[123456] CREATE INDEX idx_mapping_geraet_array ON ersatzteil_mapping USING GIN(geraet_ids); -- Standard B-Tree Index für schnelle Ersatzteil-Lookups CREATE INDEX idx_mapping_ersatzteil ON ersatzteil_mapping(ersatzteil_id); COMMENT ON TABLE ersatzteil_mapping IS 'N:N Mapping Ersatzteil↔Gerät als Arrays (reduziert 61M Zeilen auf 68k)'; COMMENT ON COLUMN ersatzteil_mapping.geraet_ids IS 'Array aller Geräte-IDs die zu diesem Ersatzteil passen'; COMMENT ON COLUMN ersatzteil_mapping.geraet_count IS 'Anzahl Geräte (für schnelle Anzeige ohne Array-Count)'; -- ============================================================================ -- Tabelle 3: ERSATZTEILE (Metadaten, optional) -- ============================================================================ DROP TABLE IF EXISTS ersatzteile CASCADE; CREATE TABLE ersatzteile ( id BIGINT PRIMARY KEY, navision_id BIGINT, originalnummer VARCHAR(100), marke VARCHAR(100), ean VARCHAR(50), altenr VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Indexe für Join mit Shop-Items CREATE INDEX idx_ersatzteile_navision ON ersatzteile(navision_id); CREATE INDEX idx_ersatzteile_original ON ersatzteile(originalnummer); CREATE INDEX idx_ersatzteile_ean ON ersatzteile(ean); COMMENT ON TABLE ersatzteile IS 'Ersatzteil-Metadaten aus Krempl'; -- ============================================================================ -- Tabelle 4: PASSENDWIE (optional) -- ============================================================================ DROP TABLE IF EXISTS passendwie CASCADE; CREATE TABLE passendwie ( row_id SERIAL PRIMARY KEY, -- Auto-increment ID für interne Zwecke id BIGINT NOT NULL, -- Krempl Ersatzteil-ID (NICHT unique!) navision_id BIGINT, vertreiber_id VARCHAR(100), vertreiber VARCHAR(200), bestellcode VARCHAR(200), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Eindeutigkeit: Ein Ersatzteil kann mehrere Vertreiber-Codes haben UNIQUE(id, vertreiber_id, bestellcode) ); CREATE INDEX idx_passendwie_id ON passendwie(id); CREATE INDEX idx_passendwie_navision ON passendwie(navision_id); CREATE INDEX idx_passendwie_vertreiber ON passendwie(vertreiber_id); CREATE INDEX idx_passendwie_bestellcode ON passendwie(bestellcode); COMMENT ON TABLE passendwie IS 'PassendWie-Zuordnungen: Ein Ersatzteil hat mehrere Vertreiber-Bestellcodes (206k Zeilen)'; -- ============================================================================ -- Hilfsfunktionen -- ============================================================================ -- Funktion: Zähle Geräte in Array (Alternative zu geraet_count-Spalte) CREATE OR REPLACE FUNCTION count_geraete(ersatzteil_id_param BIGINT) RETURNS INT AS $$ SELECT COALESCE(array_length(geraet_ids, 1), 0) FROM ersatzteil_mapping WHERE ersatzteil_id = ersatzteil_id_param; $$ LANGUAGE SQL STABLE; -- Funktion: Auto-Update updated_at Timestamp CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Trigger für geraete.updated_at CREATE TRIGGER update_geraete_updated_at BEFORE UPDATE ON geraete FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- ============================================================================ -- Views für vereinfachte Queries (optional) -- ============================================================================ -- View: Ersatzteile mit Geräte-Anzahl CREATE OR REPLACE VIEW v_ersatzteile_stats AS SELECT em.ersatzteil_id, em.geraet_count, e.navision_id, e.originalnummer, e.marke FROM ersatzteil_mapping em LEFT JOIN ersatzteile e ON e.id = em.ersatzteil_id ORDER BY em.geraet_count DESC; COMMENT ON VIEW v_ersatzteile_stats IS 'Ersatzteile mit Statistiken (Anzahl passender Geräte)'; -- ============================================================================ -- Performance-Optimierung -- ============================================================================ -- Statistiken aktualisieren (nach Import ausführen!) -- ANALYZE geraete; -- ANALYZE ersatzteil_mapping; -- ANALYZE ersatzteile; -- ANALYZE passendwie; -- ============================================================================ -- Grant Permissions -- ============================================================================ -- GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO krempl_user; -- GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO krempl_user; -- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO krempl_user; -- ============================================================================ -- Fertig! -- ============================================================================ -- Beispiel-Query: Geräte zu Ersatzteil /* SELECT g.* FROM ersatzteil_mapping em JOIN geraete g ON g.id = ANY(em.geraet_ids) WHERE em.ersatzteil_id = 7764466071 LIMIT 100; */ -- Beispiel-Query: Ersatzteile zu Gerät /* SELECT ersatzteil_id FROM ersatzteil_mapping WHERE geraet_ids @> ARRAY[123456]::bigint[] LIMIT 100; */ -- Beispiel-Query: Full-Text Suche /* SELECT id, nr, marke, typ FROM geraete WHERE to_tsvector('german', nr || ' ' || typ) @@ plainto_tsquery('german', 'AEG Kühlschrank') LIMIT 50; */