Диапазон на основе набора SQL - PullRequest
3 голосов
/ 12 сентября 2008

Как сделать так, чтобы SQL повторял некоторые операции на основе множеств произвольное количество раз без циклов? Как сделать так, чтобы SQL выполнял операции с диапазоном чисел? Я в основном ищу способ сделать основанный на множестве цикл for.

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

Например, если бы у меня была эта таблица, я мог бы сделать выбор, чтобы найти сумму чисел 100-200 следующим образом:

select sum(n) from numbers where n between 100 and 200

Есть идеи? Я вроде ищу что-то, что работает для T-SQL, но с любой платформой все будет в порядке.

[Редактировать] У меня есть собственное решение для этого с использованием SQL CLR, которое прекрасно работает для MS SQL 2005 или 2008. См. Ниже.

Ответы [ 6 ]

5 голосов
/ 12 сентября 2008

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

К сожалению, большие имена в базах данных не имеют встроенных запрашиваемых псевдотаблиц диапазона номеров. Или, в более общем смысле, простые функции генерации данных на чистом SQL. Лично я думаю, что это огромный сбой, потому что если бы они это сделали, можно было бы переместить много кода, который в настоящее время заблокирован в процедурных сценариях (T-SQL, PL / SQL и т. Д.) в чистый SQL, который имеет ряд преимуществ для производительности и сложности кода.

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

Oracle и T-SQL поддерживают предложение WITH, которое можно использовать для этого. Они работают немного по-разному в разных СУБД, и MS называет их «общими табличными выражениями», но они очень похожи по форме. Используя их с рекурсией, вы можете довольно легко сгенерировать последовательность чисел или текстовых значений. Вот как это может выглядеть ...

В Oracle SQL:

WITH
  digits AS  -- Limit recursion by just using it for digits.
    (SELECT
      LEVEL - 1 AS num
    FROM
      DUAL
    WHERE
      LEVEL < 10
    CONNECT BY
      num = (PRIOR num) + 1),
  numrange AS
    (SELECT
      ones.num
        + (tens.num * 10)
        + (hundreds.num * 100)
        AS num
    FROM
      digits ones
      CROSS JOIN
        digits tens
      CROSS JOIN
        digits hundreds
    WHERE
      hundreds.num in (1, 2)) -- Use the WHERE clause to restrict each digit as needed.
SELECT
  -- Some columns and operations
FROM
  numrange
  -- Join to other data if needed

Это, по общему признанию, довольно многословно. Функциональность рекурсии Oracle ограничена. Синтаксис неуклюжий, он не производительный, и он ограничен 500 (я думаю) вложенных уровней. Вот почему я решил использовать рекурсию только для первых 10 цифр, а затем перекрестных (декартовых) объединений, чтобы объединить их в реальные числа.

Я сам не использовал выражения общих таблиц SQL Server, но, поскольку они допускают самоссылку, рекурсия НАМНОГО проще, чем в Oracle. Сравнима ли производительность и каковы пределы вложенности, я не знаю.

В любом случае, рекурсия и предложение WITH являются очень полезными инструментами при создании запросов, которые требуют генерируемых на лету наборов данных. Затем, запрашивая этот набор данных, выполняя операции со значениями, вы можете получить всевозможные типы генерируемых данных. Агрегации, дублирования, комбинации, перестановки и т. Д. Вы даже можете использовать такие сгенерированные данные, чтобы помочь свернуть или развернуть другие данные.

ОБНОВЛЕНИЕ: Я просто хочу добавить, что, как только вы начнете работать с данными таким образом, это откроет вам новый взгляд на SQL. Это не просто язык сценариев. Это довольно надежный управляемый данными декларативный язык . Иногда это неудобно использовать, потому что в течение многих лет он испытывал недостаток усовершенствований, помогающих уменьшить избыточность, необходимую для сложных операций. Но, тем не менее, это очень мощный и довольно интуитивно понятный способ работы с наборами данных как с целью, так и с драйвером ваших алгоритмов.

3 голосов
/ 12 сентября 2008

Я создал табличную функцию SQL CLR, которая отлично подходит для этой цели.

SELECT n FROM dbo.Range(1, 11, 2) -- returns odd integers 1 to 11
SELECT n FROM dbo.RangeF(3.1, 3.5, 0.1) -- returns 3.1, 3.2, 3.3 and 3.4, but not 3.5 because of float inprecision. !fault(this)

Вот код:

using System;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Collections;

[assembly: CLSCompliant(true)]
namespace Range {
    public static partial class UserDefinedFunctions {
        [Microsoft.SqlServer.Server.SqlFunction(DataAccess = DataAccessKind.None, IsDeterministic = true, SystemDataAccess = SystemDataAccessKind.None, IsPrecise = true, FillRowMethodName = "FillRow", TableDefinition = "n bigint")]
        public static IEnumerable Range(SqlInt64 start, SqlInt64 end, SqlInt64 incr) {
            return new Ranger(start.Value, end.Value, incr.Value);
        }

        [Microsoft.SqlServer.Server.SqlFunction(DataAccess = DataAccessKind.None, IsDeterministic = true, SystemDataAccess = SystemDataAccessKind.None, IsPrecise = true, FillRowMethodName = "FillRowF", TableDefinition = "n float")]
        public static IEnumerable RangeF(SqlDouble start, SqlDouble end, SqlDouble incr) {
            return new RangerF(start.Value, end.Value, incr.Value);
        }

        public static void FillRow(object row, out SqlInt64 n) {
            n =  new SqlInt64((long)row);
        }

        public static void FillRowF(object row, out SqlDouble n) {
            n = new SqlDouble((double)row);
        }
    }

    internal class Ranger : IEnumerable {
        Int64 _start, _end, _incr;

        public Ranger(Int64 start, Int64 end, Int64 incr) {
            _start = start; _end = end; _incr = incr;
        }

        public IEnumerator GetEnumerator() {
            return new RangerEnum(_start, _end, _incr);
        }
    }

    internal class RangerF : IEnumerable {
        double _start, _end, _incr;

        public RangerF(double start, double end, double incr) {
            _start = start; _end = end; _incr = incr;
        }

        public IEnumerator GetEnumerator() {
            return new RangerFEnum(_start, _end, _incr);
        }
    }

    internal class RangerEnum : IEnumerator {
        Int64 _cur, _start, _end, _incr;
        bool hasFetched = false;

        public RangerEnum(Int64 start, Int64 end, Int64 incr) {
            _start = _cur = start; _end = end; _incr = incr;
            if ((_start < _end ^ _incr > 0) || _incr == 0)
                throw new ArgumentException("Will never reach end!");
        }

        public long Current {
            get { hasFetched = true; return _cur; }
        }

        object IEnumerator.Current {
            get { hasFetched = true; return _cur; }
        }

        public bool MoveNext() {
            if (hasFetched) _cur += _incr;
            return (_cur > _end ^ _incr > 0);
        }

        public void Reset() {
            _cur = _start; hasFetched = false;
        }
    }

    internal class RangerFEnum : IEnumerator {
        double _cur, _start, _end, _incr;
        bool hasFetched = false;

        public RangerFEnum(double start, double end, double incr) {
            _start = _cur = start; _end = end; _incr = incr;
            if ((_start < _end ^ _incr > 0) || _incr == 0)
                throw new ArgumentException("Will never reach end!");
        }

        public double Current {
            get { hasFetched = true; return _cur; }
        }

        object IEnumerator.Current {
            get { hasFetched = true; return _cur; }
        }

        public bool MoveNext() {
            if (hasFetched) _cur += _incr;
            return (_cur > _end ^ _incr > 0);
        }

        public void Reset() {
            _cur = _start; hasFetched = false;
        }
    }
}

и я развернул его так:

create assembly Range from 'Range.dll' with permission_set=safe -- mod path to point to actual dll location on disk.
go
create function dbo.Range(@start bigint, @end bigint, @incr bigint)
  returns table(n bigint)
  as external name [Range].[Range.UserDefinedFunctions].[Range]
go
create function dbo.RangeF(@start float, @end float, @incr float)
  returns table(n float)
  as external name [Range].[Range.UserDefinedFunctions].[RangeF]
go
1 голос
/ 26 сентября 2011

Вы можете использовать общее табличное выражение, чтобы сделать это в SQL2005 +.

WITH CTE AS
(
    SELECT 100 AS n
    UNION ALL
    SELECT n + 1 AS n FROM CTE WHERE n + 1 <= 200
)
SELECT n FROM CTE
1 голос
/ 12 сентября 2008

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

Я полагаю, что правильный ответ на ваш вопрос в основном "вы не можете". (К сожалению.)

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

Вот хак, который вы никогда не должны использовать:

select sum(numberGenerator.rank)
from
(
select
    rank =  ( select count(*)  
              from reallyLargeTable t1 
              where t1.uniqueValue > t2.uniqueValue ), 
    t2.uniqueValue id1, 
    t2.uniqueValue id2
from reallyLargeTable t2 
) numberGenerator
where rank between 1 and 10

Вы можете упростить это, используя функции Rank () или Row_Number в SQL 2005

0 голосов
/ 12 сентября 2008

Если вы используете SQL Server 2000 или выше, вы можете использовать тип данных таблицы , чтобы избежать создания нормальной или временной таблицы. Затем используйте обычные табличные операции над ним.

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

Я нашел хорошее обсуждение здесь: Временные таблицы против типа данных таблицы

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