База даних, що зберігається, у режимі попереднього перегляду


15

Досить поширеною схемою в застосуванні бази даних, з якою я працюю, є необхідність створення збереженої процедури для звіту або утиліти, яка має "режим попереднього перегляду". Коли така процедура робить оновлення, цей параметр вказує на те, що результати дії повинні бути повернуті, але процедура фактично не повинна виконувати оновлення бази даних.

Один із способів досягти цього - просто написати анкету if заяву для параметра і мати два повних блоки коду; один з них оновлює та повертає дані, а інший просто повертає дані. Але це небажано через дублювання коду та відносно низьку ступінь впевненості, що дані попереднього перегляду насправді є точним відображенням того, що буде з оновленням.

Наступний приклад намагається використовувати точки збереження та змінних транзакцій (на які не впливає транзакцій, на відміну від тимчасових таблиць), щоб використовувати лише один блок коду для режиму попереднього перегляду в якості режиму живого оновлення.

Примітка. Відкликання транзакцій не є варіантом, оскільки цей виклик процедури сам може бути вкладений в транзакцію. Це перевірено на SQL Server 2012.

CREATE TABLE dbo.user_table (a int);
GO

CREATE PROCEDURE [dbo].[PREVIEW_EXAMPLE] (
  @preview char(1) = 'Y'
) AS

CREATE TABLE #dataset_to_return (a int);

BEGIN TRANSACTION; -- preview mode required infrastructure
  DECLARE @output_to_return TABLE (a int);
  SAVE TRANSACTION savepoint;

  -- do stuff here
  INSERT INTO dbo.user_table (a)
    OUTPUT inserted.a INTO @output_to_return (a)
    VALUES (42);

  -- catch preview mode
  IF @preview = 'Y'
    ROLLBACK TRANSACTION savepoint;

  -- save output to temp table if used for return data
  INSERT INTO #dataset_to_return (a)
  SELECT a FROM @output_to_return;
COMMIT TRANSACTION;

SELECT a AS proc_return_data FROM #dataset_to_return;
RETURN 0;
GO

-- Examples
EXEC dbo.PREVIEW_EXAMPLE @preview = 'Y';
SELECT a AS user_table_after_preview_mode FROM user_table;

EXEC dbo.PREVIEW_EXAMPLE @preview = 'N';
SELECT a AS user_table_after_live_mode FROM user_table;

-- Cleanup
DROP TABLE dbo.user_table;
DROP PROCEDURE dbo.PREVIEW_EXAMPLE;
GO

Я шукаю відгуки щодо цього коду та структури дизайну та / або якщо існують інші рішення тієї ж проблеми в різних форматах.

Відповіді:


12

У цього підходу є кілька недоліків:

  1. Термін "попередній перегляд" в більшості випадків може бути досить оманливим, залежно від характеру даних, якими оперують (і що змінюється від операції до операції). Що потрібно забезпечити, щоб поточні дані, якими оперують, перебуватимуть у тому самому стані між часом збирання даних "попереднього перегляду" та коли користувач повернеться через 15 хвилин - після схоплення кави, виходу на вулицю для куріння, прогулянки навколо блоку, повертаючись і перевіряючи щось на eBay - і розуміє, що вони не натискали кнопку "ОК", щоб насправді виконати операцію, і, нарешті, натискають кнопку?

    У вас є обмеження часу на продовження операції після створення попереднього перегляду? Або, можливо, спосіб визначити, що дані перебувають у тому ж стані на час модифікації, як це було у початковий SELECTчас?

  2. Це незначний момент, оскільки приклад коду міг би бути зроблений поспішно і не представляти справжній випадок використання, але навіщо існувати "Попередній перегляд" для INSERTоперації? Це може мати сенс, коли вставляти кілька рядків через щось подібне, INSERT...SELECTі може бути вставлена ​​змінна кількість рядків, але це не має особливого сенсу для однотонної операції.

  3. це небажано через ... відносно низьку ступінь впевненості, що дані попереднього перегляду насправді є точним відображенням того, що буде з оновленням.

    Звідки саме ця «низька ступінь впевненості»? Незважаючи на те, що можна оновити іншу кількість рядків, ніж відображатись у випадку, SELECTколи декілька таблиць приєднані, і в наборі результатів є дублювання рядків, це не повинно бути проблемою. Будь-які рядки, на які має вплинути а, UPDATEможна вибрати самостійно. Якщо є невідповідність, ви робите запит неправильно.

    І ті ситуації, коли є дублювання через таблицю JOINed, яка відповідає декільком рядкам у таблиці, яка буде оновлена, не є ситуаціями, коли буде створено "Попередній перегляд". І якщо є такий випадок, коли це так, то потрібно пояснити користувачеві, що вони оновлюють підмножину звіту, яка повторюється у звіті, щоб не виявлялося помилок, якщо хтось лише дивлячись на кількість уражених рядів.

  4. Для повноти (хоча інші відповіді згадували про це), ви не використовуєте TRY...CATCHконструкцію, тому ви могли легко стикатися з проблемами при вкладенні цих викликів (навіть якщо не використовуєте Save Points і навіть якщо не використовуєте Transaction). Будь ласка, дивіться мою відповідь на наступне запитання, тут, на DBA.SE, для шаблону, який обробляє транзакції через вкладені виклики збереженої процедури:

    Чи потрібно нам обробляти транзакції в коді C #, а також у збереженій процедурі

  5. ВИНАСЛЯ, якщо проблеми, зазначені вище, були враховані, все ще існує критичний недолік: за короткий проміжок часу операція виконується (тобто до початку ROLLBACK), будь-які запити, що читаються брудно (чи запити з використанням WITH (NOLOCK)або SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED), можуть захоплювати дані, які хіба немає хвилини пізніше Хоча кожен, хто використовує запити з читанням, повинен уже знати про це і прийняв таку можливість, такі операції значно збільшують шанси на введення аномалій даних, які дуже важко налагодити (тобто: скільки часу ви хочете витратити на спроби знайти проблему, яка не має явної прямої причини?).

  6. Така модель також погіршує продуктивність системи, збільшуючи блокування, знімаючи більше блокувань, і генеруючи більше активності журналу транзакцій. (Я бачу, що @MartinSmith також згадував ці 2 випуски у коментарі до питання.)

    Крім того, якщо на таблицях, що змінюються, є тригери, це може бути непотрібною додатковою обробкою (читання процесора та фізичного / логічного характеру). Тригери також додатково збільшать шанси на аномалії даних, отримані внаслідок брудного читання.

  7. Зв'язане з пунктом, зазначеним вище - збільшенням блокування - використання транзакції збільшує ймовірність попадання в тупикові місця, особливо якщо задіяні тригери.

  8. Менш гостра проблема, яка має стосуватися лише менш вірогідного сценарію INSERTоперацій: дані "Попередній перегляд" можуть бути не такими, як ті, що вставляються щодо значень стовпців, визначених DEFAULTобмеженнями ( Sequences/ NEWID()/ NEWSEQUENTIALID()) та IDENTITY.

  9. Немає необхідності в додатковому накладному введенні вмісту змінної таблиці у тимчасову таблицю. Це ROLLBACKне вплине на дані в табличній змінній (саме тому ви сказали, що в першу чергу використовуєте табличні змінні), тому було б більше сенсу просто SELECT FROM @output_to_return;в кінці, а потім навіть не турбуватися про створення тимчасової Таблиця.

  10. Про всяк випадок, коли цей нюанс Save Points не відомий (важко сказати з прикладу коду, оскільки він показує лише єдину збережену процедуру): вам потрібно використовувати унікальні імена Save Point, щоб ROLLBACK {save_point_name}операція вела себе так, як ви цього очікували. Якщо ви повторно використовуєте імена, ROLLBACK відкатить останню точку збереження цього імені, яка може бути не на тому ж рівні вкладеності, з ROLLBACKякого викликається. Будь ласка, подивіться перший приклад блоку коду в наступній відповіді, щоб побачити цю поведінку в дії: транзакція у збереженій процедурі

Це зводиться до цього:

  • Робота "Попереднього перегляду" не має великого сенсу для операцій, орієнтованих на користувачів. Я роблю це часто для операцій з технічного обслуговування, щоб я міг побачити, що буде видалено / зібраний сміття, якщо я продовжую операцію. Я додаю необов'язковий параметр, який називається, @TestModeі роблю IFоператор, який або робить a, SELECTколи @TestMode = 1ще це робить DELETE. Я іноді додаю @TestModeпараметр до Збережених процедур, викликаних програмою, щоб я (та інші) могли провести тестування, не впливаючи на стан даних, але цей параметр ніколи не використовується додатком.

  • Про всяк випадок, якщо цього не було зрозуміло з верхнього розділу "питань":

    Якщо вам потрібен / хочете режим "Попередній перегляд" / "Тест", щоб побачити, на що слід вплинути, якщо слід виконати оператор DML, тоді НЕ використовуйте для цього операції (тобто BEGIN TRAN...ROLLBACKшаблон). Це закономірність, яка, в кращому випадку, реально працює лише для однокористувацької системи, і навіть не є хорошою ідеєю в цій ситуації.

  • Повторення основної маси запиту між двома гілками IFвисловлювання справді представляє потенційну проблему необхідності оновлювати їх обидва щоразу, коли є якісь зміни. Однак, відмінності між двома запитами, як правило, досить прості, щоб знайти їх в огляді коду та легко виправити. З іншого боку, такі проблеми, як розбіжність у державі та брудне читання, набагато важче знайти та виправити. І проблему зниження продуктивності системи неможливо виправити. Нам потрібно визнати і прийняти, що SQL не є об'єктно-орієнтованою мовою, і інкапсуляція / зменшення дублюваного коду не була ціллю дизайну SQL, як це було у багатьох інших мов.

    Якщо запит досить довгий / складний, його можна інкапсулювати за допомогою функції вбудованої таблиці. Тоді ви можете зробити простий SELECT * FROM dbo.MyTVF(params);для режиму "Попередній перегляд" та ПРИЄДНАЙТЕСЬ до ключового значення для режиму "зроби це". Наприклад:

    UPDATE tab
    SET    tab.Col2 = tvf.ColB
           ...
    FROM   dbo.Table tab
    INNER JOIN dbo.MyTVF(params) tvf
            ON tvf.ColA = tab.Col1;
  • Якщо це сценарій звіту, як ви згадували, це може бути, тоді запуск початкового звіту - це «Попередній перегляд». Якщо хтось хоче змінити те, що вони бачать у звіті (можливо, стан), це не потребує додаткового попереднього попереднього перегляду, оскільки очікується зміна відображуваних в даний час даних.

    Якщо операція може змінити суму ставки на певний% або бізнес-правило, то це можна вирішити в презентаційному шарі (JavaScript?).

  • Якщо вам дійсно потрібно зробити "Попередній перегляд" для операції , орієнтованої на кінцевого користувача , тоді вам потрібно спочатку зафіксувати стан даних (можливо, хеш усіх полів у наборі результатів для UPDATEоперацій або ключові значення для DELETEоперацій), а потім, перш ніж виконувати операцію, порівняйте інформацію про захоплений стан із поточною інформацією - в межах транзакції, роблячи HOLDблокування на таблиці, щоб нічого не змінилося після цього порівняння - і якщо є ЯКЩО різниця, киньте помилка та ROLLBACKскоріше робити , ніж продовжувати UPDATEабо DELETE.

    Для виявлення відмінностей для UPDATEоперацій альтернативою обчислення хешу у відповідних полях буде додавання стовпця типу ROWVERSION . Значення ROWVERSIONтипу даних автоматично змінюється щоразу, коли відбувається зміна цього рядка. Якби у вас був такий стовпець, ви б додали SELECTйого разом з іншими даними "Попередній перегляд", а потім передаєте їх разом із кроком "обов'язково, продовжуйте і виконайте оновлення" разом із ключовими значеннями та значеннями. змінювати. Потім ви порівнюєте ці ROWVERSIONпередані значення з "Попереднього перегляду" з поточними значеннями (для кожної клавіші) і продовжуєте лише з UPDATEif ВСЕвідповідних значень. Перевага тут полягає в тому, що вам не потрібно обчислювати хеш, який має потенціал, навіть якщо це малоймовірно, для помилкових негативів, і займає деяку кількість часу кожен раз, коли ви робите це SELECT. З іншого боку, ROWVERSIONзначення збільшується автоматично лише при зміні, тому нічого, про що ніколи не потрібно турбуватися. Тим не менш, ROWVERSIONтип - 8 байт, які можна скласти під час роботи з багатьма таблицями та / або багатьма рядками.

    Кожен з цих двох методів для виявлення невідповідних станів, пов'язаних з UPDATEопераціями, має плюси і мінуси , тож вам слід визначити, який метод має більше "pro", ніж "con" для вашої системи. Але в будь-якому випадку ви можете уникнути затримки між створенням попереднього перегляду та виконанням операції, що не спричинить поведінку поза очікуванням кінцевого користувача.

  • Якщо ви працюєте з режимом "Попередній перегляд", орієнтованим на кінцевого користувача, то крім того, щоб фіксувати стан записів у час вибору, проходити разом та перевіряти в час модифікації, включайте DATETIMEдля SelectTimeабо заповнення через GETDATE()або щось подібне. Передайте це разом із рівнем програми, щоб його можна було повернути до збереженої процедури (здебільшого, як єдиний вхідний параметр), щоб його можна було перевірити в Збереженій процедурі. Тоді ви можете визначити, що якщо операція не є режимом "Попередній перегляд", то @SelectTimeзначення повинно бути не більше X хвилин до поточного значення GETDATE(). Може 2 хвилини? 5 хвилин? Швидше за все, не більше 10 хвилин. Введіть помилку, якщо значення DATEDIFFMINUTES перевищує поріг.


4

Найпростіший підхід найчастіше найкращий, і у мене насправді не так багато проблем із дублюванням коду в SQL, особливо не в одному модулі. Адже два запити роблять різні речі. То чому б не взяти "Route 1" або Keep It Simple і просто мати два розділи в збереженому програмі, один для імітації роботи, яку потрібно зробити, і один для її виконання, наприклад щось подібне:

CREATE TABLE dbo.user_table ( rowId INT IDENTITY PRIMARY KEY, a INT NOT NULL, someGuid UNIQUEIDENTIFIER DEFAULT NEWID() );
GO
CREATE PROCEDURE [dbo].[PREVIEW_EXAMPLE2]

    @preview CHAR(1) = 'Y'

AS

    SET NOCOUNT ON

    --!!TODO add error handling

    IF @preview = 'Y'

        -- Simulate INSERT; could be more complex
        SELECT 
            ISNULL( ( SELECT MAX(rowId) FROM dbo.user_table ), 0 ) + 1 AS rowId,
            42 AS a,
            NEWID() AS someGuid

    ELSE

        -- Actually do the INSERT, return inserted values
        INSERT INTO dbo.user_table ( a )
        OUTPUT inserted.rowId, inserted.a, inserted.someGuid
        VALUES ( 42 )

    RETURN

GO

Це має перевагу в тому, що це самодокументування (тобто IF ... ELSEце легко простежити), низька складність (порівняно з точкою збереження із табличним змінним підходом IMO), тому менше ймовірність виникнення помилок (чудове місце від @Cody).

Щодо вашої точки зору на низьку впевненість, я не впевнений, що розумію. Логічно два запити з однаковими критеріями повинні робити те саме. Існує можливість невідповідності кардинальності між a UPDATEі a SELECT, але це буде особливістю ваших приєднань та критеріїв. Чи можете ви пояснити далі?

Як сторону слід встановити NULL/ NOT NULLвластивість та таблиці та змінні таблиці, розглянути можливість встановлення первинного ключа.

Ваш оригінальний підхід здається трохи надскладним, можливо, може бути більш схильним до тупиків, оскільки INSERT/ UPDATE/ DELETEоперації вимагають більш високих рівнів блокування, ніж звичайні SELECTs.

Я підозрюю, що ваші проекти у реальному світі є складнішими, тому, якщо ви вважаєте, що вищезазначений підхід не допоможе їм, відправте ще кілька прикладів.


3

Мої турботи такі.

  • Обробка транзакцій не відповідає стандартній схемі вкладеності в блок Почати / Почати Catch. Якщо це шаблон, то в збереженій процедурі з ще декількома кроками ви можете вийти з цієї транзакції в режимі попереднього перегляду, а дані все ще змінені.

  • Слідом за форматом збільшується робота розробника. Якщо вони змінюють внутрішні стовпці, їм також потрібно змінити визначення змінної таблиці, потім змінити визначення таблиці темп, а потім змінити стовпці вставок в кінці. Це не буде популярним.

  • Деякі збережені процедури не повертають один і той же формат даних кожен раз; уявіть sp_WhoIsActive як загальний приклад.

Я не запропонував кращого способу це зробити, але не думаю, що у вас є хороший зразок.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.