Найдите 18-месячные пробелы в данных счетов с помощью SQL

Мне нужна помощь с этим SQL-запросом. У меня есть таблица, в которую включены все счета-фактуры с 01.07.2020. В таблице есть CustomerID и InvoiceDt. Для каждого идентификатора клиента мне нужно знать, есть ли 18-месячный перерыв в транзакциях, и если был, мне нужна дата первой транзакции после самого последнего 18-месячного перерыва.

Вот как выглядит таблица, которую я использую в качестве входных данных (в своем коде я называю ее m)

Пользовательский ИД ИнвойсДт 1 '2020-1-2' 1 '2024-1-2' 1 '2024-2-2' 2 '2020-12-1' 2 '2021-12-1' 2 '2022-12-1' 2 '2023-12-1' 2 '2024-2-1' 3 '2024-2-12'

Это результат, которого я хочу. У клиента 1 между первой и второй покупкой прошло 18 месяцев, поэтому мне нужна дата второй покупки. У клиента 2 нет пробелов, поэтому я не хочу, чтобы они включались в результат. Данные относятся к 01.07.2020, поэтому вначале для Клиента 3 имеется перерыв как минимум в 18 месяцев, поэтому мне нужна дата его первого счета. В своем коде я называю эту таблицу startDates.

Пользовательский ИД Измененная дата начала 1 '2024-1-2' 3 '2024-2-12'

Мне удалось сделать это без проблем на Python, но я не могу уложиться в том, чтобы сделать это только внутри группы без использования цикла в SQL.

for cid in m.CustomerID.unique():
  m1 = m[m.CustomerID== cid]
  m1["InvShift"] = m1.InvoiceDt.shift(1)
  m1["Gap"] = ((m1.InvoiceDt - m1.InvShift)/np.timedelta64(1, 'D')/30.42)
  m1["18MonthGap"] = m1.Gap >= 18
  if m1["18MonthGap"].sum() > 0:
    startDates.ModStartDate.loc[cid] = m1[m1["18MonthGap"]].drop_duplicates("18MonthGap", keep = "last").iloc[0].InvoiceDt
  elif m1.iloc[0].InvoiceDt > pd.to_datetime("2022-1-1"):
    startDates.ModStartDate.loc[cid] = m1.iloc[0].InvoiceDt

пожалуйста, посмотрите функции LAG/LEAD

Sergey 19.04.2024 20:04

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

T N 19.04.2024 22:10
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
2
73
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Вы используете оконную функцию lag(), которая, надеюсь, доступна в вашей среде.

select customer_id,invoice_dt
from
    (select customer_id,invoice_dt
     ,lag(invoice_dt) over (partition by customer_id order by invoice_dt) prev_dt
    from invoices
    )
where months_between(invoice_dt,prev_dt) > 18

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

Что касается вашего требования к Клиенту 3. Это, вероятно, проще всего сделать, используя UNION для приведенного выше запроса:

select customer_id,first_invoice
from
    (Select customer_id,min(invoice_dt) first_invoice
    from invoices
    group by customer_id
    )
where first_invoice > date'2022-01-01'

Вопрос был помечен как SQL Server, но вы используете функцию months_between() и синтаксис date'...', который, по всей видимости, соответствует Oracle. В SQL Server можно использовать функцию DATEDIFF(month, ...), а литералы даты не используют префикс date. Кроме того, в качестве альтернативы UNION (или UNION ALL) можно использовать функцию ISNULL() для предоставления начальной даты по умолчанию, когда LAG() не возвращает значения. Следующие действия должны обрабатывать все случаи: where DATEDIFF(month, invoice_dt, ISNULL(prev_dt, '2022-01-01')) > 18.

T N 19.04.2024 21:50

Да, не заметил тег sql-server

Jon Waterhouse 19.04.2024 22:00

Исправление к вышеизложенному: поменяйте местами значения даты в DATEDIFF(), чтобы получить положительную разницу. рабочий пример.

T N 19.04.2024 22:05

Ничего страшного, я думаю, это сработало. Функция lag выдавала отрицательные значения, поэтому мне просто пришлось изменить последнюю строку. WHERE DATEDIFF(MONTH, InvoiceDt, PrevDt) < -18

Highflyer999 20.04.2024 01:56

@Highflyer999 - Или поменяйте значения местами и используйте: WHERE DATEDIFF(MONTH, PrevDt, InvoiceDt) > 18. Обратите внимание, что при этом сравниваются границы и игнорируется день месяца, поэтому DATEDIFF(MONTH, '2024-03-31, 2024-04-01) и DATEDIFF(MONTH, '2024-03-01, 2024-04-30) равны 1. Альтернативой является проверка на InvoiceDt > DATEADD(month, 18, PrevDt), который распознает дни.

T N 20.04.2024 03:24

Вы можете использовать функцию окна LAG(), чтобы получить доступ к предыдущей дате счета. Если для клиента не существует предшествующего значения, ваши стартовые данные можно ввести с помощью функции ISNULL().

Затем вы можете добавить 18 месяцев и сравнить с текущими данными счета, чтобы проверить разницу.

-- All 18-month gaps
SELECT CustomerID, InvoiceDt
FROM (
    SELECT
        *,
        LAG(InvoiceDt) OVER(PARTITION BY CustomerID ORDER BY InvoiceDt) PriorInvoiceDt
    FROM Invoice
) IP
WHERE DATEADD(month, 18, ISNULL(PriorInvoiceDt, '2020-07-01')) <= InvoiceDt
ORDER BY CustomerID, InvoiceDt

Если вам нужна только последняя дата после перерыва, вы можете GROUP BY CustomerID и выбрать MAX(InvoiceDt).

-- Latest 18-month gap, using GROUP BY and MAX() to get latest per customer
SELECT CustomerID, MAX(InvoiceDt) AS InvoiceDt
FROM (
    SELECT
        *,
        LAG(InvoiceDt) OVER(PARTITION BY CustomerID ORDER BY InvoiceDt) PriorInvoiceDt
    FROM Invoice
) IP
WHERE DATEADD(month, 18, ISNULL(PriorInvoiceDt, '2020-07-01')) <= InvoiceDt
GROUP BY CustomerID
ORDER BY CustomerID

Однако если вы хотите включить в результаты другую информацию из строки «Счет-фактура» (не показанной в примере данных) или предыдущую дату, вы можете вместо этого использовать оконную функцию ROW_NUMBER(), чтобы назначить номера строк по убыванию, а затем отфильтровать самые последние.

-- Latest 18-month gap, using ROW_NUMBER() to get latest per customer
-- This allows access to the entire invoice row, not just the date.
SELECT CustomerID, InvoiceDt, PriorInvoiceDt
FROM (
    SELECT 
        *,
        ROW_NUMBER() OVER(PARTITION BY CustomerID ORDER BY InvoiceDt DESC) AS RN
    FROM (
        SELECT
            *,
            LAG(InvoiceDt) OVER(PARTITION BY CustomerID ORDER BY InvoiceDt) PriorInvoiceDt
        FROM Invoice I
    ) IP
    WHERE DATEADD(month, 18, ISNULL(PriorInvoiceDt, '2020-07-01')) <= InvoiceDt
) IPN
WHERE IPN.RN = 1
ORDER BY CustomerID

Наконец, если вы предпочитаете избегать множественных вложенных выборок, вы можете реорганизовать запрос, чтобы использовать CTE (общие табличные выражения).

-- Same as the above, but using CTEs instead of subqueries
;WITH InvoiceAndPriorDate AS (
    SELECT
        *,
        LAG(InvoiceDt) OVER(PARTITION BY CustomerID ORDER BY InvoiceDt) PriorInvoiceDt
    FROM Invoice I
),
InvoiceAndPriorDateNumbered AS (
    SELECT 
        *,
        ROW_NUMBER() OVER(PARTITION BY CustomerID ORDER BY InvoiceDt DESC) AS RN
    FROM InvoiceAndPriorDate IP
    WHERE DATEADD(month, 18, ISNULL(PriorInvoiceDt, '2020-07-01')) <= InvoiceDt
)
SELECT CustomerID, InvoiceDt, PriorInvoiceDt
FROM InvoiceAndPriorDateNumbered IPN
WHERE IPN.RN = 1
ORDER BY CustomerID

Результаты (с некоторыми дополнительными данными испытаний):

Пользовательский ИД ИнвойсДт ПриорИнвойсДт 1 2024-01-02 2020-01-02 3 2024-02-12 нулевой 4 01.01.2024 01.01.2022 5 01.09.2023 2022-02-28 7 15 июля 2022 г. 15 января 2021 г.

Посмотрите эту db<>fiddle для демонстрации.

Другие вопросы по теме