SQL-набор

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

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

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

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

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

[Edit] У меня есть собственное решение для этого, используя SQL CLR, который отлично подходит для MS SQL 2005 или 2008. См. ниже.

Ответ 1

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

К сожалению, большие имена в базах данных не имеют встроенных псевдо-таблиц с числовыми запросами. Или, в более общем плане, простые функции генерации данных pure-SQL. Лично я считаю, что это огромная ошибка, потому что если бы они это сделали, было бы возможно переместить много кода, который в настоящее время заблокирован в процедурных сценариях (T-SQL, PL/SQL и т.д. ) в pure-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 Common Table Expressions, но поскольку они позволяют саморекламу, рекурсия намного проще, чем в Oracle. Является ли производительность сопоставимой, и каковы пределы вложенности, я не знаю.

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

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

Ответ 2

Я создал функцию 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

Ответ 3

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

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

Ответ 4

Вы можете использовать общее табличное выражение для этого в 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

Ответ 5

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

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

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

Ответ 6

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

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