Динамическая сортировка в хранимых процедурах SQL - PullRequest
126 голосов
/ 29 сентября 2008

Это проблема, которую я часами изучал в прошлом. Мне кажется, что это то, что должно было бы быть решено современными RDBMS решениями, но пока я не нашел ничего, что действительно отвечало бы тому, что я считаю невероятно обычной потребностью в любом веб-приложении или приложении Windows с база данных.

Я говорю о динамической сортировке. В моем фэнтезийном мире это должно быть так просто, как что-то вроде:

ORDER BY @sortCol1, @sortCol2

Это канонический пример, данный новичками в SQL и разработчиками хранимых процедур по всем форумам в Интернете. "Почему это невозможно?" они спрашивают. Неизменно, в конце концов, кто-то приходит с лекциями о скомпилированной природе хранимых процедур, планах выполнения в целом и всевозможных других причинах, по которым невозможно поместить параметр непосредственно в предложение ORDER BY.


Я знаю, что некоторые из вас уже думают: «Тогда пусть клиент выполняет сортировку». Естественно, это снимает нагрузку с вашей базы данных. В нашем случае, однако, наши серверы баз данных даже не выходят из строя 99% времени, и они даже не являются многоядерными или какими-либо другими бесчисленными улучшениями системной архитектуры, которые происходят каждые 6 месяцев. Только по этой причине сортировка наших баз данных не будет проблемой. Кроме того, базы данных очень хорошо сортируют. Они оптимизированы для этого, и у них были годы, чтобы сделать это правильно, язык для этого невероятно гибкий, интуитивно понятный и простой, и, прежде всего, любой начинающий автор SQL знает, как это сделать, и, что еще важнее, он знает, как его редактировать, вносить изменения, выполнять техническое обслуживание и т. д. Если ваши базы данных далеки от налогообложения и вы просто хотите упростить (и сократить!) время разработки, это кажется очевидным выбором.

Тогда есть проблема с Интернетом. Я поиграл с JavaScript, который будет выполнять сортировку HTML-таблиц на стороне клиента, но они неизбежно не будут достаточно гибкими для моих нужд, и опять же, поскольку мои базы данных не облагаются чрезмерными налогами и могут действительно выполнять сортировку легко, мне трудно оправдать время, которое потребуется, чтобы переписать или свернуть свой собственный сортировщик JavaScript. Как правило, то же самое относится и к сортировке на стороне сервера, хотя она, вероятно, уже намного предпочтительнее JavaScript. Мне не особенно нравятся накладные расходы на DataSets, поэтому подайте в суд на меня.

Но это возвращает нас к мысли, что это невозможно & mdash; точнее, не легко. С предыдущими системами я сделал невероятно хакерский способ динамической сортировки. Это не было ни красиво, ни интуитивно понятно, ни просто, ни гибко, и начинающий писатель SQL был бы потерян через несколько секунд. Уже это выглядит не столько «решением», сколько «осложнением».


Следующие примеры не предназначены для демонстрации какого-либо передового опыта или хорошего стиля кодирования или чего-либо еще, и они не указывают на мои способности как программиста T-SQL. Они такие, какие есть, и я полностью признаю, что они сбивают с толку, плохо себя чувствуют и просто взломают.

Мы передаем целочисленное значение в качестве параметра хранимой процедуре (давайте назовем параметр просто «sort»), и из этого мы определяем кучу других переменных. Например ... скажем, сортировка равна 1 (или по умолчанию):

DECLARE @sortCol1 AS varchar(20)
DECLARE @sortCol2 AS varchar(20)
DECLARE @dir1 AS varchar(20)
DECLARE @dir2 AS varchar(20)
DECLARE @col1 AS varchar(20)
DECLARE @col2 AS varchar(20)

SET @col1 = 'storagedatetime';
SET @col2 = 'vehicleid';

IF @sort = 1                -- Default sort.
BEGIN
    SET @sortCol1 = @col1;
    SET @dir1 = 'asc';
    SET @sortCol2 = @col2;
    SET @dir2 = 'asc';
END
ELSE IF @sort = 2           -- Reversed order default sort.
BEGIN
    SET @sortCol1 = @col1;
    SET @dir1 = 'desc';
    SET @sortCol2 = @col2;
    SET @dir2 = 'desc';
END

Вы уже можете видеть, как, если бы я объявил больше переменных @colX для определения других столбцов, я действительно мог бы проявить творческий подход к сортировке столбцов по значению «сортировки» ... чтобы использовать его, обычно это выглядит так: как следующий невероятно грязный пункт:

ORDER BY
    CASE @dir1
        WHEN 'desc' THEN
            CASE @sortCol1
                WHEN @col1 THEN [storagedatetime]
                WHEN @col2 THEN [vehicleid]
            END
    END DESC,
    CASE @dir1
        WHEN 'asc' THEN
            CASE @sortCol1
                WHEN @col1 THEN [storagedatetime]
                WHEN @col2 THEN [vehicleid]
            END
    END,
    CASE @dir2
        WHEN 'desc' THEN
            CASE @sortCol2
                WHEN @col1 THEN [storagedatetime]
                WHEN @col2 THEN [vehicleid]
            END
    END DESC,
    CASE @dir2
        WHEN 'asc' THEN
            CASE @sortCol2
                WHEN @col1 THEN [storagedatetime]
                WHEN @col2 THEN [vehicleid]
            END
    END

Очевидно, это очень урезанный пример. Реальные вещи, так как у нас обычно есть четыре или пять столбцов для поддержки сортировки, каждый с возможным дополнительным или даже третьим столбцом для сортировки в дополнение к этому (например, по убыванию даты, затем сортируется по имени по возрастанию), и каждый поддерживает направленная сортировка, которая эффективно удваивает количество случаев. Да ... это очень быстро становится волосатым.

Идея состоит в том, что можно "легко" изменить случаи сортировки так, чтобы транспортная система сортировалась до времени хранения ... но псевдо-гибкость, по крайней мере в этом простом примере, действительно заканчивается там. По сути, каждый случай, который не проходит тест (потому что наш метод сортировки не применяется к нему в этот раз), отображает значение NULL. И, таким образом, вы получите предложение, которое работает следующим образом:

ORDER BY NULL DESC, NULL, [storagedatetime] DESC, blah blah

Вы поняли идею. Это работает, потому что SQL Server эффективно игнорирует нулевые значения в порядке по предложениям. Это невероятно сложно поддерживать, как, вероятно, может увидеть любой, кто имеет базовые знания SQL. Если я потерял кого-то из вас, не расстраивайтесь. Нам потребовалось много времени, чтобы заставить это работать, и мы все еще запутались, пытаясь отредактировать это или создать новые как это. К счастью, его не нужно часто менять, иначе он быстро стал бы "не стоящим хлопот".

И все же сделал сработало.


Мой вопрос: Есть ли лучший способ?

У меня все в порядке с решениями, отличными от хранимых процедур, так как я понимаю, что это может быть просто не тот путь. Предпочтительно, я хотел бы знать, может ли кто-нибудь сделать это лучше в рамках хранимой процедуры, но если нет, то как вы все решаете, позволяя пользователю динамически сортировать таблицы данных (в том числе и в двух направлениях) с ASP.NET?

И спасибо, что прочитали (или хотя бы просмотрели) такой длинный вопрос!

PS: рад, что я не показал мой пример хранимой процедуры, которая поддерживает динамическую сортировку, динамическую фильтрацию / текстовый поиск столбцов, разбиение на страницы с помощью ROWNUMBER () OVER, AND try ... уловить откат транзакции при ошибках ... "размером с чудовище" даже не начинает их описывать.


Обновление:

  • Я бы хотел избегать динамического SQL . Синтаксический анализ строки и запуск EXEC на ней лишает вас смысла хранить хранимую процедуру в первую очередь. Иногда я задаюсь вопросом, не стоило ли бы того, чтобы такие вещи были полезны, по крайней мере, в этих специальных случаях динамической сортировки. Тем не менее, я всегда чувствую себя грязно, когда создаю такие динамические строки SQL & mdash; как будто я все еще живу в мире Classic ASP.
  • Во-первых, во-первых, нам нужны хранимые процедуры для безопасности . Я не могу говорить о проблемах безопасности, я только предлагаю решения. В SQL Server 2005 мы можем устанавливать разрешения (для каждого пользователя, если это необходимо) на уровне схемы для отдельных хранимых процедур, а затем напрямую отклонять любые запросы к таблицам. Критиковать за и против такого подхода, возможно, для другого вопроса, но опять же это не мое решение. Я просто обезьяна с кодом. :)

Ответы [ 15 ]

95 голосов
/ 29 сентября 2008

Да, это боль, и то, как ты это делаешь, похоже на то, что я делаю:

order by
case when @SortExpr = 'CustomerName' and @SortDir = 'ASC' 
    then CustomerName end asc, 
case when @SortExpr = 'CustomerName' and @SortDir = 'DESC' 
    then CustomerName end desc,
...

Для меня это все еще намного лучше, чем создание динамического SQL из кода, который превращается в кошмар масштабируемости и обслуживания для администраторов баз данных.

То, что я делаю из кода, это рефакторинг подкачки и сортировки, поэтому у меня, по крайней мере, нет большого количества повторений там с заполнением значений для @SortExpr и @SortDir.

Что касается SQL, сохраняйте одинаковый дизайн и форматирование между различными хранимыми процедурами, чтобы он был хотя бы аккуратным и узнаваемым при внесении изменений.

23 голосов
/ 30 сентября 2008

Этот подход предотвращает двойное дублирование сортируемых столбцов в порядке и является немного более читаемым IMO:

SELECT
  s.*
FROM
  (SELECT
    CASE @SortCol1
      WHEN 'Foo' THEN t.Foo
      WHEN 'Bar' THEN t.Bar
      ELSE null
    END as SortCol1,
    CASE @SortCol2
      WHEN 'Foo' THEN t.Foo
      WHEN 'Bar' THEN t.Bar
      ELSE null
    END as SortCol2,
    t.*
  FROM
    MyTable t) as s
ORDER BY
  CASE WHEN @dir1 = 'ASC'  THEN SortCol1 END ASC,
  CASE WHEN @dir1 = 'DESC' THEN SortCol1 END DESC,
  CASE WHEN @dir2 = 'ASC'  THEN SortCol2 END ASC,
  CASE WHEN @dir2 = 'DESC' THEN SortCol2 END DESC
6 голосов
/ 29 сентября 2008

Мои приложения делают это много, но все они динамически создают SQL. Однако, когда я имею дело с хранимыми процедурами, я делаю это:

  1. Сделайте хранимую процедуру функцией, которая возвращает таблицу ваших значений - без сортировки.
  2. Затем в коде приложения выполните select * from dbo.fn_myData() where ... order by ..., чтобы вы могли динамически указывать там порядок сортировки.

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

6 голосов
/ 29 сентября 2008

Динамический SQL все еще возможен. Вам просто нужно решить, является ли этот вариант более приемлемым, чем у вас есть в настоящее время.

Вот статья, которая показывает, что: http://www.4guysfromrolla.com/webtech/010704-1.shtml.

4 голосов
/ 02 августа 2010

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

SELECT
   name_last,
   name_first,
   CASE @sortCol WHEN 'name_last' THEN [name_last] ELSE 0 END as mySort
FROM
   table
ORDER BY 
    mySort

Это легко превзойти - вы можете объединить поля в столбце mySort, изменить порядок с помощью математических функций или функций даты и т. Д.

Предпочтительно, однако, я использую свои сетки asp.net или другие объекты со встроенной сортировкой, чтобы выполнить сортировку для меня ПОСЛЕ извлечения данных из Sql-Server. Или даже если он не встроен - например, таблицы данных и т. Д. В asp.net.

4 голосов
/ 30 сентября 2008

Есть несколько способов, которыми вы можете взломать это.

Требования:

  1. Только один оператор SELECT в sp
  2. Оставьте любую сортировку (или имейте по умолчанию)

Затем вставьте во временную таблицу:

create table #temp ( your columns )

insert #temp
exec foobar

select * from #temp order by whatever

Метод № 2: настроить связанный сервер обратно на себя, а затем выбрать из него, используя openquery: http://www.sommarskog.se/share_data.html#OPENQUERY

4 голосов
/ 29 сентября 2008

Возможен и третий вариант, поскольку на вашем сервере много резервных циклов - используйте вспомогательную процедуру для выполнения сортировки по временной таблице. Что-то вроде

create procedure uspCallAndSort
(
    @sql varchar(2048),        --exec dbo.uspSomeProcedure arg1,'arg2',etc.
    @sortClause varchar(512)    --comma-delimited field list
)
AS
insert into #tmp EXEC(@sql)
declare @msql varchar(3000)
set @msql = 'select * from #tmp order by ' + @sortClause
EXEC(@msql)
drop table #tmp
GO

Предупреждение: я не проверял это, но он "должен" работать в SQL Server 2005 (который создаст временную таблицу из набора результатов без предварительного указания столбцов).

2 голосов
/ 07 января 2014

Аргумент против выполнения сортировки на стороне клиента - данные большого объема и разбиение на страницы. Как только количество строк выходит за пределы того, что вы можете легко отобразить, вы часто сортируете как часть пропуска / выполнения, которую вы, вероятно, захотите запустить в SQL.

Для Entity Framework вы можете использовать хранимую процедуру для обработки текстового поиска. Если вы сталкиваетесь с той же проблемой сортировки, решение, которое я видел, состоит в том, чтобы использовать сохраненный процесс для поиска, возвращая только ключ идентификатора, установленный для соответствия. Затем повторите запрос (с сортировкой) к базе данных, используя идентификаторы в списке (содержит). EF справляется с этим довольно хорошо, даже если набор идентификаторов довольно большой. Да, это два раунда, но это позволяет вам всегда сохранять сортировку в БД, что может быть важно в некоторых ситуациях, и не позволяет записать загрузку логики в хранимой процедуре.

2 голосов
/ 08 мая 2009

Когда вы просматриваете отсортированные результаты, динамический SQL является хорошим вариантом. Если вы параноидально относитесь к внедрению SQL, вы можете использовать номера столбцов вместо имени столбца. Я сделал это, прежде чем использовать отрицательные значения для убывания. Как то так ...

declare @o int;
set @o = -1;

declare @sql nvarchar(2000);
set @sql = N'select * from table order by ' + 
    cast(abs(@o) as varchar) + case when @o < 0 then ' desc' else ' asc' end + ';'

exec sp_executesql @sql

Тогда вам просто нужно убедиться, что число находится в пределах от 1 до # столбцов. Вы могли бы даже расширить это до списка номеров столбцов и разобрать его в таблицу целых чисел, используя функцию типа this . Тогда вы бы выстроили порядок по предложению так ...

declare @cols varchar(100);
set @cols = '1 -2 3 6';

declare @order_by varchar(200)

select @order_by = isnull(@order_by + ', ', '') + 
        cast(abs(number) as varchar) + 
        case when number < 0 then ' desc' else '' end
from dbo.iter_intlist_to_tbl(@cols) order by listpos

print @order_by

Один недостаток - вы должны помнить порядок каждого столбца на стороне клиента. Особенно, когда вы не отображаете все столбцы или отображаете их в другом порядке. Когда клиент хочет отсортировать, вы сопоставляете имена столбцов с порядком столбцов и генерируете список целых.

2 голосов
/ 29 сентября 2008

Согласен, пользуйтесь на стороне клиента. Но, похоже, это не тот ответ, который вы хотите услышать.

Итак, все так, как есть. Я не знаю, почему вы хотели бы изменить это или даже спросить «Есть ли лучший способ». Действительно, это должно называться «Путь». Кроме того, кажется, что он отлично работает и соответствует потребностям проекта и, вероятно, будет достаточно расширяемым на долгие годы. Поскольку ваши базы данных не облагаются налогом, а сортировка действительно очень проста , она должна оставаться такой в ​​течение многих лет.

Я бы не потел.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...