Повернення IEbrobro <T> vs. IQueryable <T>


1084

Яка різниця між поверненням IQueryable<T>проти IEnumerable<T>, коли слід віддати перевагу іншому?

IQueryable<Customer> custs = from c in db.Customers
where c.City == "<City>"
select c;

IEnumerable<Customer> custs = from c in db.Customers
where c.City == "<City>"
select c;

Відповіді:


1778

Так, обидва дадуть вам відкладене виконання .

Різниця полягає в тому, що IQueryable<T>це інтерфейс, який дозволяє LINQ-SQL (LINQ.-до-що-небудь дійсно) працювати. Тож якщо ви додатково вдосконалите свій запит на IQueryable<T>, цей запит буде виконуватися в базі даних, якщо це можливо.

У цьому IEnumerable<T>випадку це буде LINQ до об'єкта, тобто всі об'єкти, що відповідають оригінальному запиту, повинні бути завантажені в пам'ять з бази даних.

У коді:

IQueryable<Customer> custs = ...;
// Later on...
var goldCustomers = custs.Where(c => c.IsGold);

Цей код виконуватиме SQL лише для вибору золотих клієнтів. Наступний код, з іншого боку, виконає оригінальний запит у базі даних, а потім відфільтрує не золотих клієнтів у пам'яті:

IEnumerable<Customer> custs = ...;
// Later on...
var goldCustomers = custs.Where(c => c.IsGold);

Це досить важлива відмінність, і робота над цим IQueryable<T>може у багатьох випадках врятувати вас від повернення занадто багато рядків із бази даних. Ще одним головним прикладом є підказка: якщо ви користуєтесь Takeі Skipвмикаєте IQueryable, ви отримаєте лише кількість запитуваних рядків; якщо це робити за IEnumerable<T>заповітом, всі ваші рядки будуть завантажені в пам'ять.


32
Чудове пояснення. Чи існують ситуації, коли IEnumerable було б кращим перед IQueryable?
fjxx

8
Таким чином, ми можемо сказати, що якщо ми використовуємо IQueryable для запиту об’єкта пам'яті, то вони не будуть різниці між IEnumerable та IQueryable?
Тарік

11
ПОПЕРЕДЖЕННЯ. Хоча IQueryable може бути спокусливим рішенням із-за заявленої оптимізації, він не повинен пропускати повз сховище чи рівень обслуговування. Це для захисту вашої бази даних від накладних витрат, викликаних "укладанням виразів LINQ".
Yorro

48
@fjxx Так. Якщо ви хочете повторної фільтрації за початковим результатом (кілька кінцевих результатів). Зробивши це в інтерфейсі IQueryable, зробимо декілька зворотних переходів до бази даних, де, як зробити це на IEnumerable, буде фільтрувати в пам'яті, роблячи її швидше (за винятком випадків, коли обсяг даних є ВЕЛИЧЕЗНИМ)
Per Hornshøj-Schierbeck

34
Ще одна причина, яку слід віддавати перевагу, полягає IEnumerableв IQueryableтому, що не всі операції LINQ підтримуються всіма постачальниками LINQ. Отже, поки ви знаєте, що ви робите, ви можете використовувати IQueryableдля того, щоб надіслати якомога більше запитів постачальнику LINQ (LINQ2SQL, EF, NHibernate, MongoDB тощо). Але якщо ви дозволите іншому коду робити все, що завгодно з вашим, IQueryableви врешті-решт опинитесь у біді, тому що десь код клієнта десь використовував непідтримувану операцію. Я погоджуюся з рекомендацією не випускати IQueryables "в дику" повз сховище або еквівалентний шар.
Авіш

302

Верхня відповідь хороша, але в ній не згадуються дерева виразів, які пояснюють "як" два інтерфейси. В основному, є два однакових набору розширень LINQ. Where(), Sum(), Count(), FirstOrDefault()І т.д. все мають дві версії: одна , яка приймає функції і один , який приймає вираження.

  • IEnumerableВерсія підписи:Where(Func<Customer, bool> predicate)

  • IQueryableВерсія підписи:Where(Expression<Func<Customer, bool>> predicate)

Ви, ймовірно, використовували обидва з них, не усвідомлюючи це, оскільки обидва називаються з використанням однакового синтаксису:

наприклад, Where(x => x.City == "<City>")працює і на, IEnumerableі наIQueryable

  • При використанні Where()на IEnumerableколекції, компілятор передає скомпільовану функціюWhere()

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

Навіщо турбуватися з цією річчю дерева виразів? Я просто хочу Where()відфільтрувати свої дані. Основна причина полягає в тому, що як EF, так і Linq2SQL ORM можуть перетворювати дерева виразів безпосередньо в SQL, де ваш код буде виконуватися набагато швидше.

О, це звучить як безкоштовне підвищення продуктивності, я повинен використовувати AsQueryable()в цьому випадку всюди? Ні, IQueryableкорисний лише в тому випадку, якщо основний постачальник даних може щось зробити з цим. Перетворення що - щось на зразок звичайного ListTo IQueryableне дасть вам ніякої користі.


9
ІМО, це краще, ніж прийнята відповідь. Однак я не розумію одного: IQueryable не дає користі для звичайних об'єктів, гаразд, але чи гірше це в будь-якому випадку? Тому що якщо це просто не дає жодних переваг, недостатньо причин віддавати перевагу IEnumerable, тому ідея використання IQueryable у всьому місці залишається дійсною.
Сергій Таченов

1
Сергій, IQueryable розширює IErumerable, тому при використанні IQueryable ви завантажуєте більше в пам'ять, ніж IEnumerable екземпляр! Тож ось аргумент. ( stackoverflow.com/questions/12064828/… c ++, хоча я думав, що можу це екстраполювати)
Вікінг

Погодьтеся з Сергієм, що це найкраща відповідь (хоча прийнята відповідь - це добре). Я хотів би додати , що в моєму досвіді, IQueryableне аналізує функції, а також IEnumerableробить: наприклад, якщо ви хочете знати , які елементи DBSet<BookEntity>не в List<BookObject>, dbSetObject.Where(e => !listObject.Any(o => o.bookEntitySource == e))кидає виняток: Expression of type 'BookEntity' cannot be used for parameter of type 'BookObject' of method 'Boolean Contains[BookObject] (IEnumerable[BookObject], BookObject)'. Мені довелося додати .ToList()після dbSetObject.
Жан-Девід Ланц

80

Так, обидва використовують відкладене виконання. Проілюструємо різницю за допомогою профілера SQL Server ....

Коли ми запускаємо наступний код:

MarketDevEntities db = new MarketDevEntities();

IEnumerable<WebLog> first = db.WebLogs;
var second = first.Where(c => c.DurationSeconds > 10);
var third = second.Where(c => c.WebLogID > 100);
var result = third.Where(c => c.EmailAddress.Length > 11);

Console.Write(result.First().UserName);

У профіле SQL Server ми знаходимо команду, рівну:

"SELECT * FROM [dbo].[WebLog]"

Для запуску цього блоку коду до таблиці WebLog, що містить 1 мільйон записів, потрібно приблизно 90 секунд.

Отже, всі записи таблиці завантажуються в пам'ять як об'єкти, а потім з кожним .Where () це буде ще один фільтр в пам'яті проти цих об'єктів.

Коли ми використовуємо IQueryableзамість IEnumerableнаведеного вище прикладу (другий рядок):

У профіле SQL Server ми знаходимо команду, рівну:

"SELECT TOP 1 * FROM [dbo].[WebLog] WHERE [DurationSeconds] > 10 AND [WebLogID] > 100 AND LEN([EmailAddress]) > 11"

Для запуску цього блоку коду за допомогою приблизно чотирьох секунд IQueryable.

IQueryable має властивість, яку називає Expressionдеревним виразом, який починає створюватися, коли ми використовували resultнаш приклад (який називається відкладеним виконанням), і в кінці це вираз буде перетворено на запит SQL для запуску в двигун бази даних.


5
Це вчить мене при передачі в IEnumerable, основна IQueryable втрачає метод IQueryable розширення.
Іпін

56

Обидва дадуть вам відкладене виконання, так.

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

Повернення IEnumerableавтоматично примусить час виконання використовувати LINQ для об'єктів для запиту вашої колекції.

Повернення IQueryable(яке IEnumerable, до речі, реалізує) забезпечує додаткову функціональність для перекладу вашого запиту в те, що може працювати краще в базовому джерелі (LINQ в SQL, LINQ в XML тощо).


30

Загалом я рекомендую наступне:

  • Поверніться, IQueryable<T>якщо ви хочете включити розробника, використовуючи ваш метод, уточнити запит, який ви повернете, перед виконанням.

  • Поверніться, IEnumerableякщо ви хочете перевезти набір Об'єктів для перерахування.

Уявіть собі IQueryable, що це таке - "запит" для даних (який ви можете уточнити, якщо хочете). А IEnumerable- це набір об'єктів (які вже отримані або створені), над якими можна перерахувати.


2
"Можна перерахувати," не "можна IEnumerable."
Кейсі

28

Багато було сказано раніше, але повернемось до коріння більш технічним способом:

  1. IEnumerable це сукупність об'єктів в пам'яті, які ви можете перерахувати - послідовність в пам'яті, яка дозволяє повторити процес (полегшує проходження в foreachциклі, хоча ви можете перейти IEnumeratorлише з ним). Вони залишаються в пам'яті як є.
  2. IQueryable це дерево виразів, яке в якийсь момент буде перетворене на щось інше з можливістю перерахувати над кінцевим результатом . Я думаю, саме це бентежить більшість людей.

Вони, очевидно, мають різні конотації.

IQueryableявляє собою дерево виразів (запит, просто), яке буде переведено на щось інше, що лежить в основі постачальника запитів, як тільки викликаються API випуску, наприклад, функції сукупності LINQ (сума, підрахунок тощо) або ToList [Array, Dictionary ,. ..]. І IQueryableоб'єкти також реалізують IEnumerable, IEnumerable<T>так що якщо вони представляють запит, результат цього запиту може бути повторений. Це означає, що IQueryable не повинен бути лише запитом. Правильний термін - це дерева виразів .

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

У світі Entity Framework (це означає, що містичний базовий постачальник джерел даних або постачальник запитів) IQueryableвирази переводяться на рідні T-SQL запити. Nhibernateробить подібні речі з ними. Ви можете написати свій власний, керуючись поняттями, досить добре описаними в LINQ: Наприклад, створення посилання постачальника послуг IQueryable , і ви, можливо, захочете мати спеціальний API запиту для служби постачальника товарів.

Отже, IQueryable об'єкти будуються протягом усього часу, поки ми явно не звільнимо їх і не скажемо системі переписати їх у SQL або що завгодно і відправити ланцюжок виконання для подальшої обробки.

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

Правильне використання обох сильно залежить від завдань, які стоять перед вами у конкретному випадку. Для відомого шаблону репозиторію я особисто вибираю повернення IList, тобто IEnumerableнад списками (індексатори тощо). Тож IQueryableрадимо використовувати лише в сховищах і IE чимало в іншому місці коду. Не кажучи про проблеми, пов'язані з встановленням тестів, які IQueryableруйнуються та руйнують принцип поділу проблем . Якщо ви повернете вираз із сховищ, споживачі можуть грати зі стійким шаром, як хотіли б.

Невелике доповнення до безладу :) (з дискусії в коментарях)) Жоден з них не є об’єктами в пам'яті, оскільки вони самі по собі не є реальними типами, вони є маркерами типу - якщо ви хочете заглибитись так глибоко. Але має сенс (і тому навіть MSDN так вважає) думати про IEnumerables як колекції пам'яті, тоді як IQueryables - як дерева виразів. Справа в тому, що інтерфейс IQueryable успадковує інтерфейс IEnumerable, щоб, якщо він представляє запит, результати цього запиту можна було перерахувати. Перерахування спричиняє виконання експресійного дерева, пов'язаного з об'єктом IQueryable. Отже, насправді ви не можете дійсно зателефонувати жодному члену IEnumerable, не маючи об’єкта в пам'яті. Він потрапить туди, якщо ви все одно, якщо він не порожній. IQueryables - це лише запити, а не дані.


3
Коментар, що IEnumrables завжди в пам'яті, не обов'язково правдивий. Інтерфейс IQueryable реалізує інтерфейс IEnumerable. Через це ви можете передати необроблений IQueryable, який представляє запит LINQ-SQL прямо у подання, яке очікує IEnumerable! Ви можете бути здивовані, коли ваш контекст даних закінчився або ви стикаєтеся з проблемами з MARS (декілька активних наборів результатів).

тому насправді ви не можете дійсно зателефонувати жодному члену IEnumerable, не маючи об’єкта в пам'яті. Він потрапить туди, якщо ви все одно, якщо він не порожній. IQueryables - це лише запити, а не дані. Але я дійсно бачу вашу думку. Я додам коментар до цього.
Арман Макгітарін

@AlexanderPritchard жоден з них не є об’єктами в пам'яті, оскільки вони самі по собі не є реальними типами, вони є маркерами типу - якщо ви хочете заглибитись так глибоко. Але має сенс (і тому навіть MSDN так вважає) думати про IEnumerables як колекції в пам'яті, тоді як IQueryables - як дерева виразів. Справа в тому, що інтерфейс IQueryable успадковує інтерфейс IEnumerable, щоб, якщо він представляє запит, результати цього запиту можна було перерахувати. Перерахування спричиняє виконання експресійного дерева, пов'язаного з об'єктом IQueryable.
Арман Макгітарін

24

Загалом ви хочете зберегти початковий статичний тип запиту, поки це не має значення.

З цієї причини, ви можете визначити змінну як «вар» замість або IQueryable<>або , IEnumerable<>і ви будете знати , що ви не змінити тип.

Якщо ви починаєте з програми IQueryable<>, ви, як правило, хочете зберегти його як до IQueryable<>тих пір, поки не з’явиться певна вагома причина змінити його. Причиною цього є те, що ви хочете дати процесору запитів якомога більше інформації. Наприклад, якщо ви збираєтесь використовувати лише 10 результатів (ви вже викликали Take(10)), ви хочете, щоб про це знав SQL Server, щоб він міг оптимізувати свої плани запитів і надсилати вам лише ті дані, які ви будете використовувати.

Переконливою причиною зміни типу з IQueryable<>на IEnumerable<>могло бути те, що ви викликаєте деяку функцію розширення, реалізація якої IQueryable<>у вашому конкретному об'єкті не може обробляти або обробляє неефективно. У такому випадку ви можете перетворити тип у IEnumerable<>(шляхом присвоєння змінної типу IEnumerable<>або, наприклад, за допомогою AsEnumerableметоду розширення), щоб функції розширення, які ви викликаєте, були тими в Enumerableкласі замість Queryableкласу.


18

Існує повідомлення в блозі з коротким зразком вихідного коду про те, як неправильне використання IEnumerable<T>може суттєво вплинути на ефективність запитів LINQ: Entity Framework: IQueryable vs. IEnumerable .

Якщо ми копаємося глибше і дивимось на джерела, ми можемо побачити, що очевидно існують різні методи розширення IEnumerable<T>:

// Type: System.Linq.Enumerable
// Assembly: System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
// Assembly location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Core.dll
public static class Enumerable
{
    public static IEnumerable<TSource> Where<TSource>(
        this IEnumerable<TSource> source, 
        Func<TSource, bool> predicate)
    {
        return (IEnumerable<TSource>) 
            new Enumerable.WhereEnumerableIterator<TSource>(source, predicate);
    }
}

і IQueryable<T>:

// Type: System.Linq.Queryable
// Assembly: System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
// Assembly location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Core.dll
public static class Queryable
{
    public static IQueryable<TSource> Where<TSource>(
        this IQueryable<TSource> source, 
        Expression<Func<TSource, bool>> predicate)
    {
        return source.Provider.CreateQuery<TSource>(
            Expression.Call(
                null, 
                ((MethodInfo) MethodBase.GetCurrentMethod()).MakeGenericMethod(
                    new Type[] { typeof(TSource) }), 
                    new Expression[] 
                        { source.Expression, Expression.Quote(predicate) }));
    }
}

Перший повертає чисельний ітератор, а другий створює запит через постачальника запитів, вказаний у IQueryableджерелі.


11

Нещодавно я зіткнувся з проблемою з IEnumerablev IQueryable. Використовуваний алгоритм спочатку виконував IQueryableзапит для отримання набору результатів. Потім вони були передані в foreachцикл з елементами, створеними як клас Entity Framework (EF). Цей клас EF потім використовувався в fromпункті запиту Linq to Entity, в результаті чого результат був IEnumerable.

Я досить новачок в EF та Linq для Entities, тому знадобився певний час, щоб зрозуміти, що таке вузьке місце. Використовуючи MiniProfiling, я знайшов запит, а потім перетворив усі окремі операції в один IQueryableзапит Linq for Entities. На виконання було IEnumerableпотрібно 15 секунд, а на виконання - IQueryable0,5 секунди. Було задіяно три таблиці, і, прочитавши це, я вважаю, що IEnumerableзапит насправді формував перехресний продукт із трьох таблиць і фільтрував результати.

Постарайтеся використовувати IQueryables як основне правило та профіліруйте свою роботу, щоб зміни були вимірними.


Причина полягала в тому, що вирази IQueryable перетворюються на власний SQL в EF та виконуються прямо в БД, тоді як списки IEnumerable є об'єктами пам'яті. Вони отримують з БД в якийсь момент, коли ви викликаєте сукупні функції, такі як Count, Sum, або будь-який To ... і працюють в пам'яті. IQueryables також застрягли в пам'яті після того, як ви викликали один з цих API, але якщо ні, ви можете передавати вираз до стеку шарів і грати з фільтрами до виклику API. Добре розроблений DAL як добре розроблений сховище вирішить подібні проблеми;)
Арман Макгітарін

10

Я хотів би уточнити кілька речей через, здавалося б, суперечливі відповіді (в основному оточуючі IEnumerable).

(1) IQueryableрозширює IEnumerableінтерфейс. (Ви можете надіслати IQueryableте, що очікує IEnumerableбез помилок.)

(2) Як IQueryableі IEnumerableLINQ спроба відкладеної завантаження , коли Перебір результуючого набору. (Зверніть увагу, що реалізацію можна побачити в методах розширення інтерфейсу для кожного типу.)

Іншими словами, IEnumerablesне є виключно "в пам'яті". IQueryablesне завжди виконуються в базі даних. IEnumerableповинен завантажувати речі в пам'ять (колись їх буде завантажено, можливо, ліниво), оскільки у неї немає абстрактного постачальника даних. IQueryablesпокладатися на абстрактного провайдера (наприклад, LINQ-to-SQL), хоча це також може бути провайдером пам'яті .NET.

Випадок використання зразка

(a) Отримайте список записів IQueryableіз контексту EF. (У пам'яті немає записів.)

(b) Перейдіть на IQueryableвигляд, модель якого є IEnumerable. (Дійсно. IQueryableРозширюється IEnumerable.)

(c) Ітерація над і перегляд записів набору даних, дочірніх об'єктів та властивостей із представлення даних та отримання доступу до них. (Може спричинити винятки!)

Можливі проблеми

(1) IEnumerableСпроби ледачого завантаження та контекст ваших даних минули. Виняток видалено, оскільки постачальник більше не доступний.

(2) Проксі-сервера сутності Entity Framework увімкнено (за замовчуванням), і ви намагаєтеся отримати доступ до пов'язаного (віртуального) об'єкта з контекстом даних, що закінчився. Те саме, що (1).

(3) Кілька наборів активних результатів (MARS). Якщо ви переглядаєте блок IEnumerableв foreach( var record in resultSet )блоці і одночасно намагаєтеся отримати доступ record.childEntity.childProperty, ви можете закінчити MARS через ледачу завантаження як набору даних, так і реляційної сутності. Це спричинить виняток, якщо він не увімкнено у вашому рядку з'єднання.

Рішення

  • Я виявив, що включення MARS у рядку з'єднання працює ненадійно. Я пропоную вам уникати MARS, якщо це не добре зрозуміло і прямо не хочеться.

Виконайте запит та зберігайте результати, використовуючи виклик. resultList = resultSet.ToList() Це, мабуть, є найбільш простим способом забезпечення вашої сутності пам’яттю.

У випадках, коли ви звертаєтесь до пов’язаних організацій, ви все ще можете вимагати контекст даних. Або це, або ви можете відключити проксі-сервера та чітко Includeпов’язані об’єкти зі свого DbSet.


9

Основна відмінність "IEnumerable" від "IQueryable" полягає в тому, де виконується логіка фільтра. Один виконується на стороні клієнта (в пам'яті), а інший виконується в базі даних.

Наприклад, ми можемо розглянути приклад, коли в нашій базі даних є 10 000 записів для користувача, і скажімо лише 900, які є активними користувачами, тож у цьому випадку, якщо ми використовуємо “IEnumerable”, то спочатку він завантажує всі 10 000 записів у пам'ять і потім застосовує на ній фільтр IsActive, який з часом повертає 900 активних користувачів.

З іншого боку, у тому ж випадку, якщо ми використовуємо "IQueryable", він безпосередньо застосує фільтр IsActive до бази даних, який безпосередньо звідти поверне 900 активних користувачів.

Довідкова посилання


яка з них оптимізована і мала вага з точки зору продуктивності?
Sitecore Sam

@Sam "IQueryable" є більш переважним з точки зору оптимізації та невеликої ваги.
Табіш Усман

6

Ми можемо використовувати обидва однаково, і вони відрізняються лише у виконанні.

IQueryable виконує лише проти бази даних ефективно. Це означає, що він створює цілий запит вибору і отримує лише відповідні записи.

Наприклад, ми хочемо взяти топ-10 клієнтів, назва яких починається з "Nimal". У цьому випадку запит вибору буде сформовано як select top 10 * from Customer where name like ‘Nimal%’.

Але якщо ми використали IEnumerable, запит був би таким, select * from Customer where name like ‘Nimal%’і десятка буде відфільтрована на рівні кодування C # (він отримує всі записи клієнтів із бази даних та передає їх у C #).


5

На додаток до перших 2 дійсно хороших відповідей (від driis & by Jacob):

Численні інтерфейси знаходяться в просторі імен System.Collections.

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

Коли запит виконується, IEnumerable завантажує всі дані, і якщо нам потрібно їх фільтрувати, сама фільтрація проводиться на стороні клієнта.

Інтерфейс IQueryable розташований у просторі імен System.Linq.

Об'єкт IQueryable забезпечує віддалений доступ до бази даних і дозволяє переміщатися по даних або в прямому порядку від початку до кінця, або у зворотному порядку. У процесі створення запиту повернутий об'єкт IQueryable, запит оптимізований. Як результат, менше споживається пам'яті під час її виконання, менше пропускної здатності мережі, але в той же час вона може оброблятися трохи повільніше, ніж запит, який повертає об'єкт IEnumerable.

Що вибрати?

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

Якщо НЕ потрібен весь набір повернених даних, а лише деякі відфільтровані дані, тоді краще використовувати IQueryable.


0

На додаток до сказаного, цікаво зазначити, що ви можете отримати винятки, якщо використовуватимете IQueryableзамість IEnumerable:

Наступне працює добре, якщо productsце IEnumerable:

products.Skip(-4);

Однак якщо productsце IQueryableі намагається отримати доступ до записів із таблиці БД, то ви отримаєте цю помилку:

Зсув, зазначений у пункті OFFSET, може бути негативним.

Це тому, що був побудований наступний запит:

SELECT [p].[ProductId]
FROM [Products] AS [p]
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS

і OFFSET не може мати негативне значення.

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