Database
 sql >> база данни >  >> RDS >> Database

Лоши навици:Преброяване на редовете по трудния начин

[Вижте индекс на всички публикации за лоши навици/най-добри практики]

Един от слайдовете в моята повтаряща се презентация за лошите навици и най-добри практики е озаглавен „Злоупотреба с COUNT(*) ." Виждам тази злоупотреба доста в природата и приема няколко форми.

Колко реда в таблицата?

Обикновено виждам това:

SELECT @count = COUNT(*) FROM dbo.tablename;

SQL Server трябва да изпълни блокиращо сканиране срещу цялата таблица, за да изведе този брой. Това е скъпо. Тази информация се съхранява в изгледите на каталога и DMV и можете да я получите без целия този I/O или блокиране:

SELECT @count = SUM(p.rows)
  FROM sys.partitions AS p
  INNER JOIN sys.tables AS t
  ON p.[object_id] = t.[object_id]
  INNER JOIN sys.schemas AS s
  ON t.[schema_id] = s.[schema_id]
  WHERE p.index_id IN (0,1) -- heap or clustered index
  AND t.name = N'tablename'
  AND s.name = N'dbo';

(Можете да получите същата информация от sys.dm_db_partition_stats , но в този случай променете p.rows до p.row_count (уау последователност!). Всъщност това е същият изглед като sp_spaceused използва за извличане на броя – и въпреки че е много по-лесно за въвеждане от горната заявка, препоръчвам да не го използвате само за извеждане на брой поради всички допълнителни изчисления, които прави – освен ако не искате и тази информация. Също така имайте предвид, че той използва функции за метаданни, които не се подчиняват на вашето външно ниво на изолация, така че може да изчакате блокирането, когато извикате тази процедура.)

Вярно е, че тези изгледи не са 100%, точни до микросекунда. Освен ако не използвате хеп, по-надежден резултат може да се получи от sys.dm_db_index_physical_stats() колона record_count (отново последователност!), но тази функция може да окаже влияние върху производителността, все още може да блокира и може да бъде дори по-скъпа от SELECT COUNT(*) – трябва да извършва същите физически операции, но трябва да изчисли допълнителна информация в зависимост от mode (като фрагментиране, което не ви интересува в този случай). Предупреждението в документацията разказва част от историята, уместна, ако използвате Групи за наличност (и вероятно засяга огледалото на база данни по подобен начин):

Ако заявите sys.dm_db_index_physical_stats на сървърен екземпляр, който хоства AlwaysOn четима вторична реплика, може да срещнете проблем с блокирането на REDO. Това е така, защото този динамичен изглед за управление придобива IS заключване на определена потребителска таблица или изглед, който може да блокира заявки от REDO нишка за X заключване на тази потребителска таблица или изглед.

Документацията също така обяснява защо този номер може да не е надежден за купчина (и също така им дава квази-пропуск за редовете спрямо несъответствието на записите):

За хийп броят на записите, върнати от тази функция, може да не съвпада с броя на редовете, които се връщат чрез изпълнение на SELECT COUNT(*) срещу купчината. Това е така, защото един ред може да съдържа множество записи. Например, при някои ситуации на актуализиране, един ред на купчина може да има запис за препращане и препратен запис в резултат на операцията за актуализиране. Освен това повечето големи LOB редове са разделени на множество записи в LOB_DATA съхранение.

Така че бих се наклонил към sys.partitions като начин за оптимизиране на това, жертвайки малко пределна точност.

    „Но аз не мога да използвам DMV; броят ми трябва да е супер точен!“

    "Супер точно" броене всъщност е доста безсмислено. Нека помислим, че единствената ви опция за „супер точен“ брой е да заключите цялата таблица и да забраните на всеки да добавя или изтрива редове (но без да предотвратявате споделените четения), напр.:

    SELECT @count = COUNT(*) FROM dbo.table_name WITH (TABLOCK); -- not TABLOCKX!

    И така, вашата заявка върви, сканирайки всички данни, работейки към този „перфектен“ брой. Междувременно заявките за запис се блокират и чакат. Изведнъж, когато точният ви брой бъде върнат, вашите заключвания на масата се освобождават и всички онези заявки за писане, които са били на опашка и чакат, започват да изстрелват всякакви видове вмъквания, актуализации и изтривания срещу вашата таблица. Колко "супер точен" е вашият брой сега? Струва ли си да получите „точно“ преброяване, което вече е ужасно остаряло? Ако системата не е заета, това не е толкова голям проблем – но ако системата не е заета, бих твърдял доста категорично, че DMV ще бъдат дяволски точни.

    Бихте могли да използвате NOLOCK вместо това, но това просто означава, че писателите могат да променят данните, докато ги четете, и води до други проблеми също (говорих за това наскоро). Това е добре за много игрища, но не и ако целта ви е точност. DMV ще бъдат точно (или поне много по-близо) в много сценарии и по-далеч в много малко (всъщност нито един, за който се сещам).

    И накрая, можете да използвате Read Committed Snapshot Isolation. Kendra Little има фантастична публикация за нивата на изолация на моментни снимки, но ще повторя списъка с предупреждения, които споменах в моя NOLOCK статия:

    • Sch-S ключалките все още трябва да се вземат дори под RCSI.
    • Нивата на изолация на моментни снимки използват версия на редове в tempdb, така че наистина трябва да тествате въздействието там.
    • RCSI не може да използва ефективни сканирания на поръчката за разпределение; вместо това ще видите сканирания обхват.
    • Пол Уайт (@SQL_Kiwi) има няколко страхотни публикации, които трябва да прочетете за тези нива на изолация:
      • Прочетете изолацията на завършена моментна снимка
      • Промени на данните при изолиране на завършена моментна снимка за четене
      • Нивото на изолация на МОМЕНТАЛНА СНИМКА

    Освен това, дори с RCSI получаването на "точния" брой отнема време (и допълнителни ресурси в tempdb). Докато операцията приключи, броят все още ли е точен? Само ако междувременно никой не е докоснал масата. Така че едно от предимствата на RCSI (четците не блокират писателите) се губи.

Колко реда съответстват на клауза WHERE?

Това е малко по-различен сценарий – трябва да знаете колко реда съществуват за определено подмножество на таблицата. Не можете да използвате DMV за това, освен ако WHERE клаузата съвпада с филтриран индекс или покрива напълно точен дял (или множествен).

Ако вашият WHERE клаузата е динамична, можете да използвате RCSI, както е описано по-горе.

Ако вашият WHERE Клаузата не е динамична, можете да използвате и RCSI, но можете също да обмислите една от тези опции:

  • Филтриран индекс – например ако имате прост филтър като is_active = 1 или status < 5 , тогава бихте могли да създадете индекс по следния начин:
    CREATE INDEX ix_f ON dbo.table_name(leading_pk_column) WHERE is_active = 1;

    Сега можете да получите доста точни преброявания от DMV, тъй като ще има записи, представляващи този индекс (просто трябва да идентифицирате index_id, вместо да разчитате на heap(0)/clustered index(1)). Все пак трябва да вземете предвид някои от слабостите на филтрираните индекси.

  • Индексиран изглед - например ако често броите поръчки по клиент, индексиран изглед може да ви помогне (въпреки че, моля, не приемайте това като общо одобрение, че „индексираните изгледи подобряват всички заявки!“):
    CREATE VIEW dbo.view_name
    WITH SCHEMABINDING
    AS
      SELECT 
        customer_id, 
        customer_count = COUNT_BIG(*)
      FROM dbo.table_name
      GROUP BY customer_id;
    GO
     
    CREATE UNIQUE CLUSTERED INDEX ix_v ON dbo.view_name(customer_id);

    Сега данните в изгледа ще бъдат материализирани и броят е гарантирано синхронизиран с данните от таблицата (има няколко неясни грешки, при които това не е вярно, като този с MERGE , но като цяло това е надеждно). Така че сега можете да получите своите бройки на клиент (или за набор от клиенти), като направите заявка за изгледа на много по-ниска цена на заявката (1 или 2 четения):

    SELECT customer_count FROM dbo.view_name WHERE customer_id = <x>;

    Няма безплатен обяд обаче . Трябва да вземете предвид режийните разходи за поддържане на индексиран изглед и влиянието, което ще има върху частта за запис от вашето работно натоварване. Ако не изпълнявате този тип заявка много често, едва ли ще си струва труда.

Поне един ред съвпада ли с клауза WHERE?

Това също е малко по-различен въпрос. Но често виждам това:

IF (SELECT COUNT(*) FROM dbo.table_name WHERE <some clause>) > 0 -- or = 0 for not exists

Тъй като очевидно не ви пука за действителния брой, интересува ви само дали съществува поне един ред, наистина мисля, че трябва да го промените на следното:

IF EXISTS (SELECT 1 FROM dbo.table_name WHERE <some clause>)

Това поне има шанс за късо съединение, преди да бъде достигнат краят на таблицата, и почти винаги ще превъзхожда COUNT вариация (въпреки че има някои случаи, когато SQL Server е достатъчно умен, за да преобразува IF (SELECT COUNT...) > 0 към по-прост IF EXISTS() ). В абсолютно най-лошия случай, когато не бъде намерен ред (или първият ред е намерен на последната страница в сканирането), производителността ще бъде същата.

[Вижте индекс на всички публикации за лоши навици/най-добри практики]


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Как да замените част от низ в T-SQL

  2. Намаляване на разходите за хостинг на вашата база данни:DigitalOcean срещу AWS срещу Azure

  3. ML{.NET} Въведение

  4. Модел на база данни за система за съобщения

  5. Минимално регистриране с INSERT...SELECT в Heap Tables