diff --git a/listing.sql b/listing.sql new file mode 100644 index 0000000..f02e2c5 --- /dev/null +++ b/listing.sql @@ -0,0 +1,728 @@ +-- Создание схемы для модуля вычисляемых полей +CREATE SCHEMA IF NOT EXISTS computed_fields_system; + +-- ============================================================================ +-- РАЗДЕЛ 1: СЛУЖЕБНЫЕ ТАБЛИЦЫ +-- ============================================================================ + +-- Таблица конфигурации вычисляемых полей (реестр правил) +CREATE TABLE IF NOT EXISTS computed_fields_system.computed_fields_config ( + id SERIAL PRIMARY KEY, + table_schema VARCHAR(63) NOT NULL DEFAULT 'public', + table_name VARCHAR(63) NOT NULL, + column_name VARCHAR(63) NOT NULL, + column_type VARCHAR(50) NOT NULL, + expression TEXT NOT NULL, + implementation_type VARCHAR(10) NOT NULL DEFAULT 'AUTO' + CHECK (implementation_type IN ('AUTO', 'NATIVE', 'TRIGGER')), + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + description TEXT, + UNIQUE (table_schema, table_name, column_name) +); + +COMMENT ON TABLE computed_fields_system.computed_fields_config IS + 'Реестр конфигурации вычисляемых полей для автоматической генерации'; + +-- Таблица логов применения правил +CREATE TABLE IF NOT EXISTS computed_fields_system.application_log ( + id SERIAL PRIMARY KEY, + config_id INTEGER REFERENCES computed_fields_system.computed_fields_config(id), + action VARCHAR(20) NOT NULL, + implementation_method VARCHAR(10) NOT NULL, + status VARCHAR(10) NOT NULL CHECK (status IN ('SUCCESS', 'ERROR')), + error_message TEXT, + executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Таблица результатов валидации +CREATE TABLE IF NOT EXISTS computed_fields_system.validation_results ( + id SERIAL PRIMARY KEY, + table_schema VARCHAR(63) NOT NULL, + table_name VARCHAR(63) NOT NULL, + column_name VARCHAR(63) NOT NULL, + validation_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + total_rows BIGINT NOT NULL, + invalid_rows BIGINT NOT NULL, + invalid_row_ids TEXT +); + +-- ============================================================================ +-- РАЗДЕЛ 2: ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ +-- ============================================================================ + +-- Функция для определения версии PostgreSQL +CREATE OR REPLACE FUNCTION computed_fields_system.get_pg_version_num() +RETURNS INTEGER +LANGUAGE plpgsql STABLE +AS $$ +DECLARE + v_version INTEGER; +BEGIN + SELECT current_setting('server_version_num')::INTEGER INTO v_version; + RETURN v_version; +END; +$$; + +-- Функция проверки поддержки GENERATED COLUMNS +CREATE OR REPLACE FUNCTION computed_fields_system.supports_generated_columns() +RETURNS BOOLEAN +LANGUAGE plpgsql STABLE +AS $$ +BEGIN + -- PostgreSQL 12+ (версия >= 120000) поддерживает GENERATED COLUMNS + RETURN computed_fields_system.get_pg_version_num() >= 120000; +END; +$$; + +-- Функция проверки существования столбца +CREATE OR REPLACE FUNCTION computed_fields_system.column_exists( + p_schema VARCHAR, + p_table VARCHAR, + p_column VARCHAR +) +RETURNS BOOLEAN +LANGUAGE plpgsql STABLE +AS $$ +DECLARE + v_exists BOOLEAN; +BEGIN + SELECT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = p_schema + AND table_name = p_table + AND column_name = p_column + ) INTO v_exists; + + RETURN v_exists; +END; +$$; + +-- ============================================================================ +-- РАЗДЕЛ 3: ОСНОВНЫЕ ПРОЦЕДУРЫ ГЕНЕРАЦИИ +-- ============================================================================ + +-- Процедура создания вычисляемого поля через GENERATED COLUMN (нативный метод) +CREATE OR REPLACE FUNCTION computed_fields_system.create_generated_column( + p_schema VARCHAR, + p_table VARCHAR, + p_column VARCHAR, + p_type VARCHAR, + p_expression TEXT +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + v_sql TEXT; +BEGIN + -- Формирование DDL-команды для GENERATED ALWAYS AS + v_sql := format( + 'ALTER TABLE %I.%I ADD COLUMN %I %s GENERATED ALWAYS AS (%s) STORED', + p_schema, p_table, p_column, p_type, p_expression + ); + + RAISE NOTICE 'Executing: %', v_sql; + EXECUTE v_sql; + + RAISE NOTICE 'Generated column %.%.% created successfully', + p_schema, p_table, p_column; +END; +$$; + +-- Процедура создания вычисляемого поля через TRIGGER (триггерный метод) +CREATE OR REPLACE FUNCTION computed_fields_system.create_trigger_computed_field( + p_schema VARCHAR, + p_table VARCHAR, + p_column VARCHAR, + p_type VARCHAR, + p_expression TEXT +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + v_function_name VARCHAR; + v_trigger_name VARCHAR; + v_sql TEXT; +BEGIN + -- Генерация уникальных имен для функции и триггера + v_function_name := format('trg_fn_%s_%s_compute', p_table, p_column); + v_trigger_name := format('trg_%s_%s_compute', p_table, p_column); + + -- 1. Добавляем столбец (если не существует) + IF NOT computed_fields_system.column_exists(p_schema, p_table, p_column) THEN + v_sql := format( + 'ALTER TABLE %I.%I ADD COLUMN %I %s', + p_schema, p_table, p_column, p_type + ); + EXECUTE v_sql; + END IF; + + -- 2. Создаем триггерную функцию + v_sql := format($FUNC$ + CREATE OR REPLACE FUNCTION %I.%I() + RETURNS TRIGGER + LANGUAGE plpgsql + AS $TRG$ + BEGIN + -- Вычисление значения с обработкой NULL + BEGIN + NEW.%I := (%s)::%s; + EXCEPTION WHEN OTHERS THEN + -- Логирование ошибки, но не прерываем транзакцию + RAISE WARNING 'Error computing field %%.%%: %%', + TG_TABLE_SCHEMA, TG_TABLE_NAME, SQLERRM; + NEW.%I := NULL; + END; + RETURN NEW; + END; + $TRG$; + $FUNC$, + p_schema, v_function_name, + p_column, p_expression, p_type, + p_column + ); + + EXECUTE v_sql; + + -- 3. Создаем триггер + v_sql := format($TRIG$ + DROP TRIGGER IF EXISTS %I ON %I.%I; + CREATE TRIGGER %I + BEFORE INSERT OR UPDATE ON %I.%I + FOR EACH ROW + EXECUTE FUNCTION %I.%I(); + $TRIG$, + v_trigger_name, p_schema, p_table, + v_trigger_name, + p_schema, p_table, + p_schema, v_function_name + ); + + EXECUTE v_sql; + + RAISE NOTICE 'Trigger-based computed field %.%.% created successfully', + p_schema, p_table, p_column; +END; +$$; + +-- ============================================================================ +-- РАЗДЕЛ 4: ОРКЕСТРАТОР - ПРИМЕНЕНИЕ ПРАВИЛ +-- ============================================================================ + +CREATE OR REPLACE FUNCTION computed_fields_system.apply_computed_fields_rules() +RETURNS TABLE ( + config_id INTEGER, + table_full_name TEXT, + column_name TEXT, + method_used VARCHAR, + status TEXT +) +LANGUAGE plpgsql +AS $$ +DECLARE + v_config RECORD; + v_implementation_method VARCHAR(10); + v_supports_native BOOLEAN; + v_error_msg TEXT; +BEGIN + -- Проверка поддержки нативных GENERATED COLUMNS + v_supports_native := computed_fields_system.supports_generated_columns(); + + RAISE NOTICE 'PostgreSQL version: %, Native GENERATED COLUMNS support: %', + computed_fields_system.get_pg_version_num(), v_supports_native; + + -- Обработка каждого правила из конфигурации + FOR v_config IN + SELECT c.id, c.table_schema, c.table_name, c.column_name, + c.column_type, c.expression, c.implementation_type + FROM computed_fields_system.computed_fields_config c + WHERE c.is_active = TRUE + ORDER BY c.id + LOOP + BEGIN + -- Определение метода реализации + IF v_config.implementation_type = 'AUTO' THEN + v_implementation_method := CASE + WHEN v_supports_native THEN 'NATIVE' + ELSE 'TRIGGER' + END; + ELSE + v_implementation_method := v_config.implementation_type; + END IF; + + -- Проверка возможности использования выбранного метода + IF v_implementation_method = 'NATIVE' AND NOT v_supports_native THEN + RAISE EXCEPTION 'NATIVE method requested but PostgreSQL version < 12'; + END IF; + + -- Проверка существования столбца + IF computed_fields_system.column_exists( + v_config.table_schema, + v_config.table_name, + v_config.column_name + ) THEN + RAISE NOTICE 'Column %.%.% already exists, skipping', + v_config.table_schema, v_config.table_name, v_config.column_name; + CONTINUE; + END IF; + + -- Применение выбранного метода + IF v_implementation_method = 'NATIVE' THEN + PERFORM computed_fields_system.create_generated_column( + v_config.table_schema, + v_config.table_name, + v_config.column_name, + v_config.column_type, + v_config.expression + ); + ELSE + PERFORM computed_fields_system.create_trigger_computed_field( + v_config.table_schema, + v_config.table_name, + v_config.column_name, + v_config.column_type, + v_config.expression + ); + END IF; + + -- Логирование успешного применения + INSERT INTO computed_fields_system.application_log + (config_id, action, implementation_method, status) + VALUES + (v_config.id, 'CREATE', v_implementation_method, 'SUCCESS'); + + -- Возврат результата + RETURN QUERY SELECT + v_config.id, + v_config.table_schema || '.' || v_config.table_name, + v_config.column_name, + v_implementation_method, + 'SUCCESS'::TEXT; + + EXCEPTION WHEN OTHERS THEN + v_error_msg := SQLERRM; + + -- Логирование ошибки + INSERT INTO computed_fields_system.application_log + (config_id, action, implementation_method, status, error_message) + VALUES + (v_config.id, 'CREATE', v_implementation_method, 'ERROR', v_error_msg); + + RAISE WARNING 'Failed to create computed field %.%.%: %', + v_config.table_schema, v_config.table_name, + v_config.column_name, v_error_msg; + + RETURN QUERY SELECT + v_config.id, + v_config.table_schema || '.' || v_config.table_name, + v_config.column_name, + v_implementation_method, + 'ERROR: ' || v_error_msg; + END; + END LOOP; +END; +$$; + +-- ============================================================================ +-- РАЗДЕЛ 5: ВАЛИДАЦИЯ ЦЕЛОСТНОСТИ +-- ============================================================================ + +CREATE OR REPLACE FUNCTION computed_fields_system.validate_computed_fields( + p_table_schema VARCHAR DEFAULT 'public', + p_table_name VARCHAR DEFAULT NULL +) +RETURNS TABLE ( + table_name TEXT, + column_name TEXT, + total_rows BIGINT, + invalid_rows BIGINT, + is_valid BOOLEAN +) +LANGUAGE plpgsql +AS $$ +DECLARE + v_config RECORD; + v_sql TEXT; + v_total_rows BIGINT; + v_invalid_rows BIGINT; + v_invalid_ids TEXT; +BEGIN + -- Обработка каждого активного правила + FOR v_config IN + SELECT c.table_schema, c.table_name, c.column_name, + c.expression, c.column_type + FROM computed_fields_system.computed_fields_config c + WHERE c.is_active = TRUE + AND c.table_schema = p_table_schema + AND (p_table_name IS NULL OR c.table_name = p_table_name) + ORDER BY c.table_name, c.column_name + LOOP + -- Проверка существования столбца + IF NOT computed_fields_system.column_exists( + v_config.table_schema, + v_config.table_name, + v_config.column_name + ) THEN + RAISE WARNING 'Column %.%.% does not exist, skipping validation', + v_config.table_schema, v_config.table_name, v_config.column_name; + CONTINUE; + END IF; + + -- Подсчет общего количества строк + v_sql := format( + 'SELECT COUNT(*) FROM %I.%I', + v_config.table_schema, v_config.table_name + ); + EXECUTE v_sql INTO v_total_rows; + + -- Подсчет строк с расхождениями + v_sql := format($VAL$ + SELECT COUNT(*), + STRING_AGG(id::TEXT, ', ') + FROM ( + SELECT + CASE + WHEN pg_typeof(id) = 'integer'::regtype THEN id::INTEGER + WHEN pg_typeof(id) = 'bigint'::regtype THEN id::BIGINT + ELSE id::INTEGER + END as id + FROM %I.%I + WHERE %I IS DISTINCT FROM ((%s)::%s) + LIMIT 100 + ) subq + $VAL$, + v_config.table_schema, v_config.table_name, + v_config.column_name, v_config.expression, v_config.column_type + ); + + BEGIN + EXECUTE v_sql INTO v_invalid_rows, v_invalid_ids; + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'Validation error for %.%.%: %', + v_config.table_schema, v_config.table_name, + v_config.column_name, SQLERRM; + v_invalid_rows := -1; + v_invalid_ids := 'ERROR: ' || SQLERRM; + END; + + -- Сохранение результатов валидации + INSERT INTO computed_fields_system.validation_results + (table_schema, table_name, column_name, total_rows, + invalid_rows, invalid_row_ids) + VALUES + (v_config.table_schema, v_config.table_name, v_config.column_name, + v_total_rows, v_invalid_rows, v_invalid_ids); + + -- Возврат результата + RETURN QUERY SELECT + v_config.table_schema || '.' || v_config.table_name, + v_config.column_name, + v_total_rows, + v_invalid_rows, + (v_invalid_rows = 0); + END LOOP; +END; +$$; + +-- Процедура принудительного пересчета значений +CREATE OR REPLACE FUNCTION computed_fields_system.force_recompute( + p_table_schema VARCHAR, + p_table_name VARCHAR, + p_column_name VARCHAR +) +RETURNS BIGINT +LANGUAGE plpgsql +AS $$ +DECLARE + v_config RECORD; + v_sql TEXT; + v_updated_rows BIGINT; +BEGIN + -- Получение конфигурации поля + SELECT c.expression, c.column_type + INTO v_config + FROM computed_fields_system.computed_fields_config c + WHERE c.table_schema = p_table_schema + AND c.table_name = p_table_name + AND c.column_name = p_column_name + AND c.is_active = TRUE; + + IF NOT FOUND THEN + RAISE EXCEPTION 'Configuration not found for %.%.%', + p_table_schema, p_table_name, p_column_name; + END IF; + + -- Массовое обновление + v_sql := format($UPD$ + UPDATE %I.%I + SET %I = (%s)::%s + $UPD$, + p_table_schema, p_table_name, + p_column_name, v_config.expression, v_config.column_type + ); + + EXECUTE v_sql; + GET DIAGNOSTICS v_updated_rows = ROW_COUNT; + + RAISE NOTICE 'Force recompute: % rows updated in %.%.%', + v_updated_rows, p_table_schema, p_table_name, p_column_name; + + RETURN v_updated_rows; +END; +$$; + +-- ============================================================================ +-- РАЗДЕЛ 6: ДЕМОНСТРАЦИОННЫЕ ПРИМЕРЫ +-- ============================================================================ + +-- Пример 1: Таблица заказов (order_items) +CREATE TABLE IF NOT EXISTS order_items ( + id SERIAL PRIMARY KEY, + order_id INTEGER NOT NULL, + product_name VARCHAR(255) NOT NULL, + quantity INTEGER NOT NULL CHECK (quantity > 0), + unit_price NUMERIC(10,2) NOT NULL CHECK (unit_price >= 0), + discount_percent NUMERIC(5,2) DEFAULT 0 CHECK (discount_percent BETWEEN 0 AND 100), + tax_rate NUMERIC(5,2) DEFAULT 0 CHECK (tax_rate >= 0), + order_date DATE DEFAULT CURRENT_DATE +); + +-- Пример 2: Таблица комнат (rooms) +CREATE TABLE IF NOT EXISTS rooms ( + id SERIAL PRIMARY KEY, + room_number VARCHAR(10) NOT NULL, + length NUMERIC(10,2) NOT NULL CHECK (length > 0), + width NUMERIC(10,2) NOT NULL CHECK (width > 0) +); + +-- Пример 3: Таблица пользователей (users) +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + middle_name VARCHAR(100), + email VARCHAR(255) UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Пример 4: Таблица логов с JSON (users_log) +CREATE TABLE IF NOT EXISTS users_log ( + id SERIAL PRIMARY KEY, + payload JSONB NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ============================================================================ +-- РАЗДЕЛ 7: КОНФИГУРАЦИЯ ВЫЧИСЛЯЕМЫХ ПОЛЕЙ +-- ============================================================================ + +-- Добавление правил для вычисляемых полей +INSERT INTO computed_fields_system.computed_fields_config + (table_schema, table_name, column_name, column_type, expression, + implementation_type, description) +VALUES + -- Пример 1: Итоговая стоимость позиции заказа + ('public', 'order_items', 'final_price', 'NUMERIC(10,2)', + '(quantity * unit_price) * (1 - discount_percent / 100.0) * (1 + tax_rate / 100.0)', + 'AUTO', + 'Финальная стоимость с учетом скидки и налога'), + + -- Пример 2: Площадь комнаты + ('public', 'rooms', 'total_area', 'NUMERIC(10,2)', + 'length * width', + 'AUTO', + 'Площадь комнаты в квадратных метрах'), + + -- Пример 3: Полное имя пользователя + ('public', 'users', 'full_name', 'TEXT', + 'CONCAT_WS('' '', last_name, first_name, middle_name)', + 'AUTO', + 'ФИО пользователя для поиска и отображения'), + + -- Пример 4: Поисковая строка (нормализованная) + ('public', 'users', 'search_string', 'TEXT', + 'LOWER(CONCAT_WS('' '', last_name, first_name, middle_name, email))', + 'AUTO', + 'Нормализованная строка для полнотекстового поиска'), + + -- Пример 5: Email из JSON + ('public', 'users_log', 'email_extracted', 'TEXT', + 'payload ->> ''email''', + 'AUTO', + 'Email извлеченный из JSON для быстрого доступа') +ON CONFLICT (table_schema, table_name, column_name) DO NOTHING; + +-- ============================================================================ +-- РАЗДЕЛ 8: ТЕСТОВЫЕ ДАННЫЕ +-- ============================================================================ + +-- Вставка тестовых данных в order_items +INSERT INTO order_items (order_id, product_name, quantity, unit_price, discount_percent, tax_rate) +VALUES + (1, 'Ноутбук Lenovo ThinkPad', 2, 75000.00, 5.00, 20.00), + (1, 'Мышь Logitech MX Master', 2, 7500.00, 10.00, 20.00), + (2, 'Монитор Dell UltraSharp', 1, 45000.00, 0.00, 20.00), + (3, 'Клавиатура Mechanical', 3, 12000.00, 15.00, 20.00); + +-- Вставка тестовых данных в rooms +INSERT INTO rooms (room_number, length, width) +VALUES + ('A101', 5.5, 4.2), + ('A102', 6.0, 5.0), + ('B201', 4.5, 3.8), + ('B202', 7.2, 6.5); + +-- Вставка тестовых данных в users +INSERT INTO users (first_name, last_name, middle_name, email) +VALUES + ('Арина', 'Полякова', 'Витальевна', 'polyakova.av@example.com'), + ('Иван', 'Иванов', 'Петрович', 'ivanov.ip@example.com'), + ('Мария', 'Сидорова', NULL, 'sidorova.m@example.com'), + ('Петр', 'Петров', 'Сергеевич', 'petrov.ps@example.com'); + +-- Вставка тестовых данных в users_log +INSERT INTO users_log (payload) +VALUES + ('{"email": "test1@example.com", "action": "login", "timestamp": "2025-12-15T10:00:00Z"}'::jsonb), + ('{"email": "test2@example.com", "action": "logout", "timestamp": "2025-12-15T11:00:00Z"}'::jsonb), + ('{"email": "admin@example.com", "action": "update", "timestamp": "2025-12-15T12:00:00Z"}'::jsonb); + +-- ============================================================================ +-- РАЗДЕЛ 9: ПРИМЕНЕНИЕ ПРАВИЛ И ТЕСТИРОВАНИЕ +-- ============================================================================ + +-- Применение всех правил +SELECT * FROM computed_fields_system.apply_computed_fields_rules(); + +-- Проверка созданных полей +SELECT + table_name, + column_name, + data_type, + is_generated, + generation_expression +FROM information_schema.columns +WHERE table_schema = 'public' + AND column_name IN ('final_price', 'total_area', 'full_name', + 'search_string', 'email_extracted') +ORDER BY table_name, ordinal_position; + +-- Проверка триггеров +SELECT + trigger_schema, + trigger_name, + event_object_table, + action_statement +FROM information_schema.triggers +WHERE trigger_name LIKE 'trg_%_compute' +ORDER BY event_object_table; + +-- Валидация целостности всех полей +SELECT * FROM computed_fields_system.validate_computed_fields(); + +-- Просмотр результатов в таблицах +SELECT id, product_name, quantity, unit_price, + discount_percent, tax_rate, final_price +FROM order_items; + +SELECT id, room_number, length, width, total_area +FROM rooms; + +SELECT id, last_name, first_name, middle_name, + full_name, search_string +FROM users; + +SELECT id, payload, email_extracted +FROM users_log; + +-- ============================================================================ +-- РАЗДЕЛ 10: УТИЛИТЫ ДЛЯ УПРАВЛЕНИЯ +-- ============================================================================ + +-- Просмотр логов применения правил +CREATE OR REPLACE VIEW computed_fields_system.v_application_log AS +SELECT + l.id, + l.executed_at, + c.table_schema || '.' || c.table_name as table_full_name, + c.column_name, + l.action, + l.implementation_method, + l.status, + l.error_message +FROM computed_fields_system.application_log l +JOIN computed_fields_system.computed_fields_config c ON c.id = l.config_id +ORDER BY l.executed_at DESC; + +-- Просмотр последних результатов валидации +CREATE OR REPLACE VIEW computed_fields_system.v_latest_validation AS +SELECT DISTINCT ON (table_schema, table_name, column_name) + table_schema || '.' || table_name as table_full_name, + column_name, + validation_time, + total_rows, + invalid_rows, + CASE + WHEN invalid_rows = 0 THEN 'VALID' + WHEN invalid_rows > 0 THEN 'INVALID' + ELSE 'ERROR' + END as validation_status +FROM computed_fields_system.validation_results +ORDER BY table_schema, table_name, column_name, validation_time DESC; + +-- Процедура деактивации правила +CREATE OR REPLACE FUNCTION computed_fields_system.deactivate_rule( + p_table_schema VARCHAR, + p_table_name VARCHAR, + p_column_name VARCHAR +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + UPDATE computed_fields_system.computed_fields_config + SET is_active = FALSE, + updated_at = CURRENT_TIMESTAMP + WHERE table_schema = p_table_schema + AND table_name = p_table_name + AND column_name = p_column_name; + + IF NOT FOUND THEN + RAISE EXCEPTION 'Rule not found for %.%.%', + p_table_schema, p_table_name, p_column_name; + END IF; + + RAISE NOTICE 'Rule deactivated for %.%.%', + p_table_schema, p_table_name, p_column_name; +END; +$$; + +-- ============================================================================ +-- РАЗДЕЛ 11: ДОКУМЕНТАЦИЯ И СПРАВКА +-- ============================================================================ + +COMMENT ON SCHEMA computed_fields_system IS + 'Система автоматической генерации вычисляемых полей в PostgreSQL'; + +COMMENT ON FUNCTION computed_fields_system.apply_computed_fields_rules() IS + 'Оркестратор применения правил: сканирует конфигурацию и создает вычисляемые поля'; + +COMMENT ON FUNCTION computed_fields_system.validate_computed_fields(VARCHAR, VARCHAR) IS + 'Валидация целостности: проверяет соответствие сохраненных значений формулам'; + +COMMENT ON FUNCTION computed_fields_system.force_recompute(VARCHAR, VARCHAR, VARCHAR) IS + 'Принудительный пересчет всех значений вычисляемого поля'; + +-- Итоговый отчет +SELECT 'Модуль успешно установлен!' as status, + computed_fields_system.get_pg_version_num() as pg_version, + computed_fields_system.supports_generated_columns() as native_support, + (SELECT COUNT(*) FROM computed_fields_system.computed_fields_config WHERE is_active = TRUE) as active_rules; + +-- ============================================================================ +-- КОНЕЦ СКРИПТА +-- ============================================================================ diff --git a/load_test_benchmark.sql b/load_test_benchmark.sql new file mode 100644 index 0000000..98d4833 --- /dev/null +++ b/load_test_benchmark.sql @@ -0,0 +1,1117 @@ +-- ============================================================================ +-- НАГРУЗОЧНОЕ ТЕСТИРОВАНИЕ СИСТЕМЫ ВЫЧИСЛЯЕМЫХ ПОЛЕЙ +-- Поддержка: PostgreSQL 11 (триггеры) и PostgreSQL 16 (GENERATED COLUMNS) +-- ============================================================================ + +-- Параметры тестирования (можно изменить) +\set TEST_ROWS 100000 +\set BATCH_SIZE 10000 + +-- Включение расширенного вывода времени +\timing on + +-- ============================================================================ +-- ЧАСТЬ 1: ОЧИСТКА ПРЕДЫДУЩИХ ДАННЫХ (если есть) +-- ============================================================================ + +DO $$ +BEGIN + RAISE NOTICE '=== НАЧАЛО НАГРУЗОЧНОГО ТЕСТИРОВАНИЯ ==='; + RAISE NOTICE 'Версия PostgreSQL: %', current_setting('server_version'); + RAISE NOTICE 'Время начала: %', clock_timestamp(); +END $$; + +-- Удаление существующих объектов (для чистого запуска) +DROP SCHEMA IF EXISTS computed_fields_system CASCADE; +DROP TABLE IF EXISTS order_items CASCADE; +DROP TABLE IF EXISTS rooms CASCADE; +DROP TABLE IF EXISTS users CASCADE; +DROP TABLE IF EXISTS users_log CASCADE; +DROP TABLE IF EXISTS benchmark_results CASCADE; + +-- ============================================================================ +-- ЧАСТЬ 2: ТАБЛИЦА ДЛЯ СОХРАНЕНИЯ РЕЗУЛЬТАТОВ ТЕСТИРОВАНИЯ +-- ============================================================================ + +CREATE TABLE benchmark_results ( + id SERIAL PRIMARY KEY, + test_name VARCHAR(100) NOT NULL, + pg_version TEXT NOT NULL, + implementation_method VARCHAR(20) NOT NULL, + operation VARCHAR(50) NOT NULL, + rows_affected BIGINT, + duration_ms NUMERIC(15,3), + started_at TIMESTAMP NOT NULL DEFAULT clock_timestamp(), + finished_at TIMESTAMP +); + +-- ============================================================================ +-- ЧАСТЬ 3: СОЗДАНИЕ СХЕМЫ И СЛУЖЕБНЫХ ТАБЛИЦ +-- ============================================================================ + +-- Засекаем время создания инфраструктуры +DO $$ +DECLARE + v_start TIMESTAMP; + v_end TIMESTAMP; + v_pg_version TEXT; +BEGIN + v_start := clock_timestamp(); + v_pg_version := current_setting('server_version'); + + -- Создание схемы для модуля вычисляемых полей + CREATE SCHEMA IF NOT EXISTS computed_fields_system; + + -- Таблица конфигурации вычисляемых полей (реестр правил) + CREATE TABLE IF NOT EXISTS computed_fields_system.computed_fields_config ( + id SERIAL PRIMARY KEY, + table_schema VARCHAR(63) NOT NULL DEFAULT 'public', + table_name VARCHAR(63) NOT NULL, + column_name VARCHAR(63) NOT NULL, + column_type VARCHAR(50) NOT NULL, + expression TEXT NOT NULL, + implementation_type VARCHAR(10) NOT NULL DEFAULT 'AUTO' + CHECK (implementation_type IN ('AUTO', 'NATIVE', 'TRIGGER')), + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + description TEXT, + UNIQUE (table_schema, table_name, column_name) + ); + + -- Таблица логов применения правил + CREATE TABLE IF NOT EXISTS computed_fields_system.application_log ( + id SERIAL PRIMARY KEY, + config_id INTEGER REFERENCES computed_fields_system.computed_fields_config(id), + action VARCHAR(20) NOT NULL, + implementation_method VARCHAR(10) NOT NULL, + status VARCHAR(10) NOT NULL CHECK (status IN ('SUCCESS', 'ERROR')), + error_message TEXT, + executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + + -- Таблица результатов валидации + CREATE TABLE IF NOT EXISTS computed_fields_system.validation_results ( + id SERIAL PRIMARY KEY, + table_schema VARCHAR(63) NOT NULL, + table_name VARCHAR(63) NOT NULL, + column_name VARCHAR(63) NOT NULL, + validation_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + total_rows BIGINT NOT NULL, + invalid_rows BIGINT NOT NULL, + invalid_row_ids TEXT + ); + + v_end := clock_timestamp(); + + INSERT INTO benchmark_results (test_name, pg_version, implementation_method, operation, rows_affected, duration_ms, finished_at) + VALUES ('Создание инфраструктуры', v_pg_version, 'SYSTEM', 'CREATE_SCHEMA', 0, + EXTRACT(EPOCH FROM (v_end - v_start)) * 1000, v_end); + + RAISE NOTICE 'Схема создана за: % мс', EXTRACT(EPOCH FROM (v_end - v_start)) * 1000; +END $$; + +-- ============================================================================ +-- ЧАСТЬ 4: СОЗДАНИЕ ВСПОМОГАТЕЛЬНЫХ ФУНКЦИЙ +-- ============================================================================ + +-- Функция для определения версии PostgreSQL +CREATE OR REPLACE FUNCTION computed_fields_system.get_pg_version_num() +RETURNS INTEGER +LANGUAGE plpgsql STABLE +AS $$ +DECLARE + v_version INTEGER; +BEGIN + SELECT current_setting('server_version_num')::INTEGER INTO v_version; + RETURN v_version; +END; +$$; + +-- Функция проверки поддержки GENERATED COLUMNS +CREATE OR REPLACE FUNCTION computed_fields_system.supports_generated_columns() +RETURNS BOOLEAN +LANGUAGE plpgsql STABLE +AS $$ +BEGIN + -- PostgreSQL 12+ (версия >= 120000) поддерживает GENERATED COLUMNS + RETURN computed_fields_system.get_pg_version_num() >= 120000; +END; +$$; + +-- Функция проверки существования столбца +CREATE OR REPLACE FUNCTION computed_fields_system.column_exists( + p_schema VARCHAR, + p_table VARCHAR, + p_column VARCHAR +) +RETURNS BOOLEAN +LANGUAGE plpgsql STABLE +AS $$ +DECLARE + v_exists BOOLEAN; +BEGIN + SELECT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = p_schema + AND table_name = p_table + AND column_name = p_column + ) INTO v_exists; + + RETURN v_exists; +END; +$$; + +-- ============================================================================ +-- ЧАСТЬ 5: ПРОЦЕДУРЫ ГЕНЕРАЦИИ ВЫЧИСЛЯЕМЫХ ПОЛЕЙ +-- ============================================================================ + +-- Процедура создания вычисляемого поля через GENERATED COLUMN (нативный метод) +CREATE OR REPLACE FUNCTION computed_fields_system.create_generated_column( + p_schema VARCHAR, + p_table VARCHAR, + p_column VARCHAR, + p_type VARCHAR, + p_expression TEXT +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + v_sql TEXT; +BEGIN + v_sql := format( + 'ALTER TABLE %I.%I ADD COLUMN %I %s GENERATED ALWAYS AS (%s) STORED', + p_schema, p_table, p_column, p_type, p_expression + ); + + EXECUTE v_sql; +END; +$$; + +-- Процедура создания вычисляемого поля через TRIGGER (триггерный метод) +CREATE OR REPLACE FUNCTION computed_fields_system.create_trigger_computed_field( + p_schema VARCHAR, + p_table VARCHAR, + p_column VARCHAR, + p_type VARCHAR, + p_expression TEXT +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + v_function_name VARCHAR; + v_trigger_name VARCHAR; + v_sql TEXT; +BEGIN + v_function_name := format('trg_fn_%s_%s_compute', p_table, p_column); + v_trigger_name := format('trg_%s_%s_compute', p_table, p_column); + + -- 1. Добавляем столбец (если не существует) + IF NOT computed_fields_system.column_exists(p_schema, p_table, p_column) THEN + v_sql := format( + 'ALTER TABLE %I.%I ADD COLUMN %I %s', + p_schema, p_table, p_column, p_type + ); + EXECUTE v_sql; + END IF; + + -- 2. Создаем триггерную функцию + v_sql := format($FUNC$ + CREATE OR REPLACE FUNCTION %I.%I() + RETURNS TRIGGER + LANGUAGE plpgsql + AS $TRG$ + BEGIN + BEGIN + NEW.%I := (%s)::%s; + EXCEPTION WHEN OTHERS THEN + NEW.%I := NULL; + END; + RETURN NEW; + END; + $TRG$; + $FUNC$, + p_schema, v_function_name, + p_column, p_expression, p_type, + p_column + ); + + EXECUTE v_sql; + + -- 3. Создаем триггер + v_sql := format($TRIG$ + DROP TRIGGER IF EXISTS %I ON %I.%I; + CREATE TRIGGER %I + BEFORE INSERT OR UPDATE ON %I.%I + FOR EACH ROW + EXECUTE FUNCTION %I.%I(); + $TRIG$, + v_trigger_name, p_schema, p_table, + v_trigger_name, + p_schema, p_table, + p_schema, v_function_name + ); + + EXECUTE v_sql; +END; +$$; + +-- ============================================================================ +-- ЧАСТЬ 6: ОРКЕСТРАТОР ПРИМЕНЕНИЯ ПРАВИЛ +-- ============================================================================ + +CREATE OR REPLACE FUNCTION computed_fields_system.apply_computed_fields_rules() +RETURNS TABLE ( + config_id INTEGER, + table_full_name TEXT, + column_name TEXT, + method_used VARCHAR, + status TEXT +) +LANGUAGE plpgsql +AS $$ +DECLARE + v_config RECORD; + v_implementation_method VARCHAR(10); + v_supports_native BOOLEAN; + v_error_msg TEXT; +BEGIN + v_supports_native := computed_fields_system.supports_generated_columns(); + + RAISE NOTICE 'PostgreSQL version: %, Native GENERATED COLUMNS support: %', + computed_fields_system.get_pg_version_num(), v_supports_native; + + FOR v_config IN + SELECT c.id, c.table_schema, c.table_name, c.column_name, + c.column_type, c.expression, c.implementation_type + FROM computed_fields_system.computed_fields_config c + WHERE c.is_active = TRUE + ORDER BY c.id + LOOP + BEGIN + IF v_config.implementation_type = 'AUTO' THEN + v_implementation_method := CASE + WHEN v_supports_native THEN 'NATIVE' + ELSE 'TRIGGER' + END; + ELSE + v_implementation_method := v_config.implementation_type; + END IF; + + IF v_implementation_method = 'NATIVE' AND NOT v_supports_native THEN + RAISE EXCEPTION 'NATIVE method requested but PostgreSQL version < 12'; + END IF; + + IF computed_fields_system.column_exists( + v_config.table_schema, + v_config.table_name, + v_config.column_name + ) THEN + CONTINUE; + END IF; + + IF v_implementation_method = 'NATIVE' THEN + PERFORM computed_fields_system.create_generated_column( + v_config.table_schema, + v_config.table_name, + v_config.column_name, + v_config.column_type, + v_config.expression + ); + ELSE + PERFORM computed_fields_system.create_trigger_computed_field( + v_config.table_schema, + v_config.table_name, + v_config.column_name, + v_config.column_type, + v_config.expression + ); + END IF; + + INSERT INTO computed_fields_system.application_log + (config_id, action, implementation_method, status) + VALUES + (v_config.id, 'CREATE', v_implementation_method, 'SUCCESS'); + + RETURN QUERY SELECT + v_config.id, + v_config.table_schema || '.' || v_config.table_name, + v_config.column_name, + v_implementation_method, + 'SUCCESS'::TEXT; + + EXCEPTION WHEN OTHERS THEN + v_error_msg := SQLERRM; + + INSERT INTO computed_fields_system.application_log + (config_id, action, implementation_method, status, error_message) + VALUES + (v_config.id, 'CREATE', v_implementation_method, 'ERROR', v_error_msg); + + RETURN QUERY SELECT + v_config.id, + v_config.table_schema || '.' || v_config.table_name, + v_config.column_name, + v_implementation_method, + 'ERROR: ' || v_error_msg; + END; + END LOOP; +END; +$$; + +-- ============================================================================ +-- ЧАСТЬ 7: СОЗДАНИЕ ТЕСТОВЫХ ТАБЛИЦ +-- ============================================================================ + +DO $$ +DECLARE + v_start TIMESTAMP; + v_end TIMESTAMP; + v_pg_version TEXT; +BEGIN + v_start := clock_timestamp(); + v_pg_version := current_setting('server_version'); + + -- Таблица заказов (order_items) + CREATE TABLE IF NOT EXISTS order_items ( + id SERIAL PRIMARY KEY, + order_id INTEGER NOT NULL, + product_name VARCHAR(255) NOT NULL, + quantity INTEGER NOT NULL CHECK (quantity > 0), + unit_price NUMERIC(10,2) NOT NULL CHECK (unit_price >= 0), + discount_percent NUMERIC(5,2) DEFAULT 0 CHECK (discount_percent BETWEEN 0 AND 100), + tax_rate NUMERIC(5,2) DEFAULT 0 CHECK (tax_rate >= 0), + order_date DATE DEFAULT CURRENT_DATE + ); + + -- Таблица комнат (rooms) + CREATE TABLE IF NOT EXISTS rooms ( + id SERIAL PRIMARY KEY, + room_number VARCHAR(10) NOT NULL, + length NUMERIC(10,2) NOT NULL CHECK (length > 0), + width NUMERIC(10,2) NOT NULL CHECK (width > 0) + ); + + -- Таблица пользователей (users) + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + middle_name VARCHAR(100), + email VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + -- Таблица логов с JSON (users_log) + CREATE TABLE IF NOT EXISTS users_log ( + id SERIAL PRIMARY KEY, + payload JSONB NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + v_end := clock_timestamp(); + + INSERT INTO benchmark_results (test_name, pg_version, implementation_method, operation, rows_affected, duration_ms, finished_at) + VALUES ('Создание тестовых таблиц', v_pg_version, 'SYSTEM', 'CREATE_TABLES', 0, + EXTRACT(EPOCH FROM (v_end - v_start)) * 1000, v_end); + + RAISE NOTICE 'Тестовые таблицы созданы за: % мс', EXTRACT(EPOCH FROM (v_end - v_start)) * 1000; +END $$; + +-- ============================================================================ +-- ЧАСТЬ 8: КОНФИГУРАЦИЯ ВЫЧИСЛЯЕМЫХ ПОЛЕЙ +-- ============================================================================ + +INSERT INTO computed_fields_system.computed_fields_config + (table_schema, table_name, column_name, column_type, expression, + implementation_type, description) +VALUES + ('public', 'order_items', 'final_price', 'NUMERIC(10,2)', + '(quantity * unit_price) * (1 - discount_percent / 100.0) * (1 + tax_rate / 100.0)', + 'AUTO', 'Финальная стоимость с учетом скидки и налога'), + + ('public', 'rooms', 'total_area', 'NUMERIC(10,2)', + 'length * width', + 'AUTO', 'Площадь комнаты в квадратных метрах'), + + ('public', 'users', 'full_name', 'TEXT', + 'CONCAT_WS('' '', last_name, first_name, middle_name)', + 'AUTO', 'ФИО пользователя для поиска и отображения'), + + ('public', 'users', 'search_string', 'TEXT', + 'LOWER(CONCAT_WS('' '', last_name, first_name, middle_name, email))', + 'AUTO', 'Нормализованная строка для полнотекстового поиска'), + + ('public', 'users_log', 'email_extracted', 'TEXT', + 'payload ->> ''email''', + 'AUTO', 'Email извлеченный из JSON для быстрого доступа') +ON CONFLICT (table_schema, table_name, column_name) DO NOTHING; + +-- ============================================================================ +-- ЧАСТЬ 9: ПРИМЕНЕНИЕ ПРАВИЛ (СОЗДАНИЕ ВЫЧИСЛЯЕМЫХ ПОЛЕЙ) +-- ============================================================================ + +DO $$ +DECLARE + v_start TIMESTAMP; + v_end TIMESTAMP; + v_pg_version TEXT; + v_method TEXT; +BEGIN + v_start := clock_timestamp(); + v_pg_version := current_setting('server_version'); + + -- Определяем метод реализации + IF computed_fields_system.supports_generated_columns() THEN + v_method := 'NATIVE (GENERATED COLUMNS)'; + ELSE + v_method := 'TRIGGER'; + END IF; + + RAISE NOTICE 'Применение правил. Метод реализации: %', v_method; + + -- Применяем правила + PERFORM * FROM computed_fields_system.apply_computed_fields_rules(); + + v_end := clock_timestamp(); + + INSERT INTO benchmark_results (test_name, pg_version, implementation_method, operation, rows_affected, duration_ms, finished_at) + VALUES ('Применение правил', v_pg_version, v_method, 'APPLY_RULES', 5, + EXTRACT(EPOCH FROM (v_end - v_start)) * 1000, v_end); + + RAISE NOTICE 'Правила применены за: % мс', EXTRACT(EPOCH FROM (v_end - v_start)) * 1000; +END $$; + +-- ============================================================================ +-- ЧАСТЬ 10: НАГРУЗОЧНОЕ ТЕСТИРОВАНИЕ - МАССОВАЯ ВСТАВКА +-- ============================================================================ + +-- Тест 1: Массовая вставка в order_items (100000 строк) +DO $$ +DECLARE + v_start TIMESTAMP; + v_end TIMESTAMP; + v_pg_version TEXT; + v_method TEXT; + v_rows_count INTEGER := 100000; +BEGIN + v_start := clock_timestamp(); + v_pg_version := current_setting('server_version'); + + IF computed_fields_system.supports_generated_columns() THEN + v_method := 'NATIVE'; + ELSE + v_method := 'TRIGGER'; + END IF; + + RAISE NOTICE 'ТЕСТ 1: Массовая вставка в order_items (% строк)...', v_rows_count; + + INSERT INTO order_items (order_id, product_name, quantity, unit_price, discount_percent, tax_rate) + SELECT + (random() * 10000)::INTEGER + 1, + 'Product_' || i, + (random() * 99)::INTEGER + 1, + (random() * 99999)::NUMERIC(10,2) + 100, + (random() * 30)::NUMERIC(5,2), + (random() * 25)::NUMERIC(5,2) + FROM generate_series(1, v_rows_count) AS i; + + v_end := clock_timestamp(); + + INSERT INTO benchmark_results (test_name, pg_version, implementation_method, operation, rows_affected, duration_ms, finished_at) + VALUES ('INSERT order_items', v_pg_version, v_method, 'BULK_INSERT', v_rows_count, + EXTRACT(EPOCH FROM (v_end - v_start)) * 1000, v_end); + + RAISE NOTICE 'Вставка % строк в order_items: % мс', v_rows_count, EXTRACT(EPOCH FROM (v_end - v_start)) * 1000; +END $$; + +-- Тест 2: Массовая вставка в rooms (50000 строк) +DO $$ +DECLARE + v_start TIMESTAMP; + v_end TIMESTAMP; + v_pg_version TEXT; + v_method TEXT; + v_rows_count INTEGER := 50000; +BEGIN + v_start := clock_timestamp(); + v_pg_version := current_setting('server_version'); + + IF computed_fields_system.supports_generated_columns() THEN + v_method := 'NATIVE'; + ELSE + v_method := 'TRIGGER'; + END IF; + + RAISE NOTICE 'ТЕСТ 2: Массовая вставка в rooms (% строк)...', v_rows_count; + + INSERT INTO rooms (room_number, length, width) + SELECT + 'R' || LPAD(i::TEXT, 6, '0'), + (random() * 20)::NUMERIC(10,2) + 2, + (random() * 15)::NUMERIC(10,2) + 2 + FROM generate_series(1, v_rows_count) AS i; + + v_end := clock_timestamp(); + + INSERT INTO benchmark_results (test_name, pg_version, implementation_method, operation, rows_affected, duration_ms, finished_at) + VALUES ('INSERT rooms', v_pg_version, v_method, 'BULK_INSERT', v_rows_count, + EXTRACT(EPOCH FROM (v_end - v_start)) * 1000, v_end); + + RAISE NOTICE 'Вставка % строк в rooms: % мс', v_rows_count, EXTRACT(EPOCH FROM (v_end - v_start)) * 1000; +END $$; + +-- Тест 3: Массовая вставка в users (100000 строк) +DO $$ +DECLARE + v_start TIMESTAMP; + v_end TIMESTAMP; + v_pg_version TEXT; + v_method TEXT; + v_rows_count INTEGER := 100000; +BEGIN + v_start := clock_timestamp(); + v_pg_version := current_setting('server_version'); + + IF computed_fields_system.supports_generated_columns() THEN + v_method := 'NATIVE'; + ELSE + v_method := 'TRIGGER'; + END IF; + + RAISE NOTICE 'ТЕСТ 3: Массовая вставка в users (% строк)...', v_rows_count; + + INSERT INTO users (first_name, last_name, middle_name, email) + SELECT + 'FirstName_' || i, + 'LastName_' || i, + CASE WHEN random() > 0.3 THEN 'MiddleName_' || i ELSE NULL END, + 'user_' || i || '@example.com' + FROM generate_series(1, v_rows_count) AS i; + + v_end := clock_timestamp(); + + INSERT INTO benchmark_results (test_name, pg_version, implementation_method, operation, rows_affected, duration_ms, finished_at) + VALUES ('INSERT users', v_pg_version, v_method, 'BULK_INSERT', v_rows_count, + EXTRACT(EPOCH FROM (v_end - v_start)) * 1000, v_end); + + RAISE NOTICE 'Вставка % строк в users: % мс', v_rows_count, EXTRACT(EPOCH FROM (v_end - v_start)) * 1000; +END $$; + +-- Тест 4: Массовая вставка в users_log с JSON (50000 строк) +DO $$ +DECLARE + v_start TIMESTAMP; + v_end TIMESTAMP; + v_pg_version TEXT; + v_method TEXT; + v_rows_count INTEGER := 50000; +BEGIN + v_start := clock_timestamp(); + v_pg_version := current_setting('server_version'); + + IF computed_fields_system.supports_generated_columns() THEN + v_method := 'NATIVE'; + ELSE + v_method := 'TRIGGER'; + END IF; + + RAISE NOTICE 'ТЕСТ 4: Массовая вставка в users_log с JSON (% строк)...', v_rows_count; + + INSERT INTO users_log (payload) + SELECT + jsonb_build_object( + 'email', 'user_' || i || '@example.com', + 'action', (ARRAY['login', 'logout', 'update', 'delete', 'create'])[1 + (random() * 4)::INTEGER], + 'timestamp', clock_timestamp(), + 'data', jsonb_build_object('id', i, 'value', random()) + ) + FROM generate_series(1, v_rows_count) AS i; + + v_end := clock_timestamp(); + + INSERT INTO benchmark_results (test_name, pg_version, implementation_method, operation, rows_affected, duration_ms, finished_at) + VALUES ('INSERT users_log', v_pg_version, v_method, 'BULK_INSERT', v_rows_count, + EXTRACT(EPOCH FROM (v_end - v_start)) * 1000, v_end); + + RAISE NOTICE 'Вставка % строк в users_log: % мс', v_rows_count, EXTRACT(EPOCH FROM (v_end - v_start)) * 1000; +END $$; + +-- ============================================================================ +-- ЧАСТЬ 11: НАГРУЗОЧНОЕ ТЕСТИРОВАНИЕ - МАССОВОЕ ОБНОВЛЕНИЕ +-- ============================================================================ + +-- Тест 5: Массовое обновление order_items +DO $$ +DECLARE + v_start TIMESTAMP; + v_end TIMESTAMP; + v_pg_version TEXT; + v_method TEXT; + v_updated BIGINT; +BEGIN + v_start := clock_timestamp(); + v_pg_version := current_setting('server_version'); + + IF computed_fields_system.supports_generated_columns() THEN + v_method := 'NATIVE'; + ELSE + v_method := 'TRIGGER'; + END IF; + + RAISE NOTICE 'ТЕСТ 5: Массовое обновление order_items...'; + + UPDATE order_items + SET discount_percent = discount_percent + 1, + tax_rate = tax_rate + 0.5 + WHERE id <= 50000; + + GET DIAGNOSTICS v_updated = ROW_COUNT; + + v_end := clock_timestamp(); + + INSERT INTO benchmark_results (test_name, pg_version, implementation_method, operation, rows_affected, duration_ms, finished_at) + VALUES ('UPDATE order_items', v_pg_version, v_method, 'BULK_UPDATE', v_updated, + EXTRACT(EPOCH FROM (v_end - v_start)) * 1000, v_end); + + RAISE NOTICE 'Обновление % строк в order_items: % мс', v_updated, EXTRACT(EPOCH FROM (v_end - v_start)) * 1000; +END $$; + +-- Тест 6: Массовое обновление users +DO $$ +DECLARE + v_start TIMESTAMP; + v_end TIMESTAMP; + v_pg_version TEXT; + v_method TEXT; + v_updated BIGINT; +BEGIN + v_start := clock_timestamp(); + v_pg_version := current_setting('server_version'); + + IF computed_fields_system.supports_generated_columns() THEN + v_method := 'NATIVE'; + ELSE + v_method := 'TRIGGER'; + END IF; + + RAISE NOTICE 'ТЕСТ 6: Массовое обновление users...'; + + UPDATE users + SET first_name = first_name || '_updated', + middle_name = COALESCE(middle_name, 'New') || '_upd' + WHERE id <= 50000; + + GET DIAGNOSTICS v_updated = ROW_COUNT; + + v_end := clock_timestamp(); + + INSERT INTO benchmark_results (test_name, pg_version, implementation_method, operation, rows_affected, duration_ms, finished_at) + VALUES ('UPDATE users', v_pg_version, v_method, 'BULK_UPDATE', v_updated, + EXTRACT(EPOCH FROM (v_end - v_start)) * 1000, v_end); + + RAISE NOTICE 'Обновление % строк в users: % мс', v_updated, EXTRACT(EPOCH FROM (v_end - v_start)) * 1000; +END $$; + +-- ============================================================================ +-- ЧАСТЬ 12: НАГРУЗОЧНОЕ ТЕСТИРОВАНИЕ - ЧТЕНИЕ ДАННЫХ +-- ============================================================================ + +-- Тест 7: Чтение order_items с вычисляемым полем +DO $$ +DECLARE + v_start TIMESTAMP; + v_end TIMESTAMP; + v_pg_version TEXT; + v_method TEXT; + v_count BIGINT; +BEGIN + v_start := clock_timestamp(); + v_pg_version := current_setting('server_version'); + + IF computed_fields_system.supports_generated_columns() THEN + v_method := 'NATIVE'; + ELSE + v_method := 'TRIGGER'; + END IF; + + RAISE NOTICE 'ТЕСТ 7: Чтение order_items с вычисляемым полем final_price...'; + + SELECT COUNT(*) INTO v_count + FROM order_items + WHERE final_price > 10000; + + v_end := clock_timestamp(); + + INSERT INTO benchmark_results (test_name, pg_version, implementation_method, operation, rows_affected, duration_ms, finished_at) + VALUES ('SELECT order_items (filter by computed)', v_pg_version, v_method, 'SELECT_FILTER', v_count, + EXTRACT(EPOCH FROM (v_end - v_start)) * 1000, v_end); + + RAISE NOTICE 'Чтение order_items (% строк соответствует): % мс', v_count, EXTRACT(EPOCH FROM (v_end - v_start)) * 1000; +END $$; + +-- Тест 8: Агрегация по вычисляемому полю +DO $$ +DECLARE + v_start TIMESTAMP; + v_end TIMESTAMP; + v_pg_version TEXT; + v_method TEXT; + v_sum NUMERIC; +BEGIN + v_start := clock_timestamp(); + v_pg_version := current_setting('server_version'); + + IF computed_fields_system.supports_generated_columns() THEN + v_method := 'NATIVE'; + ELSE + v_method := 'TRIGGER'; + END IF; + + RAISE NOTICE 'ТЕСТ 8: Агрегация по вычисляемому полю final_price...'; + + SELECT SUM(final_price) INTO v_sum + FROM order_items; + + v_end := clock_timestamp(); + + INSERT INTO benchmark_results (test_name, pg_version, implementation_method, operation, rows_affected, duration_ms, finished_at) + VALUES ('SUM(final_price)', v_pg_version, v_method, 'AGGREGATE', 1, + EXTRACT(EPOCH FROM (v_end - v_start)) * 1000, v_end); + + RAISE NOTICE 'Агрегация SUM(final_price) = %: % мс', v_sum, EXTRACT(EPOCH FROM (v_end - v_start)) * 1000; +END $$; + +-- Тест 9: Поиск по вычисляемому полю search_string +DO $$ +DECLARE + v_start TIMESTAMP; + v_end TIMESTAMP; + v_pg_version TEXT; + v_method TEXT; + v_count BIGINT; +BEGIN + v_start := clock_timestamp(); + v_pg_version := current_setting('server_version'); + + IF computed_fields_system.supports_generated_columns() THEN + v_method := 'NATIVE'; + ELSE + v_method := 'TRIGGER'; + END IF; + + RAISE NOTICE 'ТЕСТ 9: Поиск по вычисляемому полю search_string...'; + + SELECT COUNT(*) INTO v_count + FROM users + WHERE search_string LIKE '%user_1%'; + + v_end := clock_timestamp(); + + INSERT INTO benchmark_results (test_name, pg_version, implementation_method, operation, rows_affected, duration_ms, finished_at) + VALUES ('SELECT users (LIKE on computed)', v_pg_version, v_method, 'SELECT_LIKE', v_count, + EXTRACT(EPOCH FROM (v_end - v_start)) * 1000, v_end); + + RAISE NOTICE 'Поиск по search_string (% строк): % мс', v_count, EXTRACT(EPOCH FROM (v_end - v_start)) * 1000; +END $$; + +-- Тест 10: Чтение из users_log с JSON-полем +DO $$ +DECLARE + v_start TIMESTAMP; + v_end TIMESTAMP; + v_pg_version TEXT; + v_method TEXT; + v_count BIGINT; +BEGIN + v_start := clock_timestamp(); + v_pg_version := current_setting('server_version'); + + IF computed_fields_system.supports_generated_columns() THEN + v_method := 'NATIVE'; + ELSE + v_method := 'TRIGGER'; + END IF; + + RAISE NOTICE 'ТЕСТ 10: Чтение users_log с вычисляемым JSON-полем...'; + + SELECT COUNT(*) INTO v_count + FROM users_log + WHERE email_extracted LIKE '%user_1%'; + + v_end := clock_timestamp(); + + INSERT INTO benchmark_results (test_name, pg_version, implementation_method, operation, rows_affected, duration_ms, finished_at) + VALUES ('SELECT users_log (JSON extracted)', v_pg_version, v_method, 'SELECT_JSON', v_count, + EXTRACT(EPOCH FROM (v_end - v_start)) * 1000, v_end); + + RAISE NOTICE 'Поиск по email_extracted (% строк): % мс', v_count, EXTRACT(EPOCH FROM (v_end - v_start)) * 1000; +END $$; + +-- ============================================================================ +-- ЧАСТЬ 13: НАГРУЗОЧНОЕ ТЕСТИРОВАНИЕ - ИНДЕКСИРОВАНИЕ +-- ============================================================================ + +-- Тест 11: Создание индекса на вычисляемое поле +DO $$ +DECLARE + v_start TIMESTAMP; + v_end TIMESTAMP; + v_pg_version TEXT; + v_method TEXT; +BEGIN + v_start := clock_timestamp(); + v_pg_version := current_setting('server_version'); + + IF computed_fields_system.supports_generated_columns() THEN + v_method := 'NATIVE'; + ELSE + v_method := 'TRIGGER'; + END IF; + + RAISE NOTICE 'ТЕСТ 11: Создание индекса на вычисляемое поле final_price...'; + + CREATE INDEX IF NOT EXISTS idx_order_items_final_price ON order_items(final_price); + + v_end := clock_timestamp(); + + INSERT INTO benchmark_results (test_name, pg_version, implementation_method, operation, rows_affected, duration_ms, finished_at) + VALUES ('CREATE INDEX on final_price', v_pg_version, v_method, 'CREATE_INDEX', 0, + EXTRACT(EPOCH FROM (v_end - v_start)) * 1000, v_end); + + RAISE NOTICE 'Создание индекса: % мс', EXTRACT(EPOCH FROM (v_end - v_start)) * 1000; +END $$; + +-- Тест 12: Запрос с использованием индекса +DO $$ +DECLARE + v_start TIMESTAMP; + v_end TIMESTAMP; + v_pg_version TEXT; + v_method TEXT; + v_count BIGINT; +BEGIN + v_start := clock_timestamp(); + v_pg_version := current_setting('server_version'); + + IF computed_fields_system.supports_generated_columns() THEN + v_method := 'NATIVE'; + ELSE + v_method := 'TRIGGER'; + END IF; + + RAISE NOTICE 'ТЕСТ 12: Запрос с использованием индекса на вычисляемом поле...'; + + -- Анализ для обновления статистики + ANALYZE order_items; + + SELECT COUNT(*) INTO v_count + FROM order_items + WHERE final_price BETWEEN 50000 AND 100000; + + v_end := clock_timestamp(); + + INSERT INTO benchmark_results (test_name, pg_version, implementation_method, operation, rows_affected, duration_ms, finished_at) + VALUES ('SELECT with INDEX on computed', v_pg_version, v_method, 'SELECT_INDEXED', v_count, + EXTRACT(EPOCH FROM (v_end - v_start)) * 1000, v_end); + + RAISE NOTICE 'Запрос с индексом (% строк): % мс', v_count, EXTRACT(EPOCH FROM (v_end - v_start)) * 1000; +END $$; + +-- ============================================================================ +-- ЧАСТЬ 14: НАГРУЗОЧНОЕ ТЕСТИРОВАНИЕ - ПАКЕТНЫЕ ОПЕРАЦИИ +-- ============================================================================ + +-- Тест 13: Пакетная вставка с транзакцией +DO $$ +DECLARE + v_start TIMESTAMP; + v_end TIMESTAMP; + v_pg_version TEXT; + v_method TEXT; + v_rows_count INTEGER := 10000; +BEGIN + v_start := clock_timestamp(); + v_pg_version := current_setting('server_version'); + + IF computed_fields_system.supports_generated_columns() THEN + v_method := 'NATIVE'; + ELSE + v_method := 'TRIGGER'; + END IF; + + RAISE NOTICE 'ТЕСТ 13: Пакетная вставка % строк в транзакции...', v_rows_count; + + FOR i IN 1..10 LOOP + INSERT INTO rooms (room_number, length, width) + SELECT + 'B' || LPAD((50000 + (i-1)*1000 + j)::TEXT, 6, '0'), + (random() * 20)::NUMERIC(10,2) + 2, + (random() * 15)::NUMERIC(10,2) + 2 + FROM generate_series(1, 1000) AS j; + END LOOP; + + v_end := clock_timestamp(); + + INSERT INTO benchmark_results (test_name, pg_version, implementation_method, operation, rows_affected, duration_ms, finished_at) + VALUES ('BATCH INSERT rooms (10x1000)', v_pg_version, v_method, 'BATCH_INSERT', v_rows_count, + EXTRACT(EPOCH FROM (v_end - v_start)) * 1000, v_end); + + RAISE NOTICE 'Пакетная вставка % строк: % мс', v_rows_count, EXTRACT(EPOCH FROM (v_end - v_start)) * 1000; +END $$; + +-- ============================================================================ +-- ЧАСТЬ 15: ИТОГОВЫЙ ОТЧЕТ +-- ============================================================================ + +DO $$ +BEGIN + RAISE NOTICE ''; + RAISE NOTICE '=== НАГРУЗОЧНОЕ ТЕСТИРОВАНИЕ ЗАВЕРШЕНО ==='; + RAISE NOTICE 'Время окончания: %', clock_timestamp(); + RAISE NOTICE ''; +END $$; + +-- Вывод общей статистики +SELECT + '========================================' AS "ИТОГОВЫЙ ОТЧЕТ ТЕСТИРОВАНИЯ" +UNION ALL +SELECT + 'PostgreSQL версия: ' || current_setting('server_version') +UNION ALL +SELECT + 'Метод реализации: ' || + CASE WHEN computed_fields_system.supports_generated_columns() + THEN 'NATIVE (GENERATED COLUMNS)' + ELSE 'TRIGGER' + END; + +-- Детальные результаты тестов +SELECT + test_name AS "Тест", + implementation_method AS "Метод", + operation AS "Операция", + rows_affected AS "Строк", + ROUND(duration_ms, 2) AS "Время (мс)", + TO_CHAR(finished_at, 'HH24:MI:SS.MS') AS "Завершено" +FROM benchmark_results +ORDER BY id; + +-- Сводная статистика по операциям +SELECT + operation AS "Тип операции", + COUNT(*) AS "Количество тестов", + SUM(rows_affected) AS "Всего строк", + ROUND(SUM(duration_ms), 2) AS "Общее время (мс)", + ROUND(AVG(duration_ms), 2) AS "Среднее время (мс)" +FROM benchmark_results +GROUP BY operation +ORDER BY SUM(duration_ms) DESC; + +-- Общее время тестирования +SELECT + 'ОБЩЕЕ ВРЕМЯ ТЕСТИРОВАНИЯ' AS "Метрика", + ROUND(SUM(duration_ms), 2) || ' мс' AS "Значение" +FROM benchmark_results +UNION ALL +SELECT + 'ОБЩЕЕ ВРЕМЯ (секунды)', + ROUND(SUM(duration_ms) / 1000, 2) || ' сек' +FROM benchmark_results +UNION ALL +SELECT + 'ВСЕГО ОБРАБОТАНО СТРОК', + SUM(rows_affected)::TEXT +FROM benchmark_results; + +-- Сравнительная таблица для копирования результатов +SELECT + pg_version AS "Версия PG", + implementation_method AS "Метод", + SUM(CASE WHEN operation = 'BULK_INSERT' THEN duration_ms ELSE 0 END) AS "INSERT (мс)", + SUM(CASE WHEN operation = 'BULK_UPDATE' THEN duration_ms ELSE 0 END) AS "UPDATE (мс)", + SUM(CASE WHEN operation LIKE 'SELECT%' THEN duration_ms ELSE 0 END) AS "SELECT (мс)", + SUM(duration_ms) AS "ВСЕГО (мс)" +FROM benchmark_results +GROUP BY pg_version, implementation_method; + +-- ============================================================================ +-- ЧАСТЬ 16: ПРОВЕРКА КОРРЕКТНОСТИ ВЫЧИСЛЕНИЙ +-- ============================================================================ + +SELECT + '========================================' AS "ПРОВЕРКА КОРРЕКТНОСТИ"; + +-- Проверка order_items +SELECT + 'order_items' AS "Таблица", + COUNT(*) AS "Всего строк", + COUNT(final_price) AS "С final_price", + COUNT(*) - COUNT(final_price) AS "NULL значений" +FROM order_items; + +-- Проверка rooms +SELECT + 'rooms' AS "Таблица", + COUNT(*) AS "Всего строк", + COUNT(total_area) AS "С total_area", + COUNT(*) - COUNT(total_area) AS "NULL значений" +FROM rooms; + +-- Проверка users +SELECT + 'users' AS "Таблица", + COUNT(*) AS "Всего строк", + COUNT(full_name) AS "С full_name", + COUNT(search_string) AS "С search_string" +FROM users; + +-- Проверка users_log +SELECT + 'users_log' AS "Таблица", + COUNT(*) AS "Всего строк", + COUNT(email_extracted) AS "С email_extracted" +FROM users_log; + +-- Примеры данных для визуальной проверки +SELECT 'ПРИМЕРЫ ДАННЫХ order_items:' AS ""; +SELECT id, product_name, quantity, unit_price, discount_percent, tax_rate, final_price +FROM order_items LIMIT 3; + +SELECT 'ПРИМЕРЫ ДАННЫХ rooms:' AS ""; +SELECT id, room_number, length, width, total_area +FROM rooms LIMIT 3; + +SELECT 'ПРИМЕРЫ ДАННЫХ users:' AS ""; +SELECT id, first_name, last_name, full_name, LEFT(search_string, 50) AS search_string_preview +FROM users LIMIT 3; + +SELECT 'ПРИМЕРЫ ДАННЫХ users_log:' AS ""; +SELECT id, email_extracted, payload->>'action' as action +FROM users_log LIMIT 3; + +-- ============================================================================ +-- КОНЕЦ НАГРУЗОЧНОГО ТЕСТИРОВАНИЯ +-- ============================================================================ + +\echo '' +\echo '============================================' +\echo 'ИНСТРУКЦИИ ПО ЗАПУСКУ:' +\echo '============================================' +\echo '' +\echo 'PostgreSQL 11:' +\echo ' psql -h host -U user -d db11 -f load_test_benchmark.sql' +\echo '' +\echo 'PostgreSQL 16:' +\echo ' psql -h host -U user -d db16 -f load_test_benchmark.sql' +\echo '' +\echo 'Для сравнения результатов сохраните вывод:' +\echo ' psql ... -f load_test_benchmark.sql > results_pg11.txt' +\echo ' psql ... -f load_test_benchmark.sql > results_pg16.txt' +\echo '' +\echo 'Или экспортируйте таблицу benchmark_results:' +\echo ' COPY benchmark_results TO ''/tmp/benchmark_pg11.csv'' CSV HEADER;' +\echo '============================================'