sql >> Database teknologi >  >> RDS >> Database

Hvad er den mest effektive måde at trimme tiden fra datetime?

Det mest almindelige behov for at fjerne tid fra en datetime-værdi er at få alle de rækker, der repræsenterer ordrer (eller besøg eller ulykker), der fandt sted på en given dag. Men ikke alle de teknikker, der bruges til at gøre det, er effektive eller endda sikre.

TL;DR-version

Hvis du ønsker en sikker intervalforespørgsel, der fungerer godt, skal du bruge et åbent interval eller, for enkeltdagsforespørgsler på SQL Server 2008 og nyere, brug CONVERT(DATE) :

ERKLÆR @i dag DATETIME; -- kun på <=2005:SET @i dag =DATEADD(DAY, DATEDIFF(DAY, '20000101', CURRENT_TIMESTAMP), '20000101'); -- eller på 2008 og derover:SET @i dag =KONVERTER(DATE, CURRENT_TIMESTAMP); -- og brug derefter et åbent område i forespørgslen:...WHERE OrderDate>=@today AND OrderDate  

Nogle forbehold:

  • Vær forsigtig med DATEDIFF tilgang, da der er nogle uregelmæssigheder i kardinalitetsestimaterne, der kan forekomme (se dette blogindlæg og Stack Overflow-spørgsmålet, der ansporede det for mere information).
  • Mens den sidste stadig potentielt vil bruge en indekssøgning (i modsætning til alle andre ikke-sargerbare udtryk, jeg nogensinde har stødt på), skal du være forsigtig med at konvertere kolonnen til en dato, før du sammenligner. Også denne tilgang kan give fundamentalt forkerte kardinalitetsestimater. Se dette svar af Martin Smith for flere detaljer.

Under alle omstændigheder, læs videre for at forstå, hvorfor disse er de eneste to tilgange, jeg nogensinde anbefaler.

Ikke alle tilgange er sikre

Som et usikkert eksempel ser jeg denne brugt meget:

HVOR OrderDate MELLEM DATEDIFF(DAY, 0, GETDATE()) OG DATEADD(MILLISECOND, -3, DATEDIFF(DAY, 0, GETDATE()) + 1);

Der er et par problemer med denne tilgang, men den mest bemærkelsesværdige er beregningen af ​​"slutningen" af i dag – hvis den underliggende datatype er SMALLDATETIME , det endeområde kommer til at runde op; hvis det er DATETIME2 , kan du teoretisk gå glip af data sidst på dagen. Hvis du vælger minutter eller nanosekunder eller et hvilket som helst andet hul for at imødekomme den aktuelle datatype, vil din forespørgsel begynde at have mærkelig adfærd, hvis datatypen nogensinde skulle ændre sig senere (og lad os være ærlige, hvis nogen ændrer den kolonnes type til at være mere eller mindre detaljeret, de render ikke rundt og tjekker hver eneste forespørgsel, der får adgang til den). At skulle kode på denne måde afhængigt af typen af ​​dato/klokkeslæt data i den underliggende kolonne er fragmenteret og udsat for fejl. Det er meget bedre at bruge åbne datointervaller til dette:

Jeg taler meget mere om dette i et par gamle blogindlæg:

  • Hvad har BETWEEN og djævelen til fælles?
  • Dårlige vaner at sparke:forkert håndtering af dato-/intervalforespørgsler

Men jeg ville sammenligne ydelsen af ​​nogle af de mere almindelige tilgange, jeg ser derude. Jeg har altid brugt åbne intervaller, og siden SQL Server 2008 har vi været i stand til at bruge CONVERT(DATE) og stadig bruge et indeks på den kolonne, som er ret kraftfuld.

SELECT CONVERT(CHAR(8), CURRENT_TIMESTAMP, 112);SELECT CONVERT(CHAR(10), CURRENT_TIMESTAMP, 120);SELECT CONVERT(DATE, CURRENT_TIMESTAMP);SELECT DATEADD(DAY, DATEDIFF(DAY, '19000101', CURRENT_TIMESTAMP), '19000101'); SELECT CONVERT(DATETIME, DATEDIFF(DAY, '19000101', CURRENT_TIMESTAMP));SELECT CONVERT(DATETIME, CONVERT(INT, CONVERT(FLOAT, CURRENT_TIMESTAMP)));, SELECT CONVERT(DATETIME, CONVERT(INT, CONVERT(FLOAT, CURRENT_TIMESTAMP)));, SELECT CONVERT(DATETIME) CONVERT(FLOAT, CURRENT_TIMESTAMP)));

En simpel præstationstest

For at udføre en meget enkel indledende præstationstest gjorde jeg følgende for hver af ovenstående udsagn, idet jeg indstillede en variabel til outputtet af beregningen 100.000 gange:

SELECT SYSDATETIME();GO DECLARE @d DATETIME =[konverteringsmetode];GO 100000 SELECT SYSDATETIME();GO

Jeg gjorde dette tre gange for hver metode, og de kørte alle i intervallet 34-38 sekunder. Så strengt taget er der meget ubetydelige forskelle i disse metoder, når du udfører operationerne i hukommelsen:

En mere omfattende præstationstest

Jeg ønskede også at sammenligne disse metoder med forskellige datatyper (DATETIME , SMALLDATETIME og DATETIME2 ), mod både et klynget indeks og en heap og med og uden datakomprimering. Så først oprettede jeg en simpel database. Gennem eksperimenter fandt jeg ud af, at den optimale størrelse til at håndtere 120 millioner rækker og al den logaktivitet, der måtte opstå (og for at forhindre automatisk væksthændelser i at forstyrre testen) var en 20 GB datafil og en 3 GB log:

OPRET DATABASE [Datetime_Testing]PÅ PRIMÆR ( NAME =N'Datetime_Testing_Data', FILENAME =N'D:\DATA\Datetime_Testing.mdf', SIZE =20480000KB , MAXSIZE =UNLIMITED) =FILEBGROWTH, =FILEBGROWTH, =FILEBGROWTH, =FILEBGROWTH 'Datetime_Testing_Log', FILENAME =N'E:\LOGS\Datetime_Testing_log.ldf', SIZE =3000000KB, MAXSIZE =UNLIMITED, FILEGROWTH =20480KB);

Dernæst oprettede jeg 12 tabeller:

-- klynget indeks uden komprimering:CREATE TABLE dbo.smalldatetime_nocompression_clustered(dt SMALLDATETIME);CREATE CLUSTERED INDEX x ON dbo.smalldatetime_nocompression_clustered(dt); -- heap uden komprimering:CREATE TABLE dbo.smalldatetime_nocompression_heap(dt SMALLDATETIME); -- clustered index with page compression:CREATE TABLE dbo.smalldatetime_compression_clustered(dt SMALLDATETIME) WITH (DATA_COMPRESSION =PAGE); CREATE CLUSTERED INDEX x ON dbo.smalldatetime_compression_clustered(dt)WITH (DATA_COMPRESSION =PAGE); -- heap med sidekomprimering:OPRET TABEL dbo.smalldatetime_compression_heap(dt SMALLDATETIME)WITH (DATA_COMPRESSION =PAGE);

[Gentag derefter igen for DATETIME og DATETIME2.]

Dernæst indsatte jeg 10.000.000 rækker i hver tabel. Jeg gjorde dette ved at oprette en visning, der ville generere de samme 10.000.000 datoer hver gang:

OPRET VISNING dbo.TenMillionDatesAS SELECT TOP (10000000) d =DATEADD(MINUT, ROW_NUMBER() OVER (ORDER BY s1.[object_id]), '19700101') FRA sys.all_columns AS s1 s1 CROSS.2OIN BESTIL EFTER s1.[objekt_id];

Dette tillod mig at udfylde tabellerne på denne måde:

INSERT /* dt_comp_clus */ dbo.datetime_compression_clustered(dt) SELECT CONVERT(DATETIME, d) FROM dbo.TenMillionDates;CHECKPOINT;INSERT /* dt2_comp_clus */ dbo.datetime2_compression_clustered(dt) FROMTIMEbo, d)DATE CONVERT .TenMillionDates;CHECKPOINT;INSERT /* sdt_comp_clus */ dbo.smalldatetime_compression_clustered(dt) SELECT CONVERT(SMALLDATETIME, d) FROM dbo.TenMillionDates;CHECKPOINT;

[Gentag derefter igen for dyngerne og det ikke-komprimerede klyngeindeks. Jeg sætter et CHECKPOINT mellem hver indsættelse for at sikre genbrug af log (gendannelsesmodellen er enkel).]

INDSÆT Timings &plads brugt

Her er timingen for hver indsættelse (som fanget med Plan Explorer):

Og her er mængden af ​​plads optaget af hvert bord:

VÆLG [tabel] =OBJECT_NAME([object_id]), row_count, page_count =reserved_page_count, reserved_size_MB =reserved_page_count * 8/1024FROM sys.dm_db_partition_stats WHERE OBJECT_NAME([object_iddate]) %LIKE; 

Forespørgselsmønsterydeevne

Dernæst satte jeg mig for at teste to forskellige forespørgselsmønstre for ydeevne:

  • Tælling af rækkerne for en bestemt dag ved hjælp af ovenstående syv tilgange samt det åbne datointerval
  • Konvertering af alle 10.000.000 rækker ved hjælp af ovenstående syv fremgangsmåder, samt returnering af de rå data (da formatering på klientsiden kan være bedre)

[Med undtagelse af FLOAT metoder og DATETIME2 kolonne, da denne konvertering ikke er lovlig.]

For det første spørgsmål ser forespørgslerne således ud (gentages for hver tabeltype):

SELECT /* C_CHAR10 - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE CONVERT(CHAR(10), dt, 120) ='19860301'; SELECT /* C_CHAR8 - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE CONVERT(CHAR(8), dt, 112) ='19860301'; SELECT /* C_FLOOR_FLOAT - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, dt))) ='19860301'; SELECT /* C_DATETIME - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE CONVERT(DATETIME, DATEDIFF(DAY, '19000101', dt)) ='19860301'; SELECT /* C_DATE - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE CONVERT(DATE, dt) ='19860301'; SELECT /* C_INT_FLOAT - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE CONVERT(DATETIME, CONVERT(INT, CONVERT(FLOAT, dt))) ='19860301'; VÆLG /* DATEADD - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE DATEADD(DAY, DATEDIFF(DAY, '19000101', dt), '19000101') ='19860301'; VÆLG /* RANGE - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE dt>='19860301' OG dt <'19860302';

Resultaterne mod et klynget indeks ser således ud (klik for at forstørre):

Her ser vi, at konverteringen til dato og det åbne interval ved hjælp af et indeks er de bedste resultater. Men mod en bunke tager konverteringen til dato faktisk noget tid, hvilket gør det åbne område til det optimale valg (klik for at forstørre):

Og her er det andet sæt forespørgsler (igen, gentaget for hver tabeltype):

SELECT /* C_CHAR10 - dt_comp_clus */ dt =CONVERT(CHAR(10), dt, 120) FROM dbo.datetime_compression_clustered; VÆLG /* C_CHAR8 - dt_comp_clus */ dt =CONVERT(CHAR(8), dt, 112) FROM dbo.datetime_compression_clustered; SELECT /* C_FLOOR_FLOAT - dt_comp_clus */ dt =CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, dt))) FROM dbo.datetime_compression_clustered; VÆLG /* C_DATETIME - dt_comp_clus */ dt =CONVERT(DATETIME, DATEDIFF(DAY, '19000101', dt)) FROM dbo.datetime_compression_clustered; VÆLG /* C_DATE - dt_comp_clus */ dt =CONVERT(DATE, dt) FROM dbo.datetime_compression_clustered; VÆLG /* C_INT_FLOAT - dt_comp_clus */ dt =CONVERT(DATETIME, CONVERT(INT, CONVERT(FLOAT, dt))) FROM dbo.datetime_compression_clustered; VÆLG /* DATEADD - dt_comp_clus */ dt =DATEADD(DAY, DATEDIFF(DAY, '19000101', dt), '19000101') FRA dbo.datetime_compression_clustered; VÆLG /* RAW - dt_comp_clus */ dt FROM dbo.datetime_compression_clustered;

Med fokus på resultaterne for tabeller med et klynget indeks, er det klart, at konverteren til dato var en meget tæt performer til blot at vælge de rå data (klik for at forstørre):

(For dette sæt af forespørgsler viste bunken meget ens resultater - praktisk talt ikke til at skelne.)

Konklusion

Hvis du ville springe til punchline, viser disse resultater, at konverteringer i hukommelsen ikke er vigtige, men hvis du konverterer data på vej ud af en tabel (eller som en del af et søgeprædikat), kan den metode, du vælger, have en dramatisk indvirkning på ydeevnen. Konvertering til en DATE (for en enkelt dag) eller at bruge et åbent datointerval under alle omstændigheder vil give den bedste ydeevne, mens den mest populære metode derude – konvertering til en streng – er helt afgrundsdyb.

Vi ser også, at komprimering kan have en anstændig effekt på lagerplads, med meget lille indvirkning på forespørgselsydeevne. Effekten på indsætningsydelsen ser ud til at være lige så afhængig af, om tabellen har et klynget indeks eller ej, snarere end om komprimering er aktiveret eller ej. Men med et klynget indeks på plads var der et mærkbart bump i den varighed, det tog at indsætte 10 millioner rækker. Noget at huske på og balancere med diskpladsbesparelser.

Det er klart, at der kunne være meget mere test involveret, med mere omfattende og varierede arbejdsbelastninger, som jeg måske vil udforske nærmere i et fremtidigt indlæg.


  1. Lagret procedure, der eksporterer data til csv-filer, eksporterer kun til én fil

  2. Den berygtede java.sql.SQLEundtagelse:Ingen passende driver fundet

  3. Dynamisk pivotering af et bord Oracle

  4. Hvordan nuværende_dato virker i PostgreSQL