Po jakim Typie danych join najszybszy?

0

Cześć Wszystkim,

Dziś mam dość ciekawe pytanie do Was ( tzn dla mnie jest ciekawa odpowiedz :))

w tabeli A mam kolumnę Data datetime
a w tabeli B mam kolumnę Artykul int, dataOd datetime i dataDo datatime.

SELECT
	t1.Data 
	,t2.Artykul
FROM
	A t1
INNER JOIN
	B t2 ON t1.Data between t2.data_od and t2.data_do

i to działa dobrze, szybko średnio, indeksy mam założone.
idzie 7 sekund,

ale kiedy zrobiłem casta z datetime na inta dla wszystkich kolumn i w joinie połączyłem to po intach to idzie 5 sek, wynik ten sam, szybciej, super.

Moje pytanie brzmi czy może jest jeszcze jakiś inny typ danych który mógłby iść jeszcze szybciej?

Pozdrawiam.

0

Czy to zapytanie ma realizować jakieś zadanie w praktyce, czy tylko ma
testować bazę danych? Jeśli ma coś robić praktycznego, to chętnie bym
się dowiedział co.

Typy na których baza powinna działać najwydajniej, to int32, int64.

0

myślę, że jest tak dlatego, że int "waży" 4 bajty a datetime 8, i indeks sobie wolniej z tym radzi - możesz spróbować porównać z jakimś smallint-em lub tinyint-em

1

DATETIME jest bardzo wolnym typem danych, jeśli chodzi o obliczenia i porównania. Zabawa z castem tego nie załatwia za bardzo. Najlepiej by było, gdybyś zamiast DATETIME używał inta - jako czas uniksowy (ilość sekund, która upłynęła od któregoś tam roku). Wtedy w tabeli będą inty, join będzie po intach, porównania będą po intach. To da największą optymalizację. Poza tym używaj warunków. Zawsze pobieraj tylko taki zakres danych, jaki potrzebujesz. Ale używanie warunków też trzeba robić z głową. Bo możesz skonstruować waruenk, który przefiltruje już dane po joinach i to niewiele da. Filtrowanie danych przed joinami da mniejszy zbiór do joina - więc będzie szybciej. To tak na przyszłość. Natomiast faktycznie zastanów się nad przerobieniem datetime na int. Dodatkowo możesz stworzyć sobie widok, który w dodatkowej kolumnie zwróci Ci z tego inta datę czytelną dla człowieka.

0

Ok to pisze wszystko co i jak.

Więc mam zakresy dat aktywności artykułu w hurtowni lecz te daty mogą na siebie zachodzić, po prostu mam nie uporządkowane daty i chce je uporządkować.

Wrzuć sobie dane z excela do tabeli:

CREATE TABLE #Source
	(
		Artykul INT
		,data_od DATE
		,data_do DATE
		,data_odInt INT
		,data_doInt INT
	) 
	
	CREATE CLUSTERED INDEX [IDX_data_od_Source] ON #Source 
	(
		data_od ASC
		
	) WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

następnie z tego zakresu sobie biorę najmniejszą datę i największą ale data 9999-09-09 oznacza nieskonczonosc wiec ją dodaje jedną, wywnioskujesz po kodzie chyba.

CREATE TABLE #date 
(
	[Data] DATE 
	,[DataInt] INT 
)

CREATE CLUSTERED INDEX [IDX_Date_DATE] ON #date 
	(
		[Data] ASC	
	)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
	
-- wyciągnięcie maksymalnej i minimalnej daty aby stworzyc zakres
DECLARE 
	@startDate DATETIME -- początek zakresu 
	,@stopDate DATE -- koniec zakresu
	,@maxDate1 DATE -- max data z daty od
	,@maxDate2 DATE -- max data z daty do nie uwzględniajac daty do nieskonczonosci

SELECT @startDate = MIN(data_od),@maxDate1 = MAX(data_od) FROM #Source 
SELECT @maxDate2 = MAX(data_do) FROM #Source where data_do<>'9999-09-09 00:00:00.000'
SET @stopDate= CASE WHEN @maxDate1<@maxDate2 THEN @maxDate2 ELSE @maxDate1 END

DECLARE @i INT= 1;

-- tworzenei zakresu per dzien
WHILE @startDate <= @stopDate
BEGIN
	INSERT INTO #date([Data],[DataInt])
	VALUES (@startDate,cast(@startDate as INT))
	SET @i += 1
	SET @startDate = DATEADD(day,1,@startDate)
END

--dodanie 1go wpisu z nieskonczonoscia
INSERT INTO #date([Data],[DataInt])
VALUES ('9999-09-09',cast(cast('9999-09-09' as datetime) as int))

No i teraz łączenie zakresów.

SELECT 
	*
	--INTO #test2
FROM(
	SELECT 
		Artykul
		,StartDate
		,CASE WHEN lead(StartDate,1) OVER (PARTITION BY Artykul ORDER BY Artykul,StartDate)='9999-09-09' THEN lead(StartDate,1) OVER (PARTITION BY Artykul ORDER BY Artykul,StartDate) ELSE EndDate END AS EndDate
	from(
		select 
			Artykul
			,CASE WHEN lag is null  or lag<-1   THEN Data END AS StartDate
			,CASE WHEN lead is null or lead>1  THEN Data ELSE LEAD(Data,1) OVER (PARTITION BY Artykul ORDER BY Artykul,Data) END AS EndDate
		from(
			SELECT
				cast(t1.Data as DATE) as Data 
				,t2.Artykul
				,DATEDIFF(DAY,t1.Data,LAG(t1.Data,1) OVER (PARTITION BY t2.Artykul ORDER BY t2.Artykul,t1.Data)) lag
				,DATEDIFF(DAY,t1.Data,LEAD(t1.Data,1) OVER (PARTITION BY t2.Artykul ORDER BY t2.Artykul,t1.Data)) lead
			FROM
				#date t1-- daty z zakresem od do per dzien
			INNER JOIN
				#Source t2 ON t1.[DataInt] between t2.data_odInt and t2.data_doInt-- łądzenie po intach ponieważ jest szybciej
				--#Source t2 ON t1.Data between t2.data_od and t2.data_do-- łądzenie po datach ale jest wolniej
			GROUP BY
				t1.Data 
				,t2.Artykul
			)s1
		WHERE 
			lag<>-1 or lag is null or lead<>1 or lead is null
		)s2
	)s3
WHERE StartDate is not null and StartDate<>'9999-09-09'

Oczywiście mógłbym to zrobić na kursorze, ale cały myk polega na tym, że chciałem się sprawdzić i zobaczyć czy da się zrobić bez kursora :)

Ogólnie udało się jednak nie chodzi super szybko.

0
leonkuczma napisał(a):

Ok to pisze wszystko co i jak.

Więc mam zakresy dat aktywności artykułu w hurtowni lecz te daty mogą na siebie zachodzić, po prostu mam nie uporządkowane daty i chce je uporządkować.

Wrzuć sobie dane z excela do tabeli:

następnie z tego zakresu sobie biorę najmniejszą datę i największą ale data 9999-09-09 oznacza nieskonczonosc wiec ją dodaje jedną, wywnioskujesz po kodzie chyba.

Już po pierwszym zdaniu trudno zrozumieć :) Dlaczego uważasz że to normlanie iż zakresy dat nie nachodzą na siebie? W wielu bazach
zakresy na siebie nachodzą. Dlaczego nieuporządkowane to są po prostu nachodzące na siebie? Opisz problem, do czego to jest potrzebne?

0

ło Panie

1 2009-03-20 0000.000 2009-04-05 0000.000
1 2009-10-26 0000.000 2009-11-21 0000.000
1 2009-11-16 0000.000 2010-04-11 0000.000
1 2009-12-23 0000.000 2010-01-11 0000.000
1 2010-01-11 0000.000 2010-04-14 0000.000
1 2011-03-17 0000.000 2011-07-14 0000.000
1 2011-06-12 0000.000 2011-08-14 0000.000
1 2011-08-15 0000.000 2011-08-21 0000.000

a chcę tak

1 2009-03-20 2009-04-05
1 2009-10-26 2010-04-14
1 2011-03-17 2011-08-21

no to tak mniej wiecej:)

potrzebuje to zrobić aby wyświetlić juz ładnie dane użytkownikowi.

0

wymagań czasowych Ci nie powiem tak samo jak pamięciowych, musi być jak najszybciej, ale bez kursora,
to trochę taki mój wymysł, chciałem coś zrobić w miarę optymalnego, więc nie mam określonych wymagań co do pamięci i szybkości.
Wiem po prostu że nie jest to najszybsze rozwiązanie bo kursor po tych samych danych chodzi w podobnym czasie, lub o jakąś sekundę czy dwie szybciej.

Danych wejściowych jest około 35k rows.
W takiej formie jak w excelu.

Dane wyjściowe muszą być połączone w jedną datę od do jeśli zachodzą na siebie.
Po prostu później generuję plik z tych danych i nie chce w pliku mieć niepotrzebnych wpisów
typu:
2007-11-20 2015-07-09
2015-06-20 2016-11-30

tylko jednen wpis
2007-11-20 2016-11-30

itd.

inaczej chyba nie umiem tego przekazać :/

0

na 99% jeśli po zamianie datetime na int przy użyciu CAST wyszukiwanie przyśpieszyło to baza NIE UŻYWA indeksów. Pierwsze co powinieneś zrobić to zobaczyć jak wygląda plan zapytania.
Teoretycznie indeks na kolumnach data_od, data_do powinien przyśpieszyć jeszcze bardziej

0

z tego co widzę na planie to używa, ale średnio umiem czytać plany, więc mogę się mylic :/

0
leonkuczma napisał(a):

wymagań czasowych Ci nie powiem tak samo jak pamięciowych, musi być jak najszybciej, ale bez kursora,
to trochę taki mój wymysł, chciałem coś zrobić w miarę optymalnego, więc nie mam określonych wymagań co do pamięci i szybkości.
Wiem po prostu że nie jest to najszybsze rozwiązanie bo kursor po tych samych danych chodzi w podobnym czasie, lub o jakąś sekundę czy dwie szybciej.

Danych wejściowych jest około 35k rows.
W takiej formie jak w excelu.

Dane wyjściowe muszą być połączone w jedną datę od do jeśli zachodzą na siebie.
Po prostu później generuję plik z tych danych i nie chce w pliku mieć niepotrzebnych wpisów
typu:
2007-11-20 2015-07-09
2015-06-20 2016-11-30

tylko jednen wpis
2007-11-20 2016-11-30

itd.

inaczej chyba nie umiem tego przekazać :/

Trzeba użyć podstaw matematyki: zbiory, pary, krotki, część wspólna, kwantyfikator ogólny, szczegółowy.
To zdanie naprawdę nie ma żadnego sensu:
"Dane wyjściowe muszą być połączone w jedną datę od do jeśli zachodzą na siebie"

Ja na razie domyślam się, że chodzi o coś takiego:
Mamy zbiór A (uporządkowanych) par (start,end). Pierwszy element pary jest datą początkową, drugi jest datą
końcową. Data początkowa jest zawsze mniejsza lub równa od daty końcowej. Te pary nazywam
zakresami. W zbiorze A trzeba polączyć wszystkie zakresy x i y z częścią wspólną w jeden zakres,
poprzez:
nowy.start = min( x.start , y.start )
nowy.end = max( x.end , y.end )
usuń x i y.

Przyznaję, trzeba chwilę się zastanowić nad tym zadaniem ( o ile dobrze zrozumialem treść ).

Pozdrawiam

P.S.
Jutro wrzucę przykład, ale niestety ani nie znam, ani nie mam mssqla, więc wrzucę w postgresie. Może na
coś Ci się przyda, może zrobimy porównanie wydajnosci obu baz na tym zadaniu.

0

Głowy bym nie dał, ale chyba działa poprawnie. Na pewno masz wskazówkę jak zbudować sqla.

U mnie zamula to niemiłosiernie, ale póki co nic nie optymalizowałem. Bez dłuższego zastanowienia,
uważam że ndeksy wiele nie pomogą, potrzebny jest jakiś sprytny algorytm, albo jakieś rozszerzenie
bazy danych.

--trochę dupereli
rollback;

\pset pager off
\timing off
SET statement_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET client_min_messages = warning;

\c postgres;
set role postgres;

--czyścimy stare śmieci
DROP DATABASE IF EXISTS test_rng;
DROP ROLE IF EXISTS test_rng;


--zakładamy nową bazę i usera
CREATE ROLE test_rng PASSWORD 'test_rng' NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN;
CREATE DATABASE test_rng OWNER test_rng ENCODING 'UTF-8';
\c test_rng;
set role test_rng;


CREATE SEQUENCE gseq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1000;


CREATE TABLE ranges (
    id      bigint DEFAULT nextval('gseq') NOT NULL,
    fstart  int NOT NULL,
    fend    int NOT NULL
);
ALTER TABLE ONLY ranges ADD CONSTRAINT ranges_pkey PRIMARY KEY (id);

--CREATE INDEX idx_fstart ON ranges USING btree (fstart);
--CREATE INDEX idx_fend ON ranges USING btree (fend);
--CREATE INDEX idx_fboth ON ranges USING btree (fstart,fend);


create or replace function mk_data(length integer) returns void as 
$$
declare
  i       integer;
  vfstart integer;
  vfend   integer;
begin
  for i in 1..length loop
    vfstart = floor( random() * 20000 + 1 );
    vfend   = floor( random() *    30 + 1 ) + vfstart;
    execute 'insert into ranges (fstart,fend) values($1,$2)' using vfstart,vfend;
  end loop;
end;
$$ language plpgsql;



create or replace function mk_connect() returns void as 
$$
declare
  cnt1   integer;
  cnt2   integer;
begin
  loop
    execute 'with rows as (update ranges r1 set fstart = (select min(r2.fstart) from ranges as r2 where r2.fstart < r1.fstart and r2.fend >= r1.fstart) where exists (select * from ranges as r2 where r1.id <> r2.id and r2.fstart < r1.fstart and r2.fend >= r1.fstart ) returning 1) select count(*) from rows' into cnt1;
    execute 'with rows as (update ranges r1 set fend = (select max(r2.fend) from ranges as r2 where r2.fend > r1.fend and r2.fstart <= r1.fend) where exists (select * from ranges as r2 where r1.id <> r2.id and r2.fend > r1.fend and r2.fstart <= r1.fend ) returning 1) select count(*) from rows' into cnt2;
    if cnt1 + cnt2 < 1 then exit; end if;
  end loop;
-- execute 'with deleted as (delete from ranges r1 where exists (select * from ranges as r2 where r2.id <> r1.id and r2.fstart < r1.fstart and r2.fend > r1.fend) is true returning *) select count(*) from deleted' into cnt_del;  
end;
$$ language plpgsql;


\timing on
select mk_data(7000);
--select * from ranges;
select mk_connect();


select distinct fstart, fend from ranges order by fstart, fend;

1
Juhas napisał(a):

DATETIME jest bardzo wolnym typem danych, jeśli chodzi o obliczenia i porównania. Zabawa z castem tego nie załatwia za bardzo. Najlepiej by było, gdybyś zamiast DATETIME używał inta - jako czas uniksowy (ilość sekund, która upłynęła od któregoś tam roku). Wtedy w tabeli będą inty, join będzie po intach, porównania będą po intach. To da największą optymalizację.

Dobra, starczy bo dalej to już dla mnie za duża herezja...

@Juhas Ty tak naprawdę czy dla zgrywy? A może w Comarchu pracujesz, oni też mają takie pomysły aby datę pchać do inta (ale to zaszłość, z tego co wiem).
Poza tym oczywiście wiesz, że mssql DateTime przechowuje jako dwa int32 (a więc jego długość to 8 bajtów - dokładnie tyle samo co int64)?
Natomiast sam typ Date to jest 3 bajty, a int32 - 4 bajty.
Mówisz o jakiejś optymalizacji?
Jakiej konkretnie, ciekawość.

I to ma być szybsze? Gdzie, jak?
Wszelki dostęp do takich danych wymaga konwersji, a to niepotrzebny narzut. A więc - po co?

0
wloochacz napisał(a):

A może w Comarchu pracujesz, oni też mają takie pomysły aby datę pchać do inta (ale to zaszłość, z tego co wiem).
Jakiś rok temu miałem okazję robić mały programik przegrywający dokumenty z jednego systemu do drugiego. System źródłowy właśnie trzymał datę dokumentu jako int w dodatku jako liczbę dni od jakiejś tam daty. Nie wiem kto mógł wpaść na taki bzdurny pomysł.

U siebie datę trzymam natomiast jako DATE, filtruję dane, robię zestawienia po tej dacie przy założonym indeksie i nie odczuwam żadnego spowolnienia. Wszystko działa ładnie. Ogólnie to nie wiem skąd takie przekonanie że DATE ma być wolniejsze niż int. Rozumiem porównanie VARCHAR(300) vs INT...

0
wloochacz napisał(a):

Wszelki dostęp do takich danych wymaga konwersji, a to niepotrzebny narzut. A więc - po co?

Konwersji wymaga tylko taki select, który ma być wyświetlony użytkownikowi. Robiłem kilka lat temu dość spory system składający się z 3 aplikacji i dodatkowych pluginów. Optymalizacja była tutaj kluczowym aspektem, bo nawet skomplikowane obliczenia trwające 20 - 30 sekund były niesamowicie długie. Baza była optymalizowana (w ogóle cały system) przez kilka miesięcy. Zmiana DATETIME na int była jedną z metod optymalizacji, która dała radę i udało się wyciągnąć następne kilkanaście - kilkadziesiąt sekund. Jeśli komuś nie zależy, czy obliczenia wykonują się 2, czy 5 minut, to nie ma to żadnego znaczenia. Ale w systemach, gdzie szybkość jest bardzo kluczowa, to zmiana datetime na int jest jedną z tych rzeczy, które przyspieszają działanie.
Oczywiście największa odczuwalna zmiana pojawia się po prawidłowym nadaniu indeksów. Ale jeśli chcemy wyciągnąć, ile się da, to indeksy to za mało.

0

@Juhas dobrze, ale pokaż mi dlaczego zamiana DATE na INT ma przynieść tak szalone różnice w wydajności. Ja też mam system gdzie robię zestawienia po datach. Datę trzymam jako DATE i nic nie muli. Przy założonym indeksie na datę w zasadzie nie mam różnicy czy zapytanie wykonuję filtrując dane po indeksie głównym w postaci INT'a czy po datach.

0

@mr.YaHooo, nie mówię o zmianie DATE na int, ale o zmianie DATETIME na int, a to jest jednak różnica. Nie wiem, dlaczego to daje takie rezultaty. Baza była optymalizowana przez jakieś pół roku (jeśli nie dłużej), codziennie czytałem przynajmniej jakiś jeden artykuł odnośnie optymalizacji. To była walka o naprawdę każdą sekundę. Może to kwestia ilości danych, może powiązań między tabelami, może złożoności problemu. Nie wiem. Wiem, że nauczyło mnie to, że jeśli w grę wchodzi optymalizacja, to to daje rezultaty.

0

@Juhas rzeczywiście nie zrozumieliśmy się :) DATETIME ma większy rozmiar w bajtach niż int, więc siłą rzeczy samo porównanie powinno być wolniejsze niż dwóch intów. Jednak o ile nie mam pojęcia, aż tak nie wnikałem. W moich systemach nie potrzeba informacji o godzinie dokumentu. Liczy się tylko data i to po niej robię zestawienia. Co prawda jest kolumna DATETIME, ale tyczy się wprowadzenia oraz edycji rekodru. Tak aby było wiadomo kto kiedy wprowadził i zmienił rekord. Jednak to sa kolumny czysto informacyjne.

0

@Juhas ale wiesz, że są indeksu funkcyjne i jak masz pole DateTime to nic nie stoi na przeszkodzie aby zrobić indeks na Trunc(pole_datetime) i potem porównywać tak WHERE trunc(pole_datetime) = sama_data

0

A jeśli potrzeba i daty, i czasu? :)

0

Można to zrobić bez użycia kursora i pętli, do zapytania potrzebujemy oznaczenia czy dany rekord ma w dacie od datę początkową, czy datę końcową, czy obie, później pozostajeproste zapytanie o te wartości.

Krokowo

  1. widok cteS pobiera dane z tabeli i pobiera poprzednie daty od/do (p_od,p_do) oraz następne daty od/do (n_do,n_od)
    n_od | n_do | p_od | p_do | Artykul | data_od | data_do | data_odInt | data_doInt
    ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ----------------
    2009-10-26 | 2009-11-21 | NULL | NULL | 1 | 2009-03-20 | 2009-04-05 | 39890 | 39906
    2009-11-16 | 2010-04-11 | 2009-03-20 | 2009-04-05 | 1 | 2009-10-26 | 2009-11-21 | 40110 | 40136
    2009-12-23 | 2010-01-11 | 2009-10-26 | 2009-11-21 | 1 | 2009-11-16 | 2010-04-11 | 40131 | 40277
    2010-01-11 | 2010-04-14 | 2009-11-16 | 2010-04-11 | 1 | 2009-12-23 | 2010-01-11 | 40168 | 40187
    2011-03-17 | 2011-07-14 | 2009-12-23 | 2010-01-11 | 1 | 2010-01-11 | 2010-04-14 | 40187 | 40280
    2011-06-12 | 2011-08-14 | 2010-01-11 | 2010-04-14 | 1 | 2011-03-17 | 2011-07-14 | 40617 | 40736
    2011-08-15 | 2011-08-21 | 2011-03-17 | 2011-07-14 | 1 | 2011-06-12 | 2011-08-14 | 40704 | 40767
    NULL | NULL | 2011-06-12 | 2011-08-14 | 1 | 2011-08-15 | 2011-08-21 | 40768 | 40774

  2. widok o korzysta z cteS i oznacza czy coś jest końcem okresu (kolumna k) jak i początkiem (p)
    Początek jest wtedy jeśli jest to pierwszy i data_od nie wchodzi w zakres poprzedniego okresu,
    koniec jest wtedy jesli jest to ostani rekord lub data_do jest mniejsza od następnej daty_od

n_od n_do p_od p_do Artykul data_od data_do data_odInt data_doInt p k
2009-10-26 2009-11-21 NULL NULL 1 2009-03-20 2009-04-05 39890 39906 1 1
2009-11-16 2010-04-11 2009-03-20 2009-04-05 1 2009-10-26 2009-11-21 40110 40136 1 0
2009-12-23 2010-01-11 2009-10-26 2009-11-21 1 2009-11-16 2010-04-11 40131 40277 0 0
2010-01-11 2010-04-14 2009-11-16 2010-04-11 1 2009-12-23 2010-01-11 40168 40187 0 0
2011-03-17 2011-07-14 2009-12-23 2010-01-11 1 2010-01-11 2010-04-14 40187 40280 0 1
2011-06-12 2011-08-14 2010-01-11 2010-04-14 1 2011-03-17 2011-07-14 40617 40736 1 0
2011-08-15 2011-08-21 2011-03-17 2011-07-14 1 2011-06-12 2011-08-14 40704 40767 0 0
NULL NULL 2011-06-12 2011-08-14 1 2011-08-15 2011-08-21 40768 40774 0 1
  1. wynik to pobranie rekordów z o gdzie jest znacznik początku i końca i podmiana dat.

Skrypt do testów:

--DANE TESTOWE
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[source](
	[Artykul] [int] NULL,
	[data_od] [date] NULL,
	[data_do] [date] NULL,
	[data_odInt] [float] NULL,
	[data_doInt] [float] NULL
) ON [PRIMARY]
GO
INSERT INTO [dbo].[source] ([Artykul], [data_od], [data_do], [data_odInt], [data_doInt])
VALUES (1, '20090320', '20090405', 39890, 39906)
,(1, '20091026', '20091121', 40110, 40136)
,(1, '20091116', '20100411', 40131, 40277)
,(1, '20091223', '20100111', 40168, 40187)
,(1, '20100111', '20100414', 40187, 40280)
,(1, '20110317', '20110714', 40617, 40736)
,(1, '20110612', '20110814', 40704, 40767)
,(1, '20110815', '20110821', 40768, 40774)
,(2, '20110609', '20110714', 40701, 40736)
,(2, '20110612', '20110814', 40704, 40767)
,(2, '20110815', '20110821', 40768, 40774)
,(2, '20130126', '20130207', 41298, 41310)
,(2, '20130802', '20130803', 41486, 41487)
,(2, '20140225', '20140316', 41693, 41712)
,(2, '20140317', '20140330', 41713, 41726)
,(2, '20141201', '20141204', 41972, 41975)
,(2, '20141226', '20141229', 41997, 42000)
,(2, '20141228', '20141230', 41999, 42001)
,(2, '20150109', '20150113', 42011, 42015)
,(2, '20150531', '20150531', 42153, 42153)
,(2, '20151104', '20151106', 42310, 42312)
,(2, '20170101', '99990909', 42734, 2958350)
,(3, '20110609', '20110714', 40701, 40736)
,(3, '20110612', '20110814', 40704, 40767)
,(3, '20110815', '20110821', 40768, 40774)
,(3, '20160620', '20160809', 42539, 42589)
,(3, '20160620', '20160624', 42539, 42543)
,(3, '20161109', '99990909', 42681, 2958350)

Zapytanie:

with cteS as (
select 
	lead(data_od,1)  over (partition by artykul order by data_od) n_od,
	lead(data_do,1) over (partition by artykul order by data_od)  n_do,
	lag(data_od,1)  over (partition by artykul order by data_od) p_od,
	lag(data_do,1) over (partition by artykul order by data_od)  p_do,
	* 
from 
	dbo.source s)
, o as (
select
	*
	,case 
		when p_od is null then 1 
		else
			case when dateadd(d,-1,data_od) between p_od and p_do then 0
			else 1
			end
	end p
	,case 
		when n_do is null then 1 
		else
			case 
				when dateadd(d,1,data_do) < n_od then 1
				else 0
			end
	end k
from 
	ctes)

select distinct 
	artykul
	,case 
		when p=1 then data_od 
		else 
			lag(data_od,1) over (partition by artykul order by data_od)  
	end data_od
	,case 
		when k=1 then data_do 
		else 
			lead(data_do,1) over (partition by artykul order by data_od)  
	end data_do
from 
	o
where 
	p+k>=1

Wynik:

artykul data_od data_do
1 2009-03-20 2009-04-05
1 2009-10-26 2010-04-14
1 2011-03-17 2011-08-21

1 użytkowników online, w tym zalogowanych: 0, gości: 1