dev.net.ua

Українська Спільнота Розробникiв
 
Ласкаво просимо до dev.net.ua Увійти | Приєднатися | Допомога | Увійти Live ID
в Пошук

usarskyy

Сторінковий перегляд даних

Алоха!

Мабудь у всіх поставала проблема реалізації почастинного перегляду даних, які зберігаються в БД. Стандартний DataGrid робить це настільки криво (моє ІМХО!), що використовувати цю функцію у ньому - майже смертельний гріх! Тому приходиться реалізовувати такий функціонал своїми руками :(

І так, перша ідея, яка приходить в голову (і як показує практика, переходить в реалізацію) буде описана такими пунктами:
1. Вибрати з потрібної нам таблиці PageNum*PageSize записів в якусь тимчасову таблицю
2. Видалити з тимчасової таблиці перші (PageNum - 1)*PageSize записів
3. Те що залишиться, видати як результат сторінки з номером PageNum

Примітка: PageNum - номер сторінки, яку треба показати (нумерація сторінок починається з "1"); PageSize - кількість записів на сторінці

В принципі, це все прекрасно працює... але на невеликих обємах даних! (до речі, 99% тестерів проводять тестування пейджингу на обємах таблиць у 1000-2000 записів, що дає прекрасні часові результати!)

Тепер уявимо собі, що в таблиці є 500 000 записів і нам потрібно вибрати записи з 399 990 по 400 000... Результатом вибірки буде перевищення усіх можливих таймаутів :( Що ж нам робити?! Читати далі...

Реалізація нового алгоритму описується наступними пунктами:
1. Подивитись в Books Online що таке курсори
2. Реалізувати механізм вибірки на їх основі :)

Для тих хто не зрозумів (або кому не хочеться читати мануали) приводжу спрощений приклад:

    DECLARE temp_cursor SCROLL CURSOR FOR    
        select [ID]   
        from [Profile]
        where  [SomeCriteria] = 1
    declare @counter int
    declare @id   int
    OPEN temp_cursor         
    FETCH NEXT FROM temp_cursor    
    into @id           
    set @counter = 0      
    WHILE @@FETCH_STATUS = 0 AND @counter < 10
    BEGIN     
        insert into @temp ( [id] ) values ( @id )          
        set @counter = @counter + 1             
       
        FETCH NEXT FROM temp_cursor     
        into @id    
    END       
    CLOSE temp_cursor   
    DEALLOCATE temp_cursor     

    select    mp.[ID],     
                mp.[LoginName]
    from [Profile] as mp    
    where mp.[ID] in ( select [id] from @temp )


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


UPDATE (27.11.2007)

2 kosinsky:

1. До цього часу ніяких проблем з курсорами не виникало... і суттєве зниження швидкодії теж не спостерігалось (можливо, через специфіку БД) + кращого рішення я поки що не бачив (можливо, тому що я не "DB guy" :) ).
2. ДУЖЕ дякую за приведений приклад! Я знав, що MS має щось на зразок LIMIT в MySQL, але не знав як воно називається (або як грамотно реалізовується) :(
3. А Ви впевнені що синтаксис правильний для MS SQL Server 2005? Я так і не зміг заставити його працювати (я дуже старався! чесно! :) )
4. Нажаль, з третім дотнетом розібратись зможу не скоро... як завжди бракує часу. Проте як тільки випаде можливість - обовязково зверну увагу на LinqDataSource і DataPager

2 Mike Chaliy:

1. Я не помилився коли сказав DataGrid
2. Подивіться що SqlDataSource має "віддати" DataGrid-у, щоб пейджинг запрацював
3. Сказати "гівно" - це просто, а Ви спробуйте запропонувати свій варіант рішення проблеми :) як це зробив kosinsky

Тепер результати тестування.

Умови: 1) MS SQL Server Express Edition 2) 319854 строка в таблиці з якої робиться вибірка 3) результатом where-умови було 317851 строка 4) вибирались строки з 100 000 по 100 020 5) активність роботи з базою на час проведення експерименту == ~"0" 6) для справедливості експерименту запити запускались по 15 разів :)

Так як заставити спрацювати конструкцію товариша kosinsk-ого я не зміг, то прийшлось її модифікувати: внутрішній селект робився в тимчасову таблицю в памяті, а зовнішній робився з цієї таблиці.
Структура тимчасової таблиці: int, nvarchar(128), nvarchar(128), int

Результатом роботи алгоритму на курсорах стали 3860 мілісекунд, для row_number() - 27640 :(

По аналогії з LIMIT в MySQL я очікував побачити кардинально іншу картину, але факт залишається фактом!

На закінчення, я б попросив kosinsky провести експеримент на власній базі :) І якщо це можливо, виконати це все на MS SQL Server 2008

P.S.: В принципі, я припускаю, що виконання запиту на основі row_number() в такому вигляді як він був поданий в коментарі kosinsky може дати набагато кращі результати...


Опубліковані Monday, November 26, 2007 2:01 AM від usarskyy
Помічено як:

Коментарі

 

kosinsky сказав:

Курсоры - это самая плохая практика для базы данных. Т.к. они есть не привычное для нее поведение, что приводит к черезмерной загрузке базы данных (лишние чтения, блокировки и т.д.).

Для SQL Server 2005  и выше есть более элегантное решение.

SELECT * FROM

(

 SELECT *,

  row_number() over(order by <нужное поле для сортировки>) as num

 FROM MyTable

)

WHERE NUM  between <начало страницы> and <конец страницы>

Можно еще оптимизироваться заменив between на одно сравнение на "больше" и TOP.

PS. Также стоит посмотреть как ведет себя комплект из LinqDataSource и DataPager в .NET 3.5

November 25, 2007 10:20 PM
 

kosinsky сказав:

PPS. Буду рад увидеть сравнение разницы во времени предлеженного и моего варианта, а Ваших объемах данных

November 25, 2007 10:25 PM
 

Mike Chaliy сказав:

Стандартний DataGrid - нічого не робить, робить SqlDataSource.

ІМХО, що перший, що другий приклад це крейзерство.

November 26, 2007 12:40 AM
 

usarskyy сказав:

Відповіді на коментарі та результати тестування дивіться під текстом "UPDATE"

November 26, 2007 2:24 PM
 

Mike Chaliy сказав:

1) DataGrid - так вибачай, я мав на увазі GridView;

2) Незрозумів;

3) Пошук - http://search.live.com/results.aspx?srch=105&FORM=IE7RE&q=Paging+SQL+2005, усі перші сторінки розповідають про констукцію з WITH.

Чому я написав про крейзерство:

1) Рішення зі створенням додаткових таблиць це може бути і секуріті діркою, і занадто складним для підтримки.

2) Рішення з курсорами, просто занадто складним, також цікаво порівняти зі старндарними рішеннями за швидкістю та ресурсами.

Я зараз пробую встановити SQL 2008 November CTP - покищо нічого не вдається.

November 27, 2007 7:03 AM
 

Mike Chaliy сказав:

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

November 27, 2007 7:05 AM
 

LR___ сказав:

> Результатом роботи алгоритму на курсорах стали 3860 мілісекунд, для row_number() - 27640 :(

Будь ласка, опублікуйте скрипти тестування (створення, наповнення таблиць та запити), є підозра щодо оптимальності скриптів з row_number() :)

November 28, 2007 1:12 AM
 

kosinsky сказав:

Упс я пропустив AS T після вкладеного запиту без нього не працує.

Ось правильний синтаксис (використовув стандатну базу AdventureWorks):

select *

FROM

(

select *

, row_number() over(Order by SalesOrderID ) as num

from Sales.SalesOrderDetail

) as T

where num between 100000 and 100020

November 28, 2007 1:33 AM
 

kosinsky сказав:

Сделал полную проверку на 1млн. записей. У меня SQL Server 2005 Developer Edition

Скрипт ниже:

create database paging

go

use paging

go

--Создаем таблицу

create table Profile

(

id int identity primary key,

LoginName nvarchar(100),

EMail nvarchar(100),

)

go

--Наполняем ее милионом записей всякого мусора

declare @i int

set @i = 0

while @i<1000000

begin

insert into Profile values(newid(), newid())

set @i=@i+1

end

--Вариант на row_number()

select getdate()

select * from

(

select *

, row_number() over(order by id) as num

from Profile

) as T

where num between 100000 and 100020

select getdate()

--Вариант на курсорах

select getdate()

declare @temp table(id int)

DECLARE temp_cursor SCROLL CURSOR FOR    

       select [ID]    

       from [Profile]

   declare @counter int

   declare @id   int

   OPEN temp_cursor          

   FETCH NEXT FROM temp_cursor    

   into @id            

   set @counter = 0      

   WHILE @@FETCH_STATUS = 0 AND @counter < 20

   BEGIN      

       insert into @temp ( [id] ) values ( @id )          

       set @counter = @counter + 1              

       FETCH NEXT FROM temp_cursor      

       into @id    

   END        

   CLOSE temp_cursor    

   DEALLOCATE temp_cursor      

   select    mp.[ID],      

               mp.[LoginName]

   from [Profile] as mp    

   where mp.[ID] in ( select [id] from @temp )

select getdate()

Для row_number() около 80 милисекунд, для курсора 5500.

ЗЫ. Фича моего метода проявляется только с вложеным запросом. С временной таблицей, это плохой вариант

November 28, 2007 2:01 AM
 

usarskyy сказав:

kosinsky, ви мене випередили :) я вчора провів тест на своїй БД (час виконання: 723 мілісекунди), але пиво відбило будь-яке бажання писати коментар)))

Про "... as SomeTableName" в запиті дізнався з лінка який дав Mike Chaliy (за що йому велике дякую!), а далі - справа техніки :)

2 LR___ : єдина неоптимальність в тому скрипті - це тимчасова таблиця. Цієї таблиці взагалі не має бути, просто я змішений був модифікувати запит на основі row_number() щоб хоч якось провести тест (див. апдейт до мого поста)

November 28, 2007 11:41 AM
 

usarskyy сказав:

2 Mike Chaliy:

1. Порівняння рішень за швидкістю провели :) Див коменти

2. Якщо під "стандартним" мається на увазі фішка з селектом в тимчасову таблицю і подальшим видаленням N-ї кількості запитів - то такий пейджинг покаже 2-3 хвилинний час (як мінімум) для вибірки з 100000 по 100020-й запис

November 28, 2007 11:51 AM
Анонімні коментарі деактивовані. Увійдіть або Зареєструйтесь щоб мати доступ до ресурсів Спільноти.